import { HttpStatus, Injectable, OnModuleInit } from '@nestjs/common'; import { AppException, ErrorCode } from '../../common/errors'; import { CatalogEntityKind } from '@prisma/client'; import { PrismaService } from '../../../prisma/prisma.service'; import { CatalogLabelService, CatalogLocale, normalizeCatalogLocale, } from '../catalog/catalog-label.service'; export type ProsthesisTypeCatalogEntry = { code: string; sortOrder: number; label: string; category: string; subcategory: string; chartRegion: string; stackGroup: string; }; type CatalogRow = { sortOrder: number; category: string; subcategory: string; chartRegion: string; stackGroup: string; }; @Injectable() export class ProsthesisCatalogService implements OnModuleInit { private loaded = false; private byCode = new Map(); constructor( private readonly prisma: PrismaService, private readonly catalogLabels: CatalogLabelService, ) {} async onModuleInit() { await this.refresh(); } async refresh(): Promise { const rows = await this.prisma.prosthesisType.findMany({ where: { isActive: true }, orderBy: [{ sortOrder: 'asc' }, { code: 'asc' }], select: { code: true, sortOrder: true, category: true, subcategory: true, chartRegion: true, stackGroup: true, }, }); this.byCode = new Map( rows.map((row) => [ row.code, { sortOrder: row.sortOrder, category: row.category, subcategory: row.subcategory, chartRegion: row.chartRegion, stackGroup: row.stackGroup, }, ]), ); this.loaded = true; } async list( localeInput?: string | null, ): Promise { await this.refresh(); const locale = normalizeCatalogLocale(localeInput); const codes = [...this.byCode.keys()]; const labels = await this.catalogLabels.resolveLabels( CatalogEntityKind.PROSTHESIS_TYPE, codes, locale, ); return codes .map((code) => { const row = this.byCode.get(code)!; return { code, sortOrder: row.sortOrder, label: labels.get(code) ?? code, category: row.category, subcategory: row.subcategory, chartRegion: row.chartRegion, stackGroup: row.stackGroup, }; }) .sort( (a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code), ); } /** The 7 distinct category codes present in the active catalog, with labels in `locale`. */ async listCategories( localeInput?: string | null, ): Promise<{ code: string; label: string }[]> { return this.listDistinct( (row) => row.category, CatalogEntityKind.PROSTHESIS_CATEGORY, localeInput, ); } /** The 5 distinct subcategory codes (veneer, inlay, onlay, overlay, night_guard, …). */ async listSubcategories( localeInput?: string | null, ): Promise<{ code: string; label: string }[]> { return this.listDistinct( (row) => row.subcategory, CatalogEntityKind.PROSTHESIS_SUBCATEGORY, localeInput, ); } private async listDistinct( pick: (row: CatalogRow) => string, entityKind: CatalogEntityKind, localeInput?: string | null, ): Promise<{ code: string; label: string }[]> { await this.refresh(); const locale = normalizeCatalogLocale(localeInput); const codes = [ ...new Set([...this.byCode.values()].map(pick).filter(Boolean)), ].sort(); const labels = await this.catalogLabels.resolveLabels( entityKind, codes, locale, ); return codes.map((code) => ({ code, label: labels.get(code) ?? code })); } assertKnownProsthesisType(code: string): void { this.ensureLoaded(); if (!this.byCode.has(code)) { throw new AppException( ErrorCode.CATALOG_UNKNOWN_PROSTHESIS_TYPE, HttpStatus.BAD_REQUEST, ); } } async getStepCodesForProsthesisType( prosthesisTypeCode: string, ): Promise { const type = await this.prisma.prosthesisType.findUnique({ where: { code: prosthesisTypeCode }, select: { steps: { orderBy: { stepOrder: 'asc' }, select: { labWorkflowStep: { select: { code: true } } }, }, }, }); if (!type) { return []; } return type.steps.map((s) => s.labWorkflowStep.code); } async resolveStepLabels( stepCodes: string[], locale: CatalogLocale, ): Promise> { return this.catalogLabels.resolveLabels( CatalogEntityKind.LAB_WORKFLOW_STEP, stepCodes, locale, ); } private ensureLoaded() { if (!this.loaded) { throw new Error('Prosthesis catalog is not loaded yet'); } } }