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>
This commit is contained in:
Danil Suhomlinov
2026-06-08 17:42:45 +03:00
commit 33800292aa
186 changed files with 30437 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
# Ș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-ом.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.