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

250 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | 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
```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