# Ș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-ом.