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
@@ -0,0 +1,578 @@
-- CreateEnum
CREATE TYPE "Sex" AS ENUM ('F', 'M');
-- CreateEnum
CREATE TYPE "MaritalStatus" AS ENUM ('casatorit', 'necasatorit', 'divortat', 'vaduv');
-- CreateEnum
CREATE TYPE "EmployeeStatus" AS ENUM ('activ', 'concediat', 'suspendat');
-- CreateEnum
CREATE TYPE "DocumentType" AS ENUM ('buletin_de_identitate', 'pasaport');
-- CreateEnum
CREATE TYPE "FamilyMemberType" AS ENUM ('contact_principal', 'sot', 'sotie', 'mama', 'tata', 'copil');
-- CreateEnum
CREATE TYPE "StudyType" AS ENUM ('superioare', 'medii_de_specialitate', 'secundare_tehnice', 'medii');
-- CreateEnum
CREATE TYPE "StudyLevel" AS ENUM ('de_baza', 'postuniversitar');
-- CreateEnum
CREATE TYPE "PostUniversityType" AS ENUM ('masterat', 'rezidentiat', 'secundariat', 'altele');
-- CreateEnum
CREATE TYPE "DiplomaStatus" AS ENUM ('confirmata', 'neconfirmata');
-- CreateEnum
CREATE TYPE "QualificationCategory" AS ENUM ('fara', 'cat_II', 'cat_I', 'superioara');
-- CreateEnum
CREATE TYPE "ScientificTitle" AS ENUM ('doctor', 'doctor_habilitat');
-- CreateEnum
CREATE TYPE "TrainingType" AS ENUM ('orientare', 'intern', 'extern_RM', 'extern_international');
-- CreateEnum
CREATE TYPE "DisciplinarySanctionType" AS ENUM ('avertisment', 'mustrare', 'mustrare_aspra');
-- CreateEnum
CREATE TYPE "ContractPeriod" AS ENUM ('determinata', 'nedeterminata', 'replasare_temporara');
-- CreateEnum
CREATE TYPE "ContractCategory" AS ENUM ('principal', 'secundar');
-- CreateEnum
CREATE TYPE "ContractType" AS ENUM ('de_baza', 'cumul');
-- CreateEnum
CREATE TYPE "SalaryType" AS ENUM ('fix', 'pe_ore', 'in_acord');
-- CreateEnum
CREATE TYPE "CampaignStatus" AS ENUM ('draft', 'scheduled', 'in_progress', 'closed');
-- CreateEnum
CREATE TYPE "EvaluationScore" AS ENUM ('slab', 'mediu', 'bine');
-- CreateEnum
CREATE TYPE "ProposedCategory" AS ENUM ('fara', 'cat_II', 'cat_I', 'superioara');
-- CreateEnum
CREATE TYPE "MedicalCheckupType" AS ENUM ('la_angajare', 'periodic', 'la_reluarea_activitatii', 'la_incetarea_expunerii', 'suplimentar');
-- CreateEnum
CREATE TYPE "MedicalVerdict" AS ENUM ('apt', 'apt_perioada_adaptare', 'apt_conditionat', 'inapt_temporar', 'inapt');
-- CreateTable
CREATE TABLE "disability_grades" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "disability_grades_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tax_exemptions" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"description" TEXT NOT NULL,
CONSTRAINT "tax_exemptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "work_schedules" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"daysWork" INTEGER NOT NULL,
"daysRest" INTEGER NOT NULL,
"hoursPerDay" INTEGER NOT NULL,
CONSTRAINT "work_schedules_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "departments" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"code" TEXT,
"parentId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "departments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "employees" (
"id" TEXT NOT NULL,
"idnp" VARCHAR(13) NOT NULL,
"nume" TEXT NOT NULL,
"prenume" TEXT NOT NULL,
"patronimic" TEXT,
"numeAnterior" TEXT,
"dataNasterii" DATE NOT NULL,
"domiciliu" TEXT NOT NULL,
"adresaReala" TEXT,
"telefonPersonal" TEXT NOT NULL,
"telefonServiciu" TEXT,
"emailPersonal" TEXT,
"emailCorporativ" TEXT,
"sex" "Sex" NOT NULL,
"codCpas" TEXT,
"stareCivila" "MaritalStatus",
"titluStiintific" "ScientificTitle",
"titluUniversitar" TEXT,
"status" "EmployeeStatus" NOT NULL DEFAULT 'activ',
"gradDizabilitateId" TEXT,
"recomandareInternaId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "employees_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "identity_documents" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"tipAct" "DocumentType" NOT NULL,
"seria" TEXT,
"nr" TEXT NOT NULL,
"dataEmiterii" DATE NOT NULL,
"autoritateEmitenta" TEXT NOT NULL,
"dataExpirarii" DATE NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "identity_documents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "family_members" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"tip" "FamilyMemberType" NOT NULL,
"numePrenume" TEXT NOT NULL,
"dataNasterii" DATE,
"idnp" VARCHAR(13),
"telefon" TEXT,
"tipScutireId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "family_members_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "educations" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"tipStudii" "StudyType" NOT NULL,
"institutia" TEXT NOT NULL,
"specialitatea" TEXT NOT NULL,
"dataAbsolvirii" DATE,
"nrSeriaDiploma" TEXT,
"dataEmiterii" DATE,
"nrInregistrare" TEXT,
"confirmare" "DiplomaStatus",
"nivel" "StudyLevel",
"tipPostuniversitar" "PostUniversityType",
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "educations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "qualifications" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"categorie" "QualificationCategory" NOT NULL,
"dataObtinerii" DATE,
"dataUltimeiConfirmari" DATE,
"dataExpirarii" DATE,
"specialitate" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "qualifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "trainings" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"denumire" TEXT NOT NULL,
"inceput" DATE NOT NULL,
"sfirsit" DATE,
"tip" "TrainingType" NOT NULL,
"tara" TEXT,
"nrOre" INTEGER,
"organizatia" TEXT,
"certificat" BOOLEAN NOT NULL DEFAULT false,
"cost" DECIMAL(10,2),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "trainings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "disciplinary_sanctions" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"tip" "DisciplinarySanctionType" NOT NULL,
"dataAplicarii" DATE NOT NULL,
"dataExpirarii" DATE NOT NULL,
"isStinsa" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "disciplinary_sanctions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "benefits" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"uniformaId" TEXT,
"halatId" TEXT,
"ciupiciId" TEXT,
"vestaId" TEXT,
"ticheteMasa" BOOLEAN NOT NULL DEFAULT false,
"valoareTichet" DECIMAL(10,2),
"alimentatiePersonal" BOOLEAN NOT NULL DEFAULT false,
"abonamentTel" DECIMAL(10,2),
"aparatTelefonId" TEXT,
"cardCompanie" TEXT,
"automobilServiciu" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "benefits_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "employment_contracts" (
"id" TEXT NOT NULL,
"nrCim" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"categorie" "ContractCategory" NOT NULL,
"dataSemnarii" DATE NOT NULL,
"dataAngajarii" DATE NOT NULL,
"dataDemisiei" DATE,
"perioada" "ContractPeriod" NOT NULL,
"dataTerminarii" DATE,
"functiaClasificator" TEXT,
"codFunctie" TEXT,
"functiaOrganigrama" TEXT,
"tipCim" "ContractType" NOT NULL,
"departmentId" TEXT NOT NULL,
"regimMunca" TEXT,
"tipSalarizare" "SalaryType",
"salarizareDetails" JSONB,
"clausaAditionala" JSONB,
"workScheduleId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "employment_contracts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cim_service_categories" (
"id" TEXT NOT NULL,
"contractId" TEXT NOT NULL,
"categorieId" TEXT NOT NULL,
"tipRemunerare" TEXT NOT NULL,
"sumaNeta" DECIMAL(10,2),
"procent" DECIMAL(5,2),
CONSTRAINT "cim_service_categories_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "evaluation_campaigns" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"departmentId" TEXT NOT NULL,
"month" DATE NOT NULL,
"status" "CampaignStatus" NOT NULL DEFAULT 'draft',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "evaluation_campaigns_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "evaluation_forms" (
"id" TEXT NOT NULL,
"campaignId" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"abilitatiClinice" "EvaluationScore",
"judecataClinica" "EvaluationScore",
"manopere" "EvaluationScore",
"gestionareaSarcinilor" "EvaluationScore",
"constiintaProfesionala" "EvaluationScore",
"atitudineaPacienti" "EvaluationScore",
"atitudineaColegi" "EvaluationScore",
"atitudineaPersonalNonMed" "EvaluationScore",
"utilizareSmartphone" "EvaluationScore",
"respectareaProgramului" "EvaluationScore",
"respectareaDressCode" "EvaluationScore",
"testJci" JSONB,
"completareaDocMed" BOOLEAN,
"perfectioneazaCunostinte" BOOLEAN,
"membruComitetCalitate" BOOLEAN,
"functieDeMonitor" BOOLEAN,
"inlocuiesteSuperiorul" BOOLEAN,
"categorieCalculata" "ProposedCategory",
"categorieAprobata" "ProposedCategory",
"observatii" TEXT,
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "evaluation_forms_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workplace_risk_cards" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"riskFactors" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workplace_risk_cards_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "employee_medical_profiles" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"ocupatieCorm" TEXT,
"workplaceRiskCardId" TEXT,
"dataUltimControlMedical" DATE,
"expusRadiatiiIonizante" BOOLEAN NOT NULL DEFAULT false,
"dataIntrarii" DATE,
"expunereAnterioaraPerioda" TEXT,
"expunereAnterioaraAni" INTEGER,
"dozaCumulataExternaMsv" DECIMAL(10,4),
"dozaCumulataInternaMsv" DECIMAL(10,4),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "employee_medical_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "medical_checkups" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"tip" "MedicalCheckupType" NOT NULL,
"dataPlanificata" DATE NOT NULL,
"dataEfectuata" DATE,
"verdict" "MedicalVerdict",
"recomandari" TEXT,
"documenteGenerate" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "medical_checkups_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "audit_logs" (
"id" BIGSERIAL NOT NULL,
"ts" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"userRole" VARCHAR(50) NOT NULL,
"ip" VARCHAR(45),
"action" VARCHAR(20) NOT NULL,
"entity" VARCHAR(50) NOT NULL,
"entityId" VARCHAR(50) NOT NULL,
"field" VARCHAR(100),
"oldValue" TEXT,
"newValue" TEXT,
"reason" TEXT,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "disability_grades_code_key" ON "disability_grades"("code");
-- CreateIndex
CREATE UNIQUE INDEX "tax_exemptions_code_key" ON "tax_exemptions"("code");
-- CreateIndex
CREATE UNIQUE INDEX "work_schedules_name_key" ON "work_schedules"("name");
-- CreateIndex
CREATE UNIQUE INDEX "departments_code_key" ON "departments"("code");
-- CreateIndex
CREATE INDEX "departments_parentId_idx" ON "departments"("parentId");
-- CreateIndex
CREATE UNIQUE INDEX "employees_idnp_key" ON "employees"("idnp");
-- CreateIndex
CREATE INDEX "employees_idnp_idx" ON "employees"("idnp");
-- CreateIndex
CREATE INDEX "employees_nume_prenume_idx" ON "employees"("nume", "prenume");
-- CreateIndex
CREATE INDEX "employees_status_idx" ON "employees"("status");
-- CreateIndex
CREATE INDEX "employees_dataNasterii_idx" ON "employees"("dataNasterii");
-- CreateIndex
CREATE INDEX "identity_documents_employeeId_idx" ON "identity_documents"("employeeId");
-- CreateIndex
CREATE INDEX "identity_documents_dataExpirarii_idx" ON "identity_documents"("dataExpirarii");
-- CreateIndex
CREATE INDEX "family_members_employeeId_idx" ON "family_members"("employeeId");
-- CreateIndex
CREATE INDEX "educations_employeeId_idx" ON "educations"("employeeId");
-- CreateIndex
CREATE INDEX "qualifications_employeeId_idx" ON "qualifications"("employeeId");
-- CreateIndex
CREATE INDEX "qualifications_dataExpirarii_idx" ON "qualifications"("dataExpirarii");
-- CreateIndex
CREATE INDEX "trainings_employeeId_idx" ON "trainings"("employeeId");
-- CreateIndex
CREATE INDEX "disciplinary_sanctions_employeeId_idx" ON "disciplinary_sanctions"("employeeId");
-- CreateIndex
CREATE INDEX "disciplinary_sanctions_dataExpirarii_idx" ON "disciplinary_sanctions"("dataExpirarii");
-- CreateIndex
CREATE UNIQUE INDEX "benefits_employeeId_key" ON "benefits"("employeeId");
-- CreateIndex
CREATE UNIQUE INDEX "employment_contracts_nrCim_key" ON "employment_contracts"("nrCim");
-- CreateIndex
CREATE INDEX "employment_contracts_employeeId_idx" ON "employment_contracts"("employeeId");
-- CreateIndex
CREATE INDEX "employment_contracts_departmentId_idx" ON "employment_contracts"("departmentId");
-- CreateIndex
CREATE INDEX "employment_contracts_dataDemisiei_idx" ON "employment_contracts"("dataDemisiei");
-- CreateIndex
CREATE INDEX "cim_service_categories_contractId_idx" ON "cim_service_categories"("contractId");
-- CreateIndex
CREATE INDEX "evaluation_campaigns_departmentId_idx" ON "evaluation_campaigns"("departmentId");
-- CreateIndex
CREATE INDEX "evaluation_campaigns_month_idx" ON "evaluation_campaigns"("month");
-- CreateIndex
CREATE INDEX "evaluation_forms_campaignId_idx" ON "evaluation_forms"("campaignId");
-- CreateIndex
CREATE INDEX "evaluation_forms_employeeId_idx" ON "evaluation_forms"("employeeId");
-- CreateIndex
CREATE UNIQUE INDEX "evaluation_forms_campaignId_employeeId_key" ON "evaluation_forms"("campaignId", "employeeId");
-- CreateIndex
CREATE UNIQUE INDEX "workplace_risk_cards_name_key" ON "workplace_risk_cards"("name");
-- CreateIndex
CREATE UNIQUE INDEX "employee_medical_profiles_employeeId_key" ON "employee_medical_profiles"("employeeId");
-- CreateIndex
CREATE INDEX "medical_checkups_employeeId_idx" ON "medical_checkups"("employeeId");
-- CreateIndex
CREATE INDEX "medical_checkups_dataPlanificata_idx" ON "medical_checkups"("dataPlanificata");
-- CreateIndex
CREATE INDEX "audit_logs_userId_idx" ON "audit_logs"("userId");
-- CreateIndex
CREATE INDEX "audit_logs_entity_entityId_idx" ON "audit_logs"("entity", "entityId");
-- CreateIndex
CREATE INDEX "audit_logs_ts_idx" ON "audit_logs"("ts");
-- AddForeignKey
ALTER TABLE "departments" ADD CONSTRAINT "departments_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "departments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "employees" ADD CONSTRAINT "employees_gradDizabilitateId_fkey" FOREIGN KEY ("gradDizabilitateId") REFERENCES "disability_grades"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "employees" ADD CONSTRAINT "employees_recomandareInternaId_fkey" FOREIGN KEY ("recomandareInternaId") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "identity_documents" ADD CONSTRAINT "identity_documents_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "family_members" ADD CONSTRAINT "family_members_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "family_members" ADD CONSTRAINT "family_members_tipScutireId_fkey" FOREIGN KEY ("tipScutireId") REFERENCES "tax_exemptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "educations" ADD CONSTRAINT "educations_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "qualifications" ADD CONSTRAINT "qualifications_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "trainings" ADD CONSTRAINT "trainings_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "disciplinary_sanctions" ADD CONSTRAINT "disciplinary_sanctions_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "benefits" ADD CONSTRAINT "benefits_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "employment_contracts" ADD CONSTRAINT "employment_contracts_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "employment_contracts" ADD CONSTRAINT "employment_contracts_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "departments"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "employment_contracts" ADD CONSTRAINT "employment_contracts_workScheduleId_fkey" FOREIGN KEY ("workScheduleId") REFERENCES "work_schedules"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "cim_service_categories" ADD CONSTRAINT "cim_service_categories_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "employment_contracts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "evaluation_campaigns" ADD CONSTRAINT "evaluation_campaigns_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "departments"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "evaluation_forms" ADD CONSTRAINT "evaluation_forms_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "evaluation_campaigns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "evaluation_forms" ADD CONSTRAINT "evaluation_forms_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "employee_medical_profiles" ADD CONSTRAINT "employee_medical_profiles_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "employee_medical_profiles" ADD CONSTRAINT "employee_medical_profiles_workplaceRiskCardId_fkey" FOREIGN KEY ("workplaceRiskCardId") REFERENCES "workplace_risk_cards"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "medical_checkups" ADD CONSTRAINT "medical_checkups_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,35 @@
-- CreateEnum
CREATE TYPE "AnexaType" AS ENUM ('ANEXA_3', 'ANEXA_4', 'ANEXA_4B', 'ANEXA_6');
-- CreateTable
CREATE TABLE "anexa_templates" (
"id" TEXT NOT NULL,
"type" "AnexaType" NOT NULL,
"name" TEXT NOT NULL,
"contentJson" JSONB NOT NULL,
"updatedById" TEXT NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "anexa_templates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "anexa_template_versions" (
"id" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
"contentJson" JSONB NOT NULL,
"savedById" TEXT NOT NULL,
"savedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"label" TEXT,
CONSTRAINT "anexa_template_versions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "anexa_templates_type_key" ON "anexa_templates"("type");
-- CreateIndex
CREATE INDEX "anexa_template_versions_templateId_idx" ON "anexa_template_versions"("templateId");
-- AddForeignKey
ALTER TABLE "anexa_template_versions" ADD CONSTRAINT "anexa_template_versions_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "anexa_templates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,47 @@
-- CreateEnum
CREATE TYPE "InventoryItemType" AS ENUM ('uniforma', 'halat', 'ciupici', 'vesta', 'aparat_telefon', 'alte');
-- CreateTable
CREATE TABLE "inventory_items" (
"id" TEXT NOT NULL,
"sku" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" "InventoryItemType" NOT NULL,
"size" TEXT,
"color" TEXT,
"pricePerUnit" DECIMAL(10,2),
"stockQty" INTEGER NOT NULL DEFAULT 0,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "inventory_items_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "inventory_items_sku_key" ON "inventory_items"("sku");
-- CreateIndex
CREATE INDEX "inventory_items_type_active_idx" ON "inventory_items"("type", "active");
-- Reset existing free-text IDs in benefits (no FK referenced anything real before)
UPDATE "benefits" SET "uniformaId" = NULL WHERE "uniformaId" IS NOT NULL;
UPDATE "benefits" SET "halatId" = NULL WHERE "halatId" IS NOT NULL;
UPDATE "benefits" SET "ciupiciId" = NULL WHERE "ciupiciId" IS NOT NULL;
UPDATE "benefits" SET "vestaId" = NULL WHERE "vestaId" IS NOT NULL;
UPDATE "benefits" SET "aparatTelefonId" = NULL WHERE "aparatTelefonId" IS NOT NULL;
-- AddForeignKey
ALTER TABLE "benefits" ADD CONSTRAINT "benefits_uniformaId_fkey" FOREIGN KEY ("uniformaId") REFERENCES "inventory_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "benefits" ADD CONSTRAINT "benefits_halatId_fkey" FOREIGN KEY ("halatId") REFERENCES "inventory_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "benefits" ADD CONSTRAINT "benefits_ciupiciId_fkey" FOREIGN KEY ("ciupiciId") REFERENCES "inventory_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "benefits" ADD CONSTRAINT "benefits_vestaId_fkey" FOREIGN KEY ("vestaId") REFERENCES "inventory_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "benefits" ADD CONSTRAINT "benefits_aparatTelefonId_fkey" FOREIGN KEY ("aparatTelefonId") REFERENCES "inventory_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,52 @@
-- CreateEnum
CREATE TYPE "RiskExposureType" AS ENUM ('AGENT_CHIMIC', 'PULBERI', 'AGENT_BIOLOGIC', 'ZGOMOT', 'VIBRATII', 'CAMP_ELECTROMAGNETIC', 'RADIATII_OPTICE');
-- AlterTable
ALTER TABLE "workplace_risk_cards" ADD COLUMN "adresaFiliala" TEXT,
ADD COLUMN "anexeIgienicoSanitare" JSONB,
ADD COLUMN "caemDiviziune" TEXT,
ADD COLUMN "caemPrimeleDouaCifre" TEXT,
ADD COLUMN "clasaConditiilorDeMunca" TEXT,
ADD COLUMN "cormSubgrupaMajora" TEXT,
ADD COLUMN "directiaSectiaSectorul" TEXT,
ADD COLUMN "echipamentLucru" TEXT,
ADD COLUMN "evaluareDetalii" JSONB,
ADD COLUMN "filiala" TEXT,
ADD COLUMN "mijloaceProtectieColectiva" TEXT,
ADD COLUMN "mijloaceProtectieIndividuala" TEXT,
ADD COLUMN "numarLucratoriPosibili" INTEGER,
ADD COLUMN "numarulLoculuiDeMunca" TEXT,
ADD COLUMN "observatii" TEXT,
ADD COLUMN "radiatiiAparatura" TEXT,
ADD COLUMN "radiatiiGrupa" TEXT,
ADD COLUMN "radiatiiIonizante" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "radiatiiMasuriProtectie" TEXT,
ADD COLUMN "radiatiiSurse" TEXT,
ADD COLUMN "radiatiiTipExpunere" TEXT,
ALTER COLUMN "riskFactors" DROP NOT NULL;
-- CreateTable
CREATE TABLE "workplace_risk_exposures" (
"id" TEXT NOT NULL,
"cardId" TEXT NOT NULL,
"tip" "RiskExposureType" NOT NULL,
"denumire" TEXT NOT NULL,
"cas" TEXT,
"einecs" TEXT,
"clasificare" TEXT,
"zonaAfectata" TEXT,
"timpExpunere" TEXT,
"vep" TEXT,
"vlep" TEXT,
"caracteristici" TEXT,
"procesVerbal" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workplace_risk_exposures_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "workplace_risk_exposures_cardId_idx" ON "workplace_risk_exposures"("cardId");
-- AddForeignKey
ALTER TABLE "workplace_risk_exposures" ADD CONSTRAINT "workplace_risk_exposures_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "workplace_risk_cards"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,27 @@
-- AlterEnum
ALTER TYPE "AnexaType" ADD VALUE 'ANEXA_4A';
-- CreateEnum
CREATE TYPE "OverexposureKind" AS ENUM ('EXCEPTIONALA', 'ACCIDENTALA');
-- AlterTable
ALTER TABLE "workplace_risk_cards" ADD COLUMN "tipFisa" TEXT NOT NULL DEFAULT 'STANDARD';
-- CreateTable
CREATE TABLE "radiation_overexposures" (
"id" TEXT NOT NULL,
"medicalProfileId" TEXT NOT NULL,
"fel" "OverexposureKind" NOT NULL,
"tipExpunere" TEXT,
"data" DATE,
"dozaMsv" DECIMAL(10,4),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "radiation_overexposures_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "radiation_overexposures_medicalProfileId_idx" ON "radiation_overexposures"("medicalProfileId");
-- AddForeignKey
ALTER TABLE "radiation_overexposures" ADD CONSTRAINT "radiation_overexposures_medicalProfileId_fkey" FOREIGN KEY ("medicalProfileId") REFERENCES "employee_medical_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,6 @@
ALTER TABLE "workplace_risk_cards"
ADD COLUMN "telefonFiliala" TEXT;
ALTER TABLE "medical_checkups"
ADD COLUMN "valabilPanaLa" DATE,
ADD COLUMN "semnatDe" TEXT;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
+829
View File
@@ -0,0 +1,829 @@
// 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")
}
+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());