TreatmentType and ProsthesisType database shcema and data updated. Lab dispatch wired through new data.

This commit is contained in:
2026-07-06 20:40:19 +03:30
parent 3e81c110a3
commit 667b08ed0c
48 changed files with 1813 additions and 275 deletions

View File

@@ -0,0 +1,276 @@
import { CatalogEntityKind } from '@prisma/client';
export type CatalogTranslationSeed = {
entityKind: CatalogEntityKind;
entityCode: string;
locale: string;
label: string;
};
export const TREATMENT_TYPES = [
{ code: 'restoration', labDependent: false, sortOrder: 1 },
{ code: 'specialized_restoration', labDependent: false, sortOrder: 2 },
{ code: 'radiography', labDependent: false, sortOrder: 3 },
{ code: 'endo', labDependent: false, sortOrder: 4 },
{ code: 'surgery', labDependent: false, sortOrder: 5 },
{ code: 'prosthesis', labDependent: true, sortOrder: 6 },
{ code: 'implant', labDependent: false, sortOrder: 7 },
{ code: 'orthodontics', labDependent: false, sortOrder: 8 },
{ code: 'perio', labDependent: false, sortOrder: 9 },
{ code: 'pediatrics', labDependent: false, sortOrder: 10 },
{ code: 'extraction', labDependent: false, sortOrder: 11 },
{ code: 'clinic_visit', labDependent: false, sortOrder: 12 },
] as const;
/** Legacy codes kept for historical rows; hidden from catalog. */
export const LEGACY_TREATMENT_TYPES = [
{ code: 'consultation', labDependent: false, sortOrder: 99 },
{ code: 'filling', labDependent: false, sortOrder: 100 },
{ code: 'visit', labDependent: false, sortOrder: 101 },
{ code: 'hygiene', labDependent: false, sortOrder: 102 },
] as const;
export const LAB_WORKFLOW_STEPS = [
{ code: 'intraoral_scan', sortOrder: 1 },
{ code: 'design', sortOrder: 2 },
{ code: 'milling_dry', sortOrder: 3 },
{ code: 'milling_wet', sortOrder: 4 },
{ code: 'printer_resin', sortOrder: 5 },
{ code: 'printer_metal', sortOrder: 6 },
{ code: 'sinter', sortOrder: 7 },
{ code: 'build_up', sortOrder: 8 },
{ code: 'stain', sortOrder: 9 },
{ code: 'glaze', sortOrder: 10 },
{ code: 'polish_prep', sortOrder: 11 },
{ code: 'packing', sortOrder: 12 },
{ code: 'shipping', sortOrder: 13 },
{ code: 'pressing', sortOrder: 14 },
] as const;
export type ProsthesisTypeSeed = {
code: string;
sortOrder: number;
skipPackingShipping?: boolean;
/** Manufacturing steps between design and packing (exclusive of universal scan/design/pack/ship). */
manufacturingSteps: readonly string[];
};
export const PROSTHESIS_TYPES: ProsthesisTypeSeed[] = [
{
code: 'pfm_crown',
sortOrder: 1,
manufacturingSteps: ['milling_wet', 'build_up', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'pfz_crown',
sortOrder: 2,
manufacturingSteps: ['milling_dry', 'sinter', 'build_up', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'monolithic_zirconia',
sortOrder: 3,
manufacturingSteps: ['milling_dry', 'sinter', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'glass_ceramic_crown',
sortOrder: 4,
manufacturingSteps: ['milling_wet', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'full_metal_crown',
sortOrder: 5,
manufacturingSteps: ['milling_wet', 'polish_prep'],
},
{
code: 'temporary_resin_crown',
sortOrder: 6,
manufacturingSteps: ['milling_wet', 'polish_prep'],
},
{
code: 'pmma',
sortOrder: 7,
manufacturingSteps: ['milling_dry', 'polish_prep'],
},
{
code: 'peek_crown',
sortOrder: 8,
manufacturingSteps: ['milling_dry', 'polish_prep'],
},
{
code: 'veneer_zirconia',
sortOrder: 9,
manufacturingSteps: ['milling_dry', 'sinter', 'build_up', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'veneer_ips_press',
sortOrder: 10,
manufacturingSteps: [
'printer_resin',
'build_up',
'stain',
'glaze',
'polish_prep',
'pressing',
],
},
{
code: 'veneer_ips_cad',
sortOrder: 11,
manufacturingSteps: ['milling_wet', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'soft_structure',
sortOrder: 12,
manufacturingSteps: ['milling_dry', 'sinter'],
},
{
code: 'customized_abutment',
sortOrder: 13,
manufacturingSteps: ['milling_wet', 'polish_prep'],
},
{
code: 'prefabricated_abutment',
sortOrder: 14,
manufacturingSteps: ['polish_prep'],
},
{
code: 'ti_base_abutment',
sortOrder: 15,
manufacturingSteps: ['polish_prep'],
},
{
code: 'multi_unit_abutment',
sortOrder: 16,
manufacturingSteps: ['polish_prep'],
},
{
code: 'zirconia_abutment',
sortOrder: 17,
manufacturingSteps: ['milling_dry', 'sinter', 'polish_prep'],
},
{
code: 'screw_retained',
sortOrder: 18,
manufacturingSteps: [
'milling_wet',
'printer_metal',
'sinter',
'build_up',
'stain',
'glaze',
'polish_prep',
],
},
{
code: 'zirconia_overlay',
sortOrder: 19,
manufacturingSteps: ['milling_dry', 'sinter', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'ips_overlay',
sortOrder: 20,
manufacturingSteps: ['milling_wet', 'stain', 'glaze', 'polish_prep'],
},
{
code: 'smile_design',
sortOrder: 21,
skipPackingShipping: true,
manufacturingSteps: ['printer_resin'],
},
{
code: 'mockup',
sortOrder: 22,
manufacturingSteps: ['printer_resin'],
},
];
const UNIVERSAL_PREFIX = ['intraoral_scan', 'design'] as const;
const UNIVERSAL_SUFFIX = ['packing', 'shipping'] as const;
export function buildProsthesisStepCodes(type: ProsthesisTypeSeed): string[] {
const steps = [...UNIVERSAL_PREFIX, ...type.manufacturingSteps];
if (!type.skipPackingShipping) {
steps.push(...UNIVERSAL_SUFFIX);
}
return steps;
}
const TREATMENT_LABELS: Record<string, Record<string, string>> = {
restoration: { en: 'Restoration', fa: 'ترمیم', nl: 'Restauratie' },
specialized_restoration: { en: 'Specialized Restoration', fa: 'ترمیم تخصصی', nl: 'Gespecialiseerde Restauratie' },
radiography: { en: 'Radiography', fa: 'رادیوگرافی', nl: 'Röntgen' },
endo: { en: 'Endo', fa: 'اندو', nl: 'Endo' },
surgery: { en: 'Surgery', fa: 'جراحی', nl: 'Chirurgie' },
prosthesis: { en: 'Prosthesis', fa: 'پروتز', nl: 'Prothese' },
implant: { en: 'Implant', fa: 'ایمپلنت', nl: 'Implantaat' },
orthodontics: { en: 'Orthodontics', fa: 'ارتودنسی', nl: 'Orthodontie' },
perio: { en: 'Perio', fa: 'پریو', nl: 'Paro' },
pediatrics: { en: 'Pediatrics', fa: 'اطفال', nl: 'Kinderen' },
extraction: { en: 'Extraction', fa: 'کشیدن', nl: 'Extractie' },
clinic_visit: { en: 'Clinic Visit', fa: 'درمانگاه', nl: 'Kliniekbezoek' },
consultation: { en: 'Consultation', fa: 'مشاوره', nl: 'Consult' },
filling: { en: 'Filling', fa: 'پر کردن', nl: 'Vulling' },
visit: { en: 'Visit', fa: 'ویزیت', nl: 'Bezoek' },
hygiene: { en: 'Hygiene', fa: 'بهداشت', nl: 'Hygiëne' },
};
const PROSTHESIS_LABELS: Record<string, Record<string, string>> = {
pfm_crown: { en: 'PFM Crown', fa: 'روکش PFM', nl: 'PFM Kroon' },
pfz_crown: { en: 'PFZ Crown', fa: 'روکش PFZ', nl: 'PFZ Kroon' },
monolithic_zirconia: { en: 'Monolithic Zirconia', fa: 'زیرکونیا مونولیتیک', nl: 'Monolithisch Zirconia' },
glass_ceramic_crown: { en: 'Glass Ceramic Crown', fa: 'روکش سرامیک شیشه‌ای', nl: 'Glaskeramische Kroon' },
full_metal_crown: { en: 'Full Metal Crown', fa: 'روکش تمام فلز', nl: 'Volledige Metalen Kroon' },
temporary_resin_crown: { en: 'Temporary Resin Crown', fa: 'روکش موقت رزینی', nl: 'Tijdelijke Harskroon' },
pmma: { en: 'PMMA', fa: 'PMMA', nl: 'PMMA' },
peek_crown: { en: 'PEEK Crown', fa: 'روکش PEEK', nl: 'PEEK Kroon' },
veneer_zirconia: { en: 'Veneer Zirconia', fa: 'ونیر زیرکونیا', nl: 'Veneer Zirconia' },
veneer_ips_press: { en: 'Veneer IPS Press', fa: 'ونیر IPS پرس', nl: 'Veneer IPS Press' },
veneer_ips_cad: { en: 'Veneer IPS CAD', fa: 'ونیر IPS CAD', nl: 'Veneer IPS CAD' },
soft_structure: { en: 'Soft Structure', fa: 'ساختار نرم', nl: 'Zachte Structuur' },
customized_abutment: { en: 'Customized Abutment', fa: 'اباتمنت سفارشی', nl: 'Aangepast Abutment' },
prefabricated_abutment: { en: 'Prefabricated Abutment', fa: 'اباتمنت آماده', nl: 'Prefab Abutment' },
ti_base_abutment: { en: 'Ti Base Abutment', fa: 'اباتمنت پایه تیتانیوم', nl: 'Ti Basis Abutment' },
multi_unit_abutment: { en: 'Multi Unit Abutment', fa: 'اباتمنت مولتی یونیت', nl: 'Multi Unit Abutment' },
zirconia_abutment: { en: 'Zirconia Abutment', fa: 'اباتمنت زیرکونیا', nl: 'Zirconia Abutment' },
screw_retained: { en: 'Screw Retained', fa: 'پیچی', nl: 'Schroefgehouden' },
zirconia_overlay: { en: 'Zirconia Overlay', fa: 'اورلی زیرکونیا', nl: 'Zirconia Overlay' },
ips_overlay: { en: 'IPS Overlay', fa: 'اورلی IPS', nl: 'IPS Overlay' },
smile_design: { en: 'Smile Design', fa: 'طراحی لبخند', nl: 'Smile Design' },
mockup: { en: 'Mockup', fa: 'ماکاپ', nl: 'Mockup' },
};
const WORKFLOW_STEP_LABELS: Record<string, Record<string, string>> = {
intraoral_scan: { en: 'Intraoral Scan', fa: 'اسکن داخل دهان', nl: 'Intraorale Scan' },
design: { en: 'Design', fa: 'طراحی', nl: 'Ontwerp' },
milling_dry: { en: 'Milling Dry', fa: 'فرز خشک', nl: 'Droog Frezen' },
milling_wet: { en: 'Milling Wet', fa: 'فرز تر', nl: 'Nat Frezen' },
printer_resin: { en: 'Printer Resin', fa: 'پرینتر رزین', nl: 'Harsprinter' },
printer_metal: { en: 'Printer Metal', fa: 'پرینتر فلز', nl: 'Metaalprinter' },
sinter: { en: 'Sinter', fa: 'سینتر', nl: 'Sinteren' },
build_up: { en: 'Build Up', fa: 'بیلدآپ', nl: 'Opbouw' },
stain: { en: 'Stain', fa: 'رنگ‌آمیزی', nl: 'Kleuren' },
glaze: { en: 'Glaze', fa: 'گلیز', nl: 'Glazuur' },
polish_prep: { en: 'Polish/Prep', fa: 'پولیش/آماده‌سازی', nl: 'Polijsten/Voorbereiding' },
packing: { en: 'Packing', fa: 'بسته‌بندی', nl: 'Verpakken' },
shipping: { en: 'Shipping', fa: 'ارسال', nl: 'Verzending' },
pressing: { en: 'Pressing', fa: 'پرس', nl: 'Persen' },
};
function labelsToTranslations(
entityKind: CatalogEntityKind,
labels: Record<string, Record<string, string>>,
): CatalogTranslationSeed[] {
const out: CatalogTranslationSeed[] = [];
for (const [entityCode, locales] of Object.entries(labels)) {
for (const [locale, label] of Object.entries(locales)) {
out.push({ entityKind, entityCode, locale, label });
}
}
return out;
}
export const CATALOG_TRANSLATIONS: CatalogTranslationSeed[] = [
...labelsToTranslations(CatalogEntityKind.TREATMENT_TYPE, TREATMENT_LABELS),
...labelsToTranslations(CatalogEntityKind.PROSTHESIS_TYPE, PROSTHESIS_LABELS),
...labelsToTranslations(CatalogEntityKind.LAB_WORKFLOW_STEP, WORKFLOW_STEP_LABELS),
];

View File

@@ -0,0 +1,95 @@
-- Prosthesis catalog refactor: drop treatment workflow steps, add prosthesis catalog tables
CREATE TYPE "CatalogEntityKind" AS ENUM ('TREATMENT_TYPE', 'PROSTHESIS_TYPE', 'LAB_WORKFLOW_STEP');
ALTER TABLE "treatment_types" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
DROP TABLE IF EXISTS "treatment_workflow_steps";
CREATE TABLE "catalog_translations" (
"id" TEXT NOT NULL,
"entityKind" "CatalogEntityKind" NOT NULL,
"entityCode" TEXT NOT NULL,
"locale" TEXT NOT NULL,
"label" TEXT NOT NULL,
CONSTRAINT "catalog_translations_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "catalog_translations_entityKind_entityCode_locale_key"
ON "catalog_translations"("entityKind", "entityCode", "locale");
CREATE TABLE "prosthesis_types" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"skipPackingShipping" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "prosthesis_types_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "prosthesis_types_code_key" ON "prosthesis_types"("code");
CREATE TABLE "lab_workflow_steps" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "lab_workflow_steps_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "lab_workflow_steps_code_key" ON "lab_workflow_steps"("code");
CREATE TABLE "prosthesis_type_steps" (
"id" TEXT NOT NULL,
"prosthesisTypeId" TEXT NOT NULL,
"labWorkflowStepId" TEXT NOT NULL,
"stepOrder" INTEGER NOT NULL,
CONSTRAINT "prosthesis_type_steps_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "prosthesis_type_steps_prosthesisTypeId_stepOrder_key"
ON "prosthesis_type_steps"("prosthesisTypeId", "stepOrder");
CREATE UNIQUE INDEX "prosthesis_type_steps_prosthesisTypeId_labWorkflowStepId_key"
ON "prosthesis_type_steps"("prosthesisTypeId", "labWorkflowStepId");
ALTER TABLE "prosthesis_type_steps"
ADD CONSTRAINT "prosthesis_type_steps_prosthesisTypeId_fkey"
FOREIGN KEY ("prosthesisTypeId") REFERENCES "prosthesis_types"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "prosthesis_type_steps"
ADD CONSTRAINT "prosthesis_type_steps_labWorkflowStepId_fkey"
FOREIGN KEY ("labWorkflowStepId") REFERENCES "lab_workflow_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "lab_case_tooth_prosthesis" (
"id" TEXT NOT NULL,
"labCaseId" TEXT NOT NULL,
"treatmentDetailId" TEXT NOT NULL,
"tooth" TEXT NOT NULL,
"prosthesisTypeCode" TEXT NOT NULL,
CONSTRAINT "lab_case_tooth_prosthesis_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "lab_case_tooth_prosthesis_labCaseId_treatmentDetailId_tooth_key"
ON "lab_case_tooth_prosthesis"("labCaseId", "treatmentDetailId", "tooth");
ALTER TABLE "lab_case_tooth_prosthesis"
ADD CONSTRAINT "lab_case_tooth_prosthesis_labCaseId_fkey"
FOREIGN KEY ("labCaseId") REFERENCES "lab_cases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "lab_case_tooth_prosthesis"
ADD CONSTRAINT "lab_case_tooth_prosthesis_treatmentDetailId_fkey"
FOREIGN KEY ("treatmentDetailId") REFERENCES "treatment_details"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Lab case tasks: add prosthesis fields and update unique constraint
ALTER TABLE "lab_case_tasks" ADD COLUMN "prosthesisTypeCode" TEXT NOT NULL DEFAULT '';
ALTER TABLE "lab_case_tasks" ADD COLUMN "workflowStepCode" TEXT NOT NULL DEFAULT '';
DROP INDEX IF EXISTS "lab_case_tasks_labCaseId_tooth_treatmentType_stepOrder_key";
CREATE UNIQUE INDEX "lab_case_tasks_labCaseId_treatmentDetailId_tooth_stepOrder_key"
ON "lab_case_tasks"("labCaseId", "treatmentDetailId", "tooth", "stepOrder");

View File

@@ -0,0 +1,41 @@
/**
* Dev-only: truncate treatment and lab case data (preserves catalog tables).
* Usage: npx ts-node prisma/reset-treatment-data.ts
*/
import { PrismaClient } from '@prisma/client';
import { config } from 'dotenv';
import path from 'path';
const envPath = path.join(__dirname, '..', '.env');
config({ path: envPath });
if (process.env.NODE_ENV === 'production') {
console.error('reset-treatment-data is not allowed in production');
process.exit(1);
}
const prisma = new PrismaClient();
async function main() {
console.log('Truncating treatment and lab case data...');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_tasks" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_sends" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_tooth_prosthesis" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_details" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_cases" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "treatment_detail_attachments" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "treatment_details" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "treatments" CASCADE');
console.log('Done.');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -155,6 +155,7 @@ model TreatmentDetail {
attachments TreatmentDetailAttachment[]
labCaseLink LabCaseDetail?
labCaseTasks LabCaseTask[]
toothProsthesis LabCaseToothProsthesis[]
@@index([treatmentId, sortOrder])
@@map("treatment_details")
@@ -188,10 +189,11 @@ model LabCase {
labComment String?
sentAt DateTime?
treatment Treatment @relation(fields: [treatmentId], references: [id], onDelete: Cascade)
details LabCaseDetail[]
sends LabCaseSend[]
tasks LabCaseTask[]
treatment Treatment @relation(fields: [treatmentId], references: [id], onDelete: Cascade)
details LabCaseDetail[]
sends LabCaseSend[]
tasks LabCaseTask[]
toothProsthesis LabCaseToothProsthesis[]
@@index([treatmentId, sortOrder])
@@map("lab_cases")
@@ -226,36 +228,92 @@ model TreatmentType {
code String @unique
labDependent Boolean @default(false)
sortOrder Int @default(0)
workflowSteps TreatmentWorkflowStep[]
isActive Boolean @default(true)
@@map("treatment_types")
}
model TreatmentWorkflowStep {
id String @id @default(uuid())
treatmentTypeId String
stepOrder Int
label String
enum CatalogEntityKind {
TREATMENT_TYPE
PROSTHESIS_TYPE
LAB_WORKFLOW_STEP
}
treatmentType TreatmentType @relation(fields: [treatmentTypeId], references: [id], onDelete: Cascade)
model CatalogTranslation {
id String @id @default(uuid())
entityKind CatalogEntityKind
entityCode String
locale String
label String
@@unique([treatmentTypeId, stepOrder])
@@map("treatment_workflow_steps")
@@unique([entityKind, entityCode, locale])
@@map("catalog_translations")
}
model ProsthesisType {
id String @id @default(uuid())
code String @unique
sortOrder Int @default(0)
isActive Boolean @default(true)
skipPackingShipping Boolean @default(false)
steps ProsthesisTypeStep[]
@@map("prosthesis_types")
}
model LabWorkflowStep {
id String @id @default(uuid())
code String @unique
sortOrder Int @default(0)
prosthesisSteps ProsthesisTypeStep[]
@@map("lab_workflow_steps")
}
model ProsthesisTypeStep {
id String @id @default(uuid())
prosthesisTypeId String
labWorkflowStepId String
stepOrder Int
prosthesisType ProsthesisType @relation(fields: [prosthesisTypeId], references: [id], onDelete: Cascade)
labWorkflowStep LabWorkflowStep @relation(fields: [labWorkflowStepId], references: [id], onDelete: Cascade)
@@unique([prosthesisTypeId, stepOrder])
@@unique([prosthesisTypeId, labWorkflowStepId])
@@map("prosthesis_type_steps")
}
model LabCaseToothProsthesis {
id String @id @default(uuid())
labCaseId String
treatmentDetailId String
tooth String
prosthesisTypeCode String
labCase LabCase @relation(fields: [labCaseId], references: [id], onDelete: Cascade)
detail TreatmentDetail @relation(fields: [treatmentDetailId], references: [id], onDelete: Cascade)
@@unique([labCaseId, treatmentDetailId, tooth])
@@map("lab_case_tooth_prosthesis")
}
model LabCaseTask {
id String @id @default(uuid())
labCaseId String
treatmentDetailId String
tooth String
treatmentType String
stepOrder Int
stepLabel String
assigneeUserId String?
assignedAt DateTime?
priority Int @default(3)
status LabTaskStatus @default(PENDING)
id String @id @default(uuid())
labCaseId String
treatmentDetailId String
tooth String
treatmentType String
prosthesisTypeCode String
workflowStepCode String
stepOrder Int
stepLabel String
assigneeUserId String?
assignedAt DateTime?
priority Int @default(3)
status LabTaskStatus @default(PENDING)
labCase LabCase @relation(fields: [labCaseId], references: [id], onDelete: Cascade)
detail TreatmentDetail @relation(fields: [treatmentDetailId], references: [id], onDelete: Cascade)
@@ -264,7 +322,7 @@ model LabCaseTask {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([labCaseId, tooth, treatmentType, stepOrder])
@@unique([labCaseId, treatmentDetailId, tooth, stepOrder])
@@index([labCaseId, status])
@@index([assigneeUserId, priority, createdAt])
@@index([assignedAt, labCaseId, priority])

View File

@@ -3,6 +3,14 @@ import { PrismaClient } from '@prisma/client';
import { randomUUID } from 'crypto';
import { config } from 'dotenv';
import path from 'path';
import {
TREATMENT_TYPES,
LEGACY_TREATMENT_TYPES,
LAB_WORKFLOW_STEPS,
PROSTHESIS_TYPES,
CATALOG_TRANSLATIONS,
buildProsthesisStepCodes,
} from './catalog-seed-data';
// Load environment variables from the correct path
const envPath = path.join(__dirname, '..', '.env');
@@ -128,57 +136,118 @@ async function main() {
}
console.log('✅ Created features and permissions');
const workflowSteps = [
{ code: 'endo', stepOrder: 1, label: 'Access review' },
{ code: 'endo', stepOrder: 2, label: 'Fabrication' },
] as const;
const treatmentTypes = [
{ code: 'consultation', labDependent: false, sortOrder: 1 },
{ code: 'filling', labDependent: false, sortOrder: 2 },
{ code: 'endo', labDependent: true, sortOrder: 3 },
{ code: 'visit', labDependent: false, sortOrder: 4 },
{ code: 'hygiene', labDependent: false, sortOrder: 5 },
] as const;
for (const type of treatmentTypes) {
for (const type of [...TREATMENT_TYPES, ...LEGACY_TREATMENT_TYPES]) {
const isActive = TREATMENT_TYPES.some((t) => t.code === type.code);
await prisma.treatmentType.upsert({
where: { code: type.code },
update: { labDependent: type.labDependent, sortOrder: type.sortOrder },
update: {
labDependent: type.labDependent,
sortOrder: type.sortOrder,
isActive,
},
create: {
id: randomUUID(),
code: type.code,
labDependent: type.labDependent,
sortOrder: type.sortOrder,
isActive,
},
});
}
console.log('✅ Seeded treatment type catalog');
for (const step of workflowSteps) {
const treatmentType = await prisma.treatmentType.findUniqueOrThrow({
for (const step of LAB_WORKFLOW_STEPS) {
await prisma.labWorkflowStep.upsert({
where: { code: step.code },
select: { id: true },
});
await prisma.treatmentWorkflowStep.upsert({
where: {
treatmentTypeId_stepOrder: {
treatmentTypeId: treatmentType.id,
stepOrder: step.stepOrder,
},
},
update: { label: step.label },
update: { sortOrder: step.sortOrder },
create: {
id: randomUUID(),
treatmentTypeId: treatmentType.id,
stepOrder: step.stepOrder,
label: step.label,
code: step.code,
sortOrder: step.sortOrder,
},
});
}
console.log('✅ Seeded lab workflow steps');
const workflowStepByCode = new Map(
(
await prisma.labWorkflowStep.findMany({
select: { id: true, code: true },
})
).map((s) => [s.code, s.id]),
);
for (const type of PROSTHESIS_TYPES) {
const prosthesisType = await prisma.prosthesisType.upsert({
where: { code: type.code },
update: {
sortOrder: type.sortOrder,
skipPackingShipping: type.skipPackingShipping ?? false,
isActive: true,
},
create: {
id: randomUUID(),
code: type.code,
sortOrder: type.sortOrder,
skipPackingShipping: type.skipPackingShipping ?? false,
isActive: true,
},
});
const stepCodes = buildProsthesisStepCodes(type);
for (const [index, stepCode] of stepCodes.entries()) {
const labWorkflowStepId = workflowStepByCode.get(stepCode);
if (!labWorkflowStepId) {
throw new Error(`Unknown workflow step code: ${stepCode}`);
}
await prisma.prosthesisTypeStep.upsert({
where: {
prosthesisTypeId_stepOrder: {
prosthesisTypeId: prosthesisType.id,
stepOrder: index + 1,
},
},
update: { labWorkflowStepId },
create: {
id: randomUUID(),
prosthesisTypeId: prosthesisType.id,
labWorkflowStepId,
stepOrder: index + 1,
},
});
}
}
console.log('✅ Seeded prosthesis types and workflow mappings');
for (const tr of CATALOG_TRANSLATIONS) {
const existing = await prisma.catalogTranslation.findFirst({
where: {
entityKind: tr.entityKind,
entityCode: tr.entityCode,
locale: tr.locale,
},
});
if (existing) {
await prisma.catalogTranslation.update({
where: { id: existing.id },
data: { label: tr.label },
});
} else {
await prisma.catalogTranslation.create({
data: {
id: randomUUID(),
entityKind: tr.entityKind,
entityCode: tr.entityCode,
locale: tr.locale,
label: tr.label,
},
});
}
}
console.log('✅ Seeded catalog translations');
console.log('🌱 Seeding completed successfully!');
}