33800292aa
- 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>
250 lines
12 KiB
Markdown
250 lines
12 KiB
Markdown
# 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
|