import { useCallback, useEffect, useRef, useState } from 'react'; import { AlertCircle, Loader2, Share2 } from 'lucide-react'; import { useAuthServiceContext } from '../../../services/AuthService.tsx'; import { useOrganizationServiceContext } from '../../../services/OrganizationService.tsx'; import { REFERRAL_DESTINATIONS, resolveOrgReferralRouting, } from '../../../config/referralRouting.ts'; import type { ReferralDestination, ReferralRoutingPolicy } from '../../../config/referralRouting.ts'; import { Button } from '../../../components/ui/button.tsx'; import { Input } from '../../../components/ui/input.tsx'; import { Label } from '../../../components/ui/label.tsx'; import { RadioGroup, RadioGroupItem } from '../../../components/ui/radio-group.tsx'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../components/ui/select.tsx'; import { Alert, AlertDescription } from '../../../components/ui/alert.tsx'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; import { readOrganizationNpi } from '../../../utils/organizationNpi.ts'; import { useStaffPermissionServiceContext } from '../../../services/StaffPermissionService'; import { useListOwner } from '../../../hooks/useListOwner'; import { isSaveRetired, isSaveTimedOut, outcomeUnknown, SaveRetiredError, TIMED_OUT_MAY_HAVE_SAVED, TIMED_OUT_NOTHING_SAVED, useSaveScope, type SaveOperation, type SaveTransport } from '../../../utils/saveOperation'; import { useReferralWorkbenchService } from '../../../services/ReferralWorkbenchService.tsx'; import type { ReferralSharedRouting } from '../../../services/ReferralWorkbenchService.tsx'; //, client half - where this practice's incoming referrals land. // // WHAT ACTUALLY READS THIS, stated because the previous version of this comment asserted a second reader // that does not exist and reads as a description of behaviour. Anybody checking whether the setting // matters found a confident answer that was wrong, which is why nobody looked. // // THE RECEIVING SIDE READS IT, and that half is real: the redeem handler asks for this practice's // destination and, when it is the front desk, delivers the arriving referral as a message into the // existing front-desk pooled thread. That is Spencer's 2026-08-23 ruling - the front-desk option needed // no new surface, because the option's own copy already promised "alongside everything else they handle". // // THE SENDING SIDE READS IT TOO. The referral directory carries TWO flags rather than // one: `onAkashic` says this practice has an account, and `acceptsReferrals` says a referral sent through // Akashic would land somewhere it has designated - which is this setting, read by the server's own // `acceptsInSystemReferrals` off the Organization resource it already fetched to confirm the NPI match. // => Choosing a destination here is what makes this practice addressable in-system to another practice. // // AND IT ONLY WORKS IF THIS PRACTICE HAS RECORDED AN NPI, because the directory matches registry // listings to Akashic accounts on that number and nothing else. A practice with a destination and no NPI // is still invisible to a sender - the setting is necessary and is not sufficient. // // AND THE QUEUE ROW IS NOT GATED ON THIS AT ALL, which is the part that must not drift: the redeem // handler writes the row before it reads this setting. A destination decides WHERE a referral lands and // must never decide WHETHER it lands. // // Mirrors AppointmentPolicySection's shape exactly, because the server module it talks to deliberately // mirrors that one: Radix ui/* + Tailwind + lucide, seed-on-org-change via an ID-keyed effect, // `btn btn-green` save, and the server's 400 surfaced verbatim. // // There is no GET for this setting - the saved routing travels only on the Organization resource, so the // initial state comes from resolveOrgReferralRouting(currentOrganization). const NOT_CHOSEN = 'not-chosen'; // Radix will not take an empty string as an item value, so "no preference" needs a token of its own. // It is mapped back to `undefined` on the way out, because the server's own list drops anything it does // not recognise and an invented method would render as a channel nobody can use. const NO_PREFERENCE = 'no-preference'; /* * HOW ANOTHER PRACTICE SHOULD REACH THIS ONE - the second half of the same question, and the design * named it alongside the destination: *"Capture the contact method at onboarding - first row of the * shared contact table, self-reported rather than guessed from NPPES."* * * IT IS SELF-REPORTED, WHICH IS THE WHOLE REASON IT IS A FORM. The registry carries an address and a * phone; it does not carry which desk referrals go to, or that the fax by the door is the wrong one. * Only the practice knows that, and every practice that ever refers here inherits what is typed. * * NO PRIVATE NOTE HERE, deliberately. The note on this table is one org's private observation about * ANOTHER practice - a relationship somebody earned. A practice writing about itself has no such thing, * and offering the field would invite a note nobody but its author can ever read. */ const CONTACT_FIELDS: { key: keyof ReferralSharedRouting; label: string; help?: string; type?: string; }[] = [ { key: 'fax', label: 'Fax number for referrals', help: 'Where a practice that cannot reach you through Akashic should send the packet.', }, { key: 'phone', label: 'Phone number for referrals' }, { key: 'email', label: 'Email address for referrals', type: 'email' }, { key: 'routeTo', label: 'Who referrals should be addressed to', // A ROLE OR A DESK, NEVER A PERSON. The whole table is shared, so a named individual here is // published to every practice that ever refers - and it goes stale the day they leave. help: 'A role or a desk — "the referral department", not a person\'s name.', }, { key: 'hours', label: 'When somebody can be reached', help: 'For example, "Mon–Thu 8–4, closed Fridays".', }, ]; /** What the practice would rather receive. Mirrors the server's own list, which drops anything else. */ const PREFERRED_METHODS: { value: NonNullable; label: string }[] = [ { value: 'in-system', label: 'Through Akashic' }, { value: 'fax', label: 'Fax' }, { value: 'phone', label: 'Phone' }, { value: 'email', label: 'Email' }, { value: 'portal', label: 'Our own portal' }, ]; const OPTIONS: { value: ReferralDestination; label: string; help: string }[] = [ { value: REFERRAL_DESTINATIONS.FRONT_DESK, label: 'The front desk', help: 'Referrals arrive with the receptionists, alongside everything else they handle.', }, { value: REFERRAL_DESTINATIONS.REFERRAL_WORKSPACE, label: 'A referral workspace', help: 'Referrals go to staff who hold the Referral Coordinator role. Use this when referrals are somebody’s job rather than part of the front desk’s.', }, ]; const ReferralRoutingSection = () => { const { currentOrganization, wpOrganization } = useAuthServiceContext(); const { activeWorkspace, permissionsSettled, canManage } = useStaffPermissionServiceContext(); const { owner, live } = useListOwner(undefined); const mayManage = Boolean(activeWorkspace) && permissionsSettled && canManage('organization'); const npi = readOrganizationNpi(currentOrganization ?? undefined); const practice = currentOrganization?.id; const key = JSON.stringify([practice, npi, activeWorkspace, mayManage, owner.viewer, owner.session]); const currentKey = useRef(key); currentKey.current = key; const isCurrent = useCallback(() => live(owner) && currentKey.current === key && Boolean(practice) && wpOrganization.current?.id === practice, [key, live, owner, practice, wpOrganization]); return ; }; const ReferralRoutingEditor = ({ ownerKey, npi, mayManage, isCurrent }: { ownerKey: string; npi: string | undefined; mayManage: boolean; isCurrent: () => boolean; }) => { const { currentOrganization, setWpOrganization } = useAuthServiceContext(); const { updateReferralRoutingSettings, fetchOrganizationAsCaller } = useOrganizationServiceContext(); const { readReferralContact, saveReferralContact } = useReferralWorkbenchService(); const [policy, setPolicy] = useState(() => resolveOrgReferralRouting(currentOrganization ?? undefined)); const [saving, setSaving] = useState(false); const [error, setError] = useState(); const [contact, setContact] = useState({}); const [contactSaving, setContactSaving] = useState(false); const [contactSaved, setContactSaved] = useState(false); const [contactError, setContactError] = useState(); const [contactReady, setContactReady] = useState(false); const [contactLoading, setContactLoading] = useState(false); const [routingUnknown, setRoutingUnknown] = useState(false); const policyRevision = useRef(0); const contactRevision = useRef(0); const routingOperation = useRef(null); const contactOperation = useRef(null); const contactRead = useRef(null); const scope = useSaveScope(ownerKey); const transportFor = useCallback((operation: SaveOperation, revision?: { current: number }, submitted?: number, sent?: () => boolean): SaveTransport => ({ ...operation.transport, admit: () => { operation.admit(); if (!isCurrent() || (revision && !sent?.() && revision.current !== submitted)) throw new SaveRetiredError(); }, }), [isCurrent]); // THE ROW IS KEYED BY THIS PRACTICE'S OWN NPI, which is also what the server checks the write // against. No NPI means there is no row to write, and the copy says so rather than offering a form // that would be refused. // A failed read is not an empty registry entry that may be saved over somebody else's contact details. const loadContact = useCallback(async () => { if (!npi || !mayManage || !isCurrent()) return; contactRead.current?.retire(); const operation = scope.start(); contactRead.current = operation; const transport = transportFor(operation); const revision = contactRevision.current; setContactLoading(true); setContactReady(false); setContactSaved(false); setContactError(undefined); try { const stored = await operation.step(() => readReferralContact(npi, { ...transport, organizationId: currentOrganization?.id })); transport.admit(); if (revision !== contactRevision.current) return; const { updatedAt: _updatedAt, ...shared } = stored.shared ?? {}; setContact(shared); setContactReady(true); } catch (failure) { if (operation.belongs() && isCurrent() && !isSaveRetired(failure)) { setContactError('Could not read these contact details. Check again before saving.'); } } finally { if (contactRead.current === operation && operation.belongs() && isCurrent()) setContactLoading(false); operation.retire(); } }, [currentOrganization?.id, isCurrent, mayManage, npi, readReferralContact, scope, transportFor]); useEffect(() => { void loadContact(); return () => contactRead.current?.retire(); }, [loadContact]); const changeContact = (change: (current: ReferralSharedRouting) => ReferralSharedRouting) => { if (!mayManage || !isCurrent() || !contactReady) return; contactRevision.current += 1; setContact(change); setContactSaved(false); }; const handleSaveContact = async () => { if (!npi || !mayManage || !isCurrent() || !contactReady || contactOperation.current?.belongs()) return; const revision = contactRevision.current; const operation = scope.start(); contactOperation.current = operation; let sent = false; const transport = { ...transportFor(operation, contactRevision, revision, () => sent), onSend: () => { sent = true; } }; setContactSaving(true); setContactError(undefined); setContactSaved(false); try { // `shared` ONLY. The private note on this table is one practice's observation about // ANOTHER, and a practice has no such note about itself. await operation.step(() => saveReferralContact(npi, { shared: contact }, { ...transport, organizationId: currentOrganization?.id }), true); transport.admit(); if (revision !== contactRevision.current) return; setContactSaved(true); } catch (err) { if (!operation.belongs() || !isCurrent()) return; const unknown = sent && outcomeUnknown(err); // A changed draft cannot settle whether the previous sent write reached the server. if (unknown) { setContactReady(false); setContactError(TIMED_OUT_MAY_HAVE_SAVED); return; } if (revision !== contactRevision.current || isSaveRetired(err)) return; console.error('Error saving this practice\'s referral contact details:', err); setContactError(!sent && isSaveTimedOut(err) ? TIMED_OUT_NOTHING_SAVED : relayServerFailure(err, 'Failed to save these details. Please try again.')); } finally { if (contactOperation.current === operation && operation.belongs() && isCurrent()) { contactOperation.current = null; setContactSaving(false); } operation.retire(); } }; const handleSave = async () => { if (!mayManage || !isCurrent() || routingUnknown || routingOperation.current?.belongs()) return; const revision = policyRevision.current; const operation = scope.start(); routingOperation.current = operation; let sent = false; const transport = { ...transportFor(operation, policyRevision, revision, () => sent), onSend: () => { sent = true; } }; setSaving(true); setError(undefined); try { const { organization } = await operation.step(() => updateReferralRoutingSettings(policy, transport), true); transport.admit(); if (organization?.resourceType !== 'Organization' || organization.id !== currentOrganization?.id) { setRoutingUnknown(true); setError(TIMED_OUT_MAY_HAVE_SAVED); return; } if (revision !== policyRevision.current) return; setWpOrganization(organization); setPolicy(resolveOrgReferralRouting(organization)); } catch (err) { if (!operation.belongs() || !isCurrent()) return; const unknown = sent && outcomeUnknown(err); if (unknown) { setRoutingUnknown(true); setError(TIMED_OUT_MAY_HAVE_SAVED); return; } if (revision !== policyRevision.current || isSaveRetired(err)) return; console.error('Error saving the referral routing setting:', err); setError(!sent && isSaveTimedOut(err) ? TIMED_OUT_NOTHING_SAVED : relayServerFailure(err, 'Failed to save this setting. Please try again.')); } finally { if (routingOperation.current === operation && operation.belongs() && isCurrent()) { routingOperation.current = null; setSaving(false); } operation.retire(); } }; const checkRouting = async () => { if (!currentOrganization?.id || !mayManage || !isCurrent() || routingOperation.current?.belongs()) return; const revision = policyRevision.current; const operation = scope.start(); routingOperation.current = operation; const transport = transportFor(operation); setSaving(true); try { const organization = await operation.step(() => fetchOrganizationAsCaller(currentOrganization.id!, undefined, currentOrganization.id, transport)); transport.admit(); if (organization?.resourceType !== 'Organization' || organization.id !== currentOrganization.id) { setError('Could not check the saved routing. Check again before saving.'); return; } setWpOrganization(organization); if (revision === policyRevision.current) setPolicy(resolveOrgReferralRouting(organization)); setRoutingUnknown(false); setError(undefined); } catch (failure) { if (operation.belongs() && isCurrent() && !isSaveRetired(failure)) setError('Could not check the saved routing. Check again before saving.'); } finally { if (routingOperation.current === operation && operation.belongs() && isCurrent()) { routingOperation.current = null; setSaving(false); } operation.retire(); } }; return (

