import { useRef, useState } from 'react'; import type { Questionnaire } from 'fhir/r4'; import { Spinner } from '../../../../components/ui/spinner'; import { DismissibleAlert } from '../../../../components/ui/dismissible-alert'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../../../components/ui/dialog'; import { Input } from '../../../../components/ui/input'; import { Label } from '../../../../components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../../components/ui/select'; import DiscardChangesDialog from '../../../../components/DiscardChangesDialog/DiscardChangesDialog'; import { AUTO_ASSIGN_TRIGGER_OPTIONS, TASK_PRIORITY_OPTIONS } from '../../../../config/autoAssign'; import { MAX_PACKET_TITLE, PacketChangedError, describePacketRefusal, usePacketPractice, useQuestionnairePacketService, type PacketInput, type PacketPriority, type PacketStatus, type PacketTrigger, type QuestionnairePacket, } from '../../../../services/QuestionnairePacketService'; import { TIMED_OUT_NOTHING_SAVED, isSaveRetired, isSaveTimedOut, outcomeUnknown, useSaveScope, type SaveOperation, } from '../../../../utils/saveOperation'; import { draftOfPacket, emptyPacketDraft, packetDraftChanged, packetDraftProblem } from '../../../../utils/questionnairePacketDraft'; import PacketMembers from './PacketMembers'; import { packetInputOf, packetName, sameAsSent } from './packetWrites'; interface PacketEditorDialogProps { /** Absent to create a packet. */ packet?: QuestionnairePacket; questionnaires: Questionnaire[]; /** Packets already listed when the editor opened; a create is recognised only as a packet not among them. */ knownPacketIds?: string[]; onPreview: (questionnaire: Questionnaire) => void; /** The packet as the server stored it. */ onSaved: (packet: QuestionnairePacket) => void; /** A save went out and no answer came back: the list must be read again before anyone trusts it. */ onUnconfirmed: () => void; onClose: () => void; } const TRIGGER_HELP: Record = { off: 'Off keeps the packet saved without assigning it automatically.', 'all-appointments': 'Assigned when any appointment is booked.', 'new-patient-appointments': 'Assigned when a new patient appointment is booked.', }; const NEWER_EDITS = 'Saved. Changes you made after pressing Save are not saved yet.'; const PacketEditorDialog = ({ packet: openedPacket, questionnaires, knownPacketIds = [], onPreview, onSaved, onUnconfirmed, onClose, }: PacketEditorDialogProps) => { const { createPacket, updatePacket, listPackets } = useQuestionnairePacketService(); const practice = usePacketPractice(); // This opening's requests: a change of practice or viewer, closing, or unmounting retires them and closes it. const lives = useSaveScope(practice, onClose); const [packet, setPacket] = useState(openedPacket); const [opened, setOpened] = useState(() => (openedPacket ? draftOfPacket(openedPacket) : emptyPacketDraft())); const [draft, setDraftState] = useState(opened); const latest = useRef(draft); latest.current = draft; const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [conflict, setConflict] = useState<{ current?: QuestionnairePacket } | null>(null); // A create whose answer was lost: it may exist, so it is never sent again from here; only finding it settles it. const [uncertain, setUncertain] = useState<{ sent: PacketInput; known: string[] } | null>(null); const [checking, setChecking] = useState(false); // What the latest readback found, beside the warning that the create was not confirmed. const [checkNote, setCheckNote] = useState(null); const [askDiscard, setAskDiscard] = useState(false); // One save at a time: a second click while the first is out would be a second packet. const savingRef = useRef(false); const setDraft = (change: Partial) => setDraftState((current) => ({ ...current, ...change })); const problem = packetDraftProblem(draft, questionnaires); const savedTitles = Object.fromEntries((packet?.members ?? []).map((member) => [member.questionnaireId, member.title])); const scopeOf = (life: SaveOperation) => ({ admit: life.admit, signal: life.transport.signal, organizationId: practice }); const requestClose = () => { if (savingRef.current) return; if (packetDraftChanged(draft, opened)) setAskDiscard(true); else onClose(); }; // The saved packet is shown; edits made while it was being saved stay in the form rather than being dropped. const settle = (saved: QuestionnairePacket, sent: PacketInput) => { onSaved(saved); if (!packetDraftChanged(packetInputOf(latest.current), sent)) { onClose(); return; } setPacket(saved); setOpened(draftOfPacket(saved)); setNotice(NEWER_EDITS); }; const reconcile = async (sent: PacketInput, known: string[]) => { const life = lives.start(); setChecking(true); setCheckNote(null); try { const list = await life.step(() => listPackets(scopeOf(life))); // Nothing read back can prove this create made a packet (another author may have made the same one), so a // match is pointed to and never adopted; the create stays blocked here either way. if (!list.complete) { setCheckNote('Your packets could not all be read, so we cannot tell yet whether it was saved. Check again.'); return; } const match = list.packets.find((each) => !known.includes(each.id) && sameAsSent(each, sent)); setCheckNote(match ? `A packet named "${match.title}" with these settings is now in your list. It may be this one or another author's: close this and check it there before creating another.` : 'It is not in your packets yet. It may still appear, so it has not been sent again. Check again before creating it a second time.'); } catch (caught) { if (isSaveRetired(caught)) return; setCheckNote('We could not check your packets for it. Check again.'); } finally { setChecking(false); } }; const save = async () => { if (savingRef.current || uncertain) return; // An existing packet is only ever updated: without its version it must be read again, never re-created. if (packet && !packet.version) { setError('This packet could not be saved because its version is unknown. Close it and open it again.'); return; } savingRef.current = true; setSaving(true); setError(null); setNotice(null); setConflict(null); const sent = packetInputOf(draft); const life = lives.start(); try { const saved = await life.step(() => (packet ? updatePacket(packet.id, packet.version as string, sent, scopeOf(life)) : createPacket(sent, scopeOf(life))), true); settle(saved, sent); } catch (caught) { if (isSaveRetired(caught)) return; if (caught instanceof PacketChangedError) { setConflict({ current: caught.current }); } else if (outcomeUnknown(caught)) { onUnconfirmed(); if (packet) { // A repeated edit names the same version, so if the first landed the server refuses the second. setError('We could not confirm the save. Your changes are still here; the list has been refreshed so you can check it before saving again.'); } else { setError('We could not confirm the packet was saved, so it will not be sent again from here. Checking your packets for it.'); const known = [...knownPacketIds]; setUncertain({ sent, known }); void reconcile(sent, known); } } else if (isSaveTimedOut(caught)) { setError(TIMED_OUT_NOTHING_SAVED); } else { setError(describePacketRefusal(caught, 'The packet was not saved')); } } finally { savingRef.current = false; setSaving(false); } }; // Continuing from the other author's version replaces this draft; nothing of theirs is overwritten. const takeTheirs = () => { const current = conflict?.current; if (!current) return; onSaved(current); const theirs = draftOfPacket(current); setPacket(current); setOpened(theirs); setDraftState(theirs); setConflict(null); }; return ( <> { if (!isOpen) requestClose(); }}> {packet ? `Edit ${packetName(packet)}` : 'Create packet'}
{conflict && ( Someone else saved this packet after you opened it, so your changes were not saved. {conflict.current ? ( <> {' '}Load their version to continue from it; your edits here will be replaced.{' '} ) : ' Close this and reopen the packet to see the latest version.'} )} {error &&

{error}

} {checkNote &&

{checkNote}

} {notice &&

{notice}

}
setDraft({ title: event.target.value })}/>

Patients see this name.

setDraft({ members })} onPreview={onPreview}/>

{draft.status === 'active' ? 'Active packets can be assigned to patients.' : 'Drafts cannot be assigned to patients.'}

{TRIGGER_HELP[draft.trigger]}

{draft.trigger !== 'off' && (
)}
{problem && !uncertain &&

{problem}

} {uncertain && ( )}
{askDiscard && ( setAskDiscard(false)} onDiscard={onClose}/> )} ); }; export default PacketEditorDialog;