Files
hrm-medpark/docs/superpowers/specs/2026-05-08-contracts-seed-anexa-editor-design.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

12 KiB
Raw Blame History

Design: Contracts UI, Seed Data, Anexa Template Editor

Date: 2026-05-08
Scope: Three independent features added on top of the existing HRM Medpark monorepo (NestJS API + React/Vite/Mantine web).


1. Contracts UI

Goal

A standalone «Contracte» section in the main navigation showing all employment contracts across all employees, with expiry tracking for determinata contracts.

What already exists

  • contracts.controller.ts and contracts.service.ts in apps/api/src/modules/employees/sub-resources/
  • ContractDrawer.tsx and ContracteTab.tsx in the web app (employee-scoped)
  • Prisma model EmploymentContract with all fields from ТЗ §3.2

New frontend: /contracts page

List view (ContractsPage.tsx):

  • Table columns: Nr. CIM, Angajat (link to employee), Funcția, Secția, Tip perioadă, Data angajării, Data terminării, Salarizare, Status
  • Status badge: activ (teal) / expirat (red) / expiră în curând (amber — ≤30 days for determinata)
  • Filters (top bar): Departament (select), Tip perioadă (determinata / nedeterminata / replasare_temporara), Status (activ / expirat / în_curând), search by employee name
  • Click any row → opens ContractFormDrawer (reuses existing ContractDrawer.tsx logic, extended)
  • "Adaugă contract" button → same drawer with empty form + employee search field

Contract form drawer (extended):

  • Fields: nr_cim, categorie (principal/secundar), data_semnarii, data_angajarii, data_demisiei
  • perioada radio: determinata / nedeterminata / replasare_temporara
    • If determinata → show data_terminarii date field
  • functia_organigrama, cod_functie, functia_clasificator (CORM)
  • tip_cim (de_baza / cumul), sectia_id (department select), regim_munca
  • tip_salarizare radio: fix / pe_ore / in_acord
    • fixsuma, valuta, grafic_id (WorkSchedule select), ore_saptaminal, zile_concediu
    • pe_oretarif_ora, zile_concediu
    • in_acordgrafic_id, ore_saptaminal, zile_concediu, suma_fix, valuta
  • clauza_aditionala (optional): suma + valuta
  • categorii_servicii table: up to 6 rows, each: categorie_id, tip_remunerare (tarif/procent), suma_net or procent

New API endpoint needed:

  • GET /api/v1/contracts — paginated list of ALL contracts across employees, with filters: departmentId, perioada, status (active/expired/expiring), search
  • Reuses existing per-employee endpoints for create/update/delete

Business rule display:

  • If employee has multiple contracts, show vacation days as MAX across contracts (read-only badge on drawer header)

Navigation

Add { labelKey: 'nav.contracts', path: '/contracts', icon: IconFileDescription } to NAV_ITEMS in App.tsx. Roles: hr_admin, hr_specialist.


2. Seed Data

Goal

Populate reference tables that are currently empty, blocking form dropdowns.

WorkSchedule presets (4 records)

name description hoursPerDay daysOn daysOff
Standard 5/2 LuniVineri, 8h 8 5 2
Tură 7/7 12h, zi/noapte alternant 12 7 7
Ambulatoriu 5/2 12h zi 12 5 2
Gardă 24/72 24h gardă, 72h repaus 24 1 3

DisabilityGrade (4 records)

code name
NONE Fără dizabilitate
GR_I Dizabilitate severă (gr. I)
GR_II Dizabilitate accentuată (gr. II)
GR_III Dizabilitate medie (gr. III)

TaxExemption — IRS Moldova (4 records)

code name annualAmount
SP Scutire personală 27000
SPM Scutire personală majoră 34500
SS Scutire pentru soț/soție 27000
SI Scutire pentru persoane întreținute (copil) 9000

Departments tree (placeholder structure)

Medpark International Hospital (root)
├── Nursing
│   ├── Nursing Anestezie
│   ├── Nursing Terapie
│   ├── Nursing ATI
│   └── Nursing Chirurgie
├── Medicină
│   ├── Chirurgie
│   ├── Terapie
│   └── Anestezie-Reanimare
└── Administrativ
    ├── Resurse Umane
    ├── Calitate
    └── Financiar-Contabilitate

These are placeholder names — HR admin will edit the tree in the Departments UI.

Delivery

All seed data added to apps/api/prisma/seed.ts, idempotent (upsert by code/name). Run via pnpm db:seed.


3. Anexa Template Editor

Goal

An hr_admin-only section where staff can edit the text and structure of Anexa 3, 4, 4B, and 6 DOCX templates directly in the browser (rich-text, Word-like), with a live preview populated by test employee data.

Data model — new Prisma models

model AnexaTemplate {
  id          String               @id @default(uuid())
  type        AnexaType            @unique  // ANEXA_3 | ANEXA_4 | ANEXA_4B | ANEXA_6
  name        String
  contentJson Json                 // TipTap JSON document
  updatedById String
  updatedAt   DateTime             @updatedAt
  versions    AnexaTemplateVersion[]
}

model AnexaTemplateVersion {
  id          String   @id @default(uuid())
  templateId  String
  template    AnexaTemplate @relation(fields: [templateId], references: [id])
  contentJson Json
  savedById   String
  savedAt     DateTime @default(now())
  label       String?  // optional human label e.g. "Versiunea mai 2026"
}

