@wealthx/shadcn 1.0.2 → 1.2.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 (300) hide show
  1. package/.turbo/turbo-build.log +235 -138
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +82 -0
  4. package/dist/{chunk-6OJF6XRN.mjs → chunk-24FUO7TD.mjs} +4 -8
  5. package/dist/{chunk-4AJ5HWHD.mjs → chunk-2I5S2AMY.mjs} +3 -3
  6. package/dist/chunk-2SF672SZ.mjs +161 -0
  7. package/dist/{chunk-GPRJQ24C.mjs → chunk-34NWQURD.mjs} +2 -2
  8. package/dist/{chunk-MQ72DIBH.mjs → chunk-3GF7OVTP.mjs} +14 -5
  9. package/dist/chunk-3WMX6KWS.mjs +245 -0
  10. package/dist/{chunk-PMKODV6M.mjs → chunk-462HMNO4.mjs} +6 -10
  11. package/dist/chunk-4CX4SBRO.mjs +153 -0
  12. package/dist/chunk-4MN6UQHG.mjs +443 -0
  13. package/dist/chunk-5QQVZTVZ.mjs +233 -0
  14. package/dist/{chunk-BGP2N52Z.mjs → chunk-66MI7Q4B.mjs} +5 -5
  15. package/dist/chunk-6FCGKSZX.mjs +268 -0
  16. package/dist/{chunk-CGOKTPXU.mjs → chunk-6JQFUE5I.mjs} +20 -23
  17. package/dist/{chunk-Z3MK2KKZ.mjs → chunk-7DHU4VGG.mjs} +7 -3
  18. package/dist/{chunk-VZ2NR7L3.mjs → chunk-7PYJD5JI.mjs} +35 -27
  19. package/dist/{chunk-JU2RUWHF.mjs → chunk-7XJHLGUV.mjs} +1 -1
  20. package/dist/{chunk-BMFN37JH.mjs → chunk-7YAU5CY6.mjs} +1 -1
  21. package/dist/chunk-A56YQQHG.mjs +402 -0
  22. package/dist/chunk-AH52LG6N.mjs +315 -0
  23. package/dist/{chunk-SLWCCURD.mjs → chunk-CLIN5525.mjs} +8 -4
  24. package/dist/{chunk-3VQNJ235.mjs → chunk-CSDO6VBW.mjs} +7 -0
  25. package/dist/chunk-D4ILTPOG.mjs +293 -0
  26. package/dist/{chunk-HS7TFG7V.mjs → chunk-D6ID6M4V.mjs} +1 -1
  27. package/dist/chunk-DOH3EHX7.mjs +378 -0
  28. package/dist/{chunk-MJIEMGRD.mjs → chunk-EFRENWEJ.mjs} +9 -17
  29. package/dist/chunk-ERGGHC2V.mjs +185 -0
  30. package/dist/{chunk-OXQQNQZI.mjs → chunk-FEZKMUCF.mjs} +10 -1
  31. package/dist/{chunk-55CEW76V.mjs → chunk-FH6QVUVZ.mjs} +1 -1
  32. package/dist/chunk-FMAXJ2SI.mjs +71 -0
  33. package/dist/chunk-FZIXGLMV.mjs +173 -0
  34. package/dist/{chunk-DS2AMHN2.mjs → chunk-GYMYRIZP.mjs} +2 -2
  35. package/dist/{chunk-KQDD5MU3.mjs → chunk-H45TKD34.mjs} +5 -5
  36. package/dist/{chunk-BBJBJSXQ.mjs → chunk-J5UICVJS.mjs} +1 -1
  37. package/dist/{chunk-RL772EH7.mjs → chunk-JHJHG4GO.mjs} +4 -12
  38. package/dist/chunk-KMCGSZTX.mjs +177 -0
  39. package/dist/{chunk-FHNT55I5.mjs → chunk-KUDCQ4FI.mjs} +4 -4
  40. package/dist/chunk-LE6YFY6D.mjs +209 -0
  41. package/dist/{chunk-HUVTPUV2.mjs → chunk-LLVQKSU3.mjs} +23 -19
  42. package/dist/{chunk-KKHTJNMM.mjs → chunk-MARPPFOJ.mjs} +8 -4
  43. package/dist/{chunk-6AFMNC42.mjs → chunk-N2PT566P.mjs} +15 -11
  44. package/dist/chunk-NLCKVHWB.mjs +161 -0
  45. package/dist/{chunk-YN5SYTOO.mjs → chunk-NQPOYKAQ.mjs} +9 -5
  46. package/dist/{chunk-ZZV5JVNW.mjs → chunk-NSLMILBT.mjs} +3 -7
  47. package/dist/chunk-NXA3CZ7A.mjs +248 -0
  48. package/dist/chunk-OGOYQ7BG.mjs +150 -0
  49. package/dist/{chunk-3NQGYJEZ.mjs → chunk-P6AM5V7O.mjs} +10 -18
  50. package/dist/{chunk-CZ3BW5GL.mjs → chunk-P76HMUI6.mjs} +5 -11
  51. package/dist/chunk-PCPLO5HT.mjs +671 -0
  52. package/dist/chunk-PG6K5XEC.mjs +475 -0
  53. package/dist/chunk-PJHPSRYD.mjs +234 -0
  54. package/dist/{chunk-DDPA2XXS.mjs → chunk-PMB3A7V3.mjs} +2 -2
  55. package/dist/chunk-PR6V5XKM.mjs +209 -0
  56. package/dist/{chunk-46OFHMQA.mjs → chunk-Q76O3RIQ.mjs} +10 -6
  57. package/dist/chunk-QVKWW6KE.mjs +272 -0
  58. package/dist/chunk-RGU7HOEC.mjs +140 -0
  59. package/dist/{chunk-JF4PHPD5.mjs → chunk-RGVKLTLH.mjs} +4 -4
  60. package/dist/{chunk-VG6UF6UT.mjs → chunk-RP3SQYA3.mjs} +2 -2
  61. package/dist/chunk-RRBS6D63.mjs +163 -0
  62. package/dist/chunk-SMQ3DG25.mjs +285 -0
  63. package/dist/chunk-SPJ5KXW7.mjs +199 -0
  64. package/dist/chunk-SYOD63OZ.mjs +225 -0
  65. package/dist/chunk-UFYSFDER.mjs +42 -0
  66. package/dist/chunk-VACKZOMY.mjs +190 -0
  67. package/dist/chunk-VLQZANBF.mjs +42 -0
  68. package/dist/chunk-WA6O6EUR.mjs +1885 -0
  69. package/dist/{chunk-E3K6O4FZ.mjs → chunk-WAZD7NFU.mjs} +5 -2
  70. package/dist/chunk-WG6JGJXB.mjs +165 -0
  71. package/dist/{chunk-I64K754C.mjs → chunk-WNGWBVLV.mjs} +2 -2
  72. package/dist/{chunk-3U7SD3MS.mjs → chunk-WOEHFRGB.mjs} +3 -3
  73. package/dist/{chunk-DKZRJOMF.mjs → chunk-XIRTEFKH.mjs} +12 -12
  74. package/dist/chunk-Y6DWJSKZ.mjs +79 -0
  75. package/dist/chunk-YKPROFLB.mjs +161 -0
  76. package/dist/{chunk-K76E2TQU.mjs → chunk-ZRO5JO3H.mjs} +107 -67
  77. package/dist/{chunk-VYMHBV6D.mjs → chunk-ZU4NV6RG.mjs} +5 -3
  78. package/dist/components/ui/accordion.js +40 -4
  79. package/dist/components/ui/accordion.mjs +2 -2
  80. package/dist/components/ui/add-column-modal.js +789 -0
  81. package/dist/components/ui/add-column-modal.mjs +17 -0
  82. package/dist/components/ui/add-lead-modal.js +647 -0
  83. package/dist/components/ui/add-lead-modal.mjs +16 -0
  84. package/dist/components/ui/ai-assistant-drawer.js +686 -0
  85. package/dist/components/ui/ai-assistant-drawer.mjs +16 -0
  86. package/dist/components/ui/alert-dialog.js +37 -5
  87. package/dist/components/ui/alert-dialog.mjs +4 -4
  88. package/dist/components/ui/alert.js +37 -11
  89. package/dist/components/ui/alert.mjs +2 -2
  90. package/dist/components/ui/avatar.js +36 -8
  91. package/dist/components/ui/avatar.mjs +2 -2
  92. package/dist/components/ui/backoffice-alert-history-chart.js +624 -0
  93. package/dist/components/ui/backoffice-alert-history-chart.mjs +16 -0
  94. package/dist/components/ui/backoffice-contact-history-chart.js +687 -0
  95. package/dist/components/ui/backoffice-contact-history-chart.mjs +16 -0
  96. package/dist/components/ui/badge.js +37 -2
  97. package/dist/components/ui/badge.mjs +2 -2
  98. package/dist/components/ui/borrowing-capacity-line-chart.js +639 -0
  99. package/dist/components/ui/borrowing-capacity-line-chart.mjs +16 -0
  100. package/dist/components/ui/button.js +35 -3
  101. package/dist/components/ui/button.mjs +2 -2
  102. package/dist/components/ui/calendar.js +43 -19
  103. package/dist/components/ui/calendar.mjs +3 -3
  104. package/dist/components/ui/card.js +40 -4
  105. package/dist/components/ui/card.mjs +2 -2
  106. package/dist/components/ui/cash-balance-line-chart.js +627 -0
  107. package/dist/components/ui/cash-balance-line-chart.mjs +16 -0
  108. package/dist/components/ui/cashflow-bar-chart.js +650 -0
  109. package/dist/components/ui/cashflow-bar-chart.mjs +16 -0
  110. package/dist/components/ui/checkbox.js +36 -5
  111. package/dist/components/ui/checkbox.mjs +2 -3
  112. package/dist/components/ui/chip.js +37 -2
  113. package/dist/components/ui/chip.mjs +3 -3
  114. package/dist/components/ui/combobox.js +280 -0
  115. package/dist/components/ui/combobox.mjs +28 -0
  116. package/dist/components/ui/data-table.js +160 -88
  117. package/dist/components/ui/data-table.mjs +10 -11
  118. package/dist/components/ui/date-picker.js +44 -20
  119. package/dist/components/ui/date-picker.mjs +6 -7
  120. package/dist/components/ui/dialog.js +44 -12
  121. package/dist/components/ui/dialog.mjs +4 -4
  122. package/dist/components/ui/drawer.js +46 -10
  123. package/dist/components/ui/drawer.mjs +3 -3
  124. package/dist/components/ui/dropdown-menu.js +40 -16
  125. package/dist/components/ui/dropdown-menu.mjs +3 -3
  126. package/dist/components/ui/empty.js +41 -5
  127. package/dist/components/ui/empty.mjs +2 -2
  128. package/dist/components/ui/expense-bar-chart.js +642 -0
  129. package/dist/components/ui/expense-bar-chart.mjs +16 -0
  130. package/dist/components/ui/field.js +53 -21
  131. package/dist/components/ui/field.mjs +4 -4
  132. package/dist/components/ui/financial-cards.js +1002 -0
  133. package/dist/components/ui/financial-cards.mjs +24 -0
  134. package/dist/components/ui/financial-drawers.js +637 -0
  135. package/dist/components/ui/financial-drawers.mjs +17 -0
  136. package/dist/components/ui/financial-primitives.js +218 -0
  137. package/dist/components/ui/financial-primitives.mjs +22 -0
  138. package/dist/components/ui/financial-sections.js +1422 -0
  139. package/dist/components/ui/financial-sections.mjs +30 -0
  140. package/dist/components/ui/form-primitives.js +682 -0
  141. package/dist/components/ui/form-primitives.mjs +19 -0
  142. package/dist/components/ui/income-bar-chart.js +641 -0
  143. package/dist/components/ui/income-bar-chart.mjs +16 -0
  144. package/dist/components/ui/input-group.js +43 -7
  145. package/dist/components/ui/input-group.mjs +5 -5
  146. package/dist/components/ui/input-otp.js +39 -3
  147. package/dist/components/ui/input-otp.mjs +2 -2
  148. package/dist/components/ui/input.js +34 -2
  149. package/dist/components/ui/input.mjs +2 -2
  150. package/dist/components/ui/kanban-column.js +1143 -0
  151. package/dist/components/ui/kanban-column.mjs +20 -0
  152. package/dist/components/ui/label.js +35 -7
  153. package/dist/components/ui/label.mjs +2 -2
  154. package/dist/components/ui/opportunity-card.js +960 -0
  155. package/dist/components/ui/opportunity-card.mjs +20 -0
  156. package/dist/components/ui/opportunity-edit-modals.js +3360 -0
  157. package/dist/components/ui/opportunity-edit-modals.mjs +37 -0
  158. package/dist/components/ui/opportunity-summary-tab.js +4365 -0
  159. package/dist/components/ui/opportunity-summary-tab.mjs +34 -0
  160. package/dist/components/ui/pagination.js +35 -3
  161. package/dist/components/ui/pagination.mjs +3 -3
  162. package/dist/components/ui/pipeline-alerts.js +103 -0
  163. package/dist/components/ui/pipeline-alerts.mjs +8 -0
  164. package/dist/components/ui/pipeline-board.js +1408 -0
  165. package/dist/components/ui/pipeline-board.mjs +24 -0
  166. package/dist/components/ui/pipeline-chart.js +216 -0
  167. package/dist/components/ui/pipeline-chart.mjs +10 -0
  168. package/dist/components/ui/pipeline-dialogs.js +1183 -0
  169. package/dist/components/ui/pipeline-dialogs.mjs +23 -0
  170. package/dist/components/ui/pipeline-primitives.js +300 -0
  171. package/dist/components/ui/pipeline-primitives.mjs +11 -0
  172. package/dist/components/ui/popover.js +45 -4
  173. package/dist/components/ui/popover.mjs +3 -3
  174. package/dist/components/ui/progress.js +33 -1
  175. package/dist/components/ui/progress.mjs +2 -2
  176. package/dist/components/ui/property-cashflow-doughnut-chart.js +523 -0
  177. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +16 -0
  178. package/dist/components/ui/property-debt-equity-doughnut-chart.js +521 -0
  179. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +16 -0
  180. package/dist/components/ui/property-mobile-estimate-line-chart.js +682 -0
  181. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +16 -0
  182. package/dist/components/ui/radio-group.js +33 -1
  183. package/dist/components/ui/radio-group.mjs +2 -2
  184. package/dist/components/ui/select.js +66 -26
  185. package/dist/components/ui/select.mjs +3 -3
  186. package/dist/components/ui/separator.js +33 -1
  187. package/dist/components/ui/separator.mjs +2 -2
  188. package/dist/components/ui/sheet.js +37 -9
  189. package/dist/components/ui/sheet.mjs +3 -3
  190. package/dist/components/ui/skeleton.js +33 -1
  191. package/dist/components/ui/skeleton.mjs +2 -2
  192. package/dist/components/ui/slider.js +86 -102
  193. package/dist/components/ui/slider.mjs +2 -2
  194. package/dist/components/ui/spinner.js +33 -1
  195. package/dist/components/ui/spinner.mjs +2 -2
  196. package/dist/components/ui/stage-timeline.js +579 -0
  197. package/dist/components/ui/stage-timeline.mjs +15 -0
  198. package/dist/components/ui/switch.js +37 -4
  199. package/dist/components/ui/switch.mjs +2 -3
  200. package/dist/components/ui/table.js +37 -5
  201. package/dist/components/ui/table.mjs +2 -2
  202. package/dist/components/ui/tabs.js +36 -12
  203. package/dist/components/ui/tabs.mjs +2 -2
  204. package/dist/components/ui/textarea.js +34 -2
  205. package/dist/components/ui/textarea.mjs +2 -2
  206. package/dist/components/ui/toggle-group.js +35 -4
  207. package/dist/components/ui/toggle-group.mjs +3 -4
  208. package/dist/components/ui/toggle.js +35 -4
  209. package/dist/components/ui/toggle.mjs +2 -3
  210. package/dist/components/ui/tooltip.js +51 -22
  211. package/dist/components/ui/tooltip.mjs +3 -3
  212. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +528 -0
  213. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +16 -0
  214. package/dist/components/ui/transactions-income-expense-bar-chart.js +516 -0
  215. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +16 -0
  216. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +528 -0
  217. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +16 -0
  218. package/dist/index.js +11613 -2868
  219. package/dist/index.mjs +377 -164
  220. package/dist/lib/theme-provider.js +10 -1
  221. package/dist/lib/theme-provider.mjs +1 -1
  222. package/dist/lib/typography.js +8 -0
  223. package/dist/lib/typography.mjs +3 -1
  224. package/dist/lib/utils.js +33 -1
  225. package/dist/lib/utils.mjs +1 -1
  226. package/dist/styles.css +1 -1
  227. package/package.json +169 -6
  228. package/src/components/index.tsx +323 -13
  229. package/src/components/ui/accordion.tsx +6 -3
  230. package/src/components/ui/add-column-modal.tsx +339 -0
  231. package/src/components/ui/add-lead-modal.tsx +290 -0
  232. package/src/components/ui/ai-assistant-drawer.tsx +408 -0
  233. package/src/components/ui/alert-dialog.tsx +80 -54
  234. package/src/components/ui/alert.tsx +28 -28
  235. package/src/components/ui/avatar.tsx +30 -29
  236. package/src/components/ui/backoffice-alert-history-chart.tsx +260 -0
  237. package/src/components/ui/backoffice-contact-history-chart.tsx +325 -0
  238. package/src/components/ui/badge.tsx +17 -15
  239. package/src/components/ui/borrowing-capacity-line-chart.tsx +357 -0
  240. package/src/components/ui/button.tsx +30 -27
  241. package/src/components/ui/calendar.tsx +53 -67
  242. package/src/components/ui/card.tsx +27 -24
  243. package/src/components/ui/cash-balance-line-chart.tsx +302 -0
  244. package/src/components/ui/cashflow-bar-chart.tsx +363 -0
  245. package/src/components/ui/chart-shared.tsx +261 -0
  246. package/src/components/ui/checkbox.tsx +30 -26
  247. package/src/components/ui/combobox.tsx +223 -0
  248. package/src/components/ui/data-table.tsx +160 -99
  249. package/src/components/ui/date-picker.tsx +0 -2
  250. package/src/components/ui/dialog.tsx +70 -60
  251. package/src/components/ui/drawer.tsx +57 -48
  252. package/src/components/ui/dropdown-menu.tsx +90 -82
  253. package/src/components/ui/empty.tsx +31 -27
  254. package/src/components/ui/expense-bar-chart.tsx +296 -0
  255. package/src/components/ui/field.tsx +70 -62
  256. package/src/components/ui/financial-cards.tsx +830 -0
  257. package/src/components/ui/financial-drawers.tsx +339 -0
  258. package/src/components/ui/financial-primitives.tsx +331 -0
  259. package/src/components/ui/financial-sections.tsx +672 -0
  260. package/src/components/ui/form-primitives.tsx +536 -0
  261. package/src/components/ui/income-bar-chart.tsx +297 -0
  262. package/src/components/ui/input-group.tsx +41 -34
  263. package/src/components/ui/input-otp.tsx +29 -24
  264. package/src/components/ui/input.tsx +8 -8
  265. package/src/components/ui/kanban-column.tsx +333 -0
  266. package/src/components/ui/label.tsx +9 -12
  267. package/src/components/ui/opportunity-card.tsx +616 -0
  268. package/src/components/ui/opportunity-edit-modals.tsx +2528 -0
  269. package/src/components/ui/opportunity-summary-tab.tsx +579 -0
  270. package/src/components/ui/pipeline-alerts.tsx +74 -0
  271. package/src/components/ui/pipeline-board.tsx +268 -0
  272. package/src/components/ui/pipeline-chart.tsx +173 -0
  273. package/src/components/ui/pipeline-dialogs.tsx +303 -0
  274. package/src/components/ui/pipeline-primitives.tsx +108 -0
  275. package/src/components/ui/popover.tsx +41 -36
  276. package/src/components/ui/property-cashflow-doughnut-chart.tsx +188 -0
  277. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +185 -0
  278. package/src/components/ui/property-mobile-estimate-line-chart.tsx +393 -0
  279. package/src/components/ui/select.tsx +65 -52
  280. package/src/components/ui/sheet.tsx +55 -52
  281. package/src/components/ui/slider.tsx +54 -77
  282. package/src/components/ui/stage-timeline.tsx +205 -0
  283. package/src/components/ui/switch.tsx +42 -29
  284. package/src/components/ui/table.tsx +28 -28
  285. package/src/components/ui/tabs.tsx +22 -28
  286. package/src/components/ui/textarea.tsx +8 -8
  287. package/src/components/ui/toggle-group.tsx +0 -2
  288. package/src/components/ui/toggle.tsx +13 -15
  289. package/src/components/ui/tooltip.tsx +30 -28
  290. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +191 -0
  291. package/src/components/ui/transactions-income-expense-bar-chart.tsx +205 -0
  292. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +191 -0
  293. package/src/lib/theme-provider.tsx +10 -0
  294. package/src/lib/typography.ts +9 -0
  295. package/src/lib/utils.ts +41 -3
  296. package/src/styles/globals.css +371 -124
  297. package/src/styles/styles-css.ts +1 -1
  298. package/tsup.config.ts +32 -0
  299. package/dist/chunk-K74JRTJR.mjs +0 -105
  300. package/dist/chunk-V7CNWJT3.mjs +0 -10
