// HRM Medpark — Prisma Schema // Phase 1: Employee Master Data + Department + AuditLog // Phase 2 stubs: EmploymentContract // Phase 4 stubs: EvaluationCampaign, EvaluationForm // Phase 5 stubs: WorkplaceRiskCard, EmployeeMedicalProfile, MedicalCheckup generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ═══════════════════════════════════════════════════════════════ // ENUMS // ═══════════════════════════════════════════════════════════════ enum Sex { F M } enum MaritalStatus { casatorit necasatorit divortat vaduv } enum EmployeeStatus { activ concediat suspendat } enum DocumentType { buletin_de_identitate pasaport } enum FamilyMemberType { contact_principal sot sotie mama tata copil } enum StudyType { superioare medii_de_specialitate secundare_tehnice medii } enum StudyLevel { de_baza postuniversitar } enum PostUniversityType { masterat rezidentiat secundariat altele } enum DiplomaStatus { confirmata neconfirmata } enum QualificationCategory { fara cat_II cat_I superioara } enum ScientificTitle { doctor doctor_habilitat } enum TrainingType { orientare intern extern_RM extern_international } enum DisciplinarySanctionType { avertisment mustrare mustrare_aspra } // Phase 2 enum ContractPeriod { determinata nedeterminata replasare_temporara } enum ContractCategory { principal secundar } enum ContractType { de_baza cumul } enum SalaryType { fix pe_ore in_acord } // Phase 4 enum CampaignStatus { draft scheduled in_progress closed } enum EvaluationScore { slab mediu bine } enum ProposedCategory { fara cat_II cat_I superioara } // Phase 5 enum MedicalCheckupType { la_angajare periodic la_reluarea_activitatii la_incetarea_expunerii suplimentar } enum MedicalVerdict { apt apt_perioada_adaptare apt_conditionat inapt_temporar inapt } enum AnexaType { ANEXA_3 ANEXA_4 ANEXA_4A ANEXA_4B ANEXA_6 } // Tipuri de factori cu tabel de expunere în Anexa 4 (NU-10-MS-2026) enum RiskExposureType { AGENT_CHIMIC PULBERI AGENT_BIOLOGIC ZGOMOT VIBRATII CAMP_ELECTROMAGNETIC RADIATII_OPTICE } // Tipul supraexpunerii la radiații ionizante (Anexa 4B) enum OverexposureKind { EXCEPTIONALA ACCIDENTALA } // ═══════════════════════════════════════════════════════════════ // СПРАВОЧНИКИ // ═══════════════════════════════════════════════════════════════ model DisabilityGrade { id String @id @default(uuid()) code String @unique name String employees Employee[] @@map("disability_grades") } model TaxExemption { id String @id @default(uuid()) code String @unique description String familyMembers FamilyMember[] @@map("tax_exemptions") } model WorkSchedule { id String @id @default(uuid()) name String @unique // "5/2 8h", "7/7 12h" daysWork Int daysRest Int hoursPerDay Int contracts EmploymentContract[] @@map("work_schedules") } // ═══════════════════════════════════════════════════════════════ // DEPARTMENT — иерархия (adjacency list) // ═══════════════════════════════════════════════════════════════ model Department { id String @id @default(uuid()) name String code String? @unique parentId String? parent Department? @relation("DeptTree", fields: [parentId], references: [id]) children Department[] @relation("DeptTree") contracts EmploymentContract[] campaigns EvaluationCampaign[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([parentId]) @@map("departments") } // ═══════════════════════════════════════════════════════════════ // EMPLOYEE — ядро системы // ═══════════════════════════════════════════════════════════════ model Employee { id String @id @default(uuid()) // IDNP — 13 цифр, алгоритм контрольной суммы валидируется на app-уровне idnp String @unique @db.VarChar(13) // A. Личная информация nume String prenume String patronimic String? numeAnterior String? dataNasterii DateTime @db.Date domiciliu String adresaReala String? telefonPersonal String telefonServiciu String? emailPersonal String? emailCorporativ String? sex Sex codCpas String? stareCivila MaritalStatus? // Научное/университетское звание (уровень Employee, не Qualification) titluStiintific ScientificTitle? titluUniversitar String? status EmployeeStatus @default(activ) gradDizabilitateId String? gradDizabilitate DisabilityGrade? @relation(fields: [gradDizabilitateId], references: [id]) // Кто рекомендовал (самоссылка) // Бизнес-правило: нельзя выбрать супруга текущего сотрудника — проверка на service-уровне recomandareInternaId String? recomandareInterna Employee? @relation("Recomandari", fields: [recomandareInternaId], references: [id]) recomandat Employee[] @relation("Recomandari") // Связанные сущности identityDocuments IdentityDocument[] familyMembers FamilyMember[] educations Education[] qualifications Qualification[] trainings Training[] disciplinarySanctions DisciplinarySanction[] contracts EmploymentContract[] benefit Benefit? evaluationForms EvaluationForm[] medicalProfile EmployeeMedicalProfile? medicalCheckups MedicalCheckup[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([idnp]) @@index([nume, prenume]) @@index([status]) @@index([dataNasterii]) @@map("employees") } // ═══════════════════════════════════════════════════════════════ // B. IDENTITY DOCUMENT // ═══════════════════════════════════════════════════════════════ model IdentityDocument { id String @id @default(uuid()) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) tipAct DocumentType seria String? nr String dataEmiterii DateTime @db.Date autoritateEmitenta String dataExpirarii DateTime @db.Date createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Cron-задача за 30 дней до dataExpirarii → HR Inbox @@index([employeeId]) @@index([dataExpirarii]) @@map("identity_documents") } // ═══════════════════════════════════════════════════════════════ // C. FAMILY MEMBERS // ═══════════════════════════════════════════════════════════════ model FamilyMember { id String @id @default(uuid()) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) tip FamilyMemberType numePrenume String dataNasterii DateTime? @db.Date idnp String? @db.VarChar(13) telefon String? // обязателен для contact_principal — проверка на service-уровне // Скидки FISC (только для copil) tipScutireId String? tipScutire TaxExemption? @relation(fields: [tipScutireId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([employeeId]) @@map("family_members") } // ═══════════════════════════════════════════════════════════════ // D. EDUCATION // ═══════════════════════════════════════════════════════════════ model Education { id String @id @default(uuid()) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) tipStudii StudyType institutia String specialitatea String dataAbsolvirii DateTime? @db.Date nrSeriaDiploma String? dataEmiterii DateTime? @db.Date nrInregistrare String? confirmare DiplomaStatus? nivel StudyLevel? tipPostuniversitar PostUniversityType? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([employeeId]) @@map("educations") } // ═══════════════════════════════════════════════════════════════ // E. QUALIFICATIONS // ═══════════════════════════════════════════════════════════════ model Qualification { id String @id @default(uuid()) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) categorie QualificationCategory dataObtinerii DateTime? @db.Date dataUltimeiConfirmari DateTime? @db.Date dataExpirarii DateTime? @db.Date specialitate String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Cron-задача за 90/30/7 дней до dataExpirarii → HR + manager @@index([employeeId]) @@index([dataExpirarii]) @@map("qualifications") } // ═══════════════════════════════════════════════════════════════ // F. TRAINING // ═══════════════════════════════════════════════════════════════ model Training { id String @id @default(uuid()) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) denumire String inceput DateTime @db.Date sfirsit DateTime? @db.Date tip TrainingType tara String? nrOre Int? organizatia String? certificat Boolean @default(false) cost Decimal? @db.Decimal(10, 2) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([employeeId]) @@map("trainings") } // ═══════════════════════════════════════════════════════════════ // G. DISCIPLINARY SANCTIONS // ═══════════════════════════════════════════════════════════════ model DisciplinarySanction { id String @id @default(uuid()) employeeId String employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) tip DisciplinarySanctionType dataAplicarii DateTime @db.Date // auto-calc: dataAplicarii + 6 months — вычисляется на service-уровне при создании dataExpirarii DateTime @db.Date // set true cron-ом после dataExpirarii; до этого — активна при расчёте performance isStinsa Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([employeeId]) @@index([dataExpirarii]) @@map("disciplinary_sanctions") } // ═══════════════════════════════════════════════════════════════ // H. BENEFITS // ═══════════════════════════════════════════════════════════════ model Benefit { id String @id @default(uuid()) employeeId String @unique employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) uniformaId String? uniforma InventoryItem? @relation("BenefitUniforma", fields: [uniformaId], references: [id]) halatId String? halat InventoryItem? @relation("BenefitHalat", fields: [halatId], references: [id]) ciupiciId String? ciupici InventoryItem? @relation("BenefitCiupici", fields: [ciupiciId], references: [id]) vestaId String? vesta InventoryItem? @relation("BenefitVesta", fields: [vestaId], references: [id]) ticheteMasa Boolean @default(false) valoareTichet Decimal? @db.Decimal(10, 2) alimentatiePersonal Boolean @default(false) abonamentTel Decimal? @db.Decimal(10, 2) aparatTelefonId String? aparatTelefon InventoryItem? @relation("BenefitAparatTel", fields: [aparatTelefonId], references: [id]) cardCompanie String? automobilServiciu String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("benefits") } enum InventoryItemType { uniforma halat ciupici vesta aparat_telefon alte } model InventoryItem { id String @id @default(uuid()) sku String @unique name String type InventoryItemType size String? color String? pricePerUnit Decimal? @db.Decimal(10, 2) stockQty Int @default(0) active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt uniformaBenefits Benefit[] @relation("BenefitUniforma") halatBenefits Benefit[] @relation("BenefitHalat") ciupiciBenefits Benefit[] @relation("BenefitCiupici") vestaBenefits Benefit[] @relation("BenefitVesta") aparatTelBenefits Benefit[] @relation("BenefitAparatTel") @@index([type, active]) @@map("inventory_items") } // ═══════════════════════════════════════════════════════════════ // PHASE 2 STUB: EMPLOYMENT CONTRACT // ═══════════════════════════════════════════════════════════════ model EmploymentContract { id String @id @default(uuid()) nrCim String @unique employeeId String employee Employee @relation(fields: [employeeId], references: [id]) categorie ContractCategory dataSemnarii DateTime @db.Date dataAngajarii DateTime @db.Date dataDemisiei DateTime? @db.Date perioada ContractPeriod dataTerminarii DateTime? @db.Date functiaClasificator String? // CORM код codFunctie String? functiaOrganigrama String? tipCim ContractType departmentId String department Department @relation(fields: [departmentId], references: [id]) regimMunca String? tipSalarizare SalaryType? // Условные поля salariu_fix / pe_ore / in_acord хранятся как JSONB salarizareDetails Json? clausaAditionala Json? workScheduleId String? workSchedule WorkSchedule? @relation(fields: [workScheduleId], references: [id]) categoriiServicii CimServiceCategory[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Бизнес-правило: zile_concediu = MAX среди всех CIM сотрудника — проверка на service-уровне @@index([employeeId]) @@index([departmentId]) @@index([dataDemisiei]) @@map("employment_contracts") } model CimServiceCategory { id String @id @default(uuid()) contractId String contract EmploymentContract @relation(fields: [contractId], references: [id], onDelete: Cascade) categorieId String tipRemunerare String // 'tarif' | 'procent' sumaNeta Decimal? @db.Decimal(10, 2) procent Decimal? @db.Decimal(5, 2) @@index([contractId]) @@map("cim_service_categories") } // ═══════════════════════════════════════════════════════════════ // PHASE 4 STUB: PERFORMANCE EVALUATION // ═══════════════════════════════════════════════════════════════ model EvaluationCampaign { id String @id @default(uuid()) name String departmentId String department Department @relation(fields: [departmentId], references: [id]) month DateTime @db.Date // первый день месяца кампании status CampaignStatus @default(draft) forms EvaluationForm[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([departmentId]) @@index([month]) @@map("evaluation_campaigns") } model EvaluationForm { id String @id @default(uuid()) campaignId String campaign EvaluationCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) employeeId String employee Employee @relation(fields: [employeeId], references: [id]) // A. Competente clinice (slab/mediu/bine) abilitatiClinice EvaluationScore? judecataClinica EvaluationScore? manopere EvaluationScore? gestionareaSarcinilor EvaluationScore? // B. Comunicare si empatie constiintaProfesionala EvaluationScore? atitudineaPacienti EvaluationScore? atitudineaColegi EvaluationScore? atitudineaPersonalNonMed EvaluationScore? // C. Disciplina utilizareSmartphone EvaluationScore? respectareaProgramului EvaluationScore? respectareaDressCode EvaluationScore? // D. Documentatie si complianta testJci Json? // { score, max_score, percent, completed_at, source, external_id } completareaDocMed Boolean? perfectioneazaCunostinte Boolean? // E. Candidat EXPERT (Da/Nu) membruComitetCalitate Boolean? functieDeMonitor Boolean? inlocuiesteSuperiorul Boolean? // F. Verdict final categorieCalculata ProposedCategory? categorieAprobata ProposedCategory? // override de nursing_director observatii String? completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([campaignId, employeeId]) @@index([campaignId]) @@index([employeeId]) @@map("evaluation_forms") } // ═══════════════════════════════════════════════════════════════ // PHASE 5 STUB: MEDICAL CONTROL // ═══════════════════════════════════════════════════════════════ model WorkplaceRiskCard { id String @id @default(uuid()) name String @unique // "Medic profil chirurgical cu gărzi de noapte" riskFactors Json? // legacy: { chimici, fizici, biologici, ergonomici, psihosociali } profiles EmployeeMedicalProfile[] // ── Anexa 4 — antet (Fișa de evaluare a riscurilor profesionale) ── filiala String? adresaFiliala String? telefonFiliala String? caemPrimeleDouaCifre String? cormSubgrupaMajora String? directiaSectiaSectorul String? numarulLoculuiDeMunca String? caemDiviziune String? clasaConditiilorDeMunca String? numarLucratoriPosibili Int? // STANDARD (Anexa 4) | DISTANTA_DIGITAL (Anexa 4A — muncă la distanță/platforme digitale) tipFisa String @default("STANDARD") // ── Anexa 4 — bloc descriptiv (checkbox-uri / descrieri) ── evaluareDetalii Json? // ── Anexa 4 — radiații ionizante (per loc de muncă) ── radiatiiIonizante Boolean @default(false) radiatiiGrupa String? // A | B radiatiiAparatura String? radiatiiSurse String? // inchise | deschise radiatiiTipExpunere String? // X externă | gamma externă | internă | externă și internă radiatiiMasuriProtectie String? // ── Anexa 4 — subsol ── mijloaceProtectieColectiva String? mijloaceProtectieIndividuala String? echipamentLucru String? observatii String? anexeIgienicoSanitare Json? // { vestiar, chiuveta, wc, dus, salaMese, recreere } exposures WorkplaceRiskExposure[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("workplace_risk_cards") } // Rând din tabelele factoriale ale Anexei 4 model WorkplaceRiskExposure { id String @id @default(uuid()) cardId String card WorkplaceRiskCard @relation(fields: [cardId], references: [id], onDelete: Cascade) tip RiskExposureType denumire String cas String? // doar chimic / pulberi einecs String? // doar chimic / pulberi clasificare String? // doar agent biologic zonaAfectata String? // vibrații / câmp EM / radiații optice timpExpunere String? vep String? // valoarea de expunere profesională vlep String? // valoarea-limită de expunere profesională obligatorie caracteristici String? procesVerbal String? createdAt DateTime @default(now()) @@index([cardId]) @@map("workplace_risk_exposures") } model EmployeeMedicalProfile { id String @id @default(uuid()) employeeId String @unique employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) ocupatieCorm String? workplaceRiskCardId String? workplaceRiskCard WorkplaceRiskCard? @relation(fields: [workplaceRiskCardId], references: [id]) dataUltimControlMedical DateTime? @db.Date // Câmpuri radiații ionizante expusRadiatiiIonizante Boolean @default(false) dataIntrarii DateTime? @db.Date expunereAnterioaraPerioda String? // se completează o singură dată la angajare expunereAnterioaraAni Int? dozaCumulataExternaMsv Decimal? @db.Decimal(10, 4) dozaCumulataInternaMsv Decimal? @db.Decimal(10, 4) // dozaTotalaMsv = externa + interna — câmp calculat, nu stocat // Supraexpuneri excepționale/accidentale (Anexa 4B) overexposures RadiationOverexposure[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("employee_medical_profiles") } // Supraexpunere la radiații ionizante — rând din Anexa 4B (per lucrător) model RadiationOverexposure { id String @id @default(uuid()) medicalProfileId String medicalProfile EmployeeMedicalProfile @relation(fields: [medicalProfileId], references: [id], onDelete: Cascade) fel OverexposureKind // EXCEPTIONALA | ACCIDENTALA tipExpunere String? // X externă | gamma externă | internă | externă și internă data DateTime? @db.Date dozaMsv Decimal? @db.Decimal(10, 4) createdAt DateTime @default(now()) @@index([medicalProfileId]) @@map("radiation_overexposures") } model MedicalCheckup { id String @id @default(uuid()) employeeId String employee Employee @relation(fields: [employeeId], references: [id]) tip MedicalCheckupType dataPlanificata DateTime @db.Date dataEfectuata DateTime? @db.Date verdict MedicalVerdict? recomandari String? valabilPanaLa DateTime? @db.Date semnatDe String? // Ссылки на S3-документы: [{ name, url, type }] documenteGenerate Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([employeeId]) @@index([dataPlanificata]) @@map("medical_checkups") } // ═══════════════════════════════════════════════════════════════ // AUDIT LOG — append-only, 5 ani retentie // ═══════════════════════════════════════════════════════════════ model AuditLog { id BigInt @id @default(autoincrement()) ts DateTime @default(now()) userId String userRole String @db.VarChar(50) ip String? @db.VarChar(45) // IPv4 или IPv6 action String @db.VarChar(20) // READ | CREATE | UPDATE | DELETE | EXPORT entity String @db.VarChar(50) entityId String @db.VarChar(50) field String? @db.VarChar(100) // PII-значения шифруются на app-уровне (pgcrypto / KMS) перед записью oldValue String? newValue String? reason String? // обязателен для READ медицинских данных (GDPR) @@index([userId]) @@index([entity, entityId]) @@index([ts]) @@map("audit_logs") } // ═══════════════════════════════════════════════════════════════ // ANEXA TEMPLATE EDITOR // ═══════════════════════════════════════════════════════════════ model AnexaTemplate { id String @id @default(uuid()) type AnexaType @unique name String contentJson Json updatedById String updatedAt DateTime @updatedAt versions AnexaTemplateVersion[] @@map("anexa_templates") } model AnexaTemplateVersion { id String @id @default(uuid()) templateId String template AnexaTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) contentJson Json savedById String savedAt DateTime @default(now()) label String? @@index([templateId]) @@map("anexa_template_versions") }