@burdenoff/microfe-housingvista 2026.703.7 → 2026.703.8
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/dist/housingvista/components/ContactOwnerDialog.js +32 -31
- package/dist/housingvista/components/ContactOwnerDialog.js.map +1 -1
- package/dist/housingvista/pages/finance/HomeLoanDetailPage.js +67 -71
- package/dist/housingvista/pages/finance/HomeLoanDetailPage.js.map +1 -1
- package/dist/housingvista/pages/legal/LegalServiceDetailPage.js +97 -101
- package/dist/housingvista/pages/legal/LegalServiceDetailPage.js.map +1 -1
- package/dist/housingvista/pages/marketplace/ServiceDetailPage.js +94 -98
- package/dist/housingvista/pages/marketplace/ServiceDetailPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.js +376 -379
- package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,64 +8,65 @@ import { Send as l } from "lucide-react";
|
|
|
8
8
|
import { Fragment as u, jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
9
9
|
import { Button as p, ResponsiveDialog as m, ResponsiveDialogContent as h, ResponsiveDialogDescription as g, ResponsiveDialogHeader as _, ResponsiveDialogTitle as v } from "@burdenoff/fe-libs/ui";
|
|
10
10
|
//#region src/housingvista/components/ContactOwnerDialog.tsx
|
|
11
|
-
var y = ({ entityId: y, entityType: b, title: x, pagePath: S,
|
|
12
|
-
let { currentUser:
|
|
11
|
+
var y = ({ entityId: y, entityType: b, title: x, pagePath: S, triggerContent: C, triggerVariant: w = "outline", triggerClassName: T, heading: E = "Contact", description: D }) => {
|
|
12
|
+
let { currentUser: O } = e(), { openConversation: k, loading: A } = n(), { sendMessage: j, loading: M } = i(), [N, P] = c(!1), [F, I] = c(), [L, R] = c(""), { data: z, loading: B } = r(F), V = s(null);
|
|
13
13
|
o(() => {
|
|
14
|
-
if (!
|
|
14
|
+
if (!N || F) return;
|
|
15
15
|
let e = !1;
|
|
16
|
-
return
|
|
16
|
+
return k({
|
|
17
17
|
entityId: y,
|
|
18
18
|
entityType: b,
|
|
19
19
|
title: x,
|
|
20
20
|
contextHref: S,
|
|
21
21
|
pagePath: S
|
|
22
22
|
}).then((t) => {
|
|
23
|
-
e ||
|
|
23
|
+
e || I(t.id);
|
|
24
24
|
}).catch((e) => a.error("Could not open the conversation", e.message)), () => {
|
|
25
25
|
e = !0;
|
|
26
26
|
};
|
|
27
27
|
}, [
|
|
28
|
-
j,
|
|
29
28
|
N,
|
|
30
|
-
|
|
29
|
+
F,
|
|
30
|
+
k,
|
|
31
31
|
y,
|
|
32
32
|
b,
|
|
33
33
|
x,
|
|
34
34
|
S
|
|
35
35
|
]), o(() => {
|
|
36
|
-
|
|
37
|
-
}, [
|
|
38
|
-
let
|
|
39
|
-
let e =
|
|
40
|
-
if (!(!e || !
|
|
41
|
-
await
|
|
36
|
+
V.current?.scrollTo({ top: V.current.scrollHeight });
|
|
37
|
+
}, [z]);
|
|
38
|
+
let H = async () => {
|
|
39
|
+
let e = L.trim();
|
|
40
|
+
if (!(!e || !F)) try {
|
|
41
|
+
await j(F, e), R("");
|
|
42
42
|
} catch (e) {
|
|
43
43
|
a.error("Message not sent", e.message);
|
|
44
44
|
}
|
|
45
|
-
},
|
|
46
|
-
return /* @__PURE__ */ f(u, { children: [/* @__PURE__ */ d(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
}, U = O?.id ?? "";
|
|
46
|
+
return /* @__PURE__ */ f(u, { children: [/* @__PURE__ */ d(p, {
|
|
47
|
+
type: "button",
|
|
48
|
+
variant: w,
|
|
49
|
+
className: T,
|
|
50
|
+
onClick: () => P(!0),
|
|
50
51
|
children: C
|
|
51
52
|
}), /* @__PURE__ */ d(m, {
|
|
52
|
-
open:
|
|
53
|
-
onOpenChange:
|
|
53
|
+
open: N,
|
|
54
|
+
onOpenChange: P,
|
|
54
55
|
children: /* @__PURE__ */ f(h, {
|
|
55
56
|
className: "sm:max-w-lg",
|
|
56
57
|
children: [
|
|
57
|
-
/* @__PURE__ */ f(_, { children: [/* @__PURE__ */ d(v, { children:
|
|
58
|
+
/* @__PURE__ */ f(_, { children: [/* @__PURE__ */ d(v, { children: E }), /* @__PURE__ */ d(g, { children: D ?? `Start a conversation about ${x}. Messages are saved and delivered in your inbox.` })] }),
|
|
58
59
|
/* @__PURE__ */ d("div", {
|
|
59
|
-
ref:
|
|
60
|
+
ref: V,
|
|
60
61
|
className: "max-h-72 min-h-40 space-y-2 overflow-y-auto rounded-md border border-border-subtle bg-bg-sunken p-3",
|
|
61
|
-
children:
|
|
62
|
+
children: A || B ? /* @__PURE__ */ d("p", {
|
|
62
63
|
className: "py-8 text-center text-sm text-text-secondary",
|
|
63
64
|
children: "Loading conversation…"
|
|
64
|
-
}) :
|
|
65
|
+
}) : z.length === 0 ? /* @__PURE__ */ d("p", {
|
|
65
66
|
className: "py-8 text-center text-sm text-text-secondary",
|
|
66
67
|
children: "No messages yet. Say hello to get the conversation started."
|
|
67
|
-
}) :
|
|
68
|
-
let n = e.authorId ===
|
|
68
|
+
}) : z.map((e) => {
|
|
69
|
+
let n = e.authorId === U;
|
|
69
70
|
return /* @__PURE__ */ d("div", {
|
|
70
71
|
className: t("flex", n ? "justify-end" : "justify-start"),
|
|
71
72
|
children: /* @__PURE__ */ d("div", {
|
|
@@ -78,18 +79,18 @@ var y = ({ entityId: y, entityType: b, title: x, pagePath: S, trigger: C, headin
|
|
|
78
79
|
/* @__PURE__ */ f("div", {
|
|
79
80
|
className: "mt-3 flex items-end gap-2",
|
|
80
81
|
children: [/* @__PURE__ */ d("textarea", {
|
|
81
|
-
value:
|
|
82
|
-
onChange: (e) =>
|
|
82
|
+
value: L,
|
|
83
|
+
onChange: (e) => R(e.target.value),
|
|
83
84
|
onKeyDown: (e) => {
|
|
84
|
-
e.key === "Enter" && !e.shiftKey && (e.preventDefault(),
|
|
85
|
+
e.key === "Enter" && !e.shiftKey && (e.preventDefault(), H());
|
|
85
86
|
},
|
|
86
87
|
rows: 2,
|
|
87
88
|
placeholder: "Write a message…",
|
|
88
89
|
className: "min-h-10 flex-1 resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
89
90
|
}), /* @__PURE__ */ f(p, {
|
|
90
91
|
type: "button",
|
|
91
|
-
onClick:
|
|
92
|
-
disabled: !
|
|
92
|
+
onClick: H,
|
|
93
|
+
disabled: !L.trim() || !F || M,
|
|
93
94
|
children: [/* @__PURE__ */ d(l, { className: "h-4 w-4" }), /* @__PURE__ */ d("span", {
|
|
94
95
|
className: "ml-1",
|
|
95
96
|
children: "Send"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContactOwnerDialog.js","names":[],"sources":["../../../src/housingvista/components/ContactOwnerDialog.tsx"],"sourcesContent":["/**\n * Real messaging surface backed by wspace-conversations-svc. Any \"Contact /\n * Message / Apply / Contact Owner\" CTA renders this: it resolves (get-or-create)\n * the entity's conversation, shows the persisted thread, and posts messages via\n * `sendMessage`. No stub, no dead-end — everything is persisted server-side.\n * @module housingvista\n */\n\nimport { useEffect, useRef, useState, type FC, type ReactNode } from 'react';\nimport { Send } from 'lucide-react';\nimport {\n Button,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n} from '@burdenoff/fe-libs/ui';\nimport { useContactConversation, useConversationMessages, useSendMessage } from '../hooks';\nimport { useHousingVista } from '../context/HousingVistaContext';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/helpers';\n\ninterface ContactOwnerDialogProps {\n /** Stable id of the thing being discussed (property/service/listing id). */\n entityId: string;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string;\n /** Title used when the conversation is first created. */\n title: string;\n /** Router path back into this surface (deep link). */\n pagePath: string;\n /**
|
|
1
|
+
{"version":3,"file":"ContactOwnerDialog.js","names":[],"sources":["../../../src/housingvista/components/ContactOwnerDialog.tsx"],"sourcesContent":["/**\n * Real messaging surface backed by wspace-conversations-svc. Any \"Contact /\n * Message / Apply / Contact Owner\" CTA renders this: it resolves (get-or-create)\n * the entity's conversation, shows the persisted thread, and posts messages via\n * `sendMessage`. No stub, no dead-end — everything is persisted server-side.\n * @module housingvista\n */\n\nimport { useEffect, useRef, useState, type FC, type ReactNode } from 'react';\nimport { Send } from 'lucide-react';\nimport {\n Button,\n type ButtonProps,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n} from '@burdenoff/fe-libs/ui';\nimport { useContactConversation, useConversationMessages, useSendMessage } from '../hooks';\nimport { useHousingVista } from '../context/HousingVistaContext';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/helpers';\n\ninterface ContactOwnerDialogProps {\n /** Stable id of the thing being discussed (property/service/listing id). */\n entityId: string;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string;\n /** Title used when the conversation is first created. */\n title: string;\n /** Router path back into this surface (deep link). */\n pagePath: string;\n /** Inner content of the trigger button (icon + label). */\n triggerContent: ReactNode;\n /** Trigger button variant (defaults to `outline`). */\n triggerVariant?: ButtonProps['variant'];\n /** Extra classes for the trigger button. */\n triggerClassName?: string;\n /** Dialog heading, defaults to \"Contact\". */\n heading?: string;\n description?: string;\n}\n\nexport const ContactOwnerDialog: FC<ContactOwnerDialogProps> = ({\n entityId,\n entityType,\n title,\n pagePath,\n triggerContent,\n triggerVariant = 'outline',\n triggerClassName,\n heading = 'Contact',\n description,\n}) => {\n const { currentUser } = useHousingVista();\n const { openConversation, loading: opening } = useContactConversation();\n const { sendMessage, loading: sending } = useSendMessage();\n const [open, setOpen] = useState(false);\n const [conversationId, setConversationId] = useState<string | undefined>();\n const [draft, setDraft] = useState('');\n const { data: messages, loading: loadingMessages } = useConversationMessages(conversationId);\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Resolve the conversation the first time the dialog opens.\n useEffect(() => {\n if (!open || conversationId) return;\n let cancelled = false;\n openConversation({\n entityId,\n entityType,\n title,\n contextHref: pagePath,\n pagePath,\n })\n .then((res) => {\n if (!cancelled) setConversationId(res.id);\n })\n .catch((err: Error) => notify.error('Could not open the conversation', err.message));\n return () => {\n cancelled = true;\n };\n }, [open, conversationId, openConversation, entityId, entityType, title, pagePath]);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });\n }, [messages]);\n\n const handleSend = async () => {\n const text = draft.trim();\n if (!text || !conversationId) return;\n try {\n await sendMessage(conversationId, text);\n setDraft('');\n } catch (err) {\n notify.error('Message not sent', (err as Error).message);\n }\n };\n\n const currentUserId = currentUser?.id ?? '';\n\n // Render the trigger as a real Button with a direct onClick — the exact\n // controlled-open pattern the in-page dialogs (e.g. Make Offer) use. Passing a\n // pre-built element through ResponsiveDialogTrigger asChild / cloneElement /\n // a display:contents wrapper all failed to forward the click in production.\n return (\n <>\n <Button\n type=\"button\"\n variant={triggerVariant}\n className={triggerClassName}\n onClick={() => setOpen(true)}\n >\n {triggerContent}\n </Button>\n <ResponsiveDialog open={open} onOpenChange={setOpen}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{heading}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {description ?? `Start a conversation about ${title}. Messages are saved and delivered in your inbox.`}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div\n ref={scrollRef}\n className=\"max-h-72 min-h-40 space-y-2 overflow-y-auto rounded-md border border-border-subtle bg-bg-sunken p-3\"\n >\n {opening || loadingMessages ? (\n <p className=\"py-8 text-center text-sm text-text-secondary\">Loading conversation…</p>\n ) : messages.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No messages yet. Say hello to get the conversation started.\n </p>\n ) : (\n messages.map((message) => {\n const mine = message.authorId === currentUserId;\n return (\n <div key={message.id} className={cn('flex', mine ? 'justify-end' : 'justify-start')}>\n <div\n className={cn(\n 'max-w-[80%] rounded-lg px-3 py-2 text-sm',\n mine\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-surface text-text-primary border border-border-subtle'\n )}\n >\n {message.text}\n </div>\n </div>\n );\n })\n )}\n </div>\n\n <div className=\"mt-3 flex items-end gap-2\">\n <textarea\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n void handleSend();\n }\n }}\n rows={2}\n placeholder=\"Write a message…\"\n className=\"min-h-10 flex-1 resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <Button type=\"button\" onClick={handleSend} disabled={!draft.trim() || !conversationId || sending}>\n <Send className=\"h-4 w-4\" />\n <span className=\"ml-1\">Send</span>\n </Button>\n </div>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -10,30 +10,30 @@ import { useMemo as c, useState as l } from "react";
|
|
|
10
10
|
import { ArrowLeft as u, Building2 as d, Calculator as f, CalendarClock as p, CheckCircle as m, Gift as h, Percent as g, Star as _, Wallet as v } from "lucide-react";
|
|
11
11
|
import { useLocation as y, useNavigate as b, useParams as x } from "react-router-dom";
|
|
12
12
|
import { jsx as S, jsxs as C } from "react/jsx-runtime";
|
|
13
|
-
import {
|
|
13
|
+
import { EmphasisPanel as w, GlassCard as T, PagePurpose as E } from "@burdenoff/fe-libs/ui";
|
|
14
14
|
//#region src/housingvista/pages/finance/HomeLoanDetailPage.tsx
|
|
15
|
-
function
|
|
15
|
+
function D(e, t, n) {
|
|
16
16
|
if (e <= 0 || t <= 0 || n <= 0) return 0;
|
|
17
17
|
let r = t / 12 / 100, i = n * 12, a = e * r * (1 + r) ** +i / ((1 + r) ** +i - 1);
|
|
18
18
|
return Math.round(a);
|
|
19
19
|
}
|
|
20
|
-
function
|
|
20
|
+
function O({ rating: t }) {
|
|
21
21
|
return /* @__PURE__ */ S("div", {
|
|
22
22
|
className: "flex items-center gap-0.5",
|
|
23
23
|
children: Array.from({ length: 5 }).map((n, r) => /* @__PURE__ */ S(_, { className: e("h-4 w-4", r < Math.floor(t) ? "fill-status-warning text-status-warning" : "text-text-secondary/30") }, r))
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
let { id: _ } = x(),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
]),
|
|
32
|
-
if (
|
|
26
|
+
function k() {
|
|
27
|
+
let { id: _ } = x(), k = b(), A = y(), { data: j, loading: M } = o(_), [N, P] = l(5e5), [F, I] = l(20), L = j?.interestRate ?? 0, R = c(() => D(N, L, F), [
|
|
28
|
+
N,
|
|
29
|
+
L,
|
|
30
|
+
F
|
|
31
|
+
]), z = R * F * 12, B = z - N;
|
|
32
|
+
if (M) return /* @__PURE__ */ S("div", {
|
|
33
33
|
className: "p-6",
|
|
34
34
|
children: /* @__PURE__ */ S(a, { variant: "detail" })
|
|
35
35
|
});
|
|
36
|
-
if (!
|
|
36
|
+
if (!j) return /* @__PURE__ */ S("div", {
|
|
37
37
|
className: "p-6",
|
|
38
38
|
children: /* @__PURE__ */ S(r, {
|
|
39
39
|
illustration: "empty-data",
|
|
@@ -41,20 +41,20 @@ function A() {
|
|
|
41
41
|
title: "Loan Offer Not Found",
|
|
42
42
|
description: "The loan offer you're looking for doesn't exist or is no longer available.",
|
|
43
43
|
action: /* @__PURE__ */ C("button", {
|
|
44
|
-
onClick: () =>
|
|
44
|
+
onClick: () => k("../home-loans"),
|
|
45
45
|
className: "inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover",
|
|
46
46
|
children: [/* @__PURE__ */ S(u, { className: "h-4 w-4" }), "Back to Home Loans"]
|
|
47
47
|
})
|
|
48
48
|
})
|
|
49
49
|
});
|
|
50
|
-
let
|
|
50
|
+
let V = j.features ?? [];
|
|
51
51
|
return /* @__PURE__ */ C("div", {
|
|
52
52
|
className: "flex flex-col gap-6 p-6",
|
|
53
53
|
children: [
|
|
54
54
|
/* @__PURE__ */ S(i, { items: [{
|
|
55
55
|
label: "Home Loans",
|
|
56
56
|
href: "../"
|
|
57
|
-
}, { label:
|
|
57
|
+
}, { label: j.bankName }] }),
|
|
58
58
|
/* @__PURE__ */ C("div", {
|
|
59
59
|
className: "flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between",
|
|
60
60
|
children: [/* @__PURE__ */ C("div", {
|
|
@@ -64,54 +64,52 @@ function A() {
|
|
|
64
64
|
children: /* @__PURE__ */ S(d, { className: "h-6 w-6" })
|
|
65
65
|
}), /* @__PURE__ */ C("div", { children: [
|
|
66
66
|
/* @__PURE__ */ C("button", {
|
|
67
|
-
onClick: () =>
|
|
67
|
+
onClick: () => k("../home-loans"),
|
|
68
68
|
className: "mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary",
|
|
69
69
|
children: [/* @__PURE__ */ S(u, { className: "h-4 w-4" }), "Back to Home Loans"]
|
|
70
70
|
}),
|
|
71
71
|
/* @__PURE__ */ S("h1", {
|
|
72
72
|
className: "text-2xl font-semibold text-text-primary",
|
|
73
|
-
children:
|
|
73
|
+
children: j.bankName
|
|
74
74
|
}),
|
|
75
75
|
/* @__PURE__ */ C("div", {
|
|
76
76
|
className: "mt-2 flex flex-wrap items-center gap-2",
|
|
77
|
-
children: [
|
|
77
|
+
children: [j.rating != null && /* @__PURE__ */ C("span", {
|
|
78
78
|
className: "inline-flex items-center gap-1.5",
|
|
79
|
-
children: [/* @__PURE__ */ S(
|
|
79
|
+
children: [/* @__PURE__ */ S(O, { rating: j.rating }), /* @__PURE__ */ S("span", {
|
|
80
80
|
className: "text-sm text-text-secondary",
|
|
81
|
-
children:
|
|
81
|
+
children: j.rating
|
|
82
82
|
})]
|
|
83
|
-
}),
|
|
83
|
+
}), j.specialOffer && /* @__PURE__ */ S(n, {
|
|
84
84
|
label: "Special Offer",
|
|
85
85
|
variant: "warning"
|
|
86
86
|
})]
|
|
87
87
|
})
|
|
88
88
|
] })]
|
|
89
89
|
}), /* @__PURE__ */ S(s, {
|
|
90
|
-
entityId:
|
|
90
|
+
entityId: j.id,
|
|
91
91
|
entityType: "housingvista-home-loan",
|
|
92
|
-
title:
|
|
93
|
-
pagePath:
|
|
94
|
-
heading: `Apply with ${
|
|
95
|
-
description: `Start your application with ${
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
children: "Apply Now"
|
|
99
|
-
})
|
|
92
|
+
title: j.bankName,
|
|
93
|
+
pagePath: A.pathname,
|
|
94
|
+
heading: `Apply with ${j.bankName}`,
|
|
95
|
+
description: `Start your application with ${j.bankName}. Messages are saved and delivered in your inbox.`,
|
|
96
|
+
triggerVariant: "default",
|
|
97
|
+
triggerContent: "Apply Now"
|
|
100
98
|
})]
|
|
101
99
|
}),
|
|
102
|
-
/* @__PURE__ */ C(
|
|
100
|
+
/* @__PURE__ */ C(E, {
|
|
103
101
|
className: "max-w-3xl",
|
|
104
102
|
children: [
|
|
105
103
|
"The full terms of this offer with a live EMI estimate at the bank's headline rate. Model your loan amount and tenure to see the real monthly cost, then apply to connect with ",
|
|
106
|
-
|
|
104
|
+
j.bankName,
|
|
107
105
|
" through Messages."
|
|
108
106
|
]
|
|
109
107
|
}),
|
|
110
|
-
|
|
108
|
+
j.specialOffer && /* @__PURE__ */ C("div", {
|
|
111
109
|
className: "flex items-center gap-2 rounded-lg bg-status-warning-bg-subtle p-3 text-sm text-status-warning-text",
|
|
112
|
-
children: [/* @__PURE__ */ S(h, { className: "h-4 w-4 shrink-0" }),
|
|
110
|
+
children: [/* @__PURE__ */ S(h, { className: "h-4 w-4 shrink-0" }), j.specialOffer]
|
|
113
111
|
}),
|
|
114
|
-
/* @__PURE__ */ C(
|
|
112
|
+
/* @__PURE__ */ C(w, {
|
|
115
113
|
className: "p-5",
|
|
116
114
|
children: [/* @__PURE__ */ C("div", {
|
|
117
115
|
className: "mb-4 flex items-center gap-2",
|
|
@@ -119,7 +117,7 @@ function A() {
|
|
|
119
117
|
className: "text-lg font-semibold text-text-primary",
|
|
120
118
|
children: [
|
|
121
119
|
"EMI Estimate at ",
|
|
122
|
-
|
|
120
|
+
j.interestRate,
|
|
123
121
|
"%"
|
|
124
122
|
]
|
|
125
123
|
})]
|
|
@@ -135,21 +133,21 @@ function A() {
|
|
|
135
133
|
children: "Loan Amount"
|
|
136
134
|
}), /* @__PURE__ */ S("span", {
|
|
137
135
|
className: "text-sm font-semibold text-text-primary",
|
|
138
|
-
children: t(
|
|
136
|
+
children: t(N)
|
|
139
137
|
})]
|
|
140
138
|
}),
|
|
141
139
|
/* @__PURE__ */ S("input", {
|
|
142
140
|
type: "range",
|
|
143
141
|
min: 5e4,
|
|
144
|
-
max: Math.max(
|
|
142
|
+
max: Math.max(j.maxAmount, 1e5),
|
|
145
143
|
step: 1e4,
|
|
146
|
-
value: Math.min(
|
|
147
|
-
onChange: (e) =>
|
|
144
|
+
value: Math.min(N, j.maxAmount),
|
|
145
|
+
onChange: (e) => P(Number(e.target.value)),
|
|
148
146
|
className: "w-full accent-foreground"
|
|
149
147
|
}),
|
|
150
148
|
/* @__PURE__ */ C("div", {
|
|
151
149
|
className: "mt-1 flex justify-between text-xs text-text-secondary",
|
|
152
|
-
children: [/* @__PURE__ */ S("span", { children: "$50K" }), /* @__PURE__ */ C("span", { children: ["Max ", t(
|
|
150
|
+
children: [/* @__PURE__ */ S("span", { children: "$50K" }), /* @__PURE__ */ C("span", { children: ["Max ", t(j.maxAmount)] })]
|
|
153
151
|
})
|
|
154
152
|
] }), /* @__PURE__ */ C("div", { children: [
|
|
155
153
|
/* @__PURE__ */ C("div", {
|
|
@@ -159,23 +157,23 @@ function A() {
|
|
|
159
157
|
children: "Tenure (years)"
|
|
160
158
|
}), /* @__PURE__ */ C("span", {
|
|
161
159
|
className: "text-sm font-semibold text-text-primary",
|
|
162
|
-
children: [
|
|
160
|
+
children: [F, " years"]
|
|
163
161
|
})]
|
|
164
162
|
}),
|
|
165
163
|
/* @__PURE__ */ S("input", {
|
|
166
164
|
type: "range",
|
|
167
165
|
min: 1,
|
|
168
|
-
max:
|
|
166
|
+
max: j.maxTenure,
|
|
169
167
|
step: 1,
|
|
170
|
-
value: Math.min(
|
|
171
|
-
onChange: (e) =>
|
|
168
|
+
value: Math.min(F, j.maxTenure),
|
|
169
|
+
onChange: (e) => I(Number(e.target.value)),
|
|
172
170
|
className: "w-full accent-foreground"
|
|
173
171
|
}),
|
|
174
172
|
/* @__PURE__ */ C("div", {
|
|
175
173
|
className: "mt-1 flex justify-between text-xs text-text-secondary",
|
|
176
174
|
children: [/* @__PURE__ */ S("span", { children: "1 yr" }), /* @__PURE__ */ C("span", { children: [
|
|
177
175
|
"Max ",
|
|
178
|
-
|
|
176
|
+
j.maxTenure,
|
|
179
177
|
" yrs"
|
|
180
178
|
] })]
|
|
181
179
|
})
|
|
@@ -189,7 +187,7 @@ function A() {
|
|
|
189
187
|
}),
|
|
190
188
|
/* @__PURE__ */ S("p", {
|
|
191
189
|
className: "my-2 text-4xl font-bold text-text-primary",
|
|
192
|
-
children: t(
|
|
190
|
+
children: t(R)
|
|
193
191
|
}),
|
|
194
192
|
/* @__PURE__ */ C("div", {
|
|
195
193
|
className: "mt-4 grid w-full grid-cols-2 gap-4",
|
|
@@ -200,7 +198,7 @@ function A() {
|
|
|
200
198
|
children: "Total Payment"
|
|
201
199
|
}), /* @__PURE__ */ S("p", {
|
|
202
200
|
className: "text-sm font-semibold text-text-primary",
|
|
203
|
-
children: t(
|
|
201
|
+
children: t(z)
|
|
204
202
|
})]
|
|
205
203
|
}), /* @__PURE__ */ C("div", {
|
|
206
204
|
className: "text-center",
|
|
@@ -209,7 +207,7 @@ function A() {
|
|
|
209
207
|
children: "Total Interest"
|
|
210
208
|
}), /* @__PURE__ */ S("p", {
|
|
211
209
|
className: "text-sm font-semibold text-status-error-text",
|
|
212
|
-
children: t(
|
|
210
|
+
children: t(B)
|
|
213
211
|
})]
|
|
214
212
|
})]
|
|
215
213
|
})
|
|
@@ -221,7 +219,7 @@ function A() {
|
|
|
221
219
|
className: "grid grid-cols-1 gap-6 lg:grid-cols-3",
|
|
222
220
|
children: [/* @__PURE__ */ C("div", {
|
|
223
221
|
className: "space-y-6 lg:col-span-2",
|
|
224
|
-
children: [/* @__PURE__ */ C(
|
|
222
|
+
children: [/* @__PURE__ */ C(T, {
|
|
225
223
|
treatment: "plain",
|
|
226
224
|
className: "border-border-seam p-6",
|
|
227
225
|
children: [/* @__PURE__ */ S("h2", {
|
|
@@ -237,7 +235,7 @@ function A() {
|
|
|
237
235
|
children: [/* @__PURE__ */ S(g, { className: "h-3.5 w-3.5" }), "Interest Rate"]
|
|
238
236
|
}), /* @__PURE__ */ C("div", {
|
|
239
237
|
className: "mt-1 text-lg font-bold text-text-primary",
|
|
240
|
-
children: [
|
|
238
|
+
children: [j.interestRate, "%"]
|
|
241
239
|
})]
|
|
242
240
|
}),
|
|
243
241
|
/* @__PURE__ */ C("div", {
|
|
@@ -247,7 +245,7 @@ function A() {
|
|
|
247
245
|
children: [/* @__PURE__ */ S(v, { className: "h-3.5 w-3.5" }), "EMI / Lakh"]
|
|
248
246
|
}), /* @__PURE__ */ S("div", {
|
|
249
247
|
className: "mt-1 text-lg font-bold text-text-primary",
|
|
250
|
-
children:
|
|
248
|
+
children: j.emiPerLakh == null ? "—" : `$${j.emiPerLakh}`
|
|
251
249
|
})]
|
|
252
250
|
}),
|
|
253
251
|
/* @__PURE__ */ C("div", {
|
|
@@ -257,7 +255,7 @@ function A() {
|
|
|
257
255
|
children: [/* @__PURE__ */ S(p, { className: "h-3.5 w-3.5" }), "Max Tenure"]
|
|
258
256
|
}), /* @__PURE__ */ C("div", {
|
|
259
257
|
className: "mt-1 text-lg font-bold text-text-primary",
|
|
260
|
-
children: [
|
|
258
|
+
children: [j.maxTenure, " yrs"]
|
|
261
259
|
})]
|
|
262
260
|
}),
|
|
263
261
|
/* @__PURE__ */ C("div", {
|
|
@@ -267,7 +265,7 @@ function A() {
|
|
|
267
265
|
children: "Max Amount"
|
|
268
266
|
}), /* @__PURE__ */ S("div", {
|
|
269
267
|
className: "mt-1 text-sm font-semibold text-text-primary",
|
|
270
|
-
children: t(
|
|
268
|
+
children: t(j.maxAmount)
|
|
271
269
|
})]
|
|
272
270
|
}),
|
|
273
271
|
/* @__PURE__ */ C("div", {
|
|
@@ -277,7 +275,7 @@ function A() {
|
|
|
277
275
|
children: "Processing Fee"
|
|
278
276
|
}), /* @__PURE__ */ S("div", {
|
|
279
277
|
className: "mt-1 text-sm font-semibold text-text-primary",
|
|
280
|
-
children:
|
|
278
|
+
children: j.processingFee ?? "—"
|
|
281
279
|
})]
|
|
282
280
|
}),
|
|
283
281
|
/* @__PURE__ */ C("div", {
|
|
@@ -286,13 +284,13 @@ function A() {
|
|
|
286
284
|
className: "text-xs text-text-secondary",
|
|
287
285
|
children: "Prepayment"
|
|
288
286
|
}), /* @__PURE__ */ S("div", {
|
|
289
|
-
className: e("mt-1 text-sm font-semibold",
|
|
290
|
-
children:
|
|
287
|
+
className: e("mt-1 text-sm font-semibold", j.prepaymentCharges === "None" ? "text-status-success-text" : "text-text-primary"),
|
|
288
|
+
children: j.prepaymentCharges ?? "—"
|
|
291
289
|
})]
|
|
292
290
|
})
|
|
293
291
|
]
|
|
294
292
|
})]
|
|
295
|
-
}),
|
|
293
|
+
}), V.length > 0 && /* @__PURE__ */ C(T, {
|
|
296
294
|
treatment: "plain",
|
|
297
295
|
className: "border-border-seam p-6",
|
|
298
296
|
children: [/* @__PURE__ */ S("h2", {
|
|
@@ -300,7 +298,7 @@ function A() {
|
|
|
300
298
|
children: "Features"
|
|
301
299
|
}), /* @__PURE__ */ S("ul", {
|
|
302
300
|
className: "grid grid-cols-1 gap-2 sm:grid-cols-2",
|
|
303
|
-
children:
|
|
301
|
+
children: V.map((e, t) => /* @__PURE__ */ C("li", {
|
|
304
302
|
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
305
303
|
children: [/* @__PURE__ */ S(m, { className: "h-4 w-4 shrink-0 text-status-success-text" }), e]
|
|
306
304
|
}, t))
|
|
@@ -308,7 +306,7 @@ function A() {
|
|
|
308
306
|
})]
|
|
309
307
|
}), /* @__PURE__ */ S("div", {
|
|
310
308
|
className: "space-y-6",
|
|
311
|
-
children: /* @__PURE__ */ C(
|
|
309
|
+
children: /* @__PURE__ */ C(T, {
|
|
312
310
|
treatment: "plain",
|
|
313
311
|
className: "border-border-seam p-5",
|
|
314
312
|
children: [
|
|
@@ -320,22 +318,20 @@ function A() {
|
|
|
320
318
|
className: "mb-4 text-sm text-text-secondary",
|
|
321
319
|
children: [
|
|
322
320
|
"Applications are handled directly with ",
|
|
323
|
-
|
|
321
|
+
j.bankName,
|
|
324
322
|
" through HousingVista Messages. Start an enquiry and the bank will guide you through eligibility and paperwork."
|
|
325
323
|
]
|
|
326
324
|
}),
|
|
327
325
|
/* @__PURE__ */ S(s, {
|
|
328
|
-
entityId:
|
|
326
|
+
entityId: j.id,
|
|
329
327
|
entityType: "housingvista-home-loan",
|
|
330
|
-
title:
|
|
331
|
-
pagePath:
|
|
332
|
-
heading: `Apply with ${
|
|
333
|
-
description: `Start your application with ${
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
children: "Apply Now"
|
|
338
|
-
})
|
|
328
|
+
title: j.bankName,
|
|
329
|
+
pagePath: A.pathname,
|
|
330
|
+
heading: `Apply with ${j.bankName}`,
|
|
331
|
+
description: `Start your application with ${j.bankName}. Messages are saved and delivered in your inbox.`,
|
|
332
|
+
triggerVariant: "default",
|
|
333
|
+
triggerClassName: "w-full justify-center",
|
|
334
|
+
triggerContent: "Apply Now"
|
|
339
335
|
})
|
|
340
336
|
]
|
|
341
337
|
})
|
|
@@ -345,6 +341,6 @@ function A() {
|
|
|
345
341
|
});
|
|
346
342
|
}
|
|
347
343
|
//#endregion
|
|
348
|
-
export {
|
|
344
|
+
export { k as HomeLoanDetailPage };
|
|
349
345
|
|
|
350
346
|
//# sourceMappingURL=HomeLoanDetailPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomeLoanDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoanDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Home Loan Detail / Apply Page\n * Single loan offer backed by `housingvistaHomeLoanOffer(id)` — full terms plus\n * a live EMI summary for the offer's headline rate.\n *\n * \"Apply\" opens the real `ContactOwnerDialog` — a persisted conversation with\n * the bank. No fabricated approval; the EMI calculator stays for modelling.\n * @module housingvista\n */\n\nimport { useMemo, useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n Star,\n CheckCircle,\n Gift,\n Calculator,\n Percent,\n Wallet,\n CalendarClock,\n} from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { Breadcrumbs, StatusBadge, EmptyState, LoadingSkeleton, ContactOwnerDialog } from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useHomeLoanOffer } from '../../hooks';\nimport { cn, formatCurrency } from '../../utils/helpers';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-4 w-4',\n i < Math.floor(rating) ? 'fill-status-warning text-status-warning' : 'text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nexport function HomeLoanDetailPage() {\n const { id } = useParams<{ id: string }>();\n const navigate = useNavigate();\n const location = useLocation();\n const { data: loan, loading } = useHomeLoanOffer(id);\n\n // Model a representative loan against the offer's headline rate so the borrower\n // can see the real monthly cost before enquiring.\n const [amount, setAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n\n const rate = loan?.interestRate ?? 0;\n const emi = useMemo(() => calculateEMI(amount, rate, tenure), [amount, rate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - amount;\n\n if (loading) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!loan) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"Loan Offer Not Found\"\n description=\"The loan offer you're looking for doesn't exist or is no longer available.\"\n action={\n <button\n onClick={() => navigate('../home-loans')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n }\n />\n </div>\n );\n }\n\n const features = loan.features ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Home Loans', href: '../' }, { label: loan.bankName }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <Building2 className=\"h-6 w-6\" />\n </div>\n <div>\n <button\n onClick={() => navigate('../home-loans')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{loan.bankName}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n {loan.rating != null && (\n <span className=\"inline-flex items-center gap-1.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-sm text-text-secondary\">{loan.rating}</span>\n </span>\n )}\n {loan.specialOffer && <StatusBadge label=\"Special Offer\" variant=\"warning\" />}\n </div>\n </div>\n </div>\n <ContactOwnerDialog\n entityId={loan.id}\n entityType=\"housingvista-home-loan\"\n title={loan.bankName}\n pagePath={location.pathname}\n heading={`Apply with ${loan.bankName}`}\n description={`Start your application with ${loan.bankName}. Messages are saved and delivered in your inbox.`}\n trigger={<Button type=\"button\">Apply Now</Button>}\n />\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n The full terms of this offer with a live EMI estimate at the bank's headline rate. Model your loan amount and\n tenure to see the real monthly cost, then apply to connect with {loan.bankName} through Messages.\n </PagePurpose>\n\n {loan.specialOffer && (\n <div className=\"flex items-center gap-2 rounded-lg bg-status-warning-bg-subtle p-3 text-sm text-status-warning-text\">\n <Gift className=\"h-4 w-4 shrink-0\" />\n {loan.specialOffer}\n </div>\n )}\n\n {/* EMI summary — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Estimate at {loan.interestRate}%</h2>\n </div>\n <div className=\"grid gap-6 md:grid-cols-2\">\n <div className=\"space-y-5\">\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(amount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={Math.max(loan.maxAmount, 100000)}\n step={10000}\n value={Math.min(amount, loan.maxAmount)}\n onChange={(e) => setAmount(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>$50K</span>\n <span>Max {formatCurrency(loan.maxAmount)}</span>\n </div>\n </div>\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={loan.maxTenure}\n step={1}\n value={Math.min(tenure, loan.maxTenure)}\n onChange={(e) => setTenure(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>1 yr</span>\n <span>Max {loan.maxTenure} yrs</span>\n </div>\n </div>\n </div>\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-status-error-text\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Terms */}\n <div className=\"space-y-6 lg:col-span-2\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Loan Terms</h2>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Percent className=\"h-3.5 w-3.5\" />\n Interest Rate\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.interestRate}%</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Wallet className=\"h-3.5 w-3.5\" />\n EMI / Lakh\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">\n {loan.emiPerLakh != null ? `$${loan.emiPerLakh}` : '—'}\n </div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Max Tenure\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.maxTenure} yrs</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Max Amount</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{formatCurrency(loan.maxAmount)}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Processing Fee</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{loan.processingFee ?? '—'}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Prepayment</div>\n <div\n className={cn(\n 'mt-1 text-sm font-semibold',\n loan.prepaymentCharges === 'None' ? 'text-status-success-text' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges ?? '—'}\n </div>\n </div>\n </div>\n </GlassCard>\n\n {features.length > 0 && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Features</h2>\n <ul className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n {features.map((feature, i) => (\n <li key={i} className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <CheckCircle className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n {feature}\n </li>\n ))}\n </ul>\n </GlassCard>\n )}\n </div>\n\n {/* Apply card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-2 text-base font-semibold text-text-primary\">Ready to apply?</h3>\n <p className=\"mb-4 text-sm text-text-secondary\">\n Applications are handled directly with {loan.bankName} through HousingVista Messages. Start an enquiry and\n the bank will guide you through eligibility and paperwork.\n </p>\n <ContactOwnerDialog\n entityId={loan.id}\n entityType=\"housingvista-home-loan\"\n title={loan.bankName}\n pagePath={location.pathname}\n heading={`Apply with ${loan.bankName}`}\n description={`Start your application with ${loan.bankName}. Messages are saved and delivered in your inbox.`}\n trigger={\n <Button type=\"button\" className=\"w-full justify-center\">\n Apply Now\n </Button>\n }\n />\n </GlassCard>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,SAAS,EAAa,GAAmB,GAAoB,GAA6B;AACxF,KAAI,KAAa,KAAK,KAAc,KAAK,KAAe,EAAG,QAAO;CAClE,IAAM,IAAc,IAAa,KAAK,KAChC,IAAS,IAAc,IACvB,IAAO,IAAY,KAAuB,IAAI,OAAa,MAAqB,IAAI,OAAa,IAAU;AACjH,QAAO,KAAK,MAAM,EAAI;;AAGxB,SAAS,EAAY,EAAE,aAA8B;AACnD,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,MAAM,KAAK,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,MACjC,kBAAC,GAAD,EAEE,WAAW,EACT,WACA,IAAI,KAAK,MAAM,EAAO,GAAG,4CAA4C,yBACtE,EACD,EALK,EAKL,CACF;EACE,CAAA;;AAIV,SAAgB,IAAqB;CACnC,IAAM,EAAE,UAAO,GAA2B,EACpC,IAAW,GAAa,EACxB,IAAW,GAAa,EACxB,EAAE,MAAM,GAAM,eAAY,EAAiB,EAAG,EAI9C,CAAC,GAAQ,KAAa,EAAS,IAAO,EACtC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAElC,IAAO,GAAM,gBAAgB,GAC7B,IAAM,QAAc,EAAa,GAAQ,GAAM,EAAO,EAAE;EAAC;EAAQ;EAAM;EAAO,CAAC,EAC/E,IAAe,IAAM,IAAS,IAC9B,IAAgB,IAAe;AAErC,KAAI,EACF,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAiB,SAAQ,UAAW,CAAA;EAChC,CAAA;AAIV,KAAI,CAAC,EACH,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAW,WAAU,aAAc,CAAA;GACzC,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,UAAD;IACE,eAAe,EAAS,gBAAgB;IACxC,WAAU;cAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,qBAE1B;;GAEX,CAAA;EACE,CAAA;CAIV,IAAM,IAAW,EAAK,YAAY,EAAE;AAGpC,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD,EAAa,OAJqB,CAAC;IAAE,OAAO;IAAc,MAAM;IAAO,EAAE,EAAE,OAAO,EAAK,UAAU,CAAC,EAI/D,CAAA;GAGnC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;MAC7B,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;MACE,kBAAC,UAAD;OACE,eAAe,EAAS,gBAAgB;OACxC,WAAU;iBAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,qBAE1B;;MACT,kBAAC,MAAD;OAAI,WAAU;iBAA4C,EAAK;OAAc,CAAA;MAC7E,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAK,UAAU,QACd,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAK,QAAU,CAAA,EACpC,kBAAC,QAAD;SAAM,WAAU;mBAA+B,EAAK;SAAc,CAAA,CAC7D;WAER,EAAK,gBAAgB,kBAAC,GAAD;QAAa,OAAM;QAAgB,SAAQ;QAAY,CAAA,CACzE;;MACF,EAAA,CAAA,CACF;QACN,kBAAC,GAAD;KACE,UAAU,EAAK;KACf,YAAW;KACX,OAAO,EAAK;KACZ,UAAU,EAAS;KACnB,SAAS,cAAc,EAAK;KAC5B,aAAa,+BAA+B,EAAK,SAAS;KAC1D,SAAS,kBAAC,GAAD;MAAQ,MAAK;gBAAS;MAAkB,CAAA;KACjD,CAAA,CACE;;GAEN,kBAAC,GAAD;IAAa,WAAU;cAAvB;KAAmC;KAEgC,EAAK;KAAS;KACnE;;GAEb,EAAK,gBACJ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAM,WAAU,oBAAqB,CAAA,EACpC,EAAK,aACF;;GAIR,kBAAC,GAAD;IAAe,WAAU;cAAzB,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAY,WAAU,+BAAgC,CAAA,EACtD,kBAAC,MAAD;MAAI,WAAU;gBAAd;OAAwD;OAAiB,EAAK;OAAa;OAAM;QAC7F;QACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,SAAD;SAAO,WAAU;mBAAwC;SAAmB,CAAA,EAC5E,kBAAC,QAAD;SAAM,WAAU;mBAA2C,EAAe,EAAO;SAAQ,CAAA,CACrF;;OACN,kBAAC,SAAD;QACE,MAAK;QACL,KAAK;QACL,KAAK,KAAK,IAAI,EAAK,WAAW,IAAO;QACrC,MAAM;QACN,OAAO,KAAK,IAAI,GAAQ,EAAK,UAAU;QACvC,WAAW,MAAM,EAAU,OAAO,EAAE,OAAO,MAAM,CAAC;QAClD,WAAU;QACV,CAAA;OACF,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD,EAAA,UAAM,QAAW,CAAA,EACjB,kBAAC,QAAD,EAAA,UAAA,CAAM,QAAK,EAAe,EAAK,UAAU,CAAQ,EAAA,CAAA,CAC7C;;OACF,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,SAAD;SAAO,WAAU;mBAAwC;SAAsB,CAAA,EAC/E,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAA2D,GAAO,SAAa;WAC3E;;OACN,kBAAC,SAAD;QACE,MAAK;QACL,KAAK;QACL,KAAK,EAAK;QACV,MAAM;QACN,OAAO,KAAK,IAAI,GAAQ,EAAK,UAAU;QACvC,WAAW,MAAM,EAAU,OAAO,EAAE,OAAO,MAAM,CAAC;QAClD,WAAU;QACV,CAAA;OACF,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD,EAAA,UAAM,QAAW,CAAA,EACjB,kBAAC,QAAD,EAAA,UAAA;SAAM;SAAK,EAAK;SAAU;SAAW,EAAA,CAAA,CACjC;;OACF,EAAA,CAAA,CACF;SACN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,KAAD;QAAG,WAAU;kBAA8B;QAAe,CAAA;OAC1D,kBAAC,KAAD;QAAG,WAAU;kBAA6C,EAAe,EAAI;QAAK,CAAA;OAClF,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,KAAD;UAAG,WAAU;oBAA8B;UAAiB,CAAA,EAC5D,kBAAC,KAAD;UAAG,WAAU;oBAA2C,EAAe,EAAa;UAAK,CAAA,CACrF;YACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,KAAD;UAAG,WAAU;oBAA8B;UAAkB,CAAA,EAC7D,kBAAC,KAAD;UAAG,WAAU;oBAAgD,EAAe,EAAc;UAAK,CAAA,CAC3F;WACF;;OACF;QACF;OACQ;;GAEhB,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAW,WAAU;MAAQ,WAAU;gBAAvC,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAiD;OAAe,CAAA,EAC9E,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,eAAgB,CAAA,EAAA,gBAE/B;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CAA2D,EAAK,cAAa,IAAO;YAChF;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,eAAgB,CAAA,EAAA,aAE9B;aACN,kBAAC,OAAD;UAAK,WAAU;oBACZ,EAAK,cAAc,OAA+B,MAAxB,IAAI,EAAK;UAChC,CAAA,CACF;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD,EAAe,WAAU,eAAgB,CAAA,EAAA,aAErC;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CAA2D,EAAK,WAAU,OAAU;YAChF;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAA8B;UAAgB,CAAA,EAC7D,kBAAC,OAAD;UAAK,WAAU;oBAAgD,EAAe,EAAK,UAAU;UAAO,CAAA,CAChG;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAA8B;UAAoB,CAAA,EACjE,kBAAC,OAAD;UAAK,WAAU;oBAAgD,EAAK,iBAAiB;UAAU,CAAA,CAC3F;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAA8B;UAAgB,CAAA,EAC7D,kBAAC,OAAD;UACE,WAAW,EACT,8BACA,EAAK,sBAAsB,SAAS,6BAA6B,oBAClE;oBAEA,EAAK,qBAAqB;UACvB,CAAA,CACF;;QACF;SACI;SAEX,EAAS,SAAS,KACjB,kBAAC,GAAD;MAAW,WAAU;MAAQ,WAAU;gBAAvC,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAiD;OAAa,CAAA,EAC5E,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAS,KAAK,GAAS,MACtB,kBAAC,MAAD;QAAY,WAAU;kBAAtB,CACE,kBAAC,GAAD,EAAa,WAAU,6CAA8C,CAAA,EACpE,EACE;UAHI,EAGJ,CACL;OACC,CAAA,CACK;QAEV;QAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAW,WAAU;MAAQ,WAAU;gBAAvC;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAiD;QAAoB,CAAA;OACnF,kBAAC,KAAD;QAAG,WAAU;kBAAb;SAAgD;SACN,EAAK;SAAS;SAEpD;;OACJ,kBAAC,GAAD;QACE,UAAU,EAAK;QACf,YAAW;QACX,OAAO,EAAK;QACZ,UAAU,EAAS;QACnB,SAAS,cAAc,EAAK;QAC5B,aAAa,+BAA+B,EAAK,SAAS;QAC1D,SACE,kBAAC,GAAD;SAAQ,MAAK;SAAS,WAAU;mBAAwB;SAE/C,CAAA;QAEX,CAAA;OACQ;;KACR,CAAA,CACF;;GACF"}
|
|
1
|
+
{"version":3,"file":"HomeLoanDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoanDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Home Loan Detail / Apply Page\n * Single loan offer backed by `housingvistaHomeLoanOffer(id)` — full terms plus\n * a live EMI summary for the offer's headline rate.\n *\n * \"Apply\" opens the real `ContactOwnerDialog` — a persisted conversation with\n * the bank. No fabricated approval; the EMI calculator stays for modelling.\n * @module housingvista\n */\n\nimport { useMemo, useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n Star,\n CheckCircle,\n Gift,\n Calculator,\n Percent,\n Wallet,\n CalendarClock,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { Breadcrumbs, StatusBadge, EmptyState, LoadingSkeleton, ContactOwnerDialog } from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useHomeLoanOffer } from '../../hooks';\nimport { cn, formatCurrency } from '../../utils/helpers';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-4 w-4',\n i < Math.floor(rating) ? 'fill-status-warning text-status-warning' : 'text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nexport function HomeLoanDetailPage() {\n const { id } = useParams<{ id: string }>();\n const navigate = useNavigate();\n const location = useLocation();\n const { data: loan, loading } = useHomeLoanOffer(id);\n\n // Model a representative loan against the offer's headline rate so the borrower\n // can see the real monthly cost before enquiring.\n const [amount, setAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n\n const rate = loan?.interestRate ?? 0;\n const emi = useMemo(() => calculateEMI(amount, rate, tenure), [amount, rate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - amount;\n\n if (loading) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!loan) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"Loan Offer Not Found\"\n description=\"The loan offer you're looking for doesn't exist or is no longer available.\"\n action={\n <button\n onClick={() => navigate('../home-loans')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n }\n />\n </div>\n );\n }\n\n const features = loan.features ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Home Loans', href: '../' }, { label: loan.bankName }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <Building2 className=\"h-6 w-6\" />\n </div>\n <div>\n <button\n onClick={() => navigate('../home-loans')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{loan.bankName}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n {loan.rating != null && (\n <span className=\"inline-flex items-center gap-1.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-sm text-text-secondary\">{loan.rating}</span>\n </span>\n )}\n {loan.specialOffer && <StatusBadge label=\"Special Offer\" variant=\"warning\" />}\n </div>\n </div>\n </div>\n <ContactOwnerDialog\n entityId={loan.id}\n entityType=\"housingvista-home-loan\"\n title={loan.bankName}\n pagePath={location.pathname}\n heading={`Apply with ${loan.bankName}`}\n description={`Start your application with ${loan.bankName}. Messages are saved and delivered in your inbox.`}\n triggerVariant=\"default\"\n triggerContent=\"Apply Now\"\n />\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n The full terms of this offer with a live EMI estimate at the bank's headline rate. Model your loan amount and\n tenure to see the real monthly cost, then apply to connect with {loan.bankName} through Messages.\n </PagePurpose>\n\n {loan.specialOffer && (\n <div className=\"flex items-center gap-2 rounded-lg bg-status-warning-bg-subtle p-3 text-sm text-status-warning-text\">\n <Gift className=\"h-4 w-4 shrink-0\" />\n {loan.specialOffer}\n </div>\n )}\n\n {/* EMI summary — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Estimate at {loan.interestRate}%</h2>\n </div>\n <div className=\"grid gap-6 md:grid-cols-2\">\n <div className=\"space-y-5\">\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(amount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={Math.max(loan.maxAmount, 100000)}\n step={10000}\n value={Math.min(amount, loan.maxAmount)}\n onChange={(e) => setAmount(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>$50K</span>\n <span>Max {formatCurrency(loan.maxAmount)}</span>\n </div>\n </div>\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={loan.maxTenure}\n step={1}\n value={Math.min(tenure, loan.maxTenure)}\n onChange={(e) => setTenure(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>1 yr</span>\n <span>Max {loan.maxTenure} yrs</span>\n </div>\n </div>\n </div>\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-status-error-text\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Terms */}\n <div className=\"space-y-6 lg:col-span-2\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Loan Terms</h2>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Percent className=\"h-3.5 w-3.5\" />\n Interest Rate\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.interestRate}%</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Wallet className=\"h-3.5 w-3.5\" />\n EMI / Lakh\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">\n {loan.emiPerLakh != null ? `$${loan.emiPerLakh}` : '—'}\n </div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Max Tenure\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.maxTenure} yrs</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Max Amount</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{formatCurrency(loan.maxAmount)}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Processing Fee</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{loan.processingFee ?? '—'}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Prepayment</div>\n <div\n className={cn(\n 'mt-1 text-sm font-semibold',\n loan.prepaymentCharges === 'None' ? 'text-status-success-text' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges ?? '—'}\n </div>\n </div>\n </div>\n </GlassCard>\n\n {features.length > 0 && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Features</h2>\n <ul className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n {features.map((feature, i) => (\n <li key={i} className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <CheckCircle className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n {feature}\n </li>\n ))}\n </ul>\n </GlassCard>\n )}\n </div>\n\n {/* Apply card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-2 text-base font-semibold text-text-primary\">Ready to apply?</h3>\n <p className=\"mb-4 text-sm text-text-secondary\">\n Applications are handled directly with {loan.bankName} through HousingVista Messages. Start an enquiry and\n the bank will guide you through eligibility and paperwork.\n </p>\n <ContactOwnerDialog\n entityId={loan.id}\n entityType=\"housingvista-home-loan\"\n title={loan.bankName}\n pagePath={location.pathname}\n heading={`Apply with ${loan.bankName}`}\n description={`Start your application with ${loan.bankName}. Messages are saved and delivered in your inbox.`}\n triggerVariant=\"default\"\n triggerClassName=\"w-full justify-center\"\n triggerContent=\"Apply Now\"\n />\n </GlassCard>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|