import * as React from "react" import { Info } from "lucide-react" import { cn } from "../../lib/utils" import { Alert, AlertDescription } from "./alert" import { DismissibleAlert } from "./dismissible-alert" /** * - the one place that decides what a resource list says when it has nothing to show. * * The defect it exists to make impossible * Every clinical list surface hand-rolled two INDEPENDENT renders: * * {error && {error}} * {!loading && xList.length === 0 && No X found.} * * The second carries no `error` guard, so a refused fetch rendered BOTH at once - a futile * "Please try again" *and* "No health conditions found." The second is a FALSE CLINICAL * ASSERTION: the patient may well have conditions, this clinician simply may not view them. In a * health record "none recorded" and "you may not see these" are different facts, and a clinician * can act on the first. * * **A failed load and an empty result are different outcomes and must never render together.** * That invariant lives here, once, and is pinned by tests - rather than in ~20 hand-kept copies, * which is precisely how duplicated telehealth rule drifted. * * Why the copy does NOT name authorization as the cause * It would be honest only if we knew, and we do not. There is no `GatewayResponse` anywhere in * `aws/sam`, so API Gateway's default 4XX carries no `Access-Control-Allow-Origin`; the browser * blocks the denial and `fetch()` rejects with an opaque `TypeError` that never reaches * `handleErrorResponse`, so no `HttpError.status` is ever produced ( ). * A refusal is therefore INDISTINGUISHABLE from a network outage at this layer. Asserting "this * patient has not authorized this practice" would swap one false statement for another whenever the * network is genuinely down. So we state the disjunction, and we state the one thing that is * certain either way: **absence of a list is not a finding of absence.** * * Naming the cause with certainty is a follow-on, not a rewrite of this: the product already * knows the answer out-of-band (the org-authorization status endpoint that backs the receptionist's * "not authorized" badge). When that is wired in, only the copy below changes - the invariant does not. */ /** * The failure sentence, as ONE source of truth. * * Exported because a second surface needs the same words in a different LAYOUT. The clinician's * chart snapshot (`PatientSnapshot/SnapshotChart`) renders a compact per-type line inside a section * that may list ~30 types, so dropping this component's `DismissibleAlert` in there would be a wall * of banners. But hand-copying the sentence is exactly how duplicated rule drifted - and * this component's own header says so. So the WORDS live here once; the two call sites differ only * in how they present them. * * Note the curly apostrophe in "patient's" (U+2019), matching the `’` this file has always * rendered. A hand-typed straight quote elsewhere would silently fail to match this string. */ export const RESOURCE_LOAD_FAILURE_DETAIL = "You may not have access to this patient’s record, or the connection failed. " + "This is not a statement that the patient has none on file." /** The bolded lead of the failure sentence: `We couldn't load conditions.` */ export const resourceLoadFailureHeadline = (resourceLabel: string) => `We couldn't load ${resourceLabel}.` /** A refused read, named as one: `Allergies are not available to you.` */ export const resourceRefusedHeadline = (resourceLabel: string) => `${resourceLabel.charAt(0).toUpperCase()}${resourceLabel.slice(1)} are not available to you.` export const RESOURCE_REFUSED_DETAIL = "This is not a statement that the patient has none on file." /** The complete failure sentence, headline + detail, as a clinician reads it. */ export const resourceLoadFailureMessage = (resourceLabel: string) => `${resourceLoadFailureHeadline(resourceLabel)} ${RESOURCE_LOAD_FAILURE_DETAIL}` export interface ResourceListStatusProps { /** True while the load is in flight. Nothing is rendered - the caller owns its spinner. */ loading: boolean /** * Any TRUTHY value means THE LOAD FAILED. Call sites disagree on how they spell "no error" - * some hooks use `string | undefined`, some pages use `useState(null)` - so this * is deliberately truthiness-based and accepts both. Do NOT "tighten" this to * `error !== undefined`: that reads `null` as a failure and would permanently suppress the empty * state on every `useState` caller. Pinned by a test. */ error?: string | null /** Whether the list is empty. Only consulted when the load actually succeeded. */ isEmpty: boolean /** * - WAS THE SERVER ACTUALLY ASKED? * * The header above says the empty state is *"reachable ONLY on a successful load, which is what makes * it a true statement."* **That was assumed, not enforced**, and the callers do not hold it up. * * Every `useFhirX` hook opens `useState(false)` for `loading` and early-returns when `patientState` * is unresolved - so `setLoading(true)` never runs and the effect never fires. The component is then * handed `loading: false` - `error: undefined` - `isEmpty: true`, which is **byte-identical to a load * that succeeded and returned nothing** - and a patient is told *"No procedures found."* about a * question nobody asked. * * **This is own family arriving through the CALLER.** 150 made "a failure and an empty * result must never render together" impossible from inside the component; it could not - from in * here - make "no load at all" distinguishable from "loaded and empty". This prop is what supplies * that fact, because only the caller knows it. * * **NOT a way to suppress the empty message.** `false` produces its OWN sentence; it does not * silence the third state. A blank region is indistinguishable from a broken page, which is the * defect this card's own lane forbids trading for. * * Optional, defaulting to `true`, so the nineteen call sites not yet wired keep exactly today's * behaviour rather than silently flipping to the new state. The gap that leaves is NOT invisible: * `__tests__/resourceListStatusNeverAsked.test.tsx` pins the outstanding call sites BY NAME and fails * if the list grows, so a new surface cannot quietly join them. */ attempted?: boolean /** Lowercase plural, as a clinician would say it: "conditions", "allergies", "procedures". */ resourceLabel: string /** The load was answered with a 403, so the failure is named as a denial, not a lost connection. */ refused?: boolean /** * Shown ONLY on a successful load that genuinely returned nothing. * OMIT when the caller renders its own empty state because that state carries actions rather * than just a message (AllergyIntolerances offers "Add Allergy" / "No Known Allergies"). This * component then owns the FAILURE only, and the caller must gate its own block on `!error`. */ emptyMessage?: string /** * The sentence under the headline, for a surface whose reader is NOT a clinician looking at * somebody else's chart. * * MEASURED 2026-09-14 in a real two-party visit: a patient who joined before anything had been * said was shown the default - "You may not have access to this patient's record... This is not * a statement that the patient has none on file" - about themselves, in the third person. The * words are right for the chart surfaces they were written for and wrong in a room the reader * is standing in. * * OPTIONAL, and the default is the shared sentence, so nothing already using this moves. The * point is that the words still live in ONE place per audience rather than being hand-copied at * a call site, which is the drift this file's own header exists to prevent. */ failureDetail?: string /** Omit to render a non-dismissible failure banner. */ onDismissError?: () => void className?: string } /** * What a list says when nobody asked the server about it. * * Exported for the same reason the failure sentence is: the words live here ONCE. A second surface * hand-copying them is how duplicated rule drifted, and this file's own header says so. * * It states a fact about the SCREEN, never about the record. *"No procedures found"* is a claim about * the patient; *"hasn't loaded"* is a claim about us, and only one of those can be made without asking. */ export const resourceNotAskedMessage = (resourceLabel: string) => `${resourceLabel.charAt(0).toUpperCase()}${resourceLabel.slice(1)} haven't loaded yet.` export function ResourceListStatus({ loading, error, isEmpty, resourceLabel, failureDetail, emptyMessage, onDismissError, attempted = true, refused = false, className, }: ResourceListStatusProps) { // A dismissed failure is still a failure: until the next load the list is unknown, never empty. const [failedSinceLoad, setFailedSinceLoad] = React.useState(false) React.useEffect(() => { if (error) setFailedSinceLoad(true) else if (loading) setFailedSinceLoad(false) }, [error, loading]) // FAILED takes precedence over everything except loading, and deliberately renders even // when rows are on screen: a refresh that failed over stale data must not pass as current. if (error) { return ( {refused ? resourceRefusedHeadline(resourceLabel) : resourceLoadFailureHeadline(resourceLabel)} {" "} {refused ? RESOURCE_REFUSED_DETAIL : failureDetail ?? RESOURCE_LOAD_FAILURE_DETAIL} ) } if (loading) { return null } // NEVER ASKED and it sits HERE, immediately before the empty state, because that // adjacency IS the fix: these two were one branch, and the whole defect was that "we did not ask" fell // through into a sentence about the patient's record. // // A QUIET LINE, NOT AN ALERT, and that is a judgement rather than styling. The common cause is a // patient id that has not resolved yet, so on most routes this is transient - nineteen alert banners // flashing across a chart on every load would teach a clinician to ignore the one that is real. But it // is NOT nothing: if the id never resolves the line persists and is diagnosable, where a blank region // is indistinguishable from a broken page. if (!attempted) { return (

{resourceNotAskedMessage(resourceLabel)}

) } if (failedSinceLoad) { return (

{resourceLoadFailureHeadline(resourceLabel)}

) } // EMPTY reachable ONLY on a successful load, which is what makes it a true statement. // And that is now ENFORCED rather than assumed: `attempted` gates the branch above it. if (isEmpty && emptyMessage) { return ( {emptyMessage} ) } return null } export default ResourceListStatus