enum AnexaType {
  ANEXA_3
  ANEXA_4
  ANEXA_4B
  ANEXA_6
}

API endpoints (new NestJS module: AnexaTemplatesModule)

Method Path Role Purpose
GET /api/v1/admin/anexa-templates hr_admin List all 4 templates (metadata only)
GET /api/v1/admin/anexa-templates/:type hr_admin Full template with contentJson
PUT /api/v1/admin/anexa-templates/:type hr_admin Save template + auto-create version
GET /api/v1/admin/anexa-templates/:type/versions hr_admin List version history
POST /api/v1/admin/anexa-templates/:type/restore/:versionId hr_admin Restore a version
GET /api/v1/admin/anexa-templates/preview-employee hr_admin Get a test employee for preview

Frontend pages

/admin/templates — template list:

  • 4 cards, one per Anexa, showing name + last updated + updatedBy
  • Click → /admin/templates/:type

/admin/templates/:type — two-panel editor:

┌─────────────────────────────────────────────────────────────┐
│ [Teal topbar]  Anexa 3 — Fișa de solicitare    [↺ Reset] [💾 Salvează] │
├────────────────────────────────┬────────────────────────────┤
│ [Formatting toolbar]           │                             │
│ B I U | ≡ ⊞Tabel | +Angajat +Companie +Data │               │
├────────────────────────────────┤                             │
│                                │  👁 Preview                │
│   LEFT: TipTap editor          │  [Schimbă angajatul test]  │
│   with variable chips          │                             │
│                                │  RIGHT: HTML render of     │
│   teal chip = employee data    │  same doc with real values  │
│   blue chip  = document data   │  substituted in            │
│   orange chip = loop row vars  │                             │
└────────────────────────────────┴────────────────────────────┘

Variable chip system:

  • Custom TipTap Node extension VariableChip: non-editable inline atom, rendered as colored pill
  • Stored in JSON as { type: "variableChip", attrs: { key: "employee.lastName", label: "Numele" } }
  • Color coding: employee.* → teal, document.* → indigo, row.* → orange (loop vars inside table rows)
  • Sidebar panel (collapsible): grouped list of all available variables with "Insert" button

Variable namespace (initial set):

Key Label Context
company.name Denumirea unității document
company.idno IDNO document
company.address Adresa document
document.date Data solicitării document
document.number Numărul documentului document
employee.lastName Numele employee
employee.firstName Prenumele employee
employee.idnp IDNP employee
employee.birthYear Anul nașterii employee
employee.occupation Ocupația (CORM) employee
employee.department Secția employee
row.index Nr. crt. loop row
row.seatNumber Nr. loc de muncă loop row
row.employeeName Numele angajatului loop row
row.riskFactors Factorii de risc loop row
radiation.externalMsv Doza externă (mSv) radiation
radiation.internalMsv Doza internă (mSv) radiation

Preview panel:

  • A second read-only TipTap instance with a VariableChipPreview node extension — identical to VariableChip but renders the resolved value (e.g. "Ionescu") instead of the chip pill
  • The editor document is synced from the left panel's JSON on every change (editor.on('update', syncPreview))
  • Test employee data is fetched once via GET /admin/anexa-templates/preview-employee and stored in React state; variable keys are resolved against this object at render time inside the node extension
  • "Schimbă angajatul test" → opens employee search modal, replaces preview data, re-renders
  • Preview updates in real time as user types (no debounce needed — TipTap JSON sync is cheap)

Version history:

  • "Versiuni" button in topbar → right panel slides to show version list
  • Each version: timestamp, saved by, optional label, "Restaurează" button

DOCX generation integration

The existing document-generator.service.ts currently builds DOCX programmatically. After this feature, it will:

  1. Load AnexaTemplate.contentJson for the requested type
  2. Walk TipTap JSON AST (recursive)
  3. Replace variableChip nodes with resolved string values
  4. Convert nodes → docx library objects:
    • paragraphnew Paragraph()
    • tablenew Table() with TableRow/TableCell
    • bold, italic, underline marks → TextRun options
    • headingnew Paragraph({ heading: HeadingLevel.HEADING_X })
  5. Pass to Packer.toBuffer() → upload to MinIO

The converter lives in apps/api/src/modules/medical/services/tiptap-to-docx.ts.

Seed templates

On first run, seed.ts seeds each AnexaTemplate with a minimal valid TipTap JSON that matches the current programmatic output of document-generator.service.ts. This ensures zero regression on existing DOCX generation.


4. Out of scope for this iteration

  • PDF export (DOCX only, printed and signed by hand per ТЗ §9)
  • Multi-language templates (Romanian only per ТЗ)
  • Contract DOCX generation (not in ТЗ §3.2 requirements)
  • Loop block editor (table rows with row.* variables are added manually as table rows; the row.* chips simply mark which cells repeat — the actual loop logic stays in tiptap-to-docx.ts)

5. Implementation order

  1. Seed data — no dependencies, unblocks dropdowns immediately
  2. Contracts UI — GET /contracts endpoint + ContractsPage + extended drawer
  3. Anexa editor — Prisma migration → API module → TipTap editor → tiptap-to-docx converter → seed templates