import * as React from "react"; import { X } from "lucide-react"; import { Sheet, SheetContent } from "./sheet"; import { Button } from "./button"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs"; import { Spinner } from "./spinner"; /** * Financial drawer shells — WealthX DS (Level 5) * * These are the outermost drawer containers. They compose the Level 4 sections * into full-screen right-side panels used across backoffice. * * ``` * Level 2 → FinancialDetailField, FinancialLvrBar … (financial-primitives) * Level 3 → PropertyCard, DebtCard, AlertCard … (financial-cards) * Level 4 → PropertyHoldingsSection, DebtSection … (financial-sections) * Level 5 → SummaryReportDrawer, OpportunityDetailsDrawer … ← here * ``` * * Both components are **visual shells** — they own the chrome (header, tabs, * scrollable viewport) but delegate all data and content to the consuming app * via slot props. */ // --------------------------------------------------------------------------- // SummaryReportDrawer // --------------------------------------------------------------------------- export type SummaryViewMode = "individual" | "joint"; export type SummaryJointSubTab = "joint" | "userA" | "userB"; export interface SummaryReportDrawerProps { open: boolean; onOpenChange: (open: boolean) => void; /** Primary contact name shown in the header. */ contactName?: string; /** Whether a secondary contact has been added for joint view. */ hasJointView?: boolean; /** Active view mode — individual or joint. */ viewMode?: SummaryViewMode; onViewModeChange?: (mode: SummaryViewMode) => void; /** Active sub-tab when in joint + joint mode. */ jointSubTab?: SummaryJointSubTab; onJointSubTabChange?: (tab: SummaryJointSubTab) => void; /** Display name for the primary applicant in joint view. */ jointMainUserName?: string; /** Display name for the secondary applicant in joint view. */ jointCoApplicantName?: string; /** Called when "Create Joint View" is clicked (no joint view yet). */ onCreateJointView?: () => void; /** Called when "Remove Joint View" is clicked. */ onRemoveJointView?: () => void; /** * Row of action buttons rendered below the header chrome. * Typically: Export button, Send Report button, etc. */ actionButtons?: React.ReactNode; /** * Alert accordion shown below the action buttons row. * Pass an `` from financial-sections when alerts exist. */ alerts?: React.ReactNode; /** * When true, hides `children` and `actionButtons`/`alerts` and shows a * centered `` instead. */ isLoading?: boolean; /** * The scrollable financial content — stack Level 4 sections and charts here. * Recommended order (mirrors backoffice): charts → PropertyHoldings → Debt → * OtherLiabilities → IncomeExpense → AlertAccordion. */ children?: React.ReactNode; } /** * Right-side sheet shell for the Summary Report Drawer. * * Handles the header chrome (contact name, Individual/Joint view toggle, * joint sub-tabs, close button) and a scrollable content viewport. * All financial content is passed as `children`. */ export function SummaryReportDrawer({ open, onOpenChange, contactName = "Contact", hasJointView = false, viewMode = "individual", onViewModeChange, jointSubTab = "joint", onJointSubTabChange, jointMainUserName = "User A", jointCoApplicantName = "User B", onCreateJointView, onRemoveJointView, actionButtons, alerts, isLoading = false, children, }: SummaryReportDrawerProps) { return (
{/* ── Header chrome ── */}
{/* Top row: view mode toggle + close */}
{hasJointView ? ( <>
) : ( <> {contactName} View )}
{/* Joint sub-tabs — default (pill) variant, only in joint+joint mode */} {hasJointView && viewMode === "joint" && ( onJointSubTabChange?.(v as SummaryJointSubTab) } > Joint View {jointMainUserName} View {jointCoApplicantName} View )}
{/* Action buttons + alerts — hidden while loading */} {!isLoading && actionButtons &&
{actionButtons}
} {!isLoading && alerts &&
{alerts}
}
{/* ── Scrollable content viewport ── */}
{isLoading ? (
) : ( children )}
); } // --------------------------------------------------------------------------- // OpportunityDetailsDrawer // --------------------------------------------------------------------------- export type OpportunityTab = | "summary" | "tasks" | "bankStatement" | "policyAI" | "runServicing" | "emailAndNotes" | "syncLoanApp"; const OPPORTUNITY_TABS: { value: OpportunityTab; label: string }[] = [ { value: "summary", label: "Summary" }, { value: "tasks", label: "Tasks" }, { value: "bankStatement", label: "Reports & Statements" }, { value: "policyAI", label: "Policy AI" }, { value: "runServicing", label: "Run Servicing" }, { value: "emailAndNotes", label: "Email & Notes" }, { value: "syncLoanApp", label: "Sync Loan App" }, ]; export interface OpportunityDetailsDrawerProps { open: boolean; onOpenChange: (open: boolean) => void; /** Default active tab on open. Defaults to "summary". */ defaultTab?: OpportunityTab; /** * Content slots keyed by tab value. * Pass only the tabs you want to render content for — unset tabs show nothing. * * ```tsx * , * tasks: , * }} * /> * ``` */ tabs?: Partial>; } /** * Right-side sheet shell for the Opportunity Details Drawer. * * Renders a line-variant tab bar (Summary · Tasks · Reports & Statements · * Policy AI · Run Servicing · Email & Notes · Sync Loan App) with a close * button, and a scrollable `TabsContent` panel for each active tab. * * Uses the shadcn `Tabs` component internally so each tab's content panel * is properly connected to its trigger via Base UI's Tabs.Root context. */ export function OpportunityDetailsDrawer({ open, onOpenChange, defaultTab = "summary", tabs, }: OpportunityDetailsDrawerProps) { return ( {/* * Tabs.Root wraps both the header and content so that TabsContent panels * are in scope and automatically shown/hidden by Base UI's tab context. * gap-0 overrides the default gap-2 on Tabs. */} {/* * Tab header: no pt-3 so the header height hugs the tab section. * TabsList gets group-data-[orientation=horizontal]/tabs:h-12 to * override the built-in conditional h-9 at equal specificity via * cascade order. Triggers use flex-none to hug their label width * (overrides the built-in flex-1 that was stretching them equally). * No overflow-x-auto wrapper — that forces overflow-y:auto (CSS spec) * and clips the line-variant after:bottom-[-5px] indicator. */}
{OPPORTUNITY_TABS.map((tab) => ( {tab.label} ))} {/* Close button */}
{/* ── Tab content panels ── */}
{OPPORTUNITY_TABS.map((tab) => ( {tabs?.[tab.value]} ))}
); }