// Sends to the front-desk pool and never reads it: a sender without a messages grant cannot open the
// thread, so the dialog says no reply will appear here. Renders nothing without a patient.
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { Practitioner } from 'fhir/r4';
import { toast } from 'sonner';
import {
useAuthServiceContext,
useCommunicationServiceContext,
useFhirServiceContext,
usePractitionerServiceContext,
} from '../../services';
import { useCapabilityServiceContext } from '../../services/CapabilityService';
import { organizationIdsWithFrontDesk } from '../../utils/patientMessagingDirectory';
import { mayOfferFrontDeskDoor } from '../../utils/messaging/frontDeskDoor';
import { Button } from '../ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Textarea } from '../ui/textarea';
import { namedSend } from '../../utils/messaging/sendOperation';
import { MAY_HAVE_BEEN_SENT } from '../../hooks/useHeldSendOperations';
import { useScopedSendRecovery } from '../../hooks/useScopedSendRecovery';
import EarlierSendCheck from '../messages/EarlierSendCheck';
interface TellTheFrontDeskProps {
/** The patient the sender already has in front of them. Nothing renders without one. */
patientId?: string;
organizationId?: string;
}
const FrontDeskNoteDoor = ({ patientId, organizationId }: TellTheFrontDeskProps & { patientId: string }) => {
const { currentUser, userRole, USER_ROLES } = useAuthServiceContext();
const { practitionersList } = usePractitionerServiceContext();
const { extractReferenceId } = useFhirServiceContext();
const { mayCall } = useCapabilityServiceContext();
const { resolveFrontDeskNoteThread, createCommunicationForThread, createCommunication } = useCommunicationServiceContext();
const [open, setOpen] = useState(false);
// Every open and close starts a new dialog lifetime: a send pressed in an earlier one publishes nothing here.
const lifetime = useRef(0);
const changeOpen = (next: boolean) => {
lifetime.current += 1;
setOpen(next);
};
const [note, setNote] = useState("");
const [sending, setSending] = useState(false);
const recovery = useScopedSendRecovery('front-desk-note', `frontdesk:${patientId}`);
useLayoutEffect(() => {
setSending(false);
return recovery.reopen();
}, [open, patientId, organizationId, recovery.reopen]);
// Bumped on every keystroke: an answer clears only the draft it was sent from.
const revision = useRef(0);
// A practice with no receptionist has nobody on the desk, so the door is not offered there.
const orgIdsWithFrontDesk = useMemo(
() => organizationIdsWithFrontDesk(practitionersList, (role) => {
const id = extractReferenceId(role.organization) as string | undefined;
return id || role.organization?.display?.trim();
}),
[practitionersList, extractReferenceId],
);
const offered = mayOfferFrontDeskDoor({
// An empty list here is a refusal, not a practice with no desk: this seat cannot
// read staff at all. Unknown offers, and the server refuses if nobody is on the desk.
organizationHasFrontDesk: practitionersList.length === 0
? undefined
: (organizationId ? orgIdsWithFrontDesk.has(organizationId) : orgIdsWithFrontDesk.size > 0),
callerIsReceptionist: userRole === USER_ROLES.RECEPTIONIST,
patientId,
mayOpenThread: mayCall('POST', '/front-desk-note'),
});
// No patient, no desk, or the caller is the desk: the control is absent rather than disabled.
if (!offered) return null;
const send = async () => {
const text = note.trim();
if (!text || sending || recovery.waits) return;
const sentIn = lifetime.current;
const attempt = recovery.begin(undefined, () => lifetime.current === sentIn);
const drafted = revision.current;
setSending(true);
const result = await recovery.run(attempt, `frontdesk:${patientId}|${text}`, async (operation, leaving) => {
// RESOLVE, never open. openFrontDeskThread also FETCHES the conversation, and a sender who
// only holds this door is refused that read - correctly. Asking anyway would turn a
// working send into an error on screen.
// THE ASIDE POOL, not the patient one. The patient-facing thread is a conversation they are
// a party to; a note about them is not, and sharing that thread made it a disclosure.
const thread = await resolveFrontDeskNoteThread(patientId);
attempt.admit();
// Addressed to the OFFICE. Naming the patient here is what put the note in their inbox - they
// are the subject of this thread, which the thread record already carries.
const message = createCommunicationForThread(text, thread.threadId, currentUser?.fhir as Practitioner, [
{ reference: `Organization/${thread.organizationId ?? organizationId}` },
]);
leaving();
return createCommunication(namedSend(message, operation), operation);
});
if (lifetime.current !== sentIn || result.state === 'retired') return;
setSending(false);
if (result.state === 'sent') {
if (revision.current === drafted) {
setNote("");
changeOpen(false);
}
// It says WHO has it, because the sender cannot open the thread to see for themselves.
toast.success("The front desk has it");
return;
}
if (result.state === 'not-sent') {
toast.error(result.reason);
return;
}
if (result.unsure) {
toast.error("Not confirmed: the front desk may already have it. " + MAY_HAVE_BEEN_SENT);
return;
}
const said = (result.error as { body?: { message?: string } })?.body?.message;
// The server denial is named in its own response. The send stays held until Check answers whether
// an earlier attempt of it arrived, so this screen never claims that nothing was sent.
toast.error(said ?? "That did not reach the front desk. Check before sending it again.");
};
return (
<>
>
);
};
// Nothing to address without a patient, so none of the door's lookups run.
export const TellTheFrontDesk = (props: TellTheFrontDeskProps) =>
props.patientId ? : null;