Files
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

163 lines
11 KiB
Markdown
Raw Permalink 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.
# Șabloane DOCX — Control medical (docxtemplater)
Сюда кладутся **`.docx`-шаблоны** (вы их делаете в Word, вставляя плейсхолдеры). Движок генерации
загружает нужный файл, подставляет данные и отдаёт готовый документ.
- **Где лежат:** `apps/api/templates/docx/`
- **Движок:** `docxtemplater` + `pizzip` (уже в зависимостях).
- **Синтаксис плейсхолдеров:** фигурные скобки `{...}`.
## Имена файлов (фиксированные — по ним движок находит шаблон)
| Файл | Источник (регламент) | На что генерируется |
|------|----------------------|---------------------|
| `anexa-3.docx` | Anexa 3 — Fișa de solicitare | 1 на группу (карту риска) |
| `anexa-4.docx` | Anexa 4 — Fișa de evaluare a riscurilor | 1 на карту риска (`tipFisa = STANDARD`) |
| `anexa-4a.docx` | Anexa 4A — muncă la distanță/digital | 1 на карту риска (`tipFisa = DISTANTA_DIGITAL`) |
| `anexa-4b.docx` | Anexa 4B — supliment radiații | 1 на каждого облучаемого сотрудника |
| `anexa-6.docx` | Anexa 6 — Fișa de aptitudine | 1 на каждого сотрудника (вердикт врача) |
## Синтаксис docxtemplater (шпаргалка для Word)
| Нужно | Пишете в шаблоне |
|------|------------------|
| Простое значение | `{numePrenume}` |
| Таблица/список (повтор строки) | в строке таблицы: `{#chimici}``{denumire}` `{vlep}``{/chimici}` |
| Чек-бокс | `{cbEchipa}` — движок подставит `☑` или `☐` |
| Условный блок | `{#esteRadiatii}…текст…{/esteRadiatii}` (показать, если true); `{^esteRadiatii}…{/esteRadiatii}` (если false) |
> ⚠️ Циклы в таблицах: `{#name}` ставится в **первую ячейку строки-шаблона**, `{/name}` — в **последнюю ячейку той же строки**. Эта строка повторится для каждого элемента массива.
> ⚠️ Не оставляйте «висячих» `{` без пары — docxtemplater упадёт. Один плейсхолдер не должен быть разорван форматированием Word (выделите и наберите его одним стилем).
---
# Контракт данных по приложениям
Ниже — **точная форма `data`**, которую движок передаёт в каждый шаблон, и откуда берётся каждое значение.
Вставляйте в `.docx` плейсхолдеры именно с этими именами.
## Общее (есть во всех)
| Плейсхолдер | Значение | Источник |
|---|---|---|
| `{unitatea}` | Medpark International Hospital | константа |
| `{idno}` | 1003600035476 | константа |
| `{adresa}` | str. Nicolae Testemițanu 29, Chișinău | константа |
| `{dataCompletarii}` | дата генерации (DD.MM.YYYY) | now() |
## Контекст генерации batch
Эти поля заполняются в modal-е «Inițiere control medical» и используются в Anexa 3 / 4 / 4A / 4B:
`{telefon}` `{fax}` `{email}` `{solicitant}` `{functia}`.
`{telefonFiliala}` берётся из `WorkplaceRiskCard.telefonFiliala`.
---
## `anexa-3.docx` — Fișa de solicitare
**Уровень:** группа сотрудников (по карте риска).
Шапка: `{unitatea}` `{idno}` `{adresa}` `{telefon}` `{fax}` `{email}` `{filiala}` `{adresaFiliala}` `{telefonFiliala}`
Подвал: `{dataCompletarii}` `{solicitant}` `{functia}`
**Таблица сотрудников** — строку оборачиваете `{#angajati}``{/angajati}`:
| Плейсхолдер (в строке) | Значение | Источник |
|---|---|---|
| `{nr}` | порядковый № | счётчик |
| `{numePrenume}` | Фамилия Имя | Employee.nume/prenume |
| `{anNastere}` | год рождения | Employee.dataNasterii |
| `{idnp}` | IDNP | Employee.idnp |
| `{tipExamen}` | тип контроля (текст) | Batch.tip |
| `{ocupatieCorm}` | occupația CORM | EmployeeMedicalProfile.ocupatieCorm |
| `{caem}` | CAEM (diviziune) | WorkplaceRiskCard.caemDiviziune |
| `{numarLoc}` | № locului de muncă | WorkplaceRiskCard.numarulLoculuiDeMunca |
| `{factorRisc}` | факторы (через запятую) | WorkplaceRiskCard.exposures[].denumire |
---
## `anexa-4.docx` — Fișa de evaluare a riscurilor profesionale
**Уровень:** карта риска (`WorkplaceRiskCard`).
**Шапка:** `{unitatea}` `{adresa}` `{telefon}` `{fax}` `{email}` `{filiala}` `{adresaFiliala}` `{telefonFiliala}` `{caem2}` `{cormSubgrupa}` `{directiaSectia}` `{numarLoc}` `{caemDiviziune}` `{numarLucratori}` `{clasa}`
**Чек-боксы описательного блока** (`☑`/`☐`) — источник `WorkplaceRiskCard.evaluareDetalii`:
`{cbEchipa}` `{cbSchimbNoapte}` `{cbPauze}`
`{cbInfectare}` `{cbElectrocutare}` `{cbTensiuneInalta}` `{cbInecare}` `{cbAsfixiere}` `{cbStrivire}` `{cbTaiere}` `{cbIntepare}` `{cbLovire}` `{cbMuscatura}` `{cbMicrotraumatisme}`
`{cbConduceMasina}` `{categorieConducere}` `{cbUtilajeIntrauzinal}`
`{spatiuL}` `{spatiul}` `{spatiuH}` `{cbSuprafVerticala}` `{cbSuprafOrizontala}` `{cbSuprafOblica}` `{cbMuncaIzolare}` `{cbMuncaInaltime}` `{cbMuncaMiscare}`
`{cbPozitieOrtostatica}` `{cbPozitieAsezat}` `{cbPozitieAplecata}` `{cbPozitieMixta}` `{cbPozitieFortata}`
`{cbColoanaCervicala}` `{cbColoanaToracala}` `{cbColoanaLombara}`
`{cbManipRidicare}` `{cbManipCoborare}` `{cbManipImpingere}` `{cbManipTragere}` `{cbManipPurtare}` `{cbManipDeplasare}` `{greutateMaxima}`
`{cbVizuale}` `{cbAuditive}` `{cbNeuropsihice}`
Microclimat/iluminat: `{cbMicroclimatInterior}` `{cbMicroclimatExterior}` `{cbCaloriceRece}` `{cbCaloriceCalda}` `{cbIluminatSuficient}` `{cbIluminatInsuficient}` `{cbIluminatNatural}` `{cbIluminatArtificial}` `{cbIluminatMixt}`
**Факторные таблицы** — каждая = свой цикл; источник `WorkplaceRiskCard.exposures` (сгруппированы по `tip`):
Секции также имеют чек-боксы наличия данных: `{cbChimici}`/`{cbChimiciNu}`, `{cbPulberi}`/`{cbPulberiNu}`, `{cbBiologici}`/`{cbBiologiciNu}`, `{cbZgomot}`/`{cbZgomotNu}`, `{cbVibratii}`/`{cbVibratiiNu}`, `{cbCampEM}`/`{cbCampEMNu}`, `{cbOptice}`/`{cbOpticeNu}`.
| Цикл | Поля строки |
|---|---|
| `{#chimici}…{/chimici}` | `{denumire}` `{cas}` `{einecs}` `{timp}` `{vep}` `{vlep}` `{caracteristici}` |
| `{#pulberi}…{/pulberi}` | те же |
| `{#biologici}…{/biologici}` | `{denumire}` `{clasificare}` `{note}` |
| `{#zgomot}…{/zgomot}` | `{denumire}` `{timp}` `{vep}` `{vlep}` `{caracteristici}` |
| `{#vibratii}…{/vibratii}` | `{denumire}` `{zona}` `{timp}` `{vep}` `{vlep}` `{caracteristici}` |
| `{#campEM}…{/campEM}` | `{denumire}` `{zona}` `{timp}` `{vep}` `{vlep}` `{caracteristici}` |
| `{#optice}…{/optice}` | `{denumire}` `{zona}` `{timp}` `{vep}` `{vlep}` `{caracteristici}` |
**Радиация ионизирующая** (источник `WorkplaceRiskCard.radiatii*`):
`{cbRadiatii}` `{cbRadiatiiNu}` `{radGrupa}` `{radSurse}` `{radTipExpunere}` `{radAparatura}` `{radMasuri}`
**Подвал:** `{protectieColectiva}` `{protectieIndividuala}` `{echipament}` `{cbVestiar}` `{cbChiuveta}` `{cbWc}` `{cbDus}` `{cbSalaMese}` `{cbRecreere}` `{observatii}`
---
## `anexa-4a.docx` — muncă la distanță / platforme digitale
**Уровень:** карта риска (`tipFisa = DISTANTA_DIGITAL`). **Без факторных таблиц.**
Шапка: `{unitatea}` `{adresa}` `{telefon}` `{fax}` `{email}` `{filiala}` `{adresaFiliala}` `{telefonFiliala}` `{caem2}` `{cormSubgrupa}` `{directiaSectia}` `{numarLoc}` `{caemDiviziune}` `{clasa}`
Тело: `{cbEchipa}` `{oreZi}` `{schimburi}` `{cbSchimbNoapte}` `{cbPauze}` **`{cbLucruMonitor}`** **`{cbPlatformeDigitale}`**
`{cbConduceMasina}` `{categorieConducere}` `{operatiuni}`
`{cbDeplasari}` `{deplasariDescriere}`
`{cbManipRidicare}…{cbManipDeplasare}` `{greutateMaxima}`
`{cbVizuale}` `{cbAuditive}` `{cbNeuropsihice}` `{alteRiscuri}`
Подвал: `{dataCompletarii}`
---
## `anexa-4b.docx` — Supliment radiații ionizante
**Уровень:** один облучаемый сотрудник. Источник: `EmployeeMedicalProfile` + `overexposures`.
Шапка: `{unitatea}` `{adresa}` `{telefon}` `{fax}` `{email}` `{filiala}` `{adresaFiliala}` `{telefonFiliala}` `{caem2}`
Контекст места: `{cormSubgrupa}` `{directiaSectia}` `{numarLoc}` `{caemDiviziune}`
Сотрудник: `{numePrenume}` `{idnp}`
| Плейсхолдер | Значение | Источник |
|---|---|---|
| `{cbRadiatii}` | ☑/☐ | profile.expusRadiatiiIonizante |
| `{dataIntrarii}` | дата | profile.dataIntrarii |
| `{expAnterioaraPerioada}` | период | profile.expunereAnterioaraPerioda |
| `{expAnterioaraAni}` | лет | profile.expunereAnterioaraAni |
| `{dozaExterna}` | mSv | profile.dozaCumulataExternaMsv |
| `{dozaInterna}` | mSv | profile.dozaCumulataInternaMsv |
| `{dozaTotala}` | mSv (ext+int) | вычисляется |
Циклы supraexpuneri (источник `overexposures`, разделены по `fel`):
- `{#supraexpExceptionale}` `{tipExpunere}` `{data}` `{doza}` `{/supraexpExceptionale}`
- `{#supraexpAccidentale}` `{tipExpunere}` `{data}` `{doza}` `{/supraexpAccidentale}`
Подвал: `{dataCompletarii}`
---
## `anexa-6.docx` — Fișa de aptitudine
**Уровень:** один сотрудник. Источник: `MedicalCheckup` (вердикт врача).
Шапка/сотрудник: `{unitatea}` `{adresa}` `{numePrenume}` `{idnp}` `{anNastere}` `{ocupatieCorm}` `{departament}` `{caemDiviziune}` `{numarLoc}` `{factorRisc}` `{tipExamen}` `{dataCompletarii}`
Вердикт (чек-боксы): `{cbApt}` `{cbAptAdaptare}` `{cbAptConditionat}` `{cbInaptTemporar}` `{cbInapt}`
`{recomandari}` `{valabilPanaLa}` `{semnatDe}`.
`{valabilPanaLa}` и `{semnatDe}` заполняет medic_familie при finalizarea controlului.
---
## Статус движка
`DocxTemplateService.render(type, data)` уже подключён. Если файл `apps/api/templates/docx/anexa-*.docx`
существует, bulk-generation использует официальный `.docx`-шаблон через `docxtemplater`; старый `docx`+tiptap-JSON renderer остаётся fallback-ом.