Incoming referrals

Where referrals from other practices arrive.

{ if (!mayManage || !isCurrent() || routingUnknown) return; policyRevision.current += 1; setPolicy(value === NOT_CHOSEN ? {} : { destination: value as ReferralDestination }); }} className="space-y-3" > {OPTIONS.map((option) => (

{option.help}

))} {/* 🚨 "NOT CHOSEN" IS A REAL, SELECTABLE STATE, and it is not a placeholder. A practice that has designated nowhere is one other practices cannot deliver to in-system — they fax instead, which is what they do today anyway. Leaving this out would mean a practice could never WITHDRAW: once it had chosen, every sending practice would keep delivering into a queue it may have stopped watching. ⭐ And it is why the copy says what NOT choosing means, rather than treating it as an empty value nobody would pick on purpose. */}

Other practices will send you referrals by fax or phone, the way they do now.

{error && ( {error} )} {routingUnknown && }

How another practice should reach you

Shared with any practice referring a patient to you. It is how they reach you when they cannot send through Akashic.

{/* NO NPI, NO ROW. The record is keyed by this practice's NPI and the server checks a write against it, so a form offered here would be refused on save. Saying why, and naming where the NPI is recorded, beats a set of disabled inputs with nothing explaining them. */} {!npi ? ( Record your practice’s NPI above first. Other practices find you by that number, so there is nowhere to file these details until it is set. ) : ( <> {contactLoading &&

Checking these contact details.

} {!contactReady && !contactLoading && }
{CONTACT_FIELDS.map((field) => (
changeContact((current) => ({ ...current, [field.key]: event.target.value, }))} /> {field.help && (

{field.help}

)}
))} {contactError && ( {contactError} )}
{/* SAID ONLY AFTER A SAVE THAT ANSWERED. These details reach every practice that refers here, so somebody who typed a fax number and walked away needs to know it actually landed rather than assuming it from the form closing. */} {contactSaved && !contactSaving && ( Saved )}
)}
); }; export default ReferralRoutingSection;