@wealthx/shadcn 1.2.2 → 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.
Files changed (229) hide show
  1. package/.turbo/turbo-build.log +200 -156
  2. package/CHANGELOG.md +22 -0
  3. package/dist/{chunk-4Y6R4WEC.mjs → chunk-2A5RRQGG.mjs} +9 -22
  4. package/dist/{chunk-TS2ZX2VS.mjs → chunk-2UM72RJ7.mjs} +11 -15
  5. package/dist/{chunk-A56YQQHG.mjs → chunk-3NCUZIFP.mjs} +2 -2
  6. package/dist/chunk-3OYFOX3X.mjs +79 -0
  7. package/dist/{chunk-RP3SQYA3.mjs → chunk-3TTACBDP.mjs} +9 -4
  8. package/dist/chunk-4GAWMKMI.mjs +710 -0
  9. package/dist/{chunk-VGSESELX.mjs → chunk-5FQIKDKP.mjs} +5 -5
  10. package/dist/{chunk-K3JYD4IU.mjs → chunk-5IS7G74I.mjs} +11 -4
  11. package/dist/chunk-6AW4KJHE.mjs +235 -0
  12. package/dist/chunk-6CR5N2JW.mjs +302 -0
  13. package/dist/{chunk-XIRTEFKH.mjs → chunk-6DZEXFNB.mjs} +36 -8
  14. package/dist/chunk-6O6KD7CE.mjs +271 -0
  15. package/dist/chunk-7PV3IWCN.mjs +33 -0
  16. package/dist/{chunk-SPJ5KXW7.mjs → chunk-7S5AESZO.mjs} +5 -5
  17. package/dist/{chunk-RYCLWMZ7.mjs → chunk-ABFDMHOR.mjs} +9 -7
  18. package/dist/{chunk-SWGT756Z.mjs → chunk-AMQZRHEZ.mjs} +10 -4
  19. package/dist/{chunk-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
  20. package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
  21. package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
  22. package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
  23. package/dist/chunk-DOEO3CDL.mjs +27 -0
  24. package/dist/{chunk-5MEWU56Z.mjs → chunk-DUJTAXMH.mjs} +11 -6
  25. package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
  26. package/dist/chunk-EWRB4PAD.mjs +468 -0
  27. package/dist/{chunk-ZSHYDDRB.mjs → chunk-FAKPBKLT.mjs} +6 -2
  28. package/dist/chunk-FNQXOAYJ.mjs +169 -0
  29. package/dist/{chunk-A6AAWBPF.mjs → chunk-GHC7LLUX.mjs} +13 -4
  30. package/dist/chunk-HBZLGDIN.mjs +507 -0
  31. package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
  32. package/dist/{chunk-CGH4DRNG.mjs → chunk-HVY6KCCF.mjs} +10 -7
  33. package/dist/chunk-I3RZS7V2.mjs +136 -0
  34. package/dist/chunk-IAE3F7DR.mjs +1962 -0
  35. package/dist/{chunk-UT4KJR7V.mjs → chunk-IHMFS7NZ.mjs} +35 -74
  36. package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
  37. package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
  38. package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
  39. package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
  40. package/dist/chunk-LV35NGVG.mjs +272 -0
  41. package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
  42. package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
  43. package/dist/chunk-MIZQHHUO.mjs +441 -0
  44. package/dist/chunk-MN5NYQCL.mjs +29 -0
  45. package/dist/chunk-NL3ZO62D.mjs +31 -0
  46. package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
  47. package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
  48. package/dist/chunk-PBL4OQV2.mjs +283 -0
  49. package/dist/{chunk-Y4QFWRNR.mjs → chunk-PU4YZQXV.mjs} +17 -18
  50. package/dist/chunk-QMY3AZJH.mjs +80 -0
  51. package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
  52. package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
  53. package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
  54. package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
  55. package/dist/chunk-UMTOX62O.mjs +415 -0
  56. package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
  57. package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
  58. package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
  59. package/dist/{chunk-ZRSDX6OW.mjs → chunk-ZC45IGZO.mjs} +33 -30
  60. package/dist/components/ui/add-column-modal.js +42 -14
  61. package/dist/components/ui/add-column-modal.mjs +5 -5
  62. package/dist/components/ui/add-lead-modal.js +42 -11
  63. package/dist/components/ui/add-lead-modal.mjs +3 -3
  64. package/dist/components/ui/advisor-card.js +497 -0
  65. package/dist/components/ui/advisor-card.mjs +13 -0
  66. package/dist/components/ui/ai-assistant-drawer.js +11 -10
  67. package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
  68. package/dist/components/ui/alert-dialog.js +2 -2
  69. package/dist/components/ui/alert-dialog.mjs +2 -2
  70. package/dist/components/ui/appointment-action-dialogs.js +1160 -0
  71. package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
  72. package/dist/components/ui/appointment-availability-settings.js +1590 -0
  73. package/dist/components/ui/appointment-availability-settings.mjs +23 -0
  74. package/dist/components/ui/appointment-book-dialog.js +1744 -0
  75. package/dist/components/ui/appointment-book-dialog.mjs +27 -0
  76. package/dist/components/ui/appointment-calendar-view.js +833 -0
  77. package/dist/components/ui/appointment-calendar-view.mjs +14 -0
  78. package/dist/components/ui/appointment-detail-sheet.js +1517 -0
  79. package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
  80. package/dist/components/ui/appointment-gmail-connect.js +467 -0
  81. package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
  82. package/dist/components/ui/appointment-mini-card.js +345 -0
  83. package/dist/components/ui/appointment-mini-card.mjs +11 -0
  84. package/dist/components/ui/appointment-time-slot-picker.js +311 -0
  85. package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
  86. package/dist/components/ui/appointment-upcoming-card.js +1268 -0
  87. package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
  88. package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
  89. package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
  90. package/dist/components/ui/backoffice-alerts-chart.js +786 -0
  91. package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
  92. package/dist/components/ui/backoffice-connections-chart.js +817 -0
  93. package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
  94. package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
  95. package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
  96. package/dist/components/ui/badge.js +6 -6
  97. package/dist/components/ui/badge.mjs +1 -1
  98. package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
  99. package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
  100. package/dist/components/ui/button.js +2 -2
  101. package/dist/components/ui/button.mjs +1 -1
  102. package/dist/components/ui/calendar.js +2 -2
  103. package/dist/components/ui/calendar.mjs +2 -2
  104. package/dist/components/ui/card.js +1 -1
  105. package/dist/components/ui/card.mjs +1 -1
  106. package/dist/components/ui/cash-balance-line-chart.js +31 -23
  107. package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
  108. package/dist/components/ui/cashflow-bar-chart.js +12 -5
  109. package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
  110. package/dist/components/ui/chip.js +97 -18
  111. package/dist/components/ui/chip.mjs +3 -2
  112. package/dist/components/ui/color-picker.js +158 -28
  113. package/dist/components/ui/color-picker.mjs +3 -1
  114. package/dist/components/ui/data-table.js +140 -119
  115. package/dist/components/ui/data-table.mjs +3 -2
  116. package/dist/components/ui/date-picker.js +48 -27
  117. package/dist/components/ui/date-picker.mjs +4 -3
  118. package/dist/components/ui/dialog.js +37 -9
  119. package/dist/components/ui/dialog.mjs +2 -2
  120. package/dist/components/ui/expense-bar-chart.js +12 -5
  121. package/dist/components/ui/expense-bar-chart.mjs +5 -4
  122. package/dist/components/ui/field.mjs +2 -2
  123. package/dist/components/ui/financial-cards.js +322 -155
  124. package/dist/components/ui/financial-cards.mjs +5 -3
  125. package/dist/components/ui/financial-drawers.js +2 -2
  126. package/dist/components/ui/financial-drawers.mjs +3 -3
  127. package/dist/components/ui/financial-sections.js +14 -10
  128. package/dist/components/ui/financial-sections.mjs +6 -5
  129. package/dist/components/ui/income-bar-chart.js +12 -5
  130. package/dist/components/ui/income-bar-chart.mjs +5 -4
  131. package/dist/components/ui/input-group.js +2 -2
  132. package/dist/components/ui/input-group.mjs +2 -2
  133. package/dist/components/ui/kanban-column.js +52 -44
  134. package/dist/components/ui/kanban-column.mjs +7 -5
  135. package/dist/components/ui/opportunity-card.js +52 -44
  136. package/dist/components/ui/opportunity-card.mjs +6 -4
  137. package/dist/components/ui/opportunity-edit-modals.js +1367 -1263
  138. package/dist/components/ui/opportunity-edit-modals.mjs +8 -8
  139. package/dist/components/ui/opportunity-summary-tab.js +2744 -2157
  140. package/dist/components/ui/opportunity-summary-tab.mjs +14 -14
  141. package/dist/components/ui/page-header.js +92 -0
  142. package/dist/components/ui/page-header.mjs +8 -0
  143. package/dist/components/ui/page-top-bar.js +88 -0
  144. package/dist/components/ui/page-top-bar.mjs +8 -0
  145. package/dist/components/ui/pagination.js +303 -19
  146. package/dist/components/ui/pagination.mjs +11 -4
  147. package/dist/components/ui/pipeline-board.js +205 -191
  148. package/dist/components/ui/pipeline-board.mjs +9 -7
  149. package/dist/components/ui/pipeline-dialogs.js +114 -65
  150. package/dist/components/ui/pipeline-dialogs.mjs +7 -6
  151. package/dist/components/ui/pipeline-primitives.js +6 -6
  152. package/dist/components/ui/pipeline-primitives.mjs +2 -2
  153. package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
  154. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
  155. package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
  156. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
  157. package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
  158. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
  159. package/dist/components/ui/sidebar-nav.js +234 -95
  160. package/dist/components/ui/sidebar-nav.mjs +4 -1
  161. package/dist/components/ui/stage-timeline.js +6 -6
  162. package/dist/components/ui/stage-timeline.mjs +3 -3
  163. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
  164. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
  165. package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
  166. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
  167. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
  168. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
  169. package/dist/index.js +12899 -9343
  170. package/dist/index.mjs +256 -190
  171. package/dist/styles.css +1 -1
  172. package/package.json +71 -1
  173. package/src/components/index.tsx +114 -9
  174. package/src/components/ui/add-column-modal.tsx +7 -7
  175. package/src/components/ui/add-lead-modal.tsx +6 -3
  176. package/src/components/ui/advisor-card.tsx +227 -0
  177. package/src/components/ui/ai-assistant-drawer.tsx +4 -3
  178. package/src/components/ui/appointment-action-dialogs.tsx +297 -0
  179. package/src/components/ui/appointment-availability-settings.tsx +645 -0
  180. package/src/components/ui/appointment-book-dialog.tsx +618 -0
  181. package/src/components/ui/appointment-calendar-view.tsx +510 -0
  182. package/src/components/ui/appointment-detail-sheet.tsx +415 -0
  183. package/src/components/ui/appointment-gmail-connect.tsx +188 -0
  184. package/src/components/ui/appointment-mini-card.tsx +104 -0
  185. package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
  186. package/src/components/ui/appointment-upcoming-card.tsx +635 -0
  187. package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
  188. package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
  189. package/src/components/ui/backoffice-connections-chart.tsx +339 -0
  190. package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
  191. package/src/components/ui/badge.tsx +12 -6
  192. package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
  193. package/src/components/ui/button.tsx +2 -2
  194. package/src/components/ui/card.tsx +1 -1
  195. package/src/components/ui/cash-balance-line-chart.tsx +4 -23
  196. package/src/components/ui/cashflow-bar-chart.tsx +9 -2
  197. package/src/components/ui/chart-shared.tsx +4 -11
  198. package/src/components/ui/chip.tsx +23 -19
  199. package/src/components/ui/color-picker.tsx +4 -2
  200. package/src/components/ui/data-table.tsx +28 -74
  201. package/src/components/ui/date-picker.tsx +42 -37
  202. package/src/components/ui/dialog.tsx +72 -6
  203. package/src/components/ui/expense-bar-chart.tsx +11 -2
  204. package/src/components/ui/financial-cards.tsx +99 -10
  205. package/src/components/ui/income-bar-chart.tsx +11 -2
  206. package/src/components/ui/opportunity-card.tsx +10 -39
  207. package/src/components/ui/opportunity-edit-modals.tsx +98 -36
  208. package/src/components/ui/opportunity-summary-tab.tsx +548 -232
  209. package/src/components/ui/page-header.tsx +57 -0
  210. package/src/components/ui/page-top-bar.tsx +48 -0
  211. package/src/components/ui/pagination.tsx +171 -22
  212. package/src/components/ui/pipeline-board.tsx +12 -5
  213. package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
  214. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
  215. package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
  216. package/src/components/ui/sidebar-nav.tsx +36 -37
  217. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
  218. package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
  219. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
  220. package/src/lib/format-currency.ts +44 -0
  221. package/src/lib/format-date.ts +50 -0
  222. package/src/lib/opportunity-constants.ts +12 -0
  223. package/src/styles/globals.css +17 -15
  224. package/src/styles/styles-css.ts +1 -1
  225. package/tsup.config.ts +14 -0
  226. package/dist/chunk-S4QRUQNW.mjs +0 -475
  227. package/dist/chunk-URGMJAE3.mjs +0 -1885
  228. package/dist/chunk-WNGWBVLV.mjs +0 -148
  229. 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
- Accordion,
7
- AccordionItem,
8
- AccordionTrigger,
9
- AccordionContent,
10
- } from "./accordion";
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
- LoanScenarioSection,
13
- FinancialBottomSummary,
14
- } from "./financial-sections";
15
- import { AboutCard, IncomeCard, ExpensesCard } from "./financial-cards";
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
- * - Loan Application accordion (shared, above applicant sub-tabs)
39
- * - Joint / Main / Co applicant sub-tabs (accordion sections per tab)
40
- * - All inline edit modals (managed internally)
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 inline inside an AccordionTrigger.
111
- * Uses stopPropagation so the click opens the edit modal without
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 TriggerEditButton({
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
- <button
164
+ <Button
123
165
  type="button"
124
- className="ml-auto mr-2 inline-flex size-6 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
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
- </button>
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
- // ApplicantAccordionTab (internal)
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 ApplicantAccordionTabProps {
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 ApplicantAccordionTab({
152
- accordionPrefix,
330
+ function ApplicantCardTab({
153
331
  about,
154
332
  income,
155
333
  expenses,
156
334
  onEditAbout,
157
335
  onEditIncome,
158
336
  onEditExpenses,
159
- }: ApplicantAccordionTabProps) {
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
- <Accordion
175
- openMultiple
176
- defaultValue={[
177
- `${accordionPrefix}-about`,
178
- `${accordionPrefix}-income`,
179
- `${accordionPrefix}-expenses`,
180
- ]}
181
- >
182
- <AccordionItem value={`${accordionPrefix}-about`}>
183
- <AccordionTrigger>
184
- About
185
- <TriggerEditButton onClick={onEditAbout} />
186
- </AccordionTrigger>
187
- <AccordionContent className="text-foreground">
188
- <AboutCard
189
- title={about.title}
190
- firstName={about.firstName}
191
- lastName={about.lastName}
192
- phone={about.phone}
193
- email={about.email}
194
- gender={about.gender}
195
- maritalStatus={about.maritalStatus}
196
- citizenStatus={about.citizenStatus}
197
- propertyInTrust={about.propertyInTrust}
198
- companyOwnership={about.companyOwnership}
199
- />
200
- </AccordionContent>
201
- </AccordionItem>
202
-
203
- <AccordionItem value={`${accordionPrefix}-income`}>
204
- <AccordionTrigger>
205
- Income
206
- <TriggerEditButton onClick={onEditIncome} />
207
- </AccordionTrigger>
208
- <AccordionContent className="text-foreground">
209
- <IncomeCard
210
- items={income.items.map((i) => ({
211
- incomeType: i.incomeType,
212
- jobTitle: i.jobTitle,
213
- companyName: i.companyName,
214
- amountLabel: `${formatCurrency(i.incomeAmount)} / ${i.frequency}`,
215
- }))}
216
- totalMonthly={totalMonthlyIncome}
217
- />
218
- </AccordionContent>
219
- </AccordionItem>
220
-
221
- <AccordionItem value={`${accordionPrefix}-expenses`}>
222
- <AccordionTrigger>
223
- Expenses
224
- <TriggerEditButton onClick={onEditExpenses} />
225
- </AccordionTrigger>
226
- <AccordionContent className="text-foreground">
227
- <ExpensesCard
228
- items={expenses.items.map((e) => ({
229
- expenseType: e.expenseType,
230
- amountLabel: `${formatCurrency(e.amount)} / ${e.frequency}`,
231
- }))}
232
- totalMonthly={totalMonthlyExpenses}
233
- />
234
- </AccordionContent>
235
- </AccordionItem>
236
- </Accordion>
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
- // ── Sub-tab state ──────────────────────────────────────────────────────────
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
- {/* ── Loan Applicationshared section, sits above applicant sub-tabs ── */}
309
- <Accordion openMultiple defaultValue={["loan-app"]}>
310
- <AccordionItem value="loan-app">
311
- <AccordionTrigger>
312
- Loan Application
313
- <TriggerEditButton onClick={() => setEditLoanOpen(true)} />
314
- </AccordionTrigger>
315
- <AccordionContent className="text-foreground">
316
- <LoanScenarioSection
317
- lendingType="Home Loan"
318
- purposeOfLoan={loanScenario.loanPurpose}
319
- loanAmount={formatCurrency(loanScenario.loanAmount)}
320
- propertyEstimate={formatCurrency(loanScenario.propertyEstimate)}
321
- estLvr=""
322
- cashDeposit={formatCurrency(loanScenario.cashEquity)}
323
- propertyAddress="—"
324
- duration={`${loanScenario.loanDuration} years`}
325
- importantFeatures={
512
+ {/* ── Hero Bandalways 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
- <div className="mt-4 flex gap-4">
340
- {[
341
- { label: "Number of Applicants", value: isJoint ? "2" : "1" },
342
- { label: "Dependants", value: "1" },
343
- ].map(({ label, value }) => (
344
- <div key={label} className="flex flex-col gap-1">
345
- <span className="text-label-small uppercase text-muted-foreground">
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 variant="line">
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: combined data from Main + Co ── */}
375
- <TabsContent value="joint">
376
- <Accordion
377
- openMultiple
378
- defaultValue={["joint-overview", "joint-documents"]}
379
- >
380
- {/* Financial Overview — cashflow + assets + net position */}
381
- <AccordionItem value="joint-overview">
382
- <AccordionTrigger>
383
- Financial Overview
384
- <div className="ml-auto mr-2 flex items-center gap-0.5">
385
- <button
386
- type="button"
387
- className="inline-flex size-6 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
388
- onClick={(e) => {
389
- e.stopPropagation();
390
- setEditAssetsOpen(true);
391
- }}
392
- aria-label="Edit Assets"
393
- title="Edit Assets"
394
- >
395
- <Pencil className="size-3.5" />
396
- </button>
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
- </AccordionTrigger>
411
- <AccordionContent className="text-foreground">
412
- <FinancialBottomSummary
413
- cashflowItems={[
414
- {
415
- label: "Monthly Combined Income",
416
- value: formatCurrency(Math.round(combinedMonthlyIncome)),
417
- },
418
- {
419
- label: "Monthly Combined Expenses",
420
- value: formatCurrency(
421
- Math.round(combinedMonthlyExpenses),
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
- destructive: true,
424
- },
425
- ]}
426
- netSurplus={formatCurrency(Math.abs(Math.round(netSurplus)))}
427
- netSurplusDestructive={netSurplus < 0}
428
- assetItems={assets.map((a) => ({
429
- label: a.assetType,
430
- value: formatCurrency(a.value),
431
- }))}
432
- totalAssets={totalAssets}
433
- totalLiabilities={totalDebts}
434
- netPosition={formatCurrency(
435
- Math.abs(Math.round(totalAssetsAmount - totalDebtsAmount)),
436
- )}
437
- netPositionDestructive={totalAssetsAmount < totalDebtsAmount}
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 &amp; Loans
722
+ </FinancialSectionLabel>
723
+ <SectionEditButton
724
+ onClick={() => setEditDebtsOpen(true)}
725
+ title="Edit Debts"
438
726
  />
439
- </AccordionContent>
440
- </AccordionItem>
441
-
442
- {/* Documents */}
443
- <AccordionItem value="joint-documents">
444
- <AccordionTrigger>Documents</AccordionTrigger>
445
- <AccordionContent>
446
- <p className="text-sm text-muted-foreground">
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
- <ApplicantAccordionTab
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
- <ApplicantAccordionTab
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 (internal state) ── */}
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
  }