2026-09-01 21:03:04 +03:30
/ * *
* 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' ,
2026-09-02 17:24:01 +03:30
indirect : 'Veneer/Inlay/Onlay/Overlay' ,
2026-09-01 21:03:04 +03:30
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' ,
2026-09-02 17:24:01 +03:30
'One per tooth: crown, veneer, inlay, onlay, or overlay.' ,
2026-09-01 21:03:04 +03:30
] ,
2026-09-02 17:24:01 +03:30
[ 'implant' , 'One abutment add-on per tooth (Ti-base, custom, prefab, multi-unit, zirconia, screw-retained).' ] ,
[ 'post_core' , 'One post & core, only if a restoration is already present. Blocked when screw-retained is on the tooth.' ] ,
2026-09-01 21:03:04 +03:30
[
'arch' ,
2026-09-02 17:24:01 +03:30
'Complete denture, overdenture, appliances, and digital use Upper/Lower arch labels (UA/LA). Not 16 FDI teeth.' ,
] ,
[
'partial_denture' ,
'Tooth-selected (FDI), not the arch labels. All selected teeth become one lab job. May stack with implant add-ons.' ,
2026-09-01 21:03:04 +03:30
] ,
[
'screw_retained' ,
2026-09-02 17:24:01 +03:30
'Fills the implant slot (chart paints the crown) and stacks with a restoration. Blocks post & core.' ,
] ,
[
'connected' ,
'Connected teeth that share a type are one lab job. An extra type on one unit of the bridge is its own job. Unconnected teeth stay separate even if they share a type.' ,
2026-09-01 21:03:04 +03:30
] ,
[ '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 } ` ) ;