@wealthx/shadcn 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +203 -150
- package/CHANGELOG.md +29 -0
- package/dist/{chunk-4Y6R4WEC.mjs → chunk-2A5RRQGG.mjs} +9 -22
- package/dist/{chunk-TS2ZX2VS.mjs → chunk-2UM72RJ7.mjs} +11 -15
- package/dist/{chunk-A56YQQHG.mjs → chunk-3NCUZIFP.mjs} +2 -2
- package/dist/chunk-3OYFOX3X.mjs +79 -0
- package/dist/{chunk-RP3SQYA3.mjs → chunk-3TTACBDP.mjs} +9 -4
- package/dist/chunk-4GAWMKMI.mjs +710 -0
- package/dist/{chunk-SYOD63OZ.mjs → chunk-5FQIKDKP.mjs} +6 -6
- package/dist/{chunk-K3JYD4IU.mjs → chunk-5IS7G74I.mjs} +11 -4
- package/dist/chunk-6AW4KJHE.mjs +235 -0
- package/dist/chunk-6CR5N2JW.mjs +302 -0
- package/dist/{chunk-XIRTEFKH.mjs → chunk-6DZEXFNB.mjs} +36 -8
- package/dist/chunk-6O6KD7CE.mjs +271 -0
- package/dist/chunk-7PV3IWCN.mjs +33 -0
- package/dist/{chunk-SPJ5KXW7.mjs → chunk-7S5AESZO.mjs} +5 -5
- package/dist/{chunk-RYCLWMZ7.mjs → chunk-ABFDMHOR.mjs} +9 -7
- package/dist/{chunk-SWGT756Z.mjs → chunk-AMQZRHEZ.mjs} +10 -4
- package/dist/{chunk-WOEHFRGB.mjs → chunk-BDYZCBRT.mjs} +4 -4
- package/dist/{chunk-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
- package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
- package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
- package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
- package/dist/chunk-DOEO3CDL.mjs +27 -0
- package/dist/{chunk-KUDCQ4FI.mjs → chunk-DUJTAXMH.mjs} +9 -4
- package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
- package/dist/{chunk-PMB3A7V3.mjs → chunk-EI5F6FMT.mjs} +1 -1
- package/dist/chunk-EWRB4PAD.mjs +468 -0
- package/dist/chunk-FAKPBKLT.mjs +253 -0
- package/dist/chunk-FNQXOAYJ.mjs +169 -0
- package/dist/{chunk-4CX4SBRO.mjs → chunk-GHC7LLUX.mjs} +14 -5
- package/dist/chunk-HBZLGDIN.mjs +507 -0
- package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
- package/dist/{chunk-PR6V5XKM.mjs → chunk-HVY6KCCF.mjs} +7 -4
- package/dist/chunk-I3RZS7V2.mjs +136 -0
- package/dist/chunk-IAE3F7DR.mjs +1962 -0
- package/dist/{chunk-ZRO5JO3H.mjs → chunk-IHMFS7NZ.mjs} +81 -84
- package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
- package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
- package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
- package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
- package/dist/{chunk-CSDO6VBW.mjs → chunk-LBMRIB3G.mjs} +10 -10
- package/dist/chunk-LV35NGVG.mjs +272 -0
- package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
- package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
- package/dist/chunk-MIZQHHUO.mjs +441 -0
- package/dist/chunk-MN5NYQCL.mjs +29 -0
- package/dist/chunk-NL3ZO62D.mjs +31 -0
- package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
- package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
- package/dist/chunk-PBL4OQV2.mjs +283 -0
- package/dist/{chunk-3WMX6KWS.mjs → chunk-PU4YZQXV.mjs} +11 -12
- package/dist/chunk-QMY3AZJH.mjs +80 -0
- package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
- package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
- package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
- package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
- package/dist/chunk-U4NDAF2P.mjs +207 -0
- package/dist/{chunk-DOH3EHX7.mjs → chunk-U5X52X37.mjs} +1 -1
- package/dist/chunk-UMTOX62O.mjs +415 -0
- package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
- package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
- package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
- package/dist/chunk-ZC45IGZO.mjs +388 -0
- package/dist/components/ui/add-column-modal.js +42 -14
- package/dist/components/ui/add-column-modal.mjs +4 -4
- package/dist/components/ui/add-lead-modal.js +42 -11
- package/dist/components/ui/add-lead-modal.mjs +3 -3
- package/dist/components/ui/advisor-card.js +497 -0
- package/dist/components/ui/advisor-card.mjs +13 -0
- package/dist/components/ui/ai-assistant-drawer.js +11 -10
- package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
- package/dist/components/ui/alert-dialog.js +2 -2
- package/dist/components/ui/alert-dialog.mjs +2 -2
- package/dist/components/ui/appointment-action-dialogs.js +1160 -0
- package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
- package/dist/components/ui/appointment-availability-settings.js +1590 -0
- package/dist/components/ui/appointment-availability-settings.mjs +23 -0
- package/dist/components/ui/appointment-book-dialog.js +1744 -0
- package/dist/components/ui/appointment-book-dialog.mjs +27 -0
- package/dist/components/ui/appointment-calendar-view.js +833 -0
- package/dist/components/ui/appointment-calendar-view.mjs +14 -0
- package/dist/components/ui/appointment-detail-sheet.js +1517 -0
- package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
- package/dist/components/ui/appointment-gmail-connect.js +467 -0
- package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
- package/dist/components/ui/appointment-mini-card.js +345 -0
- package/dist/components/ui/appointment-mini-card.mjs +11 -0
- package/dist/components/ui/appointment-time-slot-picker.js +311 -0
- package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
- package/dist/components/ui/appointment-upcoming-card.js +1268 -0
- package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
- package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
- package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
- package/dist/components/ui/backoffice-alerts-chart.js +786 -0
- package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
- package/dist/components/ui/backoffice-connections-chart.js +817 -0
- package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
- package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
- package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
- package/dist/components/ui/badge.js +6 -6
- package/dist/components/ui/badge.mjs +1 -1
- package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
- package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/button.mjs +1 -1
- package/dist/components/ui/calendar.js +2 -2
- package/dist/components/ui/calendar.mjs +2 -2
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/card.mjs +1 -1
- package/dist/components/ui/cash-balance-line-chart.js +31 -23
- package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
- package/dist/components/ui/cashflow-bar-chart.js +12 -5
- package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
- package/dist/components/ui/chip.js +97 -18
- package/dist/components/ui/chip.mjs +3 -2
- package/dist/components/ui/color-picker.js +547 -0
- package/dist/components/ui/color-picker.mjs +24 -0
- package/dist/components/ui/data-table.js +182 -129
- package/dist/components/ui/data-table.mjs +3 -2
- package/dist/components/ui/date-picker.js +48 -27
- package/dist/components/ui/date-picker.mjs +4 -3
- package/dist/components/ui/dialog.js +37 -9
- package/dist/components/ui/dialog.mjs +2 -2
- package/dist/components/ui/expense-bar-chart.js +12 -5
- package/dist/components/ui/expense-bar-chart.mjs +5 -4
- package/dist/components/ui/field.mjs +2 -2
- package/dist/components/ui/financial-cards.js +322 -155
- package/dist/components/ui/financial-cards.mjs +5 -3
- package/dist/components/ui/financial-drawers.js +2 -2
- package/dist/components/ui/financial-drawers.mjs +3 -3
- package/dist/components/ui/financial-sections.js +14 -10
- package/dist/components/ui/financial-sections.mjs +6 -5
- package/dist/components/ui/form-primitives.js +4 -4
- package/dist/components/ui/form-primitives.mjs +3 -3
- package/dist/components/ui/income-bar-chart.js +12 -5
- package/dist/components/ui/income-bar-chart.mjs +5 -4
- package/dist/components/ui/input-group.js +2 -2
- package/dist/components/ui/input-group.mjs +2 -2
- package/dist/components/ui/kanban-column.js +52 -44
- package/dist/components/ui/kanban-column.mjs +7 -5
- package/dist/components/ui/opportunity-card.js +52 -44
- package/dist/components/ui/opportunity-card.mjs +6 -4
- package/dist/components/ui/opportunity-edit-modals.js +1371 -1267
- package/dist/components/ui/opportunity-edit-modals.mjs +10 -10
- package/dist/components/ui/opportunity-summary-tab.js +2748 -2161
- package/dist/components/ui/opportunity-summary-tab.mjs +16 -16
- package/dist/components/ui/page-header.js +92 -0
- package/dist/components/ui/page-header.mjs +8 -0
- package/dist/components/ui/page-top-bar.js +88 -0
- package/dist/components/ui/page-top-bar.mjs +8 -0
- package/dist/components/ui/pagination.js +303 -19
- package/dist/components/ui/pagination.mjs +11 -4
- package/dist/components/ui/pipeline-board.js +209 -195
- package/dist/components/ui/pipeline-board.mjs +10 -8
- package/dist/components/ui/pipeline-dialogs.js +118 -69
- package/dist/components/ui/pipeline-dialogs.mjs +8 -7
- package/dist/components/ui/pipeline-primitives.js +6 -6
- package/dist/components/ui/pipeline-primitives.mjs +2 -2
- package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
- package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
- package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
- package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
- package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
- package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
- package/dist/components/ui/sidebar-nav.js +679 -0
- package/dist/components/ui/sidebar-nav.mjs +14 -0
- package/dist/components/ui/stage-timeline.js +6 -6
- package/dist/components/ui/stage-timeline.mjs +3 -3
- package/dist/components/ui/stepper.js +283 -0
- package/dist/components/ui/stepper.mjs +18 -0
- package/dist/components/ui/toggle-group.js +4 -4
- package/dist/components/ui/toggle-group.mjs +2 -2
- package/dist/components/ui/toggle.js +4 -4
- package/dist/components/ui/toggle.mjs +1 -1
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
- package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
- package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
- package/dist/index.js +12927 -8522
- package/dist/index.mjs +288 -190
- package/dist/lib/typography.js +10 -10
- package/dist/lib/typography.mjs +1 -1
- package/dist/styles.css +1 -1
- package/package.json +86 -1
- package/src/components/index.tsx +146 -0
- package/src/components/ui/add-column-modal.tsx +7 -7
- package/src/components/ui/add-lead-modal.tsx +6 -3
- package/src/components/ui/advisor-card.tsx +227 -0
- package/src/components/ui/ai-assistant-drawer.tsx +4 -3
- package/src/components/ui/appointment-action-dialogs.tsx +297 -0
- package/src/components/ui/appointment-availability-settings.tsx +645 -0
- package/src/components/ui/appointment-book-dialog.tsx +618 -0
- package/src/components/ui/appointment-calendar-view.tsx +510 -0
- package/src/components/ui/appointment-detail-sheet.tsx +415 -0
- package/src/components/ui/appointment-gmail-connect.tsx +188 -0
- package/src/components/ui/appointment-mini-card.tsx +104 -0
- package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
- package/src/components/ui/appointment-upcoming-card.tsx +635 -0
- package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
- package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
- package/src/components/ui/backoffice-connections-chart.tsx +339 -0
- package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
- package/src/components/ui/badge.tsx +12 -6
- package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
- package/src/components/ui/button.tsx +2 -2
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/cash-balance-line-chart.tsx +4 -23
- package/src/components/ui/cashflow-bar-chart.tsx +9 -2
- package/src/components/ui/chart-shared.tsx +4 -11
- package/src/components/ui/chip.tsx +23 -19
- package/src/components/ui/color-picker.tsx +309 -0
- package/src/components/ui/data-table.tsx +117 -83
- package/src/components/ui/date-picker.tsx +42 -37
- package/src/components/ui/dialog.tsx +72 -6
- package/src/components/ui/expense-bar-chart.tsx +11 -2
- package/src/components/ui/financial-cards.tsx +99 -10
- package/src/components/ui/income-bar-chart.tsx +11 -2
- package/src/components/ui/opportunity-card.tsx +10 -39
- package/src/components/ui/opportunity-edit-modals.tsx +98 -36
- package/src/components/ui/opportunity-summary-tab.tsx +548 -232
- package/src/components/ui/page-header.tsx +57 -0
- package/src/components/ui/page-top-bar.tsx +48 -0
- package/src/components/ui/pagination.tsx +171 -22
- package/src/components/ui/pipeline-board.tsx +12 -5
- package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
- package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
- package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
- package/src/components/ui/sidebar-nav.tsx +516 -0
- package/src/components/ui/stepper.tsx +347 -0
- package/src/components/ui/toggle.tsx +4 -4
- package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
- package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
- package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
- package/src/lib/format-currency.ts +44 -0
- package/src/lib/format-date.ts +50 -0
- package/src/lib/opportunity-constants.ts +12 -0
- package/src/lib/typography.ts +11 -11
- package/src/styles/globals.css +36 -34
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +17 -0
- package/dist/chunk-PG6K5XEC.mjs +0 -475
- package/dist/chunk-WA6O6EUR.mjs +0 -1885
- package/dist/chunk-WNGWBVLV.mjs +0 -148
- package/dist/{chunk-LLVQKSU3.mjs → chunk-GD4BJDJR.mjs} +3 -3
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { Pencil } from "lucide-react";
|
|
3
|
+
import { ChevronDown, ChevronUp, Mail, Pencil, Phone } from "lucide-react";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { formatCurrency } from "@/lib/format-currency";
|
|
7
|
+
import { PROPERTY_ASSET_TYPES } from "@/lib/opportunity-constants";
|
|
4
8
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
|
|
5
9
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
FinancialDetailField,
|
|
11
|
+
FinancialLineItem,
|
|
12
|
+
FinancialSectionLabel,
|
|
13
|
+
FinancialSubtotalFrame,
|
|
14
|
+
FinancialSubtotalBlock,
|
|
15
|
+
} from "./financial-primitives";
|
|
16
|
+
import type { DebtCardProps, PropertyCardProps } from "./financial-cards";
|
|
11
17
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
AboutCard,
|
|
19
|
+
DebtCard,
|
|
20
|
+
IncomeCard,
|
|
21
|
+
ExpensesCard,
|
|
22
|
+
PropertyCard,
|
|
23
|
+
} from "./financial-cards";
|
|
16
24
|
import {
|
|
17
25
|
EditLoanScenarioModal,
|
|
18
26
|
EditAssetsModal,
|
|
@@ -35,28 +43,13 @@ import type {
|
|
|
35
43
|
*
|
|
36
44
|
* Full Summary tab for the OpportunityDetailsDrawer.
|
|
37
45
|
* Renders:
|
|
38
|
-
* -
|
|
39
|
-
*
|
|
40
|
-
* -
|
|
46
|
+
* - Hero Band: deal-at-a-glance strip (applicant names, LVR, net surplus,
|
|
47
|
+
* serviceability signal, contact shortcuts)
|
|
48
|
+
* - Loan Scenario card (collapsible, always above the sub-tabs)
|
|
49
|
+
* - Joint / Main / Co applicant sub-tabs (filled variant, full width)
|
|
50
|
+
* - All inline edit modals (managed internally, scoped to drawer portal)
|
|
41
51
|
*
|
|
42
52
|
* Pass as the `summary` slot of `OpportunityDetailsDrawer`.
|
|
43
|
-
*
|
|
44
|
-
* ```tsx
|
|
45
|
-
* <OpportunityDetailsDrawer
|
|
46
|
-
* tabs={{
|
|
47
|
-
* summary: (
|
|
48
|
-
* <OpportunitySummaryTab
|
|
49
|
-
* isJoint={true}
|
|
50
|
-
* loanScenario={loanScenario}
|
|
51
|
-
* onLoanScenarioChange={setLoanScenario}
|
|
52
|
-
* assets={assets}
|
|
53
|
-
* onAssetsChange={setAssets}
|
|
54
|
-
* // … other data + callbacks
|
|
55
|
-
* />
|
|
56
|
-
* ),
|
|
57
|
-
* }}
|
|
58
|
-
* />
|
|
59
|
-
* ```
|
|
60
53
|
*/
|
|
61
54
|
|
|
62
55
|
// ---------------------------------------------------------------------------
|
|
@@ -98,20 +91,69 @@ export interface OpportunitySummaryTabProps {
|
|
|
98
91
|
// Internal helpers
|
|
99
92
|
// ---------------------------------------------------------------------------
|
|
100
93
|
|
|
101
|
-
function formatCurrency(n: number) {
|
|
102
|
-
return "$" + n.toLocaleString("en-AU");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
94
|
function toMonthly(amount: number, freq: "Monthly" | "Weekly") {
|
|
106
95
|
return freq === "Monthly" ? amount : (amount * 52) / 12;
|
|
107
96
|
}
|
|
108
97
|
|
|
98
|
+
/** Map an AssetLineItem for a property to PropertyCardProps. */
|
|
99
|
+
function assetToPropertyCard(item: AssetLineItem): PropertyCardProps {
|
|
100
|
+
return {
|
|
101
|
+
address: item.address || item.assetType,
|
|
102
|
+
type: item.usedAs,
|
|
103
|
+
estimated: formatCurrency(item.value),
|
|
104
|
+
isLinkedToBank: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Map a DebtLineItem to DebtCardProps. */
|
|
109
|
+
function debtToCard(debt: DebtLineItem): DebtCardProps {
|
|
110
|
+
return {
|
|
111
|
+
lenderName: debt.lender || debt.debtType,
|
|
112
|
+
currentLoanAmount: formatCurrency(debt.amountOwing),
|
|
113
|
+
interestRate: debt.interestRate ? `${debt.interestRate}% p.a.` : undefined,
|
|
114
|
+
originalLoanAmount: debt.originalLoanAmount
|
|
115
|
+
? formatCurrency(debt.originalLoanAmount)
|
|
116
|
+
: undefined,
|
|
117
|
+
monthlyRepayments: debt.repaymentAmount
|
|
118
|
+
? `${formatCurrency(debt.repaymentAmount)} / ${debt.repaymentFrequency}`
|
|
119
|
+
: undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Tailwind text-color class for the LVR metric. */
|
|
124
|
+
function lvrColorClass(pct: number): string {
|
|
125
|
+
if (pct <= 0) return "text-muted-foreground";
|
|
126
|
+
if (pct < 80) return "text-emerald-600";
|
|
127
|
+
if (pct < 90) return "text-amber-600";
|
|
128
|
+
return "text-destructive";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Serviceability label + badge class based on net monthly surplus. */
|
|
132
|
+
function serviceabilityInfo(surplus: number): {
|
|
133
|
+
label: string;
|
|
134
|
+
badgeClass: string;
|
|
135
|
+
} {
|
|
136
|
+
if (surplus > 1500)
|
|
137
|
+
return {
|
|
138
|
+
label: "Likely Serviceable",
|
|
139
|
+
badgeClass: "bg-emerald-50 text-emerald-700 border border-emerald-200",
|
|
140
|
+
};
|
|
141
|
+
if (surplus > 0)
|
|
142
|
+
return {
|
|
143
|
+
label: "Borderline",
|
|
144
|
+
badgeClass: "bg-amber-50 text-amber-700 border border-amber-200",
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
label: "At Risk",
|
|
148
|
+
badgeClass: "bg-red-50 text-destructive border border-red-200",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
109
152
|
/**
|
|
110
|
-
* Pencil icon button rendered
|
|
111
|
-
* Uses stopPropagation so the click
|
|
112
|
-
* toggling the accordion open/closed state.
|
|
153
|
+
* Pencil icon button rendered next to a section header.
|
|
154
|
+
* Uses stopPropagation so the click doesn't toggle a collapsible parent.
|
|
113
155
|
*/
|
|
114
|
-
function
|
|
156
|
+
function SectionEditButton({
|
|
115
157
|
onClick,
|
|
116
158
|
title = "Edit",
|
|
117
159
|
}: {
|
|
@@ -119,9 +161,11 @@ function TriggerEditButton({
|
|
|
119
161
|
title?: string;
|
|
120
162
|
}) {
|
|
121
163
|
return (
|
|
122
|
-
<
|
|
164
|
+
<Button
|
|
123
165
|
type="button"
|
|
124
|
-
|
|
166
|
+
variant="ghost"
|
|
167
|
+
size="icon"
|
|
168
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
125
169
|
onClick={(e) => {
|
|
126
170
|
e.stopPropagation();
|
|
127
171
|
onClick();
|
|
@@ -130,16 +174,151 @@ function TriggerEditButton({
|
|
|
130
174
|
title={title}
|
|
131
175
|
>
|
|
132
176
|
<Pencil className="size-3.5" />
|
|
133
|
-
</
|
|
177
|
+
</Button>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// HeroBand (internal)
|
|
183
|
+
// Deal-at-a-glance strip always visible at the top of the summary.
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
interface HeroBandProps {
|
|
187
|
+
mainAbout: AboutApplicantFormData;
|
|
188
|
+
coAbout?: AboutApplicantFormData;
|
|
189
|
+
isJoint: boolean;
|
|
190
|
+
loanAmount: number;
|
|
191
|
+
propertyEstimate: number;
|
|
192
|
+
cashEquity: number;
|
|
193
|
+
netSurplus: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function HeroBand({
|
|
197
|
+
mainAbout,
|
|
198
|
+
coAbout,
|
|
199
|
+
isJoint,
|
|
200
|
+
loanAmount,
|
|
201
|
+
propertyEstimate,
|
|
202
|
+
cashEquity,
|
|
203
|
+
netSurplus,
|
|
204
|
+
}: HeroBandProps) {
|
|
205
|
+
const lvrPct =
|
|
206
|
+
propertyEstimate > 0 ? (loanAmount / propertyEstimate) * 100 : 0;
|
|
207
|
+
const svc = serviceabilityInfo(netSurplus);
|
|
208
|
+
|
|
209
|
+
const mainName =
|
|
210
|
+
[mainAbout.firstName, mainAbout.lastName].filter(Boolean).join(" ") ||
|
|
211
|
+
"Main Applicant";
|
|
212
|
+
const coName = coAbout
|
|
213
|
+
? [coAbout.firstName, coAbout.lastName].filter(Boolean).join(" ")
|
|
214
|
+
: "";
|
|
215
|
+
|
|
216
|
+
const surplusAbs = Math.abs(Math.round(netSurplus));
|
|
217
|
+
const surplusDisplay = `${netSurplus >= 0 ? "+" : "-"}${formatCurrency(surplusAbs)}`;
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className="mb-4 border border-border bg-muted/40">
|
|
221
|
+
{/* ── Applicant row ── */}
|
|
222
|
+
<div className="flex items-center justify-between gap-4 px-4 py-3">
|
|
223
|
+
<div className="flex flex-col gap-0.5">
|
|
224
|
+
<span className="text-sm font-medium text-foreground">
|
|
225
|
+
{mainName}
|
|
226
|
+
</span>
|
|
227
|
+
{isJoint && coName && (
|
|
228
|
+
<span className="text-xs text-muted-foreground">
|
|
229
|
+
+ {coName} · Joint Application
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
<div className="flex items-center gap-1">
|
|
234
|
+
{mainAbout.phone && (
|
|
235
|
+
<a
|
|
236
|
+
href={`tel:${mainAbout.phone}`}
|
|
237
|
+
className="inline-flex size-7 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
238
|
+
title={`Call: ${mainAbout.phone}`}
|
|
239
|
+
>
|
|
240
|
+
<Phone className="size-3.5" />
|
|
241
|
+
</a>
|
|
242
|
+
)}
|
|
243
|
+
{mainAbout.email && (
|
|
244
|
+
<a
|
|
245
|
+
href={`mailto:${mainAbout.email}`}
|
|
246
|
+
className="inline-flex size-7 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
247
|
+
title={`Email: ${mainAbout.email}`}
|
|
248
|
+
>
|
|
249
|
+
<Mail className="size-3.5" />
|
|
250
|
+
</a>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* ── Key metrics row ── */}
|
|
256
|
+
<div className="grid grid-cols-4 divide-x divide-border border-t border-border">
|
|
257
|
+
<div className="flex flex-col px-4 py-2.5">
|
|
258
|
+
<span className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
259
|
+
Loan Amount
|
|
260
|
+
</span>
|
|
261
|
+
<span className="text-sm font-semibold text-foreground">
|
|
262
|
+
{formatCurrency(loanAmount)}
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="flex flex-col px-4 py-2.5">
|
|
266
|
+
<span className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
267
|
+
Property
|
|
268
|
+
</span>
|
|
269
|
+
<span className="text-sm font-semibold text-foreground">
|
|
270
|
+
{propertyEstimate > 0 ? formatCurrency(propertyEstimate) : "—"}
|
|
271
|
+
</span>
|
|
272
|
+
</div>
|
|
273
|
+
<div className="flex flex-col px-4 py-2.5">
|
|
274
|
+
<span className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
275
|
+
LVR
|
|
276
|
+
</span>
|
|
277
|
+
<span className={cn("text-sm font-semibold", lvrColorClass(lvrPct))}>
|
|
278
|
+
{lvrPct > 0 ? `${lvrPct.toFixed(1)}%` : "—"}
|
|
279
|
+
</span>
|
|
280
|
+
</div>
|
|
281
|
+
<div className="flex flex-col px-4 py-2.5">
|
|
282
|
+
<span className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
283
|
+
Cash / Deposit
|
|
284
|
+
</span>
|
|
285
|
+
<span className="text-sm font-semibold text-foreground">
|
|
286
|
+
{formatCurrency(cashEquity)}
|
|
287
|
+
</span>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* ── Serviceability row ── */}
|
|
292
|
+
<div className="flex items-center justify-between border-t border-border px-4 py-2.5">
|
|
293
|
+
<div className="flex flex-col gap-0.5">
|
|
294
|
+
<span className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
295
|
+
Net Surplus / Month
|
|
296
|
+
</span>
|
|
297
|
+
<span
|
|
298
|
+
className={cn(
|
|
299
|
+
"text-sm font-semibold",
|
|
300
|
+
netSurplus >= 0 ? "text-emerald-600" : "text-destructive",
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{surplusDisplay}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div className={cn("px-2.5 py-1 text-xs font-medium", svc.badgeClass)}>
|
|
307
|
+
{svc.label}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
134
311
|
);
|
|
135
312
|
}
|
|
136
313
|
|
|
137
314
|
// ---------------------------------------------------------------------------
|
|
138
|
-
//
|
|
315
|
+
// ApplicantCardTab (internal)
|
|
316
|
+
// Replaces ApplicantAccordionTab — uses bordered card pattern for consistency.
|
|
317
|
+
// AboutCard / IncomeCard / ExpensesCard each carry their own border so we float
|
|
318
|
+
// the section label + edit button ABOVE each card (no outer bordered wrapper).
|
|
139
319
|
// ---------------------------------------------------------------------------
|
|
140
320
|
|
|
141
|
-
interface
|
|
142
|
-
accordionPrefix: string;
|
|
321
|
+
interface ApplicantCardTabProps {
|
|
143
322
|
about: AboutApplicantFormData;
|
|
144
323
|
income: IncomeFormData;
|
|
145
324
|
expenses: ExpensesFormData;
|
|
@@ -148,15 +327,14 @@ interface ApplicantAccordionTabProps {
|
|
|
148
327
|
onEditExpenses: () => void;
|
|
149
328
|
}
|
|
150
329
|
|
|
151
|
-
function
|
|
152
|
-
accordionPrefix,
|
|
330
|
+
function ApplicantCardTab({
|
|
153
331
|
about,
|
|
154
332
|
income,
|
|
155
333
|
expenses,
|
|
156
334
|
onEditAbout,
|
|
157
335
|
onEditIncome,
|
|
158
336
|
onEditExpenses,
|
|
159
|
-
}:
|
|
337
|
+
}: ApplicantCardTabProps) {
|
|
160
338
|
const totalMonthlyIncome = formatCurrency(
|
|
161
339
|
income.items.reduce(
|
|
162
340
|
(sum, i) => sum + toMonthly(i.incomeAmount, i.frequency),
|
|
@@ -171,69 +349,72 @@ function ApplicantAccordionTab({
|
|
|
171
349
|
);
|
|
172
350
|
|
|
173
351
|
return (
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
</
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
352
|
+
<div className="flex flex-col gap-4 py-4">
|
|
353
|
+
{/* ── About ── */}
|
|
354
|
+
<div className="flex flex-col gap-2">
|
|
355
|
+
<div className="flex items-center justify-between">
|
|
356
|
+
<FinancialSectionLabel>About</FinancialSectionLabel>
|
|
357
|
+
<SectionEditButton onClick={onEditAbout} title="Edit About" />
|
|
358
|
+
</div>
|
|
359
|
+
<AboutCard
|
|
360
|
+
title={about.title}
|
|
361
|
+
firstName={about.firstName}
|
|
362
|
+
lastName={about.lastName}
|
|
363
|
+
phone={about.phone}
|
|
364
|
+
email={about.email}
|
|
365
|
+
dob={about.dob}
|
|
366
|
+
gender={about.gender}
|
|
367
|
+
maritalStatus={about.maritalStatus}
|
|
368
|
+
numDependants={about.numDependants}
|
|
369
|
+
citizenStatus={about.citizenStatus}
|
|
370
|
+
residentialAddress={about.residentialAddress}
|
|
371
|
+
residentialStatus={about.residentialStatus}
|
|
372
|
+
timeAtAddressYears={about.timeAtAddressYears}
|
|
373
|
+
timeAtAddressMonths={about.timeAtAddressMonths}
|
|
374
|
+
driversLicence={about.driversLicence}
|
|
375
|
+
passport={about.passport}
|
|
376
|
+
propertyInTrust={about.propertyInTrust}
|
|
377
|
+
companyOwnership={about.companyOwnership}
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* ── Income ── */}
|
|
382
|
+
<div className="flex flex-col gap-2">
|
|
383
|
+
<div className="flex items-center justify-between">
|
|
384
|
+
<FinancialSectionLabel>Income</FinancialSectionLabel>
|
|
385
|
+
<SectionEditButton onClick={onEditIncome} title="Edit Income" />
|
|
386
|
+
</div>
|
|
387
|
+
<IncomeCard
|
|
388
|
+
items={income.items.map((i) => ({
|
|
389
|
+
incomeType: i.incomeType,
|
|
390
|
+
jobTitle: i.jobTitle,
|
|
391
|
+
companyName: i.companyName,
|
|
392
|
+
companyAddress: i.companyAddress,
|
|
393
|
+
startDate: i.startDate,
|
|
394
|
+
stillInPosition: i.stillInPosition,
|
|
395
|
+
endDate: i.endDate,
|
|
396
|
+
companyType: i.companyType,
|
|
397
|
+
amountLabel: `${formatCurrency(i.incomeAmount)} / ${i.frequency}`,
|
|
398
|
+
}))}
|
|
399
|
+
totalMonthly={totalMonthlyIncome}
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
{/* ── Expenses ── */}
|
|
404
|
+
<div className="flex flex-col gap-2">
|
|
405
|
+
<div className="flex items-center justify-between">
|
|
406
|
+
<FinancialSectionLabel>Expenses</FinancialSectionLabel>
|
|
407
|
+
<SectionEditButton onClick={onEditExpenses} title="Edit Expenses" />
|
|
408
|
+
</div>
|
|
409
|
+
<ExpensesCard
|
|
410
|
+
items={expenses.items.map((e) => ({
|
|
411
|
+
expenseType: e.expenseType,
|
|
412
|
+
amountLabel: `${formatCurrency(e.amount)} / ${e.frequency}`,
|
|
413
|
+
}))}
|
|
414
|
+
totalMonthly={totalMonthlyExpenses}
|
|
415
|
+
/>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
237
418
|
);
|
|
238
419
|
}
|
|
239
420
|
|
|
@@ -262,11 +443,17 @@ export function OpportunitySummaryTab({
|
|
|
262
443
|
coExpenses,
|
|
263
444
|
onCoExpensesChange,
|
|
264
445
|
}: OpportunitySummaryTabProps) {
|
|
265
|
-
// ──
|
|
446
|
+
// ── Portal container — scopes edit modal overlays inside the drawer ────────
|
|
447
|
+
const [portalEl, setPortalEl] = useState<HTMLDivElement | null>(null);
|
|
448
|
+
|
|
449
|
+
// ── Sub-tab state ─────────────────────────────────────────────────────────
|
|
266
450
|
const [summarySubTab, setSummarySubTab] = useState<"joint" | "main" | "co">(
|
|
267
451
|
"joint",
|
|
268
452
|
);
|
|
269
453
|
|
|
454
|
+
// ── Loan Scenario collapse ─────────────────────────────────────────────────
|
|
455
|
+
const [loanScenarioExpanded, setLoanScenarioExpanded] = useState(true);
|
|
456
|
+
|
|
270
457
|
// ── Modal open state (internal) ───────────────────────────────────────────
|
|
271
458
|
const [editLoanOpen, setEditLoanOpen] = useState(false);
|
|
272
459
|
const [editAssetsOpen, setEditAssetsOpen] = useState(false);
|
|
@@ -303,26 +490,88 @@ export function OpportunitySummaryTab({
|
|
|
303
490
|
? [coAbout.firstName, coAbout.lastName].filter(Boolean).join(" ")
|
|
304
491
|
: "Co-Applicant";
|
|
305
492
|
|
|
493
|
+
// ── Property cards derived from asset list ────────────────────────────────
|
|
494
|
+
const propertyCards: PropertyCardProps[] = assets
|
|
495
|
+
.filter((a) => PROPERTY_ASSET_TYPES.has(a.assetType))
|
|
496
|
+
.map(assetToPropertyCard);
|
|
497
|
+
|
|
498
|
+
// ── Scrim: true when any edit modal is open ────────────────────────────────
|
|
499
|
+
const anyModalOpen =
|
|
500
|
+
editLoanOpen ||
|
|
501
|
+
editAssetsOpen ||
|
|
502
|
+
editDebtsOpen ||
|
|
503
|
+
editMainAboutOpen ||
|
|
504
|
+
editCoAboutOpen ||
|
|
505
|
+
editMainIncomeOpen ||
|
|
506
|
+
editCoIncomeOpen ||
|
|
507
|
+
editMainExpensesOpen ||
|
|
508
|
+
editCoExpensesOpen;
|
|
509
|
+
|
|
306
510
|
return (
|
|
307
511
|
<>
|
|
308
|
-
{/* ──
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
512
|
+
{/* ── Hero Band — always visible, deal at a glance ── */}
|
|
513
|
+
<HeroBand
|
|
514
|
+
mainAbout={mainAbout}
|
|
515
|
+
coAbout={coAbout}
|
|
516
|
+
isJoint={isJoint}
|
|
517
|
+
loanAmount={loanScenario.loanAmount}
|
|
518
|
+
propertyEstimate={loanScenario.propertyEstimate}
|
|
519
|
+
cashEquity={loanScenario.cashEquity}
|
|
520
|
+
netSurplus={netSurplus}
|
|
521
|
+
/>
|
|
522
|
+
|
|
523
|
+
{/* ── Loan Scenario — collapsible bordered card ── */}
|
|
524
|
+
<div className="mb-4 border border-border">
|
|
525
|
+
<div className="flex items-center justify-between px-4 py-2.5">
|
|
526
|
+
{/* Left: toggle area (label + chevron) */}
|
|
527
|
+
<Button
|
|
528
|
+
type="button"
|
|
529
|
+
variant="ghost"
|
|
530
|
+
className="h-auto flex-1 justify-start gap-2 px-0 text-left"
|
|
531
|
+
onClick={() => setLoanScenarioExpanded((v) => !v)}
|
|
532
|
+
aria-expanded={loanScenarioExpanded}
|
|
533
|
+
>
|
|
534
|
+
<FinancialSectionLabel>Loan Scenario</FinancialSectionLabel>
|
|
535
|
+
{loanScenarioExpanded ? (
|
|
536
|
+
<ChevronUp className="size-3.5 text-muted-foreground" />
|
|
537
|
+
) : (
|
|
538
|
+
<ChevronDown className="size-3.5 text-muted-foreground" />
|
|
539
|
+
)}
|
|
540
|
+
</Button>
|
|
541
|
+
{/* Right: edit button */}
|
|
542
|
+
<SectionEditButton
|
|
543
|
+
onClick={() => setEditLoanOpen(true)}
|
|
544
|
+
title="Edit Loan Scenario"
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
{loanScenarioExpanded && (
|
|
549
|
+
<div className="grid grid-cols-4 gap-x-6 gap-y-4 border-t border-border p-4">
|
|
550
|
+
<FinancialDetailField label="Lending Type" value="Home Loan" />
|
|
551
|
+
<FinancialDetailField
|
|
552
|
+
label="Purpose of Loan"
|
|
553
|
+
value={loanScenario.loanPurpose}
|
|
554
|
+
/>
|
|
555
|
+
<FinancialDetailField
|
|
556
|
+
label="Loan Amount"
|
|
557
|
+
value={formatCurrency(loanScenario.loanAmount)}
|
|
558
|
+
/>
|
|
559
|
+
<FinancialDetailField
|
|
560
|
+
label="Property Estimate"
|
|
561
|
+
value={formatCurrency(loanScenario.propertyEstimate)}
|
|
562
|
+
/>
|
|
563
|
+
<FinancialDetailField
|
|
564
|
+
label="Cash / Deposit"
|
|
565
|
+
value={formatCurrency(loanScenario.cashEquity)}
|
|
566
|
+
/>
|
|
567
|
+
<FinancialDetailField label="Property Address" value="—" />
|
|
568
|
+
<FinancialDetailField
|
|
569
|
+
label="Duration"
|
|
570
|
+
value={`${loanScenario.loanDuration} years`}
|
|
571
|
+
/>
|
|
572
|
+
<FinancialDetailField
|
|
573
|
+
label="Important Features"
|
|
574
|
+
value={
|
|
326
575
|
[
|
|
327
576
|
loanScenario.featureVariableRate && "Variable Rate",
|
|
328
577
|
loanScenario.featureFixedRate && "Fixed Rate",
|
|
@@ -334,127 +583,160 @@ export function OpportunitySummaryTab({
|
|
|
334
583
|
.filter(Boolean)
|
|
335
584
|
.join(", ") || "—"
|
|
336
585
|
}
|
|
337
|
-
topThreePriorities={loanScenario.priorities.join(", ") || "—"}
|
|
338
586
|
/>
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
{label}
|
|
347
|
-
</span>
|
|
348
|
-
<div className="border border-border px-4 py-2">
|
|
349
|
-
<span className="text-label-medium text-foreground">
|
|
350
|
-
{value}
|
|
351
|
-
</span>
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
))}
|
|
355
|
-
</div>
|
|
356
|
-
</AccordionContent>
|
|
357
|
-
</AccordionItem>
|
|
358
|
-
</Accordion>
|
|
587
|
+
<FinancialDetailField
|
|
588
|
+
label="Top Priorities"
|
|
589
|
+
value={loanScenario.priorities.join(", ") || "—"}
|
|
590
|
+
/>
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
359
594
|
|
|
360
|
-
{/* ── Applicant sub-tabs ── */}
|
|
595
|
+
{/* ── Applicant sub-tabs — filled variant, full width ── */}
|
|
361
596
|
<Tabs
|
|
362
597
|
value={summarySubTab}
|
|
363
598
|
onValueChange={(v) => setSummarySubTab(v as "joint" | "main" | "co")}
|
|
364
599
|
className="pb-6"
|
|
365
600
|
>
|
|
366
601
|
<div className="sticky top-0 z-10 bg-background pb-1 pt-2">
|
|
367
|
-
<TabsList
|
|
602
|
+
<TabsList className="w-full">
|
|
368
603
|
<TabsTrigger value="joint">Joint</TabsTrigger>
|
|
369
604
|
<TabsTrigger value="main">{mainName}</TabsTrigger>
|
|
370
605
|
{isJoint && <TabsTrigger value="co">{coName}</TabsTrigger>}
|
|
371
606
|
</TabsList>
|
|
372
607
|
</div>
|
|
373
608
|
|
|
374
|
-
{/* ── Joint tab
|
|
375
|
-
<TabsContent value="joint">
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
<div className="
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
<button
|
|
398
|
-
type="button"
|
|
399
|
-
className="inline-flex size-6 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
400
|
-
onClick={(e) => {
|
|
401
|
-
e.stopPropagation();
|
|
402
|
-
setEditDebtsOpen(true);
|
|
403
|
-
}}
|
|
404
|
-
aria-label="Edit Debts"
|
|
405
|
-
title="Edit Debts"
|
|
406
|
-
>
|
|
407
|
-
<Pencil className="size-3.5" />
|
|
408
|
-
</button>
|
|
609
|
+
{/* ── Joint tab — combined overview ── */}
|
|
610
|
+
<TabsContent value="joint" className="flex flex-col gap-4 pb-6 pt-4">
|
|
611
|
+
{/* Financial Overview — 2-col: Cashflow | Balance Sheet */}
|
|
612
|
+
<div className="border border-border">
|
|
613
|
+
<div className="px-4 py-2.5">
|
|
614
|
+
<FinancialSectionLabel>Financial Overview</FinancialSectionLabel>
|
|
615
|
+
</div>
|
|
616
|
+
<div className="grid grid-cols-2 divide-x divide-border border-t border-border">
|
|
617
|
+
{/* Col 1: Cashflow */}
|
|
618
|
+
<div className="flex flex-col">
|
|
619
|
+
<div className="px-4 pb-2 pt-3">
|
|
620
|
+
<FinancialSectionLabel>Cashflow</FinancialSectionLabel>
|
|
621
|
+
</div>
|
|
622
|
+
<div className="flex flex-1 flex-col px-4 pb-4">
|
|
623
|
+
<FinancialLineItem
|
|
624
|
+
label="Monthly Income"
|
|
625
|
+
value={formatCurrency(Math.round(combinedMonthlyIncome))}
|
|
626
|
+
/>
|
|
627
|
+
<FinancialLineItem
|
|
628
|
+
label="Monthly Expenses"
|
|
629
|
+
value={formatCurrency(Math.round(combinedMonthlyExpenses))}
|
|
630
|
+
destructive
|
|
631
|
+
/>
|
|
409
632
|
</div>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
633
|
+
<FinancialSubtotalFrame>
|
|
634
|
+
<FinancialSubtotalBlock
|
|
635
|
+
monthlyAverage={formatCurrency(
|
|
636
|
+
Math.abs(Math.round(netSurplus)),
|
|
637
|
+
)}
|
|
638
|
+
label={netSurplus < 0 ? "Net Deficit" : "Net Surplus"}
|
|
639
|
+
/>
|
|
640
|
+
</FinancialSubtotalFrame>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
{/* Col 2: Balance Sheet */}
|
|
644
|
+
<div className="flex flex-col">
|
|
645
|
+
<div className="px-4 pb-2 pt-3">
|
|
646
|
+
<FinancialSectionLabel>Balance Sheet</FinancialSectionLabel>
|
|
647
|
+
</div>
|
|
648
|
+
<div className="flex flex-1 flex-col px-4 pb-4">
|
|
649
|
+
<FinancialLineItem label="Total Assets" value={totalAssets} />
|
|
650
|
+
<FinancialLineItem
|
|
651
|
+
label="Total Liabilities"
|
|
652
|
+
value={totalDebts}
|
|
653
|
+
destructive
|
|
654
|
+
/>
|
|
655
|
+
</div>
|
|
656
|
+
<FinancialSubtotalFrame>
|
|
657
|
+
<FinancialSubtotalBlock
|
|
658
|
+
monthlyAverage={formatCurrency(
|
|
659
|
+
Math.abs(
|
|
660
|
+
Math.round(totalAssetsAmount - totalDebtsAmount),
|
|
422
661
|
),
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
662
|
+
)}
|
|
663
|
+
label={
|
|
664
|
+
totalAssetsAmount < totalDebtsAmount
|
|
665
|
+
? "Net Deficit"
|
|
666
|
+
: "Net Position"
|
|
667
|
+
}
|
|
668
|
+
/>
|
|
669
|
+
</FinancialSubtotalFrame>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
|
|
674
|
+
{/* Documents — placed early so brokers can immediately check
|
|
675
|
+
what's been uploaded vs what's still missing */}
|
|
676
|
+
<div className="border border-border">
|
|
677
|
+
<div className="px-4 py-2.5">
|
|
678
|
+
<FinancialSectionLabel>Documents</FinancialSectionLabel>
|
|
679
|
+
</div>
|
|
680
|
+
<div className="p-4">
|
|
681
|
+
<p className="text-body-small text-muted-foreground">
|
|
682
|
+
No documents uploaded yet.
|
|
683
|
+
</p>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
|
|
687
|
+
{/* Property Holdings — bordered card, divide-x columns, max 3 per row */}
|
|
688
|
+
{propertyCards.length > 0 && (
|
|
689
|
+
<div className="border border-border">
|
|
690
|
+
<div className="flex items-center justify-between px-4 py-2.5">
|
|
691
|
+
<FinancialSectionLabel>Property Holdings</FinancialSectionLabel>
|
|
692
|
+
<SectionEditButton
|
|
693
|
+
onClick={() => setEditAssetsOpen(true)}
|
|
694
|
+
title="Edit Assets"
|
|
695
|
+
/>
|
|
696
|
+
</div>
|
|
697
|
+
<div
|
|
698
|
+
className={cn(
|
|
699
|
+
"grid divide-x divide-border border-t border-border",
|
|
700
|
+
propertyCards.length === 1
|
|
701
|
+
? "grid-cols-1"
|
|
702
|
+
: propertyCards.length === 2
|
|
703
|
+
? "grid-cols-2"
|
|
704
|
+
: "grid-cols-3",
|
|
705
|
+
)}
|
|
706
|
+
>
|
|
707
|
+
{propertyCards.map((card, i) => (
|
|
708
|
+
<div key={card.address + i} className="p-4">
|
|
709
|
+
<PropertyCard {...card} borderless />
|
|
710
|
+
</div>
|
|
711
|
+
))}
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
)}
|
|
715
|
+
|
|
716
|
+
{/* Mortgages & Loans — bordered card, grid of DebtCards */}
|
|
717
|
+
{debts.length > 0 && (
|
|
718
|
+
<div className="border border-border">
|
|
719
|
+
<div className="flex items-center justify-between px-4 py-2.5">
|
|
720
|
+
<FinancialSectionLabel>
|
|
721
|
+
Mortgages & Loans
|
|
722
|
+
</FinancialSectionLabel>
|
|
723
|
+
<SectionEditButton
|
|
724
|
+
onClick={() => setEditDebtsOpen(true)}
|
|
725
|
+
title="Edit Debts"
|
|
438
726
|
/>
|
|
439
|
-
</
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
No documents uploaded yet.
|
|
448
|
-
</p>
|
|
449
|
-
</AccordionContent>
|
|
450
|
-
</AccordionItem>
|
|
451
|
-
</Accordion>
|
|
727
|
+
</div>
|
|
728
|
+
<div className="grid grid-cols-2 gap-3 p-4">
|
|
729
|
+
{debts.map((debt) => (
|
|
730
|
+
<DebtCard key={debt.id} {...debtToCard(debt)} />
|
|
731
|
+
))}
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
)}
|
|
452
735
|
</TabsContent>
|
|
453
736
|
|
|
454
737
|
{/* ── Main Applicant tab ── */}
|
|
455
738
|
<TabsContent value="main">
|
|
456
|
-
<
|
|
457
|
-
accordionPrefix="main"
|
|
739
|
+
<ApplicantCardTab
|
|
458
740
|
about={mainAbout}
|
|
459
741
|
income={mainIncome}
|
|
460
742
|
expenses={mainExpenses}
|
|
@@ -467,8 +749,7 @@ export function OpportunitySummaryTab({
|
|
|
467
749
|
{/* ── Co-Applicant tab (joint applications only) ── */}
|
|
468
750
|
{isJoint && coAbout && coIncome && coExpenses && (
|
|
469
751
|
<TabsContent value="co">
|
|
470
|
-
<
|
|
471
|
-
accordionPrefix="co"
|
|
752
|
+
<ApplicantCardTab
|
|
472
753
|
about={coAbout}
|
|
473
754
|
income={coIncome}
|
|
474
755
|
expenses={coExpenses}
|
|
@@ -480,11 +761,12 @@ export function OpportunitySummaryTab({
|
|
|
480
761
|
)}
|
|
481
762
|
</Tabs>
|
|
482
763
|
|
|
483
|
-
{/* ── Edit modals
|
|
764
|
+
{/* ── Edit modals — portal scoped inside the drawer ── */}
|
|
484
765
|
<EditLoanScenarioModal
|
|
485
766
|
open={editLoanOpen}
|
|
486
767
|
onOpenChange={setEditLoanOpen}
|
|
487
768
|
initialData={loanScenario}
|
|
769
|
+
container={portalEl}
|
|
488
770
|
onSave={(data) => {
|
|
489
771
|
onLoanScenarioChange?.(data);
|
|
490
772
|
setEditLoanOpen(false);
|
|
@@ -494,6 +776,7 @@ export function OpportunitySummaryTab({
|
|
|
494
776
|
open={editAssetsOpen}
|
|
495
777
|
onOpenChange={setEditAssetsOpen}
|
|
496
778
|
initialItems={assets}
|
|
779
|
+
container={portalEl}
|
|
497
780
|
onSave={(items) => {
|
|
498
781
|
onAssetsChange?.(items);
|
|
499
782
|
setEditAssetsOpen(false);
|
|
@@ -503,6 +786,7 @@ export function OpportunitySummaryTab({
|
|
|
503
786
|
open={editDebtsOpen}
|
|
504
787
|
onOpenChange={setEditDebtsOpen}
|
|
505
788
|
initialItems={debts}
|
|
789
|
+
container={portalEl}
|
|
506
790
|
onSave={(items) => {
|
|
507
791
|
onDebtsChange?.(items);
|
|
508
792
|
setEditDebtsOpen(false);
|
|
@@ -513,6 +797,7 @@ export function OpportunitySummaryTab({
|
|
|
513
797
|
onOpenChange={setEditMainAboutOpen}
|
|
514
798
|
applicantLabel="Main Applicant"
|
|
515
799
|
initialData={mainAbout}
|
|
800
|
+
container={portalEl}
|
|
516
801
|
onSave={(data) => {
|
|
517
802
|
onMainAboutChange?.(data);
|
|
518
803
|
setEditMainAboutOpen(false);
|
|
@@ -524,6 +809,7 @@ export function OpportunitySummaryTab({
|
|
|
524
809
|
onOpenChange={setEditCoAboutOpen}
|
|
525
810
|
applicantLabel="Co-Applicant"
|
|
526
811
|
initialData={coAbout}
|
|
812
|
+
container={portalEl}
|
|
527
813
|
onSave={(data) => {
|
|
528
814
|
onCoAboutChange?.(data);
|
|
529
815
|
setEditCoAboutOpen(false);
|
|
@@ -535,6 +821,7 @@ export function OpportunitySummaryTab({
|
|
|
535
821
|
onOpenChange={setEditMainIncomeOpen}
|
|
536
822
|
applicantLabel="Main Applicant"
|
|
537
823
|
initialData={mainIncome}
|
|
824
|
+
container={portalEl}
|
|
538
825
|
onSave={(data) => {
|
|
539
826
|
onMainIncomeChange?.(data);
|
|
540
827
|
setEditMainIncomeOpen(false);
|
|
@@ -546,6 +833,7 @@ export function OpportunitySummaryTab({
|
|
|
546
833
|
onOpenChange={setEditCoIncomeOpen}
|
|
547
834
|
applicantLabel="Co-Applicant"
|
|
548
835
|
initialData={coIncome}
|
|
836
|
+
container={portalEl}
|
|
549
837
|
onSave={(data) => {
|
|
550
838
|
onCoIncomeChange?.(data);
|
|
551
839
|
setEditCoIncomeOpen(false);
|
|
@@ -557,6 +845,7 @@ export function OpportunitySummaryTab({
|
|
|
557
845
|
onOpenChange={setEditMainExpensesOpen}
|
|
558
846
|
applicantLabel="Main Applicant"
|
|
559
847
|
initialData={mainExpenses}
|
|
848
|
+
container={portalEl}
|
|
560
849
|
onSave={(data) => {
|
|
561
850
|
onMainExpensesChange?.(data);
|
|
562
851
|
setEditMainExpensesOpen(false);
|
|
@@ -568,12 +857,39 @@ export function OpportunitySummaryTab({
|
|
|
568
857
|
onOpenChange={setEditCoExpensesOpen}
|
|
569
858
|
applicantLabel="Co-Applicant"
|
|
570
859
|
initialData={coExpenses}
|
|
860
|
+
container={portalEl}
|
|
571
861
|
onSave={(data) => {
|
|
572
862
|
onCoExpensesChange?.(data);
|
|
573
863
|
setEditCoExpensesOpen(false);
|
|
574
864
|
}}
|
|
575
865
|
/>
|
|
576
866
|
)}
|
|
867
|
+
|
|
868
|
+
{/*
|
|
869
|
+
* Drawer scrim — dims the drawer content when any edit modal is open.
|
|
870
|
+
*
|
|
871
|
+
* Why this lives here (not inside the Dialog itself):
|
|
872
|
+
* The portalEl container div below is positioned LAST in the DOM so
|
|
873
|
+
* the Dialog overlay + popup paint on top of all drawer content.
|
|
874
|
+
* This scrim is rendered BEFORE portalEl so it is covered by the
|
|
875
|
+
* Dialog overlay (z-50) while itself sitting above drawer content
|
|
876
|
+
* (z-auto). Using `fixed inset-0` ensures it covers the full drawer
|
|
877
|
+
* viewport regardless of how far the user has scrolled.
|
|
878
|
+
*/}
|
|
879
|
+
{anyModalOpen && (
|
|
880
|
+
<div
|
|
881
|
+
className="fixed inset-0 z-[49] bg-foreground/50"
|
|
882
|
+
aria-hidden="true"
|
|
883
|
+
/>
|
|
884
|
+
)}
|
|
885
|
+
|
|
886
|
+
{/*
|
|
887
|
+
* Portal container — MUST be the last DOM element in this component.
|
|
888
|
+
* React portals render their output INTO this node, so placing it last
|
|
889
|
+
* ensures the Dialog overlay + popup paint on top of all drawer content
|
|
890
|
+
* and on top of the scrim above.
|
|
891
|
+
*/}
|
|
892
|
+
<div ref={setPortalEl} data-shadcn-scope="" />
|
|
577
893
|
</>
|
|
578
894
|
);
|
|
579
895
|
}
|