import { useState, useEffect } from "react"; import { Phone, Mail, Plus, SquarePen, Trash2, Globe } from "lucide-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../../components/ui/select"; import { Button } from "../../components/ui/button"; import clsx from "clsx"; import { ContactPoint, RelatedPerson } from "fhir/r4"; import { toast } from "sonner"; interface ContactsProps { data: ContactPoint[]; relatedPerson: RelatedPerson; updateRelatedPerson: ( updatedRelatedPerson: RelatedPerson, ) => Promise; submitting: boolean; setSubmitting: (submitting: boolean) => void; /** * When true, renders contact values with no add/edit/delete affordances and never invokes * the telecom-write (PUT) path. Used on the related-person view: the contact shown there is * a server-side projection of the helper's own home Patient telecom, so writing `telecom` * onto the RelatedPerson resource here is a dead end - it doesn't persist to what's shown, * so an edit would appear to succeed and then silently revert on reload. Defaults to false * so other Contacts consumers keep their existing editable behavior. */ readOnly?: boolean; } const labelStyles: Record = { phone: "bg-blue-100 text-blue-600", email: "bg-green-100 text-green-600", url: "bg-purple-100 text-purple-600", fax: "bg-orange-100 text-orange-600", other: "bg-gray-100 text-gray-600", }; const getSystemIcon = (system: string) => { switch (system) { case "phone": return ; case "email": return ; case "url": return ; default: return ; } }; const getSystemLabel = (system: string): string => { const labelMap: Record = { phone: "Phone", email: "Email", url: "URL", fax: "Fax", other: "Other", }; return labelMap[system] || "Other"; }; const getUseLabel = (use?: string): string => { const useMap: Record = { home: "Home", work: "Work", temp: "Temporary", old: "Old", mobile: "Mobile", }; return useMap[use || ""] || use || "Other"; }; const getContactKey = (contact: ContactPoint, index: number): string => { return `${contact.system}-${contact.value}-${contact.use}-${index}`; }; const Contacts = ({ data, relatedPerson, updateRelatedPerson, submitting, setSubmitting, readOnly = false, }: ContactsProps) => { const [open, setOpen] = useState(false); const [mode, setMode] = useState<"add" | "edit" | "delete">("add"); const [selectedIndex, setSelectedIndex] = useState(null); const [contacts, setContacts] = useState([]); const [formData, setFormData] = useState({ system: "phone" as ContactPoint["system"], value: "", use: "home" as ContactPoint["use"], rank: 1, }); useEffect(() => { if (data) { setContacts(data); } }, [data]); useEffect(() => { if (mode === "edit" && selectedIndex !== null && contacts[selectedIndex]) { const contact = contacts[selectedIndex]; setFormData({ system: contact.system || "phone", value: contact.value || "", use: contact.use || "home", rank: contact.rank || 1, }); } else if ( mode === "delete" && selectedIndex !== null && contacts[selectedIndex] ) { // For delete mode, we don't need to populate form data // Just keep the selected contact info for display } else { setFormData({ system: "phone", value: "", use: "home", rank: 1, }); } }, [mode, selectedIndex, contacts]); const handleInputChange = ( field: keyof typeof formData, value: string | number, ) => { setFormData((prev) => ({ ...prev, [field]: value, })); }; const handleSaveContact = async () => { // Belt-and-suspenders: the UI never exposes a trigger for this in readOnly mode (no // add/edit/delete affordances render below), but guard the write path itself too so the // telecom PUT can never fire regardless of how this is invoked. if (readOnly) return; try { setSubmitting(true); if (mode !== "delete" && !formData.value.trim()) { toast.error("Please enter a value", { position: "top-right" }); setSubmitting(false); return; } const newContact: ContactPoint = { system: formData.system, value: formData.value.trim(), use: formData.use, rank: formData.rank, }; let updatedContacts: ContactPoint[]; if (mode === "add") { updatedContacts = [...contacts, newContact]; } else if (mode === "edit" && selectedIndex !== null) { updatedContacts = contacts.map((contact, index) => index === selectedIndex ? newContact : contact, ); } else if (mode === "delete" && selectedIndex !== null) { updatedContacts = contacts.filter( (_, index) => index !== selectedIndex, ); } else { return; } const updatedRelatedPerson: RelatedPerson = { ...relatedPerson, telecom: updatedContacts.length > 0 ? updatedContacts : undefined, }; await updateRelatedPerson(updatedRelatedPerson); setContacts(updatedContacts); toast.success( mode === "add" ? "Contact added successfully!" : mode === "edit" ? "Contact updated successfully!" : "Contact deleted successfully!", { position: "top-right" }, ); setOpen(false); setSelectedIndex(null); } catch (error: unknown) { console.error("Failed to update contact:", error); let errorMessage = "An unknown error occurred"; if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === "string") { errorMessage = error; } else if (error && typeof error === "object" && "message" in error) { errorMessage = String(error.message); } toast.error(`Failed to update contact: ${errorMessage}`, { position: "top-right", }); } finally { setSubmitting(false); } }; const handleOpenDialog = ( newMode: "add" | "edit" | "delete", index?: number, ) => { if (readOnly) return; setMode(newMode); if (index !== undefined) { setSelectedIndex(index); } else { setSelectedIndex(null); } setOpen(true); }; const hasData = contacts.length > 0; const selectedContact = selectedIndex !== null ? contacts[selectedIndex] : null; return ( {hasData && (

Contact Details

{!readOnly && ( )}
)} {!hasData ? (

No Contacts added yet

{!readOnly && ( Add your first contact to help us keep in touch. )}
{!readOnly && ( )}
) : ( <> {contacts.map((item, index) => (
{getSystemIcon(item.system || "phone")}

{item.value}

{item.use && ( {getUseLabel(item.use)} )}

{getSystemLabel(item.system || "other")}

{!readOnly && (
{ if (!submitting) { handleOpenDialog("delete", index); } }} /> { if (!submitting) { handleOpenDialog("edit", index); } }} />
)}
))} )}
{mode === "add" ? "Add New Contact" : mode === "edit" ? "Edit Contact" : "Delete Contact"}
{mode === "delete" ? "Are you sure? You can't undo this action afterwards." : "Please fill in all the required contact information."}
{mode === "delete" && selectedContact && (
{getSystemIcon(selectedContact.system || "phone")}

{selectedContact.value}

{selectedContact.use && ( {getUseLabel(selectedContact.use)} )}
{getSystemLabel(selectedContact.system || "")}
)} {mode !== "delete" && (
handleInputChange("value", e.target.value)} placeholder={ formData.system === "phone" ? "Enter phone number" : formData.system === "email" ? "Enter email address" : formData.system === "url" ? "Enter URL" : "Enter value" } type={ formData.system === "phone" ? "tel" : formData.system === "email" ? "email" : formData.system === "url" ? "url" : "text" } className="rounded" />
)}
); }; export default Contacts;