import { Ban, Banknote, ChartPie, CircleAlert } from "lucide-react";
import { cn } from "../../../lib/utils";
/*
* THE STATE OF A BILL, AS A MARK RATHER THAN A PILL.
*
* A STATUS IS NOT A CONTROL, AND A FILLED PILL SAYS IT IS. The row rendered these as solid shadcn
* Badges - a red "Past due", a blue "Sent" - which are the same shape, weight and colour as this
* product's buttons, so a patient read the state of their bill as something to press.
*
* AND BEING LAST IN THE ROW MADE IT THE PART THAT GOT CLIPPED. Measured in the mockup at 390px, a bill
* carrying tax and a "Past due" pill needed 368px in a 361px row - clipped rather than reflowed, so the
* bill that most needed attention was the one whose state went missing first. The pill is 64px of text
* where the mark is 17px of icon.
*
* EVERY STATE CARRIES ITS OWN SILHOUETTE, NOT JUST ITS OWN COLOUR - the same rule `ui/status.tsx`
* follows, for the same reason: colour alone fails for roughly one man in twelve, and a red dot and a
* green dot are the same dot. Alert ring - banknote - banknote-with-tick - struck-through circle -
* pie chart. Take the shape away and this stops doing the one thing it is for.
*
* It is separate from `ui/status.tsx` deliberately. That component speaks the FHIR CLINICAL vocabulary
* and derives its tone from `fhirStatusVocabulary`; a bill's states are money words a patient reads about
* their own account. Adding a variant prop there is explicitly ruled out, and rightly.
*/
export type { InvoiceState } from "./invoiceDisplay";
import type { InvoiceState } from "./invoiceDisplay";
/**
* The banknote-with-tick is drawn rather than imported: the mockup uses it and the pinned lucide
* version has no such icon, so the paths are lifted from the drawing verbatim.
*/
const BanknoteTick = ({ className }: { className?: string }) => (
);
type Presentation = {
label: string;
className: string;
/** Both a lucide icon and a locally drawn one - they agree on the only prop used here. */
Icon: React.ComponentType<{ className?: string }>;
};
/*
* THE PATIENT'S WORDS, NOT THE BILLER'S - and the two were the same map until now.
*
* `STATUS_LABEL` in invoiceDisplay.ts says so in its own comment: its wording was chosen because
* "Sent"/"Void" read better TO A BILLER. That is true, and this component is the one the patient
* reads. On a screen whose whole job is what do I owe, to whom, by when, "Sent" answers a
* different question - it says what the practice did, not what the patient owes.
*
* Taken from the approved drawing rather than chosen here: mockups/patient-portal/
* bills-and-payments.html shows every row ending in one of Past due / Due / Paid / Cancelled.
*
* The biller's map is left exactly as it was. Two audiences, two vocabularies, and the
* separation is the point - the Billing workspace's own list still says Sent and Void, which is
* correct there.
*/
const PRESENTATION: Readonly> = {
// The one state that is about to cost the patient something.
"past-due": { label: "Past due", className: "text-destructive", Icon: CircleAlert },
// Blue, the product's "this is live and normal" - not green, which would read as settled.
sent: { label: "Due", className: "text-primary", Icon: Banknote },
paid: { label: "Paid", className: "text-success", Icon: BanknoteTick },
// Spencer 08-28: the chart-pie icon for partial payments; the word is the deck's.
"part-paid": { label: "Partially paid", className: "text-warning", Icon: ChartPie },
void: { label: "Cancelled", className: "text-faint", Icon: Ban },
// Deliberately quiet: a draft is not a bill anybody owes yet.
draft: { label: "Draft", className: "text-ink-muted", Icon: Banknote },
};
/** The biller's words where they differ; the Billing workspace says Sent and Void. */
const STAFF_LABEL: Readonly>> = {
sent: "Sent",
void: "Void",
};
/** The billing badge per state as a square status pill; draft and void take the plain pill. */
const BADGE: Readonly> = {
"past-due": "ak-pill--bad",
sent: "ak-pill--info",
paid: "ak-pill--ok",
"part-paid": "ak-pill--warn",
void: "",
draft: "",
};
export interface InvoiceStatusMarkProps {
state: InvoiceState;
/** Whose vocabulary the label uses; the patient's unless a biller is reading. */
audience?: "patient" | "staff";
/**
* Drops the word and keeps the mark. The wide row uses this - the practice name has taken the
* space under the invoice number, so the far right holds a mark rather than a phrase. The label
* survives as the accessible name and the tooltip, so nothing is lost to a reader who needs it.
*/
iconOnly?: boolean;
/** The billing deck's list badge: the word in a squared badge, no icon. */
bordered?: boolean;
className?: string;
}
/**
* The word this state shows a patient.
*
* Exported so a caller building an accessible name for a whole row says the SAME word the row
* displays. They were derived separately before, from two different maps, so a row could read
* "Due" and announce "Sent".
*/
export const invoiceStateLabel = (state: InvoiceState): string => PRESENTATION[state].label;
export const InvoiceStatusMark = ({ state, audience = "patient", iconOnly = false, bordered = false, className }: InvoiceStatusMarkProps) => {
const { className: tone, Icon } = PRESENTATION[state];
const label = (audience === "staff" ? STAFF_LABEL[state] : undefined) ?? PRESENTATION[state].label;
return (
{!bordered && }
{!iconOnly && label}
);
};