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:
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Generează BOLĂVANKE (stub) .docx pentru Anexele 3/4/4A/4B/6 cu TOATE placeholder-ele
|
||||
* docxtemplater din `templates/docx/README.md`. Formatarea o ajustați apoi în Word.
|
||||
*
|
||||
* Rulare: pnpm --filter api exec ts-node scripts/generate-docx-stubs.ts
|
||||
*/
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
|
||||
HeadingLevel, WidthType, BorderStyle,
|
||||
} from 'docx';
|
||||
|
||||
const OUT = join(__dirname, '..', 'templates', 'docx');
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
// ── helpers ──
|
||||
const T = (text: string, bold = false) => new TextRun({ text, bold });
|
||||
const ph = (name: string) => new TextRun({ text: `{${name}}`, bold: true, color: '0B6E70' });
|
||||
const P = (...children: TextRun[]) => new Paragraph({ children });
|
||||
const H = (text: string, level: (typeof HeadingLevel)[keyof typeof HeadingLevel] = HeadingLevel.HEADING_2) =>
|
||||
new Paragraph({ heading: level, children: [T(text, true)] });
|
||||
const empty = () => new Paragraph({ children: [] });
|
||||
// "Label {ph}"
|
||||
const line = (label: string, name: string) => P(T(label + ' '), ph(name));
|
||||
// checkbox: "{cbX} Label"
|
||||
const cb = (name: string, label: string) => [ph(name), T(' ' + label + ' ')];
|
||||
const cbLine = (...pairs: [string, string][]) =>
|
||||
P(...pairs.flatMap(([n, l]) => cb(n, l)));
|
||||
|
||||
const BORDER = {
|
||||
top: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
|
||||
bottom: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
|
||||
left: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
|
||||
right: { style: BorderStyle.SINGLE, size: 1, color: '999999' },
|
||||
};
|
||||
const cell = (children: Paragraph[]) => new TableCell({ children, borders: BORDER });
|
||||
const headerRow = (labels: string[]) =>
|
||||
new TableRow({ children: labels.map((l) => cell([P(T(l, true))])) });
|
||||
|
||||
/**
|
||||
* Tabel repetabil: rândul-șablon repetă pentru fiecare element din `loop`.
|
||||
* `{#loop}` în prima celulă, `{/loop}` în ultima.
|
||||
*/
|
||||
function loopTable(loop: string, headers: string[], rowFields: string[]): Table {
|
||||
const tplCells = rowFields.map((f, i) => {
|
||||
const runs: TextRun[] = [];
|
||||
if (i === 0) runs.push(new TextRun({ text: `{#${loop}}`, bold: true, color: 'B11116' }));
|
||||
runs.push(ph(f));
|
||||
if (i === rowFields.length - 1) runs.push(new TextRun({ text: `{/${loop}}`, bold: true, color: 'B11116' }));
|
||||
return cell([P(...runs)]);
|
||||
});
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [headerRow(headers), new TableRow({ children: tplCells })],
|
||||
});
|
||||
}
|
||||
|
||||
function save(name: string, children: (Paragraph | Table)[]) {
|
||||
const doc = new Document({ sections: [{ children }] });
|
||||
return Packer.toBuffer(doc).then((buf) => {
|
||||
writeFileSync(join(OUT, name), buf);
|
||||
console.log(' ✓', name, `(${buf.length} bytes)`);
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════ ANEXA 3 ════════════════════════
|
||||
const anexa3: (Paragraph | Table)[] = [
|
||||
H('FIȘA de solicitare a examenului medical'),
|
||||
line('Unitatea economică/instituția:', 'unitatea'),
|
||||
P(T('IDNO: '), ph('idno'), T(' Adresa: '), ph('adresa')),
|
||||
P(T('Telefon: '), ph('telefon'), T(' Fax: '), ph('fax'), T(' E-mail: '), ph('email')),
|
||||
P(T('Filiala: '), ph('filiala'), T(' Adresa filialei: '), ph('adresaFiliala'), T(' Telefon: '), ph('telefonFiliala')),
|
||||
empty(),
|
||||
loopTable('angajati',
|
||||
['Nr.', 'Numele și prenumele', 'Anul nașterii', 'IDNP', 'Tipul examenului', 'Ocupația (CORM)', 'CAEM', 'Nr. loc muncă', 'Factorul de risc'],
|
||||
['nr', 'numePrenume', 'anNastere', 'idnp', 'tipExamen', 'ocupatieCorm', 'caem', 'numarLoc', 'factorRisc']),
|
||||
empty(),
|
||||
line('Data completării:', 'dataCompletarii'),
|
||||
P(T('Solicitant: '), ph('solicitant'), T(' Funcția: '), ph('functia')),
|
||||
P(T('Semnătura: ____________________')),
|
||||
];
|
||||
|
||||
// ════════════════════════ ANEXA 4 ════════════════════════
|
||||
const anexa4Header: (Paragraph | Table)[] = [
|
||||
line('Unitatea economică/instituția:', 'unitatea'),
|
||||
P(T('Adresa: '), ph('adresa')),
|
||||
P(T('Filiala: '), ph('filiala'), T(' Adresa filialei: '), ph('adresaFiliala'), T(' CAEM (2 cifre): '), ph('caem2')),
|
||||
H('FIȘA de evaluare a riscurilor profesionale'),
|
||||
line('Ocupația (subgrupa majoră CORM):', 'cormSubgrupa'),
|
||||
line('Direcția/secția/sectorul:', 'directiaSectia'),
|
||||
P(T('Numărul locului de muncă: '), ph('numarLoc'), T(' CAEM (diviziune): '), ph('caemDiviziune')),
|
||||
P(T('Nr. lucrători care pot activa: '), ph('numarLucratori'), T(' Clasa condițiilor de muncă: '), ph('clasa')),
|
||||
];
|
||||
const anexa4Descriptiv: (Paragraph | Table)[] = [
|
||||
H('Descrierea activității', HeadingLevel.HEADING_3),
|
||||
P(T('Lucru în echipă '), ph('cbEchipa'), T(' Nr. ore/zi: '), ph('oreZi'), T(' Nr. schimburi: '), ph('schimburi')),
|
||||
cbLine(['cbSchimbNoapte', 'schimb de noapte'], ['cbPauze', 'pauze organizate']),
|
||||
P(T('Riscuri:')),
|
||||
cbLine(['cbInfectare', 'infectare'], ['cbElectrocutare', 'electrocutare'], ['cbTensiuneInalta', 'tensiune înaltă'], ['cbInecare', 'înecare'], ['cbAsfixiere', 'asfixiere']),
|
||||
cbLine(['cbStrivire', 'strivire'], ['cbTaiere', 'tăiere'], ['cbIntepare', 'înțepare'], ['cbLovire', 'lovire'], ['cbMuscatura', 'mușcătură'], ['cbMicrotraumatisme', 'microtraumatisme']),
|
||||
P(T('Conduce mașina '), ph('cbConduceMasina'), T(' categorie: '), ph('categorieConducere'), T(' '), ph('cbUtilajeIntrauzinal'), T(' utilaje intrauzinal')),
|
||||
H('Descrierea spațiului de lucru', HeadingLevel.HEADING_3),
|
||||
P(T('Dimensiuni: L '), ph('spatiuL'), T(' l '), ph('spatiul'), T(' H '), ph('spatiuH'), T(' m')),
|
||||
cbLine(['cbSuprafVerticala', 'suprafață verticală'], ['cbSuprafOrizontala', 'orizontală'], ['cbSuprafOblica', 'oblică']),
|
||||
cbLine(['cbMuncaIzolare', 'în izolare'], ['cbMuncaInaltime', 'la înălțime'], ['cbMuncaMiscare', 'în mișcare']),
|
||||
H('Efort fizic', HeadingLevel.HEADING_3),
|
||||
P(T('Poziție: ')),
|
||||
cbLine(['cbPozitieOrtostatica', 'ortostatică'], ['cbPozitieAsezat', 'așezat'], ['cbPozitieAplecata', 'aplecată'], ['cbPozitieMixta', 'mixtă'], ['cbPozitieFortata', 'forțată']),
|
||||
P(T('Suprasolicitări coloană: ')),
|
||||
cbLine(['cbColoanaCervicala', 'cervicală'], ['cbColoanaToracala', 'toracală'], ['cbColoanaLombara', 'lombară']),
|
||||
P(T('Manipulare manuală: ')),
|
||||
cbLine(['cbManipRidicare', 'ridicare'], ['cbManipCoborare', 'coborâre'], ['cbManipImpingere', 'împingere'], ['cbManipTragere', 'tragere'], ['cbManipPurtare', 'purtare'], ['cbManipDeplasare', 'deplasare']),
|
||||
P(T('Greutate maximă manipulată: '), ph('greutateMaxima')),
|
||||
cbLine(['cbVizuale', 'suprasolicitări vizuale'], ['cbAuditive', 'auditive'], ['cbNeuropsihice', 'neuropsihice']),
|
||||
];
|
||||
const factorTables: (Paragraph | Table)[] = [
|
||||
H('AGENȚI CHIMICI', HeadingLevel.HEADING_3),
|
||||
loopTable('chimici', ['Agentul chimic', 'CAS', 'EINECS', 'Timp', 'VEP', 'VLEP', 'Caracteristici'],
|
||||
['denumire', 'cas', 'einecs', 'timp', 'vep', 'vlep', 'caracteristici']),
|
||||
H('PULBERI', HeadingLevel.HEADING_3),
|
||||
loopTable('pulberi', ['Pulberi', 'CAS', 'EINECS', 'Timp', 'VEP', 'VLEP', 'Caracteristici'],
|
||||
['denumire', 'cas', 'einecs', 'timp', 'vep', 'vlep', 'caracteristici']),
|
||||
H('AGENȚI BIOLOGICI', HeadingLevel.HEADING_3),
|
||||
loopTable('biologici', ['Agent biologic', 'Clasificare', 'Note'], ['denumire', 'clasificare', 'note']),
|
||||
H('ZGOMOT PROFESIONAL', HeadingLevel.HEADING_3),
|
||||
loopTable('zgomot', ['Tipul', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'timp', 'vep', 'vlep', 'caracteristici']),
|
||||
H('VIBRAȚII MECANICE', HeadingLevel.HEADING_3),
|
||||
loopTable('vibratii', ['Tipul', 'Zona', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'zona', 'timp', 'vep', 'vlep', 'caracteristici']),
|
||||
H('CÂMP ELECTROMAGNETIC', HeadingLevel.HEADING_3),
|
||||
loopTable('campEM', ['Tipul', 'Zona', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'zona', 'timp', 'vep', 'vlep', 'caracteristici']),
|
||||
H('RADIAȚII OPTICE ARTIFICIALE', HeadingLevel.HEADING_3),
|
||||
loopTable('optice', ['Tipul', 'Zona', 'Timp', 'VEP', 'VLEP', 'Caracteristici'], ['denumire', 'zona', 'timp', 'vep', 'vlep', 'caracteristici']),
|
||||
];
|
||||
const anexa4Footer: (Paragraph | Table)[] = [
|
||||
H('MICROCLIMAT / RADIAȚII / ILUMINAT', HeadingLevel.HEADING_3),
|
||||
cbLine(['cbMicroclimatInterior', 'interior'], ['cbMicroclimatExterior', 'exterior'], ['cbCaloriceRece', 'rad. calorice (rece)'], ['cbCaloriceCalda', 'rad. calorice (caldă)']),
|
||||
P(T('Radiații ionizante '), ph('cbRadiatii'), T(' Grupa: '), ph('radGrupa'), T(' Surse: '), ph('radSurse')),
|
||||
P(T('Tip expunere: '), ph('radTipExpunere'), T(' Aparatură: '), ph('radAparatura'), T(' Măsuri: '), ph('radMasuri')),
|
||||
cbLine(['cbIluminatSuficient', 'iluminat suficient'], ['cbIluminatInsuficient', 'insuficient'], ['cbIluminatNatural', 'natural'], ['cbIluminatArtificial', 'artificial'], ['cbIluminatMixt', 'mixt']),
|
||||
H('Protecție și dotări', HeadingLevel.HEADING_3),
|
||||
line('Mijloace de protecție colectivă:', 'protectieColectiva'),
|
||||
line('Mijloace de protecție individuală:', 'protectieIndividuala'),
|
||||
line('Echipament de lucru:', 'echipament'),
|
||||
P(T('Anexe igienico-sanitare: ')),
|
||||
cbLine(['cbVestiar', 'vestiar'], ['cbChiuveta', 'chiuvetă'], ['cbWc', 'WC'], ['cbDus', 'duș'], ['cbSalaMese', 'sală de mese'], ['cbRecreere', 'recreere']),
|
||||
line('Observații:', 'observatii'),
|
||||
line('Data completării:', 'dataCompletarii'),
|
||||
P(T('Angajatorul (nume, prenume, semnătura): ____________________')),
|
||||
P(new TextRun({ text: 'Instrucțiuni: răspuns afirmativ [☑]; răspuns negativ [☐].', italics: true })),
|
||||
];
|
||||
|
||||
// ════════════════════════ ANEXA 4A ════════════════════════
|
||||
const anexa4a: (Paragraph | Table)[] = [
|
||||
line('Unitatea economică/instituția:', 'unitatea'),
|
||||
P(T('Adresa: '), ph('adresa'), T(' Filiala: '), ph('filiala'), T(' CAEM (2 cifre): '), ph('caem2')),
|
||||
H('FIȘA de evaluare — muncă la distanță / platforme digitale'),
|
||||
line('Ocupația (subgrupa majoră CORM):', 'cormSubgrupa'),
|
||||
line('Direcția/secția/sectorul:', 'directiaSectia'),
|
||||
P(T('Numărul locului de muncă: '), ph('numarLoc'), T(' CAEM (diviziune): '), ph('caemDiviziune'), T(' Clasa: '), ph('clasa')),
|
||||
H('Descrierea activității', HeadingLevel.HEADING_3),
|
||||
P(T('Lucru în echipă '), ph('cbEchipa'), T(' Nr. ore/zi: '), ph('oreZi'), T(' Nr. schimburi: '), ph('schimburi')),
|
||||
cbLine(['cbSchimbNoapte', 'schimb de noapte'], ['cbPauze', 'pauze organizate'], ['cbLucruMonitor', 'lucru la monitor'], ['cbPlatformeDigitale', 'platforme digitale']),
|
||||
P(T('Conduce mașina '), ph('cbConduceMasina'), T(' categorie: '), ph('categorieConducere')),
|
||||
line('Operațiuni executate:', 'operatiuni'),
|
||||
P(T('Deplasări pe teren '), ph('cbDeplasari'), T(' '), ph('deplasariDescriere')),
|
||||
H('Efort fizic', HeadingLevel.HEADING_3),
|
||||
P(T('Manipulare manuală: ')),
|
||||
cbLine(['cbManipRidicare', 'ridicare'], ['cbManipCoborare', 'coborâre'], ['cbManipImpingere', 'împingere'], ['cbManipTragere', 'tragere'], ['cbManipPurtare', 'purtare'], ['cbManipDeplasare', 'deplasare']),
|
||||
P(T('Greutate maximă: '), ph('greutateMaxima')),
|
||||
cbLine(['cbVizuale', 'vizuale'], ['cbAuditive', 'auditive'], ['cbNeuropsihice', 'neuropsihice']),
|
||||
line('Alte riscuri:', 'alteRiscuri'),
|
||||
line('Data completării:', 'dataCompletarii'),
|
||||
P(T('Angajatorul (nume, prenume, semnătura): ____________________')),
|
||||
];
|
||||
|
||||
// ════════════════════════ ANEXA 4B ════════════════════════
|
||||
const anexa4b: (Paragraph | Table)[] = [
|
||||
line('Unitatea economică/instituția:', 'unitatea'),
|
||||
P(T('Adresa: '), ph('adresa'), T(' Telefon: '), ph('telefon'), T(' Fax: '), ph('fax'), T(' E-mail: '), ph('email')),
|
||||
P(T('Filiala: '), ph('filiala'), T(' Adresa filialei: '), ph('adresaFiliala'), T(' CAEM (2 cifre): '), ph('caem2')),
|
||||
H('SUPLIMENT la Fișa de evaluare a riscurilor profesionale'),
|
||||
line('Ocupația (subgrupa majoră CORM):', 'cormSubgrupa'),
|
||||
line('Direcția/secția/sectorul:', 'directiaSectia'),
|
||||
P(T('Numărul locului de muncă: '), ph('numarLoc'), T(' CAEM (diviziune): '), ph('caemDiviziune')),
|
||||
P(T('Numele, prenumele lucrătorului: '), ph('numePrenume'), T(' IDNP: '), ph('idnp')),
|
||||
P(T('RADIAȚII IONIZANTE: '), ph('cbRadiatii')),
|
||||
line('Data intrării în mediul cu expunere:', 'dataIntrarii'),
|
||||
P(T('Expunere anterioară — perioada: '), ph('expAnterioaraPerioada'), T(' ani: '), ph('expAnterioaraAni')),
|
||||
P(T('Doză externă (mSv): '), ph('dozaExterna'), T(' Doză internă (mSv): '), ph('dozaInterna'), T(' Doză totală (mSv): '), ph('dozaTotala')),
|
||||
H('Supraexpuneri excepționale', HeadingLevel.HEADING_3),
|
||||
loopTable('supraexpExceptionale', ['Tip de expunere', 'Data', 'Doză (mSv)'], ['tipExpunere', 'data', 'doza']),
|
||||
H('Supraexpuneri accidentale', HeadingLevel.HEADING_3),
|
||||
loopTable('supraexpAccidentale', ['Tip de expunere', 'Data', 'Doză (mSv)'], ['tipExpunere', 'data', 'doza']),
|
||||
line('Data completării:', 'dataCompletarii'),
|
||||
P(T('Angajatorul (nume, prenume, semnătura): ____________________')),
|
||||
];
|
||||
|
||||
// ════════════════════════ ANEXA 6 ════════════════════════
|
||||
const anexa6: (Paragraph | Table)[] = [
|
||||
H('FIȘĂ DE APTITUDINE ÎN MUNCĂ'),
|
||||
line('Unitatea:', 'unitatea'),
|
||||
P(T('Angajat: '), ph('numePrenume'), T(' IDNP: '), ph('idnp'), T(' Anul nașterii: '), ph('anNastere')),
|
||||
P(T('Ocupația: '), ph('ocupatieCorm'), T(' Departament: '), ph('departament')),
|
||||
P(T('Tipul examenului: '), ph('tipExamen'), T(' Data: '), ph('dataCompletarii')),
|
||||
H('Verdict', HeadingLevel.HEADING_3),
|
||||
P(ph('cbApt'), T(' Apt')),
|
||||
P(ph('cbAptAdaptare'), T(' Apt în perioada de adaptare')),
|
||||
P(ph('cbAptConditionat'), T(' Apt condiționat')),
|
||||
P(ph('cbInaptTemporar'), T(' Inapt temporar')),
|
||||
P(ph('cbInapt'), T(' Inapt')),
|
||||
line('Recomandări:', 'recomandari'),
|
||||
line('Valabil până la:', 'valabilPanaLa'),
|
||||
P(T('Semnătura medicului: '), ph('semnatDe')),
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('📄 Generez bolăvanke .docx în', OUT);
|
||||
await save('anexa-3.docx', anexa3);
|
||||
await save('anexa-4.docx', [...anexa4Header, ...anexa4Descriptiv, ...factorTables, ...anexa4Footer]);
|
||||
await save('anexa-4a.docx', anexa4a);
|
||||
await save('anexa-4b.docx', anexa4b);
|
||||
await save('anexa-6.docx', anexa6);
|
||||
console.log('✅ Gata. Editați formatarea în Word — placeholder-ele rămân ca {nume}.');
|
||||
}
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,662 @@
|
||||
import {
|
||||
AnexaType,
|
||||
CampaignStatus,
|
||||
ContractCategory,
|
||||
ContractPeriod,
|
||||
ContractType,
|
||||
DisciplinarySanctionType,
|
||||
DiplomaStatus,
|
||||
DocumentType,
|
||||
EmployeeStatus,
|
||||
EvaluationScore,
|
||||
FamilyMemberType,
|
||||
InventoryItemType,
|
||||
MedicalCheckupType,
|
||||
MedicalVerdict,
|
||||
OverexposureKind,
|
||||
PrismaClient,
|
||||
ProposedCategory,
|
||||
QualificationCategory,
|
||||
RiskExposureType,
|
||||
SalaryType,
|
||||
ScientificTitle,
|
||||
Sex,
|
||||
StudyLevel,
|
||||
PostUniversityType,
|
||||
StudyType,
|
||||
TrainingType,
|
||||
} from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function dbNameFromUrl(url: string): string {
|
||||
return decodeURIComponent(new URL(url).pathname.replace(/^\//, ''));
|
||||
}
|
||||
|
||||
function requireTemporaryDatabase() {
|
||||
const url = process.env.DATABASE_URL;
|
||||
if (!url) throw new Error('DATABASE_URL is required for test seed');
|
||||
const dbName = dbNameFromUrl(url);
|
||||
if (!dbName.startsWith('hrm_medpark_test_') && process.env.ALLOW_NON_TEST_DB !== 'true') {
|
||||
throw new Error(`Refusing to seed non-test database "${dbName}". Expected hrm_medpark_test_*.`);
|
||||
}
|
||||
return dbName;
|
||||
}
|
||||
|
||||
const d = (value: string) => new Date(`${value}T00:00:00.000Z`);
|
||||
|
||||
async function resetData() {
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.anexaTemplateVersion.deleteMany();
|
||||
await prisma.anexaTemplate.deleteMany();
|
||||
await prisma.radiationOverexposure.deleteMany();
|
||||
await prisma.medicalCheckup.deleteMany();
|
||||
await prisma.employeeMedicalProfile.deleteMany();
|
||||
await prisma.workplaceRiskExposure.deleteMany();
|
||||
await prisma.workplaceRiskCard.deleteMany();
|
||||
await prisma.evaluationForm.deleteMany();
|
||||
await prisma.evaluationCampaign.deleteMany();
|
||||
await prisma.cimServiceCategory.deleteMany();
|
||||
await prisma.employmentContract.deleteMany();
|
||||
await prisma.benefit.deleteMany();
|
||||
await prisma.disciplinarySanction.deleteMany();
|
||||
await prisma.training.deleteMany();
|
||||
await prisma.qualification.deleteMany();
|
||||
await prisma.education.deleteMany();
|
||||
await prisma.familyMember.deleteMany();
|
||||
await prisma.identityDocument.deleteMany();
|
||||
await prisma.employee.deleteMany();
|
||||
await prisma.inventoryItem.deleteMany();
|
||||
await prisma.department.deleteMany();
|
||||
await prisma.workSchedule.deleteMany();
|
||||
await prisma.taxExemption.deleteMany();
|
||||
await prisma.disabilityGrade.deleteMany();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dbName = requireTemporaryDatabase();
|
||||
console.log(`Seeding Medpark test data into ${dbName}...`);
|
||||
await resetData();
|
||||
|
||||
const disability = await prisma.disabilityGrade.create({
|
||||
data: { code: 'TEST-GR-I', name: 'Grad dizabilitate I - test' },
|
||||
});
|
||||
const childTax = await prisma.taxExemption.create({
|
||||
data: { code: 'TEST-SCUTIRE-COPIL', description: 'Scutire copil - test' },
|
||||
});
|
||||
const schedule = await prisma.workSchedule.create({
|
||||
data: { name: 'Test 5/2 8h', daysWork: 5, daysRest: 2, hoursPerDay: 8 },
|
||||
});
|
||||
const shiftSchedule = await prisma.workSchedule.create({
|
||||
data: { name: 'Test 12/24', daysWork: 1, daysRest: 1, hoursPerDay: 12 },
|
||||
});
|
||||
|
||||
const root = await prisma.department.create({ data: { name: 'Medpark Test', code: 'TEST_ROOT' } });
|
||||
const surgeryDept = await prisma.department.create({
|
||||
data: { name: 'Chirurgie Test', code: 'TEST_CHIR', parentId: root.id },
|
||||
});
|
||||
const radiologyDept = await prisma.department.create({
|
||||
data: { name: 'Radiologie Test', code: 'TEST_RAD', parentId: root.id },
|
||||
});
|
||||
const remoteDept = await prisma.department.create({
|
||||
data: { name: 'Administrativ Digital Test', code: 'TEST_REMOTE', parentId: root.id },
|
||||
});
|
||||
const labDept = await prisma.department.create({
|
||||
data: { name: 'Laborator Test', code: 'TEST_LAB', parentId: root.id },
|
||||
});
|
||||
|
||||
const uniform = await prisma.inventoryItem.create({
|
||||
data: { sku: 'TEST-UNIFORM-M', name: 'Uniformă test M', type: InventoryItemType.uniforma, size: 'M', color: 'teal', stockQty: 20 },
|
||||
});
|
||||
const coat = await prisma.inventoryItem.create({
|
||||
data: { sku: 'TEST-HALAT-L', name: 'Halat test L', type: InventoryItemType.halat, size: 'L', color: 'alb', stockQty: 20 },
|
||||
});
|
||||
const shoes = await prisma.inventoryItem.create({
|
||||
data: { sku: 'TEST-CIUPICI-40', name: 'Ciupici test 40', type: InventoryItemType.ciupici, size: '40', color: 'alb', stockQty: 20 },
|
||||
});
|
||||
const phone = await prisma.inventoryItem.create({
|
||||
data: { sku: 'TEST-PHONE-A15', name: 'Telefon test Samsung A15', type: InventoryItemType.aparat_telefon, stockQty: 5 },
|
||||
});
|
||||
|
||||
const commonEval = {
|
||||
echipa: true,
|
||||
oreZi: '8',
|
||||
schimburi: '2',
|
||||
schimbNoapte: true,
|
||||
pauzeOrganizate: true,
|
||||
riscInfectare: true,
|
||||
riscElectrocutare: true,
|
||||
riscTensiuneInalta: false,
|
||||
riscInecare: false,
|
||||
riscAsfixiere: false,
|
||||
riscStrivire: true,
|
||||
riscTaiere: true,
|
||||
riscIntepare: true,
|
||||
riscLovire: true,
|
||||
riscMuscatura: false,
|
||||
riscMicrotraumatisme: true,
|
||||
conduceMasina: true,
|
||||
conduceMasinaCategorie: 'B',
|
||||
conduceUtilajeIntrauzinal: false,
|
||||
spatiuL: '4',
|
||||
spatiul: '5',
|
||||
spatiuH: '3',
|
||||
suprafataVerticala: false,
|
||||
suprafataOrizontala: true,
|
||||
suprafataOblica: false,
|
||||
muncaIzolare: false,
|
||||
muncaInaltime: true,
|
||||
muncaInMiscare: true,
|
||||
pozitieOrtostatica: true,
|
||||
pozitieAsezat: false,
|
||||
pozitieAplecata: true,
|
||||
pozitieMixta: true,
|
||||
pozitieFortata: false,
|
||||
coloanaCervicala: true,
|
||||
coloanaToracala: true,
|
||||
coloanaLombara: true,
|
||||
manipulareRidicare: true,
|
||||
manipulareCoborare: true,
|
||||
manipulareImpingere: true,
|
||||
manipulareTragere: false,
|
||||
manipularePurtare: true,
|
||||
manipulareDeplasare: true,
|
||||
greutateMaxima: '15 kg',
|
||||
suprasolicitariVizuale: true,
|
||||
suprasolicitariAuditive: true,
|
||||
suprasolicitariNeuropsihice: true,
|
||||
microclimatInterior: true,
|
||||
microclimatExterior: false,
|
||||
radiatiiCaloriceRece: false,
|
||||
radiatiiCaloriceCalda: false,
|
||||
iluminatSuficient: true,
|
||||
iluminatInsuficient: false,
|
||||
iluminatNatural: true,
|
||||
iluminatArtificial: true,
|
||||
iluminatMixt: true,
|
||||
};
|
||||
|
||||
const surgeryCard = await prisma.workplaceRiskCard.create({
|
||||
data: {
|
||||
name: 'Test - Medic profil chirurgical cu gărzi de noapte',
|
||||
riskFactors: { source: 'Control medical (5).docx', categories: ['chimici', 'biologici', 'fizici', 'ergonomici'] },
|
||||
filiala: 'Sediul central',
|
||||
adresaFiliala: 'str. Nicolae Testemițanu 29, Chișinău',
|
||||
telefonFiliala: '+373 22 000 101',
|
||||
caemPrimeleDouaCifre: '86',
|
||||
cormSubgrupaMajora: 'Personal medical profil chirurgical',
|
||||
directiaSectiaSectorul: 'Bloc operator / Chirurgie',
|
||||
numarulLoculuiDeMunca: 'TEST-CHIR-01',
|
||||
caemDiviziune: '86.10',
|
||||
clasaConditiilorDeMunca: '3.2',
|
||||
numarLucratoriPosibili: 12,
|
||||
tipFisa: 'STANDARD',
|
||||
evaluareDetalii: commonEval,
|
||||
radiatiiIonizante: false,
|
||||
mijloaceProtectieColectiva: 'Ventilație locală, containere pentru obiecte ascuțite',
|
||||
mijloaceProtectieIndividuala: 'Mănuși, mască, halat steril, vizieră',
|
||||
echipamentLucru: 'Uniformă chirurgicală',
|
||||
anexeIgienicoSanitare: { vestiar: true, chiuveta: true, wc: true, dus: true, salaMese: true, recreere: true },
|
||||
observatii: 'Set complet pentru testarea Anexa 4.',
|
||||
exposures: {
|
||||
create: [
|
||||
{ tip: RiskExposureType.AGENT_CHIMIC, denumire: 'Glutaraldehidă', cas: '111-30-8', einecs: '203-856-5', timpExpunere: '2 h/zi', vep: '0,03 ppm', vlep: '0,1 ppm', caracteristici: 'iritant respirator' },
|
||||
{ tip: RiskExposureType.PULBERI, denumire: 'Pulberi textile sterile', cas: '—', einecs: '—', timpExpunere: '1 h/zi', vep: '2 mg/m3', vlep: '5 mg/m3', caracteristici: 'pulberi inhalabile' },
|
||||
{ tip: RiskExposureType.AGENT_BIOLOGIC, denumire: 'HBV/HCV/HIV', clasificare: 'grupa 3', caracteristici: 'risc prin înțepare/tăiere' },
|
||||
{ tip: RiskExposureType.ZGOMOT, denumire: 'Echipamente bloc operator', timpExpunere: '4 h/zi', vep: '80 dB', vlep: '87 dB', caracteristici: 'zgomot intermitent' },
|
||||
{ tip: RiskExposureType.VIBRATII, denumire: 'Instrumentar oscilant', zonaAfectata: 'mână-braț', timpExpunere: '30 min/zi', vep: '2,5 m/s2', vlep: '5 m/s2', caracteristici: 'vibrații locale' },
|
||||
{ tip: RiskExposureType.CAMP_ELECTROMAGNETIC, denumire: 'Electrocauter', zonaAfectata: 'corp întreg', timpExpunere: '1 h/zi', vep: 'conform NU-10', vlep: 'conform NU-10', caracteristici: 'câmp EM local' },
|
||||
{ tip: RiskExposureType.RADIATII_OPTICE, denumire: 'Lămpi chirurgicale', zonaAfectata: 'ochi', timpExpunere: '6 h/zi', vep: 'conform NU-10', vlep: 'conform NU-10', caracteristici: 'lumină intensă' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const radiologyCard = await prisma.workplaceRiskCard.create({
|
||||
data: {
|
||||
name: 'Test - Radiologie cu radiații ionizante',
|
||||
riskFactors: { source: 'Control medical (5).docx', categories: ['radiații ionizante', 'câmp electromagnetic'] },
|
||||
filiala: 'Sediul central',
|
||||
adresaFiliala: 'str. Nicolae Testemițanu 29, Chișinău',
|
||||
telefonFiliala: '+373 22 000 102',
|
||||
caemPrimeleDouaCifre: '86',
|
||||
cormSubgrupaMajora: 'Personal imagistică medicală',
|
||||
directiaSectiaSectorul: 'Diagnostic / Radiologie',
|
||||
numarulLoculuiDeMunca: 'TEST-RAD-01',
|
||||
caemDiviziune: '86.90',
|
||||
clasaConditiilorDeMunca: '3.3',
|
||||
numarLucratoriPosibili: 8,
|
||||
tipFisa: 'STANDARD',
|
||||
evaluareDetalii: { ...commonEval, riscInfectare: false, pozitieAsezat: true, pozitieOrtostatica: false },
|
||||
radiatiiIonizante: true,
|
||||
radiatiiGrupa: 'A',
|
||||
radiatiiAparatura: 'CT, Rx digital',
|
||||
radiatiiSurse: 'închise',
|
||||
radiatiiTipExpunere: 'X externă',
|
||||
radiatiiMasuriProtectie: 'Ecran de protecție, șorț plumb, dozimetru individual',
|
||||
mijloaceProtectieColectiva: 'Ecrane plumbate și semnalizare zonă controlată',
|
||||
mijloaceProtectieIndividuala: 'Șorț plumb, ochelari, dozimetru',
|
||||
echipamentLucru: 'Uniformă radiologie',
|
||||
anexeIgienicoSanitare: { vestiar: true, chiuveta: true, wc: true, dus: true, salaMese: true, recreere: false },
|
||||
exposures: {
|
||||
create: [
|
||||
{ tip: RiskExposureType.CAMP_ELECTROMAGNETIC, denumire: 'Câmp electromagnetic RMN', zonaAfectata: 'corp întreg', timpExpunere: '4 h/zi', vep: 'conform NU-10', vlep: 'conform NU-10', caracteristici: 'câmp magnetic static intens' },
|
||||
{ tip: RiskExposureType.ZGOMOT, denumire: 'Aparatură imagistică', timpExpunere: '3 h/zi', vep: '75 dB', vlep: '87 dB', caracteristici: 'zgomot tehnic' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const remoteCard = await prisma.workplaceRiskCard.create({
|
||||
data: {
|
||||
name: 'Test - Activități administrative la distanță',
|
||||
riskFactors: { source: 'Control medical (5).docx', categories: ['vizual', 'neuropsihic', 'platforme digitale'] },
|
||||
filiala: 'Sediul central',
|
||||
adresaFiliala: 'str. Nicolae Testemițanu 29, Chișinău',
|
||||
telefonFiliala: '+373 22 000 103',
|
||||
caemPrimeleDouaCifre: '86',
|
||||
cormSubgrupaMajora: 'Personal administrativ',
|
||||
directiaSectiaSectorul: 'Administrativ / Digital',
|
||||
numarulLoculuiDeMunca: 'TEST-REMOTE-01',
|
||||
caemDiviziune: '86.90',
|
||||
clasaConditiilorDeMunca: '2',
|
||||
numarLucratoriPosibili: 4,
|
||||
tipFisa: 'DISTANTA_DIGITAL',
|
||||
evaluareDetalii: {
|
||||
echipa: false,
|
||||
oreZi: '8',
|
||||
schimburi: '1',
|
||||
schimbNoapte: false,
|
||||
pauzeOrganizate: true,
|
||||
lucruMonitor: true,
|
||||
platformeDigitale: true,
|
||||
conduceMasina: false,
|
||||
operatiuni: 'Operare HIS, e-mail, raportare digitală',
|
||||
deplasari: true,
|
||||
deplasariDescriere: 'Deplasări ocazionale la sediu',
|
||||
manipulareRidicare: false,
|
||||
manipulareCoborare: false,
|
||||
manipulareImpingere: false,
|
||||
manipulareTragere: false,
|
||||
manipularePurtare: false,
|
||||
manipulareDeplasare: false,
|
||||
greutateMaxima: 'sub 3 kg',
|
||||
suprasolicitariVizuale: true,
|
||||
suprasolicitariAuditive: false,
|
||||
suprasolicitariNeuropsihice: true,
|
||||
alteRiscuri: 'Lucru prelungit la monitor',
|
||||
},
|
||||
radiatiiIonizante: false,
|
||||
mijloaceProtectieIndividuala: 'Scaun ergonomic, monitor extern',
|
||||
echipamentLucru: 'Laptop corporativ',
|
||||
anexeIgienicoSanitare: { vestiar: false, chiuveta: true, wc: true, dus: false, salaMese: false, recreere: true },
|
||||
observatii: 'Set pentru testarea Anexa 4A.',
|
||||
},
|
||||
});
|
||||
|
||||
const employees = await Promise.all([
|
||||
prisma.employee.create({
|
||||
data: {
|
||||
idnp: '1985061500016',
|
||||
nume: 'Popescu',
|
||||
prenume: 'Alexandru',
|
||||
patronimic: 'Ion',
|
||||
dataNasterii: d('1985-06-15'),
|
||||
domiciliu: 'mun. Chișinău, str. Ștefan cel Mare 1',
|
||||
adresaReala: 'mun. Chișinău, str. Test 1',
|
||||
telefonPersonal: '+37369100001',
|
||||
telefonServiciu: '+37322100001',
|
||||
emailPersonal: 'alexandru.popescu.test@example.com',
|
||||
emailCorporativ: 'alexandru.popescu@medpark.test',
|
||||
sex: Sex.M,
|
||||
codCpas: 'CPAS-001',
|
||||
stareCivila: 'casatorit',
|
||||
titluStiintific: ScientificTitle.doctor,
|
||||
status: EmployeeStatus.activ,
|
||||
},
|
||||
}),
|
||||
prisma.employee.create({
|
||||
data: {
|
||||
idnp: '1990032200017',
|
||||
nume: 'Ionescu',
|
||||
prenume: 'Maria',
|
||||
patronimic: 'Vasile',
|
||||
dataNasterii: d('1990-03-22'),
|
||||
domiciliu: 'mun. Chișinău, str. Mihai Viteazul 5',
|
||||
telefonPersonal: '+37369100002',
|
||||
telefonServiciu: '+37322100002',
|
||||
emailCorporativ: 'maria.ionescu@medpark.test',
|
||||
sex: Sex.F,
|
||||
status: EmployeeStatus.activ,
|
||||
gradDizabilitateId: disability.id,
|
||||
},
|
||||
}),
|
||||
prisma.employee.create({
|
||||
data: {
|
||||
idnp: '1978110800016',
|
||||
nume: 'Rusu',
|
||||
prenume: 'Viorel',
|
||||
dataNasterii: d('1978-11-08'),
|
||||
domiciliu: 'mun. Chișinău, str. Alba Iulia 12',
|
||||
telefonPersonal: '+37369100003',
|
||||
emailCorporativ: 'viorel.rusu@medpark.test',
|
||||
sex: Sex.M,
|
||||
status: EmployeeStatus.activ,
|
||||
},
|
||||
}),
|
||||
prisma.employee.create({
|
||||
data: {
|
||||
idnp: '2001091400010',
|
||||
nume: 'Cojocaru',
|
||||
prenume: 'Elena',
|
||||
dataNasterii: d('2001-09-14'),
|
||||
domiciliu: 'mun. Chișinău, str. Trandafirilor 3',
|
||||
telefonPersonal: '+37369100004',
|
||||
emailCorporativ: 'elena.cojocaru@medpark.test',
|
||||
sex: Sex.F,
|
||||
status: EmployeeStatus.activ,
|
||||
},
|
||||
}),
|
||||
prisma.employee.create({
|
||||
data: {
|
||||
idnp: '1995120100019',
|
||||
nume: 'Munteanu',
|
||||
prenume: 'Ana',
|
||||
dataNasterii: d('1995-12-01'),
|
||||
domiciliu: 'mun. Chișinău, bd. Dacia 20',
|
||||
telefonPersonal: '+37369100005',
|
||||
emailCorporativ: 'ana.munteanu@medpark.test',
|
||||
sex: Sex.F,
|
||||
status: EmployeeStatus.activ,
|
||||
},
|
||||
}),
|
||||
prisma.employee.create({
|
||||
data: {
|
||||
idnp: '1989020300012',
|
||||
nume: 'Lungu',
|
||||
prenume: 'Sergiu',
|
||||
dataNasterii: d('1989-02-03'),
|
||||
domiciliu: 'mun. Chișinău, str. Laboratorului 7',
|
||||
telefonPersonal: '+37369100006',
|
||||
emailCorporativ: 'sergiu.lungu@medpark.test',
|
||||
sex: Sex.M,
|
||||
status: EmployeeStatus.activ,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const [surgeon, nurse, radiologist, radiologyNurse, remoteAdmin, labDoctor] = employees;
|
||||
|
||||
await prisma.identityDocument.createMany({
|
||||
data: employees.map((employee, index) => ({
|
||||
employeeId: employee.id,
|
||||
tipAct: DocumentType.buletin_de_identitate,
|
||||
seria: `T${index + 1}`,
|
||||
nr: `TESTDOC${index + 1}`,
|
||||
dataEmiterii: d('2021-01-10'),
|
||||
autoritateEmitenta: 'ASP Test',
|
||||
dataExpirarii: d(`2031-01-${10 + index}`),
|
||||
})),
|
||||
});
|
||||
|
||||
await prisma.familyMember.createMany({
|
||||
data: [
|
||||
{ employeeId: surgeon.id, tip: FamilyMemberType.contact_principal, numePrenume: 'Popescu Elena', telefon: '+37368111111' },
|
||||
{ employeeId: surgeon.id, tip: FamilyMemberType.copil, numePrenume: 'Popescu Andrei', dataNasterii: d('2015-05-20'), idnp: '2015052000015', tipScutireId: childTax.id },
|
||||
{ employeeId: nurse.id, tip: FamilyMemberType.mama, numePrenume: 'Ionescu Tatiana', telefon: '+37368222222' },
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.education.createMany({
|
||||
data: [
|
||||
{ employeeId: surgeon.id, tipStudii: StudyType.superioare, institutia: 'USMF Nicolae Testemițanu', specialitatea: 'Chirurgie', dataAbsolvirii: d('2008-06-30'), nrSeriaDiploma: 'DIP-TEST-001', dataEmiterii: d('2008-07-10'), nrInregistrare: 'REG-001', confirmare: DiplomaStatus.confirmata, nivel: StudyLevel.postuniversitar, tipPostuniversitar: PostUniversityType.rezidentiat },
|
||||
{ employeeId: nurse.id, tipStudii: StudyType.medii_de_specialitate, institutia: 'Colegiul Național de Medicină', specialitatea: 'Nursing', dataAbsolvirii: d('2012-06-30'), nrSeriaDiploma: 'DIP-TEST-002', confirmare: DiplomaStatus.confirmata, nivel: StudyLevel.de_baza },
|
||||
{ employeeId: radiologist.id, tipStudii: StudyType.superioare, institutia: 'USMF Nicolae Testemițanu', specialitatea: 'Radiologie', dataAbsolvirii: d('2002-06-30'), nrSeriaDiploma: 'DIP-TEST-003', confirmare: DiplomaStatus.confirmata, nivel: StudyLevel.postuniversitar, tipPostuniversitar: PostUniversityType.rezidentiat },
|
||||
{ employeeId: radiologyNurse.id, tipStudii: StudyType.medii_de_specialitate, institutia: 'Colegiul Național de Medicină', specialitatea: 'Radiologie', dataAbsolvirii: d('2022-06-30'), nrSeriaDiploma: 'DIP-TEST-004', confirmare: DiplomaStatus.confirmata, nivel: StudyLevel.de_baza },
|
||||
{ employeeId: remoteAdmin.id, tipStudii: StudyType.superioare, institutia: 'ASEM', specialitatea: 'Management', dataAbsolvirii: d('2017-06-30'), nrSeriaDiploma: 'DIP-TEST-005', confirmare: DiplomaStatus.confirmata, nivel: StudyLevel.de_baza },
|
||||
{ employeeId: labDoctor.id, tipStudii: StudyType.superioare, institutia: 'USMF Nicolae Testemițanu', specialitatea: 'Medicină de laborator', dataAbsolvirii: d('2013-06-30'), nrSeriaDiploma: 'DIP-TEST-006', confirmare: DiplomaStatus.confirmata, nivel: StudyLevel.postuniversitar, tipPostuniversitar: PostUniversityType.rezidentiat },
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.qualification.createMany({
|
||||
data: [
|
||||
{ employeeId: surgeon.id, categorie: QualificationCategory.superioara, dataObtinerii: d('2020-02-01'), dataUltimeiConfirmari: d('2024-02-01'), dataExpirarii: d('2029-02-01'), specialitate: 'Chirurgie' },
|
||||
{ employeeId: nurse.id, categorie: QualificationCategory.cat_I, dataObtinerii: d('2021-03-01'), dataUltimeiConfirmari: d('2024-03-01'), dataExpirarii: d('2029-03-01'), specialitate: 'Nursing' },
|
||||
{ employeeId: radiologist.id, categorie: QualificationCategory.superioara, dataObtinerii: d('2019-04-01'), dataUltimeiConfirmari: d('2024-04-01'), dataExpirarii: d('2029-04-01'), specialitate: 'Radiologie' },
|
||||
{ employeeId: radiologyNurse.id, categorie: QualificationCategory.cat_II, dataObtinerii: d('2024-05-01'), dataUltimeiConfirmari: d('2024-05-01'), dataExpirarii: d('2029-05-01'), specialitate: 'Radiologie' },
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.training.createMany({
|
||||
data: employees.map((employee, index) => ({
|
||||
employeeId: employee.id,
|
||||
denumire: `Instruire Control medical test ${index + 1}`,
|
||||
inceput: d('2026-01-10'),
|
||||
sfirsit: d('2026-01-12'),
|
||||
tip: index % 2 === 0 ? TrainingType.intern : TrainingType.extern_RM,
|
||||
tara: 'Republica Moldova',
|
||||
nrOre: 16,
|
||||
organizatia: 'Medpark Academy Test',
|
||||
certificat: true,
|
||||
cost: '1000.00',
|
||||
})),
|
||||
});
|
||||
|
||||
await prisma.disciplinarySanction.createMany({
|
||||
data: [
|
||||
{ employeeId: nurse.id, tip: DisciplinarySanctionType.avertisment, dataAplicarii: d('2026-02-01'), dataExpirarii: d('2026-08-01') },
|
||||
{ employeeId: remoteAdmin.id, tip: DisciplinarySanctionType.mustrare, dataAplicarii: d('2025-09-01'), dataExpirarii: d('2026-03-01'), isStinsa: true },
|
||||
],
|
||||
});
|
||||
|
||||
const contractRows = [
|
||||
{ employee: surgeon, dept: surgeryDept, nr: 'TEST-CIM-001', role: 'Chirurg', corm: '221201', card: surgeryCard, schedule: shiftSchedule },
|
||||
{ employee: nurse, dept: surgeryDept, nr: 'TEST-CIM-002', role: 'Asistentă medicală chirurgie', corm: '222101', card: surgeryCard, schedule: shiftSchedule },
|
||||
{ employee: radiologist, dept: radiologyDept, nr: 'TEST-CIM-003', role: 'Medic radiolog', corm: '221203', card: radiologyCard, schedule },
|
||||
{ employee: radiologyNurse, dept: radiologyDept, nr: 'TEST-CIM-004', role: 'Asistentă radiologie', corm: '222102', card: radiologyCard, schedule },
|
||||
{ employee: remoteAdmin, dept: remoteDept, nr: 'TEST-CIM-005', role: 'Specialist documente digitale', corm: '242101', card: remoteCard, schedule },
|
||||
{ employee: labDoctor, dept: labDept, nr: 'TEST-CIM-006', role: 'Medic laborator', corm: '221207', card: surgeryCard, schedule },
|
||||
];
|
||||
|
||||
for (const row of contractRows) {
|
||||
const contract = await prisma.employmentContract.create({
|
||||
data: {
|
||||
nrCim: row.nr,
|
||||
employeeId: row.employee.id,
|
||||
categorie: ContractCategory.principal,
|
||||
dataSemnarii: d('2024-01-05'),
|
||||
dataAngajarii: d('2024-01-15'),
|
||||
perioada: ContractPeriod.nedeterminata,
|
||||
functiaClasificator: row.corm,
|
||||
codFunctie: row.corm,
|
||||
functiaOrganigrama: row.role,
|
||||
tipCim: ContractType.de_baza,
|
||||
departmentId: row.dept.id,
|
||||
regimMunca: 'normă întreagă',
|
||||
tipSalarizare: SalaryType.fix,
|
||||
salarizareDetails: { tip: 'fix', salariu: 15000 + contractRows.indexOf(row) * 500, zileConcediu: 28 },
|
||||
clausaAditionala: { test: true, source: 'Rubrici necesare (6).xlsx / CIM' },
|
||||
workScheduleId: row.schedule.id,
|
||||
categoriiServicii: {
|
||||
create: [
|
||||
{ categorieId: `TEST-SERV-${contractRows.indexOf(row) + 1}`, tipRemunerare: 'tarif', sumaNeta: '250.00' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.auditLog.create({
|
||||
data: { userId: 'seed-test', userRole: 'hr_admin', action: 'CREATE', entity: 'EmploymentContract', entityId: contract.id },
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.benefit.createMany({
|
||||
data: [
|
||||
{
|
||||
employeeId: nurse.id,
|
||||
uniformaId: uniform.id,
|
||||
halatId: coat.id,
|
||||
ciupiciId: shoes.id,
|
||||
ticheteMasa: true,
|
||||
valoareTichet: '70.00',
|
||||
alimentatiePersonal: true,
|
||||
abonamentTel: '150.00',
|
||||
aparatTelefonId: phone.id,
|
||||
cardCompanie: 'TEST-CARD-001',
|
||||
},
|
||||
{
|
||||
employeeId: radiologyNurse.id,
|
||||
uniformaId: uniform.id,
|
||||
halatId: coat.id,
|
||||
ticheteMasa: true,
|
||||
valoareTichet: '70.00',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const profileByEmployeeId = new Map<string, string>();
|
||||
for (const row of contractRows) {
|
||||
const radiology = row.card.id === radiologyCard.id;
|
||||
const profile = await prisma.employeeMedicalProfile.create({
|
||||
data: {
|
||||
employeeId: row.employee.id,
|
||||
ocupatieCorm: `${row.role} (${row.corm})`,
|
||||
workplaceRiskCardId: row.card.id,
|
||||
dataUltimControlMedical: row.employee.id === surgeon.id ? null : d(row.employee.id === nurse.id ? '2025-02-01' : '2025-06-01'),
|
||||
expusRadiatiiIonizante: radiology,
|
||||
dataIntrarii: radiology ? d('2020-01-15') : null,
|
||||
expunereAnterioaraPerioda: radiology ? '2017-2019' : null,
|
||||
expunereAnterioaraAni: radiology ? 3 : null,
|
||||
dozaCumulataExternaMsv: radiology ? '4.2500' : null,
|
||||
dozaCumulataInternaMsv: radiology ? '0.8000' : null,
|
||||
overexposures: radiology && row.employee.id === radiologist.id
|
||||
? {
|
||||
create: [
|
||||
{ fel: OverexposureKind.EXCEPTIONALA, tipExpunere: 'X externă', data: d('2023-05-12'), dozaMsv: '2.5000' },
|
||||
{ fel: OverexposureKind.ACCIDENTALA, tipExpunere: 'gamma externă', data: d('2024-09-03'), dozaMsv: '1.2000' },
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
profileByEmployeeId.set(row.employee.id, profile.id);
|
||||
}
|
||||
|
||||
await prisma.medicalCheckup.createMany({
|
||||
data: [
|
||||
{ employeeId: surgeon.id, tip: MedicalCheckupType.la_angajare, dataPlanificata: d('2026-05-20') },
|
||||
{ employeeId: nurse.id, tip: MedicalCheckupType.periodic, dataPlanificata: d('2026-05-28') },
|
||||
{ employeeId: radiologist.id, tip: MedicalCheckupType.la_reluarea_activitatii, dataPlanificata: d('2026-05-29') },
|
||||
{ employeeId: surgeon.id, tip: MedicalCheckupType.periodic, dataPlanificata: d('2025-05-20'), dataEfectuata: d('2025-05-20'), verdict: MedicalVerdict.apt, recomandari: 'Control anual', valabilPanaLa: d('2026-05-20'), semnatDe: 'Dr. Test Apt', documenteGenerate: [{ name: 'Anexa_6_Final_Apt', url: 's3://test/anexa6_apt.docx', type: 'ANEXA_6' }] },
|
||||
{ employeeId: nurse.id, tip: MedicalCheckupType.periodic, dataPlanificata: d('2025-04-20'), dataEfectuata: d('2025-04-20'), verdict: MedicalVerdict.apt_perioada_adaptare, recomandari: 'Adaptare 30 zile', valabilPanaLa: d('2026-04-20'), semnatDe: 'Dr. Test Adaptare', documenteGenerate: [{ name: 'Anexa_6_Final_Adaptare', url: 's3://test/anexa6_adaptare.docx', type: 'ANEXA_6' }] },
|
||||
{ employeeId: radiologist.id, tip: MedicalCheckupType.periodic, dataPlanificata: d('2025-03-20'), dataEfectuata: d('2025-03-20'), verdict: MedicalVerdict.apt_conditionat, recomandari: 'Dozimetru obligatoriu', valabilPanaLa: d('2026-03-20'), semnatDe: 'Dr. Test Conditionat', documenteGenerate: [{ name: 'Anexa_6_Final_Conditionat', url: 's3://test/anexa6_conditionat.docx', type: 'ANEXA_6' }] },
|
||||
{ employeeId: radiologyNurse.id, tip: MedicalCheckupType.suplimentar, dataPlanificata: d('2025-02-20'), dataEfectuata: d('2025-02-20'), verdict: MedicalVerdict.inapt_temporar, recomandari: 'Reevaluare peste 30 zile', valabilPanaLa: d('2025-03-20'), semnatDe: 'Dr. Test Temporar', documenteGenerate: [{ name: 'Anexa_6_Final_Temporar', url: 's3://test/anexa6_temporar.docx', type: 'ANEXA_6' }] },
|
||||
{ employeeId: remoteAdmin.id, tip: MedicalCheckupType.periodic, dataPlanificata: d('2025-01-20'), dataEfectuata: d('2025-01-20'), verdict: MedicalVerdict.inapt, recomandari: 'Inapt pentru postul curent', valabilPanaLa: d('2025-02-20'), semnatDe: 'Dr. Test Inapt', documenteGenerate: [{ name: 'Anexa_6_Final_Inapt', url: 's3://test/anexa6_inapt.docx', type: 'ANEXA_6' }] },
|
||||
],
|
||||
});
|
||||
|
||||
const campaign = await prisma.evaluationCampaign.create({
|
||||
data: {
|
||||
name: 'Test evaluare anuală nursing - Chirurgie 2026',
|
||||
departmentId: surgeryDept.id,
|
||||
month: d('2026-05-01'),
|
||||
status: CampaignStatus.in_progress,
|
||||
},
|
||||
});
|
||||
await prisma.evaluationForm.createMany({
|
||||
data: [
|
||||
{
|
||||
campaignId: campaign.id,
|
||||
employeeId: nurse.id,
|
||||
abilitatiClinice: EvaluationScore.bine,
|
||||
judecataClinica: EvaluationScore.bine,
|
||||
manopere: EvaluationScore.bine,
|
||||
gestionareaSarcinilor: EvaluationScore.mediu,
|
||||
constiintaProfesionala: EvaluationScore.bine,
|
||||
atitudineaPacienti: EvaluationScore.bine,
|
||||
atitudineaColegi: EvaluationScore.bine,
|
||||
atitudineaPersonalNonMed: EvaluationScore.mediu,
|
||||
utilizareSmartphone: EvaluationScore.bine,
|
||||
respectareaProgramului: EvaluationScore.bine,
|
||||
respectareaDressCode: EvaluationScore.bine,
|
||||
testJci: { score: 18, max_score: 20, percent: 90, source: 'academy_ocean_test' },
|
||||
completareaDocMed: true,
|
||||
perfectioneazaCunostinte: true,
|
||||
membruComitetCalitate: true,
|
||||
functieDeMonitor: false,
|
||||
inlocuiesteSuperiorul: false,
|
||||
categorieCalculata: ProposedCategory.superioara,
|
||||
},
|
||||
{
|
||||
campaignId: campaign.id,
|
||||
employeeId: surgeon.id,
|
||||
abilitatiClinice: EvaluationScore.mediu,
|
||||
judecataClinica: EvaluationScore.mediu,
|
||||
manopere: EvaluationScore.bine,
|
||||
categorieCalculata: ProposedCategory.fara,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const closedCampaign = await prisma.evaluationCampaign.create({
|
||||
data: {
|
||||
name: 'Test evaluare nursing - Radiologie 2025',
|
||||
departmentId: radiologyDept.id,
|
||||
month: d('2025-11-01'),
|
||||
status: CampaignStatus.closed,
|
||||
},
|
||||
});
|
||||
await prisma.evaluationForm.create({
|
||||
data: {
|
||||
campaignId: closedCampaign.id,
|
||||
employeeId: radiologyNurse.id,
|
||||
abilitatiClinice: EvaluationScore.bine,
|
||||
judecataClinica: EvaluationScore.bine,
|
||||
manopere: EvaluationScore.bine,
|
||||
gestionareaSarcinilor: EvaluationScore.bine,
|
||||
constiintaProfesionala: EvaluationScore.bine,
|
||||
atitudineaPacienti: EvaluationScore.mediu,
|
||||
atitudineaColegi: EvaluationScore.bine,
|
||||
atitudineaPersonalNonMed: EvaluationScore.bine,
|
||||
utilizareSmartphone: EvaluationScore.bine,
|
||||
respectareaProgramului: EvaluationScore.bine,
|
||||
respectareaDressCode: EvaluationScore.mediu,
|
||||
completareaDocMed: true,
|
||||
perfectioneazaCunostinte: true,
|
||||
membruComitetCalitate: false,
|
||||
functieDeMonitor: false,
|
||||
inlocuiesteSuperiorul: false,
|
||||
categorieCalculata: ProposedCategory.cat_I,
|
||||
categorieAprobata: ProposedCategory.cat_I,
|
||||
observatii: 'Formular închis pentru test read-only.',
|
||||
completedAt: d('2025-11-20'),
|
||||
},
|
||||
});
|
||||
|
||||
for (const type of [AnexaType.ANEXA_3, AnexaType.ANEXA_4, AnexaType.ANEXA_4A, AnexaType.ANEXA_4B, AnexaType.ANEXA_6]) {
|
||||
await prisma.anexaTemplate.create({
|
||||
data: {
|
||||
type,
|
||||
name: `Test ${type}`,
|
||||
contentJson: { source: 'templates/docx', note: 'DOCX template is stored on disk' },
|
||||
updatedById: 'seed-test',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Seed summary:');
|
||||
console.log(` employees=${await prisma.employee.count()}`);
|
||||
console.log(` workplaceRiskCards=${await prisma.workplaceRiskCard.count()}`);
|
||||
console.log(` riskExposures=${await prisma.workplaceRiskExposure.count()}`);
|
||||
console.log(` medicalCheckups=${await prisma.medicalCheckup.count()}`);
|
||||
console.log(` evaluationForms=${await prisma.evaluationForm.count()}`);
|
||||
console.log(` profiles=${profileByEmployeeId.size}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { spawn, ChildProcess } from 'node:child_process';
|
||||
import { createServer } from 'node:net';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
type CommandEnv = NodeJS.ProcessEnv;
|
||||
|
||||
function loadEnvFile(filePath: string) {
|
||||
if (!existsSync(filePath)) return;
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
||||
if (!match) continue;
|
||||
const [, key, rawValue] = match;
|
||||
if (process.env[key] !== undefined) continue;
|
||||
process.env[key] = rawValue.trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
function loadLocalEnv() {
|
||||
loadEnvFile(resolve(process.cwd(), '.env'));
|
||||
loadEnvFile(resolve(process.cwd(), '..', '..', '.env'));
|
||||
}
|
||||
|
||||
function timestamp() {
|
||||
const now = new Date();
|
||||
const pad = (value: number) => String(value).padStart(2, '0');
|
||||
return [
|
||||
now.getFullYear(),
|
||||
pad(now.getMonth() + 1),
|
||||
pad(now.getDate()),
|
||||
'_',
|
||||
pad(now.getHours()),
|
||||
pad(now.getMinutes()),
|
||||
pad(now.getSeconds()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function databaseNameFromUrl(url: string) {
|
||||
return decodeURIComponent(new URL(url).pathname.replace(/^\//, ''));
|
||||
}
|
||||
|
||||
function databaseUrl(baseUrl: string, dbName: string) {
|
||||
const url = new URL(baseUrl);
|
||||
url.pathname = `/${dbName}`;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function adminUrl(baseUrl: string) {
|
||||
const url = new URL(baseUrl);
|
||||
url.pathname = '/postgres';
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function assertTestDatabaseName(dbName: string) {
|
||||
if (!/^hrm_medpark_test_[A-Za-z0-9_]+$/.test(dbName)) {
|
||||
throw new Error(`Refusing unsafe database name "${dbName}". Expected hrm_medpark_test_*.`);
|
||||
}
|
||||
}
|
||||
|
||||
function quotedIdentifier(dbName: string) {
|
||||
assertTestDatabaseName(dbName);
|
||||
return `"${dbName.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
function pnpmCommand() {
|
||||
return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
||||
}
|
||||
|
||||
function cleanEnv(env: CommandEnv) {
|
||||
const cleaned: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (value === undefined || key.startsWith('=')) continue;
|
||||
cleaned[key] = value;
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
async function createDatabase(baseUrl: string, dbName: string) {
|
||||
assertTestDatabaseName(dbName);
|
||||
const admin = new PrismaClient({ datasources: { db: { url: adminUrl(baseUrl) } } });
|
||||
try {
|
||||
await admin.$executeRawUnsafe(`CREATE DATABASE ${quotedIdentifier(dbName)}`);
|
||||
} finally {
|
||||
await admin.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
async function dropDatabase(baseUrl: string, dbName: string) {
|
||||
assertTestDatabaseName(dbName);
|
||||
const admin = new PrismaClient({ datasources: { db: { url: adminUrl(baseUrl) } } });
|
||||
try {
|
||||
await admin.$executeRawUnsafe(
|
||||
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()',
|
||||
dbName,
|
||||
);
|
||||
await admin.$executeRawUnsafe(`DROP DATABASE IF EXISTS ${quotedIdentifier(dbName)}`);
|
||||
} finally {
|
||||
await admin.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommand(label: string, args: string[], env: CommandEnv) {
|
||||
console.log(`\n> ${label}`);
|
||||
await new Promise<void>((resolvePromise, reject) => {
|
||||
const child = spawn(pnpmCommand(), args, {
|
||||
cwd: process.cwd(),
|
||||
env: cleanEnv(env),
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) resolvePromise();
|
||||
else reject(new Error(`${label} failed with exit code ${code ?? 'unknown'}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findFreePort() {
|
||||
return new Promise<number>((resolvePromise, reject) => {
|
||||
const server = createServer();
|
||||
server.on('error', reject);
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
server.close();
|
||||
reject(new Error('Unable to allocate a free API port'));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
server.close(() => resolvePromise(port));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForApi(baseUrl: string, child: ChildProcess) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < 60_000) {
|
||||
if (child.exitCode !== null) {
|
||||
throw new Error(`Temporary API exited before becoming ready (exit code ${child.exitCode})`);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/auth/dev-login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'readiness', role: 'hr_admin' }),
|
||||
});
|
||||
if (response.status === 200 || response.status === 201) return;
|
||||
} catch {
|
||||
// API is still booting.
|
||||
}
|
||||
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
|
||||
}
|
||||
throw new Error(`Temporary API did not become ready at ${baseUrl}`);
|
||||
}
|
||||
|
||||
async function startTemporaryApi(dbUrl: string, dbName: string) {
|
||||
const port = Number(process.env.TEST_API_PORT ?? (await findFreePort()));
|
||||
const baseUrl = `http://127.0.0.1:${port}/api/v1`;
|
||||
const bucket = `hrm-docs-test-${dbName.replace(/_/g, '-')}`.slice(0, 63);
|
||||
const env: CommandEnv = {
|
||||
...process.env,
|
||||
DATABASE_URL: dbUrl,
|
||||
PORT: String(port),
|
||||
NODE_ENV: 'test',
|
||||
ALLOW_DEV_LOGIN: 'true',
|
||||
MINIO_BUCKET: bucket,
|
||||
};
|
||||
|
||||
console.log(`\n> Starting temporary API on ${baseUrl}`);
|
||||
const child = spawn(pnpmCommand(), ['exec', 'nest', 'start'], {
|
||||
cwd: process.cwd(),
|
||||
env: cleanEnv(env),
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
child.stdout?.on('data', (chunk: Buffer) => process.stdout.write(`[api] ${chunk.toString()}`));
|
||||
child.stderr?.on('data', (chunk: Buffer) => process.stderr.write(`[api] ${chunk.toString()}`));
|
||||
await waitForApi(baseUrl, child);
|
||||
return { child, baseUrl };
|
||||
}
|
||||
|
||||
async function stopTemporaryApi(child: ChildProcess | null) {
|
||||
if (!child || child.exitCode !== null) return;
|
||||
const waitForExit = async () => {
|
||||
if (child.exitCode !== null) return;
|
||||
await new Promise<void>((resolvePromise) => {
|
||||
const timeout = setTimeout(() => resolvePromise(), 10_000);
|
||||
const done = () => {
|
||||
clearTimeout(timeout);
|
||||
resolvePromise();
|
||||
};
|
||||
child.once('exit', done);
|
||||
child.once('close', done);
|
||||
});
|
||||
};
|
||||
if (process.platform === 'win32' && child.pid) {
|
||||
await new Promise<void>((resolvePromise) => {
|
||||
const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
||||
killer.on('exit', () => resolvePromise());
|
||||
killer.on('error', () => resolvePromise());
|
||||
});
|
||||
await waitForExit();
|
||||
child.stdout?.destroy();
|
||||
child.stderr?.destroy();
|
||||
child.unref();
|
||||
return;
|
||||
}
|
||||
child.kill('SIGTERM');
|
||||
await waitForExit();
|
||||
child.stdout?.destroy();
|
||||
child.stderr?.destroy();
|
||||
child.unref();
|
||||
}
|
||||
|
||||
async function run() {
|
||||
loadLocalEnv();
|
||||
const baseUrl = process.env.DATABASE_URL;
|
||||
if (!baseUrl) throw new Error('DATABASE_URL is required. Put it in apps/api/.env or export it before running testdb:run.');
|
||||
|
||||
const dbName = `hrm_medpark_test_${timestamp()}`;
|
||||
const dbUrl = databaseUrl(baseUrl, dbName);
|
||||
const commandEnv: CommandEnv = { ...process.env, DATABASE_URL: dbUrl };
|
||||
let api: Awaited<ReturnType<typeof startTemporaryApi>> | null = null;
|
||||
|
||||
console.log(`Creating temporary database ${dbName}...`);
|
||||
await createDatabase(baseUrl, dbName);
|
||||
|
||||
try {
|
||||
await runCommand('prisma migrate deploy', ['exec', 'prisma', 'migrate', 'deploy'], commandEnv);
|
||||
await runCommand('seed test data', ['exec', 'ts-node', 'scripts/seed-test-data.ts'], commandEnv);
|
||||
|
||||
api = await startTemporaryApi(dbUrl, dbName);
|
||||
await runCommand('verify functionality', ['exec', 'ts-node', 'scripts/verify-functionality.ts'], {
|
||||
...commandEnv,
|
||||
API_BASE_URL: api.baseUrl,
|
||||
});
|
||||
|
||||
console.log('\nTemporary database is ready for manual checks.');
|
||||
console.log(`DATABASE_URL=${dbUrl}`);
|
||||
console.log('\nRun API manually from the repo root with:');
|
||||
console.log(` $env:DATABASE_URL='${dbUrl}'; pnpm.cmd --filter api dev`);
|
||||
console.log('\nDrop it later with:');
|
||||
console.log(` $env:TEST_DATABASE_URL='${dbUrl}'; pnpm.cmd --filter api testdb:drop`);
|
||||
} catch (error) {
|
||||
console.error('\nTest database run failed. The database was left in place for inspection:');
|
||||
console.error(`DATABASE_URL=${dbUrl}`);
|
||||
throw error;
|
||||
} finally {
|
||||
await stopTemporaryApi(api?.child ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
function targetDatabaseFromArgs(baseUrl: string) {
|
||||
const arg = process.argv.slice(3).find((value) => value !== '--') ?? process.env.TEST_DATABASE_URL ?? process.env.TEST_DATABASE_NAME;
|
||||
if (!arg) {
|
||||
throw new Error('Provide a test DB name or URL: pnpm.cmd --filter api testdb:drop -- hrm_medpark_test_YYYYMMDD_HHMMSS');
|
||||
}
|
||||
if (/^postgres(?:ql)?:\/\//.test(arg)) {
|
||||
return { dbName: databaseNameFromUrl(arg), baseUrl: arg };
|
||||
}
|
||||
return { dbName: arg, baseUrl };
|
||||
}
|
||||
|
||||
async function drop() {
|
||||
loadLocalEnv();
|
||||
const baseUrl = process.env.DATABASE_URL;
|
||||
if (!baseUrl) throw new Error('DATABASE_URL is required for admin connection');
|
||||
const target = targetDatabaseFromArgs(baseUrl);
|
||||
assertTestDatabaseName(target.dbName);
|
||||
console.log(`Dropping temporary database ${target.dbName}...`);
|
||||
await dropDatabase(target.baseUrl, target.dbName);
|
||||
console.log(`Dropped ${target.dbName}.`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const command = process.argv[2] ?? 'run';
|
||||
if (command === 'run') {
|
||||
await run();
|
||||
return;
|
||||
}
|
||||
if (command === 'drop') {
|
||||
await drop();
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unknown command "${command}". Use "run" or "drop".`);
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,446 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import ExcelJS = require('exceljs');
|
||||
import PizZip from 'pizzip';
|
||||
import {
|
||||
AnexaType,
|
||||
MedicalCheckupType,
|
||||
MedicalVerdict,
|
||||
PrismaClient,
|
||||
RiskExposureType,
|
||||
} from '@prisma/client';
|
||||
import { DocxTemplateService } from '../src/modules/medical/services/docx-template.service';
|
||||
|
||||
type HttpResult = { status: number; body: unknown; text: string };
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const warnings: string[] = [];
|
||||
|
||||
function ok(message: string) {
|
||||
console.log(`OK ${message}`);
|
||||
}
|
||||
|
||||
function warn(message: string) {
|
||||
warnings.push(message);
|
||||
console.warn(`WARN ${message}`);
|
||||
}
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function dbNameFromUrl(url: string): string {
|
||||
return decodeURIComponent(new URL(url).pathname.replace(/^\//, ''));
|
||||
}
|
||||
|
||||
function requireTemporaryDatabase() {
|
||||
const url = process.env.DATABASE_URL;
|
||||
if (!url) throw new Error('DATABASE_URL is required for verification');
|
||||
const dbName = dbNameFromUrl(url);
|
||||
if (!dbName.startsWith('hrm_medpark_test_') && process.env.ALLOW_NON_TEST_DB !== 'true') {
|
||||
throw new Error(`Refusing to verify non-test database "${dbName}". Expected hrm_medpark_test_*.`);
|
||||
}
|
||||
return dbName;
|
||||
}
|
||||
|
||||
function sourcePath(fileName: string) {
|
||||
return resolve(process.cwd(), '..', '..', '..', fileName);
|
||||
}
|
||||
|
||||
function docxText(filePath: string): string {
|
||||
const zip = new PizZip(readFileSync(filePath));
|
||||
const xml = Object.keys(zip.files)
|
||||
.filter((name) => /^word\/(document|header|footer).*\.xml$/.test(name))
|
||||
.map((name) => zip.file(name)?.asText() ?? '')
|
||||
.join('\n');
|
||||
return xml
|
||||
.replace(/<\/w:p>/g, '\n')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function verifySourceFiles() {
|
||||
const controlDocx = process.env.CONTROL_MEDICAL_DOCX ?? sourcePath('Control medical (5).docx');
|
||||
const rubriciXlsx = process.env.RUBRICI_NECESARE_XLSX ?? sourcePath('Rubrici necesare (6).xlsx');
|
||||
|
||||
assert(existsSync(controlDocx), `Missing source file: ${controlDocx}`);
|
||||
assert(existsSync(rubriciXlsx), `Missing source file: ${rubriciXlsx}`);
|
||||
|
||||
const controlText = docxText(controlDocx);
|
||||
for (const phrase of [
|
||||
/Baza de date a angajatilor trebuie sa contina/i,
|
||||
/Persoana expusa profesional la radiatii ionizante/i,
|
||||
/Tipul controlului medical/i,
|
||||
/Fisa de solicitare/i,
|
||||
/aptitudine/i,
|
||||
/Apt condi/i,
|
||||
/Inapt temporar/i,
|
||||
]) {
|
||||
assert(phrase.test(controlText), `Control medical checklist phrase not found: ${phrase}`);
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(rubriciXlsx);
|
||||
const rubrici = workbook.getWorksheet('Rubrici');
|
||||
const performance = workbook.getWorksheet('Sheet1');
|
||||
assert(rubrici, 'Rubrici necesare workbook must contain sheet "Rubrici"');
|
||||
assert(performance, 'Rubrici necesare workbook must contain sheet "Sheet1"');
|
||||
|
||||
const fields = new Set<string>();
|
||||
for (let rowIndex = 1; rowIndex <= rubrici.actualRowCount; rowIndex += 1) {
|
||||
const value = excelCellText(rubrici.getRow(rowIndex).getCell(3)).trim();
|
||||
if (value) fields.add(value);
|
||||
}
|
||||
for (const field of ['IDNP', 'Nume', 'Prenume', 'Data nasterii', 'Domiciliu', 'Nr de telefon personal', 'tipul actului de identitate', 'Nr CIM']) {
|
||||
assert(fields.has(field), `Rubrici checklist field not found: ${field}`);
|
||||
}
|
||||
|
||||
const performanceText = docxLikeSheetText(performance);
|
||||
for (const phrase of ['Abilitati clinice nursing', 'Judecata clinica', 'Respectarea Dress Code', 'Membru unui comitet']) {
|
||||
assert(performanceText.includes(phrase), `Performance checklist phrase not found: ${phrase}`);
|
||||
}
|
||||
|
||||
ok('source DOCX/XLSX checklists are present and readable');
|
||||
}
|
||||
|
||||
function docxLikeSheetText(worksheet: ExcelJS.Worksheet) {
|
||||
const parts: string[] = [];
|
||||
worksheet.eachRow((row) => {
|
||||
row.eachCell((cell) => {
|
||||
const text = excelCellText(cell).trim();
|
||||
if (text) parts.push(text);
|
||||
});
|
||||
});
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function excelCellText(cell: ExcelJS.Cell) {
|
||||
const value = cell.value;
|
||||
if (value == null) return '';
|
||||
if (value instanceof Date) return value.toISOString();
|
||||
if (typeof value !== 'object') return String(value);
|
||||
if ('text' in value && typeof value.text === 'string') return value.text;
|
||||
if ('result' in value && value.result != null) return String(value.result);
|
||||
if ('richText' in value && Array.isArray(value.richText)) {
|
||||
return value.richText.map((part) => part.text).join('');
|
||||
}
|
||||
if ('hyperlink' in value && 'text' in value && typeof value.text === 'string') return value.text;
|
||||
return '';
|
||||
}
|
||||
|
||||
async function verifyDatabaseCoverage() {
|
||||
const [
|
||||
employees,
|
||||
identityDocuments,
|
||||
familyMembers,
|
||||
educations,
|
||||
qualifications,
|
||||
trainings,
|
||||
sanctions,
|
||||
benefits,
|
||||
contracts,
|
||||
riskCards,
|
||||
profiles,
|
||||
checkups,
|
||||
evaluationForms,
|
||||
] = await Promise.all([
|
||||
prisma.employee.count(),
|
||||
prisma.identityDocument.count(),
|
||||
prisma.familyMember.count(),
|
||||
prisma.education.count(),
|
||||
prisma.qualification.count(),
|
||||
prisma.training.count(),
|
||||
prisma.disciplinarySanction.count(),
|
||||
prisma.benefit.count(),
|
||||
prisma.employmentContract.count(),
|
||||
prisma.workplaceRiskCard.count(),
|
||||
prisma.employeeMedicalProfile.count(),
|
||||
prisma.medicalCheckup.count(),
|
||||
prisma.evaluationForm.count(),
|
||||
]);
|
||||
|
||||
assert(employees >= 6, `Expected at least 6 employees, got ${employees}`);
|
||||
assert(identityDocuments >= employees, 'Every seeded employee should have an identity document');
|
||||
assert(familyMembers >= 3, 'Expected family/contact records');
|
||||
assert(educations >= employees, 'Expected education records for all employees');
|
||||
assert(qualifications >= 4, 'Expected qualification records');
|
||||
assert(trainings >= employees, 'Expected training records for all employees');
|
||||
assert(sanctions >= 2, 'Expected disciplinary sanction scenarios');
|
||||
assert(benefits >= 2, 'Expected benefit scenarios');
|
||||
assert(contracts >= employees, 'Expected active CIM for all employees');
|
||||
assert(riskCards >= 3, 'Expected STANDARD, radiation, and DISTANTA_DIGITAL risk cards');
|
||||
assert(profiles >= employees, 'Expected medical profile for all employees');
|
||||
assert(checkups >= 8, 'Expected pending and completed checkups');
|
||||
assert(evaluationForms >= 3, 'Expected performance evaluation forms');
|
||||
|
||||
const exposureTypes = await prisma.workplaceRiskExposure.findMany({ select: { tip: true }, distinct: ['tip'] });
|
||||
const seededExposureTypes = new Set(exposureTypes.map((row) => row.tip));
|
||||
for (const type of Object.values(RiskExposureType)) {
|
||||
assert(seededExposureTypes.has(type), `Missing risk exposure type in test seed: ${type}`);
|
||||
}
|
||||
|
||||
const verdicts = await prisma.medicalCheckup.findMany({
|
||||
where: { verdict: { not: null } },
|
||||
select: { verdict: true },
|
||||
distinct: ['verdict'],
|
||||
});
|
||||
const seededVerdicts = new Set(verdicts.map((row) => row.verdict));
|
||||
for (const verdict of Object.values(MedicalVerdict)) {
|
||||
assert(seededVerdicts.has(verdict), `Missing medical verdict scenario: ${verdict}`);
|
||||
}
|
||||
|
||||
const radiationProfiles = await prisma.employeeMedicalProfile.count({ where: { expusRadiatiiIonizante: true } });
|
||||
const overexposures = await prisma.radiationOverexposure.findMany({ select: { fel: true }, distinct: ['fel'] });
|
||||
const remoteCards = await prisma.workplaceRiskCard.count({ where: { tipFisa: 'DISTANTA_DIGITAL' } });
|
||||
const pending = await prisma.medicalCheckup.count({ where: { verdict: null } });
|
||||
|
||||
assert(radiationProfiles >= 2, 'Expected at least two radiation-exposed employees');
|
||||
assert(overexposures.length >= 2, 'Expected exceptional and accidental overexposure rows');
|
||||
assert(remoteCards >= 1, 'Expected Anexa 4A/DISTANTA_DIGITAL risk card');
|
||||
assert(pending >= 3, 'Expected pending checkups for medic inbox');
|
||||
|
||||
ok('database seed covers HR, performance, and Control medical scenarios');
|
||||
}
|
||||
|
||||
function extractTemplatePlaceholders(fileName: string) {
|
||||
const fullPath = resolve(process.cwd(), 'templates', 'docx', fileName);
|
||||
assert(existsSync(fullPath), `Missing DOCX template: ${fullPath}`);
|
||||
const zip = new PizZip(readFileSync(fullPath));
|
||||
const xml = Object.keys(zip.files)
|
||||
.filter((name) => /^word\/(document|header|footer).*\.xml$/.test(name))
|
||||
.map((name) => zip.file(name)?.asText() ?? '')
|
||||
.join('\n');
|
||||
const opens = (xml.match(/\{/g) ?? []).length;
|
||||
const closes = (xml.match(/\}/g) ?? []).length;
|
||||
assert(opens === closes, `${fileName} has unbalanced placeholders: opens=${opens}, closes=${closes}`);
|
||||
const placeholders = Array.from(xml.matchAll(/\{([^{}]+)\}/g)).map((match) => match[1]);
|
||||
return { placeholders, opens };
|
||||
}
|
||||
|
||||
function valueForPlaceholder(name: string, index = 1) {
|
||||
if (name.startsWith('cb')) return index % 2 === 0 ? '☐' : '☑';
|
||||
if (/data|PanaLa/i.test(name)) return '27.05.2026';
|
||||
if (/email/i.test(name)) return 'hr.test@medpark.md';
|
||||
if (/telefon|fax/i.test(name)) return '+373 22 000 000';
|
||||
if (/idnp/i.test(name)) return `19850615000${index}`;
|
||||
if (/nr|numar|anNastere|ani/i.test(name)) return String(index);
|
||||
if (/doza/i.test(name)) return (index + 0.25).toFixed(4);
|
||||
return `Test ${name} ${index}`;
|
||||
}
|
||||
|
||||
function sampleDocxData(placeholders: string[]) {
|
||||
const data: Record<string, unknown> = {};
|
||||
for (const raw of placeholders) {
|
||||
const name = raw.replace(/^#|\//g, '');
|
||||
if (!name || raw.startsWith('#') || raw.startsWith('/')) continue;
|
||||
data[name] = valueForPlaceholder(name);
|
||||
}
|
||||
const row = (index: number) => {
|
||||
const values: Record<string, string> = {};
|
||||
for (const raw of placeholders) {
|
||||
const name = raw.replace(/^#|\//g, '');
|
||||
if (!name || raw.startsWith('#') || raw.startsWith('/')) continue;
|
||||
values[name] = valueForPlaceholder(name, index);
|
||||
}
|
||||
values.nr = String(index);
|
||||
values.numePrenume = index === 1 ? 'Popescu Alexandru' : 'Ionescu Maria';
|
||||
values.denumire = index === 1 ? 'Glutaraldehidă' : 'HBV/HCV/HIV';
|
||||
values.tipExpunere = index === 1 ? 'X externă' : 'gamma externă';
|
||||
return values;
|
||||
};
|
||||
for (const loopName of ['angajati', 'chimici', 'pulberi', 'biologici', 'zgomot', 'vibratii', 'campEM', 'optice', 'supraexpExceptionale', 'supraexpAccidentale']) {
|
||||
data[loopName] = [row(1), row(2)];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function verifyDocxTemplates() {
|
||||
const service = new DocxTemplateService();
|
||||
const files: Record<AnexaType, string> = {
|
||||
ANEXA_3: 'anexa-3.docx',
|
||||
ANEXA_4: 'anexa-4.docx',
|
||||
ANEXA_4A: 'anexa-4a.docx',
|
||||
ANEXA_4B: 'anexa-4b.docx',
|
||||
ANEXA_6: 'anexa-6.docx',
|
||||
};
|
||||
|
||||
for (const [type, fileName] of Object.entries(files) as [AnexaType, string][]) {
|
||||
const { placeholders, opens } = extractTemplatePlaceholders(fileName);
|
||||
assert(opens > 0, `${fileName} should contain docxtemplater placeholders`);
|
||||
const rendered = service.render(type, sampleDocxData(placeholders));
|
||||
const zip = new PizZip(rendered);
|
||||
const badParts: string[] = [];
|
||||
for (const name of Object.keys(zip.files).filter((entry) => /^word\/(document|header|footer).*\.xml$/.test(entry))) {
|
||||
const xml = zip.file(name)?.asText() ?? '';
|
||||
if (/[{}]/.test(xml) || /\b(undefined|null)\b/i.test(xml)) badParts.push(name);
|
||||
}
|
||||
assert(badParts.length === 0, `${fileName} rendered XML still contains placeholders/nulls in ${badParts.join(', ')}`);
|
||||
}
|
||||
|
||||
ok('all Anexa DOCX templates render cleanly through DocxTemplateService');
|
||||
}
|
||||
|
||||
async function isMinioAvailable() {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1500);
|
||||
try {
|
||||
const response = await fetch('http://localhost:9000/minio/health/live', { signal: controller.signal });
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function apiRequest(baseUrl: string, method: string, path: string, body?: unknown, token?: string): Promise<HttpResult> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 20_000);
|
||||
try {
|
||||
const headers: Record<string, string> = { Accept: 'application/json' };
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await response.text();
|
||||
let parsed: unknown = null;
|
||||
try {
|
||||
parsed = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
parsed = text;
|
||||
}
|
||||
return { status: response.status, body: parsed, text };
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < 3) await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000 * attempt));
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
throw new Error(`${method} ${path} failed after retries: ${String(lastError)}`);
|
||||
}
|
||||
|
||||
function tokenFrom(body: unknown) {
|
||||
if (typeof body === 'object' && body && 'token' in body && typeof (body as { token: unknown }).token === 'string') {
|
||||
return (body as { token: string }).token;
|
||||
}
|
||||
throw new Error('dev-login response did not include token');
|
||||
}
|
||||
|
||||
async function verifyHttpSmoke(minioAvailable: boolean) {
|
||||
const baseUrl = process.env.API_BASE_URL;
|
||||
if (!baseUrl) {
|
||||
warn('API_BASE_URL is not set; skipped HTTP smoke tests. testdb:run starts a temporary API automatically.');
|
||||
return;
|
||||
}
|
||||
|
||||
const adminLogin = await apiRequest(baseUrl, 'POST', '/auth/dev-login', { username: 'test-admin', role: 'hr_admin' });
|
||||
const specialistLogin = await apiRequest(baseUrl, 'POST', '/auth/dev-login', { username: 'test-specialist', role: 'hr_specialist' });
|
||||
const medicLogin = await apiRequest(baseUrl, 'POST', '/auth/dev-login', { username: 'test-medic', role: 'medic_familie' });
|
||||
assert(adminLogin.status === 201 || adminLogin.status === 200, `hr_admin dev-login failed: ${adminLogin.text}`);
|
||||
assert(specialistLogin.status === 201 || specialistLogin.status === 200, `hr_specialist dev-login failed: ${specialistLogin.text}`);
|
||||
assert(medicLogin.status === 201 || medicLogin.status === 200, `medic_familie dev-login failed: ${medicLogin.text}`);
|
||||
const adminToken = tokenFrom(adminLogin.body);
|
||||
const specialistToken = tokenFrom(specialistLogin.body);
|
||||
const medicToken = tokenFrom(medicLogin.body);
|
||||
|
||||
for (const path of ['/dashboard/stats', '/medical/risk-cards', '/medical/upcoming-expirations']) {
|
||||
const response = await apiRequest(baseUrl, 'GET', path, undefined, adminToken);
|
||||
assert(response.status >= 200 && response.status < 300, `GET ${path} failed: ${response.status} ${response.text}`);
|
||||
}
|
||||
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { status: 'activ', medicalProfile: { workplaceRiskCardId: { not: null } } },
|
||||
select: { id: true },
|
||||
take: 5,
|
||||
});
|
||||
assert(employees.length >= 5, 'HTTP bulk smoke needs at least five eligible employees');
|
||||
const bulkBody = {
|
||||
employeeIds: employees.map((employee) => employee.id),
|
||||
tip: MedicalCheckupType.periodic,
|
||||
dataPlanificata: '2026-06-15',
|
||||
documentContext: {
|
||||
telefon: '+373 22 000 000',
|
||||
fax: '+373 22 000 001',
|
||||
email: 'hr.test@medpark.md',
|
||||
solicitant: 'Test HR Admin',
|
||||
functia: 'Specialist resurse umane',
|
||||
},
|
||||
};
|
||||
|
||||
const forbidden = await apiRequest(baseUrl, 'POST', '/medical/bulk/initiate', bulkBody, specialistToken);
|
||||
assert(forbidden.status === 403, `hr_specialist bulk initiate should be 403, got ${forbidden.status}: ${forbidden.text}`);
|
||||
|
||||
if (!minioAvailable) {
|
||||
warn('MinIO is not reachable on localhost:9000; skipped full upload-dependent bulk success and medic completion HTTP tests.');
|
||||
ok('HTTP smoke passed for auth, read endpoints, and role-based 403');
|
||||
return;
|
||||
}
|
||||
|
||||
const bulk = await apiRequest(baseUrl, 'POST', '/medical/bulk/initiate', bulkBody, adminToken);
|
||||
assert(bulk.status >= 200 && bulk.status < 300, `hr_admin bulk initiate failed: ${bulk.status} ${bulk.text}`);
|
||||
assert(typeof bulk.body === 'object' && bulk.body && 'groupsCount' in bulk.body, 'bulk response should include groupsCount');
|
||||
|
||||
const pending = await prisma.medicalCheckup.findFirst({
|
||||
where: { verdict: null },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
assert(pending, 'No pending checkup found for medic completion smoke');
|
||||
const complete = await apiRequest(
|
||||
baseUrl,
|
||||
'PATCH',
|
||||
`/medical/checkups/${pending.id}/complete`,
|
||||
{
|
||||
verdict: MedicalVerdict.apt_conditionat,
|
||||
dataEfectuata: '2026-06-16',
|
||||
recomandari: 'Test: lucru cu dozimetru și reevaluare anuală.',
|
||||
valabilPanaLa: '2027-06-16',
|
||||
semnatDe: 'Dr. Verificare Test',
|
||||
},
|
||||
medicToken,
|
||||
);
|
||||
assert(complete.status >= 200 && complete.status < 300, `medic completion failed: ${complete.status} ${complete.text}`);
|
||||
|
||||
const completed = await prisma.medicalCheckup.findUnique({ where: { id: pending.id } });
|
||||
assert(completed?.verdict === MedicalVerdict.apt_conditionat, 'Medic completion did not persist verdict');
|
||||
assert(completed.semnatDe === 'Dr. Verificare Test', 'Medic completion did not persist semnatDe');
|
||||
assert(Array.isArray(completed.documenteGenerate), 'Completed checkup should contain generated documents');
|
||||
assert(JSON.stringify(completed.documenteGenerate).includes('Anexa_6_Final'), 'Completed checkup should include final Anexa 6 document');
|
||||
|
||||
ok('HTTP smoke passed, including MinIO-backed document generation');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dbName = requireTemporaryDatabase();
|
||||
console.log(`Verifying Medpark test database ${dbName}...`);
|
||||
await verifySourceFiles();
|
||||
await verifyDatabaseCoverage();
|
||||
await verifyDocxTemplates();
|
||||
const minioAvailable = await isMinioAvailable();
|
||||
if (minioAvailable) ok('MinIO is reachable on localhost:9000');
|
||||
else warn('MinIO is not reachable on localhost:9000; upload-dependent checks will be skipped or marked failed.');
|
||||
await verifyHttpSmoke(minioAvailable);
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log('\nVerification completed with warnings:');
|
||||
for (const message of warnings) console.log(`- ${message}`);
|
||||
} else {
|
||||
console.log('\nVerification completed without warnings.');
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user