@wealthx/shadcn 1.1.0 → 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.
- package/.turbo/turbo-build.log +235 -154
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-6OJF6XRN.mjs → chunk-24FUO7TD.mjs} +4 -8
- package/dist/{chunk-4AJ5HWHD.mjs → chunk-2I5S2AMY.mjs} +3 -3
- package/dist/chunk-2SF672SZ.mjs +161 -0
- package/dist/{chunk-GPRJQ24C.mjs → chunk-34NWQURD.mjs} +2 -2
- package/dist/{chunk-MQ72DIBH.mjs → chunk-3GF7OVTP.mjs} +14 -5
- package/dist/chunk-3WMX6KWS.mjs +245 -0
- package/dist/{chunk-PMKODV6M.mjs → chunk-462HMNO4.mjs} +6 -10
- package/dist/chunk-4CX4SBRO.mjs +153 -0
- package/dist/chunk-4MN6UQHG.mjs +443 -0
- package/dist/{chunk-GLW2UO6O.mjs → chunk-5QQVZTVZ.mjs} +82 -61
- package/dist/{chunk-BGP2N52Z.mjs → chunk-66MI7Q4B.mjs} +5 -5
- package/dist/chunk-6FCGKSZX.mjs +268 -0
- package/dist/{chunk-CGOKTPXU.mjs → chunk-6JQFUE5I.mjs} +20 -23
- package/dist/{chunk-Z3MK2KKZ.mjs → chunk-7DHU4VGG.mjs} +7 -3
- package/dist/{chunk-VZ2NR7L3.mjs → chunk-7PYJD5JI.mjs} +35 -27
- package/dist/{chunk-JU2RUWHF.mjs → chunk-7XJHLGUV.mjs} +1 -1
- package/dist/{chunk-BMFN37JH.mjs → chunk-7YAU5CY6.mjs} +1 -1
- package/dist/chunk-A56YQQHG.mjs +402 -0
- package/dist/chunk-AH52LG6N.mjs +315 -0
- package/dist/{chunk-SLWCCURD.mjs → chunk-CLIN5525.mjs} +8 -4
- package/dist/{chunk-3VQNJ235.mjs → chunk-CSDO6VBW.mjs} +7 -0
- package/dist/chunk-D4ILTPOG.mjs +293 -0
- package/dist/{chunk-HS7TFG7V.mjs → chunk-D6ID6M4V.mjs} +1 -1
- package/dist/chunk-DOH3EHX7.mjs +378 -0
- package/dist/{chunk-MJIEMGRD.mjs → chunk-EFRENWEJ.mjs} +9 -17
- package/dist/{chunk-YBXCIF5Q.mjs → chunk-ERGGHC2V.mjs} +36 -49
- package/dist/{chunk-OXQQNQZI.mjs → chunk-FEZKMUCF.mjs} +10 -1
- package/dist/{chunk-55CEW76V.mjs → chunk-FH6QVUVZ.mjs} +1 -1
- package/dist/chunk-FMAXJ2SI.mjs +71 -0
- package/dist/chunk-FZIXGLMV.mjs +173 -0
- package/dist/{chunk-DS2AMHN2.mjs → chunk-GYMYRIZP.mjs} +2 -2
- package/dist/{chunk-KQDD5MU3.mjs → chunk-H45TKD34.mjs} +5 -5
- package/dist/{chunk-BBJBJSXQ.mjs → chunk-J5UICVJS.mjs} +1 -1
- package/dist/{chunk-RL772EH7.mjs → chunk-JHJHG4GO.mjs} +4 -12
- package/dist/{chunk-RN67642N.mjs → chunk-KMCGSZTX.mjs} +47 -41
- package/dist/{chunk-FHNT55I5.mjs → chunk-KUDCQ4FI.mjs} +4 -4
- package/dist/chunk-LE6YFY6D.mjs +209 -0
- package/dist/{chunk-NLLKTU4B.mjs → chunk-LLVQKSU3.mjs} +21 -17
- package/dist/{chunk-KKHTJNMM.mjs → chunk-MARPPFOJ.mjs} +8 -4
- package/dist/{chunk-6AFMNC42.mjs → chunk-N2PT566P.mjs} +15 -11
- package/dist/chunk-NLCKVHWB.mjs +161 -0
- package/dist/{chunk-YN5SYTOO.mjs → chunk-NQPOYKAQ.mjs} +9 -5
- package/dist/{chunk-ZZV5JVNW.mjs → chunk-NSLMILBT.mjs} +3 -7
- package/dist/chunk-NXA3CZ7A.mjs +248 -0
- package/dist/chunk-OGOYQ7BG.mjs +150 -0
- package/dist/{chunk-3NQGYJEZ.mjs → chunk-P6AM5V7O.mjs} +10 -18
- package/dist/{chunk-CZ3BW5GL.mjs → chunk-P76HMUI6.mjs} +5 -11
- package/dist/chunk-PCPLO5HT.mjs +671 -0
- package/dist/chunk-PG6K5XEC.mjs +475 -0
- package/dist/{chunk-5JGQAAQV.mjs → chunk-PJHPSRYD.mjs} +84 -62
- package/dist/{chunk-DDPA2XXS.mjs → chunk-PMB3A7V3.mjs} +2 -2
- package/dist/chunk-PR6V5XKM.mjs +209 -0
- package/dist/{chunk-46OFHMQA.mjs → chunk-Q76O3RIQ.mjs} +10 -6
- package/dist/chunk-QVKWW6KE.mjs +272 -0
- package/dist/chunk-RGU7HOEC.mjs +140 -0
- package/dist/{chunk-JF4PHPD5.mjs → chunk-RGVKLTLH.mjs} +4 -4
- package/dist/{chunk-VG6UF6UT.mjs → chunk-RP3SQYA3.mjs} +2 -2
- package/dist/chunk-RRBS6D63.mjs +163 -0
- package/dist/{chunk-UEL4RD5P.mjs → chunk-SMQ3DG25.mjs} +80 -67
- package/dist/chunk-SPJ5KXW7.mjs +199 -0
- package/dist/chunk-SYOD63OZ.mjs +225 -0
- package/dist/chunk-UFYSFDER.mjs +42 -0
- package/dist/chunk-VACKZOMY.mjs +190 -0
- package/dist/chunk-VLQZANBF.mjs +42 -0
- package/dist/chunk-WA6O6EUR.mjs +1885 -0
- package/dist/{chunk-E3K6O4FZ.mjs → chunk-WAZD7NFU.mjs} +5 -2
- package/dist/chunk-WG6JGJXB.mjs +165 -0
- package/dist/{chunk-I64K754C.mjs → chunk-WNGWBVLV.mjs} +2 -2
- package/dist/{chunk-3U7SD3MS.mjs → chunk-WOEHFRGB.mjs} +3 -3
- package/dist/{chunk-DKZRJOMF.mjs → chunk-XIRTEFKH.mjs} +12 -12
- package/dist/chunk-Y6DWJSKZ.mjs +79 -0
- package/dist/chunk-YKPROFLB.mjs +161 -0
- package/dist/{chunk-CJ46PDXE.mjs → chunk-ZRO5JO3H.mjs} +106 -66
- package/dist/{chunk-VYMHBV6D.mjs → chunk-ZU4NV6RG.mjs} +5 -3
- package/dist/components/ui/accordion.js +40 -4
- package/dist/components/ui/accordion.mjs +2 -2
- package/dist/components/ui/add-column-modal.js +789 -0
- package/dist/components/ui/add-column-modal.mjs +17 -0
- package/dist/components/ui/add-lead-modal.js +647 -0
- package/dist/components/ui/add-lead-modal.mjs +16 -0
- package/dist/components/ui/ai-assistant-drawer.js +686 -0
- package/dist/components/ui/ai-assistant-drawer.mjs +16 -0
- package/dist/components/ui/alert-dialog.js +37 -5
- package/dist/components/ui/alert-dialog.mjs +4 -4
- package/dist/components/ui/alert.js +37 -11
- package/dist/components/ui/alert.mjs +2 -2
- package/dist/components/ui/avatar.js +36 -8
- package/dist/components/ui/avatar.mjs +2 -2
- package/dist/components/ui/backoffice-alert-history-chart.js +624 -0
- package/dist/components/ui/backoffice-alert-history-chart.mjs +16 -0
- package/dist/components/ui/backoffice-contact-history-chart.js +687 -0
- package/dist/components/ui/backoffice-contact-history-chart.mjs +16 -0
- package/dist/components/ui/badge.js +37 -2
- package/dist/components/ui/badge.mjs +2 -2
- package/dist/components/ui/borrowing-capacity-line-chart.js +639 -0
- package/dist/components/ui/borrowing-capacity-line-chart.mjs +16 -0
- package/dist/components/ui/button.js +35 -3
- package/dist/components/ui/button.mjs +2 -2
- package/dist/components/ui/calendar.js +43 -19
- package/dist/components/ui/calendar.mjs +3 -3
- package/dist/components/ui/card.js +40 -4
- package/dist/components/ui/card.mjs +2 -2
- package/dist/components/ui/cash-balance-line-chart.js +627 -0
- package/dist/components/ui/cash-balance-line-chart.mjs +16 -0
- package/dist/components/ui/cashflow-bar-chart.js +123 -69
- package/dist/components/ui/cashflow-bar-chart.mjs +8 -8
- package/dist/components/ui/checkbox.js +36 -5
- package/dist/components/ui/checkbox.mjs +2 -3
- package/dist/components/ui/chip.js +37 -2
- package/dist/components/ui/chip.mjs +3 -3
- package/dist/components/ui/combobox.js +68 -49
- package/dist/components/ui/combobox.mjs +2 -2
- package/dist/components/ui/data-table.js +160 -88
- package/dist/components/ui/data-table.mjs +10 -11
- package/dist/components/ui/date-picker.js +44 -20
- package/dist/components/ui/date-picker.mjs +6 -7
- package/dist/components/ui/dialog.js +44 -12
- package/dist/components/ui/dialog.mjs +4 -4
- package/dist/components/ui/drawer.js +46 -10
- package/dist/components/ui/drawer.mjs +3 -3
- package/dist/components/ui/dropdown-menu.js +40 -16
- package/dist/components/ui/dropdown-menu.mjs +3 -3
- package/dist/components/ui/empty.js +41 -5
- package/dist/components/ui/empty.mjs +2 -2
- package/dist/components/ui/expense-bar-chart.js +165 -66
- package/dist/components/ui/expense-bar-chart.mjs +8 -8
- package/dist/components/ui/field.js +53 -21
- package/dist/components/ui/field.mjs +4 -4
- package/dist/components/ui/financial-cards.js +1002 -0
- package/dist/components/ui/financial-cards.mjs +24 -0
- package/dist/components/ui/financial-drawers.js +637 -0
- package/dist/components/ui/financial-drawers.mjs +17 -0
- package/dist/components/ui/financial-primitives.js +218 -0
- package/dist/components/ui/financial-primitives.mjs +22 -0
- package/dist/components/ui/financial-sections.js +1422 -0
- package/dist/components/ui/financial-sections.mjs +30 -0
- package/dist/components/ui/form-primitives.js +682 -0
- package/dist/components/ui/form-primitives.mjs +19 -0
- package/dist/components/ui/income-bar-chart.js +163 -65
- package/dist/components/ui/income-bar-chart.mjs +8 -8
- package/dist/components/ui/input-group.js +43 -7
- package/dist/components/ui/input-group.mjs +5 -5
- package/dist/components/ui/input-otp.js +39 -3
- package/dist/components/ui/input-otp.mjs +2 -2
- package/dist/components/ui/input.js +34 -2
- package/dist/components/ui/input.mjs +2 -2
- package/dist/components/ui/kanban-column.js +1143 -0
- package/dist/components/ui/kanban-column.mjs +20 -0
- package/dist/components/ui/label.js +35 -7
- package/dist/components/ui/label.mjs +2 -2
- package/dist/components/ui/opportunity-card.js +960 -0
- package/dist/components/ui/opportunity-card.mjs +20 -0
- package/dist/components/ui/opportunity-edit-modals.js +3360 -0
- package/dist/components/ui/opportunity-edit-modals.mjs +37 -0
- package/dist/components/ui/opportunity-summary-tab.js +4365 -0
- package/dist/components/ui/opportunity-summary-tab.mjs +34 -0
- package/dist/components/ui/pagination.js +35 -3
- package/dist/components/ui/pagination.mjs +3 -3
- package/dist/components/ui/pipeline-alerts.js +103 -0
- package/dist/components/ui/pipeline-alerts.mjs +8 -0
- package/dist/components/ui/pipeline-board.js +1408 -0
- package/dist/components/ui/pipeline-board.mjs +24 -0
- package/dist/components/ui/pipeline-chart.js +216 -0
- package/dist/components/ui/pipeline-chart.mjs +10 -0
- package/dist/components/ui/pipeline-dialogs.js +1183 -0
- package/dist/components/ui/pipeline-dialogs.mjs +23 -0
- package/dist/components/ui/pipeline-primitives.js +300 -0
- package/dist/components/ui/pipeline-primitives.mjs +11 -0
- package/dist/components/ui/popover.js +45 -4
- package/dist/components/ui/popover.mjs +3 -3
- package/dist/components/ui/progress.js +33 -1
- package/dist/components/ui/progress.mjs +2 -2
- package/dist/components/ui/property-cashflow-doughnut-chart.js +523 -0
- package/dist/components/ui/property-cashflow-doughnut-chart.mjs +16 -0
- package/dist/components/ui/property-debt-equity-doughnut-chart.js +521 -0
- package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +16 -0
- package/dist/components/ui/property-mobile-estimate-line-chart.js +682 -0
- package/dist/components/ui/property-mobile-estimate-line-chart.mjs +16 -0
- package/dist/components/ui/radio-group.js +33 -1
- package/dist/components/ui/radio-group.mjs +2 -2
- package/dist/components/ui/select.js +66 -26
- package/dist/components/ui/select.mjs +3 -3
- package/dist/components/ui/separator.js +33 -1
- package/dist/components/ui/separator.mjs +2 -2
- package/dist/components/ui/sheet.js +37 -9
- package/dist/components/ui/sheet.mjs +3 -3
- package/dist/components/ui/skeleton.js +33 -1
- package/dist/components/ui/skeleton.mjs +2 -2
- package/dist/components/ui/slider.js +86 -102
- package/dist/components/ui/slider.mjs +2 -2
- package/dist/components/ui/spinner.js +33 -1
- package/dist/components/ui/spinner.mjs +2 -2
- package/dist/components/ui/stage-timeline.js +579 -0
- package/dist/components/ui/stage-timeline.mjs +15 -0
- package/dist/components/ui/switch.js +37 -4
- package/dist/components/ui/switch.mjs +2 -3
- package/dist/components/ui/table.js +37 -5
- package/dist/components/ui/table.mjs +2 -2
- package/dist/components/ui/tabs.js +36 -12
- package/dist/components/ui/tabs.mjs +2 -2
- package/dist/components/ui/textarea.js +34 -2
- package/dist/components/ui/textarea.mjs +2 -2
- package/dist/components/ui/toggle-group.js +35 -4
- package/dist/components/ui/toggle-group.mjs +3 -4
- package/dist/components/ui/toggle.js +35 -4
- package/dist/components/ui/toggle.mjs +2 -3
- package/dist/components/ui/tooltip.js +51 -22
- package/dist/components/ui/tooltip.mjs +3 -3
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +528 -0
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +16 -0
- package/dist/components/ui/transactions-income-expense-bar-chart.js +76 -38
- package/dist/components/ui/transactions-income-expense-bar-chart.mjs +8 -8
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +528 -0
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +16 -0
- package/dist/index.js +11616 -3831
- package/dist/index.mjs +333 -161
- package/dist/lib/theme-provider.js +10 -1
- package/dist/lib/theme-provider.mjs +1 -1
- package/dist/lib/typography.js +8 -0
- package/dist/lib/typography.mjs +3 -1
- package/dist/lib/utils.js +33 -1
- package/dist/lib/utils.mjs +1 -1
- package/dist/styles.css +1 -1
- package/package.json +140 -5
- package/src/components/index.tsx +296 -42
- package/src/components/ui/accordion.tsx +6 -3
- package/src/components/ui/add-column-modal.tsx +339 -0
- package/src/components/ui/add-lead-modal.tsx +290 -0
- package/src/components/ui/ai-assistant-drawer.tsx +408 -0
- package/src/components/ui/alert-dialog.tsx +80 -54
- package/src/components/ui/alert.tsx +28 -28
- package/src/components/ui/avatar.tsx +30 -29
- package/src/components/ui/backoffice-alert-history-chart.tsx +260 -0
- package/src/components/ui/backoffice-contact-history-chart.tsx +325 -0
- package/src/components/ui/badge.tsx +17 -15
- package/src/components/ui/borrowing-capacity-line-chart.tsx +357 -0
- package/src/components/ui/button.tsx +30 -27
- package/src/components/ui/calendar.tsx +53 -67
- package/src/components/ui/card.tsx +27 -24
- package/src/components/ui/cash-balance-line-chart.tsx +302 -0
- package/src/components/ui/cashflow-bar-chart.tsx +104 -77
- package/src/components/ui/chart-shared.tsx +176 -15
- package/src/components/ui/checkbox.tsx +30 -26
- package/src/components/ui/combobox.tsx +78 -72
- package/src/components/ui/data-table.tsx +160 -99
- package/src/components/ui/date-picker.tsx +0 -2
- package/src/components/ui/dialog.tsx +70 -60
- package/src/components/ui/drawer.tsx +57 -48
- package/src/components/ui/dropdown-menu.tsx +90 -82
- package/src/components/ui/empty.tsx +31 -27
- package/src/components/ui/expense-bar-chart.tsx +83 -65
- package/src/components/ui/field.tsx +70 -62
- package/src/components/ui/financial-cards.tsx +830 -0
- package/src/components/ui/financial-drawers.tsx +339 -0
- package/src/components/ui/financial-primitives.tsx +331 -0
- package/src/components/ui/financial-sections.tsx +672 -0
- package/src/components/ui/form-primitives.tsx +536 -0
- package/src/components/ui/income-bar-chart.tsx +79 -60
- package/src/components/ui/input-group.tsx +41 -34
- package/src/components/ui/input-otp.tsx +29 -24
- package/src/components/ui/input.tsx +8 -8
- package/src/components/ui/kanban-column.tsx +333 -0
- package/src/components/ui/label.tsx +9 -12
- package/src/components/ui/opportunity-card.tsx +616 -0
- package/src/components/ui/opportunity-edit-modals.tsx +2528 -0
- package/src/components/ui/opportunity-summary-tab.tsx +579 -0
- package/src/components/ui/pipeline-alerts.tsx +74 -0
- package/src/components/ui/pipeline-board.tsx +268 -0
- package/src/components/ui/pipeline-chart.tsx +173 -0
- package/src/components/ui/pipeline-dialogs.tsx +303 -0
- package/src/components/ui/pipeline-primitives.tsx +108 -0
- package/src/components/ui/popover.tsx +41 -36
- package/src/components/ui/property-cashflow-doughnut-chart.tsx +188 -0
- package/src/components/ui/property-debt-equity-doughnut-chart.tsx +185 -0
- package/src/components/ui/property-mobile-estimate-line-chart.tsx +393 -0
- package/src/components/ui/select.tsx +65 -52
- package/src/components/ui/sheet.tsx +55 -52
- package/src/components/ui/slider.tsx +54 -77
- package/src/components/ui/stage-timeline.tsx +205 -0
- package/src/components/ui/switch.tsx +42 -29
- package/src/components/ui/table.tsx +28 -28
- package/src/components/ui/tabs.tsx +22 -28
- package/src/components/ui/textarea.tsx +8 -8
- package/src/components/ui/toggle-group.tsx +0 -2
- package/src/components/ui/toggle.tsx +13 -15
- package/src/components/ui/tooltip.tsx +30 -28
- package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +191 -0
- package/src/components/ui/transactions-income-expense-bar-chart.tsx +45 -38
- package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +191 -0
- package/src/lib/theme-provider.tsx +10 -0
- package/src/lib/typography.ts +9 -0
- package/src/lib/utils.ts +41 -3
- package/src/styles/globals.css +371 -124
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +27 -0
- package/dist/chunk-3EQP72AW.mjs +0 -58
- package/dist/chunk-K74JRTJR.mjs +0 -105
- 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
|
+
}
|