2026-07-07 15:31:09 +03:30
|
|
|
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));
|
|
|
|
|
}
|
2026-09-01 21:03:04 +03:30
|
|
|
|
|
|
|
|
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<string> {
|
|
|
|
|
const byGroup = new Map<string, Set<string>>();
|
|
|
|
|
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<string>();
|
|
|
|
|
for (const tooth of teeth) set.add(tooth);
|
|
|
|
|
byGroup.set(id, set);
|
|
|
|
|
}
|
|
|
|
|
const connected = new Set<string>();
|
|
|
|
|
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));
|
|
|
|
|
}
|