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