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
+226
View File
@@ -0,0 +1,226 @@
/**
* Generează BOLĂVANKE (stub) .docx pentru Anexele 3/4/4A/4B/6 cu TOATE placeholder-ele
* docxtemplater din `templates/docx/README.md`. Formatarea o ajustați apoi în Word.
*
* Rulare: pnpm --filter api exec ts-node scripts/generate-docx-stubs.ts
*/
import { writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import {
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
HeadingLevel, WidthType, BorderStyle,
} from 'docx';
const OUT = join(__dirname, '..', 'templates', 'docx');
mkdirSync(OUT, { recursive: true });
// ── helpers ──
const T = (text: string, bold = false) => new TextRun({ text, bold });
const ph = (name: string) => new TextRun({ text: `{${name}}`, bold: true, color: '0B6E70' });
const P = (...children: TextRun[]) => new Paragraph({ children });
const H = (text: string, level: (typeof HeadingLevel)[keyof typeof HeadingLevel] = HeadingLevel.HEADING_2) =>
new Paragraph({ heading: level, children: [T(text, true)] });
const empty = () => new Paragraph({ children: [] });
// "Label {ph}"
const line = (label: string, name: string) => P(T(label + ' '), ph(name));
// checkbox: "{cbX} Label"
const cb = (name: string, label: string) => [ph(name), T(' ' + label + ' ')];
const cbLine = (...pairs: [string, string][]) =>
P(...pairs.flatMap(([n, l]) => cb(n, l)));
const BORDER = {
top: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
bottom: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
left: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
right: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
};
const cell = (children: Paragraph[]) => new TableCell({ children, borders: BORDER });
const headerRow = (labels: string[]) =>
new TableRow({ children: labels.map((l) => cell([P(T(l, true))])) });
/**
* Tabel repetabil: rândul-șablon repetă pentru fiecare element din `loop`.
* `{#loop}` în prima celulă, `{/loop}` în ultima.
*/
function loopTable(loop: string, headers: string[], rowFields: string[]): Table {
const tplCells = rowFields.map((f, i) => {
const runs: TextRun[] = [];
if (i === 0) runs.push(new TextRun({ text: `{#${loop}}`, bold: true, color: 'B11116' }));
runs.push(ph(f));
if (i === rowFields.length - 1) runs.push(new TextRun({ text: `{/${loop}}`, bold: true, color: 'B11116' }));
return cell([P(...runs)]);
});
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [headerRow(headers), new TableRow({ children: tplCells })],
});
}
function save(name: string, children: (Paragraph | Table)[]) {
const doc = new Document({ sections: [{ children }] });
return Packer.toBuffer(doc).then((buf) => {
writeFileSync(join(OUT, name), buf);
console.log(' ✓', name, `(${buf.length} bytes)`);
});
}
// ════════════════════════ ANEXA 3 ════════════════════════
const anexa3: (Paragraph | Table)[] = [
H('FIȘA de solicitare a examenului medical'),
line('Unitatea economică/instituția:', 'unitatea'),
P(T('IDNO: '), ph('idno'), T(' Adresa: '), ph('adresa')),
P(T('Telefon: '), ph('telefon'), T(' Fax: '), ph('fax'), T(' E-mail: '), ph('email')),
P(T('Filiala: '), ph('filiala'), T(' Adresa filialei: '), ph('adresaFiliala'), T(' Telefon: '), ph('telefonFiliala')),
empty(),
loopTable('angajati',
['Nr.', 'Numele și prenumele', 'Anul nașterii', 'IDNP', 'Tipul examenului', 'Ocupația (CORM)', 'CAEM', 'Nr. loc muncă', 'Factorul de risc'],
['nr', 'numePrenume', 'anNastere', 'idnp', 'tipExamen', 'ocupatieCorm', 'caem', 'numarLoc', 'factorRisc']),
empty(),
line('Data completării:', 'dataCompletarii'),
P(T('Solicitant: '), ph('solicitant'), T(' Funcția: '), ph('functia')),
P(T('Semnătura: ____________________')),
];
// ════════════════════════ ANEXA 4 ════════════════════════
const anexa4Header: (Paragraph | Table)[] = [
line('Unitatea economică/instituția:', 'unitatea'),
P(T('Adresa: '), ph('adresa')),
P(T('Filiala: '), ph('filiala'), T(' Adresa filialei: '), ph('adresaFiliala'), T(' CAEM (2 cifre): '), ph('caem2')),
H('FIȘA de evaluare a riscurilor profesionale'),
line('Ocupația (subgrupa majoră CORM):', 'cormSubgrupa'),
line('Direcția/secția/sectorul:', 'directiaSectia'),
P(T('Numărul locului de muncă: '), ph('numarLoc'), T(' CAEM (diviziune): '), ph('caemDiviziune')),
P(T('Nr. lucrători care pot activa: '), ph('numarLucratori'), T(' Clasa condițiilor de muncă: '), ph('clasa')),
];
const anexa4Descriptiv: (Paragraph | Table)[] = [
H('Descrierea activității', HeadingLevel.HEADING_3),
P(T('Lucru în echipă '), ph('cbEchipa'), T(' Nr. ore/zi: '), ph('oreZi'), T(' Nr. schimburi: '), ph('schimburi')),
cbLine(['cbSchimbNoapte', 'schimb de noapte'], ['cbPauze', 'pauze organizate']),
P(T('Riscuri:')),
cbLine(['cbInfectare', 'infectare'], ['cbElectrocutare', 'electrocutare'], ['cbTensiuneInalta', 'tensiune înaltă'], ['cbInecare', 'înecare'], ['cbAsfixiere', 'asfixiere']),
cbLine(['cbStrivire', 'strivire'], ['cbTaiere', 'tăiere'], ['cbIntepare', 'înțepare'], ['cbLovire', 'lovire'], ['cbMuscatura', 'mușcătură'], ['cbMicrotraumatisme', 'microtraumatisme']),
P(T('Conduce mașina '), ph('cbConduceMasina'), T(' categorie: '), ph('categorieConducere'), T(' '), ph('cbUtilajeIntrauzinal'), T(' utilaje intrauzinal')),
H('Descrierea spațiului de lucru', HeadingLevel.HEADING_3),
P(T('Dimensiuni: L '), ph('spatiuL'), T(' l '), ph('spatiul'), T(' H '), ph('spatiuH'), T(' m')),
cbLine(['cbSuprafVerticala', 'suprafață verticală'], ['cbSuprafOrizontala', 'orizontală'], ['cbSuprafOblica', 'oblică']),
cbLine(['cbMuncaIzolare', 'în izolare'], ['cbMuncaInaltime', 'la înălțime'], ['cbMuncaMiscare', 'în mișcare']),
H('Efort fizic', HeadingLevel.HEADING_3),
P(T('Poziție: ')),
cbLine(['cbPozitieOrtostatica', 'ortostatică'], ['cbPozitieAsezat', 'așezat'], ['cbPozitieAplecata', 'aplecată'], ['cbPozitieMixta', 'mixtă'], ['cbPozitieFortata', 'forțată']),
P(T('Suprasolicitări coloană: ')),
cbLine(['cbColoanaCervicala', 'cervicală'], ['cbColoanaToracala', 'toracală'], ['cbColoanaLombara', 'lombară']),
P(T('Manipulare manuală: ')),
cbLine(['cbManipRidicare', 'ridicare'], ['cbManipCoborare', 'coborâre'], ['cbManipImpingere', 'împingere'], ['cbManipTragere', 'tragere'], ['cbManipPurtare', 'purtare'], ['cbManipDeplasare', 'deplasare']),
P(T('Greutate maximă manipulată: '), ph('greutateMaxima')),
cbLine(['cbVizuale', 'suprasolicitări vizuale'], ['cbAuditive', 'auditive'], ['cbNeuropsihice', 'neuropsihice']),
];
const factorTables: (Paragraph | Table)[] = [
H('AGENȚI CHIMICI', HeadingLevel.HEADING_3),
loopTable('chimici', ['Agentul chimic', 'CAS', 'EINECS', 'Timp', 'VEP', 'VLEP', 'Caracteristici'],
['denumire', 'cas', 'einecs', 'timp', 'vep', 'vlep', 'caracteristici']),
H('PULBERI', HeadingLevel.HEADING_3),
loopTable('pulberi', ['Pulberi', 'CAS', 'EINECS', 'Timp', 'VEP', 'VLEP', 'Caracteristici'],
['denumire', 'cas', 'einecs', 'timp', 'vep', 'vlep', 'caracteristici']),
H('AGENȚI BIOLOGICI', HeadingLevel.HEADING_3),
loopTable('biologici', ['Agent biologic', 'Clasificare', 'Note'], ['denumire', 'clasificare', 'note']),
H('ZGOMOT PROFESIONAL', HeadingLevel.HEADING_3),
loopTable('zgomot', ['Tipul', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'timp', 'vep', 'vlep', 'caracteristici']),
H('VIBRAȚII MECANICE', HeadingLevel.HEADING_3),
loopTable('vibratii', ['Tipul', 'Zona', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'zona', 'timp', 'vep', 'vlep', 'caracteristici']),
H('CÂMP ELECTROMAGNETIC', HeadingLevel.HEADING_3),
loopTable('campEM', ['Tipul', 'Zona', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'zona', 'timp', 'vep', 'vlep', 'caracteristici']),
H('RADIAȚII OPTICE ARTIFICIALE', HeadingLevel.HEADING_3),
loopTable('optice', ['Tipul', 'Zona', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'zona', 'timp', 'vep', 'vlep', 'caracteristici']),
];
const anexa4Footer: (Paragraph | Table)[] = [
H('MICROCLIMAT / RADIAȚII / ILUMINAT', HeadingLevel.HEADING_3),
cbLine(['cbMicroclimatInterior', 'interior'], ['cbMicroclimatExterior', 'exterior'], ['cbCaloriceRece', 'rad. calorice (rece)'], ['cbCaloriceCalda', 'rad. calorice (caldă)']),
P(T('Radiații ionizante '), ph('cbRadiatii'), T(' Grupa: '), ph('radGrupa'), T(' Surse: '), ph('radSurse')),
P(T('Tip expunere: '), ph('radTipExpunere'), T(' Aparatură: '), ph('radAparatura'), T(' Măsuri: '), ph('radMasuri')),
cbLine(['cbIluminatSuficient', 'iluminat suficient'], ['cbIluminatInsuficient', 'insuficient'], ['cbIluminatNatural', 'natural'], ['cbIluminatArtificial', 'artificial'], ['cbIluminatMixt', 'mixt']),
H('Protecție și dotări', HeadingLevel.HEADING_3),
line('Mijloace de protecție colectivă:', 'protectieColectiva'),
line('Mijloace de protecție individuală:', 'protectieIndividuala'),
line('Echipament de lucru:', 'echipament'),
P(T('Anexe igienico-sanitare: ')),
cbLine(['cbVestiar', 'vestiar'], ['cbChiuveta', 'chiuvetă'], ['cbWc', 'WC'], ['cbDus', 'duș'], ['cbSalaMese', 'sală de mese'], ['cbRecreere', 'recreere']),
line('Observații:', 'observatii'),
line('Data completării:', 'dataCompletarii'),
P(T('Angajatorul (nume, prenume, semnătura): ____________________')),
P(new TextRun({ text: 'Instrucțiuni: răspuns afirmativ [☑]; răspuns negativ [☐].', italics: true })),
];
// ════════════════════════ ANEXA 4A ════════════════════════
const anexa4a: (Paragraph | Table)[] = [
line('Unitatea economică/instituția:', 'unitatea'),
P(T('Adresa: '), ph('adresa'), T(' Filiala: '), ph('filiala'), T(' CAEM (2 cifre): '), ph('caem2')),
H('FIȘA de evaluare — muncă la distanță / platforme digitale'),
line('Ocupația (subgrupa majoră CORM):', 'cormSubgrupa'),
line('Direcția/secția/sectorul:', 'directiaSectia'),
P(T('Numărul locului de muncă: '), ph('numarLoc'), T(' CAEM (diviziune): '), ph('caemDiviziune'), T(' Clasa: '), ph('clasa')),
H('Descrierea activității', HeadingLevel.HEADING_3),
P(T('Lucru în echipă '), ph('cbEchipa'), T(' Nr. ore/zi: '), ph('oreZi'), T(' Nr. schimburi: '), ph('schimburi')),
cbLine(['cbSchimbNoapte', 'schimb de noapte'], ['cbPauze', 'pauze organizate'], ['cbLucruMonitor', 'lucru la monitor'], ['cbPlatformeDigitale', 'platforme digitale']),
P(T('Conduce mașina '), ph('cbConduceMasina'), T(' categorie: '), ph('categorieConducere')),
line('Operațiuni executate:', 'operatiuni'),
P(T('Deplasări pe teren '), ph('cbDeplasari'), T(' '), ph('deplasariDescriere')),
H('Efort fizic', HeadingLevel.HEADING_3),
P(T('Manipulare manuală: ')),
cbLine(['cbManipRidicare', 'ridicare'], ['cbManipCoborare', 'coborâre'], ['cbManipImpingere', 'împingere'], ['cbManipTragere', 'tragere'], ['cbManipPurtare', 'purtare'], ['cbManipDeplasare', 'deplasare']),
P(T('Greutate maximă: '), ph('greutateMaxima')),
cbLine(['cbVizuale', 'vizuale'], ['cbAuditive', 'auditive'], ['cbNeuropsihice', 'neuropsihice']),
line('Alte riscuri:', 'alteRiscuri'),
line('Data completării:', 'dataCompletarii'),
P(T('Angajatorul (nume, prenume, semnătura): ____________________')),
];
// ════════════════════════ ANEXA 4B ════════════════════════
const anexa4b: (Paragraph | Table)[] = [
line('Unitatea economică/instituția:', 'unitatea'),
P(T('Adresa: '), ph('adresa'), T(' Telefon: '), ph('telefon'), T(' Fax: '), ph('fax'), T(' E-mail: '), ph('email')),
P(T('Filiala: '), ph('filiala'), T(' Adresa filialei: '), ph('adresaFiliala'), T(' CAEM (2 cifre): '), ph('caem2')),
H('SUPLIMENT la Fișa de evaluare a riscurilor profesionale'),
line('Ocupația (subgrupa majoră CORM):', 'cormSubgrupa'),
line('Direcția/secția/sectorul:', 'directiaSectia'),
P(T('Numărul locului de muncă: '), ph('numarLoc'), T(' CAEM (diviziune): '), ph('caemDiviziune')),
P(T('Numele, prenumele lucrătorului: '), ph('numePrenume'), T(' IDNP: '), ph('idnp')),
P(T('RADIAȚII IONIZANTE: '), ph('cbRadiatii')),
line('Data intrării în mediul cu expunere:', 'dataIntrarii'),
P(T('Expunere anterioară — perioada: '), ph('expAnterioaraPerioada'), T(' ani: '), ph('expAnterioaraAni')),
P(T('Doză externă (mSv): '), ph('dozaExterna'), T(' Doză internă (mSv): '), ph('dozaInterna'), T(' Doză totală (mSv): '), ph('dozaTotala')),
H('Supraexpuneri excepționale', HeadingLevel.HEADING_3),
loopTable('supraexpExceptionale', ['Tip de expunere', 'Data', 'Doză (mSv)'], ['tipExpunere', 'data', 'doza']),
H('Supraexpuneri accidentale', HeadingLevel.HEADING_3),
loopTable('supraexpAccidentale', ['Tip de expunere', 'Data', 'Doză (mSv)'], ['tipExpunere', 'data', 'doza']),
line('Data completării:', 'dataCompletarii'),
P(T('Angajatorul (nume, prenume, semnătura): ____________________')),
];
// ════════════════════════ ANEXA 6 ════════════════════════
const anexa6: (Paragraph | Table)[] = [
H('FIȘĂ DE APTITUDINE ÎN MUNCĂ'),
line('Unitatea:', 'unitatea'),
P(T('Angajat: '), ph('numePrenume'), T(' IDNP: '), ph('idnp'), T(' Anul nașterii: '), ph('anNastere')),
P(T('Ocupația: '), ph('ocupatieCorm'), T(' Departament: '), ph('departament')),
P(T('Tipul examenului: '), ph('tipExamen'), T(' Data: '), ph('dataCompletarii')),
H('Verdict', HeadingLevel.HEADING_3),
P(ph('cbApt'), T(' Apt')),
P(ph('cbAptAdaptare'), T(' Apt în perioada de adaptare')),
P(ph('cbAptConditionat'), T(' Apt condiționat')),
P(ph('cbInaptTemporar'), T(' Inapt temporar')),
P(ph('cbInapt'), T(' Inapt')),
line('Recomandări:', 'recomandari'),
line('Valabil până la:', 'valabilPanaLa'),
P(T('Semnătura medicului: '), ph('semnatDe')),
];
async function main() {
console.log('📄 Generez bolăvanke .docx în', OUT);
await save('anexa-3.docx', anexa3);
await save('anexa-4.docx', [...anexa4Header, ...anexa4Descriptiv, ...factorTables, ...anexa4Footer]);
await save('anexa-4a.docx', anexa4a);
await save('anexa-4b.docx', anexa4b);
await save('anexa-6.docx', anexa6);
console.log('✅ Gata. Editați formatarea în Word — placeholder-ele rămân ca {nume}.');
}
main().catch((e) => { console.error(e); process.exit(1); });