import * as React from "react"; import { cn } from "@/lib/utils"; import { formatDateShort } from "@/lib/format-date"; /** * Statement document primitives - WealthX DS (L1 Atoms) * * Atomic building blocks for a generated statement document (the PDF a broker * downloads from Reports & Statements). Compose upward: * * Atom → StatementPage, StatementDocumentHeader, StatementField … * Organism → StatementTransactionTable * Template → StatementDocument * * A statement is a printed artefact, so these render on fixed white paper with * fixed ink colours rather than the themed app surface - see * `component-descriptions/bank-statement-document.md`. Tenant branding enters * through the `companyLogo` slot on `StatementDocumentHeader`. * * Reference: `Sample_Open_Banking_statement_.pdf` on COMPANY-501 - * missing values print as "Not available", exactly as the samples do. */ const NO_DATA = "Not available"; /** Ink colours for the printed page - deliberately not theme-reactive. */ export const STATEMENT_INK = "text-[#040d13]"; export const STATEMENT_INK_MUTED = "text-[#686e71]"; export const STATEMENT_RULE = "border-[#e0e0e0]"; export const STATEMENT_RULE_LIGHT = "border-[#f0f0f0]"; // --------------------------------------------------------------------------- // StatementPage // --------------------------------------------------------------------------- export interface StatementPageProps { children: React.ReactNode; /** * Render at A4 aspect ratio (210:297). Off by default so a long ledger can * grow past one page in Storybook instead of clipping. */ fixedHeight?: boolean; className?: string; } /** White A4-proportioned paper the statement is laid out on. */ export function StatementPage({ children, fixedHeight = false, className, }: StatementPageProps) { return (
{children}
); } // --------------------------------------------------------------------------- // StatementDocumentHeader // --------------------------------------------------------------------------- export interface StatementDocumentHeaderProps { /** Eyebrow above the title, e.g. "Open Banking". */ overline?: string; /** Document title, e.g. "Account statement". */ title?: string; institutionName?: string; /** Institution logo URL - shown beside the institution name. */ institutionLogo?: string; /** Tenant / company logo URL - shown top-right. White-label slot. */ companyLogo?: string; companyName?: string; /** CDR accreditation line, e.g. "Under CDR principal: ADRBNK000208". */ cdrLine?: string; className?: string; } /** Document title + institution left, tenant branding + CDR line right. */ export function StatementDocumentHeader({ overline = "Open Banking", title = "Account statement", institutionName, institutionLogo, companyLogo, companyName = "WealthX Pty Ltd", cdrLine = "Under CDR principal: ADRBNK000208", className, }: StatementDocumentHeaderProps) { return (
{overline && {overline}} {title} {institutionLogo && ( )} {institutionName || NO_DATA}
{companyLogo && ( {companyName} )} {companyName} {cdrLine && ( {cdrLine} )}
); } // --------------------------------------------------------------------------- // StatementHolderBlock // --------------------------------------------------------------------------- export interface StatementHolderBusinessFields { legalName?: string; agentName?: string; abn?: string; } export interface StatementHolderBlockProps { holderName?: string; /** Multi-line postal address. */ address?: string; /** Legal name / agent name / ABN rows for business accounts. */ business?: StatementHolderBusinessFields; className?: string; } /** Who the statement is addressed to. */ export function StatementHolderBlock({ holderName, address, business, className, }: StatementHolderBlockProps) { return (
{holderName || "Account holder"} {address ?.split("\n") .filter(Boolean) .map((line) => ( {line} ))} {business && (
)}
); } // --------------------------------------------------------------------------- // StatementPageFooter // --------------------------------------------------------------------------- export interface StatementPageFooterProps { /** Left-hand reference line, e.g. "Jordan Whitfield - Everyday Access 5678". */ reference?: string; /** ISO date the statement was generated. */ generatedOn?: string; /** Right-hand page label. */ pageLabel?: string; className?: string; } /** Ruled page footer - reference and generated-on left, page number right. */ export function StatementPageFooter({ reference, generatedOn, pageLabel = "Page 1 of 1", className, }: StatementPageFooterProps) { return (
{reference && {reference}} {generatedOn && Generated on: {formatDateShort(generatedOn)}}
{pageLabel}
); } // --------------------------------------------------------------------------- // StatementLink // --------------------------------------------------------------------------- export interface StatementLinkProps { href: string; children: React.ReactNode; className?: string; } /** * A link on printed paper: underlined in the document's own ink rather than a * theme colour, so it survives being printed in black and white while still * resolving when the PDF is read on screen. */ export function StatementLink({ href, children, className }: StatementLinkProps) { return ( {children} ); } // --------------------------------------------------------------------------- // StatementComplianceFooter // --------------------------------------------------------------------------- export interface StatementComplianceParty { name?: string; abn?: string; } export interface StatementComplianceInfo { /** Pre-formatted date and time the data was current, e.g. "01 Aug 2026, 09:12". */ dataCurrentAsAt?: string; consentProvidedBy?: string; dataHolder?: StatementComplianceParty; dataRecipient?: StatementComplianceParty; serviceProvider?: StatementComplianceParty; verificationReference?: string; /** Accreditation line, e.g. "Produced by ... CDR accreditation ADRBNK000208". */ producedByLine?: string; /** * Public page where a recipient checks the verification reference, printed * without a scheme so it stays readable on paper (e.g. "wealthx.au/verify"). */ verifyUrl?: string; /** * Scannable code for `verifyUrl` plus the reference. Supplied by the * consumer so the design system carries no QR dependency, matching the * `qrCodeNode` slot on `TwoFASetupForm`. */ verifyQrNode?: React.ReactNode; /** Issuer's website, printed without a scheme (e.g. "wealthx.au"). */ websiteUrl?: string; } export interface StatementComplianceFooterProps extends StatementComplianceInfo { className?: string; } function ComplianceCell({ label, children, }: { label: string; children: React.ReactNode; }) { return (
{label} {children}
); } const partyLines = (party?: StatementComplianceParty) => ( <> {party?.name || NO_DATA} {party?.abn && ( <>
ABN: {party.abn} )} ); /** The Consumer Data Right provenance block printed at the foot of a statement. */ export function StatementComplianceFooter({ dataCurrentAsAt, consentProvidedBy, dataHolder, dataRecipient, serviceProvider, verificationReference, producedByLine, verifyUrl, verifyQrNode, websiteUrl, className, }: StatementComplianceFooterProps) { return (
Consumer Data Right

This statement has been generated using verified data obtained via the Consumer Data Right, an Australian Government regulated scheme.

Data in this document is provided by {dataHolder?.name || NO_DATA}{" "} on behalf of {consentProvidedBy || NO_DATA}.

{producedByLine && (

{producedByLine}

)}
{dataCurrentAsAt || NO_DATA} {consentProvidedBy || NO_DATA} {partyLines(dataHolder)} {partyLines(dataRecipient)} {partyLines(serviceProvider)} {verificationReference || NO_DATA}
{verifyUrl && (
{verifyQrNode &&
{verifyQrNode}
}
Check this statement is genuine

Scan the code, or visit{" "} {verifyUrl} {" "} and enter the verification reference above. The check confirms who issued this document and that it has not been altered since. It does not show balances or transactions.

)}

This document has been generated using data provided under the terms of the Australian Consumer Data Right (CDR) by the specified financial institution. It may vary from their online banking portal or supplied documents in presentation, date range, sequence of transactions or specific data elements shown.

If you notice any errors or unauthorised transactions, please report them to your financial institution immediately.

{websiteUrl && ( {websiteUrl} )}
); } // --------------------------------------------------------------------------- // StatementField // --------------------------------------------------------------------------- export interface StatementFieldProps { label: string; /** Falsy values render as "Not available" so the row keeps its rhythm. */ value?: React.ReactNode; /** Emphasise the value - used for balances the reader is looking for. */ emphasis?: boolean; className?: string; } /** One label-left / value-right row inside a statement field group. */ export function StatementField({ label, value, emphasis = false, className, }: StatementFieldProps) { const isEmpty = value === undefined || value === null || value === ""; return (
{label} {isEmpty ? NO_DATA : value}
); } // --------------------------------------------------------------------------- // StatementFieldGroup // --------------------------------------------------------------------------- export interface StatementFieldGroupProps { /** Optional heading above the grid. */ title?: string; children: React.ReactNode; /** One column instead of two - used inside the two-column page sections. */ singleColumn?: boolean; className?: string; } /** Titled grid of `StatementField` rows. */ export function StatementFieldGroup({ title, children, singleColumn = false, className, }: StatementFieldGroupProps) { return (
{title && {title}}
{children}
); }