import { FormEvent, useState } from 'react'; import { Building2, ChevronLeft, Search, UserRound } from 'lucide-react'; import { Button } from '../ui/button.tsx'; import { Input } from '../ui/input.tsx'; import { Badge } from '../ui/badge.tsx'; import { DismissibleAlert } from '../ui/dismissible-alert'; import { Skeleton } from '../ui/skeleton.tsx'; import { useReferralDirectory, type DirectoryLocation, type DirectoryOrganization, type DirectoryPractitioner, type DirectoryResult, } from '../../hooks/useReferralDirectory.tsx'; // THE REFERRAL DIRECTORY SEARCH -, design ruling Addendum 4. // // ORG FIRST, THEN THE PEOPLE AT THE LOCATION YOU PICKED. Not a stylistic ordering: measured against // the live registry, the org-then-address direction returned 77 named cardiologists at one institution // while the practitioner-first direction resolved 3 of 6 searches and was ambiguous when it did. And it // is how a referring practitioner actually thinks - *"send them to cardiology at University of Iowa"*, // not *"find me Dr Someone."* // // THE REFERRER MAY STOP AFTER STEP ONE. Addendum 5: a referral addressed to a department rather than a // person is normal and plausibly the more common case - the receiving org triages internally, and the // referrer usually cannot know who is available. So "Refer to this organization" is a first-class ending, // never a fallback for a failed practitioner search. export interface ReferralTarget { organization: DirectoryOrganization; location?: DirectoryLocation; /** Absent = the referral is addressed to the organization. That is a complete answer. */ practitioner?: DirectoryPractitioner; } interface ReferralDirectorySearchProps { onSelect: (target: ReferralTarget) => void; /** Offered when the registry cannot answer, so a referral is never blocked on a third party. */ onEnterManually?: () => void; /** Also offer manual entry when the registry answers with nothing, as a send-out laboratory often is not listed by its common name. */ manualOnNoMatch?: boolean; /** What choosing an organization is called where it is not a referral. */ chooseLabel?: string; } const addressLine = (location: DirectoryLocation | undefined): string => { if (!location) return 'No practice address listed'; return [ location.street, location.city && `${location.city}, ${location.state} ${location.postalCode}`.trim(), ].filter(Boolean).join(' ยท '); }; /** * THE HONEST LIMIT, SAID ON THE SCREEN RATHER THAN IN A COMMENT. The registry is an IDENTIFIER * registry, not a licensure one - an NPI is not revoked when a licence lapses. Somebody choosing where to * send a patient must not read a listing as verification. */ const REGISTRY_CAVEAT = 'Listings come from the national provider registry. It confirms a provider is registered โ€” it does not confirm a licence is current.'; const Unavailable = ({ onEnterManually }: { onEnterManually?: () => void }) => (

The provider registry is not answering right now.

{onEnterManually && ( )}
); const Refused = ({ onEnterManually }: { onEnterManually?: () => void }) => (

You do not have permission to search the provider registry.

{onEnterManually && ( )}
); const ResultsSkeleton = () => (
{[0, 1, 2].map((row) => (
))}
); export const ReferralDirectorySearch = ({ onSelect, onEnterManually, manualOnNoMatch = false, chooseLabel = 'Refer to this organization', }: ReferralDirectorySearchProps) => { const { searchOrganizations, searchPractitioners } = useReferralDirectory(); const [organizationName, setOrganizationName] = useState(''); const [organizationState, setOrganizationState] = useState(''); const [specialty, setSpecialty] = useState(''); const [organizations, setOrganizations] = useState>(); const [practitioners, setPractitioners] = useState>(); const [chosen, setChosen] = useState(); const [loading, setLoading] = useState(false); const runOrganizationSearch = async (page = 0) => { setLoading(true); const result = await searchOrganizations({ name: organizationName, state: organizationState || undefined, page, }); // Appended rather than replaced when paging, so "Show more" adds to the list the reader is // already looking at instead of silently replacing it under them. setOrganizations((previous) => merge(previous, result, page)); setLoading(false); }; const runPractitionerSearch = async (organization: DirectoryOrganization, page = 0) => { setLoading(true); const result = await searchPractitioners({ postalCode: organization.location?.postalCode ?? '', street: organization.location?.street, specialty: specialty || undefined, state: organization.location?.state, page, }); setPractitioners((previous) => merge(previous, result, page)); setLoading(false); }; const chooseOrganization = async (organization: DirectoryOrganization) => { setChosen(organization); setPractitioners(undefined); await runPractitionerSearch(organization); }; const backToOrganizations = () => { setChosen(undefined); setPractitioners(undefined); }; const onSubmitOrganizationSearch = (event: FormEvent) => { event.preventDefault(); void runOrganizationSearch(0); }; const onSubmitSpecialty = (event: FormEvent) => { event.preventDefault(); if (chosen) void runPractitionerSearch(chosen, 0); }; if (chosen) { return (

{chosen.name}

{addressLine(chosen.location)}

setSpecialty(event.target.value)} />
{loading && !practitioners && } {practitioners && !practitioners.available && practitioners.reason === 'unavailable' && ( )} {practitioners && !practitioners.available && practitioners.reason === 'refused' && ( )} {practitioners?.available && ( onSelect({ organization: chosen, location: chosen.location, practitioner, })} onShowMore={() => void runPractitionerSearch(chosen, practitioners.page + 1)} /> )}

