# 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 - **fix** → `suma`, `valuta`, `grafic_id` (WorkSchedule select), `ore_saptaminal`, `zile_concediu` - **pe_ore** → `tarif_ora`, `zile_concediu` - **in_acord** → `grafic_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 | Luni–Vineri, 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 ```prisma 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: - `paragraph` → `new Paragraph()` - `table` → `new Table()` with `TableRow`/`TableCell` - `bold`, `italic`, `underline` marks → `TextRun` options - `heading` → `new 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