Files
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

830 lines
28 KiB
Plaintext

// 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")
}