{REGISTRY_CAVEAT}

); } return (
{/* On a phone the name takes its own line: beside State and Search it had about 125 px. */}
setOrganizationName(event.target.value)} /> {/* The shared field rule sets every input to 100% width, which outranks a width class on the input, so the wrapper holds State to 6rem and keeps Search beside it. */}
setOrganizationState(event.target.value)} />
{loading && !organizations && } {organizations && !organizations.available && ( organizations.reason === 'unavailable' ? : organizations.reason === 'refused' ? : ( Enter at least two characters of the organization's name. ) )} {organizations?.available && organizations.results.length === 0 && (

No organizations matched that name. The registry lists the LEGAL name, which is often not the common one โ€” try a shorter piece of it.

{manualOnNoMatch && onEnterManually && ( )}
)} {organizations?.available && organizations.results.length > 0 && ( <>
    {organizations.results.map((organization) => (
  • ))}
void runOrganizationSearch(organizations.page + 1)} /> )}

{REGISTRY_CAVEAT}

); }; const PractitionerResults = ({ result, loading, onSelect, onShowMore, }: { result: Extract, { available: true }>; loading: boolean; onSelect: (practitioner: DirectoryPractitioner) => void; onShowMore: () => void; }) => { const atThisAddress = result.results.filter((entry) => entry.proximity === 'exact-street'); const nearby = result.results.filter((entry) => entry.proximity === 'nearby'); if (result.results.length === 0) { return ( No providers matched that specialty at this location. ); } return (
{atThisAddress.length > 0 && ( )} {/* โ›” NEVER MERGED INTO THE SET ABOVE. These are practitioners in the same postal code โ€” often the institution's other buildings, sometimes an unrelated practice. Offering them is useful; presenting them as being at the address the referrer picked is a different claim. */} {nearby.length > 0 && ( )}
); }; const Group = ({ label, people, onSelect, }: { label: string; people: DirectoryPractitioner[]; onSelect: (practitioner: DirectoryPractitioner) => void; }) => (

{label}

    {people.map((practitioner) => (
  • ))}
); /** * "THERE IS MORE" AND "THERE IS MORE AND YOU CANNOT HAVE IT" ARE DIFFERENT SENTENCES. The registry * stops paging at a fixed offset, so a wide search cannot be walked to its end - and a list that simply * stops reads as a complete answer. When that happens the reader is told to narrow instead of being * offered a button that would return nothing. */ const MoreResults = ({ result, loading, onShowMore, }: { result: { hasMore: boolean; pagingExhausted: boolean }; loading: boolean; onShowMore: () => void; }) => { if (!result.hasMore) return null; if (result.pagingExhausted) { return ( There are more matches than the registry will list. Narrow the search โ€” add a state, a city, or more of the name. ); } return ( ); }; /** Paging appends; a fresh search replaces. A failure never wipes results the reader is still reading. */ function merge( previous: DirectoryResult | undefined, next: DirectoryResult, page: number, ): DirectoryResult { if (!next.available) { return page > 0 && previous ? previous : next; } if (page === 0 || !previous?.available) { return next; } return { ...next, results: [...previous.results, ...next.results] }; } export default ReferralDirectorySearch;