Files
hrm-medpark/CLAUDE.md
T
Danil Suhomlinov 33800292aa chore: add Coolify deployment scaffolding (Dockerfiles, prod compose, git hygiene)
- 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>
2026-06-08 17:42:45 +03:00

24 KiB
Raw Blame History

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/PATCHhr_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-фидбека.

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

Доступ

При изменении схемы

# отредактировать 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.tsxNAV_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