import React, { useState } from "react"; import { Button } from "./button"; import { Separator } from "./separator"; import { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from "./accordion"; import { PropertyCard, DebtCard, OtherLiabilityCard, AlertCard, type PropertyCardProps, type DebtCardProps, type OtherLiabilityCardProps, type AlertCardProps, type AlertActionType, type AlertSeverity, } from "./financial-cards"; import { FinancialCardHeader, FinancialDetailField, FinancialLineItem, FinancialSectionLabel, FinancialSubtotalFrame, FinancialSubtotalBlock, } from "./financial-primitives"; /** * Financial section components — WealthX DS (Level 4) * * Composed from Level 3 cards into full page sections. * Used inside the Summary Report Drawer and Opportunity Details Drawer. * * ``` * Level 2 → FinancialDetailField, FinancialLvrBar … (financial-primitives) * Level 3 → PropertyCard, DebtCard, AlertCard … (financial-cards) * Level 4 → PropertyHoldingsSection, DebtSection … ← here * Level 5 → SummaryReportDrawer, OpportunityDetailsDrawer … * ``` * * Component inventory: * FinancialViewSection — bordered card wrapping income + properties + current liabilities * PropertyHoldingsSection — grid of PropertyCards under a section header * DebtSection — grid of DebtCards under a section header * OtherLiabilitiesSection — grid of OtherLiabilityCards under a section header * IncomeExpenseSection — 3-column income / expenses / future-payments grid * AlertAccordion — collapsible accordion wrapping a grid of AlertCards * LoanScenarioSection — read-only Loan Quiz card (4-column grid) * FinancialBottomSummary — 3 stacked summary cards: cashflow / assets / net position */ // --------------------------------------------------------------------------- // PropertyHoldingsSection // --------------------------------------------------------------------------- export interface PropertyHoldingsSectionProps { /** Section heading. Defaults to "Property Holdings". */ title?: string; /** Each item maps 1:1 to a PropertyCard. */ items: PropertyCardProps[]; /** * When true renders columns without individual card borders, separated by a 1px divider. * Use inside `FinancialViewSection` to match the Figma Financial View card layout. */ borderless?: boolean; } /** Two-column grid of PropertyCards under a section header. */ export function PropertyHoldingsSection({ title = "Property Holdings", items, borderless = false, }: PropertyHoldingsSectionProps) { if (items.length === 0) return null; return (
{title} {borderless ? (
{items.map((item, i) => (
))}
) : (
{items.map((item, i) => ( ))}
)}
); } // --------------------------------------------------------------------------- // DebtSection // --------------------------------------------------------------------------- export interface DebtSectionProps { /** Section heading. Defaults to "Mortgages & Investment Loans". */ title?: string; /** Each item maps 1:1 to a DebtCard. */ items: DebtCardProps[]; } /** Two-column grid of DebtCards under a section header. */ export function DebtSection({ title = "Mortgages & Investment Loans", items, }: DebtSectionProps) { if (items.length === 0) return null; return (
{title}
{items.map((item, i) => ( ))}
); } // --------------------------------------------------------------------------- // OtherLiabilitiesSection // --------------------------------------------------------------------------- export interface OtherLiabilitiesSectionProps { /** Section heading. Defaults to "Other Liabilities". */ title?: string; /** Each item maps 1:1 to an OtherLiabilityCard. */ items: OtherLiabilityCardProps[]; } /** Two-column grid of OtherLiabilityCards under a section header. */ export function OtherLiabilitiesSection({ title = "Other Liabilities", items, }: OtherLiabilitiesSectionProps) { if (items.length === 0) return null; return (
{title}
{items.map((item, i) => ( ))}
); } // --------------------------------------------------------------------------- // IncomeExpenseSection // --------------------------------------------------------------------------- export interface SummaryLineItem { label: string; /** Formatted value string e.g. "$9,500". Renders "—" when falsy. */ value?: string | null; /** * When true renders the value in `text-destructive`. * Use for expenses, liabilities, or negative cashflow rows. */ destructive?: boolean; } export interface SummaryColumn { /** Column heading override. */ label?: string; items: SummaryLineItem[]; /** Formatted monthly average displayed in the tinted footer. */ monthlyAverage?: string; /** Formatted 12-month total displayed in the tinted footer. */ totalLast12Months?: string; } export interface IncomeExpenseSectionProps { income?: SummaryColumn; expenses?: SummaryColumn; futurePayments?: SummaryColumn; /** Override the "Future Payments" column heading. */ futurePaymentsHeading?: string; } /** * Three-column layout: income | expenses | future payments. * * Each column has a header, a scrollable list of `FinancialLineItem` rows, * and a brand-tinted `FinancialSubtotalFrame` footer. */ export function IncomeExpenseSection({ income, expenses, futurePayments, futurePaymentsHeading = "Future Payments", }: IncomeExpenseSectionProps) { return (
{/* Income */}
{income?.label ?? "Average Monthly Income"}
{(income?.items ?? []).map((item) => ( ))}
{/* Expenses */}
{expenses?.label ?? "Monthly Expenses and Commitments"}
{(expenses?.items ?? []).map((item) => ( ))}
{/* Future Payments */}
{futurePayments?.label ?? futurePaymentsHeading}
{(futurePayments?.items ?? []).map((item) => ( ))}
); } // --------------------------------------------------------------------------- // AlertAccordion // --------------------------------------------------------------------------- const SEVERITY_BG: Record = { NEED_ACTION: "bg-destructive text-destructive-foreground", WATCH: "bg-warning text-warning-foreground", INSIGHT: "bg-success text-success-foreground", }; export interface AlertAccordionSavePayload { alertId: string; action: AlertActionType; } export interface AlertAccordionProps { /** List of alerts — each maps 1:1 to an AlertCard. */ alerts: AlertCardProps[]; /** * Called when the user clicks Save. * Receives only alerts with a pending dismiss or snooze action selected. * When omitted the Save button is never shown. */ onSaveActions?: (actions: AlertAccordionSavePayload[]) => void; /** When `true` the Save button shows "Saving…" and is disabled. */ isSaving?: boolean; } /** * Collapsible accordion wrapping a 3-column grid of AlertCards. * * - The trigger header shows the section title + severity count badges. * - A Save button appears when the user has pending dismiss/snooze selections * and `onSaveActions` is provided. * - Returns `null` when `alerts` is empty. */ export function AlertAccordion({ alerts, onSaveActions, isSaving = false, }: AlertAccordionProps) { const [actionMap, setActionMap] = useState>( {}, ); const handleActionChange = (id: string) => (action: AlertActionType) => { setActionMap((prev) => ({ ...prev, [id]: action })); }; const handleSave = (e: React.MouseEvent) => { e.stopPropagation(); onSaveActions?.( Object.entries(actionMap).map(([alertId, action]) => ({ alertId, action, })), ); }; const countBySeverity = alerts.reduce>((acc, a) => { acc[a.severityCode] = (acc[a.severityCode] || 0) + 1; return acc; }, {}); const hasPendingActions = Object.keys(actionMap).length > 0; if (alerts.length === 0) return null; return (
Alerts {(["INSIGHT", "WATCH", "NEED_ACTION"] as const).map((code) => countBySeverity[code] ? ( {countBySeverity[code]} ) : null, )}
{hasPendingActions && onSaveActions && ( )}
{alerts.map((alert) => ( ))}
); } // --------------------------------------------------------------------------- // LoanScenarioSection // --------------------------------------------------------------------------- export interface LoanScenarioSectionProps { /** Override the section heading. Defaults to "Loan Scenario (Loan Quiz)". */ title?: string; /** "Lending Type" — e.g. "Home Loan", "Investment Loan" */ lendingType?: string; purposeOfLoan?: string; loanAmount?: string; propertyEstimate?: string; estLvr?: string; /** Cash or deposit available e.g. "$120,000" */ cashDeposit?: string; propertyAddress?: string; duration?: string; importantFeatures?: string; topThreePriorities?: string; } /** * Read-only Loan Quiz card — 4-column grid matching the LoanQuiz component in backoffice. * * Row 1: Lending Type | Purpose of Loan | Loan Amount | Property Estimate * Row 2: EST LVR | Cash/Deposit | Property Address | Duration * Row 3: Important Features | Top Three Priorities * * Mirrors `LoanQuiz.tsx` in backoffice OpportunityDetailsDrawer. */ export function LoanScenarioSection({ title = "Loan Scenario (Loan Quiz)", lendingType, purposeOfLoan, loanAmount, propertyEstimate, estLvr, cashDeposit, propertyAddress, duration, importantFeatures, topThreePriorities, }: LoanScenarioSectionProps) { return (
{title}
{/* Row 1 */} {/* Row 2 */} {/* Row 3 */}
); } // --------------------------------------------------------------------------- // FinancialBottomSummary // --------------------------------------------------------------------------- export interface BottomSummaryLineItem { label: string; value?: string | null; destructive?: boolean; } export interface FinancialBottomSummaryProps { /** Incoming vs Outgoing Summary card items. */ cashflowItems?: BottomSummaryLineItem[]; /** Net surplus/deficit value e.g. "$1,050". */ netSurplus?: string; /** Whether net surplus is negative (renders in destructive color). */ netSurplusDestructive?: boolean; /** Assets card items. */ assetItems?: BottomSummaryLineItem[]; /** Total assets value e.g. "$3,630,000". */ totalAssets?: string; /** Total liabilities value e.g. "$1,263,200". Renders in destructive. */ totalLiabilities?: string; /** Net position value e.g. "$2,366,800". */ netPosition?: string; /** Whether net position is negative. */ netPositionDestructive?: boolean; } /** * Three summary cards stacked vertically: * 1. Incoming vs Outgoing Summary — cashflow line items + net surplus * 2. Assets — asset line items + total * 3. Net Position — total liabilities + assets + net position * * Mirrors `financial-bottom-summary.tsx` in backoffice. */ export function FinancialBottomSummary({ cashflowItems = [], netSurplus, netSurplusDestructive = false, assetItems = [], totalAssets, totalLiabilities, netPosition, netPositionDestructive = false, }: FinancialBottomSummaryProps) { return (
{/* Card 1: Incoming vs Outgoing Summary */}
Incoming vs Outgoing Summary
{cashflowItems.map((item) => ( ))}
{/* Card 2: Assets */}
Assets
{assetItems.map((item) => ( ))}
{/* Card 3: Net Position */}
Net Position
); } // --------------------------------------------------------------------------- // FinancialViewSection // --------------------------------------------------------------------------- export interface FinancialViewSectionProps { /** Override the "Financial View" section label. */ title?: string; income?: IncomeExpenseSectionProps["income"]; expenses?: IncomeExpenseSectionProps["expenses"]; futurePayments?: IncomeExpenseSectionProps["futurePayments"]; futurePaymentsHeading?: string; /** Section heading for property columns. Defaults to "Property Assets and Liabilities". */ propertyTitle?: string; /** Each item renders as a borderless property column with a 1px divider. */ properties?: PropertyCardProps[]; /** Section heading for current liabilities. Defaults to "Current Liabilities". */ currentLiabilitiesTitle?: string; /** Each item maps 1:1 to a DebtCard. */ currentLiabilities?: DebtCardProps[]; } /** * Composite Financial View card — wraps income, property and current liabilities * inside a single bordered container, matching the Figma Financial View design. * * Property columns render without individual card borders (borderless), * separated by a 1px divider — same visual language as the income columns. * Current Liabilities (DebtCards) retain their individual borders. */ export function FinancialViewSection({ title = "Financial View", income, expenses, futurePayments, futurePaymentsHeading, propertyTitle = "Property Assets and Liabilities", properties = [], currentLiabilitiesTitle = "Current Liabilities", currentLiabilities = [], }: FinancialViewSectionProps) { return (
{title} {properties.length > 0 && ( <> )} {currentLiabilities.length > 0 && ( <> )}
); }