// src/components/ArchiveConfirmModal/ArchiveConfirmModal.tsx // Delete Archive (Phase 1b). Shared confirm dialog for archiving a clinical resource: // requires a reason, and a note when the reason is "other". See // docs/_CLAUDE/2026-07-16-delete-to-archive-phase1b-design.md. import { useEffect, useState } from 'react'; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, } from '../ui/alert-dialog'; import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; import { Textarea } from '../ui/textarea'; import type { ArchiveReason } from '../../utils/archivedResource'; const REASONS: { value: ArchiveReason; label: string }[] = [ { value: 'entered-in-error', label: 'Entered in error / mistake' }, { value: 'duplicate', label: 'Duplicate' }, { value: 'no-longer-applies', label: 'No longer applies' }, { value: 'other', label: 'Other (add a note)' }, ]; interface ArchiveConfirmModalProps { open: boolean; onOpenChange: (open: boolean) => void; resourceLabel: string; submitting?: boolean; error?: string; onConfirm: (reason: ArchiveReason, note?: string) => void; /** Wording for an action that is not an archive; each defaults to the archive text. */ title?: string; description?: string; confirmLabel?: string; submittingLabel?: string; cancelLabel?: string; /** A reason and note already chosen for this same resource, offered again when it reopens. */ initialReason?: ArchiveReason; initialNote?: string; } export const ArchiveConfirmModal = ({ open, onOpenChange, resourceLabel, submitting, error, onConfirm, title, description, confirmLabel, submittingLabel, cancelLabel, initialReason, initialNote, }: ArchiveConfirmModalProps) => { const [reason, setReason] = useState(); const [note, setNote] = useState(''); // A REASON BELONGS TO THE RESOURCE IT WAS CHOSEN FOR, and this dialog outlives any one of them: // it stays mounted and only `open` toggles, so a selection made for the last resource was still // checked when it reopened for the next. That defeats the disabled-until-chosen guard below, // whose purpose is to force a deliberate choice - a second opening looked identical to a first // one somebody had already answered, with the primary button live. The reason is recorded as the // stated justification, and the families differ in what they write: entered-in-error and // duplicate set a status, the others write an archive extension. A carried-over reason can // therefore record a different shape of record than was intended. useEffect(() => { if (open) { setReason(initialReason); setNote(initialNote ?? ''); } }, [open, initialReason, initialNote]); const invalid = !reason || (reason === 'other' && !note.trim()); return ( {title ?? `Archive ${resourceLabel}?`} {description ?? ( <> This files the record away and removes it from the active list. Nothing is deleted — it stays in the record's history. )}{' '} Choose a reason: setReason(value as ArchiveReason)} className="flex flex-col gap-2" disabled={submitting} > {REASONS.map((r) => (
))}
{reason === 'other' && (