import * as React from "react"; import { ChevronDownIcon, Plus, Trash2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { PROPERTY_ASSET_TYPES } from "@/lib/opportunity-constants"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Accordion, AccordionItem, AccordionContent, } from "@/components/ui/accordion"; import { Checkbox } from "@/components/ui/checkbox"; import { DatePicker } from "@/components/ui/date-picker"; import { Slider } from "@/components/ui/slider"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { AddressAutocomplete, CurrencyInputWithSlider, OwnershipSplit, } from "@/components/ui/form-primitives"; import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; /** * Opportunity Edit Modals — WealthX DS (L4) * * Six focused edit dialogs for the Opportunity Details Drawer Summary tab: * - EditLoanScenarioModal — edit loan quiz fields * - EditAssetsModal — add/edit/remove asset line items * - EditDebtsModal — add/edit/remove debt line items * - EditAboutApplicantModal — personal details (name, gender, phone, email…) * - EditIncomeModal — income fields per applicant * - EditExpensesModal — expense breakdown per applicant * * All modals are fully controlled. Each accepts initial data and fires * `onSave` with the updated payload when confirmed. */ // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const LOAN_PURPOSES = [ "Purchase a home", "Refinance my home loan", "Buy an investment property", "Refinance investment loan", "Renovate/build", "Equity release", ]; const ASSET_TYPES = [ "Primary Residence", "Investment Property", "Holiday Home", "Commercial Property", "Rural Property", "Cash & Savings", "Term Deposit", "Shares / ETFs", "Managed Funds", "Superannuation", "Motor Vehicle", "Business Equity", "Cryptocurrency", "Personal Belongings", "Life Insurance", "Trust Assets", "SMSF", "Bonds / Fixed Income", "Other", ]; const PROPERTY_SUBTYPES = [ "House", "Unit / Apartment", "Townhouse", "Land", "Rural / Farm", "Commercial", ]; const PROPERTY_USED_AS = ["Owner Occupied", "Investment", "Vacant Land"]; const FINANCIAL_ACCOUNT_ASSET_TYPES = new Set([ "Cash & Savings", "Term Deposit", ]); const INVESTMENT_ASSET_TYPES = new Set([ "Shares / ETFs", "Managed Funds", "Bonds / Fixed Income", "Cryptocurrency", ]); const SUPER_ASSET_TYPES = new Set(["Superannuation", "SMSF"]); // Fields shown per asset type category function assetFields(type: string) { return { isProperty: PROPERTY_ASSET_TYPES.has(type), isVehicle: type === "Motor Vehicle", isFinancialAccount: FINANCIAL_ACCOUNT_ASSET_TYPES.has(type), isInvestment: INVESTMENT_ASSET_TYPES.has(type), isSuper: SUPER_ASSET_TYPES.has(type), isBusiness: type === "Business Equity", isInsurance: type === "Life Insurance", }; } const PROPERTY_LOAN_DEBT_TYPES = new Set([ "Home Loan (Owner Occupied)", "Home Loan (Investment)", "Construction Loan", ]); const GENERAL_LOAN_DEBT_TYPES = new Set([ "Personal Loan", "Business Loan", "Overdraft", "Line of Credit", "Guarantor Liability", ]); const CARD_DEBT_TYPES = new Set([ "Credit Card", "Store Card", "Buy Now Pay Later", ]); const VEHICLE_DEBT_TYPES = new Set(["Car Loan", "Vehicle Lease"]); function debtFields(type: string) { return { isPropertyLoan: PROPERTY_LOAN_DEBT_TYPES.has(type), isGeneralLoan: GENERAL_LOAN_DEBT_TYPES.has(type), isCard: CARD_DEBT_TYPES.has(type), isVehicle: VEHICLE_DEBT_TYPES.has(type), isHecs: type === "HECS / Student Debt", isTax: type === "Tax Debt", isGuarantor: type === "Guarantor Liability", }; } const DEBT_TYPES = [ "Home Loan (Owner Occupied)", "Home Loan (Investment)", "Construction Loan", "Personal Loan", "Car Loan", "Credit Card", "Store Card", "HECS / Student Debt", "Business Loan", "Overdraft", "Tax Debt", "Buy Now Pay Later", "Vehicle Lease", "Guarantor Liability", "Line of Credit", "Other", ]; const TITLE_OPTIONS = ["Mr", "Mrs", "Ms", "Dr", "Prof"]; const GENDER_OPTIONS = ["Male", "Female", "Non-binary", "Prefer not to say"]; const MARITAL_OPTIONS = [ "Single", "Married", "De facto", "Divorced", "Widowed", ]; const CITIZEN_OPTIONS = [ "Australian Citizen", "Permanent Resident", "Temporary Resident", "Foreign National", ]; const RESIDENTIAL_STATUS_OPTIONS = [ "Own", "Renting", "Boarding", "With Parents", "Other", ]; const PROPERTY_IN_TRUST_OPTIONS = ["None", "Yes - Discretionary", "Yes - Unit"]; const COMPANY_OWNERSHIP_OPTIONS = [ "None", "Yes - Director", "Yes - Shareholder", ]; const INCOME_TYPES = [ "PAYG", "Self-employed", "Contractor", "Casual", "Commission", "Government Benefits", "Rental", "Other", ]; const EXPENSE_TYPES = [ "Groceries", "Dining Out", "Transport", "Fuel", "Utilities", "Phone Plan", "Internet", "Medical", "Entertainment", "Clothing", "Gym & Fitness", "Subscriptions", "Childcare", "Education", "Pet Care", "Insurance", "Council Rates", "Body Corp / Strata", "Home Maintenance", "Travel & Holidays", "Donations", "Other", ]; const PRIORITIES = [ "Maximize Borrow Amount", "Cheapest Interest Rate", "The Best Features", "Major Lender", "Small Lender", "Regional Lender", "Non-Bank Lender", "Branch Network", "Good Customer Service", "Environmentally Friendly Lender", ]; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- // --- Loan Scenario --- export interface LoanScenarioFormData { loanPurpose: string; propertyEstimate: number; cashEquity: number; loanAmount: number; loanDuration: number; knowsFeatures: string; featureFixedRate: boolean; featureVariableRate: boolean; featureSplitLoan: boolean; featureInterestOnly: boolean; featureRedrawFacility: boolean; feature100Offset: boolean; priorities: string[]; borrowOtherThanProperty: string; concernedAboutRates: string; considerFixedRate: string; retirementAge: string; anticipatedChanges: string; } // --- Assets --- export interface AssetLineItem { id: string; assetType: string; value: number; mainShare: number; coShare: number; // Property address?: string; propertySubtype?: string; usedAs?: string; // Vehicle make?: string; model?: string; year?: string; // Financial accounts (Cash, Term Deposit) + Investments + Crypto institution?: string; // Superannuation / SMSF fundName?: string; memberNumber?: string; // Business Equity businessName?: string; abn?: string; // Life Insurance insurer?: string; policyNumber?: string; } // --- Debts --- export interface DebtLineItem { id: string; debtType: string; amountOwing: number; originalLoanAmount: number; repaymentAmount: number; repaymentFrequency: "Monthly" | "Weekly"; interestRate: string; details: string; mainShare: number; coShare: number; // Property loans lender?: string; accountNumber?: string; propertyAddress?: string; // Vehicle loans/leases vehicleMake?: string; vehicleModel?: string; vehicleYear?: string; // Cards / BNPL creditLimit?: number; // HECS institution?: string; // Tax debt taxYear?: string; referenceNumber?: string; // Guarantor liability beneficiary?: string; } // --- About --- export interface DependantInfo { dob: string; } export interface AboutApplicantFormData { // Name title: string; firstName: string; lastName: string; // Contact phone: string; email: string; // Personal dob: string; gender: string; maritalStatus: string; numDependants: string; dependants: DependantInfo[]; // Residency & ID citizenStatus: string; residentialAddress: string; residentialStatus: string; timeAtAddressYears: string; timeAtAddressMonths: string; previousAddress: string; driversLicence: string; passport: string; // Financial structure propertyInTrust: string; companyOwnership: string; } // --- Income (array-based) --- export interface IncomeItem { id: string; incomeType: string; jobTitle: string; startDate: string; stillInPosition: boolean; endDate: string; companyName: string; companyAddress: string; incomeAmount: number; frequency: "Monthly" | "Weekly"; companyType: "Public" | "Private" | ""; } export interface IncomeFormData { items: IncomeItem[]; } // --- Expenses (array-based) --- export interface ExpenseItem { id: string; expenseType: string; amount: number; frequency: "Monthly" | "Weekly"; mainShare: number; coShare: number; } export interface ExpensesFormData { items: ExpenseItem[]; } // --------------------------------------------------------------------------- // Prop interfaces // --------------------------------------------------------------------------- export interface EditLoanScenarioModalProps { open: boolean; onOpenChange: (open: boolean) => void; initialData?: Partial; onSave: (data: LoanScenarioFormData) => void; mainApplicantName?: string; coApplicantName?: string; /** Scope the dialog portal into this element (e.g. a Sheet drawer) instead of document.body. */ container?: HTMLElement | null; className?: string; } export interface EditAssetsModalProps { open: boolean; onOpenChange: (open: boolean) => void; initialItems?: AssetLineItem[]; onSave: (items: AssetLineItem[]) => void; mainApplicantName?: string; coApplicantName?: string; container?: HTMLElement | null; className?: string; } export interface EditDebtsModalProps { open: boolean; onOpenChange: (open: boolean) => void; initialItems?: DebtLineItem[]; onSave: (items: DebtLineItem[]) => void; mainApplicantName?: string; coApplicantName?: string; container?: HTMLElement | null; className?: string; } export interface EditAboutApplicantModalProps { open: boolean; onOpenChange: (open: boolean) => void; applicantLabel?: string; initialData?: Partial; onSave: (data: AboutApplicantFormData) => void; container?: HTMLElement | null; className?: string; } export interface EditIncomeModalProps { open: boolean; onOpenChange: (open: boolean) => void; applicantLabel?: string; initialData?: IncomeFormData; onSave: (data: IncomeFormData) => void; container?: HTMLElement | null; className?: string; } export interface EditExpensesModalProps { open: boolean; onOpenChange: (open: boolean) => void; applicantLabel?: string; initialData?: ExpensesFormData; onSave: (data: ExpensesFormData) => void; container?: HTMLElement | null; className?: string; } // --------------------------------------------------------------------------- // Shared internal helpers // --------------------------------------------------------------------------- function FormField({ label, children, className, }: { label: string; children: React.ReactNode; className?: string; }) { return (
{children}
); } function ModalScroll({ children }: { children: React.ReactNode }) { return (
{children}
); } function FrequencyToggle({ value, onValueChange, }: { value: "Monthly" | "Weekly"; onValueChange: (val: "Monthly" | "Weekly") => void; }) { return ( { const val = vals[0]; if (val === "Monthly" || val === "Weekly") { onValueChange(val); } }} > Monthly Weekly ); } function AccordionItemHeader({ label, onRemove, removeLabel = "Remove item", }: { label: string; onRemove: () => void; removeLabel?: string; }) { return ( svg]:rotate-180", )} > {label} ); } // --------------------------------------------------------------------------- // Defaults // --------------------------------------------------------------------------- const LOAN_SCENARIO_DEFAULTS: LoanScenarioFormData = { loanPurpose: "", propertyEstimate: 0, cashEquity: 0, loanAmount: 0, loanDuration: 30, knowsFeatures: "", featureFixedRate: false, featureVariableRate: false, featureSplitLoan: false, featureInterestOnly: false, featureRedrawFacility: false, feature100Offset: false, priorities: [], borrowOtherThanProperty: "", concernedAboutRates: "", considerFixedRate: "", retirementAge: "", anticipatedChanges: "", }; const ABOUT_APPLICANT_DEFAULTS: AboutApplicantFormData = { title: "", firstName: "", lastName: "", phone: "", email: "", dob: "", gender: "", maritalStatus: "", numDependants: "", dependants: [], citizenStatus: "", residentialAddress: "", residentialStatus: "", timeAtAddressYears: "", timeAtAddressMonths: "", previousAddress: "", driversLicence: "", passport: "", propertyInTrust: "", companyOwnership: "", }; function makeDefaultIncomeItem(): IncomeItem { return { id: `income-${Date.now()}-${Math.random()}`, incomeType: "", jobTitle: "", startDate: "", stillInPosition: true, endDate: "", companyName: "", companyAddress: "", incomeAmount: 0, frequency: "Monthly", companyType: "", }; } function makeDefaultAssetItem(): AssetLineItem { return { id: `asset-${Date.now()}-${Math.random()}`, assetType: "", value: 0, mainShare: 100, coShare: 0, }; } function makeDefaultDebtItem(): DebtLineItem { return { id: `debt-${Date.now()}-${Math.random()}`, debtType: "", amountOwing: 0, originalLoanAmount: 0, repaymentAmount: 0, repaymentFrequency: "Monthly", interestRate: "", details: "", mainShare: 100, coShare: 0, }; } function makeDefaultExpenseItem(): ExpenseItem { return { id: `expense-${Date.now()}-${Math.random()}`, expenseType: "", amount: 0, frequency: "Monthly", mainShare: 100, coShare: 0, }; } // --------------------------------------------------------------------------- // EditLoanScenarioModal // --------------------------------------------------------------------------- export function EditLoanScenarioModal({ open, onOpenChange, initialData, onSave, container, className, }: EditLoanScenarioModalProps) { const [form, setForm] = React.useState({ ...LOAN_SCENARIO_DEFAULTS, ...initialData, }); const initialSnapshot = React.useRef(""); React.useEffect(() => { if (open) { const data = { ...LOAN_SCENARIO_DEFAULTS, ...initialData }; setForm(data); initialSnapshot.current = JSON.stringify(data); } }, [open]); // eslint-disable-line react-hooks/exhaustive-deps const isDirty = JSON.stringify(form) !== initialSnapshot.current; const set = ( key: K, val: LoanScenarioFormData[K], ) => setForm((prev) => ({ ...prev, [key]: val })); const togglePriority = (priority: string) => { setForm((prev) => { const exists = prev.priorities.includes(priority); if (exists) { return { ...prev, priorities: prev.priorities.filter((p) => p !== priority), }; } if (prev.priorities.length >= 3) return prev; return { ...prev, priorities: [...prev.priorities, priority] }; }); }; const prioritiesAtMax = form.priorities.length >= 3; const LOAN_FEATURES: { key: keyof LoanScenarioFormData; label: string }[] = [ { key: "featureFixedRate", label: "Fixed Rate" }, { key: "featureVariableRate", label: "Variable Rate" }, { key: "featureSplitLoan", label: "Split Loan" }, { key: "featureInterestOnly", label: "Interest Only" }, { key: "featureRedrawFacility", label: "Redraw Facility" }, { key: "feature100Offset", label: "100% Offset" }, ]; const YES_NO_FIELDS: { key: keyof LoanScenarioFormData; label: string }[] = [ { key: "borrowOtherThanProperty", label: "Do you intend to borrow money other than your property?", }, { key: "concernedAboutRates", label: "Are you concerned about rising interest rates?", }, { key: "considerFixedRate", label: "Would you consider taking a fixed rate?", }, { key: "anticipatedChanges", label: "Anticipated changes impacting repayment?", }, ]; return ( Edit Loan Scenario {/* 1. Loan purpose */} {/* 2. Two-column: property estimate | cash equity */}
set("propertyEstimate", val)} /> set("cashEquity", val)} />
{/* 3. Two-column: loan amount | loan duration */}
set("loanAmount", val)} />
{ const parsed = parseInt(e.target.value, 10); if (!isNaN(parsed)) { set("loanDuration", Math.min(40, Math.max(1, parsed))); } }} className="flex-1" /> years
set("loanDuration", val)} />
{/* 4. Do you know what loan features you want? */} {/* 5. Features grid — only when knowsFeatures == "Yes" */} {form.knowsFeatures === "Yes" && (
{LOAN_FEATURES.map(({ key, label }) => (
set(key, checked === true)} />
))}
)} {/* 6. Top 3 priorities */}
{PRIORITIES.map((priority) => { const checked = form.priorities.includes(priority); const disabled = prioritiesAtMax && !checked; return (
togglePriority(priority)} />
); })}
{/* 7. Yes/No radio sections */} {YES_NO_FIELDS.map(({ key, label }) => (
{ if (val === "Yes" || val === "No") set(key, val); }} className="flex gap-4" > {(["Yes", "No"] as const).map((opt) => ( ))}
))} {/* 8. Retirement age */} set("retirementAge", e.target.value)} placeholder="e.g. 65" />
); } // --------------------------------------------------------------------------- // EditAssetsModal // --------------------------------------------------------------------------- export function EditAssetsModal({ open, onOpenChange, initialItems = [], onSave, mainApplicantName = "Main Applicant", coApplicantName = "Co-Applicant", container, className, }: EditAssetsModalProps) { const [items, setItems] = React.useState( initialItems.length > 0 ? initialItems : [makeDefaultAssetItem()], ); const initialSnapshot = React.useRef(""); React.useEffect(() => { if (open) { const data = initialItems.length > 0 ? initialItems : [makeDefaultAssetItem()]; setItems(data); initialSnapshot.current = JSON.stringify(data); } }, [open]); // eslint-disable-line react-hooks/exhaustive-deps const isDirty = JSON.stringify(items) !== initialSnapshot.current; const updateItem = ( id: string, key: K, val: AssetLineItem[K], ) => setItems((prev) => prev.map((item) => (item.id === id ? { ...item, [key]: val } : item)), ); const removeItem = (id: string) => setItems((prev) => prev.filter((item) => item.id !== id)); const addItem = () => setItems((prev) => [...prev, makeDefaultAssetItem()]); const defaultOpenItems = items.length > 0 ? [items[0].id] : []; return ( Edit Assets {items.map((item, index) => ( {/* Custom header: trigger + delete button as siblings */} removeItem(item.id)} removeLabel="Remove asset" />
updateItem(item.id, "value", val) } /> {/* Conditional fields based on asset type */} {(() => { const f = assetFields(item.assetType); return ( <> {f.isProperty && ( <> updateItem(item.id, "address", val) } onSelect={(opt) => updateItem(item.id, "address", opt.label) } />
)} {f.isVehicle && (
updateItem(item.id, "make", e.target.value) } placeholder="e.g. Toyota" /> updateItem(item.id, "model", e.target.value) } placeholder="e.g. Camry" /> updateItem(item.id, "year", e.target.value) } placeholder="e.g. 2022" />
)} {(f.isFinancialAccount || f.isInvestment) && ( updateItem( item.id, "institution", e.target.value, ) } placeholder="e.g. CommBank, Vanguard" /> )} {f.isSuper && (
updateItem( item.id, "fundName", e.target.value, ) } placeholder="e.g. Australian Super" /> updateItem( item.id, "memberNumber", e.target.value, ) } placeholder="e.g. 1234567" />
)} {f.isBusiness && (
updateItem( item.id, "businessName", e.target.value, ) } placeholder="e.g. Acme Pty Ltd" /> updateItem(item.id, "abn", e.target.value) } placeholder="e.g. 12 345 678 901" />
)} {f.isInsurance && (
updateItem( item.id, "insurer", e.target.value, ) } placeholder="e.g. TAL, AIA" /> updateItem( item.id, "policyNumber", e.target.value, ) } placeholder="e.g. POL-123456" />
)} ); })()} { updateItem( item.id, "mainShare", owners.find((o) => o.id === "main")?.share ?? 50, ); updateItem( item.id, "coShare", owners.find((o) => o.id === "co")?.share ?? 50, ); }} />
))}
); } // --------------------------------------------------------------------------- // EditDebtsModal // --------------------------------------------------------------------------- export function EditDebtsModal({ open, onOpenChange, initialItems = [], onSave, mainApplicantName = "Main Applicant", coApplicantName = "Co-Applicant", container, className, }: EditDebtsModalProps) { const [items, setItems] = React.useState( initialItems.length > 0 ? initialItems : [makeDefaultDebtItem()], ); const initialSnapshot = React.useRef(""); React.useEffect(() => { if (open) { const data = initialItems.length > 0 ? initialItems : [makeDefaultDebtItem()]; setItems(data); initialSnapshot.current = JSON.stringify(data); } }, [open]); // eslint-disable-line react-hooks/exhaustive-deps const isDirty = JSON.stringify(items) !== initialSnapshot.current; const updateItem = ( id: string, key: K, val: DebtLineItem[K], ) => setItems((prev) => prev.map((item) => (item.id === id ? { ...item, [key]: val } : item)), ); const removeItem = (id: string) => setItems((prev) => prev.filter((item) => item.id !== id)); const addItem = () => setItems((prev) => [...prev, makeDefaultDebtItem()]); const defaultOpenItems = items.length > 0 ? [items[0].id] : []; return ( Edit Debts {items.map((item) => { const df = debtFields(item.debtType); return ( removeItem(item.id)} removeLabel="Remove debt" />
{/* Conditional fields by debt type */} {(df.isPropertyLoan || df.isGeneralLoan || df.isVehicle) && (
updateItem(item.id, "lender", e.target.value) } placeholder="e.g. CommBank" /> updateItem( item.id, "accountNumber", e.target.value, ) } placeholder="e.g. 123-456" />
)} {df.isPropertyLoan && ( updateItem(item.id, "propertyAddress", val) } onSelect={(opt) => updateItem(item.id, "propertyAddress", opt.label) } /> )} {df.isVehicle && (
updateItem( item.id, "vehicleMake", e.target.value, ) } placeholder="e.g. Toyota" /> updateItem( item.id, "vehicleModel", e.target.value, ) } placeholder="e.g. Camry" /> updateItem( item.id, "vehicleYear", e.target.value, ) } placeholder="e.g. 2022" />
)} {df.isCard && (
updateItem(item.id, "lender", e.target.value) } placeholder="e.g. ANZ, Afterpay" /> updateItem(item.id, "creditLimit", val) } />
)} {df.isHecs && ( updateItem(item.id, "institution", e.target.value) } placeholder="e.g. University of Sydney" /> )} {df.isTax && (
updateItem(item.id, "taxYear", e.target.value) } placeholder="e.g. 2023–24" /> updateItem( item.id, "referenceNumber", e.target.value, ) } placeholder="e.g. 1234567890" />
)} {df.isGuarantor && ( updateItem(item.id, "beneficiary", e.target.value) } placeholder="e.g. Jane Smith" /> )}
updateItem(item.id, "amountOwing", val) } /> updateItem(item.id, "originalLoanAmount", val) } />
updateItem(item.id, "repaymentAmount", val) } />
updateItem(item.id, "repaymentFrequency", val) } />
updateItem( item.id, "interestRate", e.target.value, ) } placeholder="e.g. 6.5" className="flex-1" /> %