- 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>
12 KiB
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.tsandcontracts.service.tsinapps/api/src/modules/employees/sub-resources/ContractDrawer.tsxandContracteTab.tsxin the web app (employee-scoped)- Prisma model
EmploymentContractwith 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 existingContractDrawer.tsxlogic, 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 perioadaradio: determinata / nedeterminata / replasare_temporara- If
determinata→ showdata_terminariidate field
- If
functia_organigrama,cod_functie,functia_clasificator(CORM)tip_cim(de_baza / cumul),sectia_id(department select),regim_muncatip_salarizareradio: 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
- fix →
clauza_aditionala(optional):suma+valutacategorii_serviciitable: 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
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
VariableChipPreviewnode extension — identical toVariableChipbut 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-employeeand 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:
- Load
AnexaTemplate.contentJsonfor the requested type - Walk TipTap JSON AST (recursive)
- Replace
variableChipnodes with resolved string values - Convert nodes →
docxlibrary objects:paragraph→new Paragraph()table→new Table()withTableRow/TableCellbold,italic,underlinemarks →TextRunoptionsheading→new Paragraph({ heading: HeadingLevel.HEADING_X })
- 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; therow.*chips simply mark which cells repeat — the actual loop logic stays intiptap-to-docx.ts)
5. Implementation order
- Seed data — no dependencies, unblocks dropdowns immediately
- Contracts UI — GET /contracts endpoint + ContractsPage + extended drawer
- Anexa editor — Prisma migration → API module → TipTap editor → tiptap-to-docx converter → seed templates