import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Loader2, MessagesSquare } from 'lucide-react'; import { useAuthServiceContext, useCommunicationServiceContext, useFhirServiceContext, usePractitionerServiceContext, } from '../../services'; import { useCapabilityServiceContext } from '../../services/CapabilityService'; import { organizationIdsWithFrontDesk } from '../../utils/patientMessagingDirectory'; import { mayOfferFrontDeskDoor } from '../../utils/messaging/frontDeskDoor'; import { isSaveRetired } from '../../utils/saveOperation'; /** * - **message the FRONT DESK about this patient**, from wherever the patient is already in hand. * * SPENCER'S USE CASE IS THE DESIGN ARGUMENT, so it is quoted rather than summarised * * *"if billing recognized an issue with a **patient's** invoices/payments and wants the front desk to * know right away about it before a patient checks in. They'll want whoever is working now and can * take it quickly to get the message, not try and message a specific receptionist directly and hope * they have time to see it before the patient comes in."* * * **THE UNIT IS WHO IS ON THE DESK RIGHT NOW.** A named-receptionist message goes to a person who may * be off shift; the pool goes to the office, and is answered by whoever can answer it. It is * time-critical by construction - the value expires when the patient reaches the desk. * * WHY THIS IS A BUTTON ON THE INVOICE AND NOT AN ENTRY IN THE COLLEAGUE DIRECTORY * * Spencer ruled (A): **the message names the patient it is about**. So the door needs a patient - and * **HR, Billing, Marketing and the Platform Manager cannot read a Patient AT ALL**: `GET /R4/Patient` * lists only Practitioners and Receptionists, with no `requiredPermission` for an org to grant through, * and `Patient.read` is minted only for those two. * Those roles can never NAME a patient from a cold directory, so *"open Messages, click Front desk"* * would be a click that cannot complete - the **"door onto nobody"** this card's own §2 warns about, * arriving through the front. **It belongs where the patient is already in hand**, which is exactly where * the use case starts. An `Invoice` carries `subject.reference` and `subject.display`, and billing reads * both with no patient scope whatsoever. * * **DELIBERATELY REUSABLE.** Nothing here knows about invoices. The day an appointment or a chart * wants the same door it passes the same three props - and a rule re-derived per surface is a rule that * disagrees with itself, which is why the decision lives in `mayOfferFrontDeskDoor`. */ interface Props { /** The patient the message is ABOUT - Spencer's (A) ruling. Absent no door. */ patientId?: string; /** How the sender refers to them. Presentation only; the thread is keyed on the id. */ patientLabel?: string; /** The practice whose desk this is. Absent the caller's entry org, resolved server-side. */ organizationId?: string; className?: string; } export const MessageFrontDeskButton: React.FC = ({ patientId, patientLabel, organizationId, className, }) => { const navigate = useNavigate(); const { userRole, USER_ROLES } = useAuthServiceContext(); const { practitionersList } = usePractitionerServiceContext(); const { extractReferenceId } = useFhirServiceContext(); const { openFrontDeskThread } = useCommunicationServiceContext(); const { mayCall } = useCapabilityServiceContext(); const [busy, setBusy] = useState(false); const [failed, setFailed] = useState(false); /** * DERIVED, NEVER AUTHORED - the same set the patient's own picker uses. Offering *Front desk* at a * practice with no receptionist is a door onto nobody: the pool resolves its members BY ROLE at read * time, so the message would sit unread with nobody it could notify, and the sender has been told the * office knows. */ const orgIdsWithFrontDesk = useMemo( () => organizationIdsWithFrontDesk(practitionersList, (role) => { const id = extractReferenceId(role.organization) as string | undefined; return id || role.organization?.display?.trim(); }), [practitionersList, extractReferenceId], ); const organizationHasFrontDesk = organizationId ? orgIdsWithFrontDesk.has(organizationId) // No org named by the surface: the caller has one desk to reach, so ANY desk in their directory // is that desk. Not `true` - a practice with no receptionist at all still gets no door. : orgIdsWithFrontDesk.size > 0; const offered = mayOfferFrontDeskDoor({ organizationHasFrontDesk, // A receptionist IS the desk. Offering them a route to message themselves is nonsense, and it // would let them "claim" a thread nobody sent. callerIsReceptionist: userRole === USER_ROLES.RECEPTIONIST, patientId, // `mayCall` returns `undefined` for NOT KNOWN, and the predicate treats only `false` as a // denial - convention. Hiding the door while capability is still answering makes the // surface flicker; the request is its own backstop. // The path is written WITHOUT /R4 - mayCall adds it. mayOpenThread: mayCall('POST', '/Communication/thread'), }); if (!offered) return null; const open = async () => { if (!patientId) return; setBusy(true); setFailed(false); try { const thread = await openFrontDeskThread(patientId, organizationId); // The staff Messages page opens a thread by its CommunicationRequest id - the same deep link // the receptionist overview already uses, rather than a second mechanism. navigate('/dashboard/staff-messages', { state: { tab: 'colleagues', openRequestId: thread.request?.id }, }); } catch (error) { // Retired with the patient or practice it was opened for: nowhere to go, and nothing failed here. if (isSaveRetired(error)) return; // SAID OUT LOUD. A silent failure here is the exact defect was raised for on the // patient's side: a message that looks sent and never left the browser. The sender is about to // walk away believing the desk has been told. console.error('Could not open the front-desk conversation', error); setFailed(true); } finally { setBusy(false); } }; return (
{/* Names the patient so the sender can see WHICH conversation they are about to open — the whole point of the (A) ruling is that this message is about somebody. */} {patientLabel && !failed && (

Goes to whoever is on the desk now, about {patientLabel}.

)} {failed && (

Could not open that conversation — the front desk has not been told. Please try again.

)}
); };