# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. # HRM Medpark — Полное описание системы > HR-management система для **Medpark International Hospital** (Кишинёв, Молдова), заменяющая Excel/MS Forms процессы для отдела кадров и контроля качества медицинского персонала. > > UI — только на румынском (`ro`). Backend сообщения / валидации — также на румынском. --- ## 1. Архитектура ### Monorepo (pnpm workspace) ``` hrm-medpark/ ├── apps/ │ ├── api/ ← NestJS + Prisma + PostgreSQL │ └── web/ ← React + Vite + Mantine ├── docker-compose.yml ← Postgres, Redis, Keycloak, MinIO └── package.json ← root scripts (api:dev, web:dev, db:migrate) ``` ### Stack | Слой | Технологии | |------------|------------------------------------------------------------------------------------| | Backend | NestJS 10 (Fastify), Prisma 5, PostgreSQL 16, BullMQ (Redis), nestjs-i18n, Passport-JWT (Keycloak JWKS) | | Frontend | React 18, Vite, Mantine v7, React Router v6, TanStack Query v5, react-hook-form + Zod, dayjs | | Auth | Keycloak (внешний IdP), JWT валидация через JWKS | | Storage | MinIO (S3-совместимый) для DOCX-документов | | Notif | n8n webhooks (cron-based reminders) | | Документы | `docx` (npm) — рендер из TipTap-JSON шаблонов (`AnexaTemplate`), редактируются в `/admin/anexa-templates` | ### Глобальный API префикс Все REST-эндпоинты под `/api/v1/...` (через `app.setGlobalPrefix('api/v1')`). Vite proxy на dev: `/api → http://localhost:3001`. --- ## 2. Аутентификация - Токены выдаёт Keycloak. - `KeycloakStrategy` (passport-jwt) валидирует через JWKS. - Роли извлекаются из `realm_access.roles` и `resource_access[clientId].roles`. - Если в токене нет ни одной из 7 HRM-ролей — `UnauthorizedException`. ### Роли | Role | Назначение | |-------------------|------------------------------------------------------------| | `hr_admin` | Полный CRUD по сотрудникам, документам, кампаниям, риск-картам | | `hr_specialist` | CREATE/UPDATE по основным сущностям, без удаления | | `manager` | Редактирование форм оценки своих подчинённых | | `nursing_director`| Утверждение финальной категории при оценке | | `quality_auditor` | Заполняет блоки A-D в формах оценки | | `medic_familie` | Выдаёт вердикт по контролю медиц. (apt / inapt / etc.) | | `employee` | Read-only доступ к собственным данным | `@Roles(...)` декоратор + `RolesGuard` на каждом контроллере. --- ## 3. База данных (Prisma, 20 моделей) Ключевые сущности: ``` Employee (1) ──┬── (N) IdentityDocument ├── (N) FamilyMember ├── (N) Education ├── (N) Qualification ├── (N) Training ├── (N) DisciplinarySanction ├── (N) EmploymentContract ├── (1) Benefit ← upsert (one-to-one) ├── (1) EmployeeMedicalProfile ← upsert ├── (N) MedicalCheckup └── (N) PerformanceEvaluation Department (self-referencing tree, parentId) — adjacency list WorkplaceRiskCard (1) ── (N) EmployeeMedicalProfile ← FK PerformanceCampaign (1) ── (N) PerformanceEvaluation ``` ### Reference (read-only) таблицы: - `DisabilityGrade`, `TaxExemption`, `WorkSchedule` ### Поля типа `@db.Date` — используются для дат без времени (рождение, экспирация). ### `@db.Decimal` — для денежных и физических величин (зарплата, дозы радиации). ### Audit log - `AuditService.logRead(...)` и `logChange({ action: 'CREATE'|'UPDATE'|'DELETE' })`. - `AuditModule` помечен `@Global()` — доступен везде без явного импорта. - `PrismaModule` тоже `@Global()`. --- ## 4. Backend — модули ``` apps/api/src/modules/ ├── auth/ ← KeycloakStrategy + AuthGuard ├── employees/ ← основной CRUD + 7 sub-resources │ ├── employees.{controller,service,module}.ts │ ├── dto/{create,query}-employee.dto.ts │ └── sub-resources/ │ ├── identity-documents/ │ ├── family-members/ │ ├── educations/ │ ├── qualifications/ │ ├── trainings/ │ ├── disciplinary-sanctions/ │ ├── benefit/ │ ├── contracts/ │ └── sub-resources.service-factory.ts ← общая фабрика subCreate/subUpdate/subRemove ├── departments/ ← дерево отделов ├── reference/ ← read-only справочники, без аудита ├── evaluation/ ← Phase 4 — оценка персонала │ ├── evaluation.{controller,service,module}.ts │ ├── dto/{create-campaign,update-form,approve-form}.dto.ts │ └── workers/evaluation-notifications.processor.ts ← BullMQ ├── medical/ ← Phase 5 — медицинский контроль │ ├── medical.{controller,module}.ts │ ├── dto/{risk-card,medical-profile,checkup}.dto.ts │ └── services/ │ ├── risk-cards.service.ts │ ├── medical-profile.service.ts │ ├── checkup.service.ts │ ├── bulk.service.ts │ ├── document-generator.service.ts ← рендер из TipTap-JSON через tiptap-to-docx │ ├── tiptap-to-docx.ts ← конвертер TipTap doc → docx, поддержка repeatRows + variableChip │ └── storage.service.ts ← MinIO client ├── admin/ │ └── anexa-templates/ ← CRUD на AnexaTemplate (only hr_admin), drafts + версии ├── inventory/ ← Vestimentație / Echipament — InventoryItem CRUD + adjust-stock ├── contracts/ ← глобальный список CIM (отдельная страница) ├── notifications/ ← BullMQ daily-expiry cron (08:00 EE/Bucharest) → n8n webhook │ └── workers/daily-expiry.processor.ts └── dashboard/ ← агрегированная статистика ``` ### Sub-resource pattern (employees) Все вложенные ресурсы (документы, семья, образование...) идут под: ``` GET /employees/:employeeId/ POST /employees/:employeeId/ PATCH /employees/:employeeId//:id DELETE /employees/:employeeId//:id ``` Сервисы используют общую фабрику `subCreate/subUpdate/subRemove` с проверкой принадлежности к employeeId. Все write-операции пишут в audit log. Роли по умолчанию: - `GET` — все HR роли - `POST/PATCH` — `hr_admin`, `hr_specialist` - `DELETE` — только `hr_admin` ### Особенности - **DisciplinarySanctions**: `dataExpirarii = dataAplicarii + 6 месяцев` вычисляется на сервере, не принимается от клиента. - **Benefit**: используется `prisma.benefit.upsert()` — у сотрудника всегда максимум одна запись. - **EmployeeMedicalProfile**: `dozaTotalaMsv` — computed поле (`externa + interna`) в response. --- ## 5. Frontend — pages & роутинг ``` apps/web/src/ ├── App.tsx ← AppShell + NAV_ITEMS + Routes ├── main.tsx ← Mantine theme (medpark teal #008286, Montserrat) ├── api/ │ ├── client.ts ← axios baseURL '/api/v1' + bearer token │ └── types.ts ← все TS-интерфейсы синхронизированные с Prisma ├── pages/ │ ├── auth/LoginPage.tsx │ ├── dashboard/DashboardPage.tsx │ ├── employees/ │ │ ├── EmployeesPage.tsx ← список + поиск + фильтр │ │ ├── EmployeeDetailPage.tsx ← header + 10 табов │ │ ├── employeeSchema.ts ← Zod + IDNP checksum │ │ ├── components/ │ │ │ ├── EmployeeHeader.tsx │ │ │ └── EmployeeDrawer.tsx ← создание / редактирование │ │ ├── tabs/ │ │ │ ├── PersonalTab.tsx │ │ │ ├── DocumenteTab.tsx ← подсветка expirare < 30 дней (amber/red) │ │ │ ├── FamilieTab.tsx │ │ │ ├── StudiiTab.tsx │ │ │ ├── CalificariTab.tsx │ │ │ ├── TrainingTab.tsx │ │ │ ├── SanctiuniTab.tsx ← активные (не stinsa) = красная строка │ │ │ ├── BeneficiiTab.tsx │ │ │ ├── ContracteTab.tsx │ │ │ └── MedicalTab.tsx │ │ └── drawers/ │ │ └── *Drawer.tsx ← по одному на каждый sub-resource │ ├── departments/DepartmentsPage.tsx ← дерево с раскрытием │ ├── evaluation/ │ │ ├── EvaluationPage.tsx ← список кампаний │ │ ├── CampaignDetailPage.tsx ← список форм / категорий │ │ ├── EvaluationFormPage.tsx ← заполнение блоков A-D │ │ └── components/{ScoreInput,StatusBadge,CategoryBadge}.tsx │ └── medical/ │ ├── RiskCardsPage.tsx ← карты риска NU-10-MS-2026 │ ├── MedicalControlPage.tsx ← bulk-select сотрудников + Generează documente │ └── MedicalInboxPage.tsx ← inbox для medic_familie └── styles/global.css ← CSS-переменные бренда ``` ### Маршрутизация (фрагмент App.tsx) ``` / → DashboardPage /employees → EmployeesPage /employees/:id → EmployeeDetailPage /departments → DepartmentsPage /evaluation → EvaluationPage /evaluation/:id → CampaignDetailPage /evaluation/form/:id → EvaluationFormPage /risk-cards → RiskCardsPage /medical → MedicalControlPage /medic-inbox → MedicalInboxPage ``` > Маршруты `/admin/templates` и `/admin/templates/:type` удалены. Бэкенд-API `admin/anexa-templates` (CRUD + версии) сохранён. ### State management - **TanStack Query** — fetch + кэш. Reference data: `staleTime: 300_000` (5 мин). - Все табы сотрудника шарят один `useQuery(['employee', id])` — нет per-tab fetch'ей. - Sub-resource записи делают `invalidateQueries(['employee', id])` → весь профиль рефетчится. - Ключ кэша для медицины: `['medical-profile', employeeId]`, `['medical-checkups', employeeId]`, `['risk-cards']`. --- ## 6. Бизнес-логика — критичные алгоритмы ### IDNP (Moldovan ID number) — 13 цифр Реализовано **дважды**: на бэке (custom validator в DTO) и на фронте (Zod) для live-фидбека. ```ts function validateIdnp(idnp: string): boolean { const weights = [7, 3, 1, 7, 3, 1, 7, 3, 1, 7, 3, 1]; const sum = weights.reduce((acc, w, i) => acc + w * +idnp[i], 0); return sum % 10 === +idnp[12]; } ``` ### Performance Evaluation — расчёт категории ``` function calculateCategory(form): total = blockA + blockB + blockC + blockD + (testJci * 0.1) if total >= 90 && expert_score >= threshold → 'superioara' else if total >= 75 → 'cat_I' else if total >= 50 → 'cat_II' else → 'fara_categorie' ``` ### Eligibility для оценки Сотрудник попадает в кампанию если стаж в компании на cutoff > 6 месяцев (по `dataAngajarii` контракта). ### Anexa-документы (NU-10-MS-2026) Генерируются в `document-generator.service.ts` через `docx` библиотеку: | Документ | Когда генерируется | |--------------------------|--------------------------------------------------------| | Anexa 3 (Fișa solicitare) | при инициации контроля (bulk) | | Anexa 4 (Fișa evaluare) | при инициации контроля (bulk) | | Anexa 4B (Suplim. radiații) | если `expusRadiatiiIonizante = true` | | Anexa 6 (Verdict) | после `medic_familie` ставит вердикт (`/checkups/:id/complete`) | DOCX сохраняются в MinIO (`s3://hrm-docs/`), URL пишется в `MedicalCheckup.documenteGenerate` (JSON массив `{name, url, type}`). ### Скачивание / удаление документов - **Скачивание**: фронт извлекает key из `s3://bucket/key`, вызывает `GET /medical/documents/presign?key=...`, открывает presigned URL в новой вкладке. - **Удаление одного**: `DELETE /medical/checkups/:id/documents?name=...` — удаляет из MinIO И из массива. - **Удаление всех**: `DELETE /medical/checkups/:id/documents/all` — параллельно убирает все файлы из MinIO + чистит массив. ### Контроль медицинский — типы ``` la_angajare ← перед трудоустройством periodic ← плановый по карте риска la_reluarea_activitatii ← после длительного отсутствия la_incetarea_expunerii ← при увольнении из вредной среды suplimentar ← по запросу ``` При `complete` для типов `la_angajare/periodic/la_reluarea_activitatii` обновляется `EmployeeMedicalProfile.dataUltimControlMedical` через `updateMany`. ### Notifications (BullMQ) `evaluation-notifications.processor.ts` — `@Process('campaign-reminder')`. Постит в n8n webhook с массивом сотрудников. Cron-планирование на 14 дней до ожидаемого срока. --- ## 7. Brand & UX ### Цвета (Medpark) - **Teal** `#008286` — primary, акценты, ссылки - **Charcoal** `#58595b` — основной текст - **Amber** `#fbb034` — предупреждения (умеренно) - **Red** `#b11116` — деструктив, истёкшие даты, активные санкции ### Шрифт - **Montserrat**, weights 300/500/600/700 (импортируется в `index.html`) ### Mantine theme ```ts const medparkTeal = ['#e6f4f4', ..., '#008286', ..., '#003d3f']; // 10 shades createTheme({ fontFamily: "'Montserrat', Arial, sans-serif", primaryColor: 'medpark', colors: { medpark: medparkTeal }, }); ``` ### Конвенции - Заголовки страниц: `` + teal underline (40×3 px, `borderRadius: 2`) - Таблицы: `borderBottom: '2px solid teal'` на `<thead>`, `borderBottom: '1px solid #e9ecef'` на строках - Кнопки primary: `background: teal`, `fontWeight: 500`, `height: 40` - Drawers: `size="xl"`, секции с teal-left-border divider - Badges по статусам: `medpark` (success), `red` (danger), `gray` (neutral), `orange` (warning) - DateInput: `valueFormat="DD.MM.YYYY"`, на бэк отправляем `YYYY-MM-DD` (через `dayjs`) ### Подсветка дат ```ts const days = dayjs(dataExpirarii).diff(dayjs(), 'day'); if (days < 0) return 'red'; // истёкло if (days < 30) return 'amber'; // истекает скоро ``` --- ## 8. Интернационализация - Только румынский (`ro`). - Backend: nestjs-i18n с файлом `apps/api/i18n/ro/translation.json`. - Frontend: ключи захардкожены в компонентах (без i18next), либо в `apps/web/src/i18n/ro.json`. > Языковой переключатель в UI **отсутствует** — было удалено по требованию. --- ## 9. Workflow разработки ### Запуск ```bash # 1. Инфраструктура docker compose up -d # postgres, redis, keycloak, minio # 2. Миграции (первый раз) pnpm db:migrate # 3. Dev mode pnpm dev # одновременно api (3001) + web (5173) # или раздельно: pnpm api:dev pnpm web:dev ``` ### Доступ - Frontend: http://localhost:5173 - API: http://localhost:3001/api/v1 - Prisma Studio: `pnpm db:studio` - Keycloak admin: http://localhost:8080 - MinIO console: http://localhost:9001 (minioadmin/minioadmin) ### При изменении схемы ```bash # отредактировать apps/api/prisma/schema.prisma pnpm --filter api prisma:migrate dev --name <migration_name> # Prisma Client регенерится автоматически ``` ### Линт / типчек ```bash pnpm --filter api typecheck pnpm --filter web typecheck ``` --- ## 10. Соглашения по коду ### TypeScript - `apps/web/src/api/types.ts` — единственный источник истины для интерфейсов на фронте; обновляется вручную при изменении Prisma-схемы. - DTO бэкенда → используют `class-validator` декораторы. ### Auth в контроллерах ```ts @Controller('...') @UseGuards(AuthGuard('jwt'), RolesGuard) export class ... { @Get(...) @Roles('hr_admin', 'hr_specialist') ...(@Request() req: AuthReq) { return this.svc.method(..., req.user.id, req.user.role); } } ``` ### Audit Каждый write-метод в сервисе ОБЯЗАН вызвать `this.audit.logChange({ userId, userRole, action, entity, entityId })`. Read-методы — `logRead({ userId, userRole, entity, entityId })` для чувствительных данных. ### React Query mutations ```ts const mutation = useMutation({ mutationFn: (data) => apiClient.post(...), onSuccess: () => { void qc.invalidateQueries({ queryKey: ['...'] }); notifications.show({ color: 'medpark', title: '...', message: '...' }); }, onError: (err) => { const msg = (err as any)?.response?.data?.message ?? 'Eroare'; notifications.show({ color: 'red', title: 'Eroare', message: msg }); }, }); ``` --- ## 11. Roadmap & статус | Phase | Статус | Что входит | |------------------------------|--------------|--------------------------------------------------------------| | Phase 1: Employee master data | ✅ Done | CRUD + 8 sub-resources + drawer + детальная страница | | Phase 2: Contracts | ⚠️ Частично | Schema + sub-resource controller, но нет специальной страницы | | Phase 3: Departments | ✅ Done | Tree CRUD | | Phase 4: Performance Evaluation | ✅ Done | Кампании, формы, утверждение, BullMQ напоминания | | Phase 5: Medical Control | ✅ Done | Risk Cards, профили, checkups, bulk + DOCX, MinIO, inbox | | Phase 5b: Anexa template editor | ✅ Done | TipTap v3 редактор шаблонов в `/admin/anexa-templates` (бэкенд + API), repeatRows, verdict.checkbox; **UI редактора удалён из фронтенда** | | Phase 5c: Inventory | ✅ Done | InventoryItem CRUD, atomic stock $transaction в Benefit upsert | | Phase 5d: Notifications | ✅ Done | BullMQ daily-expiry cron → n8n webhook (документы, calificări, contracte, medical, sancțiuni) | | Phase 6: Polish | 🔲 Pending | Дашборды per-role, performance тесты, GDPR DPIA | ### Pending hardening (TODO) - ✅ `POST /auth/dev-login` защищён `NODE_ENV === 'production'` guard (`ALLOW_DEV_LOGIN=true` для тестового продакшена) - ✅ BullMQ daily cron → n8n webhook (docs/categories/contracts/medical expiry) — `notifications` модуль - 🔲 Excel HR import (`POST /employees/import` через `exceljs`) — НЕ требование, отложено - 🔲 Seed data для DisabilityGrade / TaxExemption (department seed готов) - 🔲 Dev-mode Keycloak bypass (сейчас все API возвращают 401 без валидного JWT) --- ## 12. Где искать что | Задача | Файл / директория | |-------------------------------------|------------------------------------------------------------------| | Поменять схему БД | `apps/api/prisma/schema.prisma` | | Добавить эндпоинт | `apps/api/src/modules/<module>/*.controller.ts` | | Изменить роли доступа | `@Roles(...)` в контроллере | | Изменить тему / цвета | `apps/web/src/main.tsx` + `apps/web/src/styles/global.css` | | Поменять навигацию | `apps/web/src/App.tsx` → `NAV_ITEMS` | | Добавить TypeScript-тип | `apps/web/src/api/types.ts` | | Добавить sub-resource сотрудника | `apps/api/src/modules/employees/sub-resources/<name>/` | | Изменить DOCX-документ | `apps/api/src/modules/medical/services/document-generator.service.ts` (рендер) + `tiptap-to-docx.ts` (конвертер) + `/admin/anexa-templates` UI (контент) | | Поправить cron-нотификации | `apps/api/src/modules/notifications/notifications.service.ts` (правила expiry) + `notifications.module.ts` (cron-расписание) | | Управление складом | `apps/api/src/modules/inventory/` + `apps/web/src/pages/inventory/` | | Аудит-логи | таблица `AuditLog`, доступ через `AuditService` | | Reference-справочники | `apps/api/src/modules/reference/reference.controller.ts` | --- ## 13. Полезные команды ```bash # Открыть Prisma Studio (визуальный БД-инспектор) pnpm db:studio # Создать миграцию после изменения schema.prisma pnpm --filter api exec prisma migrate dev --name <name> # Перегенерировать Prisma Client pnpm --filter api exec prisma generate # Type check всего монорепо pnpm -r typecheck # Сбросить БД (только в dev!) pnpm --filter api exec prisma migrate reset ```