'use client'; import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'; import { useTranslations } from 'next-intl'; import { ArrowLeft, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/shared/Button'; import { ResponsiveDialogOverlay, ResponsiveDialogPanel, } from '@/components/ui/shared/ResponsiveDialog'; import type { ProsthesisCatalogEntry } from '@/types/treatment-catalog'; import { prosthesisFamilyTone } from '@/components/shared/catalog-type-colors'; import { prosthesisTypeColorFromCatalog, prosthesisTypeFillAccent } from '@/components/treatment/prosthesisTypeDisplay'; import { prosthesisCatalogIconSrc } from '@/components/treatment/prosthesisCatalogIcons'; import { PROSTHESIS_JOB_PATH_SEP, prosthesisJobPathParts, } from '@/components/treatment/prosthesisJobPath'; import { catalogByCode, categoryCodesInCatalog, categoryDisabledForJobs, categoryTileColor, crownRestorationCode, leavesFor, subcategoriesFor, type AddonSlot, type ArchTarget, type PickerScope, } from '@/components/treatment/prosthesisTree'; import { ProsthesisCatalogIcon } from '@/components/ui/treatment/ProsthesisCatalogIcon'; const CHIP_INK = '#14253d'; const PARENT_BACK_INK = '#000000'; const TILE_CLASS = 'flex min-h-[4.5rem] min-w-0 w-full flex-col items-center justify-center rounded-[var(--radius-md)] border px-2.5 py-2 text-center transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-45'; const CHILD_TILE_CLASS = 'flex min-h-[3.75rem] min-w-0 w-full flex-col items-center justify-center rounded-[var(--radius-md)] border px-2 py-1 text-center transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-45'; const PARENT_BAR_CLASS = 'flex min-h-[3.75rem] min-w-0 w-full flex-row items-center justify-start gap-2 rounded-[var(--radius-md)] border px-3 py-1.5 text-start transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-45'; const TILE_TEXT_CLASS = 'w-full whitespace-normal break-words text-center text-xs font-medium leading-tight sm:text-[13px]'; const TILE_TEXT_CHILD_CLASS = 'w-full min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-center text-[11px] font-medium leading-tight sm:text-xs'; const TILE_GRID_CLASS = 'grid gap-2 [grid-template-columns:repeat(auto-fill,minmax(8rem,1fr))]'; const TILE_GRID_CHILD_CLASS = 'grid gap-2 [grid-template-columns:repeat(auto-fill,minmax(10.2rem,1fr))]'; const HEADER_BTN_CLASS = 'h-auto min-h-8 w-full px-2 py-1 text-xs leading-tight whitespace-normal'; function tileSurfaceClass(active?: boolean) { return active ? 'border-primary ring-1 ring-primary/30' : 'border-border/70 hover:border-border hover:bg-background-card/70'; } export type PickerStep = | { kind: 'category' } | { kind: 'subcategory'; category: string; arch?: ArchTarget } | { kind: 'leaves'; category: string; subcategory?: string; arch?: ArchTarget } | { kind: 'addon'; slot: AddonSlot }; interface ProsthesisJobPopoverProps { open: boolean; mobile: boolean; catalog: readonly ProsthesisCatalogEntry[]; step: PickerStep; scope: PickerScope; lockedArch?: ArchTarget; jobCodes: string[]; tooth: string | null; allowCrownAddon: boolean; onStep: (step: PickerStep) => void; onLockedArchChange?: (arch: ArchTarget) => void; onPickLeaf: (code: string, arch?: ArchTarget) => void; onPickAddon: (code: string | null, kind: AddonSlot) => void; onRemoveJob: (code: string) => void; onClear: () => void; onClose: () => void; } function Tile({ label, color, active, disabled, hint, iconSrc, compact, className, onClick, }: { label: string; color?: string; active?: boolean; disabled?: boolean; hint?: string; iconSrc?: string; compact?: boolean; className?: string; onClick: () => void; }) { const base = className ?? (compact ? CHILD_TILE_CLASS : TILE_CLASS); return ( ); } function parentStep(step: PickerStep): PickerStep | null { switch (step.kind) { case 'category': return null; case 'addon': case 'subcategory': return { kind: 'category' }; case 'leaves': return step.subcategory ? { kind: 'subcategory', category: step.category, arch: step.arch } : { kind: 'category' }; } } function pickerStepKey(step: PickerStep): string { switch (step.kind) { case 'category': return 'category'; case 'addon': return `addon:${step.slot}`; case 'subcategory': return `sub:${step.category}:${step.arch ?? ''}`; case 'leaves': return `leaves:${step.category}:${step.subcategory ?? ''}:${step.arch ?? ''}`; } } export function ProsthesisJobPopover({ open, mobile, catalog, step, scope, lockedArch, jobCodes, tooth, allowCrownAddon, onStep, onLockedArchChange, onPickLeaf, onPickAddon, onRemoveJob, onClear, onClose, }: ProsthesisJobPopoverProps) { const t = useTranslations('prosthesis'); const childGridRef = useRef(null); const [branchH, setBranchH] = useState(0); const stepKey = pickerStepKey(step); useLayoutEffect(() => { if (!open || stepKey === 'category') return; const grid = childGridRef.current; if (!grid) return; const sync = () => { const first = grid.firstElementChild; if (!(first instanceof HTMLElement)) return; setBranchH(first.offsetHeight); }; sync(); const ro = new ResizeObserver(sync); ro.observe(grid); grid.addEventListener('animationend', sync); return () => { ro.disconnect(); grid.removeEventListener('animationend', sync); }; }, [open, stepKey]); if (!open) return null; const categories = categoryCodesInCatalog(catalog, scope); const byCode = catalogByCode(catalog); const crownLeaves = leavesFor(catalog, 'crown', undefined, 'tooth'); const currentCrown = crownRestorationCode(jobCodes, byCode) ?? ''; const hasLeaf = jobCodes.length > 0; const archForLeaves = step.kind === 'leaves' || step.kind === 'subcategory' ? step.arch : lockedArch; function categoryLabel(code: string) { return t(`category_${code}` as never); } function subLabel(code: string) { return t(`sub_${code}` as never); } const back = parentStep(step); function openCategory(code: string) { if (categoryDisabledForJobs(code, jobCodes, byCode)) return; const subs = subcategoriesFor(catalog, code, scope); const arch = scope === 'arch' ? lockedArch : undefined; if (subs.length > 0) { onStep({ kind: 'subcategory', category: code, arch }); return; } onStep({ kind: 'leaves', category: code, arch }); } function leafTile(leaf: ProsthesisCatalogEntry) { const path = prosthesisJobPathParts(leaf.code, catalog, (key) => t(key as never)); const underIndirectSub = leaf.category === 'indirect' && step.kind === 'leaves' && Boolean(step.subcategory); return ( onPickLeaf(leaf.code, archForLeaves)} /> ); } const addonTiles: ReactNode[] = []; if (scope === 'tooth' && allowCrownAddon && crownLeaves.length > 0) { const addonActive = step.kind === 'addon' && step.slot === 'crown'; const openAddon = () => onStep(addonActive ? { kind: 'category' } : { kind: 'addon', slot: 'crown' }); if (currentCrown) { addonTiles.push( , ); } else { addonTiles.push( , ); } } let selectedNode: { label: string; color?: string; iconSrc?: string } | null = null; let childTiles: ReactNode[] = []; let childrenKey = 'root'; switch (step.kind) { case 'category': childTiles = categories.map((code) => { const disabled = categoryDisabledForJobs(code, jobCodes, byCode); return ( openCategory(code)} /> ); }); break; case 'addon': selectedNode = { label: t('slotCrown'), color: categoryTileColor('crown'), iconSrc: prosthesisCatalogIconSrc({ category: 'crown' }), }; childrenKey = 'addon-crown'; childTiles = [ onPickAddon(null, 'crown')} />, ...crownLeaves.map((opt) => ( onPickAddon(opt.code, 'crown')} /> )), ]; break; case 'subcategory': { const parentFill = categoryTileColor(step.category); selectedNode = { label: categoryLabel(step.category), color: parentFill, iconSrc: prosthesisCatalogIconSrc({ category: step.category }), }; childrenKey = `sub-${step.category}`; childTiles = [ ...subcategoriesFor(catalog, step.category, scope).map((sub, i) => ( onStep({ kind: 'leaves', category: step.category, subcategory: sub, arch: step.arch, }) } /> )), ...leavesFor(catalog, step.category, undefined, scope).map(leafTile), ]; break; } case 'leaves': if (step.subcategory) { selectedNode = { label: subLabel(step.subcategory), color: prosthesisFamilyTone(categoryTileColor(step.category), 1), iconSrc: prosthesisCatalogIconSrc({ category: step.category, subcategory: step.subcategory, }), }; childrenKey = `leaves-${step.category}-${step.subcategory}`; childTiles = leavesFor(catalog, step.category, step.subcategory, scope).map(leafTile); } else { selectedNode = { label: categoryLabel(step.category), color: categoryTileColor(step.category), iconSrc: prosthesisCatalogIconSrc({ category: step.category }), }; childrenKey = `leaves-${step.category}`; childTiles = leavesFor(catalog, step.category, undefined, scope).map(leafTile); } break; } const tileGrid = (
{childTiles}
); const branchColor = selectedNode?.color ?? 'var(--color-border-strong)'; const tileRow = selectedNode ? (
{tileGrid}
) : ( tileGrid ); const title = scope === 'arch' ? lockedArch === 'both' ? t('pickerArchBoth') : lockedArch === 'lower' ? t('pickerArchLower') : t('pickerArchUpper') : tooth ? t('pickerTooth', { tooth }) : t('pickerTitle'); const jobChips = jobCodes.length === 0 ? (

{t('noneYet')}

) : (
{jobCodes.map((code) => { const path = prosthesisJobPathParts(code, catalog, (key) => t(key as never)); const color = prosthesisTypeColorFromCatalog(code, catalog); const trashColor = prosthesisTypeFillAccent(color); return ( {path.ancestors.length > 0 ? ( {path.ancestors.join(PROSTHESIS_JOB_PATH_SEP)} {PROSTHESIS_JOB_PATH_SEP} ) : null} {path.leaf} ); })}
); const body = (

{title}

{scope === 'arch' && onLockedArchChange ? (
{(['upper', 'lower', 'both'] as const).map((arch) => ( ))}
) : null}
{jobChips}

{scope === 'arch' ? t('clearHintArch') : t('clearHint')}

{tileRow}
{addonTiles.length > 0 ? (

{t('suggestionsLabel')}

{addonTiles}
) : null}
); if (mobile) { return (

{t('pickerTitle')}

{body}
); } return (
); }