import { Prisma } from '@prisma/client'; /** Normalize the JSON `teeth` column of a lab case task into a clean string[]. */ export function normalizeTaskTeeth(value: Prisma.JsonValue | null | undefined): string[] { if (!Array.isArray(value)) { return []; } return value .filter((v): v is string | number => typeof v === 'string' || typeof v === 'number') .map((v) => String(v)); } export function sortProsthesisTeeth(teeth: readonly string[]): string[] { return [...teeth].sort((a, b) => { const rank = (tooth: string) => (tooth === 'UA' ? 0 : tooth === 'LA' ? 1 : 2); const diff = rank(a) - rank(b); if (diff !== 0) return diff; return a.localeCompare(b, undefined, { numeric: true }); }); } /** Group ids that span more than one tooth (bridges). */ export function connectedSelectionGroupIds( rows: Array<{ selectionGroupId?: string | null; tooth?: string; teeth?: Prisma.JsonValue; }>, ): Set { const byGroup = new Map>(); for (const row of rows) { const id = row.selectionGroupId?.trim(); if (!id) continue; const teeth = row.tooth != null && row.tooth !== '' ? [row.tooth] : normalizeTaskTeeth(row.teeth); const set = byGroup.get(id) ?? new Set(); for (const tooth of teeth) set.add(tooth); byGroup.set(id, set); } const connected = new Set(); for (const [id, teeth] of byGroup) { if (teeth.size > 1) connected.add(id); } return connected; } export function aggregateProsthesisGroupsFromTasks( tasks: Array<{ prosthesisTypeCode: string; teeth: Prisma.JsonValue; selectionGroupId?: string | null; }>, ): Array<{ prosthesisTypeCode: string; teeth: string[]; connected?: boolean; selectionGroupId?: string; }> { const connectedIds = connectedSelectionGroupIds(tasks); const byKey = new Map< string, { prosthesisTypeCode: string; teeth: string[]; selectionGroupId: string } >(); for (const task of tasks) { if (!task.prosthesisTypeCode) continue; const selectionGroupId = task.selectionGroupId?.trim() ?? ''; const teeth = normalizeTaskTeeth(task.teeth); const key = `${teeth.join(',')}::${task.prosthesisTypeCode}`; const entry = byKey.get(key) ?? { prosthesisTypeCode: task.prosthesisTypeCode, teeth: [], selectionGroupId, }; entry.teeth.push(...teeth); if (!entry.selectionGroupId && selectionGroupId) { entry.selectionGroupId = selectionGroupId; } byKey.set(key, entry); } return [...byKey.values()] .map((group) => { const teeth = sortProsthesisTeeth([...new Set(group.teeth)]); return { prosthesisTypeCode: group.prosthesisTypeCode, teeth, selectionGroupId: group.selectionGroupId || undefined, connected: Boolean(group.selectionGroupId) && connectedIds.has(group.selectionGroupId), }; }) .sort((a, b) => a.prosthesisTypeCode.localeCompare(b.prosthesisTypeCode)); }