Files
hrm-medpark/apps/api/prisma/seed.ts
T
Danil Suhomlinov 33800292aa 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>
2026-06-08 17:42:45 +03:00

739 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());