import * as React from "react"; import { Baby, Building2, Car, ChevronDown, CreditCard, Dumbbell, GraduationCap, HeartPulse, Landmark, Receipt, RefreshCw, Shield, ShoppingCart, Shirt, Tv, UtensilsCrossed, Zap, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { formatDateShort } from "@/lib/format-date"; import { Badge } from "./badge"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "./accordion"; import { RadioGroup, RadioGroupItem } from "./radio-group"; import { FinancialDetailField, FinancialLineItem, FinancialLvrBar, FinancialSubsectionTitle, FinancialSubtotalBlock, FinancialSubtotalFrame, } from "./financial-primitives"; import { Button } from "./button"; /** * Financial card molecules — WealthX DS (Level 3) * * Composed from financial primitives (Level 2) + shadcn atoms. * Used inside summary report drawers, opportunity detail panels, * and any financial data section in the backoffice. * * Component inventory: * PropertyCard — property holding with optional bank link + expandable loan detail * DebtCard — mortgage / investment-loan breakdown * OtherLiabilityCard — credit card or personal loan card (discriminated by `type`) * AlertCard — single alert item with severity indicator + dismiss/snooze actions */ // --------------------------------------------------------------------------- // PropertyCard // --------------------------------------------------------------------------- export interface PropertyCardProps { /** Street address or property name */ address: string; /** Badge label: "Owner Occupier", "Investment", etc. */ type?: string; /** Estimated property value e.g. "$1,200,000" */ estimated: string; /** * When true removes the outer card border. * Use inside `FinancialViewSection` where the card is already inside a bordered container. */ borderless?: boolean; /** Whether a bank mortgage account is linked to this property */ isLinkedToBank?: boolean; /** Outstanding loan amount — only shown when isLinkedToBank */ loanAmount?: string; /** Equity = estimated − loan — only shown when isLinkedToBank */ equity?: string; /** LVR label e.g. "56% — Good" — only shown when isLinkedToBank */ lvr?: string; /** LVR numeric percentage (drives color thresholds) */ lvrPercent?: number; // — Expandable loan detail (visible only when isLinkedToBank && expanded) — lenderName?: string; interestRate?: string; yearsRemaining?: string; minRepayments?: string; averageRepayments?: string; redrawAmount?: string; offsetAccount?: string; // — SubtotalFrame footer — totalMinRepayments?: string; totalExtraRepayments?: string; interestCharged?: string; principlePaidOff?: string; } export function PropertyCard({ address, type, estimated, isLinkedToBank = false, borderless = false, loanAmount, equity, lvr, lvrPercent = 0, lenderName, interestRate, yearsRemaining, minRepayments, averageRepayments, redrawAmount, offsetAccount, totalMinRepayments, totalExtraRepayments, interestCharged, principlePaidOff, }: PropertyCardProps) { const [expanded, setExpanded] = React.useState(false); return (
{/* Header row */} {isLinkedToBank ? ( ) : (
{address} {type && {type}}
)} {!isLinkedToBank && (

No mortgage account linked to this property

)} {/* Static fields — always visible */}
{isLinkedToBank && ( <>
)}
{isLinkedToBank && (
)} {/* Expandable loan detail — CSS grid transition for smooth open/close */} {isLinkedToBank && (
Loan
)}
); } // --------------------------------------------------------------------------- // DebtCard // --------------------------------------------------------------------------- export interface DebtCardProps { // — Header row — lenderName?: string; currentLoanAmount?: string; /** Current annual interest rate e.g. `"6.04% p.a."` */ interestRate?: string; // — Loan Stats section — originalLoanAmount?: string; /** Original term at drawdown e.g. `"30 years"` */ originalLoanTerm?: string; /** Minimum scheduled monthly repayment */ monthlyRepayments?: string; /** Available redraw balance */ redrawAmount?: string; /** Linked offset account balance */ offsetAmount?: string; /** Additional voluntary repayments per month */ extraLoanRepayments?: string; // — SubtotalFrame footer — /** Cumulative repayments made within the selected date range */ totalRepaymentsMade?: string; /** Cumulative interest paid within the selected date range */ totalInterestPaid?: string; /** Remaining loan term e.g. `"25 years"` */ yearsRemaining?: string; } export function DebtCard({ lenderName, currentLoanAmount, interestRate, originalLoanAmount, originalLoanTerm, monthlyRepayments, redrawAmount, offsetAmount, extraLoanRepayments, totalRepaymentsMade, totalInterestPaid, yearsRemaining, }: DebtCardProps) { return (
{/* Header row — fixed min-h so two cards in a grid row have aligned headers */}
{/* Loan Stats section */}
Loan Stats
{/* Footer */}
); } // --------------------------------------------------------------------------- // OtherLiabilityCard // --------------------------------------------------------------------------- export type OtherLiabilityType = "credit_card" | "personal_loan"; export interface OtherLiabilityCardProps { /** * Discriminates the field set and footer layout: * - `"credit_card"` → Card Stats (Credit Limit, Min Payment, Annual Fee) * - `"personal_loan"` → Loan Stats (Original Amount, Term, Monthly Repayments) */ type: OtherLiabilityType; lenderName?: string; /** Annual interest rate e.g. `"19.99% p.a."` */ interestRate?: string; // ── Credit card fields ── /** Current outstanding balance on the card */ currentBalance?: string; /** Approved credit limit */ creditLimit?: string; /** Minimum required monthly payment */ minMonthlyPayment?: string; annualFee?: string; /** Pre-computed available credit: `creditLimit − currentBalance` */ availableCredit?: string; // ── Personal loan fields ── /** Current outstanding loan balance */ currentLoanAmount?: string; originalLoanAmount?: string; /** Original term at drawdown e.g. `"5 years"` */ originalLoanTerm?: string; /** Minimum scheduled monthly repayment */ monthlyRepayments?: string; /** Remaining term e.g. `"2.5 years"` */ currentLoanTermLeft?: string; // ── Shared footer ── /** Cumulative repayments made within the selected date range */ totalRepaymentsMade?: string; /** Cumulative interest paid within the selected date range */ totalInterestPaid?: string; } export function OtherLiabilityCard({ type, lenderName, interestRate, currentBalance, creditLimit, minMonthlyPayment, annualFee, availableCredit, currentLoanAmount, originalLoanAmount, originalLoanTerm, monthlyRepayments, currentLoanTermLeft, totalRepaymentsMade, totalInterestPaid, }: OtherLiabilityCardProps) { const isCreditCard = type === "credit_card"; return (
{/* Header row — fixed min-h so two cards in a grid row have aligned headers */}
{isCreditCard ? ( ) : ( )}
{/* Stats section */}
{isCreditCard ? "Card Stats" : "Loan Stats"} {isCreditCard ? (
) : (
)}
{/* Footer */} {isCreditCard ? (
) : (
)}
); } // --------------------------------------------------------------------------- // AlertCard // --------------------------------------------------------------------------- export type AlertSeverity = "NEED_ACTION" | "WATCH" | "INSIGHT"; export type AlertActionType = "DISMISS" | "SNOOZE"; const SEVERITY_CLASSES: Record = { NEED_ACTION: { dot: "bg-destructive", border: "border-destructive" }, WATCH: { dot: "bg-warning", border: "border-warning" }, INSIGHT: { dot: "bg-success", border: "border-success" }, }; export interface AlertCardProps { id: string; name: string; severityCode: AlertSeverity; /** Whether this alert has an active ignore period */ ignored?: boolean; /** Human-readable date string e.g. "31/12/2025" */ ignoredUntil?: string; /** Currently selected action, controlled externally */ selectedAction?: AlertActionType | null; onActionChange?: (action: AlertActionType) => void; } export function AlertCard({ id, name, severityCode, ignored = false, ignoredUntil, selectedAction = null, onActionChange, }: AlertCardProps) { const { dot, border } = SEVERITY_CLASSES[severityCode]; return (
{/* Alert name row */}
{name}
{ignored ? ( Ignored until {ignoredUntil} ) : ( onActionChange?.(val as AlertActionType)} className="ml-[15px] flex flex-col gap-1" >
)}
); } // --------------------------------------------------------------------------- // AboutCard // --------------------------------------------------------------------------- export interface AboutCardProps { title?: string; firstName?: string; lastName?: string; phone?: string; email?: string; dob?: string; gender?: string; maritalStatus?: string; numDependants?: string; /** Individual dependant records — shows DOB + age for each. */ dependants?: Array<{ dob: string }>; citizenStatus?: string; residentialAddress?: string; residentialStatus?: string; timeAtAddressYears?: string; timeAtAddressMonths?: string; driversLicence?: string; passport?: string; propertyInTrust?: string; companyOwnership?: string; /** Remove the outer card border. Use when the card is already inside a bordered container. */ borderless?: boolean; } function ageFromDob(dobStr: string): number | null { if (!dobStr) return null; const dob = new Date(dobStr); if (isNaN(dob.getTime())) return null; const today = new Date(); let age = today.getFullYear() - dob.getFullYear(); const m = today.getMonth() - dob.getMonth(); if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--; return age >= 0 ? age : null; } /** * Display card for applicant personal / contact information. * Used inside the Summary tab applicant sub-tabs. */ export function AboutCard({ title, firstName, lastName, phone, email, dob, gender, maritalStatus, numDependants, dependants, citizenStatus, residentialAddress, residentialStatus, timeAtAddressYears, timeAtAddressMonths, driversLicence, passport, propertyInTrust, companyOwnership, borderless = false, }: AboutCardProps) { const fullName = [title, firstName, lastName].filter(Boolean).join(" ") || "—"; const timeAtAddress = timeAtAddressYears || timeAtAddressMonths ? [ timeAtAddressYears && `${timeAtAddressYears}yr`, timeAtAddressMonths && `${timeAtAddressMonths}mo`, ] .filter(Boolean) .join(" ") : undefined; return (
{/* Identity — personal details + citizenship documents */}
{/* Contact */}
{/* Family — marital status + dependants as a dedicated section */}
{dependants && dependants.map((dep, i) => { const age = ageFromDob(dep.dob); const dobFormatted = formatDateShort(dep.dob) || "—"; const value = age !== null ? `${dobFormatted} · age ${age}` : dobFormatted; return ( ); })}
{/* Residence */}
{timeAtAddress && ( )}
{/* Ownership */}
); } // --------------------------------------------------------------------------- // IncomeCard // --------------------------------------------------------------------------- export interface IncomeCardItem { incomeType: string; jobTitle?: string; companyName?: string; companyAddress?: string; startDate?: string; stillInPosition?: boolean; endDate?: string; companyType?: string; /** Pre-formatted amount + frequency e.g. "$9,500 / Monthly" */ amountLabel: string; } export interface IncomeCardProps { items: IncomeCardItem[]; /** Pre-formatted total monthly income e.g. "$12,300" */ totalMonthly?: string; /** Remove the outer card border. Use when the card is already inside a bordered container. */ borderless?: boolean; } /** * Display card for applicant income items. * Each income source is an accordion row: collapsed = type + amount, * expanded = full employment details. */ export function IncomeCard({ items, totalMonthly, borderless = false, }: IncomeCardProps) { if (items.length === 0) { return (

