improvement: new prosthesis type data structure implemented and finally working!
This commit is contained in:
@@ -29,7 +29,6 @@ export const TREATMENT_TYPES: readonly TreatmentTypeSeed[] = [
|
||||
{ code: 'perio', labDependent: false, sortOrder: 9 },
|
||||
{ code: 'pediatrics', labDependent: false, sortOrder: 10 },
|
||||
{ code: 'extraction', labDependent: false, sortOrder: 11 },
|
||||
// Appointment-only: not real treatment plan details.
|
||||
{
|
||||
code: 'clinic_visit',
|
||||
labDependent: false,
|
||||
@@ -69,140 +68,278 @@ export const LAB_WORKFLOW_STEPS = [
|
||||
{ code: 'pressing', sortOrder: 14 },
|
||||
] as const;
|
||||
|
||||
export type ProsthesisChartRegion = 'crown' | 'root' | 'arch';
|
||||
export type ProsthesisStackGroup = 'restoration' | 'implant' | 'post_core' | 'arch';
|
||||
export type ProsthesisAddonKind = '' | 'implant' | 'post_core';
|
||||
|
||||
export type ProsthesisTypeSeed = {
|
||||
code: string;
|
||||
sortOrder: number;
|
||||
skipPackingShipping?: boolean;
|
||||
isActive?: boolean;
|
||||
category: string;
|
||||
subcategory?: string;
|
||||
chartRegion: ProsthesisChartRegion;
|
||||
stackGroup: ProsthesisStackGroup;
|
||||
addonKind?: ProsthesisAddonKind;
|
||||
/** Manufacturing steps between design and packing (exclusive of universal scan/design/pack/ship). */
|
||||
manufacturingSteps: readonly string[];
|
||||
};
|
||||
|
||||
const FULL_CONTOUR = ['milling_wet', 'stain', 'glaze', 'polish_prep'] as const;
|
||||
const LAYERED = ['milling_dry', 'sinter', 'build_up', 'stain', 'glaze', 'polish_prep'] as const;
|
||||
const DENTURE = ['milling_wet', 'polish_prep'] as const;
|
||||
const APPLIANCE = ['printer_resin', 'polish_prep'] as const;
|
||||
const POST_CORE = ['milling_wet', 'polish_prep'] as const;
|
||||
|
||||
function crown(
|
||||
code: string,
|
||||
sortOrder: number,
|
||||
manufacturingSteps: readonly string[],
|
||||
extra?: Partial<ProsthesisTypeSeed>,
|
||||
): ProsthesisTypeSeed {
|
||||
return {
|
||||
code,
|
||||
sortOrder,
|
||||
category: 'crown',
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
manufacturingSteps,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function implantAddon(
|
||||
code: string,
|
||||
sortOrder: number,
|
||||
manufacturingSteps: readonly string[],
|
||||
): ProsthesisTypeSeed {
|
||||
return {
|
||||
code,
|
||||
sortOrder,
|
||||
category: 'implant',
|
||||
chartRegion: 'root',
|
||||
stackGroup: 'implant',
|
||||
addonKind: 'implant',
|
||||
manufacturingSteps,
|
||||
};
|
||||
}
|
||||
|
||||
function indirect(
|
||||
indication: string,
|
||||
technique: 'full_contour' | 'layered',
|
||||
sortOrder: number,
|
||||
): ProsthesisTypeSeed {
|
||||
return {
|
||||
code: `${indication}_${technique}`,
|
||||
sortOrder,
|
||||
category: 'indirect',
|
||||
subcategory: indication,
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
manufacturingSteps: technique === 'layered' ? LAYERED : FULL_CONTOUR,
|
||||
};
|
||||
}
|
||||
|
||||
export const PROSTHESIS_TYPES: ProsthesisTypeSeed[] = [
|
||||
crown('pfm_crown', 1, ['milling_wet', 'build_up', 'stain', 'glaze', 'polish_prep']),
|
||||
crown('pfz_crown', 2, ['milling_dry', 'sinter', 'build_up', 'stain', 'glaze', 'polish_prep']),
|
||||
crown('monolithic_zirconia', 3, ['milling_dry', 'sinter', 'stain', 'glaze', 'polish_prep']),
|
||||
crown('glass_ceramic_crown', 4, FULL_CONTOUR),
|
||||
crown('full_metal_crown', 5, ['milling_wet', 'polish_prep']),
|
||||
crown('temporary_resin_crown', 6, ['milling_wet', 'polish_prep']),
|
||||
crown('pmma', 7, ['milling_dry', 'polish_prep']),
|
||||
crown('peek_crown', 8, ['milling_dry', 'polish_prep']),
|
||||
crown('press_ceramic', 9, ['printer_resin', 'pressing', 'stain', 'glaze', 'polish_prep']),
|
||||
indirect('veneer', 'full_contour', 20),
|
||||
indirect('veneer', 'layered', 21),
|
||||
indirect('inlay', 'full_contour', 22),
|
||||
indirect('inlay', 'layered', 23),
|
||||
indirect('onlay', 'full_contour', 24),
|
||||
indirect('onlay', 'layered', 25),
|
||||
indirect('overlay', 'full_contour', 26),
|
||||
indirect('overlay', 'layered', 27),
|
||||
{
|
||||
code: 'pfm_crown',
|
||||
sortOrder: 1,
|
||||
manufacturingSteps: ['milling_wet', 'build_up', 'stain', 'glaze', 'polish_prep'],
|
||||
code: 'cast_post_core',
|
||||
sortOrder: 30,
|
||||
category: 'post_core',
|
||||
chartRegion: 'root',
|
||||
stackGroup: 'post_core',
|
||||
addonKind: 'post_core',
|
||||
manufacturingSteps: POST_CORE,
|
||||
},
|
||||
{
|
||||
code: 'pfz_crown',
|
||||
sortOrder: 2,
|
||||
manufacturingSteps: ['milling_dry', 'sinter', 'build_up', 'stain', 'glaze', 'polish_prep'],
|
||||
code: 'fiber_post_core',
|
||||
sortOrder: 31,
|
||||
category: 'post_core',
|
||||
chartRegion: 'root',
|
||||
stackGroup: 'post_core',
|
||||
addonKind: 'post_core',
|
||||
manufacturingSteps: POST_CORE,
|
||||
},
|
||||
implantAddon('prefabricated_abutment', 40, ['polish_prep']),
|
||||
implantAddon('ti_base_abutment', 41, ['polish_prep']),
|
||||
implantAddon('multi_unit_abutment', 42, ['polish_prep']),
|
||||
implantAddon('customized_abutment', 43, ['milling_wet', 'polish_prep']),
|
||||
implantAddon('zirconia_abutment', 44, ['milling_dry', 'sinter', '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,
|
||||
code: 'screw_retained',
|
||||
sortOrder: 45,
|
||||
category: 'implant',
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
manufacturingSteps: ['milling_wet', 'polish_prep'],
|
||||
},
|
||||
{
|
||||
code: 'temporary_resin_crown',
|
||||
sortOrder: 6,
|
||||
manufacturingSteps: ['milling_wet', 'polish_prep'],
|
||||
code: 'complete_denture',
|
||||
sortOrder: 50,
|
||||
category: 'removable',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: DENTURE,
|
||||
},
|
||||
{
|
||||
code: 'pmma',
|
||||
sortOrder: 7,
|
||||
manufacturingSteps: ['milling_dry', 'polish_prep'],
|
||||
code: 'partial_denture',
|
||||
sortOrder: 51,
|
||||
category: 'removable',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: DENTURE,
|
||||
},
|
||||
{
|
||||
code: 'peek_crown',
|
||||
sortOrder: 8,
|
||||
manufacturingSteps: ['milling_dry', 'polish_prep'],
|
||||
code: 'overdenture',
|
||||
sortOrder: 52,
|
||||
category: 'removable',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: DENTURE,
|
||||
},
|
||||
{
|
||||
code: 'veneer_zirconia',
|
||||
sortOrder: 9,
|
||||
manufacturingSteps: ['milling_dry', 'sinter', 'build_up', 'stain', 'glaze', 'polish_prep'],
|
||||
code: 'night_guard_soft',
|
||||
sortOrder: 60,
|
||||
category: 'appliance',
|
||||
subcategory: 'night_guard',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: APPLIANCE,
|
||||
},
|
||||
{
|
||||
code: 'veneer_ips_press',
|
||||
sortOrder: 10,
|
||||
manufacturingSteps: [
|
||||
'printer_resin',
|
||||
'build_up',
|
||||
'stain',
|
||||
'glaze',
|
||||
'polish_prep',
|
||||
'pressing',
|
||||
],
|
||||
code: 'night_guard_hard',
|
||||
sortOrder: 61,
|
||||
category: 'appliance',
|
||||
subcategory: 'night_guard',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: APPLIANCE,
|
||||
},
|
||||
{
|
||||
code: 'veneer_ips_cad',
|
||||
sortOrder: 11,
|
||||
manufacturingSteps: ['milling_wet', 'stain', 'glaze', 'polish_prep'],
|
||||
code: 'night_guard_dual',
|
||||
sortOrder: 62,
|
||||
category: 'appliance',
|
||||
subcategory: 'night_guard',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: APPLIANCE,
|
||||
},
|
||||
{
|
||||
code: 'bleaching_tray',
|
||||
sortOrder: 63,
|
||||
category: 'appliance',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: APPLIANCE,
|
||||
},
|
||||
{
|
||||
code: 'clear_aligner',
|
||||
sortOrder: 64,
|
||||
category: 'appliance',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: APPLIANCE,
|
||||
},
|
||||
{
|
||||
code: 'soft_structure',
|
||||
sortOrder: 12,
|
||||
sortOrder: 65,
|
||||
category: 'appliance',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: ['milling_dry', 'sinter'],
|
||||
},
|
||||
{
|
||||
code: 'customized_abutment',
|
||||
sortOrder: 13,
|
||||
manufacturingSteps: ['milling_wet', 'polish_prep'],
|
||||
code: 'surgical_guide',
|
||||
sortOrder: 70,
|
||||
category: 'digital',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: APPLIANCE,
|
||||
},
|
||||
{
|
||||
code: 'prefabricated_abutment',
|
||||
sortOrder: 14,
|
||||
manufacturingSteps: ['polish_prep'],
|
||||
code: 'smile_design',
|
||||
sortOrder: 71,
|
||||
category: 'digital',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
skipPackingShipping: true,
|
||||
manufacturingSteps: [],
|
||||
},
|
||||
{
|
||||
code: 'ti_base_abutment',
|
||||
sortOrder: 15,
|
||||
manufacturingSteps: ['polish_prep'],
|
||||
code: 'mockup',
|
||||
sortOrder: 72,
|
||||
category: 'digital',
|
||||
chartRegion: 'arch',
|
||||
stackGroup: 'arch',
|
||||
manufacturingSteps: ['printer_resin'],
|
||||
},
|
||||
// Legacy leaves — labels for historical cases; hidden from the picker.
|
||||
{
|
||||
code: 'veneer_zirconia',
|
||||
sortOrder: 200,
|
||||
isActive: false,
|
||||
category: 'indirect',
|
||||
subcategory: 'veneer',
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
manufacturingSteps: LAYERED,
|
||||
},
|
||||
{
|
||||
code: 'multi_unit_abutment',
|
||||
sortOrder: 16,
|
||||
manufacturingSteps: ['polish_prep'],
|
||||
code: 'veneer_ips_press',
|
||||
sortOrder: 201,
|
||||
isActive: false,
|
||||
category: 'indirect',
|
||||
subcategory: 'veneer',
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
manufacturingSteps: ['printer_resin', 'build_up', 'stain', 'glaze', 'polish_prep', 'pressing'],
|
||||
},
|
||||
{
|
||||
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: 'veneer_ips_cad',
|
||||
sortOrder: 202,
|
||||
isActive: false,
|
||||
category: 'indirect',
|
||||
subcategory: 'veneer',
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
manufacturingSteps: FULL_CONTOUR,
|
||||
},
|
||||
{
|
||||
code: 'zirconia_overlay',
|
||||
sortOrder: 19,
|
||||
sortOrder: 203,
|
||||
isActive: false,
|
||||
category: 'indirect',
|
||||
subcategory: 'overlay',
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
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'],
|
||||
sortOrder: 204,
|
||||
isActive: false,
|
||||
category: 'indirect',
|
||||
subcategory: 'overlay',
|
||||
chartRegion: 'crown',
|
||||
stackGroup: 'restoration',
|
||||
manufacturingSteps: FULL_CONTOUR,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -217,6 +354,12 @@ export function buildProsthesisStepCodes(type: ProsthesisTypeSeed): string[] {
|
||||
return steps;
|
||||
}
|
||||
|
||||
export {
|
||||
unionWorkflowStepCodes,
|
||||
prosthesisGroupKey,
|
||||
splitProsthesisGroupKey,
|
||||
} from '../src/common/prosthesis-group';
|
||||
|
||||
const TREATMENT_LABELS: Record<string, Record<string, string>> = {
|
||||
restoration: { en: 'Restoration', fa: 'ترمیم', nl: 'Restauratie' },
|
||||
specialized_restoration: { en: 'Specialized Restoration', fa: 'ترمیم تخصصی', nl: 'Gespecialiseerde Restauratie' },
|
||||
@@ -237,7 +380,7 @@ const TREATMENT_LABELS: Record<string, Record<string, string>> = {
|
||||
hygiene: { en: 'Hygiene', fa: 'بهداشت', nl: 'Hygiëne' },
|
||||
};
|
||||
|
||||
const PROSTHESIS_LABELS: Record<string, Record<string, string>> = {
|
||||
export 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' },
|
||||
@@ -246,23 +389,43 @@ const PROSTHESIS_LABELS: Record<string, Record<string, string>> = {
|
||||
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' },
|
||||
press_ceramic: { en: 'Pressed Ceramic / IPS e.max', fa: 'سرامیک پرس / IPS e.max', nl: 'Perskeramiek / IPS e.max' },
|
||||
veneer_full_contour: { en: 'Veneer · Full contour', fa: 'ونیر · تمامکانتور', nl: 'Veneer · Full contour' },
|
||||
veneer_layered: { en: 'Veneer · Layered ceramic', fa: 'ونیر · سرامیک لایهای', nl: 'Veneer · Gelaagd keramiek' },
|
||||
inlay_full_contour: { en: 'Inlay · Full contour', fa: 'اینلی · تمامکانتور', nl: 'Inlay · Full contour' },
|
||||
inlay_layered: { en: 'Inlay · Layered ceramic', fa: 'اینلی · سرامیک لایهای', nl: 'Inlay · Gelaagd keramiek' },
|
||||
onlay_full_contour: { en: 'Onlay · Full contour', fa: 'آنلی · تمامکانتور', nl: 'Onlay · Full contour' },
|
||||
onlay_layered: { en: 'Onlay · Layered ceramic', fa: 'آنلی · سرامیک لایهای', nl: 'Onlay · Gelaagd keramiek' },
|
||||
overlay_full_contour: { en: 'Overlay · Full contour', fa: 'اورلی · تمامکانتور', nl: 'Overlay · Full contour' },
|
||||
overlay_layered: { en: 'Overlay · Layered ceramic', fa: 'اورلی · سرامیک لایهای', nl: 'Overlay · Gelaagd keramiek' },
|
||||
cast_post_core: { en: 'Cast Post & Core', fa: 'پست و کور ریختگی', nl: 'Gegoten Stiftopbouw' },
|
||||
fiber_post_core: { en: 'Fiber Post & Core', fa: 'پست و کور فایبر', nl: 'Fiber Stiftopbouw' },
|
||||
customized_abutment: { en: 'Custom Abutment (Titanium)', fa: 'اباتمنت سفارشی (تیتانیوم)', nl: 'Aangepast abutment (titanium)' },
|
||||
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' },
|
||||
zirconia_abutment: { en: 'Custom Abutment (Zirconia)', fa: 'اباتمنت سفارشی (زیرکونیا)', nl: 'Aangepast abutment (zirconia)' },
|
||||
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' },
|
||||
complete_denture: { en: 'Complete Denture', fa: 'دنچر کامل', nl: 'Volledige prothese' },
|
||||
partial_denture: { en: 'Partial Denture', fa: 'دنچر پارسیل', nl: 'Partiële prothese' },
|
||||
overdenture: { en: 'Overdenture', fa: 'اوردنچر', nl: 'Overkappingsprothese' },
|
||||
night_guard_soft: { en: 'Night Guard · Soft', fa: 'نایت گارد · نرم', nl: 'Nachtbeugel · Zacht' },
|
||||
night_guard_hard: { en: 'Night Guard · Hard', fa: 'نایت گارد · سخت', nl: 'Nachtbeugel · Hard' },
|
||||
night_guard_dual: { en: 'Night Guard · Dual laminate', fa: 'نایت گارد · دو لایه', nl: 'Nachtbeugel · Dual laminate' },
|
||||
bleaching_tray: { en: 'Bleaching Tray', fa: 'تری بلیچینگ', nl: 'Bleeklepel' },
|
||||
clear_aligner: { en: 'Clear Aligner', fa: 'الاینر شفاف', nl: 'Clear aligner' },
|
||||
soft_structure: { en: 'Soft Structure', fa: 'ساختار نرم', nl: 'Zachte Structuur' },
|
||||
surgical_guide: { en: 'Surgical Guide', fa: 'گاید جراحی', nl: 'Chirurgische mal' },
|
||||
smile_design: { en: 'Smile Design', fa: 'طراحی لبخند', nl: 'Smile Design' },
|
||||
mockup: { en: 'Mockup', fa: 'ماکاپ', nl: 'Mockup' },
|
||||
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' },
|
||||
zirconia_overlay: { en: 'Zirconia Overlay', fa: 'اورلی زیرکونیا', nl: 'Zirconia Overlay' },
|
||||
ips_overlay: { en: 'IPS Overlay', fa: 'اورلی IPS', nl: 'IPS Overlay' },
|
||||
};
|
||||
|
||||
const WORKFLOW_STEP_LABELS: Record<string, Record<string, string>> = {
|
||||
export 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' },
|
||||
|
||||
286
backend/prisma/export-prosthesis-catalog.ts
Normal file
286
backend/prisma/export-prosthesis-catalog.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Regenerates docs/prosthesis-catalog.xlsx from catalog-seed-data.ts.
|
||||
*
|
||||
* cd backend && npx ts-node --transpile-only prisma/export-prosthesis-catalog.ts
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as zlib from 'zlib';
|
||||
import {
|
||||
PROSTHESIS_TYPES,
|
||||
PROSTHESIS_LABELS,
|
||||
WORKFLOW_STEP_LABELS,
|
||||
buildProsthesisStepCodes,
|
||||
} from './catalog-seed-data';
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
crown: 'Crowns',
|
||||
indirect: 'Indirect restorations',
|
||||
implant: 'Implants',
|
||||
post_core: 'Post & core',
|
||||
removable: 'Removable',
|
||||
appliance: 'Appliances',
|
||||
digital: 'Digital',
|
||||
};
|
||||
|
||||
const SUBCATEGORY_LABELS: Record<string, string> = {
|
||||
full_contour: 'Full contour',
|
||||
layered: 'Layered',
|
||||
veneer: 'Veneer',
|
||||
inlay: 'Inlay',
|
||||
onlay: 'Onlay',
|
||||
overlay: 'Overlay',
|
||||
complete_denture: 'Complete denture',
|
||||
partial_denture: 'Partial denture',
|
||||
night_guard: 'Night guard',
|
||||
};
|
||||
|
||||
const STACK_RULES = [
|
||||
['Slot', 'Rule'],
|
||||
[
|
||||
'restoration',
|
||||
'One per tooth: crown, veneer, inlay, onlay, overlay, or screw-retained.',
|
||||
],
|
||||
['implant', 'One abutment add-on per tooth (Ti-base, custom, prefab, multi-unit, zirconia).'],
|
||||
['post_core', 'One post & core, only if a restoration is already present.'],
|
||||
[
|
||||
'arch',
|
||||
'One arch product (denture, night guard, digital). Most arch products block implant add-ons; overdenture may take an implant add-on.',
|
||||
],
|
||||
[
|
||||
'screw_retained',
|
||||
'Fills the restoration slot and blocks implant + post & core (it is the restoration).',
|
||||
],
|
||||
['maximum', 'Usual maximum is 3 types per tooth (e.g. zirconia crown + Ti-base + fiber post).'],
|
||||
['two_crowns', 'Two crowns cannot stack.'],
|
||||
];
|
||||
|
||||
function xmlEscape(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function colLetter(index: number): string {
|
||||
let n = index + 1;
|
||||
let s = '';
|
||||
while (n > 0) {
|
||||
const r = (n - 1) % 26;
|
||||
s = String.fromCharCode(65 + r) + s;
|
||||
n = Math.floor((n - 1) / 26);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function sheetXml(rows: string[][]): string {
|
||||
const cells = rows
|
||||
.map((row, r) => {
|
||||
const rowNum = r + 1;
|
||||
const inner = row
|
||||
.map((value, c) => {
|
||||
const ref = `${colLetter(c)}${rowNum}`;
|
||||
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${xmlEscape(value)}</t></is></c>`;
|
||||
})
|
||||
.join('');
|
||||
return `<row r="${rowNum}">${inner}</row>`;
|
||||
})
|
||||
.join('');
|
||||
const dim = `A1:${colLetter(Math.max(0, ...(rows.map((r) => r.length - 1))))}${rows.length || 1}`;
|
||||
return (
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">` +
|
||||
`<dimension ref="${dim}"/>` +
|
||||
`<sheetData>${cells}</sheetData>` +
|
||||
`</worksheet>`
|
||||
);
|
||||
}
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
let c = i;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
table[i] = c >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(buf: Buffer): number {
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function zipStore(files: Array<{ name: string; data: Buffer }>): Buffer {
|
||||
const locals: Buffer[] = [];
|
||||
const centrals: Buffer[] = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const name = Buffer.from(file.name, 'utf8');
|
||||
const compressed = zlib.deflateRawSync(file.data);
|
||||
const crc = crc32(file.data);
|
||||
const local = Buffer.alloc(30 + name.length);
|
||||
local.writeUInt32LE(0x04034b50, 0);
|
||||
local.writeUInt16LE(20, 4);
|
||||
local.writeUInt16LE(0, 6);
|
||||
local.writeUInt16LE(8, 8);
|
||||
local.writeUInt16LE(0, 10);
|
||||
local.writeUInt16LE(0, 12);
|
||||
local.writeUInt32LE(crc, 14);
|
||||
local.writeUInt32LE(compressed.length, 18);
|
||||
local.writeUInt32LE(file.data.length, 22);
|
||||
local.writeUInt16LE(name.length, 26);
|
||||
local.writeUInt16LE(0, 28);
|
||||
name.copy(local, 30);
|
||||
locals.push(local, compressed);
|
||||
|
||||
const central = Buffer.alloc(46 + name.length);
|
||||
central.writeUInt32LE(0x02014b50, 0);
|
||||
central.writeUInt16LE(20, 4);
|
||||
central.writeUInt16LE(20, 6);
|
||||
central.writeUInt16LE(0, 8);
|
||||
central.writeUInt16LE(8, 10);
|
||||
central.writeUInt16LE(0, 12);
|
||||
central.writeUInt16LE(0, 14);
|
||||
central.writeUInt32LE(crc, 16);
|
||||
central.writeUInt32LE(compressed.length, 20);
|
||||
central.writeUInt32LE(file.data.length, 24);
|
||||
central.writeUInt16LE(name.length, 28);
|
||||
central.writeUInt16LE(0, 30);
|
||||
central.writeUInt16LE(0, 32);
|
||||
central.writeUInt16LE(0, 34);
|
||||
central.writeUInt16LE(0, 36);
|
||||
central.writeUInt32LE(0, 38);
|
||||
central.writeUInt32LE(offset, 42);
|
||||
name.copy(central, 46);
|
||||
centrals.push(central);
|
||||
|
||||
offset += local.length + compressed.length;
|
||||
}
|
||||
|
||||
const centralDir = Buffer.concat(centrals);
|
||||
const eocd = Buffer.alloc(22);
|
||||
eocd.writeUInt32LE(0x06054b50, 0);
|
||||
eocd.writeUInt16LE(0, 4);
|
||||
eocd.writeUInt16LE(0, 6);
|
||||
eocd.writeUInt16LE(files.length, 8);
|
||||
eocd.writeUInt16LE(files.length, 10);
|
||||
eocd.writeUInt32LE(centralDir.length, 12);
|
||||
eocd.writeUInt32LE(offset, 16);
|
||||
eocd.writeUInt16LE(0, 20);
|
||||
|
||||
return Buffer.concat([...locals, centralDir, eocd]);
|
||||
}
|
||||
|
||||
function buildWorkbook(): Buffer {
|
||||
const typeRows: string[][] = [
|
||||
[
|
||||
'category',
|
||||
'category_label',
|
||||
'subcategory',
|
||||
'subcategory_label',
|
||||
'code',
|
||||
'label_en',
|
||||
'label_fa',
|
||||
'label_nl',
|
||||
'chartRegion',
|
||||
'stackGroup',
|
||||
'addonKind',
|
||||
'active',
|
||||
],
|
||||
];
|
||||
for (const type of PROSTHESIS_TYPES) {
|
||||
const labels = PROSTHESIS_LABELS[type.code] ?? {};
|
||||
typeRows.push([
|
||||
type.category,
|
||||
CATEGORY_LABELS[type.category] ?? type.category,
|
||||
type.subcategory ?? '',
|
||||
type.subcategory ? SUBCATEGORY_LABELS[type.subcategory] ?? type.subcategory : '',
|
||||
type.code,
|
||||
labels.en ?? type.code,
|
||||
labels.fa ?? '',
|
||||
labels.nl ?? '',
|
||||
type.chartRegion,
|
||||
type.stackGroup,
|
||||
type.addonKind ?? '',
|
||||
type.isActive === false ? 'false' : 'true',
|
||||
]);
|
||||
}
|
||||
|
||||
const stepRows: string[][] = [
|
||||
['code', 'label_en', 'step_order', 'step_code', 'step_en', 'step_fa', 'step_nl'],
|
||||
];
|
||||
for (const type of PROSTHESIS_TYPES) {
|
||||
if (type.isActive === false) continue;
|
||||
const labels = PROSTHESIS_LABELS[type.code] ?? {};
|
||||
const steps = buildProsthesisStepCodes(type);
|
||||
steps.forEach((stepCode, index) => {
|
||||
const stepLabels = WORKFLOW_STEP_LABELS[stepCode] ?? {};
|
||||
stepRows.push([
|
||||
type.code,
|
||||
labels.en ?? type.code,
|
||||
String(index + 1),
|
||||
stepCode,
|
||||
stepLabels.en ?? stepCode,
|
||||
stepLabels.fa ?? '',
|
||||
stepLabels.nl ?? '',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
const contentTypes =
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
|
||||
`<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
|
||||
`<Default Extension="xml" ContentType="application/xml"/>` +
|
||||
`<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` +
|
||||
`<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
|
||||
`<Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
|
||||
`<Override PartName="/xl/worksheets/sheet3.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
|
||||
`</Types>`;
|
||||
|
||||
const rels =
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
||||
`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>` +
|
||||
`</Relationships>`;
|
||||
|
||||
const workbook =
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ` +
|
||||
`xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">` +
|
||||
`<sheets>` +
|
||||
`<sheet name="Types" sheetId="1" r:id="rId1"/>` +
|
||||
`<sheet name="Steps" sheetId="2" r:id="rId2"/>` +
|
||||
`<sheet name="Stack rules" sheetId="3" r:id="rId3"/>` +
|
||||
`</sheets>` +
|
||||
`</workbook>`;
|
||||
|
||||
const workbookRels =
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
||||
`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>` +
|
||||
`<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>` +
|
||||
`<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet3.xml"/>` +
|
||||
`</Relationships>`;
|
||||
|
||||
return zipStore([
|
||||
{ name: '[Content_Types].xml', data: Buffer.from(contentTypes, 'utf8') },
|
||||
{ name: '_rels/.rels', data: Buffer.from(rels, 'utf8') },
|
||||
{ name: 'xl/workbook.xml', data: Buffer.from(workbook, 'utf8') },
|
||||
{ name: 'xl/_rels/workbook.xml.rels', data: Buffer.from(workbookRels, 'utf8') },
|
||||
{ name: 'xl/worksheets/sheet1.xml', data: Buffer.from(sheetXml(typeRows), 'utf8') },
|
||||
{ name: 'xl/worksheets/sheet2.xml', data: Buffer.from(sheetXml(stepRows), 'utf8') },
|
||||
{ name: 'xl/worksheets/sheet3.xml', data: Buffer.from(sheetXml(STACK_RULES), 'utf8') },
|
||||
]);
|
||||
}
|
||||
|
||||
const outPath = path.resolve(__dirname, '../../docs/prosthesis-catalog.xlsx');
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, buildWorkbook());
|
||||
console.log(`Wrote ${outPath}`);
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Prosthesis type tree fields + multiple work items per tooth.
|
||||
|
||||
ALTER TABLE "prosthesis_types"
|
||||
ADD COLUMN IF NOT EXISTS "category" TEXT NOT NULL DEFAULT 'crown',
|
||||
ADD COLUMN IF NOT EXISTS "subcategory" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS "chartRegion" TEXT NOT NULL DEFAULT 'crown',
|
||||
ADD COLUMN IF NOT EXISTS "stackGroup" TEXT NOT NULL DEFAULT 'restoration',
|
||||
ADD COLUMN IF NOT EXISTS "addonKind" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE "lab_case_tooth_prosthesis"
|
||||
DROP CONSTRAINT IF EXISTS "lab_case_tooth_prosthesis_labCaseId_sourceKey_tooth_key";
|
||||
|
||||
DROP INDEX IF EXISTS "lab_case_tooth_prosthesis_labCaseId_sourceKey_tooth_key";
|
||||
|
||||
-- Deduplicate stacked rows that would collide after adding prosthesisTypeCode to the unique key
|
||||
-- (legacy unique already forbade two types per tooth, so this is a no-op for current data).
|
||||
DELETE FROM "lab_case_tooth_prosthesis" AS a
|
||||
USING "lab_case_tooth_prosthesis" AS b
|
||||
WHERE a."id" > b."id"
|
||||
AND a."labCaseId" = b."labCaseId"
|
||||
AND a."sourceKey" = b."sourceKey"
|
||||
AND a."tooth" = b."tooth"
|
||||
AND a."prosthesisTypeCode" = b."prosthesisTypeCode";
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "lab_case_tooth_prosthesis_case_source_tooth_type_key"
|
||||
ON "lab_case_tooth_prosthesis"("labCaseId", "sourceKey", "tooth", "prosthesisTypeCode");
|
||||
@@ -0,0 +1,12 @@
|
||||
-- One LabCaseTask pipeline per tooth: unique must include tooth.
|
||||
ALTER TABLE "lab_case_tasks" ADD COLUMN IF NOT EXISTS "tooth" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE "lab_case_tasks"
|
||||
SET "tooth" = COALESCE("teeth"->>0, '')
|
||||
WHERE "tooth" = '';
|
||||
|
||||
ALTER TABLE "lab_case_tasks" DROP CONSTRAINT IF EXISTS "lab_case_tasks_case_source_prosthesis_step_key";
|
||||
DROP INDEX IF EXISTS "lab_case_tasks_case_source_prosthesis_step_key";
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "lab_case_tasks_case_source_tooth_prosthesis_step_key"
|
||||
ON "lab_case_tasks"("labCaseId", "sourceKey", "tooth", "prosthesisTypeCode", "stepOrder");
|
||||
@@ -351,6 +351,16 @@ model ProsthesisType {
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
skipPackingShipping Boolean @default(false)
|
||||
/// Picker column: crown | indirect | implant | post_core | removable | appliance | digital
|
||||
category String @default("crown")
|
||||
/// Optional second drill (veneer/inlay/onlay/overlay, night_guard, …). Empty if none.
|
||||
subcategory String @default("")
|
||||
/// Chart tint: crown | root | arch
|
||||
chartRegion String @default("crown")
|
||||
/// Mutual exclusion group: restoration | implant | post_core | arch
|
||||
stackGroup String @default("restoration")
|
||||
/// Also listed as an add-on: implant | post_core | empty
|
||||
addonKind String @default("")
|
||||
|
||||
steps ProsthesisTypeStep[]
|
||||
|
||||
@@ -398,7 +408,7 @@ model LabCaseToothProsthesis {
|
||||
detail TreatmentDetail? @relation(fields: [treatmentDetailId], references: [id], onDelete: Cascade)
|
||||
line LabCaseLine? @relation(fields: [lineId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([labCaseId, sourceKey, tooth])
|
||||
@@unique([labCaseId, sourceKey, tooth, prosthesisTypeCode], map: "lab_case_tooth_prosthesis_case_source_tooth_type_key")
|
||||
@@map("lab_case_tooth_prosthesis")
|
||||
}
|
||||
|
||||
@@ -409,6 +419,8 @@ model LabCaseTask {
|
||||
lineId String?
|
||||
/// treatmentDetailId (clinic) or lineId (lab-origin) — required unique grouping key.
|
||||
sourceKey String
|
||||
/// Single FDI tooth or arch sentinel (UA/LA). One task pipeline per tooth.
|
||||
tooth String @default("")
|
||||
teeth Json
|
||||
treatmentType String
|
||||
prosthesisTypeCode String
|
||||
@@ -433,7 +445,7 @@ model LabCaseTask {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([labCaseId, sourceKey, prosthesisTypeCode, stepOrder], map: "lab_case_tasks_case_source_prosthesis_step_key")
|
||||
@@unique([labCaseId, sourceKey, tooth, prosthesisTypeCode, stepOrder], map: "lab_case_tasks_case_source_tooth_prosthesis_step_key")
|
||||
@@index([labCaseId, status])
|
||||
@@index([assigneeUserId])
|
||||
@@map("lab_case_tasks")
|
||||
|
||||
@@ -183,22 +183,30 @@ async function main() {
|
||||
);
|
||||
|
||||
for (const type of PROSTHESIS_TYPES) {
|
||||
const treeFields = {
|
||||
sortOrder: type.sortOrder,
|
||||
skipPackingShipping: type.skipPackingShipping ?? false,
|
||||
isActive: type.isActive ?? true,
|
||||
category: type.category,
|
||||
subcategory: type.subcategory ?? '',
|
||||
chartRegion: type.chartRegion,
|
||||
stackGroup: type.stackGroup,
|
||||
addonKind: type.addonKind ?? '',
|
||||
};
|
||||
const prosthesisType = await prisma.prosthesisType.upsert({
|
||||
where: { code: type.code },
|
||||
update: {
|
||||
sortOrder: type.sortOrder,
|
||||
skipPackingShipping: type.skipPackingShipping ?? false,
|
||||
isActive: true,
|
||||
},
|
||||
update: treeFields,
|
||||
create: {
|
||||
id: randomUUID(),
|
||||
code: type.code,
|
||||
sortOrder: type.sortOrder,
|
||||
skipPackingShipping: type.skipPackingShipping ?? false,
|
||||
isActive: true,
|
||||
...treeFields,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.prosthesisTypeStep.deleteMany({
|
||||
where: { prosthesisTypeId: prosthesisType.id },
|
||||
});
|
||||
|
||||
const stepCodes = buildProsthesisStepCodes(type);
|
||||
for (const [index, stepCode] of stepCodes.entries()) {
|
||||
const labWorkflowStepId = workflowStepByCode.get(stepCode);
|
||||
@@ -206,15 +214,8 @@ async function main() {
|
||||
throw new Error(`Unknown workflow step code: ${stepCode}`);
|
||||
}
|
||||
|
||||
await prisma.prosthesisTypeStep.upsert({
|
||||
where: {
|
||||
prosthesisTypeId_stepOrder: {
|
||||
prosthesisTypeId: prosthesisType.id,
|
||||
stepOrder: index + 1,
|
||||
},
|
||||
},
|
||||
update: { labWorkflowStepId },
|
||||
create: {
|
||||
await prisma.prosthesisTypeStep.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
prosthesisTypeId: prosthesisType.id,
|
||||
labWorkflowStepId,
|
||||
|
||||
Reference in New Issue
Block a user