import { useState } from "react"; import type { Organization, Extension, Attachment } from "fhir/r4"; import { Link } from "react-router-dom"; import { CalendarPlus, ChevronDown, MessageSquareText, Phone } from "lucide-react"; import { OrgDetailBody } from "./OrgDetailBody.tsx"; import { ORG_LOGO_EXTENSION_URL } from "../../../config/organizationExtensions.ts"; import { getHostedPatientPortalUrl, getPatientPortalUrl } from "../../../utils/patientPortalUrl.ts"; import { formatSingleAddress } from "../../../utils/fhirAddress.tsx"; import { getFirstContactPointBySystem } from "../../../utils/fhirContactPoint.tsx"; import { formatPhoneNumber } from "../../../utils/phoneNumber.tsx"; import { PortalDepartureCard } from "../../../components/PortalDeparture/PortalDepartureCard"; import { useBookAppointmentDialog } from "../../../components/BookAppointmentDialog/BookAppointmentDialogProvider"; import { usePhotoUrl } from "../../../hooks/usePhotoUrl.ts"; import { initialsOf } from "../../../components/ui/org-badge.tsx"; import { Alert, AlertDescription, AlertTitle } from "../../../components/ui/alert.tsx"; import "./MyProvidersPage.css"; import { isUnreadableOrganization } from "../../../utils/patientOrganizations"; const getLogo = (org: Organization): Attachment | undefined => org.extension?.find((e: Extension) => e.url === ORG_LOGO_EXTENSION_URL) ?.valueAttachment as Attachment | undefined; // Smaller than this on either side is not a picture of anything; staging holds a 1x1 logo that drew as // a flat colour block (the inbox avatar's rule, 50046935). const SMALLEST_USABLE_IMAGE = 8; /** * The practice's mark, the shared round .ak-org-mark: its logo when it has a usable one, otherwise its * initials. Logos are always round. A practice with no logo used to show a stock * hospital photograph, and a logo that fails to load or is a speck falls back to the initials too. */ export const PracticeMark = ({ organization, size }: { organization: Organization; size?: "sm" | "lg" }) => { const mark = "ahr-practice-mark ak-org-mark" + (size ? " ak-org-mark--" + size : ""); const logo = getLogo(organization); const hasLogo = Boolean(logo?.data || logo?.url); const { url } = usePhotoUrl(logo, "organization"); const [unusable, setUnusable] = useState(null); const name = organization.name?.trim() || "Practice"; if (hasLogo && url && unusable !== url) { return ( ); } return ( ); }; export const OrgListCard = ({ organization, isEntryOrg, onRetry, retrying = false, isOpen = false, onToggle, }: { organization: Organization; isEntryOrg: boolean; /** Re-reads this practice when it could not be read. Absent means there is no way to retry. */ onRetry?: () => void; /** This practice is being re-read now. */ retrying?: boolean; /** Whether this practice is opened. The LIST owns it, so only one is open at a time. */ isOpen?: boolean; onToggle?: () => void; }) => { const { openBookAppointment } = useBookAppointmentDialog(); const address = organization.address?.[0] ? formatSingleAddress(organization.address[0]) : ""; /** * ONE NUMBER, PLAIN - and the Call control below depends on this as much as the row does. * * `formatFhirContactPoint` appends the contact point's `use` and joins every phone on file with a * comma, which is right where a patient has several and is wrong here twice over. Watched at 390 on * the mount: the row read "Fort Madison IA (608) 555-0142 (wo..." - the qualifier pushed the number * off the end of the line the deck draws whole. * * AND THE SECOND HALF IS NOT COSMETIC AT ALL. Call builds its `tel:` href by stripping non-digits * from this string, so a practice with two numbers on file produced one href made of both of them * run together - a number that dials nothing, on the control a patient presses when they want * somebody now. */ const phoneContact = getFirstContactPointBySystem(organization.telecom, "phone"); const phone = phoneContact?.value ? formatPhoneNumber(phoneContact.value) : ""; /** * WHERE THEY ARE, NOT THEIR STREET ADDRESS - the row's line is "West Burlington IA - (319) 555-0142". * * The deck puts place and phone on ONE line on the row and the full street address INSIDE the opened * card, and the reason is what the two lines are for. A patient scanning a list of practices is * telling them apart - which town, and can I ring them - and the street number is the thing you want * once you have picked one and are working out how to get there. Two lines of address on every * collapsed row pushed the hint sentence and the next practice down the phone for a datum nobody was * reading yet. * * Joined from the parts that exist, never templated: a practice with a city and no state reads * "Cedar Valley", not "Cedar Valley ". */ const place = [organization.address?.[0]?.city, organization.address?.[0]?.state] .filter((part) => Boolean(part && part.trim())) .join(" "); const rowLine = [place, phone].filter(Boolean).join(" \u00b7 "); // A practice the server could not read keeps whatever the list knew (its name, at least, from the // patient's own record) and says what is missing, with a retry for that practice alone. if (isUnreadableOrganization(organization)) { const name = organization.name?.trim(); return (

{name || "One of your practices"}

{rowLine &&

{rowLine}

}
{isEntryOrg && You’re here}
We couldn’t load {name || "this practice"}.

Appointments and contacts for this practice are temporarily unavailable.

{onRetry && ( )}
); } // Match the chooser: a configured page takes precedence over the server-resolved hosted door. const portalUrl = isEntryOrg ? undefined : getPatientPortalUrl(organization) || getHostedPatientPortalUrl(organization); /** * THE ROW OPENS IN PLACE. It used to be a `` to a page of its own, and the deck records that * shape being tried and written out: "A practice is a card in a list: opening one must not spill out * of it, and there is no page to go to." * * THE BODY RENDERS OUTSIDE THIS BUTTON AND INSIDE THE CARD, which is a correctness constraint rather * than a layout choice: a ` {isOpen && (
{/* THE DECK'S ORDER: where they are, then leaving for their portal, then the two ways to reach them, then what this practice holds. All of it acts on THIS practice, so all of it lives in its card - the actions and the departure used to sit outside it, where at 390 they read as belonging to the list rather than to the practice they name. */} {address &&

{address}

} {portalUrl && }
{/* NEITHER LABEL NAMES THE PRACTICE, and it is one argument covering both. The card's own header names it two lines above, so repeating it here spends the width on something the reader has just finished reading. The deck makes that case about the phone - "the number is on the card immediately above it, so printing it again made a button out of something the reader had just finished reading" - and it holds just as well for the practice's name. THEY ARE NOT THE SAME WIDTH, and the deck sets the ratio rather than leaving it to the labels: `.pd-msg{flex:1.5}` against `.pd-tel{flex:1}`. Writing to the front desk is the ordinary thing to do from here and calling is for the patient who wants somebody now, so equal halves would say the two are equally likely. Both are outlined and neither is filled, because neither is this card's primary action. */} {/* `btn-blue-ghost`, NOT `btn-outline`. The latter was on both of these and is not a class this app defines - `grep` finds it in exactly these two places and in no stylesheet - so the controls inherited the base `.btn`'s `1px solid transparent` and rendered as bare text with an icon. Watched at 390: two labels floating with no edge, where the deck draws `border:1px solid var(--primary); color:var(--primary)`, which is what `btn-blue-ghost` is. Neither is filled, because neither is this card's primary action. */} )}
)} ); // ONE SHAPE NOW, WHICHEVER THE PRACTICE IS. This used to return two different wrappers - one // with a portal departure and one without - because both controls hung OUTSIDE the card. They // are inside it now, so the difference is a conditional within the opened region rather than two // returns, and a collapsed row is the same element for every practice. return row; }; export default OrgListCard;