No income recorded.

); } return (
{items.map((item, i) => (
{item.incomeType} {item.jobTitle && ( · {item.jobTitle} )}
{item.amountLabel}
{item.companyAddress && ( )}
))}
{totalMonthly && ( )}
); } // --------------------------------------------------------------------------- // ExpensesCard // --------------------------------------------------------------------------- const EXPENSE_ICON_MAP: Record = { groceries: ShoppingCart, "dining out": UtensilsCrossed, dining: UtensilsCrossed, restaurants: UtensilsCrossed, transport: Car, transportation: Car, vehicle: Car, utilities: Zap, electricity: Zap, insurance: Shield, "council rates": Landmark, council: Landmark, rates: Landmark, medical: HeartPulse, health: HeartPulse, subscriptions: RefreshCw, subscription: RefreshCw, "credit card": CreditCard, education: GraduationCap, childcare: Baby, entertainment: Tv, gym: Dumbbell, fitness: Dumbbell, clothing: Shirt, rent: Building2, }; function getExpenseIcon(expenseType: string): LucideIcon { return EXPENSE_ICON_MAP[expenseType.toLowerCase()] ?? Receipt; } export interface ExpensesCardItem { expenseType: string; /** Pre-formatted amount + frequency e.g. "$800 / Monthly" */ amountLabel: string; } export interface ExpensesCardProps { items: ExpensesCardItem[]; /** Pre-formatted total monthly expenses e.g. "$2,100" */ totalMonthly?: string; /** Remove the outer card border. Use when the card is already inside a bordered container. */ borderless?: boolean; } /** * Display card for applicant expense items. * Each expense renders as an icon-labelled row with a destructive amount value. */ export function ExpensesCard({ items, totalMonthly, borderless = false, }: ExpensesCardProps) { if (items.length === 0) { return (

No expenses recorded.

); } return (
{items.map((item, i) => { const Icon = getExpenseIcon(item.expenseType); return (
{item.expenseType}
{item.amountLabel}
); })}
{totalMonthly && ( )}
); }