'use client'; import { useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; import { AlertTriangle } from 'lucide-react'; import { Button } from '@/components/ui/shared/Button'; import { Checkbox } from '@/components/ui/shared/Checkbox'; import { ResponsiveDialogOverlay, ResponsiveDialogPanel, } from '@/components/ui/shared/ResponsiveDialog'; import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart'; import { connectedTeethFromResult, countSelected, hasAnythingToApply, initialVoiceSelection, voiceRowAvailability, } from '@/components/treatment/voiceReviewRows'; import { useLocale } from 'next-intl'; import { useAppFormatters } from '@/lib/hooks/useAppFormatters'; import type { TreatmentCatalogEntry } from '@/types/treatment-catalog'; import type { ProsthesisCatalogEntry } from '@/types/treatment-catalog'; import type { LinkedOrganizationOption } from '@/types/treatment'; import type { VoiceApplySelection, VoiceExtractionResult } from '@/types/voice'; interface VoiceReviewSheetProps { result: VoiceExtractionResult; treatmentCatalog: TreatmentCatalogEntry[]; prosthesisCatalog: ProsthesisCatalogEntry[]; labs: LinkedOrganizationOption[]; onApply: (selection: VoiceApplySelection) => void; onDiscard: () => void; } /** * Confirmation step between the model's output and the form. * * Modal on desktop, bottom sheet on mobile via ResponsiveDialog — deliberately an overlay * and not a route, because navigating would unmount TreatmentWorkspace and destroy the * in-progress draft. */ export function VoiceReviewSheet({ result, treatmentCatalog, prosthesisCatalog, labs, onApply, onDiscard, }: VoiceReviewSheetProps) { const t = useTranslations('treatment'); const locale = useLocale(); const { formatDate } = useAppFormatters(); const [selection, setSelection] = useState(() => initialVoiceSelection(result), ); const available = useMemo(() => voiceRowAvailability(result), [result]); const connectedTeeth = useMemo(() => connectedTeethFromResult(result), [result]); const selectedTeeth = useMemo(() => new Set(result.teeth), [result.teeth]); const nothingToApply = !hasAnythingToApply(result); const selectedCount = countSelected(selection); const labelFor = (code: string | null, catalog: { code: string; label: string }[]) => catalog.find((entry) => entry.code === code)?.label ?? code ?? ''; const toggle = (key: keyof VoiceApplySelection) => (checked: boolean) => setSelection((prev) => ({ ...prev, [key]: checked })); return (

{t('voiceReviewTitle')}

{result.transcript}

{nothingToApply ? (

{t('voiceNothingExtracted')}

) : (
{available.treatmentType ? ( {labelFor(result.treatmentType, treatmentCatalog)} ) : null} {available.teeth ? (
) : null} {available.comment ? ( {result.comment} ) : null} {available.prosthesis && result.prosthesis ? ( {Object.entries(result.prosthesis.byTooth) .map( ([tooth, code]) => `${tooth}: ${labelFor(code, prosthesisCatalog)}`, ) .join(' · ')} ) : null} {available.lab ? ( {labs.find((lab) => lab.id === result.labId)?.name ?? result.labId} ) : null} {available.dueDate && result.dueDate ? ( {formatDate(civilDateToLocalDate(result.dueDate))} ) : null}
)} {result.unresolved.length > 0 ? (

{t('voiceNotUnderstood')}

    {result.unresolved.map((item, index) => (
  • {item.spoken ? `“${item.spoken}” — ` : ''} {t(`voiceUnresolved.${item.reason}`)}
  • ))}
) : null}
); } function Row({ label, checked, onChange, warning, children, }: { label: string; checked: boolean; onChange: (checked: boolean) => void; warning?: string; children: React.ReactNode; }) { return (
{children}
{warning ? (

{warning}

) : null}
); } /** * A bare `YYYY-MM-DD` is a *civil* date, but `new Date('2025-10-17')` parses it as UTC * midnight — which renders as the 16th for any viewer west of Greenwich. Build the date * from its parts so it means the same day everywhere. */ function civilDateToLocalDate(iso: string): Date { const [year, month, day] = iso.split('-').map(Number); return new Date(year, (month ?? 1) - 1, day ?? 1); } /** Locale-aware list separator — the Arabic comma is not correct in en or nl. */ function formatToothList(teeth: readonly string[], locale: string): string { try { return new Intl.ListFormat(locale, { style: 'short', type: 'unit' }).format([...teeth]); } catch { return teeth.join(', '); } }