- apps/api/Dockerfile: build NestJS, run prisma migrate deploy on start - apps/web/Dockerfile + nginx.conf: build Vite, serve static, proxy /api -> api - docker-compose.coolify.yml: full prod stack (postgres, redis, minio, keycloak, api, web) - .dockerignore / .gitignore / .gitattributes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
24 KiB
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/<resource>
POST /employees/:employeeId/<resource>
PATCH /employees/:employeeId/<resource>/:id
DELETE /employees/:employeeId/<resource>/:id
Сервисы используют общую фабрику subCreate/subUpdate/subRemove с проверкой принадлежности к employeeId. Все write-операции пишут в audit log.
Роли по умолчанию:
GET— все HR ролиPOST/PATCH—hr_admin,hr_specialistDELETE— только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удалены. Бэкенд-APIadmin/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-фидбека.
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/<key>), 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
const medparkTeal = ['#e6f4f4', ..., '#008286', ..., '#003d3f']; // 10 shades
createTheme({
fontFamily: "'Montserrat', Arial, sans-serif",
primaryColor: 'medpark',
colors: { medpark: medparkTeal },
});
Конвенции
- Заголовки страниц:
<Title order={2}>+ 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)
Подсветка дат
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 разработки
Запуск
# 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)
При изменении схемы
# отредактировать apps/api/prisma/schema.prisma
pnpm --filter api prisma:migrate dev --name <migration_name>
# Prisma Client регенерится автоматически
Линт / типчек
pnpm --filter api typecheck
pnpm --filter web typecheck
10. Соглашения по коду
TypeScript
apps/web/src/api/types.ts— единственный источник истины для интерфейсов на фронте; обновляется вручную при изменении Prisma-схемы.- DTO бэкенда → используют
class-validatorдекораторы.
Auth в контроллерах
@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
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. Полезные команды
# Открыть 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