"use client"; /** * Add or correct one address-book entry. * * Six plaintext-ish fields and one encrypted note. `name` is the only required * one, for the same reason the visit only requires a date: a person typing a * practice into a form after a doctor's appointment should not be stopped by a * phone number they do not have to hand. * * Reachable two ways — from the address-book page and nested inside the visit * form's picker — so it owns no navigation of its own and reports what it * created back to whoever opened it. */ import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { FieldGroup } from "@/components/ui/field-group"; import { Input } from "@/components/ui/input"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Textarea } from "@/components/ui/textarea"; import { useTranslations } from "@/lib/i18n/context"; import { usePractitionerMutations, type Practitioner, type PractitionerWriteBody, } from "@/hooks/use-practitioners"; interface Draft { name: string; specialty: string; practice: string; location: string; phone: string; note: string; } const EMPTY: Draft = { name: "", specialty: "", practice: "", location: "", phone: "", note: "", }; function draftFrom(row: Practitioner | null): Draft { if (!row) return EMPTY; return { name: row.name, specialty: row.specialty ?? "", practice: row.practice ?? "", location: row.location ?? "", phone: row.phone ?? "", note: row.note ?? "", }; } /** An empty field clears the column rather than writing an empty string. */ function orNull(value: string): string | null { const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } export function PractitionerSheet({ open, onOpenChange, practitioner, onSaved, }: { open: boolean; onOpenChange: (open: boolean) => void; /** `null` creates; a row edits it. */ practitioner: Practitioner | null; onSaved?: (row: Practitioner) => void; }) { const { t } = useTranslations(); const { create, update } = usePractitionerMutations(); // Seeded once, at mount. The caller varies this component's `key` on every // open, so a second edit never shows the first one's values and a create // after an edit starts empty — without an effect that copies props into // state and briefly renders the stale pair. const [draft, setDraft] = useState(() => draftFrom(practitioner)); const [error, setError] = useState(null); const pending = create.isPending || update.isPending; const nameGiven = draft.name.trim().length > 0; const submit = async () => { if (!nameGiven) return; const body: PractitionerWriteBody = { name: draft.name.trim(), specialty: orNull(draft.specialty), practice: orNull(draft.practice), location: orNull(draft.location), phone: orNull(draft.phone), note: orNull(draft.note), }; try { const saved = practitioner ? await update.mutateAsync({ id: practitioner.id, body }) : await create.mutateAsync(body); onSaved?.(saved); onOpenChange(false); } catch (err) { const message = err instanceof Error ? err.message : t("practitioners.saveFailed"); setError(message); toast.error(message); } }; const field = (key: keyof Draft) => ({ value: draft[key], onChange: ( event: React.ChangeEvent, ) => setDraft((prev) => ({ ...prev, [key]: event.target.value })), }); return ( } >