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
+738
View File
@@ -0,0 +1,738 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding reference data...');
// ── Disability Grades (grade de dizabilitate MD) ─────────────────
await prisma.disabilityGrade.createMany({
data: [
{ code: 'GRAD_I', name: 'Grad I (sever)' },
{ code: 'GRAD_II', name: 'Grad II (accentuat)' },
{ code: 'GRAD_III', name: 'Grad III (mediu)' },
],
skipDuplicates: true,
});
console.log(' ✓ DisabilityGrade (3)');
// ── Tax Exemptions (scutiri Codul Fiscal RM) ─────────────────────
await prisma.taxExemption.createMany({
data: [
{ code: 'PE', description: 'Scutire personală (art. 33 CF)' },
{ code: 'PI', description: 'Scutire personală majorată (art. 33 alin. 2 CF)' },
{ code: 'SO', description: 'Scutire pentru soț/soție (art. 34 CF)' },
{ code: 'MP1', description: 'Scutire pentru 1 copil minor (art. 35 CF)' },
{ code: 'MP2', description: 'Scutire pentru 2 copii minori (art. 35 CF)' },
{ code: 'MP3', description: 'Scutire pentru 3+ copii minori (art. 35 CF)' },
{ code: 'INVALID', description: 'Scutire persoană cu dizabilitate (art. 33 alin. 2 lit. a CF)' },
],
skipDuplicates: true,
});
console.log(' ✓ TaxExemption (7)');
// ── Work Schedules ───────────────────────────────────────────────
await prisma.workSchedule.createMany({
data: [
{ name: '5/2 — 8h/zi', daysWork: 5, daysRest: 2, hoursPerDay: 8 },
{ name: '5/2 — 7h/zi', daysWork: 5, daysRest: 2, hoursPerDay: 7 },
{ name: 'Gărzi 24h (1/3)', daysWork: 1, daysRest: 3, hoursPerDay: 24 },
{ name: 'Gărzi 12h zi (1/1)', daysWork: 1, daysRest: 1, hoursPerDay: 12 },
{ name: 'Gărzi 12h noapte (1/1)',daysWork: 1, daysRest: 1, hoursPerDay: 12 },
{ name: '7/7 — 12h/zi', daysWork: 7, daysRest: 7, hoursPerDay: 12 },
{ name: 'Rotație 2/2 — 12h', daysWork: 2, daysRest: 2, hoursPerDay: 12 },
],
skipDuplicates: true,
});
console.log(' ✓ WorkSchedule (7)');
// ── Departments — Medpark International Hospital ─────────────────
// Level 0: hospital root
const root = await prisma.department.upsert({
where: { code: 'MEDPARK' },
update: {},
create: { name: 'Medpark International Hospital', code: 'MEDPARK' },
});
// Helper to upsert a department
const dept = async (name: string, code: string, parentId?: string) =>
prisma.department.upsert({
where: { code },
update: {},
create: { name, code, parentId: parentId ?? null },
});
// Administration
const admin = await dept('Administrare', 'ADMIN', root.id);
await dept('Resurse Umane', 'HR', admin.id);
await dept('Financiar-Contabil', 'FIN', admin.id);
await dept('Juridic', 'JUR', admin.id);
await dept('IT', 'IT', admin.id);
await dept('Achiziții', 'ACHIZ', admin.id);
// Medical divisions
const med = await dept('Bloc Medical', 'MED', root.id);
const terapie = await dept('Terapie și Medicină Internă', 'TERAP', med.id);
await dept('Cardiologie', 'CARDIO', terapie.id);
await dept('Gastroenterologie', 'GASTRO', terapie.id);
await dept('Endocrinologie', 'ENDO', terapie.id);
await dept('Neurologie', 'NEURO', terapie.id);
await dept('Pneumologie', 'PNEUMO', terapie.id);
await dept('Reumatologie', 'REUMA', terapie.id);
const chir = await dept('Chirurgie', 'CHIR', med.id);
await dept('Chirurgie Generală', 'CHIR_GEN', chir.id);
await dept('Chirurgie Vasculară', 'CHIR_VAS', chir.id);
await dept('Ortopedie și Traumatologie','ORTOPED', chir.id);
await dept('Urologie', 'UROL', chir.id);
await dept('ORL', 'ORL', chir.id);
await dept('Oftalmologie', 'OFTALMO', chir.id);
const ped = await dept('Pediatrie', 'PED', med.id);
await dept('Pediatrie Generală', 'PED_GEN', ped.id);
await dept('Neonatologie', 'NEONAT', ped.id);
const obst = await dept('Obstetrică-Ginecologie', 'OBG', med.id);
await dept('Obstetrică', 'OBSTET', obst.id);
await dept('Ginecologie', 'GINECO', obst.id);
await dept('Oncologie', 'ONCOL', med.id);
await dept('Hemodializă', 'HEMODIAL', med.id);
await dept('Psihiatrie', 'PSIHIAT', med.id);
await dept('Dermatologie', 'DERMA', med.id);
await dept('Medicină Sportivă și Reabilitare', 'REAB', med.id);
// Diagnostics
const diag = await dept('Diagnostic', 'DIAG', root.id);
await dept('Laborator Clinic', 'LAB', diag.id);
await dept('Imagistică Medicală (CT/RMN/Rx)', 'IMAG', diag.id);
await dept('Endoscopie', 'ENDOSC', diag.id);
await dept('Ecografie', 'ECO', diag.id);
await dept('Cardiologie Funcțională (ECG/Holter)', 'ECG', diag.id);
// Support
const suport = await dept('Servicii Suport', 'SUPORT', root.id);
await dept('Urgențe (UPU)', 'UPU', suport.id);
await dept('Anestezie și Terapie Intensivă (ATI)', 'ATI', suport.id);
await dept('Bloc Operator', 'BLOC_OP', suport.id);
await dept('Sterilizare', 'STERIL', suport.id);
await dept('Farmacie', 'FARMACIE', suport.id);
await dept('Nutriție și Dietetică', 'NUTRIT', suport.id);
await dept('Serviciu Social', 'SOC', suport.id);
await dept('Curățenie și Dezinfecție', 'CURATENIE',suport.id);
await dept('Securitate', 'SECUR', suport.id);
await dept('Tehnică Medicală', 'TEH_MED', suport.id);
// Ambulatory
const ambul = await dept('Centru Ambulator', 'AMBUL', root.id);
await dept('Medicină de Familie', 'MED_FAM', ambul.id);
await dept('Consultații Specializate', 'CONSULT', ambul.id);
await dept('Fizioterapie', 'FIZIOTER', ambul.id);
const deptCount = await prisma.department.count();
console.log(` ✓ Department (${deptCount})`);
// ── Anexa Templates — minimal seed ─────────────────────────
const heading = (text: string, level = 2) => ({
type: 'heading',
attrs: { level, textAlign: 'center' },
content: [{ type: 'text', text }],
});
const para = (content: object[], textAlign: string = 'left') => ({
type: 'paragraph',
attrs: { textAlign },
content,
});
const txt = (text: string, marks?: { type: string }[]) =>
marks ? { type: 'text', text, marks } : { type: 'text', text };
const chip = (key: string, label: string) => ({
type: 'variableChip',
attrs: { key, label },
});
const cell = (content: object[]) => ({ type: 'tableCell', content });
const row = (cells: object[]) => ({ type: 'tableRow', content: cells });
const headerRow = (labels: string[]) =>
row(labels.map((l) => cell([para([txt(l, [{ type: 'bold' }])], 'center')])));
// ── Anexa 3: Fișa de solicitare ─────────────────────────────────
const anexa3 = {
type: 'doc',
content: [
heading('FIȘA DE SOLICITARE A EXAMENULUI MEDICAL'),
para([txt('Unitatea economică: '), chip('company.name', 'Denumirea unității')]),
para([txt('IDNO: '), chip('company.idno', 'IDNO'), txt(' Adresa: '), chip('company.address', 'Adresa')]),
para([txt('Tipul examenului: '), chip('tipExamen', 'Tipul examenului')]),
para([txt('Departament: '), chip('department.name', 'Departament'), txt(' Carta de risc: '), chip('riskCard.name', 'Carta de risc')]),
para([txt('Data: '), chip('document.date', 'Data documentului'), txt(' Nr.: '), chip('document.number', 'Număr')]),
para([txt('Lista angajaților:', [{ type: 'bold' }])]),
{
type: 'table',
attrs: { repeatRows: true },
content: [
headerRow(['Nr.', 'Nume Prenume', 'IDNP', 'Anul nașterii', 'Ocupația', 'Tipul examenului']),
row([
cell([para([chip('row.index', 'Nr.')])]),
cell([para([chip('row.employeeName', 'Nume Prenume')])]),
cell([para([chip('row.idnp', 'IDNP')])]),
cell([para([chip('row.birthYear', 'Anul nașterii')])]),
cell([para([chip('row.occupation', 'Ocupația')])]),
cell([para([chip('row.tipExamen', 'Tipul examenului')])]),
]),
],
},
],
};
// ── Anexa 4: Fișa de evaluare a riscurilor profesionale (NU-10-MS-2026) ──
const cb = (key: string, label: string) => [chip(`a4.cb.${key}`, '☐'), txt(' ' + label)];
const factorTable = (rowsKey: string, cols: string[], rowChips: string[]) => ({
type: 'table',
attrs: { repeatRows: true, rowsKey },
content: [
headerRow(cols),
row(rowChips.map((k) => cell([para([chip(k, '—')])]))),
],
});
const anexa4 = {
type: 'doc',
content: [
// ── Antet ──
para([txt('Unitatea economică/instituția: '), chip('a4.unitatea', 'Denumirea unității')]),
para([txt('Adresa, telefon, fax, e-mail: '), chip('a4.adresa', 'Adresa')]),
para([txt('Filiala: '), chip('a4.filiala', '—'), txt(' Adresa filialei: '), chip('a4.adresaFiliala', '—'), txt(' CAEM (primele 2 cifre): '), chip('a4.caem2', '—')]),
heading('FIȘA de evaluare a riscurilor profesionale', 2),
para([txt('Ocupația (subgrupa majoră CORM): '), chip('a4.cormSubgrupa', '—')]),
para([txt('Direcția/secția/sectorul: '), chip('a4.directiaSectia', '—')]),
para([txt('Numărul locului de muncă: '), chip('a4.numarLoc', '—'), txt(' CAEM (nivel diviziune): '), chip('a4.caemDiviziune', '—')]),
para([txt('Numărul de lucrători care pot activa la acest loc de muncă: '), chip('a4.numarLucratori', '—'), txt(' Clasa condițiilor de muncă: '), chip('a4.clasa', '—')]),
// ── Descrierea activității ──
heading('Descrierea activității', 3),
para([txt('Lucrul în echipă: '), ...cb('echipa', 'da'), txt(' Nr. ore/zi: '), chip('a4.val.oreZi', '—'), txt(' Nr. schimburi: '), chip('a4.val.schimburi', '—')]),
para([...cb('schimbNoapte', 'schimb de noapte'), txt(' '), ...cb('pauzeOrganizate', 'pauze organizate')]),
para([txt('Riscuri: '), ...cb('riscInfectare', 'infectare'), txt(' '), ...cb('riscElectrocutare', 'electrocutare'), txt(' '), ...cb('riscTensiuneInalta', 'tensiune înaltă'), txt(' '), ...cb('riscInecare', 'înecare'), txt(' '), ...cb('riscAsfixiere', 'asfixiere')]),
para([...cb('riscStrivire', 'strivire'), txt(' '), ...cb('riscTaiere', 'tăiere'), txt(' '), ...cb('riscIntepare', 'înțepare'), txt(' '), ...cb('riscLovire', 'lovire'), txt(' '), ...cb('riscMuscatura', 'mușcătură'), txt(' '), ...cb('riscMicrotraumatisme', 'microtraumatisme repetate')]),
para([txt('Conduce mașina instituției: '), ...cb('conduceMasina', 'da'), txt(' categorie: '), chip('a4.val.conduceMasinaCategorie', '—'), txt(' '), ...cb('conduceUtilajeIntrauzinal', 'conduce utilaje numai intrauzinal')]),
// ── Spațiul de lucru ──
heading('Descrierea spațiului de lucru', 3),
para([txt('Dimensiunile încăperii: L '), chip('a4.val.spatiuL', '—'), txt(' l '), chip('a4.val.spatiul', '—'), txt(' H '), chip('a4.val.spatiuH', '—'), txt(' m')]),
para([txt('Suprafața de lucru: '), ...cb('suprafataVerticala', 'verticală'), txt(' '), ...cb('suprafataOrizontala', 'orizontală'), txt(' '), ...cb('suprafataOblica', 'oblică')]),
para([txt('Muncă: '), ...cb('muncaIzolare', 'în condiții de izolare'), txt(' '), ...cb('muncaInaltime', 'la înălțime'), txt(' '), ...cb('muncaInMiscare', 'în mișcare')]),
// ── Efort fizic ──
heading('Efort fizic', 3),
para([txt('Poziție preponderent: '), ...cb('pozitieOrtostatica', 'ortostatică'), txt(' '), ...cb('pozitieAsezat', 'așezat'), txt(' '), ...cb('pozitieAplecata', 'aplecată'), txt(' '), ...cb('pozitieMixta', 'mixtă'), txt(' '), ...cb('pozitieFortata', 'forțată/nefiziologică')]),
para([txt('Suprasolicitări musculo-articulare (coloană): '), ...cb('coloanaCervicala', 'cervicală'), txt(' '), ...cb('coloanaToracala', 'toracală'), txt(' '), ...cb('coloanaLombara', 'lombară')]),
para([txt('Manipulare manuală a maselor: '), ...cb('manipulareRidicare', 'ridicare'), txt(' '), ...cb('manipulareCoborare', 'coborâre'), txt(' '), ...cb('manipulareImpingere', 'împingere'), txt(' '), ...cb('manipulareTragere', 'tragere'), txt(' '), ...cb('manipularePurtare', 'purtare'), txt(' '), ...cb('manipulareDeplasare', 'deplasare')]),
para([txt('Greutate maximă manipulată manual: '), chip('a4.val.greutateMaxima', '—')]),
para([txt('Suprasolicitări: '), ...cb('suprasolicitariVizuale', 'vizuale'), txt(' '), ...cb('suprasolicitariAuditive', 'auditive'), txt(' '), ...cb('suprasolicitariNeuropsihice', 'neuropsihosenzoriale')]),
// ── Factori de risc cu tabel ──
heading('AGENȚI CHIMICI', 3),
para([...cb('chimici_da', 'da'), txt(' '), ...cb('chimici_nu', 'nu'), txt(' (se atașează Fișa cu date de securitate, în limba română)')]),
factorTable('chimici',
['Agentul chimic', 'CAS', 'EINECS', 'Timp expunere', 'VEP', 'VLEP obligatorie', 'Caracteristici'],
['row.denumire', 'row.cas', 'row.einecs', 'row.timp', 'row.vep', 'row.vlep', 'row.caracteristici']),
heading('PULBERI', 3),
para([...cb('pulberi_da', 'da'), txt(' '), ...cb('pulberi_nu', 'nu')]),
factorTable('pulberi',
['Pulberi', 'CAS', 'EINECS', 'Timp expunere', 'VEP', 'VLEP obligatorie', 'Caracteristici'],
['row.denumire', 'row.cas', 'row.einecs', 'row.timp', 'row.vep', 'row.vlep', 'row.caracteristici']),
heading('AGENȚI BIOLOGICI', 3),
para([...cb('biologici_da', 'da'), txt(' '), ...cb('biologici_nu', 'nu')]),
factorTable('biologici',
['Agent biologic', 'Clasificare', 'Note'],
['row.denumire', 'row.clasificare', 'row.caracteristici']),
heading('ZGOMOT PROFESIONAL', 3),
para([...cb('zgomot_da', 'da'), txt(' '), ...cb('zgomot_nu', 'nu')]),
factorTable('zgomot',
['Tipul', 'Timp expunere', 'VEP', 'VLEP obligatorie', 'Caracteristici'],
['row.denumire', 'row.timp', 'row.vep', 'row.vlep', 'row.caracteristici']),
heading('VIBRAȚII MECANICE', 3),
para([...cb('vibratii_da', 'da'), txt(' '), ...cb('vibratii_nu', 'nu')]),
factorTable('vibratii',
['Tipul', 'Zona afectată', 'Timp expunere', 'VEP', 'VLEP obligatorie', 'Caracteristici'],
['row.denumire', 'row.zona', 'row.timp', 'row.vep', 'row.vlep', 'row.caracteristici']),
// ── Microclimat (descriptiv) ──
heading('MICROCLIMAT', 3),
para([...cb('microclimatInterior', 'lucrări interior'), txt(' '), ...cb('microclimatExterior', 'lucru exterior/sub cerul liber')]),
para([txt('Radiații calorice (perioada rece): '), ...cb('radiatiiCaloriceRece', 'da'), txt(' Radiații calorice (perioada caldă): '), ...cb('radiatiiCaloriceCalda', 'da')]),
// ── Radiații ionizante ──
heading('RADIAȚII IONIZANTE', 3),
para([...cb('radiatii_da', 'da'), txt(' '), ...cb('radiatii_nu', 'nu'), txt(' Grupa: '), chip('a4.rad.grupa', '—')]),
para([txt('Aparatură folosită: '), chip('a4.rad.aparatura', '—'), txt(' Surse: '), chip('a4.rad.surse', '—')]),
para([txt('Tip de expunere: '), chip('a4.rad.tipExpunere', '—'), txt(' Măsuri de protecție: '), chip('a4.rad.masuriProtectie', '—')]),
// ── Câmp electromagnetic ──
heading('CÂMP ELECTROMAGNETIC', 3),
para([...cb('campEM_da', 'da'), txt(' '), ...cb('campEM_nu', 'nu')]),
factorTable('campEM',
['Tipul', 'Zona afectată', 'Timp expunere', 'VEP', 'VLEP obligatorie', 'Caracteristici'],
['row.denumire', 'row.zona', 'row.timp', 'row.vep', 'row.vlep', 'row.caracteristici']),
// ── Iluminat ──
heading('ILUMINAT', 3),
para([...cb('iluminatSuficient', 'suficient'), txt(' '), ...cb('iluminatInsuficient', 'insuficient'), txt(' '), ...cb('iluminatNatural', 'natural'), txt(' '), ...cb('iluminatArtificial', 'artificial'), txt(' '), ...cb('iluminatMixt', 'mixt')]),
// ── Radiații optice artificiale ──
heading('RADIAȚII OPTICE ARTIFICIALE', 3),
para([...cb('optice_da', 'da'), txt(' '), ...cb('optice_nu', 'nu')]),
factorTable('optice',
['Tipul', 'Zona afectată', 'Timp expunere', 'VEP', 'VLEP obligatorie', 'Caracteristici'],
['row.denumire', 'row.zona', 'row.timp', 'row.vep', 'row.vlep', 'row.caracteristici']),
// ── Subsol ──
heading('Protecție și dotări', 3),
para([txt('Mijloace de protecție colectivă: '), chip('a4.protectieColectiva', '—')]),
para([txt('Mijloace de protecție individuală: '), chip('a4.protectieIndividuala', '—')]),
para([txt('Echipament de lucru: '), chip('a4.echipament', '—')]),
para([txt('Anexe igienico-sanitare: '), ...cb('anexe.vestiar', 'vestiar'), txt(' '), ...cb('anexe.chiuveta', 'chiuvetă'), txt(' '), ...cb('anexe.wc', 'WC'), txt(' '), ...cb('anexe.dus', 'duș'), txt(' '), ...cb('anexe.salaMese', 'sală de mese'), txt(' '), ...cb('anexe.recreere', 'spațiu de recreere')]),
para([txt('Observații: '), chip('a4.observatii', '—')]),
para([txt('Data completării: '), chip('document.date', 'Data')]),
para([txt('Angajatorul (nume, prenume, semnătura): ____________________')]),
para([txt('Instrucțiuni de completare: răspuns afirmativ [☑]; răspuns negativ [☐].', [{ type: 'italic' }])]),
],
};
// ── Anexa 4B: Supliment radiații ionizante ───────────────────────
const anexa4b = {
type: 'doc',
content: [
heading('SUPLIMENT — EXPUNERE LA RADIAȚII IONIZANTE'),
para([txt('Unitatea economică: '), chip('company.name', 'Denumirea unității')]),
para([txt('Data: '), chip('document.date', 'Data documentului')]),
para([txt('Personal expus radiațiilor ionizante:', [{ type: 'bold' }])]),
{
type: 'table',
attrs: { repeatRows: true },
content: [
headerRow(['Nr.', 'Nume Prenume', 'IDNP', 'Data intrării', 'Perioada anterioară', 'Ani', 'Doza ext. (mSv)', 'Doza int. (mSv)', 'Total (mSv)']),
row([
cell([para([chip('row.index', 'Nr.')])]),
cell([para([chip('row.employeeName', 'Nume Prenume')])]),
cell([para([chip('row.idnp', 'IDNP')])]),
cell([para([chip('row.entryDate', 'Data intrării')])]),
cell([para([chip('row.priorPeriod', 'Perioada anterioară')])]),
cell([para([chip('row.priorYears', 'Ani')])]),
cell([para([chip('row.externalMsv', 'Doza ext.')])]),
cell([para([chip('row.internalMsv', 'Doza int.')])]),
cell([para([chip('row.totalMsv', 'Total')])]),
]),
],
},
],
};
// ── Anexa 6: Verdict medic de familie (per-employee) ─────────────
const anexa6 = {
type: 'doc',
content: [
heading('FIȘĂ DE APTITUDINE — VERDICTUL MEDICULUI DE FAMILIE'),
para([txt('Angajat: '), chip('employee.fullName', 'Nume Prenume')]),
para([txt('IDNP: '), chip('employee.idnp', 'IDNP'), txt(' Data nașterii: '), chip('employee.birthDate', 'Data nașterii')]),
para([txt('Ocupația: '), chip('employee.occupation', 'Ocupația'), txt(' Departament: '), chip('employee.department', 'Departament')]),
para([txt('Tipul examenului: '), chip('tipExamen', 'Tipul examenului')]),
para([txt('Data examinării: '), chip('document.date', 'Data')]),
para([txt('Verdict:', [{ type: 'bold' }])]),
para([chip('verdict.checkbox.apt', '☐'), txt(' Apt')]),
para([chip('verdict.checkbox.apt_perioada_adaptare', '☐'), txt(' Apt în perioada de adaptare')]),
para([chip('verdict.checkbox.apt_conditionat', '☐'), txt(' Apt condiționat')]),
para([chip('verdict.checkbox.inapt_temporar', '☐'), txt(' Inapt temporar')]),
para([chip('verdict.checkbox.inapt', '☐'), txt(' Inapt')]),
para([txt('Recomandări: '), chip('verdict.recomandari', 'Recomandări')]),
para([txt(' ')]),
para([txt('Semnătura medicului de familie: ____________________')]),
],
};
const SYS = '00000000-0000-0000-0000-000000000000';
const templates: Array<{ type: 'ANEXA_3' | 'ANEXA_4' | 'ANEXA_4B' | 'ANEXA_6'; name: string; doc: object }> = [
{ type: 'ANEXA_3', name: 'Fișa de solicitare a examenului medical', doc: anexa3 },
{ type: 'ANEXA_4', name: 'Fișa de evaluare a locului de muncă', doc: anexa4 },
{ type: 'ANEXA_4B', name: 'Supliment radiații ionizante', doc: anexa4b },
{ type: 'ANEXA_6', name: 'Verdict medic de familie', doc: anexa6 },
];
for (const t of templates) {
await prisma.anexaTemplate.upsert({
where: { type: t.type },
update: { name: t.name, contentJson: t.doc as never },
create: { type: t.type, name: t.name, contentJson: t.doc as never, updatedById: SYS },
});
}
console.log(' ✓ AnexaTemplate (4)');
// ── Inventory items (depozit Vestimentație + Echipament) ─────────
const inventory = [
{ sku: 'UN-CHIR-S-AL', name: 'Uniformă chirurgie S albastru', type: 'uniforma' as const, size: 'S', color: 'albastru', stockQty: 50 },
{ sku: 'UN-CHIR-M-AL', name: 'Uniformă chirurgie M albastru', type: 'uniforma' as const, size: 'M', color: 'albastru', stockQty: 50 },
{ sku: 'UN-CHIR-L-AL', name: 'Uniformă chirurgie L albastru', type: 'uniforma' as const, size: 'L', color: 'albastru', stockQty: 50 },
{ sku: 'UN-ATI-M-VE', name: 'Uniformă ATI M verde', type: 'uniforma' as const, size: 'M', color: 'verde', stockQty: 30 },
{ sku: 'HA-MED-M-AL', name: 'Halat medical M alb', type: 'halat' as const, size: 'M', color: 'alb', stockQty: 50 },
{ sku: 'HA-MED-L-AL', name: 'Halat medical L alb', type: 'halat' as const, size: 'L', color: 'alb', stockQty: 50 },
{ sku: 'HA-LAB-M-AL', name: 'Halat laborator M alb', type: 'halat' as const, size: 'M', color: 'alb', stockQty: 30 },
{ sku: 'HA-LAB-L-AL', name: 'Halat laborator L alb', type: 'halat' as const, size: 'L', color: 'alb', stockQty: 30 },
{ sku: 'CI-38-AL', name: 'Ciupici 38-40 albi', type: 'ciupici' as const, size: '38-40', color: 'alb', stockQty: 80 },
{ sku: 'CI-41-AL', name: 'Ciupici 41-43 albi', type: 'ciupici' as const, size: '41-43', color: 'alb', stockQty: 80 },
{ sku: 'CI-44-AL', name: 'Ciupici 44-46 albi', type: 'ciupici' as const, size: '44-46', color: 'alb', stockQty: 80 },
{ sku: 'VE-S-TE', name: 'Vestă S teal', type: 'vesta' as const, size: 'S', color: 'teal', stockQty: 20 },
{ sku: 'VE-M-TE', name: 'Vestă M teal', type: 'vesta' as const, size: 'M', color: 'teal', stockQty: 20 },
{ sku: 'AT-SAMS-A15', name: 'Samsung Galaxy A15', type: 'aparat_telefon' as const, stockQty: 15 },
{ sku: 'AT-IPHONE-SE', name: 'iPhone SE 2022', type: 'aparat_telefon' as const, stockQty: 10 },
];
for (const item of inventory) {
await prisma.inventoryItem.upsert({
where: { sku: item.sku },
update: {},
create: item,
});
}
console.log(` ✓ InventoryItem (${inventory.length})`);
// ── Demo data pentru prezentare ──────────────────────────────
console.log('\n🎭 Seeding demo data...');
// Risk cards
const chirExposures = [
{ tip: 'AGENT_CHIMIC' as const, denumire: 'Glutaraldehidă (dezinfectant)', cas: '111-30-8', einecs: '203-856-5', timpExpunere: '2 h/zi', vep: '0,03 ppm', vlep: '0,1 ppm', caracteristici: 'iritant respirator' },
{ tip: 'AGENT_BIOLOGIC' as const, denumire: 'Virusuri hematogene (HBV, HCV, HIV)', clasificare: 'grupa 3', caracteristici: 'risc de infectare prin înțepare/tăiere' },
];
const chirHeader = {
filiala: 'Sediul central',
caemPrimeleDouaCifre: '86',
cormSubgrupaMajora: 'Personal medical — secție chirurgie',
directiaSectiaSectorul: 'Bloc Medical / Chirurgie Generală',
numarulLoculuiDeMunca: 'CH-01',
caemDiviziune: '86.10',
clasaConditiilorDeMunca: '3.2',
numarLucratoriPosibili: 12,
evaluareDetalii: {
echipa: true, oreZi: '8', schimburi: '2', schimbNoapte: true, pauzeOrganizate: true,
riscInfectare: true, riscTaiere: true, riscIntepare: true,
pozitieOrtostatica: true, manipulareRidicare: true, suprasolicitariVizuale: true,
},
anexeIgienicoSanitare: { vestiar: true, chiuveta: true, wc: true, dus: true, salaMese: true },
mijloaceProtectieIndividuala: 'Mănuși, mască, halat steril',
echipamentLucru: 'Uniformă chirurgicală',
};
const rcChir = await prisma.workplaceRiskCard.upsert({
where: { name: 'Secție chirurgie generală' },
update: { ...chirHeader, exposures: { deleteMany: {}, create: chirExposures } },
create: { name: 'Secție chirurgie generală', ...chirHeader, exposures: { create: chirExposures } },
});
const imagExposures = [
{ tip: 'CAMP_ELECTROMAGNETIC' as const, denumire: 'Câmp electromagnetic RMN', zonaAfectata: 'corp întreg', timpExpunere: '4 h/zi', vep: '—', vlep: 'conform NU-10', caracteristici: 'câmp magnetic static intens' },
];
const imagHeader = {
filiala: 'Sediul central',
caemPrimeleDouaCifre: '86',
cormSubgrupaMajora: 'Personal imagistică medicală',
directiaSectiaSectorul: 'Diagnostic / Imagistică Medicală',
numarulLoculuiDeMunca: 'IMG-01',
caemDiviziune: '86.90',
clasaConditiilorDeMunca: '3.3',
numarLucratoriPosibili: 8,
radiatiiIonizante: true,
radiatiiGrupa: 'A',
radiatiiSurse: 'închise',
radiatiiTipExpunere: 'X externă',
radiatiiAparatura: 'CT, aparat Rx',
radiatiiMasuriProtectie: 'șorț cu plumb, ecran de protecție, dozimetru individual',
evaluareDetalii: {
echipa: true, oreZi: '7', schimburi: '2',
riscElectrocutare: true, pozitieAsezat: true, suprasolicitariVizuale: true,
},
anexeIgienicoSanitare: { vestiar: true, chiuveta: true, wc: true },
mijloaceProtectieIndividuala: 'Șorț cu plumb, ochelari, dozimetru',
};
const rcImag = await prisma.workplaceRiskCard.upsert({
where: { name: 'Radiologie și imagistică' },
update: { ...imagHeader, exposures: { deleteMany: {}, create: imagExposures } },
create: { name: 'Radiologie și imagistică', ...imagHeader, exposures: { create: imagExposures } },
});
console.log(' ✓ WorkplaceRiskCard demo (2) — cu antet Anexa 4 + factori');
// Lookup departments & inventory items
const chirGenDept = await prisma.department.findUnique({ where: { code: 'CHIR_GEN' } });
const imagDept = await prisma.department.findUnique({ where: { code: 'IMAG' } });
const uniformaS = await prisma.inventoryItem.findUnique({ where: { sku: 'UN-CHIR-S-AL' } });
const halatM = await prisma.inventoryItem.findUnique({ where: { sku: 'HA-MED-M-AL' } });
// 4 demo employees (IDNPs pre-validated cu algoritmul de sumă de control MD)
const emp1 = await prisma.employee.upsert({
where: { idnp: '1985061500016' },
update: {},
create: {
idnp: '1985061500016', nume: 'Popescu', prenume: 'Alexandru',
sex: 'M', dataNasterii: new Date('1985-06-15'),
domiciliu: 'mun. Chișinău, str. Ștefan cel Mare 1',
telefonPersonal: '+37369100001', status: 'activ',
},
});
const emp2 = await prisma.employee.upsert({
where: { idnp: '1990032200017' },
update: {},
create: {
idnp: '1990032200017', nume: 'Ionescu', prenume: 'Maria',
sex: 'F', dataNasterii: new Date('1990-03-22'),
domiciliu: 'mun. Chișinău, str. Mihai Viteazul 5',
telefonPersonal: '+37369100002', status: 'activ',
},
});
const emp3 = await prisma.employee.upsert({
where: { idnp: '1978110800016' },
update: {},
create: {
idnp: '1978110800016', nume: 'Rusu', prenume: 'Viorel',
sex: 'M', dataNasterii: new Date('1978-11-08'),
domiciliu: 'mun. Chișinău, str. Alba Iulia 12',
telefonPersonal: '+37369100003', status: 'activ',
},
});
const emp4 = await prisma.employee.upsert({
where: { idnp: '2001091400010' },
update: {},
create: {
idnp: '2001091400010', nume: 'Cojocaru', prenume: 'Elena',
sex: 'F', dataNasterii: new Date('2001-09-14'),
domiciliu: 'mun. Chișinău, str. Trandafirilor 3',
telefonPersonal: '+37369100004', status: 'activ',
},
});
console.log(' ✓ Employee demo (4)');
// Employment contracts
if (chirGenDept) {
await prisma.employmentContract.upsert({
where: { nrCim: 'CIM-DEMO-001' },
update: {},
create: {
nrCim: 'CIM-DEMO-001', employeeId: emp1.id,
categorie: 'principal', tipCim: 'de_baza', perioada: 'nedeterminata',
dataSemnarii: new Date('2020-01-10'), dataAngajarii: new Date('2020-01-15'),
departmentId: chirGenDept.id, functiaOrganigrama: 'Chirurg',
salarizareDetails: { tip: 'fix', salariu: 18000, zileConcediu: 28 },
},
});
await prisma.employmentContract.upsert({
where: { nrCim: 'CIM-DEMO-002' },
update: {},
create: {
nrCim: 'CIM-DEMO-002', employeeId: emp2.id,
categorie: 'principal', tipCim: 'de_baza', perioada: 'nedeterminata',
dataSemnarii: new Date('2021-03-01'), dataAngajarii: new Date('2021-03-05'),
departmentId: chirGenDept.id, functiaOrganigrama: 'Asistentă medicală',
salarizareDetails: { tip: 'fix', salariu: 10000, zileConcediu: 28 },
},
});
}
if (imagDept) {
await prisma.employmentContract.upsert({
where: { nrCim: 'CIM-DEMO-003' },
update: {},
create: {
nrCim: 'CIM-DEMO-003', employeeId: emp3.id,
categorie: 'principal', tipCim: 'de_baza', perioada: 'nedeterminata',
dataSemnarii: new Date('2018-06-01'), dataAngajarii: new Date('2018-06-10'),
departmentId: imagDept.id, functiaOrganigrama: 'Radiolog',
salarizareDetails: { tip: 'fix', salariu: 20000, zileConcediu: 35 },
},
});
await prisma.employmentContract.upsert({
where: { nrCim: 'CIM-DEMO-004' },
update: {},
create: {
nrCim: 'CIM-DEMO-004', employeeId: emp4.id,
categorie: 'principal', tipCim: 'de_baza', perioada: 'nedeterminata',
dataSemnarii: new Date('2023-09-01'), dataAngajarii: new Date('2023-09-15'),
departmentId: imagDept.id, functiaOrganigrama: 'Asistentă radiologie',
salarizareDetails: { tip: 'fix', salariu: 9500, zileConcediu: 28 },
},
});
}
console.log(' ✓ EmploymentContract demo (4)');
// Medical profiles:
// emp1 — chirurgie, niciodată examinat
// emp2 — chirurgie, examinat acum 15 luni (expirat)
// emp3 — radiologie, examinat acum 11 luni + radiații (expiră curând)
// emp4 — radiologie, niciodată examinat + radiații
await prisma.employeeMedicalProfile.upsert({
where: { employeeId: emp1.id }, update: {},
create: { employeeId: emp1.id, workplaceRiskCardId: rcChir.id, expusRadiatiiIonizante: false },
});
await prisma.employeeMedicalProfile.upsert({
where: { employeeId: emp2.id }, update: {},
create: {
employeeId: emp2.id, workplaceRiskCardId: rcChir.id,
dataUltimControlMedical: new Date('2025-02-14'),
expusRadiatiiIonizante: false,
},
});
const emp3Radiatii = {
workplaceRiskCardId: rcImag.id,
dataUltimControlMedical: new Date('2025-06-14'),
expusRadiatiiIonizante: true,
dataIntrarii: new Date('2019-02-01'),
expunereAnterioaraPerioda: '20152018',
expunereAnterioaraAni: 3,
dozaCumulataExternaMsv: 4.2500,
dozaCumulataInternaMsv: 0.8000,
};
const emp3Supra = [
{ fel: 'EXCEPTIONALA' as const, tipExpunere: 'X externă', data: new Date('2023-05-12'), dozaMsv: 2.5000 },
{ fel: 'ACCIDENTALA' as const, tipExpunere: 'gamma externă', data: new Date('2024-09-03'), dozaMsv: 1.2000 },
];
await prisma.employeeMedicalProfile.upsert({
where: { employeeId: emp3.id },
update: { ...emp3Radiatii, overexposures: { deleteMany: {}, create: emp3Supra } },
create: { employeeId: emp3.id, ...emp3Radiatii, overexposures: { create: emp3Supra } },
});
await prisma.employeeMedicalProfile.upsert({
where: { employeeId: emp4.id }, update: {},
create: { employeeId: emp4.id, workplaceRiskCardId: rcImag.id, expusRadiatiiIonizante: true },
});
console.log(' ✓ EmployeeMedicalProfile demo (4)');
// Pending checkups for inbox (verdict = null)
// emp1 — la_angajare, acum 5 zile (depășit → roșu)
// emp2 — periodic, peste 3 zile
// emp3 — la_reluarea_activitatii, mâine
const day = (offsetDays: number) => {
const d = new Date('2026-05-14');
d.setDate(d.getDate() + offsetDays);
return d;
};
for (const [empId, tip, offset] of [
[emp1.id, 'la_angajare', -5],
[emp2.id, 'periodic', 3],
[emp3.id, 'la_reluarea_activitatii', 1],
] as [string, string, number][]) {
const exists = await prisma.medicalCheckup.findFirst({ where: { employeeId: empId, verdict: null } });
if (!exists) {
await prisma.medicalCheckup.create({
data: { employeeId: empId, tip: tip as never, dataPlanificata: day(offset) },
});
}
}
console.log(' ✓ MedicalCheckup demo — pending inbox (3)');
// ── Evaluation campaigns demo (modulul de evaluare nursing) ──────
// Campania A — Chirurgie Generală, IN_PROGRESS:
// emp2 (Ionescu Maria) — formular complet, scoruri bune + 1 criteriu EXPERT
// → categorie calculată "superioara", ÎNCĂ NEAPROBATĂ
// (nursing_director o poate aproba — demo aprobare)
// emp1 (Popescu Alexandru) — formular parțial (în lucru) → "fara"
if (chirGenDept) {
const campMonth = new Date('2026-05-01');
let camp = await prisma.evaluationCampaign.findFirst({
where: { departmentId: chirGenDept.id, month: campMonth },
});
if (!camp) {
camp = await prisma.evaluationCampaign.create({
data: {
name: 'Evaluare anuală nursing — Chirurgie Generală 2026',
departmentId: chirGenDept.id,
month: campMonth,
status: 'in_progress',
},
});
}
await prisma.evaluationForm.upsert({
where: { campaignId_employeeId: { campaignId: camp.id, employeeId: emp2.id } },
update: {},
create: {
campaignId: camp.id, employeeId: emp2.id,
abilitatiClinice: 'bine', judecataClinica: 'bine', manopere: 'bine', gestionareaSarcinilor: 'mediu',
constiintaProfesionala: 'bine', atitudineaPacienti: 'bine', atitudineaColegi: 'bine', atitudineaPersonalNonMed: 'mediu',
utilizareSmartphone: 'bine', respectareaProgramului: 'bine', respectareaDressCode: 'bine',
testJci: { score: 18, max_score: 20, percent: 90, completed_at: '2026-05-10', source: 'academy_ocean', external_id: 'AO-DEMO-001' },
completareaDocMed: true, perfectioneazaCunostinte: true,
membruComitetCalitate: true, functieDeMonitor: false, inlocuiesteSuperiorul: false,
categorieCalculata: 'superioara',
},
});
await prisma.evaluationForm.upsert({
where: { campaignId_employeeId: { campaignId: camp.id, employeeId: emp1.id } },
update: {},
create: {
campaignId: camp.id, employeeId: emp1.id,
abilitatiClinice: 'bine', judecataClinica: 'mediu', manopere: 'mediu',
categorieCalculata: 'fara',
},
});
console.log(' ✓ EvaluationCampaign demo — Chirurgie (in_progress, 2 formulare)');
}
// Campania B — Imagistică, CLOSED (istoric read-only):
// emp3 (Rusu Viorel) — formular finalizat și aprobat → "cat_I"
if (imagDept) {
const campMonth = new Date('2025-11-01');
let camp = await prisma.evaluationCampaign.findFirst({
where: { departmentId: imagDept.id, month: campMonth },
});
if (!camp) {
camp = await prisma.evaluationCampaign.create({
data: {
name: 'Evaluare anuală nursing — Imagistică 2025',
departmentId: imagDept.id,
month: campMonth,
status: 'closed',
},
});
}
await prisma.evaluationForm.upsert({
where: { campaignId_employeeId: { campaignId: camp.id, employeeId: emp3.id } },
update: {},
create: {
campaignId: camp.id, employeeId: emp3.id,
abilitatiClinice: 'bine', judecataClinica: 'bine', manopere: 'bine', gestionareaSarcinilor: 'bine',
constiintaProfesionala: 'bine', atitudineaPacienti: 'mediu', atitudineaColegi: 'bine', atitudineaPersonalNonMed: 'bine',
utilizareSmartphone: 'bine', respectareaProgramului: 'bine', respectareaDressCode: 'mediu',
completareaDocMed: true, perfectioneazaCunostinte: true,
membruComitetCalitate: false, functieDeMonitor: false, inlocuiesteSuperiorul: false,
categorieCalculata: 'cat_I',
categorieAprobata: 'cat_I',
observatii: 'Performanță constantă, recomandat pentru categoria I.',
completedAt: new Date('2025-11-20'),
},
});
console.log(' ✓ EvaluationCampaign demo — Imagistică (closed, 1 formular aprobat)');
}
// Benefit cu vestimentație pentru emp1
if (uniformaS && halatM) {
await prisma.benefit.upsert({
where: { employeeId: emp1.id },
update: {},
create: {
employeeId: emp1.id,
uniformaId: uniformaS.id,
halatId: halatM.id,
ticheteMasa: true,
valoareTichet: 65,
alimentatiePersonal: false,
abonamentTel: 150,
},
});
console.log(' ✓ Benefit demo (1) — Popescu Alexandru: uniformă + halat');
}
console.log('\n✅ Seed complete.');
}
main()
.catch((e) => { console.error(e); process.exit(1); })
.finally(() => prisma.$disconnect());