@@ -0,0 +1,2528 @@
1
+ import * as React from "react";
2
+ import { ChevronDownIcon, Plus, Trash2 } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogFooter,
10
+ } from "@/components/ui/dialog";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Label } from "@/components/ui/label";
14
+ import { Textarea } from "@/components/ui/textarea";
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from "@/components/ui/select";
22
+ import {
23
+ Accordion,
24
+ AccordionItem,
25
+ AccordionContent,
26
+ } from "@/components/ui/accordion";
27
+ import { Checkbox } from "@/components/ui/checkbox";
28
+ import { DatePicker } from "@/components/ui/date-picker";
29
+ import { Slider } from "@/components/ui/slider";
30
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
31
+ import {
32
+ AddressAutocomplete,
33
+ CurrencyInputWithSlider,
34
+ OwnershipSplit,
35
+ } from "@/components/ui/form-primitives";
36
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
37
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
38
+
39
+ /**
40
+ * Opportunity Edit Modals — WealthX DS (L4)
41
+ *
42
+ * Six focused edit dialogs for the Opportunity Details Drawer Summary tab:
43
+ * - EditLoanScenarioModal — edit loan quiz fields
44
+ * - EditAssetsModal — add/edit/remove asset line items
45
+ * - EditDebtsModal — add/edit/remove debt line items
46
+ * - EditAboutApplicantModal — personal details (name, gender, phone, email…)
47
+ * - EditIncomeModal — income fields per applicant
48
+ * - EditExpensesModal — expense breakdown per applicant
49
+ *
50
+ * All modals are fully controlled. Each accepts initial data and fires
51
+ * `onSave` with the updated payload when confirmed.
52
+ */
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Constants
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const LOAN_PURPOSES = [
59
+ "Purchase a home",
60
+ "Refinance my home loan",
61
+ "Buy an investment property",
62
+ "Refinance investment loan",
63
+ "Renovate/build",
64
+ "Equity release",
65
+ ];
66
+
67
+ const ASSET_TYPES = [
68
+ "Primary Residence",
69
+ "Investment Property",
70
+ "Holiday Home",
71
+ "Commercial Property",
72
+ "Rural Property",
73
+ "Cash & Savings",
74
+ "Term Deposit",
75
+ "Shares / ETFs",
76
+ "Managed Funds",
77
+ "Superannuation",
78
+ "Motor Vehicle",
79
+ "Business Equity",
80
+ "Cryptocurrency",
81
+ "Personal Belongings",
82
+ "Life Insurance",
83
+ "Trust Assets",
84
+ "SMSF",
85
+ "Bonds / Fixed Income",
86
+ "Other",
87
+ ];
88
+
89
+ const PROPERTY_ASSET_TYPES = new Set([
90
+ "Primary Residence",
91
+ "Investment Property",
92
+ "Holiday Home",
93
+ "Commercial Property",
94
+ "Rural Property",
95
+ ]);
96
+
97
+ const PROPERTY_SUBTYPES = [
98
+ "House",
99
+ "Unit / Apartment",
100
+ "Townhouse",
101
+ "Land",
102
+ "Rural / Farm",
103
+ "Commercial",
104
+ ];
105
+
106
+ const PROPERTY_USED_AS = ["Owner Occupied", "Investment", "Vacant Land"];
107
+
108
+ const FINANCIAL_ACCOUNT_ASSET_TYPES = new Set([
109
+ "Cash & Savings",
110
+ "Term Deposit",
111
+ ]);
112
+
113
+ const INVESTMENT_ASSET_TYPES = new Set([
114
+ "Shares / ETFs",
115
+ "Managed Funds",
116
+ "Bonds / Fixed Income",
117
+ "Cryptocurrency",
118
+ ]);
119
+
120
+ const SUPER_ASSET_TYPES = new Set(["Superannuation", "SMSF"]);
121
+
122
+ // Fields shown per asset type category
123
+ function assetFields(type: string) {
124
+ return {
125
+ isProperty: PROPERTY_ASSET_TYPES.has(type),
126
+ isVehicle: type === "Motor Vehicle",
127
+ isFinancialAccount: FINANCIAL_ACCOUNT_ASSET_TYPES.has(type),
128
+ isInvestment: INVESTMENT_ASSET_TYPES.has(type),
129
+ isSuper: SUPER_ASSET_TYPES.has(type),
130
+ isBusiness: type === "Business Equity",
131
+ isInsurance: type === "Life Insurance",
132
+ };
133
+ }
134
+
135
+ const PROPERTY_LOAN_DEBT_TYPES = new Set([
136
+ "Home Loan (Owner Occupied)",
137
+ "Home Loan (Investment)",
138
+ "Construction Loan",
139
+ ]);
140
+
141
+ const GENERAL_LOAN_DEBT_TYPES = new Set([
142
+ "Personal Loan",
143
+ "Business Loan",
144
+ "Overdraft",
145
+ "Line of Credit",
146
+ "Guarantor Liability",
147
+ ]);
148
+
149
+ const CARD_DEBT_TYPES = new Set([
150
+ "Credit Card",
151
+ "Store Card",
152
+ "Buy Now Pay Later",
153
+ ]);
154
+
155
+ const VEHICLE_DEBT_TYPES = new Set(["Car Loan", "Vehicle Lease"]);
156
+
157
+ function debtFields(type: string) {
158
+ return {
159
+ isPropertyLoan: PROPERTY_LOAN_DEBT_TYPES.has(type),
160
+ isGeneralLoan: GENERAL_LOAN_DEBT_TYPES.has(type),
161
+ isCard: CARD_DEBT_TYPES.has(type),
162
+ isVehicle: VEHICLE_DEBT_TYPES.has(type),
163
+ isHecs: type === "HECS / Student Debt",
164
+ isTax: type === "Tax Debt",
165
+ isGuarantor: type === "Guarantor Liability",
166
+ };
167
+ }
168
+
169
+ const DEBT_TYPES = [
170
+ "Home Loan (Owner Occupied)",
171
+ "Home Loan (Investment)",
172
+ "Construction Loan",
173
+ "Personal Loan",
174
+ "Car Loan",
175
+ "Credit Card",
176
+ "Store Card",
177
+ "HECS / Student Debt",
178
+ "Business Loan",
179
+ "Overdraft",
180
+ "Tax Debt",
181
+ "Buy Now Pay Later",
182
+ "Vehicle Lease",
183
+ "Guarantor Liability",
184
+ "Line of Credit",
185
+ "Other",
186
+ ];
187
+
188
+ const TITLE_OPTIONS = ["Mr", "Mrs", "Ms", "Dr", "Prof"];
189
+
190
+ const GENDER_OPTIONS = ["Male", "Female", "Non-binary", "Prefer not to say"];
191
+
192
+ const MARITAL_OPTIONS = [
193
+ "Single",
194
+ "Married",
195
+ "De facto",
196
+ "Divorced",
197
+ "Widowed",
198
+ ];
199
+
200
+ const CITIZEN_OPTIONS = [
201
+ "Australian Citizen",
202
+ "Permanent Resident",
203
+ "Temporary Resident",
204
+ "Foreign National",
205
+ ];
206
+
207
+ const RESIDENTIAL_STATUS_OPTIONS = [
208
+ "Own",
209
+ "Renting",
210
+ "Boarding",
211
+ "With Parents",
212
+ "Other",
213
+ ];
214
+
215
+ const PROPERTY_IN_TRUST_OPTIONS = ["None", "Yes - Discretionary", "Yes - Unit"];
216
+
217
+ const COMPANY_OWNERSHIP_OPTIONS = [
218
+ "None",
219
+ "Yes - Director",
220
+ "Yes - Shareholder",
221
+ ];
222
+
223
+ const INCOME_TYPES = [
224
+ "PAYG",
225
+ "Self-employed",
226
+ "Contractor",
227
+ "Casual",
228
+ "Commission",
229
+ "Government Benefits",
230
+ "Rental",
231
+ "Other",
232
+ ];
233
+
234
+ const EXPENSE_TYPES = [
235
+ "Groceries",
236
+ "Dining Out",
237
+ "Transport",
238
+ "Fuel",
239
+ "Utilities",
240
+ "Phone Plan",
241
+ "Internet",
242
+ "Medical",
243
+ "Entertainment",
244
+ "Clothing",
245
+ "Gym & Fitness",
246
+ "Subscriptions",
247
+ "Childcare",
248
+ "Education",
249
+ "Pet Care",
250
+ "Insurance",
251
+ "Council Rates",
252
+ "Body Corp / Strata",
253
+ "Home Maintenance",
254
+ "Travel & Holidays",
255
+ "Donations",
256
+ "Other",
257
+ ];
258
+
259
+ const PRIORITIES = [
260
+ "Maximize Borrow Amount",
261
+ "Cheapest Interest Rate",
262
+ "The Best Features",
263
+ "Major Lender",
264
+ "Small Lender",
265
+ "Regional Lender",
266
+ "Non-Bank Lender",
267
+ "Branch Network",
268
+ "Good Customer Service",
269
+ "Environmentally Friendly Lender",
270
+ ];
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Types
274
+ // ---------------------------------------------------------------------------
275
+
276
+ // --- Loan Scenario ---
277
+ export interface LoanScenarioFormData {
278
+ loanPurpose: string;
279
+ propertyEstimate: number;
280
+ cashEquity: number;
281
+ loanAmount: number;
282
+ loanDuration: number;
283
+ knowsFeatures: string;
284
+ featureFixedRate: boolean;
285
+ featureVariableRate: boolean;
286
+ featureSplitLoan: boolean;
287
+ featureInterestOnly: boolean;
288
+ featureRedrawFacility: boolean;
289
+ feature100Offset: boolean;
290
+ priorities: string[];
291
+ borrowOtherThanProperty: string;
292
+ concernedAboutRates: string;
293
+ considerFixedRate: string;
294
+ retirementAge: string;
295
+ anticipatedChanges: string;
296
+ }
297
+
298
+ // --- Assets ---
299
+ export interface AssetLineItem {
300
+ id: string;
301
+ assetType: string;
302
+ value: number;
303
+ mainShare: number;
304
+ coShare: number;
305
+
306
+ // Property
307
+ address?: string;
308
+ propertySubtype?: string;
309
+ usedAs?: string;
310
+
311
+ // Vehicle
312
+ make?: string;
313
+ model?: string;
314
+ year?: string;
315
+
316
+ // Financial accounts (Cash, Term Deposit) + Investments + Crypto
317
+ institution?: string;
318
+
319
+ // Superannuation / SMSF
320
+ fundName?: string;
321
+ memberNumber?: string;
322
+
323
+ // Business Equity
324
+ businessName?: string;
325
+ abn?: string;
326
+
327
+ // Life Insurance
328
+ insurer?: string;
329
+ policyNumber?: string;
330
+ }
331
+
332
+ // --- Debts ---
333
+ export interface DebtLineItem {
334
+ id: string;
335
+ debtType: string;
336
+ amountOwing: number;
337
+ originalLoanAmount: number;
338
+ repaymentAmount: number;
339
+ repaymentFrequency: "Monthly" | "Weekly";
340
+ interestRate: string;
341
+ details: string;
342
+ mainShare: number;
343
+ coShare: number;
344
+
345
+ // Property loans
346
+ lender?: string;
347
+ accountNumber?: string;
348
+ propertyAddress?: string;
349
+
350
+ // Vehicle loans/leases
351
+ vehicleMake?: string;
352
+ vehicleModel?: string;
353
+ vehicleYear?: string;
354
+
355
+ // Cards / BNPL
356
+ creditLimit?: number;
357
+
358
+ // HECS
359
+ institution?: string;
360
+
361
+ // Tax debt
362
+ taxYear?: string;
363
+ referenceNumber?: string;
364
+
365
+ // Guarantor liability
366
+ beneficiary?: string;
367
+ }
368
+
369
+ // --- About ---
370
+ export interface DependantInfo {
371
+ dob: string;
372
+ }
373
+
374
+ export interface AboutApplicantFormData {
375
+ // Name
376
+ title: string;
377
+ firstName: string;
378
+ lastName: string;
379
+
380
+ // Contact
381
+ phone: string;
382
+ email: string;
383
+
384
+ // Personal
385
+ dob: string;
386
+ gender: string;
387
+ maritalStatus: string;
388
+ numDependants: string;
389
+ dependants: DependantInfo[];
390
+
391
+ // Residency & ID
392
+ citizenStatus: string;
393
+ residentialAddress: string;
394
+ residentialStatus: string;
395
+ timeAtAddressYears: string;
396
+ timeAtAddressMonths: string;
397
+ previousAddress: string;
398
+ driversLicence: string;
399
+ passport: string;
400
+
401
+ // Financial structure
402
+ propertyInTrust: string;
403
+ companyOwnership: string;
404
+ }
405
+
406
+ // --- Income (array-based) ---
407
+ export interface IncomeItem {
408
+ id: string;
409
+ incomeType: string;
410
+ jobTitle: string;
411
+ startDate: string;
412
+ stillInPosition: boolean;
413
+ endDate: string;
414
+ companyName: string;
415
+ companyAddress: string;
416
+ incomeAmount: number;
417
+ frequency: "Monthly" | "Weekly";
418
+ companyType: "Public" | "Private" | "";
419
+ }
420
+
421
+ export interface IncomeFormData {
422
+ items: IncomeItem[];
423
+ }
424
+
425
+ // --- Expenses (array-based) ---
426
+ export interface ExpenseItem {
427
+ id: string;
428
+ expenseType: string;
429
+ amount: number;
430
+ frequency: "Monthly" | "Weekly";
431
+ mainShare: number;
432
+ coShare: number;
433
+ }
434
+
435
+ export interface ExpensesFormData {
436
+ items: ExpenseItem[];
437
+ }
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // Prop interfaces
441
+ // ---------------------------------------------------------------------------
442
+
443
+ export interface EditLoanScenarioModalProps {
444
+ open: boolean;
445
+ onOpenChange: (open: boolean) => void;
446
+ initialData?: Partial<LoanScenarioFormData>;
447
+ onSave: (data: LoanScenarioFormData) => void;
448
+ mainApplicantName?: string;
449
+ coApplicantName?: string;
450
+ className?: string;
451
+ }
452
+
453
+ export interface EditAssetsModalProps {
454
+ open: boolean;
455
+ onOpenChange: (open: boolean) => void;
456
+ initialItems?: AssetLineItem[];
457
+ onSave: (items: AssetLineItem[]) => void;
458
+ mainApplicantName?: string;
459
+ coApplicantName?: string;
460
+ className?: string;
461
+ }
462
+
463
+ export interface EditDebtsModalProps {
464
+ open: boolean;
465
+ onOpenChange: (open: boolean) => void;
466
+ initialItems?: DebtLineItem[];
467
+ onSave: (items: DebtLineItem[]) => void;
468
+ mainApplicantName?: string;
469
+ coApplicantName?: string;
470
+ className?: string;
471
+ }
472
+
473
+ export interface EditAboutApplicantModalProps {
474
+ open: boolean;
475
+ onOpenChange: (open: boolean) => void;
476
+ applicantLabel?: string;
477
+ initialData?: Partial<AboutApplicantFormData>;
478
+ onSave: (data: AboutApplicantFormData) => void;
479
+ className?: string;
480
+ }
481
+
482
+ export interface EditIncomeModalProps {
483
+ open: boolean;
484
+ onOpenChange: (open: boolean) => void;
485
+ applicantLabel?: string;
486
+ initialData?: IncomeFormData;
487
+ onSave: (data: IncomeFormData) => void;
488
+ className?: string;
489
+ }
490
+
491
+ export interface EditExpensesModalProps {
492
+ open: boolean;
493
+ onOpenChange: (open: boolean) => void;
494
+ applicantLabel?: string;
495
+ initialData?: ExpensesFormData;
496
+ onSave: (data: ExpensesFormData) => void;
497
+ className?: string;
498
+ }
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // Shared internal helpers
502
+ // ---------------------------------------------------------------------------
503
+
504
+ function FormField({
505
+ label,
506
+ children,
507
+ className,
508
+ }: {
509
+ label: string;
510
+ children: React.ReactNode;
511
+ className?: string;
512
+ }) {
513
+ return (
514
+ <div className={cn("flex flex-col gap-1.5", className)}>
515
+ <Label className="text-sm font-medium text-muted-foreground">
516
+ {label}
517
+ </Label>
518
+ {children}
519
+ </div>
520
+ );
521
+ }
522
+
523
+ function ModalScroll({ children }: { children: React.ReactNode }) {
524
+ return (
525
+ <div className="max-h-[65vh] overflow-y-auto flex flex-col gap-4 py-1 pr-1">
526
+ {children}
527
+ </div>
528
+ );
529
+ }
530
+
531
+ function FrequencyToggle({
532
+ value,
533
+ onValueChange,
534
+ }: {
535
+ value: "Monthly" | "Weekly";
536
+ onValueChange: (val: "Monthly" | "Weekly") => void;
537
+ }) {
538
+ return (
539
+ <ToggleGroup
540
+ variant="outline"
541
+ value={value}
542
+ onValueChange={(val) => {
543
+ if (val === "Monthly" || val === "Weekly") {
544
+ onValueChange(val);
545
+ }
546
+ }}
547
+ >
548
+ <ToggleGroupItem value="Monthly" size="sm">
549
+ Monthly
550
+ </ToggleGroupItem>
551
+ <ToggleGroupItem value="Weekly" size="sm">
552
+ Weekly
553
+ </ToggleGroupItem>
554
+ </ToggleGroup>
555
+ );
556
+ }
557
+
558
+ function AccordionItemHeader({
559
+ label,
560
+ onRemove,
561
+ removeLabel = "Remove item",
562
+ }: {
563
+ label: string;
564
+ onRemove: () => void;
565
+ removeLabel?: string;
566
+ }) {
567
+ return (
568
+ <AccordionPrimitive.Header className="flex items-center">
569
+ <AccordionPrimitive.Trigger
570
+ className={cn(
571
+ "flex flex-1 items-center justify-between gap-4 py-4 text-left",
572
+ "text-label-medium rounded-none outline-none transition-[color,opacity]",
573
+ "hover:underline focus-visible:ring-2 focus-visible:ring-foreground/30",
574
+ "[&[data-panel-open]>svg]:rotate-180",
575
+ )}
576
+ >
577
+ {label}
578
+ <ChevronDownIcon className="pointer-events-none size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
579
+ </AccordionPrimitive.Trigger>
580
+ <button
581
+ type="button"
582
+ onClick={onRemove}
583
+ className="p-2 hover:bg-foreground/5 transition-colors"
584
+ aria-label={removeLabel}
585
+ >
586
+ <Trash2 className="h-3.5 w-3.5 text-destructive" />
587
+ </button>
588
+ </AccordionPrimitive.Header>
589
+ );
590
+ }
591
+
592
+ // ---------------------------------------------------------------------------
593
+ // Defaults
594
+ // ---------------------------------------------------------------------------
595
+
596
+ const LOAN_SCENARIO_DEFAULTS: LoanScenarioFormData = {
597
+ loanPurpose: "",
598
+ propertyEstimate: 0,
599
+ cashEquity: 0,
600
+ loanAmount: 0,
601
+ loanDuration: 30,
602
+ knowsFeatures: "",
603
+ featureFixedRate: false,
604
+ featureVariableRate: false,
605
+ featureSplitLoan: false,
606
+ featureInterestOnly: false,
607
+ featureRedrawFacility: false,
608
+ feature100Offset: false,
609
+ priorities: [],
610
+ borrowOtherThanProperty: "",
611
+ concernedAboutRates: "",
612
+ considerFixedRate: "",
613
+ retirementAge: "",
614
+ anticipatedChanges: "",
615
+ };
616
+
617
+ const ABOUT_APPLICANT_DEFAULTS: AboutApplicantFormData = {
618
+ title: "",
619
+ firstName: "",
620
+ lastName: "",
621
+ phone: "",
622
+ email: "",
623
+ dob: "",
624
+ gender: "",
625
+ maritalStatus: "",
626
+ numDependants: "",
627
+ dependants: [],
628
+ citizenStatus: "",
629
+ residentialAddress: "",
630
+ residentialStatus: "",
631
+ timeAtAddressYears: "",
632
+ timeAtAddressMonths: "",
633
+ previousAddress: "",
634
+ driversLicence: "",
635
+ passport: "",
636
+ propertyInTrust: "",
637
+ companyOwnership: "",
638
+ };
639
+
640
+ function makeDefaultIncomeItem(): IncomeItem {
641
+ return {
642
+ id: `income-${Date.now()}-${Math.random()}`,
643
+ incomeType: "",
644
+ jobTitle: "",
645
+ startDate: "",
646
+ stillInPosition: true,
647
+ endDate: "",
648
+ companyName: "",
649
+ companyAddress: "",
650
+ incomeAmount: 0,
651
+ frequency: "Monthly",
652
+ companyType: "",
653
+ };
654
+ }
655
+
656
+ function makeDefaultAssetItem(): AssetLineItem {
657
+ return {
658
+ id: `asset-${Date.now()}-${Math.random()}`,
659
+ assetType: "",
660
+ value: 0,
661
+ mainShare: 100,
662
+ coShare: 0,
663
+ };
664
+ }
665
+
666
+ function makeDefaultDebtItem(): DebtLineItem {
667
+ return {
668
+ id: `debt-${Date.now()}-${Math.random()}`,
669
+ debtType: "",
670
+ amountOwing: 0,
671
+ originalLoanAmount: 0,
672
+ repaymentAmount: 0,
673
+ repaymentFrequency: "Monthly",
674
+ interestRate: "",
675
+ details: "",
676
+ mainShare: 100,
677
+ coShare: 0,
678
+ };
679
+ }
680
+
681
+ function makeDefaultExpenseItem(): ExpenseItem {
682
+ return {
683
+ id: `expense-${Date.now()}-${Math.random()}`,
684
+ expenseType: "",
685
+ amount: 0,
686
+ frequency: "Monthly",
687
+ mainShare: 100,
688
+ coShare: 0,
689
+ };
690
+ }
691
+
692
+ // ---------------------------------------------------------------------------
693
+ // EditLoanScenarioModal
694
+ // ---------------------------------------------------------------------------
695
+
696
+ export function EditLoanScenarioModal({
697
+ open,
698
+ onOpenChange,
699
+ initialData,
700
+ onSave,
701
+ className,
702
+ }: EditLoanScenarioModalProps) {
703
+ const [form, setForm] = React.useState<LoanScenarioFormData>({
704
+ ...LOAN_SCENARIO_DEFAULTS,
705
+ ...initialData,
706
+ });
707
+
708
+ React.useEffect(() => {
709
+ if (open) {
710
+ setForm({ ...LOAN_SCENARIO_DEFAULTS, ...initialData });
711
+ }
712
+ }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
713
+
714
+ const set = <K extends keyof LoanScenarioFormData>(
715
+ key: K,
716
+ val: LoanScenarioFormData[K],
717
+ ) => setForm((prev) => ({ ...prev, [key]: val }));
718
+
719
+ const togglePriority = (priority: string) => {
720
+ setForm((prev) => {
721
+ const exists = prev.priorities.includes(priority);
722
+ if (exists) {
723
+ return {
724
+ ...prev,
725
+ priorities: prev.priorities.filter((p) => p !== priority),
726
+ };
727
+ }
728
+ if (prev.priorities.length >= 3) return prev;
729
+ return { ...prev, priorities: [...prev.priorities, priority] };
730
+ });
731
+ };
732
+
733
+ const prioritiesAtMax = form.priorities.length >= 3;
734
+
735
+ const LOAN_FEATURES: { key: keyof LoanScenarioFormData; label: string }[] = [
736
+ { key: "featureFixedRate", label: "Fixed Rate" },
737
+ { key: "featureVariableRate", label: "Variable Rate" },
738
+ { key: "featureSplitLoan", label: "Split Loan" },
739
+ { key: "featureInterestOnly", label: "Interest Only" },
740
+ { key: "featureRedrawFacility", label: "Redraw Facility" },
741
+ { key: "feature100Offset", label: "100% Offset" },
742
+ ];
743
+
744
+ const YES_NO_FIELDS: { key: keyof LoanScenarioFormData; label: string }[] = [
745
+ {
746
+ key: "borrowOtherThanProperty",
747
+ label: "Do you intend to borrow money other than your property?",
748
+ },
749
+ {
750
+ key: "concernedAboutRates",
751
+ label: "Are you concerned about rising interest rates?",
752
+ },
753
+ {
754
+ key: "considerFixedRate",
755
+ label: "Would you consider taking a fixed rate?",
756
+ },
757
+ {
758
+ key: "anticipatedChanges",
759
+ label: "Anticipated changes impacting repayment?",
760
+ },
761
+ ];
762
+
763
+ return (
764
+ <Dialog open={open} onOpenChange={onOpenChange}>
765
+ <DialogContent className={cn("max-w-lg", className)}>
766
+ <DialogHeader>
767
+ <DialogTitle>Edit Loan Scenario</DialogTitle>
768
+ </DialogHeader>
769
+ <ModalScroll>
770
+ {/* 1. Loan purpose */}
771
+ <FormField label="What would you like to do?">
772
+ <Select
773
+ value={form.loanPurpose}
774
+ onValueChange={(val) => set("loanPurpose", val)}
775
+ >
776
+ <SelectTrigger className="w-full">
777
+ <SelectValue placeholder="Select purpose" />
778
+ </SelectTrigger>
779
+ <SelectContent>
780
+ {LOAN_PURPOSES.map((p) => (
781
+ <SelectItem key={p} value={p}>
782
+ {p}
783
+ </SelectItem>
784
+ ))}
785
+ </SelectContent>
786
+ </Select>
787
+ </FormField>
788
+
789
+ {/* 2. Two-column: property estimate | cash equity */}
790
+ <div className="grid grid-cols-2 gap-4">
791
+ <FormField label="Property Estimate">
792
+ <CurrencyInputWithSlider
793
+ value={form.propertyEstimate}
794
+ min={0}
795
+ max={10_000_000}
796
+ step={50_000}
797
+ onValueChange={(val) => set("propertyEstimate", val)}
798
+ />
799
+ </FormField>
800
+ <FormField label="Cash/equity towards property">
801
+ <CurrencyInputWithSlider
802
+ value={form.cashEquity}
803
+ min={0}
804
+ max={2_000_000}
805
+ step={5_000}
806
+ onValueChange={(val) => set("cashEquity", val)}
807
+ />
808
+ </FormField>
809
+ </div>
810
+
811
+ {/* 3. Two-column: loan amount | loan duration */}
812
+ <div className="grid grid-cols-2 gap-4">
813
+ <FormField label="Loan Amount">
814
+ <CurrencyInputWithSlider
815
+ value={form.loanAmount}
816
+ min={0}
817
+ max={5_000_000}
818
+ step={10_000}
819
+ onValueChange={(val) => set("loanAmount", val)}
820
+ />
821
+ </FormField>
822
+ <FormField label="Loan Duration">
823
+ <div className="flex flex-col gap-2">
824
+ <div className="flex items-center gap-2">
825
+ <Input
826
+ type="number"
827
+ min={1}
828
+ max={40}
829
+ value={form.loanDuration}
830
+ onChange={(e) => {
831
+ const parsed = parseInt(e.target.value, 10);
832
+ if (!isNaN(parsed)) {
833
+ set("loanDuration", Math.min(40, Math.max(1, parsed)));
834
+ }
835
+ }}
836
+ className="flex-1"
837
+ />
838
+ <span className="text-sm text-muted-foreground">years</span>
839
+ </div>
840
+ <Slider
841
+ min={1}
842
+ max={40}
843
+ step={1}
844
+ value={form.loanDuration}
845
+ onValueChange={(val) => set("loanDuration", val)}
846
+ />
847
+ </div>
848
+ </FormField>
849
+ </div>
850
+
851
+ {/* 4. Do you know what loan features you want? */}
852
+ <FormField label="Do you know what loan features you want?">
853
+ <Select
854
+ value={form.knowsFeatures}
855
+ onValueChange={(val) => set("knowsFeatures", val)}
856
+ >
857
+ <SelectTrigger className="w-full">
858
+ <SelectValue placeholder="Select" />
859
+ </SelectTrigger>
860
+ <SelectContent>
861
+ <SelectItem value="Yes">Yes</SelectItem>
862
+ <SelectItem value="No">No</SelectItem>
863
+ </SelectContent>
864
+ </Select>
865
+ </FormField>
866
+
867
+ {/* 5. Features grid — only when knowsFeatures == "Yes" */}
868
+ {form.knowsFeatures === "Yes" && (
869
+ <div className="flex flex-col gap-2">
870
+ <Label className="text-sm font-medium text-muted-foreground">
871
+ What features are important to you?
872
+ </Label>
873
+ <div className="grid grid-cols-2 gap-2">
874
+ {LOAN_FEATURES.map(({ key, label }) => (
875
+ <div key={key} className="flex items-center gap-2">
876
+ <Checkbox
877
+ id={`feature-${key}`}
878
+ checked={form[key] as boolean}
879
+ onCheckedChange={(checked) => set(key, checked === true)}
880
+ />
881
+ <label
882
+ htmlFor={`feature-${key}`}
883
+ className="text-sm cursor-pointer select-none"
884
+ >
885
+ {label}
886
+ </label>
887
+ </div>
888
+ ))}
889
+ </div>
890
+ </div>
891
+ )}
892
+
893
+ {/* 6. Top 3 priorities */}
894
+ <div className="flex flex-col gap-2">
895
+ <Label className="text-sm font-medium text-muted-foreground">
896
+ Top 3 Priorities (Select up to 3)
897
+ </Label>
898
+ <div className="grid grid-cols-2 gap-2">
899
+ {PRIORITIES.map((priority) => {
900
+ const checked = form.priorities.includes(priority);
901
+ const disabled = prioritiesAtMax && !checked;
902
+ return (
903
+ <div key={priority} className="flex items-center gap-2">
904
+ <Checkbox
905
+ id={`priority-${priority}`}
906
+ checked={checked}
907
+ disabled={disabled}
908
+ onCheckedChange={() => togglePriority(priority)}
909
+ />
910
+ <label
911
+ htmlFor={`priority-${priority}`}
912
+ className={cn(
913
+ "text-sm cursor-pointer select-none",
914
+ disabled && "opacity-40 cursor-not-allowed",
915
+ )}
916
+ >
917
+ {priority}
918
+ </label>
919
+ </div>
920
+ );
921
+ })}
922
+ </div>
923
+ </div>
924
+
925
+ {/* 7. Yes/No radio sections */}
926
+ {YES_NO_FIELDS.map(({ key, label }) => (
927
+ <div key={key} className="flex flex-col gap-2">
928
+ <Label className="text-sm font-medium text-muted-foreground">
929
+ {label}
930
+ </Label>
931
+ <RadioGroup
932
+ value={form[key] as string}
933
+ onValueChange={(val) => {
934
+ if (val === "Yes" || val === "No") set(key, val);
935
+ }}
936
+ className="flex gap-4"
937
+ >
938
+ {(["Yes", "No"] as const).map((opt) => (
939
+ <label
940
+ key={opt}
941
+ className="flex items-center gap-2 cursor-pointer select-none"
942
+ >
943
+ <RadioGroupItem value={opt} />
944
+ <span className="text-sm">{opt}</span>
945
+ </label>
946
+ ))}
947
+ </RadioGroup>
948
+ </div>
949
+ ))}
950
+
951
+ {/* 8. Retirement age */}
952
+ <FormField label="What age are you planning to retire?">
953
+ <Input
954
+ type="number"
955
+ min={0}
956
+ max={100}
957
+ value={form.retirementAge}
958
+ onChange={(e) => set("retirementAge", e.target.value)}
959
+ placeholder="e.g. 65"
960
+ />
961
+ </FormField>
962
+ </ModalScroll>
963
+ <DialogFooter>
964
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
965
+ Cancel
966
+ </Button>
967
+ <Button
968
+ onClick={() => {
969
+ onSave(form);
970
+ onOpenChange(false);
971
+ }}
972
+ >
973
+ Save
974
+ </Button>
975
+ </DialogFooter>
976
+ </DialogContent>
977
+ </Dialog>
978
+ );
979
+ }
980
+
981
+ // ---------------------------------------------------------------------------
982
+ // EditAssetsModal
983
+ // ---------------------------------------------------------------------------
984
+
985
+ export function EditAssetsModal({
986
+ open,
987
+ onOpenChange,
988
+ initialItems = [],
989
+ onSave,
990
+ mainApplicantName = "Main Applicant",
991
+ coApplicantName = "Co-Applicant",
992
+ className,
993
+ }: EditAssetsModalProps) {
994
+ const [items, setItems] = React.useState<AssetLineItem[]>(
995
+ initialItems.length > 0 ? initialItems : [makeDefaultAssetItem()],
996
+ );
997
+
998
+ React.useEffect(() => {
999
+ if (open) {
1000
+ setItems(
1001
+ initialItems.length > 0 ? initialItems : [makeDefaultAssetItem()],
1002
+ );
1003
+ }
1004
+ }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
1005
+
1006
+ const updateItem = <K extends keyof AssetLineItem>(
1007
+ id: string,
1008
+ key: K,
1009
+ val: AssetLineItem[K],
1010
+ ) =>
1011
+ setItems((prev) =>
1012
+ prev.map((item) => (item.id === id ? { ...item, [key]: val } : item)),
1013
+ );
1014
+
1015
+ const removeItem = (id: string) =>
1016
+ setItems((prev) => prev.filter((item) => item.id !== id));
1017
+
1018
+ const addItem = () => setItems((prev) => [...prev, makeDefaultAssetItem()]);
1019
+
1020
+ const defaultOpenItems = items.length > 0 ? [items[0].id] : [];
1021
+
1022
+ return (
1023
+ <Dialog open={open} onOpenChange={onOpenChange}>
1024
+ <DialogContent className={cn("max-w-lg", className)}>
1025
+ <DialogHeader>
1026
+ <DialogTitle>Edit Assets</DialogTitle>
1027
+ </DialogHeader>
1028
+ <ModalScroll>
1029
+ <Accordion
1030
+ multiple
1031
+ defaultValue={defaultOpenItems}
1032
+ className="w-full"
1033
+ >
1034
+ {items.map((item, index) => (
1035
+ <AccordionItem key={item.id} value={item.id}>
1036
+ {/* Custom header: trigger + delete button as siblings */}
1037
+ <AccordionItemHeader
1038
+ label={`Asset ${index + 1} — ${item.assetType || "New Asset"}`}
1039
+ onRemove={() => removeItem(item.id)}
1040
+ removeLabel="Remove asset"
1041
+ />
1042
+ <AccordionContent>
1043
+ <div className="flex flex-col gap-4 pt-1">
1044
+ <FormField label="Asset Type">
1045
+ <Select
1046
+ value={item.assetType}
1047
+ onValueChange={(val) =>
1048
+ updateItem(item.id, "assetType", val)
1049
+ }
1050
+ >
1051
+ <SelectTrigger className="w-full">
1052
+ <SelectValue placeholder="Select type" />
1053
+ </SelectTrigger>
1054
+ <SelectContent>
1055
+ {ASSET_TYPES.map((t) => (
1056
+ <SelectItem key={t} value={t}>
1057
+ {t}
1058
+ </SelectItem>
1059
+ ))}
1060
+ </SelectContent>
1061
+ </Select>
1062
+ </FormField>
1063
+
1064
+ <FormField label="Value">
1065
+ <CurrencyInputWithSlider
1066
+ value={item.value}
1067
+ min={0}
1068
+ max={10_000_000}
1069
+ step={10_000}
1070
+ onValueChange={(val) =>
1071
+ updateItem(item.id, "value", val)
1072
+ }
1073
+ />
1074
+ </FormField>
1075
+
1076
+ {/* Conditional fields based on asset type */}
1077
+ {(() => {
1078
+ const f = assetFields(item.assetType);
1079
+ return (
1080
+ <>
1081
+ {f.isProperty && (
1082
+ <>
1083
+ <FormField label="Property Address">
1084
+ <AddressAutocomplete
1085
+ value={item.address ?? ""}
1086
+ onValueChange={(val) =>
1087
+ updateItem(item.id, "address", val)
1088
+ }
1089
+ onSelect={(opt) =>
1090
+ updateItem(item.id, "address", opt.label)
1091
+ }
1092
+ />
1093
+ </FormField>
1094
+ <div className="grid grid-cols-2 gap-4">
1095
+ <FormField label="Property Type">
1096
+ <Select
1097
+ value={item.propertySubtype ?? ""}
1098
+ onValueChange={(val) =>
1099
+ updateItem(
1100
+ item.id,
1101
+ "propertySubtype",
1102
+ val,
1103
+ )
1104
+ }
1105
+ >
1106
+ <SelectTrigger className="w-full">
1107
+ <SelectValue placeholder="Select" />
1108
+ </SelectTrigger>
1109
+ <SelectContent>
1110
+ {PROPERTY_SUBTYPES.map((t) => (
1111
+ <SelectItem key={t} value={t}>
1112
+ {t}
1113
+ </SelectItem>
1114
+ ))}
1115
+ </SelectContent>
1116
+ </Select>
1117
+ </FormField>
1118
+ <FormField label="Used As">
1119
+ <Select
1120
+ value={item.usedAs ?? ""}
1121
+ onValueChange={(val) =>
1122
+ updateItem(item.id, "usedAs", val)
1123
+ }
1124
+ >
1125
+ <SelectTrigger className="w-full">
1126
+ <SelectValue placeholder="Select" />
1127
+ </SelectTrigger>
1128
+ <SelectContent>
1129
+ {PROPERTY_USED_AS.map((t) => (
1130
+ <SelectItem key={t} value={t}>
1131
+ {t}
1132
+ </SelectItem>
1133
+ ))}
1134
+ </SelectContent>
1135
+ </Select>
1136
+ </FormField>
1137
+ </div>
1138
+ </>
1139
+ )}
1140
+
1141
+ {f.isVehicle && (
1142
+ <div className="grid grid-cols-3 gap-4">
1143
+ <FormField label="Make">
1144
+ <Input
1145
+ value={item.make ?? ""}
1146
+ onChange={(e) =>
1147
+ updateItem(item.id, "make", e.target.value)
1148
+ }
1149
+ placeholder="e.g. Toyota"
1150
+ />
1151
+ </FormField>
1152
+ <FormField label="Model">
1153
+ <Input
1154
+ value={item.model ?? ""}
1155
+ onChange={(e) =>
1156
+ updateItem(item.id, "model", e.target.value)
1157
+ }
1158
+ placeholder="e.g. Camry"
1159
+ />
1160
+ </FormField>
1161
+ <FormField label="Year">
1162
+ <Input
1163
+ type="number"
1164
+ min={1900}
1165
+ max={new Date().getFullYear() + 1}
1166
+ value={item.year ?? ""}
1167
+ onChange={(e) =>
1168
+ updateItem(item.id, "year", e.target.value)
1169
+ }
1170
+ placeholder="e.g. 2022"
1171
+ />
1172
+ </FormField>
1173
+ </div>
1174
+ )}
1175
+
1176
+ {(f.isFinancialAccount || f.isInvestment) && (
1177
+ <FormField label="Institution / Platform">
1178
+ <Input
1179
+ value={item.institution ?? ""}
1180
+ onChange={(e) =>
1181
+ updateItem(
1182
+ item.id,
1183
+ "institution",
1184
+ e.target.value,
1185
+ )
1186
+ }
1187
+ placeholder="e.g. CommBank, Vanguard"
1188
+ />
1189
+ </FormField>
1190
+ )}
1191
+
1192
+ {f.isSuper && (
1193
+ <div className="grid grid-cols-2 gap-4">
1194
+ <FormField label="Fund Name">
1195
+ <Input
1196
+ value={item.fundName ?? ""}
1197
+ onChange={(e) =>
1198
+ updateItem(
1199
+ item.id,
1200
+ "fundName",
1201
+ e.target.value,
1202
+ )
1203
+ }
1204
+ placeholder="e.g. Australian Super"
1205
+ />
1206
+ </FormField>
1207
+ <FormField label="Member Number">
1208
+ <Input
1209
+ value={item.memberNumber ?? ""}
1210
+ onChange={(e) =>
1211
+ updateItem(
1212
+ item.id,
1213
+ "memberNumber",
1214
+ e.target.value,
1215
+ )
1216
+ }
1217
+ placeholder="e.g. 1234567"
1218
+ />
1219
+ </FormField>
1220
+ </div>
1221
+ )}
1222
+
1223
+ {f.isBusiness && (
1224
+ <div className="grid grid-cols-2 gap-4">
1225
+ <FormField label="Business Name">
1226
+ <Input
1227
+ value={item.businessName ?? ""}
1228
+ onChange={(e) =>
1229
+ updateItem(
1230
+ item.id,
1231
+ "businessName",
1232
+ e.target.value,
1233
+ )
1234
+ }
1235
+ placeholder="e.g. Acme Pty Ltd"
1236
+ />
1237
+ </FormField>
1238
+ <FormField label="ABN">
1239
+ <Input
1240
+ value={item.abn ?? ""}
1241
+ onChange={(e) =>
1242
+ updateItem(item.id, "abn", e.target.value)
1243
+ }
1244
+ placeholder="e.g. 12 345 678 901"
1245
+ />
1246
+ </FormField>
1247
+ </div>
1248
+ )}
1249
+
1250
+ {f.isInsurance && (
1251
+ <div className="grid grid-cols-2 gap-4">
1252
+ <FormField label="Insurer">
1253
+ <Input
1254
+ value={item.insurer ?? ""}
1255
+ onChange={(e) =>
1256
+ updateItem(
1257
+ item.id,
1258
+ "insurer",
1259
+ e.target.value,
1260
+ )
1261
+ }
1262
+ placeholder="e.g. TAL, AIA"
1263
+ />
1264
+ </FormField>
1265
+ <FormField label="Policy Number">
1266
+ <Input
1267
+ value={item.policyNumber ?? ""}
1268
+ onChange={(e) =>
1269
+ updateItem(
1270
+ item.id,
1271
+ "policyNumber",
1272
+ e.target.value,
1273
+ )
1274
+ }
1275
+ placeholder="e.g. POL-123456"
1276
+ />
1277
+ </FormField>
1278
+ </div>
1279
+ )}
1280
+ </>
1281
+ );
1282
+ })()}
1283
+
1284
+ <FormField label="Ownership (%)">
1285
+ <OwnershipSplit
1286
+ owners={[
1287
+ {
1288
+ id: "main",
1289
+ name: mainApplicantName ?? "Main Applicant",
1290
+ share: item.mainShare,
1291
+ },
1292
+ {
1293
+ id: "co",
1294
+ name: coApplicantName ?? "Co-Applicant",
1295
+ share: item.coShare,
1296
+ },
1297
+ ]}
1298
+ onOwnersChange={(owners) => {
1299
+ updateItem(
1300
+ item.id,
1301
+ "mainShare",
1302
+ owners.find((o) => o.id === "main")?.share ?? 50,
1303
+ );
1304
+ updateItem(
1305
+ item.id,
1306
+ "coShare",
1307
+ owners.find((o) => o.id === "co")?.share ?? 50,
1308
+ );
1309
+ }}
1310
+ />
1311
+ </FormField>
1312
+ </div>
1313
+ </AccordionContent>
1314
+ </AccordionItem>
1315
+ ))}
1316
+ </Accordion>
1317
+
1318
+ <Button
1319
+ variant="outline"
1320
+ onClick={addItem}
1321
+ className="w-full gap-1.5"
1322
+ >
1323
+ <Plus className="h-4 w-4" />
1324
+ Add More
1325
+ </Button>
1326
+ </ModalScroll>
1327
+ <DialogFooter>
1328
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
1329
+ Cancel
1330
+ </Button>
1331
+ <Button
1332
+ onClick={() => {
1333
+ onSave(items);
1334
+ onOpenChange(false);
1335
+ }}
1336
+ >
1337
+ Save
1338
+ </Button>
1339
+ </DialogFooter>
1340
+ </DialogContent>
1341
+ </Dialog>
1342
+ );
1343
+ }
1344
+
1345
+ // ---------------------------------------------------------------------------
1346
+ // EditDebtsModal
1347
+ // ---------------------------------------------------------------------------
1348
+
1349
+ export function EditDebtsModal({
1350
+ open,
1351
+ onOpenChange,
1352
+ initialItems = [],
1353
+ onSave,
1354
+ mainApplicantName = "Main Applicant",
1355
+ coApplicantName = "Co-Applicant",
1356
+ className,
1357
+ }: EditDebtsModalProps) {
1358
+ const [items, setItems] = React.useState<DebtLineItem[]>(
1359
+ initialItems.length > 0 ? initialItems : [makeDefaultDebtItem()],
1360
+ );
1361
+
1362
+ React.useEffect(() => {
1363
+ if (open) {
1364
+ setItems(
1365
+ initialItems.length > 0 ? initialItems : [makeDefaultDebtItem()],
1366
+ );
1367
+ }
1368
+ }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
1369
+
1370
+ const updateItem = <K extends keyof DebtLineItem>(
1371
+ id: string,
1372
+ key: K,
1373
+ val: DebtLineItem[K],
1374
+ ) =>
1375
+ setItems((prev) =>
1376
+ prev.map((item) => (item.id === id ? { ...item, [key]: val } : item)),
1377
+ );
1378
+
1379
+ const removeItem = (id: string) =>
1380
+ setItems((prev) => prev.filter((item) => item.id !== id));
1381
+
1382
+ const addItem = () => setItems((prev) => [...prev, makeDefaultDebtItem()]);
1383
+
1384
+ const defaultOpenItems = items.length > 0 ? [items[0].id] : [];
1385
+
1386
+ return (
1387
+ <Dialog open={open} onOpenChange={onOpenChange}>
1388
+ <DialogContent className={cn("max-w-xl", className)}>
1389
+ <DialogHeader>
1390
+ <DialogTitle>Edit Debts</DialogTitle>
1391
+ </DialogHeader>
1392
+ <ModalScroll>
1393
+ <Accordion
1394
+ multiple
1395
+ defaultValue={defaultOpenItems}
1396
+ className="w-full"
1397
+ >
1398
+ {items.map((item) => {
1399
+ const df = debtFields(item.debtType);
1400
+ return (
1401
+ <AccordionItem key={item.id} value={item.id}>
1402
+ <AccordionItemHeader
1403
+ label={item.debtType || "New Debt"}
1404
+ onRemove={() => removeItem(item.id)}
1405
+ removeLabel="Remove debt"
1406
+ />
1407
+ <AccordionContent>
1408
+ <div className="flex flex-col gap-4 pt-1">
1409
+ <FormField label="Debt Type">
1410
+ <Select
1411
+ value={item.debtType}
1412
+ onValueChange={(val) =>
1413
+ updateItem(item.id, "debtType", val)
1414
+ }
1415
+ >
1416
+ <SelectTrigger className="w-full">
1417
+ <SelectValue placeholder="Select type" />
1418
+ </SelectTrigger>
1419
+ <SelectContent>
1420
+ {DEBT_TYPES.map((t) => (
1421
+ <SelectItem key={t} value={t}>
1422
+ {t}
1423
+ </SelectItem>
1424
+ ))}
1425
+ </SelectContent>
1426
+ </Select>
1427
+ </FormField>
1428
+
1429
+ {/* Conditional fields by debt type */}
1430
+ {(df.isPropertyLoan ||
1431
+ df.isGeneralLoan ||
1432
+ df.isVehicle) && (
1433
+ <div className="grid grid-cols-2 gap-4">
1434
+ <FormField label="Lender">
1435
+ <Input
1436
+ value={item.lender ?? ""}
1437
+ onChange={(e) =>
1438
+ updateItem(item.id, "lender", e.target.value)
1439
+ }
1440
+ placeholder="e.g. CommBank"
1441
+ />
1442
+ </FormField>
1443
+ <FormField label="Account Number">
1444
+ <Input
1445
+ value={item.accountNumber ?? ""}
1446
+ onChange={(e) =>
1447
+ updateItem(
1448
+ item.id,
1449
+ "accountNumber",
1450
+ e.target.value,
1451
+ )
1452
+ }
1453
+ placeholder="e.g. 123-456"
1454
+ />
1455
+ </FormField>
1456
+ </div>
1457
+ )}
1458
+
1459
+ {df.isPropertyLoan && (
1460
+ <FormField label="Property Address">
1461
+ <AddressAutocomplete
1462
+ value={item.propertyAddress ?? ""}
1463
+ onValueChange={(val) =>
1464
+ updateItem(item.id, "propertyAddress", val)
1465
+ }
1466
+ onSelect={(opt) =>
1467
+ updateItem(item.id, "propertyAddress", opt.label)
1468
+ }
1469
+ />
1470
+ </FormField>
1471
+ )}
1472
+
1473
+ {df.isVehicle && (
1474
+ <div className="grid grid-cols-3 gap-4">
1475
+ <FormField label="Make">
1476
+ <Input
1477
+ value={item.vehicleMake ?? ""}
1478
+ onChange={(e) =>
1479
+ updateItem(
1480
+ item.id,
1481
+ "vehicleMake",
1482
+ e.target.value,
1483
+ )
1484
+ }
1485
+ placeholder="e.g. Toyota"
1486
+ />
1487
+ </FormField>
1488
+ <FormField label="Model">
1489
+ <Input
1490
+ value={item.vehicleModel ?? ""}
1491
+ onChange={(e) =>
1492
+ updateItem(
1493
+ item.id,
1494
+ "vehicleModel",
1495
+ e.target.value,
1496
+ )
1497
+ }
1498
+ placeholder="e.g. Camry"
1499
+ />
1500
+ </FormField>
1501
+ <FormField label="Year">
1502
+ <Input
1503
+ type="number"
1504
+ min={1900}
1505
+ max={new Date().getFullYear() + 1}
1506
+ value={item.vehicleYear ?? ""}
1507
+ onChange={(e) =>
1508
+ updateItem(
1509
+ item.id,
1510
+ "vehicleYear",
1511
+ e.target.value,
1512
+ )
1513
+ }
1514
+ placeholder="e.g. 2022"
1515
+ />
1516
+ </FormField>
1517
+ </div>
1518
+ )}
1519
+
1520
+ {df.isCard && (
1521
+ <div className="grid grid-cols-2 gap-4">
1522
+ <FormField label="Provider">
1523
+ <Input
1524
+ value={item.lender ?? ""}
1525
+ onChange={(e) =>
1526
+ updateItem(item.id, "lender", e.target.value)
1527
+ }
1528
+ placeholder="e.g. ANZ, Afterpay"
1529
+ />
1530
+ </FormField>
1531
+ <FormField label="Credit Limit">
1532
+ <CurrencyInputWithSlider
1533
+ value={item.creditLimit ?? 0}
1534
+ min={0}
1535
+ max={100_000}
1536
+ step={500}
1537
+ onValueChange={(val) =>
1538
+ updateItem(item.id, "creditLimit", val)
1539
+ }
1540
+ />
1541
+ </FormField>
1542
+ </div>
1543
+ )}
1544
+
1545
+ {df.isHecs && (
1546
+ <FormField label="Institution">
1547
+ <Input
1548
+ value={item.institution ?? ""}
1549
+ onChange={(e) =>
1550
+ updateItem(item.id, "institution", e.target.value)
1551
+ }
1552
+ placeholder="e.g. University of Sydney"
1553
+ />
1554
+ </FormField>
1555
+ )}
1556
+
1557
+ {df.isTax && (
1558
+ <div className="grid grid-cols-2 gap-4">
1559
+ <FormField label="Tax Year">
1560
+ <Input
1561
+ value={item.taxYear ?? ""}
1562
+ onChange={(e) =>
1563
+ updateItem(item.id, "taxYear", e.target.value)
1564
+ }
1565
+ placeholder="e.g. 2023–24"
1566
+ />
1567
+ </FormField>
1568
+ <FormField label="ATO Reference">
1569
+ <Input
1570
+ value={item.referenceNumber ?? ""}
1571
+ onChange={(e) =>
1572
+ updateItem(
1573
+ item.id,
1574
+ "referenceNumber",
1575
+ e.target.value,
1576
+ )
1577
+ }
1578
+ placeholder="e.g. 1234567890"
1579
+ />
1580
+ </FormField>
1581
+ </div>
1582
+ )}
1583
+
1584
+ {df.isGuarantor && (
1585
+ <FormField label="Beneficiary (borrower's name)">
1586
+ <Input
1587
+ value={item.beneficiary ?? ""}
1588
+ onChange={(e) =>
1589
+ updateItem(item.id, "beneficiary", e.target.value)
1590
+ }
1591
+ placeholder="e.g. Jane Smith"
1592
+ />
1593
+ </FormField>
1594
+ )}
1595
+
1596
+ <div className="grid grid-cols-2 gap-4">
1597
+ <FormField label="Amount Owing">
1598
+ <CurrencyInputWithSlider
1599
+ value={item.amountOwing}
1600
+ min={0}
1601
+ max={1_000_000}
1602
+ step={1_000}
1603
+ onValueChange={(val) =>
1604
+ updateItem(item.id, "amountOwing", val)
1605
+ }
1606
+ />
1607
+ </FormField>
1608
+ <FormField label="Original Loan Amount">
1609
+ <CurrencyInputWithSlider
1610
+ value={item.originalLoanAmount}
1611
+ min={0}
1612
+ max={1_000_000}
1613
+ step={1_000}
1614
+ onValueChange={(val) =>
1615
+ updateItem(item.id, "originalLoanAmount", val)
1616
+ }
1617
+ />
1618
+ </FormField>
1619
+ </div>
1620
+
1621
+ <div className="flex items-start gap-3">
1622
+ <div className="flex-1">
1623
+ <FormField label="Repayments">
1624
+ <CurrencyInputWithSlider
1625
+ value={item.repaymentAmount}
1626
+ min={0}
1627
+ max={1_000_000}
1628
+ step={100}
1629
+ onValueChange={(val) =>
1630
+ updateItem(item.id, "repaymentAmount", val)
1631
+ }
1632
+ />
1633
+ </FormField>
1634
+ </div>
1635
+ <div className="shrink-0 mt-[1.625rem]">
1636
+ <FrequencyToggle
1637
+ value={item.repaymentFrequency}
1638
+ onValueChange={(val) =>
1639
+ updateItem(item.id, "repaymentFrequency", val)
1640
+ }
1641
+ />
1642
+ </div>
1643
+ </div>
1644
+
1645
+ <FormField label="Interest Rate">
1646
+ <div className="flex items-center gap-2">
1647
+ <Input
1648
+ value={item.interestRate}
1649
+ onChange={(e) =>
1650
+ updateItem(
1651
+ item.id,
1652
+ "interestRate",
1653
+ e.target.value,
1654
+ )
1655
+ }
1656
+ placeholder="e.g. 6.5"
1657
+ className="flex-1"
1658
+ />
1659
+ <span className="text-sm text-muted-foreground shrink-0">
1660
+ %
1661
+ </span>
1662
+ </div>
1663
+ </FormField>
1664
+
1665
+ <FormField label="Notes">
1666
+ <Textarea
1667
+ value={item.details}
1668
+ onChange={(e) =>
1669
+ updateItem(item.id, "details", e.target.value)
1670
+ }
1671
+ placeholder="Short comment"
1672
+ rows={2}
1673
+ />
1674
+ </FormField>
1675
+
1676
+ <FormField label="Ownership (%)">
1677
+ <OwnershipSplit
1678
+ owners={[
1679
+ {
1680
+ id: "main",
1681
+ name: mainApplicantName ?? "Main Applicant",
1682
+ share: item.mainShare,
1683
+ },
1684
+ {
1685
+ id: "co",
1686
+ name: coApplicantName ?? "Co-Applicant",
1687
+ share: item.coShare,
1688
+ },
1689
+ ]}
1690
+ onOwnersChange={(owners) => {
1691
+ updateItem(
1692
+ item.id,
1693
+ "mainShare",
1694
+ owners.find((o) => o.id === "main")?.share ?? 50,
1695
+ );
1696
+ updateItem(
1697
+ item.id,
1698
+ "coShare",
1699
+ owners.find((o) => o.id === "co")?.share ?? 50,
1700
+ );
1701
+ }}
1702
+ />
1703
+ </FormField>
1704
+ </div>
1705
+ </AccordionContent>
1706
+ </AccordionItem>
1707
+ );
1708
+ })}
1709
+ </Accordion>
1710
+
1711
+ <Button
1712
+ variant="outline"
1713
+ onClick={addItem}
1714
+ className="w-full gap-1.5"
1715
+ >
1716
+ <Plus className="h-4 w-4" />
1717
+ Add More
1718
+ </Button>
1719
+ </ModalScroll>
1720
+ <DialogFooter>
1721
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
1722
+ Cancel
1723
+ </Button>
1724
+ <Button
1725
+ onClick={() => {
1726
+ onSave(items);
1727
+ onOpenChange(false);
1728
+ }}
1729
+ >
1730
+ Save
1731
+ </Button>
1732
+ </DialogFooter>
1733
+ </DialogContent>
1734
+ </Dialog>
1735
+ );
1736
+ }
1737
+
1738
+ // ---------------------------------------------------------------------------
1739
+ // EditAboutApplicantModal
1740
+ // ---------------------------------------------------------------------------
1741
+
1742
+ export function EditAboutApplicantModal({
1743
+ open,
1744
+ onOpenChange,
1745
+ applicantLabel = "Applicant",
1746
+ initialData,
1747
+ onSave,
1748
+ className,
1749
+ }: EditAboutApplicantModalProps) {
1750
+ const [form, setForm] = React.useState<AboutApplicantFormData>({
1751
+ ...ABOUT_APPLICANT_DEFAULTS,
1752
+ ...initialData,
1753
+ });
1754
+
1755
+ React.useEffect(() => {
1756
+ if (open) {
1757
+ setForm({ ...ABOUT_APPLICANT_DEFAULTS, ...initialData });
1758
+ }
1759
+ }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
1760
+
1761
+ const setStr = (key: keyof AboutApplicantFormData) => (val: string) =>
1762
+ setForm((prev) => ({ ...prev, [key]: val }));
1763
+
1764
+ const handleNumDependantsChange = (val: string) => {
1765
+ const n = Math.max(0, Math.min(20, parseInt(val, 10) || 0));
1766
+ setForm((prev) => {
1767
+ const current = prev.dependants ?? [];
1768
+ const next: DependantInfo[] =
1769
+ n > current.length
1770
+ ? [...current, ...Array(n - current.length).fill({ dob: "" })]
1771
+ : current.slice(0, n);
1772
+ return { ...prev, numDependants: val, dependants: next };
1773
+ });
1774
+ };
1775
+
1776
+ const setDependantDob = (index: number, dob: string) =>
1777
+ setForm((prev) => {
1778
+ const next = prev.dependants.map((d, i) => (i === index ? { dob } : d));
1779
+ return { ...prev, dependants: next };
1780
+ });
1781
+
1782
+ return (
1783
+ <Dialog open={open} onOpenChange={onOpenChange}>
1784
+ <DialogContent className={cn("max-w-md", className)}>
1785
+ <DialogHeader>
1786
+ <DialogTitle>Edit About {applicantLabel}</DialogTitle>
1787
+ </DialogHeader>
1788
+ <ModalScroll>
1789
+ {/* Name */}
1790
+ <div className="flex gap-3 items-start">
1791
+ <div className="w-24 shrink-0">
1792
+ <FormField label="Title">
1793
+ <Select value={form.title} onValueChange={setStr("title")}>
1794
+ <SelectTrigger className="w-full">
1795
+ <SelectValue placeholder="Title" />
1796
+ </SelectTrigger>
1797
+ <SelectContent>
1798
+ {TITLE_OPTIONS.map((t) => (
1799
+ <SelectItem key={t} value={t}>
1800
+ {t}
1801
+ </SelectItem>
1802
+ ))}
1803
+ </SelectContent>
1804
+ </Select>
1805
+ </FormField>
1806
+ </div>
1807
+ <FormField label="First Name" className="flex-1">
1808
+ <Input
1809
+ value={form.firstName}
1810
+ onChange={(e) => setStr("firstName")(e.target.value)}
1811
+ placeholder="First name"
1812
+ />
1813
+ </FormField>
1814
+ <FormField label="Last Name" className="flex-1">
1815
+ <Input
1816
+ value={form.lastName}
1817
+ onChange={(e) => setStr("lastName")(e.target.value)}
1818
+ placeholder="Last name"
1819
+ />
1820
+ </FormField>
1821
+ </div>
1822
+
1823
+ {/* Contact */}
1824
+ <div className="grid grid-cols-2 gap-4">
1825
+ <FormField label="Phone Number">
1826
+ <Input
1827
+ value={form.phone}
1828
+ onChange={(e) => setStr("phone")(e.target.value)}
1829
+ placeholder="+61 4xx xxx xxx"
1830
+ type="tel"
1831
+ />
1832
+ </FormField>
1833
+ <FormField label="Email">
1834
+ <Input
1835
+ value={form.email}
1836
+ onChange={(e) => setStr("email")(e.target.value)}
1837
+ placeholder="email@example.com"
1838
+ type="email"
1839
+ />
1840
+ </FormField>
1841
+ </div>
1842
+
1843
+ {/* Personal details */}
1844
+ <div className="grid grid-cols-2 gap-4">
1845
+ <FormField label="Date of Birth">
1846
+ <DatePicker
1847
+ value={form.dob ? new Date(form.dob) : undefined}
1848
+ onChange={(d) =>
1849
+ setStr("dob")(d ? d.toISOString().slice(0, 10) : "")
1850
+ }
1851
+ calendarProps={{
1852
+ fromYear: 1900,
1853
+ toYear: new Date().getFullYear() - 16,
1854
+ }}
1855
+ />
1856
+ </FormField>
1857
+ <FormField label="Gender">
1858
+ <Select value={form.gender} onValueChange={setStr("gender")}>
1859
+ <SelectTrigger className="w-full">
1860
+ <SelectValue placeholder="Select" />
1861
+ </SelectTrigger>
1862
+ <SelectContent>
1863
+ {GENDER_OPTIONS.map((g) => (
1864
+ <SelectItem key={g} value={g}>
1865
+ {g}
1866
+ </SelectItem>
1867
+ ))}
1868
+ </SelectContent>
1869
+ </Select>
1870
+ </FormField>
1871
+ </div>
1872
+
1873
+ <div className="grid grid-cols-2 gap-4">
1874
+ <FormField label="Marital Status">
1875
+ <Select
1876
+ value={form.maritalStatus}
1877
+ onValueChange={setStr("maritalStatus")}
1878
+ >
1879
+ <SelectTrigger className="w-full">
1880
+ <SelectValue placeholder="Select" />
1881
+ </SelectTrigger>
1882
+ <SelectContent>
1883
+ {MARITAL_OPTIONS.map((m) => (
1884
+ <SelectItem key={m} value={m}>
1885
+ {m}
1886
+ </SelectItem>
1887
+ ))}
1888
+ </SelectContent>
1889
+ </Select>
1890
+ </FormField>
1891
+ <FormField label="No. of Dependants">
1892
+ <Input
1893
+ type="number"
1894
+ min={0}
1895
+ max={20}
1896
+ value={form.numDependants}
1897
+ onChange={(e) => handleNumDependantsChange(e.target.value)}
1898
+ placeholder="0"
1899
+ />
1900
+ </FormField>
1901
+ </div>
1902
+
1903
+ {/* Dependant DOBs */}
1904
+ {form.dependants.length > 0 && (
1905
+ <div className="flex flex-col gap-3">
1906
+ <Label className="text-sm font-medium">
1907
+ Dependant Date of Birth
1908
+ </Label>
1909
+ <div className="grid grid-cols-2 gap-3">
1910
+ {form.dependants.map((dep, i) => (
1911
+ <FormField key={i} label={`Dependant ${i + 1}`}>
1912
+ <DatePicker
1913
+ value={dep.dob ? new Date(dep.dob) : undefined}
1914
+ onChange={(d) =>
1915
+ setDependantDob(
1916
+ i,
1917
+ d ? d.toISOString().slice(0, 10) : "",
1918
+ )
1919
+ }
1920
+ calendarProps={{
1921
+ fromYear: 1900,
1922
+ toYear: new Date().getFullYear(),
1923
+ }}
1924
+ />
1925
+ </FormField>
1926
+ ))}
1927
+ </div>
1928
+ </div>
1929
+ )}
1930
+
1931
+ {/* Residency */}
1932
+ <FormField label="Citizen Status">
1933
+ <Select
1934
+ value={form.citizenStatus}
1935
+ onValueChange={setStr("citizenStatus")}
1936
+ >
1937
+ <SelectTrigger className="w-full">
1938
+ <SelectValue placeholder="Select status" />
1939
+ </SelectTrigger>
1940
+ <SelectContent>
1941
+ {CITIZEN_OPTIONS.map((c) => (
1942
+ <SelectItem key={c} value={c}>
1943
+ {c}
1944
+ </SelectItem>
1945
+ ))}
1946
+ </SelectContent>
1947
+ </Select>
1948
+ </FormField>
1949
+
1950
+ <FormField label="Current Residential Address">
1951
+ <AddressAutocomplete
1952
+ value={form.residentialAddress}
1953
+ onValueChange={setStr("residentialAddress")}
1954
+ onSelect={(opt) => setStr("residentialAddress")(opt.label)}
1955
+ />
1956
+ </FormField>
1957
+
1958
+ <div className="grid grid-cols-2 gap-4">
1959
+ <FormField label="Residential Status">
1960
+ <Select
1961
+ value={form.residentialStatus}
1962
+ onValueChange={setStr("residentialStatus")}
1963
+ >
1964
+ <SelectTrigger className="w-full">
1965
+ <SelectValue placeholder="Select" />
1966
+ </SelectTrigger>
1967
+ <SelectContent>
1968
+ {RESIDENTIAL_STATUS_OPTIONS.map((o) => (
1969
+ <SelectItem key={o} value={o}>
1970
+ {o}
1971
+ </SelectItem>
1972
+ ))}
1973
+ </SelectContent>
1974
+ </Select>
1975
+ </FormField>
1976
+ <FormField label="Time at Address">
1977
+ <div className="flex gap-2">
1978
+ <div className="flex items-center gap-1 flex-1">
1979
+ <Input
1980
+ type="number"
1981
+ min={0}
1982
+ value={form.timeAtAddressYears}
1983
+ onChange={(e) =>
1984
+ setStr("timeAtAddressYears")(e.target.value)
1985
+ }
1986
+ placeholder="0"
1987
+ />
1988
+ <span className="text-sm text-muted-foreground shrink-0">
1989
+ yr
1990
+ </span>
1991
+ </div>
1992
+ <div className="flex items-center gap-1 flex-1">
1993
+ <Input
1994
+ type="number"
1995
+ min={0}
1996
+ max={11}
1997
+ value={form.timeAtAddressMonths}
1998
+ onChange={(e) =>
1999
+ setStr("timeAtAddressMonths")(e.target.value)
2000
+ }
2001
+ placeholder="0"
2002
+ />
2003
+ <span className="text-sm text-muted-foreground shrink-0">
2004
+ mo
2005
+ </span>
2006
+ </div>
2007
+ </div>
2008
+ </FormField>
2009
+ </div>
2010
+
2011
+ {/* Previous address if < 2 years at current */}
2012
+ {parseInt(form.timeAtAddressYears || "99") < 2 && (
2013
+ <FormField label="Previous Address">
2014
+ <AddressAutocomplete
2015
+ value={form.previousAddress}
2016
+ onValueChange={setStr("previousAddress")}
2017
+ onSelect={(opt) => setStr("previousAddress")(opt.label)}
2018
+ />
2019
+ </FormField>
2020
+ )}
2021
+
2022
+ {/* ID */}
2023
+ <div className="grid grid-cols-2 gap-4">
2024
+ <FormField label="Driver's Licence">
2025
+ <Input
2026
+ value={form.driversLicence}
2027
+ onChange={(e) => setStr("driversLicence")(e.target.value)}
2028
+ placeholder="e.g. 12345678"
2029
+ />
2030
+ </FormField>
2031
+ <FormField label="Passport Number">
2032
+ <Input
2033
+ value={form.passport}
2034
+ onChange={(e) => setStr("passport")(e.target.value)}
2035
+ placeholder="e.g. PA1234567"
2036
+ />
2037
+ </FormField>
2038
+ </div>
2039
+
2040
+ {/* Financial structure */}
2041
+ <div className="grid grid-cols-2 gap-4">
2042
+ <FormField label="Property in Trust">
2043
+ <Select
2044
+ value={form.propertyInTrust}
2045
+ onValueChange={setStr("propertyInTrust")}
2046
+ >
2047
+ <SelectTrigger className="w-full">
2048
+ <SelectValue placeholder="Select" />
2049
+ </SelectTrigger>
2050
+ <SelectContent>
2051
+ {PROPERTY_IN_TRUST_OPTIONS.map((o) => (
2052
+ <SelectItem key={o} value={o}>
2053
+ {o}
2054
+ </SelectItem>
2055
+ ))}
2056
+ </SelectContent>
2057
+ </Select>
2058
+ </FormField>
2059
+ <FormField label="Company Ownership">
2060
+ <Select
2061
+ value={form.companyOwnership}
2062
+ onValueChange={setStr("companyOwnership")}
2063
+ >
2064
+ <SelectTrigger className="w-full">
2065
+ <SelectValue placeholder="Select" />
2066
+ </SelectTrigger>
2067
+ <SelectContent>
2068
+ {COMPANY_OWNERSHIP_OPTIONS.map((o) => (
2069
+ <SelectItem key={o} value={o}>
2070
+ {o}
2071
+ </SelectItem>
2072
+ ))}
2073
+ </SelectContent>
2074
+ </Select>
2075
+ </FormField>
2076
+ </div>
2077
+ </ModalScroll>
2078
+ <DialogFooter>
2079
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
2080
+ Cancel
2081
+ </Button>
2082
+ <Button
2083
+ onClick={() => {
2084
+ onSave(form);
2085
+ onOpenChange(false);
2086
+ }}
2087
+ >
2088
+ Save
2089
+ </Button>
2090
+ </DialogFooter>
2091
+ </DialogContent>
2092
+ </Dialog>
2093
+ );
2094
+ }
2095
+
2096
+ // ---------------------------------------------------------------------------
2097
+ // EditIncomeModal
2098
+ // ---------------------------------------------------------------------------
2099
+
2100
+ export function EditIncomeModal({
2101
+ open,
2102
+ onOpenChange,
2103
+ applicantLabel = "Applicant",
2104
+ initialData,
2105
+ onSave,
2106
+ className,
2107
+ }: EditIncomeModalProps) {
2108
+ const defaultItems = React.useMemo(
2109
+ () =>
2110
+ initialData?.items?.length
2111
+ ? initialData.items
2112
+ : [makeDefaultIncomeItem()],
2113
+ [], // eslint-disable-line react-hooks/exhaustive-deps
2114
+ );
2115
+
2116
+ const [items, setItems] = React.useState<IncomeItem[]>(defaultItems);
2117
+
2118
+ React.useEffect(() => {
2119
+ if (open) {
2120
+ setItems(
2121
+ initialData?.items?.length
2122
+ ? initialData.items
2123
+ : [makeDefaultIncomeItem()],
2124
+ );
2125
+ }
2126
+ }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
2127
+
2128
+ const updateItem = <K extends keyof IncomeItem>(
2129
+ id: string,
2130
+ key: K,
2131
+ val: IncomeItem[K],
2132
+ ) =>
2133
+ setItems((prev) =>
2134
+ prev.map((item) => (item.id === id ? { ...item, [key]: val } : item)),
2135
+ );
2136
+
2137
+ const removeItem = (id: string) =>
2138
+ setItems((prev) => prev.filter((item) => item.id !== id));
2139
+
2140
+ const addItem = () => setItems((prev) => [...prev, makeDefaultIncomeItem()]);
2141
+
2142
+ const defaultOpenItems = items.length > 0 ? [items[0].id] : [];
2143
+
2144
+ return (
2145
+ <Dialog open={open} onOpenChange={onOpenChange}>
2146
+ <DialogContent className={cn("max-w-lg", className)}>
2147
+ <DialogHeader>
2148
+ <DialogTitle>Edit Income {applicantLabel}</DialogTitle>
2149
+ </DialogHeader>
2150
+ <ModalScroll>
2151
+ <Accordion
2152
+ multiple
2153
+ defaultValue={defaultOpenItems}
2154
+ className="w-full"
2155
+ >
2156
+ {items.map((item, index) => (
2157
+ <AccordionItem key={item.id} value={item.id}>
2158
+ <AccordionItemHeader
2159
+ label={`Income ${index + 1} — ${item.incomeType || "New Income"}`}
2160
+ onRemove={() => removeItem(item.id)}
2161
+ removeLabel="Remove income item"
2162
+ />
2163
+ <AccordionContent>
2164
+ <div className="flex flex-col gap-4 pt-1">
2165
+ <FormField label="Income Type">
2166
+ <Select
2167
+ value={item.incomeType}
2168
+ onValueChange={(val) =>
2169
+ updateItem(item.id, "incomeType", val)
2170
+ }
2171
+ >
2172
+ <SelectTrigger className="w-full">
2173
+ <SelectValue placeholder="Select type" />
2174
+ </SelectTrigger>
2175
+ <SelectContent>
2176
+ {INCOME_TYPES.map((t) => (
2177
+ <SelectItem key={t} value={t}>
2178
+ {t}
2179
+ </SelectItem>
2180
+ ))}
2181
+ </SelectContent>
2182
+ </Select>
2183
+ </FormField>
2184
+
2185
+ <FormField label="Job Title">
2186
+ <Input
2187
+ value={item.jobTitle}
2188
+ onChange={(e) =>
2189
+ updateItem(item.id, "jobTitle", e.target.value)
2190
+ }
2191
+ placeholder="Job title"
2192
+ />
2193
+ </FormField>
2194
+
2195
+ <div className="grid grid-cols-2 gap-4">
2196
+ <FormField label="Start Date">
2197
+ <DatePicker
2198
+ value={
2199
+ item.startDate
2200
+ ? new Date(item.startDate)
2201
+ : undefined
2202
+ }
2203
+ onChange={(d) =>
2204
+ updateItem(
2205
+ item.id,
2206
+ "startDate",
2207
+ d ? d.toISOString().slice(0, 10) : "",
2208
+ )
2209
+ }
2210
+ calendarProps={{
2211
+ fromYear: 1950,
2212
+ toYear: new Date().getFullYear(),
2213
+ }}
2214
+ />
2215
+ </FormField>
2216
+ {!item.stillInPosition && (
2217
+ <FormField label="End Date">
2218
+ <DatePicker
2219
+ value={
2220
+ item.endDate ? new Date(item.endDate) : undefined
2221
+ }
2222
+ onChange={(d) =>
2223
+ updateItem(
2224
+ item.id,
2225
+ "endDate",
2226
+ d ? d.toISOString().slice(0, 10) : "",
2227
+ )
2228
+ }
2229
+ calendarProps={{
2230
+ fromYear: 1950,
2231
+ toYear: new Date().getFullYear(),
2232
+ }}
2233
+ />
2234
+ </FormField>
2235
+ )}
2236
+ </div>
2237
+
2238
+ <div className="flex items-center gap-2">
2239
+ <Checkbox
2240
+ id={`still-in-position-${item.id}`}
2241
+ checked={item.stillInPosition}
2242
+ onCheckedChange={(checked) =>
2243
+ updateItem(
2244
+ item.id,
2245
+ "stillInPosition",
2246
+ checked === true,
2247
+ )
2248
+ }
2249
+ />
2250
+ <label
2251
+ htmlFor={`still-in-position-${item.id}`}
2252
+ className="text-sm cursor-pointer select-none"
2253
+ >
2254
+ Still in position
2255
+ </label>
2256
+ </div>
2257
+
2258
+ <FormField label="Company Name">
2259
+ <Input
2260
+ value={item.companyName}
2261
+ onChange={(e) =>
2262
+ updateItem(item.id, "companyName", e.target.value)
2263
+ }
2264
+ placeholder="Company name"
2265
+ />
2266
+ </FormField>
2267
+
2268
+ <FormField label="Company Address">
2269
+ <AddressAutocomplete
2270
+ value={item.companyAddress}
2271
+ onValueChange={(val) =>
2272
+ updateItem(item.id, "companyAddress", val)
2273
+ }
2274
+ placeholder="Search company address"
2275
+ />
2276
+ </FormField>
2277
+
2278
+ <div className="flex items-start gap-3">
2279
+ <div className="flex-1">
2280
+ <FormField label="Income Amount">
2281
+ <CurrencyInputWithSlider
2282
+ value={item.incomeAmount}
2283
+ min={0}
2284
+ max={1_000_000}
2285
+ step={1_000}
2286
+ onValueChange={(val) =>
2287
+ updateItem(item.id, "incomeAmount", val)
2288
+ }
2289
+ />
2290
+ </FormField>
2291
+ </div>
2292
+ <div className="shrink-0 mt-[1.625rem]">
2293
+ <FrequencyToggle
2294
+ value={item.frequency}
2295
+ onValueChange={(val) =>
2296
+ updateItem(item.id, "frequency", val)
2297
+ }
2298
+ />
2299
+ </div>
2300
+ </div>
2301
+
2302
+ <FormField label="Type of Company">
2303
+ <RadioGroup
2304
+ value={item.companyType}
2305
+ onValueChange={(val) =>
2306
+ updateItem(
2307
+ item.id,
2308
+ "companyType",
2309
+ val as IncomeItem["companyType"],
2310
+ )
2311
+ }
2312
+ className="flex gap-4"
2313
+ >
2314
+ {(["Public", "Private"] as const).map((opt) => (
2315
+ <label
2316
+ key={opt}
2317
+ className="flex items-center gap-2 cursor-pointer text-sm"
2318
+ >
2319
+ <RadioGroupItem value={opt} />
2320
+ {opt}
2321
+ </label>
2322
+ ))}
2323
+ </RadioGroup>
2324
+ </FormField>
2325
+ </div>
2326
+ </AccordionContent>
2327
+ </AccordionItem>
2328
+ ))}
2329
+ </Accordion>
2330
+
2331
+ <Button
2332
+ variant="outline"
2333
+ onClick={addItem}
2334
+ className="w-full gap-1.5"
2335
+ >
2336
+ <Plus className="h-4 w-4" />
2337
+ Add More
2338
+ </Button>
2339
+ </ModalScroll>
2340
+ <DialogFooter>
2341
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
2342
+ Cancel
2343
+ </Button>
2344
+ <Button
2345
+ onClick={() => {
2346
+ onSave({ items });
2347
+ onOpenChange(false);
2348
+ }}
2349
+ >
2350
+ Save
2351
+ </Button>
2352
+ </DialogFooter>
2353
+ </DialogContent>
2354
+ </Dialog>
2355
+ );
2356
+ }
2357
+
2358
+ // ---------------------------------------------------------------------------
2359
+ // EditExpensesModal
2360
+ // ---------------------------------------------------------------------------
2361
+
2362
+ export function EditExpensesModal({
2363
+ open,
2364
+ onOpenChange,
2365
+ applicantLabel = "Applicant",
2366
+ initialData,
2367
+ onSave,
2368
+ className,
2369
+ }: EditExpensesModalProps) {
2370
+ const defaultItems = React.useMemo(
2371
+ () =>
2372
+ initialData?.items?.length
2373
+ ? initialData.items
2374
+ : [makeDefaultExpenseItem()],
2375
+ [], // eslint-disable-line react-hooks/exhaustive-deps
2376
+ );
2377
+
2378
+ const [items, setItems] = React.useState<ExpenseItem[]>(defaultItems);
2379
+
2380
+ React.useEffect(() => {
2381
+ if (open) {
2382
+ setItems(
2383
+ initialData?.items?.length
2384
+ ? initialData.items
2385
+ : [makeDefaultExpenseItem()],
2386
+ );
2387
+ }
2388
+ }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
2389
+
2390
+ const updateItem = <K extends keyof ExpenseItem>(
2391
+ id: string,
2392
+ key: K,
2393
+ val: ExpenseItem[K],
2394
+ ) =>
2395
+ setItems((prev) =>
2396
+ prev.map((item) => (item.id === id ? { ...item, [key]: val } : item)),
2397
+ );
2398
+
2399
+ const removeItem = (id: string) =>
2400
+ setItems((prev) => prev.filter((item) => item.id !== id));
2401
+
2402
+ const addItem = () => setItems((prev) => [...prev, makeDefaultExpenseItem()]);
2403
+
2404
+ const defaultOpenItems = items.length > 0 ? [items[0].id] : [];
2405
+
2406
+ return (
2407
+ <Dialog open={open} onOpenChange={onOpenChange}>
2408
+ <DialogContent className={cn("max-w-lg", className)}>
2409
+ <DialogHeader>
2410
+ <DialogTitle>Edit Expenses {applicantLabel}</DialogTitle>
2411
+ </DialogHeader>
2412
+ <ModalScroll>
2413
+ <Accordion
2414
+ multiple
2415
+ defaultValue={defaultOpenItems}
2416
+ className="w-full"
2417
+ >
2418
+ {items.map((item, index) => (
2419
+ <AccordionItem key={item.id} value={item.id}>
2420
+ <AccordionItemHeader
2421
+ label={`Expense ${index + 1} — ${item.expenseType || "New Expense"}`}
2422
+ onRemove={() => removeItem(item.id)}
2423
+ removeLabel="Remove expense item"
2424
+ />
2425
+ <AccordionContent>
2426
+ <div className="flex flex-col gap-4 pt-1">
2427
+ <FormField label="Expense Type">
2428
+ <Select
2429
+ value={item.expenseType}
2430
+ onValueChange={(val) =>
2431
+ updateItem(item.id, "expenseType", val)
2432
+ }
2433
+ >
2434
+ <SelectTrigger className="w-full">
2435
+ <SelectValue placeholder="Select type" />
2436
+ </SelectTrigger>
2437
+ <SelectContent>
2438
+ {EXPENSE_TYPES.map((t) => (
2439
+ <SelectItem key={t} value={t}>
2440
+ {t}
2441
+ </SelectItem>
2442
+ ))}
2443
+ </SelectContent>
2444
+ </Select>
2445
+ </FormField>
2446
+
2447
+ <div className="flex items-start gap-3">
2448
+ <div className="flex-1">
2449
+ <FormField label="Amount">
2450
+ <CurrencyInputWithSlider
2451
+ value={item.amount}
2452
+ min={0}
2453
+ max={100_000}
2454
+ step={100}
2455
+ onValueChange={(val) =>
2456
+ updateItem(item.id, "amount", val)
2457
+ }
2458
+ />
2459
+ </FormField>
2460
+ </div>
2461
+ <div className="shrink-0 mt-[1.625rem]">
2462
+ <FrequencyToggle
2463
+ value={item.frequency}
2464
+ onValueChange={(val) =>
2465
+ updateItem(item.id, "frequency", val)
2466
+ }
2467
+ />
2468
+ </div>
2469
+ </div>
2470
+
2471
+ <FormField label="Ownership (%)">
2472
+ <OwnershipSplit
2473
+ owners={[
2474
+ {
2475
+ id: "main",
2476
+ name: "Main Applicant",
2477
+ share: item.mainShare,
2478
+ },
2479
+ {
2480
+ id: "co",
2481
+ name: "Co-Applicant",
2482
+ share: item.coShare,
2483
+ },
2484
+ ]}
2485
+ onOwnersChange={(owners) => {
2486
+ const main =
2487
+ owners.find((o) => o.id === "main")?.share ??
2488
+ item.mainShare;
2489
+ const co =
2490
+ owners.find((o) => o.id === "co")?.share ??
2491
+ item.coShare;
2492
+ updateItem(item.id, "mainShare", main);
2493
+ updateItem(item.id, "coShare", co);
2494
+ }}
2495
+ />
2496
+ </FormField>
2497
+ </div>
2498
+ </AccordionContent>
2499
+ </AccordionItem>
2500
+ ))}
2501
+ </Accordion>
2502
+
2503
+ <Button
2504
+ variant="outline"
2505
+ onClick={addItem}
2506
+ className="w-full gap-1.5"
2507
+ >
2508
+ <Plus className="h-4 w-4" />
2509
+ Add More
2510
+ </Button>
2511
+ </ModalScroll>
2512
+ <DialogFooter>
2513
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
2514
+ Cancel
2515
+ </Button>
2516
+ <Button
2517
+ onClick={() => {
2518
+ onSave({ items });
2519
+ onOpenChange(false);
2520
+ }}
2521
+ >
2522
+ Save
2523
+ </Button>
2524
+ </DialogFooter>
2525
+ </DialogContent>
2526
+ </Dialog>
2527
+ );
2528
+ }