@burdenoff/microfe-housingvista 2026.624.2 → 2026.625.2
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/ChartCard.js +11 -9
- package/dist/housingvista/components/ChartCard.js.map +1 -1
- package/dist/housingvista/components/DataTable.js +63 -60
- package/dist/housingvista/components/DataTable.js.map +1 -1
- package/dist/housingvista/components/EmptyState.js +16 -9
- package/dist/housingvista/components/EmptyState.js.map +1 -1
- package/dist/housingvista/components/PageLayout.js +21 -14
- package/dist/housingvista/components/PageLayout.js.map +1 -1
- package/dist/housingvista/components/PropertyCard.js +2 -2
- package/dist/housingvista/components/PropertyCard.js.map +1 -1
- package/dist/housingvista/components/StatCard.js +20 -17
- package/dist/housingvista/components/StatCard.js.map +1 -1
- package/dist/housingvista/components/StatusBadge.js +1 -1
- package/dist/housingvista/components/StatusBadge.js.map +1 -1
- package/dist/housingvista/pages/HomePage.js +101 -58
- package/dist/housingvista/pages/HomePage.js.map +1 -1
- package/dist/housingvista/pages/amc/AMCManagementPage.js +59 -48
- package/dist/housingvista/pages/amc/AMCManagementPage.js.map +1 -1
- package/dist/housingvista/pages/analytics/AnalyticsPage.js +113 -90
- package/dist/housingvista/pages/analytics/AnalyticsPage.js.map +1 -1
- package/dist/housingvista/pages/assets/AssetManagementPage.js +66 -60
- package/dist/housingvista/pages/assets/AssetManagementPage.js.map +1 -1
- package/dist/housingvista/pages/chat/ChatPage.js +60 -58
- package/dist/housingvista/pages/chat/ChatPage.js.map +1 -1
- package/dist/housingvista/pages/finance/HomeLoansPage.js +23 -20
- package/dist/housingvista/pages/finance/HomeLoansPage.js.map +1 -1
- package/dist/housingvista/pages/investments/InvestmentDetailPage.js +114 -81
- package/dist/housingvista/pages/investments/InvestmentDetailPage.js.map +1 -1
- package/dist/housingvista/pages/investments/InvestmentsPage.js +65 -59
- package/dist/housingvista/pages/investments/InvestmentsPage.js.map +1 -1
- package/dist/housingvista/pages/legal/LegalPage.js +17 -14
- package/dist/housingvista/pages/legal/LegalPage.js.map +1 -1
- package/dist/housingvista/pages/market/AreaInsightsPage.js +13 -10
- package/dist/housingvista/pages/market/AreaInsightsPage.js.map +1 -1
- package/dist/housingvista/pages/market/MarketAnalyticsPage.js +155 -123
- package/dist/housingvista/pages/market/MarketAnalyticsPage.js.map +1 -1
- package/dist/housingvista/pages/marketplace/MarketplacePage.js +15 -12
- package/dist/housingvista/pages/marketplace/MarketplacePage.js.map +1 -1
- package/dist/housingvista/pages/notifications/NotificationsPage.js +24 -21
- package/dist/housingvista/pages/notifications/NotificationsPage.js.map +1 -1
- package/dist/housingvista/pages/properties/AddPropertyPage.js +73 -71
- package/dist/housingvista/pages/properties/AddPropertyPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.js +141 -135
- package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.js +192 -154
- package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
- package/dist/housingvista/pages/rental/RentalManagementPage.js +185 -160
- package/dist/housingvista/pages/rental/RentalManagementPage.js.map +1 -1
- package/dist/housingvista/pages/settings/ProfilePage.js +51 -49
- package/dist/housingvista/pages/settings/ProfilePage.js.map +1 -1
- package/dist/housingvista/pages/settings/SettingsPage.js +56 -54
- package/dist/housingvista/pages/settings/SettingsPage.js.map +1 -1
- package/dist/housingvista/pages/transactions/TransactionDetailPage.js +183 -142
- package/dist/housingvista/pages/transactions/TransactionDetailPage.js.map +1 -1
- package/dist/housingvista/pages/transactions/TransactionsPage.js +90 -84
- package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
- package/dist/housingvista/pages/verification/VerificationPage.js +49 -42
- package/dist/housingvista/pages/verification/VerificationPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -12,8 +12,9 @@ import { useMemo as u, useState as d } from "react";
|
|
|
12
12
|
import { ArrowDownRight as f, ArrowUpRight as p, BarChart3 as m, Building as h, DollarSign as g, Download as _, Eye as v, MapPin as y, Plus as b, TrendingUp as x } from "lucide-react";
|
|
13
13
|
import { useNavigate as S } from "react-router-dom";
|
|
14
14
|
import { jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
15
|
+
import { EmphasisPanel as T } from "@burdenoff/fe-libs/ui";
|
|
15
16
|
//#region src/housingvista/pages/assets/AssetManagementPage.tsx
|
|
16
|
-
function
|
|
17
|
+
function E(e) {
|
|
17
18
|
switch (e) {
|
|
18
19
|
case "active": return "success";
|
|
19
20
|
case "under_renovation": return "warning";
|
|
@@ -22,23 +23,24 @@ function T(e) {
|
|
|
22
23
|
default: return "muted";
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
-
let
|
|
27
|
-
if (!
|
|
28
|
-
let e =
|
|
29
|
-
return
|
|
30
|
-
}, [
|
|
31
|
-
let e =
|
|
26
|
+
function D() {
|
|
27
|
+
let D = S(), O = l(10), { data: k, loading: A } = c(O.pagination), [j, M] = d(""), N = u(() => {
|
|
28
|
+
if (!j) return k;
|
|
29
|
+
let e = j.toLowerCase();
|
|
30
|
+
return k.filter((t) => t.title.toLowerCase().includes(e) || t.location.toLowerCase().includes(e) || t.type.toLowerCase().includes(e));
|
|
31
|
+
}, [k, j]), P = u(() => {
|
|
32
|
+
let e = k.reduce((e, t) => e + t.currentValue, 0), t = k.filter((e) => e.rentalYield).reduce((e, t) => e + t.currentValue * (t.rentalYield || 0) / 100 / 12, 0), n = k.length > 0 ? k.reduce((e, t) => e + t.appreciationPercent, 0) / k.length : 0, r = k.reduce((e, t) => e + t.purchasePrice, 0);
|
|
32
33
|
return {
|
|
33
34
|
totalValue: e,
|
|
34
35
|
totalMonthlyIncome: t,
|
|
35
36
|
avgAppreciation: n,
|
|
36
37
|
totalROI: r > 0 ? (e - r) / r * 100 : 0
|
|
37
38
|
};
|
|
38
|
-
}, [
|
|
39
|
+
}, [k]);
|
|
39
40
|
return /* @__PURE__ */ w(n, {
|
|
40
41
|
title: "Asset Management",
|
|
41
42
|
description: "Monitor and manage your property portfolio performance",
|
|
43
|
+
purpose: "See how every property you own is performing as an investment — current value, appreciation, rental yield, and overall ROI in one portfolio view. Use it to decide what to hold, renovate, or sell.",
|
|
42
44
|
actions: /* @__PURE__ */ w("div", {
|
|
43
45
|
className: "flex items-center gap-2",
|
|
44
46
|
children: [/* @__PURE__ */ w("button", {
|
|
@@ -50,48 +52,51 @@ function E() {
|
|
|
50
52
|
})]
|
|
51
53
|
}),
|
|
52
54
|
children: [
|
|
53
|
-
/* @__PURE__ */
|
|
54
|
-
className: "
|
|
55
|
-
children:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
55
|
+
/* @__PURE__ */ C(T, {
|
|
56
|
+
className: "p-4",
|
|
57
|
+
children: /* @__PURE__ */ w("div", {
|
|
58
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
59
|
+
children: [
|
|
60
|
+
/* @__PURE__ */ C(r, {
|
|
61
|
+
label: "Total Value",
|
|
62
|
+
value: P.totalValue,
|
|
63
|
+
prefix: "$",
|
|
64
|
+
icon: /* @__PURE__ */ C(g, { className: "h-4 w-4" }),
|
|
65
|
+
trend: "up",
|
|
66
|
+
change: 8
|
|
67
|
+
}),
|
|
68
|
+
/* @__PURE__ */ C(r, {
|
|
69
|
+
label: "Monthly Income",
|
|
70
|
+
value: Math.round(P.totalMonthlyIncome),
|
|
71
|
+
prefix: "$",
|
|
72
|
+
icon: /* @__PURE__ */ C(x, { className: "h-4 w-4" }),
|
|
73
|
+
trend: "up",
|
|
74
|
+
change: 5
|
|
75
|
+
}),
|
|
76
|
+
/* @__PURE__ */ C(r, {
|
|
77
|
+
label: "Avg. Appreciation",
|
|
78
|
+
value: `${P.avgAppreciation.toFixed(1)}%`,
|
|
79
|
+
icon: /* @__PURE__ */ C(m, { className: "h-4 w-4" }),
|
|
80
|
+
trend: "up",
|
|
81
|
+
change: 2
|
|
82
|
+
}),
|
|
83
|
+
/* @__PURE__ */ C(r, {
|
|
84
|
+
label: "Overall ROI",
|
|
85
|
+
value: `${P.totalROI.toFixed(1)}%`,
|
|
86
|
+
icon: /* @__PURE__ */ C(h, { className: "h-4 w-4" }),
|
|
87
|
+
trend: "up",
|
|
88
|
+
change: 3
|
|
89
|
+
})
|
|
90
|
+
]
|
|
91
|
+
})
|
|
87
92
|
}),
|
|
88
93
|
/* @__PURE__ */ C("div", {
|
|
89
94
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
90
|
-
children:
|
|
95
|
+
children: k.map((n) => {
|
|
91
96
|
let r = n.currentValue - n.purchasePrice >= 0;
|
|
92
97
|
return /* @__PURE__ */ w("div", {
|
|
93
98
|
className: "cursor-pointer rounded-lg border border-border-subtle bg-bg-surface p-4 shadow-sm transition-shadow hover:shadow-md",
|
|
94
|
-
onClick: () =>
|
|
99
|
+
onClick: () => D(`/assets/${n.id}`),
|
|
95
100
|
children: [/* @__PURE__ */ w("div", {
|
|
96
101
|
className: "mb-3 flex items-start justify-between",
|
|
97
102
|
children: [/* @__PURE__ */ w("div", {
|
|
@@ -109,7 +114,7 @@ function E() {
|
|
|
109
114
|
})] })]
|
|
110
115
|
}), /* @__PURE__ */ C(a, {
|
|
111
116
|
label: n.status.replace("_", " "),
|
|
112
|
-
variant:
|
|
117
|
+
variant: E(n.status)
|
|
113
118
|
})]
|
|
114
119
|
}), /* @__PURE__ */ w("div", {
|
|
115
120
|
className: "grid grid-cols-2 gap-3",
|
|
@@ -157,13 +162,13 @@ function E() {
|
|
|
157
162
|
children: "All Assets"
|
|
158
163
|
}),
|
|
159
164
|
/* @__PURE__ */ C(s, {
|
|
160
|
-
searchValue:
|
|
161
|
-
onSearchChange:
|
|
165
|
+
searchValue: j,
|
|
166
|
+
onSearchChange: M,
|
|
162
167
|
searchPlaceholder: "Search assets by name, location, or type..."
|
|
163
168
|
}),
|
|
164
169
|
/* @__PURE__ */ C("div", {
|
|
165
170
|
className: "mt-4",
|
|
166
|
-
children:
|
|
171
|
+
children: N.length > 0 ? /* @__PURE__ */ C(i, {
|
|
167
172
|
columns: [
|
|
168
173
|
{
|
|
169
174
|
key: "title",
|
|
@@ -234,7 +239,7 @@ function E() {
|
|
|
234
239
|
header: "Status",
|
|
235
240
|
render: (e) => /* @__PURE__ */ C(a, {
|
|
236
241
|
label: e.status.replace("_", " "),
|
|
237
|
-
variant:
|
|
242
|
+
variant: E(e.status)
|
|
238
243
|
})
|
|
239
244
|
},
|
|
240
245
|
{
|
|
@@ -243,26 +248,27 @@ function E() {
|
|
|
243
248
|
sortable: !1,
|
|
244
249
|
render: (e) => /* @__PURE__ */ w("button", {
|
|
245
250
|
onClick: (t) => {
|
|
246
|
-
t.stopPropagation(),
|
|
251
|
+
t.stopPropagation(), D(`/assets/${e.id}`);
|
|
247
252
|
},
|
|
248
253
|
className: "inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
249
254
|
children: [/* @__PURE__ */ C(v, { className: "h-3 w-3" }), "View"]
|
|
250
255
|
})
|
|
251
256
|
}
|
|
252
257
|
],
|
|
253
|
-
data:
|
|
258
|
+
data: N,
|
|
254
259
|
keyField: "id",
|
|
255
|
-
onRowClick: (e) =>
|
|
260
|
+
onRowClick: (e) => D(`/assets/${e.id}`),
|
|
256
261
|
pageSize: 10,
|
|
257
262
|
serverPagination: {
|
|
258
|
-
page:
|
|
259
|
-
pageSize:
|
|
260
|
-
hasNextPage:
|
|
261
|
-
loading:
|
|
262
|
-
onPageChange:
|
|
263
|
-
onPageSizeChange:
|
|
263
|
+
page: O.page,
|
|
264
|
+
pageSize: O.pageSize,
|
|
265
|
+
hasNextPage: k.length >= O.pageSize,
|
|
266
|
+
loading: A,
|
|
267
|
+
onPageChange: O.setPage,
|
|
268
|
+
onPageSizeChange: O.setPageSize
|
|
264
269
|
}
|
|
265
270
|
}) : /* @__PURE__ */ C(o, {
|
|
271
|
+
illustration: "empty-search",
|
|
266
272
|
icon: /* @__PURE__ */ C(h, { className: "h-12 w-12" }),
|
|
267
273
|
title: "No assets found",
|
|
268
274
|
description: "Try adjusting your search criteria or add a new asset.",
|
|
@@ -277,6 +283,6 @@ function E() {
|
|
|
277
283
|
});
|
|
278
284
|
}
|
|
279
285
|
//#endregion
|
|
280
|
-
export {
|
|
286
|
+
export { D as AssetManagementPage };
|
|
281
287
|
|
|
282
288
|
//# sourceMappingURL=AssetManagementPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetManagementPage.js","names":[],"sources":["../../../../src/housingvista/pages/assets/AssetManagementPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Building,\n DollarSign,\n TrendingUp,\n BarChart3,\n Plus,\n Download,\n ArrowUpRight,\n ArrowDownRight,\n MapPin,\n Eye,\n} from 'lucide-react';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { StatusBadge } from '../../components';\nimport { FilterBar } from '../../components';\nimport { EmptyState } from '../../components';\nimport { useAssets } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { cn, formatCurrency } from '../../utils/helpers';\nimport type { Asset } from '../../types';\n\nfunction getStatusVariant(status: Asset['status']): 'success' | 'warning' | 'info' | 'muted' {\n switch (status) {\n case 'active':\n return 'success';\n case 'under_renovation':\n return 'warning';\n case 'pending_sale':\n return 'info';\n case 'sold':\n return 'muted';\n default:\n return 'muted';\n }\n}\n\nexport function AssetManagementPage() {\n const navigate = useNavigate();\n const pagination = useServerPagination(10);\n const { data: assets, loading } = useAssets(pagination.pagination);\n const [search, setSearch] = useState('');\n\n const filtered = useMemo(() => {\n if (!search) return assets;\n const q = search.toLowerCase();\n return assets.filter(\n (a) =>\n a.title.toLowerCase().includes(q) || a.location.toLowerCase().includes(q) || a.type.toLowerCase().includes(q)\n );\n }, [assets, search]);\n\n const portfolioStats = useMemo(() => {\n const totalValue = assets.reduce((sum, a) => sum + a.currentValue, 0);\n const totalMonthlyIncome = assets\n .filter((a) => a.rentalYield)\n .reduce((sum, a) => sum + (a.currentValue * (a.rentalYield || 0)) / 100 / 12, 0);\n const avgAppreciation =\n assets.length > 0 ? assets.reduce((sum, a) => sum + a.appreciationPercent, 0) / assets.length : 0;\n const totalPurchasePrice = assets.reduce((sum, a) => sum + a.purchasePrice, 0);\n const totalROI = totalPurchasePrice > 0 ? ((totalValue - totalPurchasePrice) / totalPurchasePrice) * 100 : 0;\n return { totalValue, totalMonthlyIncome, avgAppreciation, totalROI };\n }, [assets]);\n\n const assetColumns: Column<Asset>[] = [\n {\n key: 'title',\n header: 'Property',\n render: (item) => (\n <div className=\"flex items-center gap-3\">\n <img src={item.imageUrl} alt={item.title} className=\"h-10 w-10 rounded-md object-cover\" />\n <div>\n <div className=\"font-medium text-text-primary\">{item.title}</div>\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {item.location}\n </div>\n </div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (item) => <span className=\"text-sm capitalize text-text-primary\">{item.type}</span>,\n },\n {\n key: 'purchasePrice',\n header: 'Purchase Price',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatCurrency(item.purchasePrice)}</span>,\n },\n {\n key: 'currentValue',\n header: 'Current Value',\n render: (item) => (\n <span className=\"text-sm font-medium text-text-primary\">{formatCurrency(item.currentValue)}</span>\n ),\n },\n {\n key: 'appreciationPercent',\n header: 'Change',\n render: (item) => {\n const change = item.currentValue - item.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n className={cn(\n 'flex items-center gap-1 text-sm font-medium',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {formatCurrency(Math.abs(change))}\n <span className=\"text-xs\">({item.appreciationPercent}%)</span>\n </div>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n render: (item) => <StatusBadge label={item.status.replace('_', ' ')} variant={getStatusVariant(item.status)} />,\n },\n {\n key: 'actions',\n header: '',\n sortable: false,\n render: (item) => (\n <button\n onClick={(e) => {\n e.stopPropagation();\n navigate(`/assets/${item.id}`);\n }}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Eye className=\"h-3 w-3\" />\n View\n </button>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Asset Management\"\n description=\"Monitor and manage your property portfolio performance\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\">\n <Download className=\"h-4 w-4\" />\n Export\n </button>\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n </div>\n }\n >\n {/* Portfolio Value Stats */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n label=\"Total Value\"\n value={portfolioStats.totalValue}\n prefix=\"$\"\n icon={<DollarSign className=\"h-4 w-4\" />}\n trend=\"up\"\n change={8}\n />\n <StatCard\n label=\"Monthly Income\"\n value={Math.round(portfolioStats.totalMonthlyIncome)}\n prefix=\"$\"\n icon={<TrendingUp className=\"h-4 w-4\" />}\n trend=\"up\"\n change={5}\n />\n <StatCard\n label=\"Avg. Appreciation\"\n value={`${portfolioStats.avgAppreciation.toFixed(1)}%`}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n trend=\"up\"\n change={2}\n />\n <StatCard\n label=\"Overall ROI\"\n value={`${portfolioStats.totalROI.toFixed(1)}%`}\n icon={<Building className=\"h-4 w-4\" />}\n trend=\"up\"\n change={3}\n />\n </div>\n\n {/* Asset Cards Overview */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {assets.map((asset) => {\n const change = asset.currentValue - asset.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n key={asset.id}\n className=\"cursor-pointer rounded-lg border border-border-subtle bg-bg-surface p-4 shadow-sm transition-shadow hover:shadow-md\"\n onClick={() => navigate(`/assets/${asset.id}`)}\n >\n <div className=\"mb-3 flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <img src={asset.imageUrl} alt={asset.title} className=\"h-12 w-12 rounded-lg object-cover\" />\n <div>\n <h3 className=\"font-medium text-text-primary\">{asset.title}</h3>\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {asset.location}\n </p>\n </div>\n </div>\n <StatusBadge label={asset.status.replace('_', ' ')} variant={getStatusVariant(asset.status)} />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">Current Value</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(asset.currentValue)}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Change</p>\n <p\n className={cn(\n 'flex items-center gap-0.5 text-sm font-semibold',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {asset.appreciationPercent}%\n </p>\n </div>\n {asset.rentalYield && (\n <div>\n <p className=\"text-xs text-text-secondary\">Rental Yield</p>\n <p className=\"text-sm font-semibold text-text-primary\">{asset.rentalYield}%</p>\n </div>\n )}\n <div>\n <p className=\"text-xs text-text-secondary\">Category</p>\n <p className=\"text-sm capitalize text-text-primary\">{asset.category}</p>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Asset Table */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">All Assets</h2>\n <FilterBar\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder=\"Search assets by name, location, or type...\"\n />\n <div className=\"mt-4\">\n {filtered.length > 0 ? (\n <DataTable\n columns={assetColumns}\n data={filtered}\n keyField=\"id\"\n onRowClick={(item) => navigate(`/assets/${item.id}`)}\n pageSize={10}\n serverPagination={{\n page: pagination.page,\n pageSize: pagination.pageSize,\n hasNextPage: assets.length >= pagination.pageSize,\n loading,\n onPageChange: pagination.setPage,\n onPageSizeChange: pagination.setPageSize,\n }}\n />\n ) : (\n <EmptyState\n icon={<Building className=\"h-12 w-12\" />}\n title=\"No assets found\"\n description=\"Try adjusting your search criteria or add a new asset.\"\n action={\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n }\n />\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssetManagementPage.js","names":[],"sources":["../../../../src/housingvista/pages/assets/AssetManagementPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Building,\n DollarSign,\n TrendingUp,\n BarChart3,\n Plus,\n Download,\n ArrowUpRight,\n ArrowDownRight,\n MapPin,\n Eye,\n} from 'lucide-react';\nimport { EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { StatusBadge } from '../../components';\nimport { FilterBar } from '../../components';\nimport { EmptyState } from '../../components';\nimport { useAssets } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { cn, formatCurrency } from '../../utils/helpers';\nimport type { Asset } from '../../types';\n\nfunction getStatusVariant(status: Asset['status']): 'success' | 'warning' | 'info' | 'muted' {\n switch (status) {\n case 'active':\n return 'success';\n case 'under_renovation':\n return 'warning';\n case 'pending_sale':\n return 'info';\n case 'sold':\n return 'muted';\n default:\n return 'muted';\n }\n}\n\nexport function AssetManagementPage() {\n const navigate = useNavigate();\n const pagination = useServerPagination(10);\n const { data: assets, loading } = useAssets(pagination.pagination);\n const [search, setSearch] = useState('');\n\n const filtered = useMemo(() => {\n if (!search) return assets;\n const q = search.toLowerCase();\n return assets.filter(\n (a) =>\n a.title.toLowerCase().includes(q) || a.location.toLowerCase().includes(q) || a.type.toLowerCase().includes(q)\n );\n }, [assets, search]);\n\n const portfolioStats = useMemo(() => {\n const totalValue = assets.reduce((sum, a) => sum + a.currentValue, 0);\n const totalMonthlyIncome = assets\n .filter((a) => a.rentalYield)\n .reduce((sum, a) => sum + (a.currentValue * (a.rentalYield || 0)) / 100 / 12, 0);\n const avgAppreciation =\n assets.length > 0 ? assets.reduce((sum, a) => sum + a.appreciationPercent, 0) / assets.length : 0;\n const totalPurchasePrice = assets.reduce((sum, a) => sum + a.purchasePrice, 0);\n const totalROI = totalPurchasePrice > 0 ? ((totalValue - totalPurchasePrice) / totalPurchasePrice) * 100 : 0;\n return { totalValue, totalMonthlyIncome, avgAppreciation, totalROI };\n }, [assets]);\n\n const assetColumns: Column<Asset>[] = [\n {\n key: 'title',\n header: 'Property',\n render: (item) => (\n <div className=\"flex items-center gap-3\">\n <img src={item.imageUrl} alt={item.title} className=\"h-10 w-10 rounded-md object-cover\" />\n <div>\n <div className=\"font-medium text-text-primary\">{item.title}</div>\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {item.location}\n </div>\n </div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (item) => <span className=\"text-sm capitalize text-text-primary\">{item.type}</span>,\n },\n {\n key: 'purchasePrice',\n header: 'Purchase Price',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatCurrency(item.purchasePrice)}</span>,\n },\n {\n key: 'currentValue',\n header: 'Current Value',\n render: (item) => (\n <span className=\"text-sm font-medium text-text-primary\">{formatCurrency(item.currentValue)}</span>\n ),\n },\n {\n key: 'appreciationPercent',\n header: 'Change',\n render: (item) => {\n const change = item.currentValue - item.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n className={cn(\n 'flex items-center gap-1 text-sm font-medium',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {formatCurrency(Math.abs(change))}\n <span className=\"text-xs\">({item.appreciationPercent}%)</span>\n </div>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n render: (item) => <StatusBadge label={item.status.replace('_', ' ')} variant={getStatusVariant(item.status)} />,\n },\n {\n key: 'actions',\n header: '',\n sortable: false,\n render: (item) => (\n <button\n onClick={(e) => {\n e.stopPropagation();\n navigate(`/assets/${item.id}`);\n }}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Eye className=\"h-3 w-3\" />\n View\n </button>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Asset Management\"\n description=\"Monitor and manage your property portfolio performance\"\n purpose=\"See how every property you own is performing as an investment — current value, appreciation, rental yield, and overall ROI in one portfolio view. Use it to decide what to hold, renovate, or sell.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\">\n <Download className=\"h-4 w-4\" />\n Export\n </button>\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n </div>\n }\n >\n {/* Portfolio Value Stats — primary insight zone (one EmphasisPanel per page) */}\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n label=\"Total Value\"\n value={portfolioStats.totalValue}\n prefix=\"$\"\n icon={<DollarSign className=\"h-4 w-4\" />}\n trend=\"up\"\n change={8}\n />\n <StatCard\n label=\"Monthly Income\"\n value={Math.round(portfolioStats.totalMonthlyIncome)}\n prefix=\"$\"\n icon={<TrendingUp className=\"h-4 w-4\" />}\n trend=\"up\"\n change={5}\n />\n <StatCard\n label=\"Avg. Appreciation\"\n value={`${portfolioStats.avgAppreciation.toFixed(1)}%`}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n trend=\"up\"\n change={2}\n />\n <StatCard\n label=\"Overall ROI\"\n value={`${portfolioStats.totalROI.toFixed(1)}%`}\n icon={<Building className=\"h-4 w-4\" />}\n trend=\"up\"\n change={3}\n />\n </div>\n </EmphasisPanel>\n\n {/* Asset Cards Overview */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {assets.map((asset) => {\n const change = asset.currentValue - asset.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n key={asset.id}\n className=\"cursor-pointer rounded-lg border border-border-subtle bg-bg-surface p-4 shadow-sm transition-shadow hover:shadow-md\"\n onClick={() => navigate(`/assets/${asset.id}`)}\n >\n <div className=\"mb-3 flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <img src={asset.imageUrl} alt={asset.title} className=\"h-12 w-12 rounded-lg object-cover\" />\n <div>\n <h3 className=\"font-medium text-text-primary\">{asset.title}</h3>\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {asset.location}\n </p>\n </div>\n </div>\n <StatusBadge label={asset.status.replace('_', ' ')} variant={getStatusVariant(asset.status)} />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">Current Value</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(asset.currentValue)}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Change</p>\n <p\n className={cn(\n 'flex items-center gap-0.5 text-sm font-semibold',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {asset.appreciationPercent}%\n </p>\n </div>\n {asset.rentalYield && (\n <div>\n <p className=\"text-xs text-text-secondary\">Rental Yield</p>\n <p className=\"text-sm font-semibold text-text-primary\">{asset.rentalYield}%</p>\n </div>\n )}\n <div>\n <p className=\"text-xs text-text-secondary\">Category</p>\n <p className=\"text-sm capitalize text-text-primary\">{asset.category}</p>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Asset Table */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">All Assets</h2>\n <FilterBar\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder=\"Search assets by name, location, or type...\"\n />\n <div className=\"mt-4\">\n {filtered.length > 0 ? (\n <DataTable\n columns={assetColumns}\n data={filtered}\n keyField=\"id\"\n onRowClick={(item) => navigate(`/assets/${item.id}`)}\n pageSize={10}\n serverPagination={{\n page: pagination.page,\n pageSize: pagination.pageSize,\n hasNextPage: assets.length >= pagination.pageSize,\n loading,\n onPageChange: pagination.setPage,\n onPageSizeChange: pagination.setPageSize,\n }}\n />\n ) : (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Building className=\"h-12 w-12\" />}\n title=\"No assets found\"\n description=\"Try adjusting your search criteria or add a new asset.\"\n action={\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n }\n />\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -7,10 +7,11 @@ import { useEffect as a, useMemo as o, useRef as s, useState as c } from "react"
|
|
|
7
7
|
import { ArrowLeft as l, MessageCircle as u, Search as d, Send as f } from "lucide-react";
|
|
8
8
|
import { useNavigate as p } from "react-router-dom";
|
|
9
9
|
import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
10
|
+
import { EmphasisPanel as _ } from "@burdenoff/fe-libs/ui";
|
|
10
11
|
//#region src/housingvista/pages/chat/ChatPage.tsx
|
|
11
|
-
var
|
|
12
|
-
function
|
|
13
|
-
let a = n.participants.find((e) => e.id !==
|
|
12
|
+
var v = "1";
|
|
13
|
+
function y({ conversation: n, isActive: r, onClick: i }) {
|
|
14
|
+
let a = n.participants.find((e) => e.id !== v) ?? n.participants[0];
|
|
14
15
|
return /* @__PURE__ */ g("button", {
|
|
15
16
|
onClick: i,
|
|
16
17
|
className: e("flex w-full items-start gap-3 rounded-lg p-3 text-left transition-colors", r ? "bg-bg-sunken" : "hover:bg-bg-sunken/50"),
|
|
@@ -48,7 +49,7 @@ function v({ conversation: n, isActive: r, onClick: i }) {
|
|
|
48
49
|
})]
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
|
-
function
|
|
52
|
+
function b({ message: t, isOwn: n }) {
|
|
52
53
|
return /* @__PURE__ */ h("div", {
|
|
53
54
|
className: e("flex", n ? "justify-end" : "justify-start"),
|
|
54
55
|
children: /* @__PURE__ */ g("div", {
|
|
@@ -73,54 +74,55 @@ function y({ message: t, isOwn: n }) {
|
|
|
73
74
|
})
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
|
-
function
|
|
77
|
+
function x() {
|
|
77
78
|
p();
|
|
78
|
-
let { data: t } = i(), [
|
|
79
|
-
if (!
|
|
80
|
-
let e =
|
|
79
|
+
let { data: t } = i(), [x, S] = c(t[0]?.id ?? null), [C, w] = c(""), [T, E] = c(""), [D, O] = c({}), k = s(null), A = s(null), j = o(() => {
|
|
80
|
+
if (!C) return t;
|
|
81
|
+
let e = C.toLowerCase();
|
|
81
82
|
return t.filter((t) => t.title.toLowerCase().includes(e) || t.lastMessage.toLowerCase().includes(e) || t.participants.some((t) => t.name.toLowerCase().includes(e)));
|
|
82
|
-
}, [t,
|
|
83
|
-
if (!
|
|
84
|
-
let e =
|
|
85
|
-
return [...
|
|
86
|
-
}, [
|
|
83
|
+
}, [t, C]), M = t.find((e) => e.id === x), N = o(() => {
|
|
84
|
+
if (!M) return [];
|
|
85
|
+
let e = D[M.id] ?? [];
|
|
86
|
+
return [...M.messages, ...e];
|
|
87
|
+
}, [M, D]);
|
|
87
88
|
a(() => {
|
|
88
|
-
|
|
89
|
-
}, [
|
|
90
|
-
let
|
|
91
|
-
if (!
|
|
89
|
+
k.current?.scrollIntoView({ behavior: "smooth" });
|
|
90
|
+
}, [N.length]);
|
|
91
|
+
let P = () => {
|
|
92
|
+
if (!T.trim() || !x) return;
|
|
92
93
|
let e = {
|
|
93
94
|
id: `local-${Date.now()}`,
|
|
94
|
-
senderId:
|
|
95
|
+
senderId: v,
|
|
95
96
|
senderName: "John Doe",
|
|
96
|
-
content:
|
|
97
|
+
content: T.trim(),
|
|
97
98
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
98
99
|
type: "text"
|
|
99
100
|
};
|
|
100
|
-
|
|
101
|
+
O((t) => ({
|
|
101
102
|
...t,
|
|
102
|
-
[
|
|
103
|
-
})),
|
|
104
|
-
},
|
|
105
|
-
e.key === "Enter" && !e.shiftKey && (e.preventDefault(),
|
|
106
|
-
}, [
|
|
107
|
-
|
|
103
|
+
[x]: [...t[x] ?? [], e]
|
|
104
|
+
})), E(""), A.current?.focus();
|
|
105
|
+
}, F = (e) => {
|
|
106
|
+
e.key === "Enter" && !e.shiftKey && (e.preventDefault(), P());
|
|
107
|
+
}, [I, L] = c(!1), R = (e) => {
|
|
108
|
+
S(e), L(!0);
|
|
108
109
|
};
|
|
109
110
|
return /* @__PURE__ */ h(n, {
|
|
110
111
|
title: "Messages",
|
|
111
112
|
description: "Connect with property owners, tenants, and professionals",
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
purpose: "One inbox for every property conversation — owners, tenants, agents, and service providers. Use it to negotiate deals, coordinate viewings and repairs, and keep a written record of what was agreed.",
|
|
114
|
+
children: /* @__PURE__ */ g(_, {
|
|
115
|
+
className: "flex h-[calc(100vh-220px)] min-h-[500px] overflow-hidden p-0",
|
|
114
116
|
children: [/* @__PURE__ */ g("div", {
|
|
115
|
-
className: e("w-full shrink-0 border-r border-border-subtle md:w-80",
|
|
117
|
+
className: e("w-full shrink-0 border-r border-border-subtle md:w-80", I && "hidden md:block"),
|
|
116
118
|
children: [/* @__PURE__ */ h("div", {
|
|
117
119
|
className: "border-b border-border-subtle p-3",
|
|
118
120
|
children: /* @__PURE__ */ g("div", {
|
|
119
121
|
className: "relative",
|
|
120
122
|
children: [/* @__PURE__ */ h(d, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }), /* @__PURE__ */ h("input", {
|
|
121
123
|
type: "text",
|
|
122
|
-
value:
|
|
123
|
-
onChange: (e) =>
|
|
124
|
+
value: C,
|
|
125
|
+
onChange: (e) => w(e.target.value),
|
|
124
126
|
placeholder: "Search conversations...",
|
|
125
127
|
className: "h-9 w-full rounded-md border border-border-subtle bg-bg-surface pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
126
128
|
})]
|
|
@@ -128,44 +130,43 @@ function b() {
|
|
|
128
130
|
}), /* @__PURE__ */ h("div", {
|
|
129
131
|
className: "overflow-y-auto",
|
|
130
132
|
style: { height: "calc(100% - 58px)" },
|
|
131
|
-
children:
|
|
133
|
+
children: j.length > 0 ? /* @__PURE__ */ h("div", {
|
|
132
134
|
className: "space-y-0.5 p-2",
|
|
133
|
-
children:
|
|
135
|
+
children: j.map((e) => /* @__PURE__ */ h(y, {
|
|
134
136
|
conversation: e,
|
|
135
|
-
isActive: e.id ===
|
|
136
|
-
onClick: () =>
|
|
137
|
+
isActive: e.id === x,
|
|
138
|
+
onClick: () => R(e.id)
|
|
137
139
|
}, e.id))
|
|
138
|
-
}) : /* @__PURE__ */
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
})]
|
|
140
|
+
}) : /* @__PURE__ */ h(r, {
|
|
141
|
+
illustration: "empty-search",
|
|
142
|
+
title: "No conversations found",
|
|
143
|
+
description: "Try a different search, or start a chat from a property or service provider.",
|
|
144
|
+
className: "border-0 bg-transparent p-8"
|
|
144
145
|
})
|
|
145
146
|
})]
|
|
146
147
|
}), /* @__PURE__ */ h("div", {
|
|
147
|
-
className: e("flex flex-1 flex-col", !
|
|
148
|
-
children:
|
|
148
|
+
className: e("flex flex-1 flex-col", !I && "hidden md:flex"),
|
|
149
|
+
children: M ? /* @__PURE__ */ g(m, { children: [
|
|
149
150
|
/* @__PURE__ */ g("div", {
|
|
150
151
|
className: "flex items-center gap-3 border-b border-border-subtle px-4 py-3",
|
|
151
152
|
children: [
|
|
152
153
|
/* @__PURE__ */ h("button", {
|
|
153
|
-
onClick: () =>
|
|
154
|
+
onClick: () => L(!1),
|
|
154
155
|
className: "md:hidden rounded-md p-1 text-text-secondary hover:bg-bg-sunken",
|
|
155
156
|
children: /* @__PURE__ */ h(l, { className: "h-5 w-5" })
|
|
156
157
|
}),
|
|
157
158
|
/* @__PURE__ */ h("div", {
|
|
158
159
|
className: "flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-primary",
|
|
159
|
-
children:
|
|
160
|
+
children: M.participants.find((e) => e.id !== v)?.name.charAt(0) ?? "?"
|
|
160
161
|
}),
|
|
161
162
|
/* @__PURE__ */ g("div", {
|
|
162
163
|
className: "min-w-0 flex-1",
|
|
163
164
|
children: [/* @__PURE__ */ h("h3", {
|
|
164
165
|
className: "truncate text-sm font-semibold text-text-primary",
|
|
165
|
-
children:
|
|
166
|
+
children: M.title
|
|
166
167
|
}), /* @__PURE__ */ h("p", {
|
|
167
168
|
className: "text-xs text-text-secondary",
|
|
168
|
-
children:
|
|
169
|
+
children: M.participants.map((e) => e.name).join(", ")
|
|
169
170
|
})]
|
|
170
171
|
})
|
|
171
172
|
]
|
|
@@ -174,10 +175,10 @@ function b() {
|
|
|
174
175
|
className: "flex-1 overflow-y-auto p-4",
|
|
175
176
|
children: /* @__PURE__ */ g("div", {
|
|
176
177
|
className: "space-y-4",
|
|
177
|
-
children: [
|
|
178
|
+
children: [N.map((e) => /* @__PURE__ */ h(b, {
|
|
178
179
|
message: e,
|
|
179
|
-
isOwn: e.senderId ===
|
|
180
|
-
}, e.id)), /* @__PURE__ */ h("div", { ref:
|
|
180
|
+
isOwn: e.senderId === v
|
|
181
|
+
}, e.id)), /* @__PURE__ */ h("div", { ref: k })]
|
|
181
182
|
})
|
|
182
183
|
}),
|
|
183
184
|
/* @__PURE__ */ h("div", {
|
|
@@ -185,17 +186,17 @@ function b() {
|
|
|
185
186
|
children: /* @__PURE__ */ g("div", {
|
|
186
187
|
className: "flex items-center gap-2",
|
|
187
188
|
children: [/* @__PURE__ */ h("input", {
|
|
188
|
-
ref:
|
|
189
|
+
ref: A,
|
|
189
190
|
type: "text",
|
|
190
|
-
value:
|
|
191
|
-
onChange: (e) =>
|
|
192
|
-
onKeyDown:
|
|
191
|
+
value: T,
|
|
192
|
+
onChange: (e) => E(e.target.value),
|
|
193
|
+
onKeyDown: F,
|
|
193
194
|
placeholder: "Type a message...",
|
|
194
195
|
className: "h-10 flex-1 rounded-md border border-border-subtle bg-bg-surface px-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
195
196
|
}), /* @__PURE__ */ h("button", {
|
|
196
|
-
onClick:
|
|
197
|
-
disabled: !
|
|
198
|
-
className: e("flex h-10 w-10 items-center justify-center rounded-md transition-colors",
|
|
197
|
+
onClick: P,
|
|
198
|
+
disabled: !T.trim(),
|
|
199
|
+
className: e("flex h-10 w-10 items-center justify-center rounded-md transition-colors", T.trim() ? "bg-foreground text-background hover:bg-foreground/90" : "bg-bg-sunken text-text-secondary cursor-not-allowed"),
|
|
199
200
|
children: /* @__PURE__ */ h(f, { className: "h-4 w-4" })
|
|
200
201
|
})]
|
|
201
202
|
})
|
|
@@ -203,6 +204,7 @@ function b() {
|
|
|
203
204
|
] }) : /* @__PURE__ */ h("div", {
|
|
204
205
|
className: "flex flex-1 items-center justify-center",
|
|
205
206
|
children: /* @__PURE__ */ h(r, {
|
|
207
|
+
illustration: "empty-generic",
|
|
206
208
|
icon: /* @__PURE__ */ h(u, { className: "h-12 w-12" }),
|
|
207
209
|
title: "Select a conversation",
|
|
208
210
|
description: "Choose a conversation from the list to start chatting"
|
|
@@ -213,6 +215,6 @@ function b() {
|
|
|
213
215
|
});
|
|
214
216
|
}
|
|
215
217
|
//#endregion
|
|
216
|
-
export {
|
|
218
|
+
export { x as ChatPage };
|
|
217
219
|
|
|
218
220
|
//# sourceMappingURL=ChatPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatPage.js","names":[],"sources":["../../../../src/housingvista/pages/chat/ChatPage.tsx"],"sourcesContent":["import { useState, useRef, useEffect, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Send, Search, MessageCircle, ArrowLeft } from 'lucide-react';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useConversations } from '../../hooks';\nimport { cn, getRelativeTime } from '../../utils/helpers';\nimport type { ConversationListItem, ConversationMessage } from '../../types';\n\nconst CURRENT_USER_ID = '1'; // John Doe\n\nfunction ConversationItem({\n conversation,\n isActive,\n onClick,\n}: {\n conversation: ConversationListItem;\n isActive: boolean;\n onClick: () => void;\n}) {\n const otherParticipant =\n conversation.participants.find((p) => p.id !== CURRENT_USER_ID) ?? conversation.participants[0];\n\n return (\n <button\n onClick={onClick}\n className={cn(\n 'flex w-full items-start gap-3 rounded-lg p-3 text-left transition-colors',\n isActive ? 'bg-bg-sunken' : 'hover:bg-bg-sunken/50'\n )}\n >\n {/* Avatar */}\n <div className=\"relative shrink-0\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-primary\">\n {otherParticipant?.avatar ? (\n <img\n src={otherParticipant.avatar}\n alt={otherParticipant.name}\n className=\"h-full w-full rounded-full object-cover\"\n />\n ) : (\n (otherParticipant?.name.charAt(0) ?? '?')\n )}\n </div>\n </div>\n\n {/* Content */}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span\n className={cn(\n 'truncate text-sm font-medium',\n conversation.unreadCount > 0 ? 'text-text-primary' : 'text-text-primary'\n )}\n >\n {conversation.title}\n </span>\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {getRelativeTime(conversation.lastMessageTime)}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2 mt-0.5\">\n <p\n className={cn(\n 'truncate text-xs',\n conversation.unreadCount > 0 ? 'font-medium text-text-primary' : 'text-text-secondary'\n )}\n >\n {conversation.lastMessage}\n </p>\n {conversation.unreadCount > 0 && (\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-600 text-[10px] font-bold text-white\">\n {conversation.unreadCount}\n </span>\n )}\n </div>\n </div>\n </button>\n );\n}\n\nfunction MessageBubble({ message, isOwn }: { message: ConversationMessage; isOwn: boolean }) {\n return (\n <div className={cn('flex', isOwn ? 'justify-end' : 'justify-start')}>\n <div className={cn('max-w-[75%] space-y-1')}>\n {!isOwn && <span className=\"text-xs font-medium text-text-secondary\">{message.senderName}</span>}\n <div\n className={cn(\n 'rounded-xl px-4 py-2 text-sm',\n isOwn ? 'bg-foreground text-background rounded-br-sm' : 'bg-bg-sunken text-text-primary rounded-bl-sm'\n )}\n >\n {message.content}\n </div>\n <span className={cn('block text-[10px] text-text-secondary', isOwn && 'text-right')}>\n {new Date(message.timestamp).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}\n </span>\n </div>\n </div>\n );\n}\n\nexport function ChatPage() {\n const navigate = useNavigate();\n const { data: conversations } = useConversations();\n const [selectedId, setSelectedId] = useState<string | null>(conversations[0]?.id ?? null);\n const [searchQuery, setSearchQuery] = useState('');\n const [newMessage, setNewMessage] = useState('');\n const [localMessages, setLocalMessages] = useState<Record<string, ConversationMessage[]>>({});\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const filteredConversations = useMemo(() => {\n if (!searchQuery) return conversations;\n const q = searchQuery.toLowerCase();\n return conversations.filter(\n (c) =>\n c.title.toLowerCase().includes(q) ||\n c.lastMessage.toLowerCase().includes(q) ||\n c.participants.some((p) => p.name.toLowerCase().includes(q))\n );\n }, [conversations, searchQuery]);\n\n const activeConversation = conversations.find((c) => c.id === selectedId);\n\n const allMessages = useMemo(() => {\n if (!activeConversation) return [];\n const extras = localMessages[activeConversation.id] ?? [];\n return [...activeConversation.messages, ...extras];\n }, [activeConversation, localMessages]);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, [allMessages.length]);\n\n const handleSend = () => {\n if (!newMessage.trim() || !selectedId) return;\n const msg: ConversationMessage = {\n id: `local-${Date.now()}`,\n senderId: CURRENT_USER_ID,\n senderName: 'John Doe',\n content: newMessage.trim(),\n timestamp: new Date().toISOString(),\n type: 'text',\n };\n setLocalMessages((prev) => ({\n ...prev,\n [selectedId]: [...(prev[selectedId] ?? []), msg],\n }));\n setNewMessage('');\n inputRef.current?.focus();\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n };\n\n // Mobile: show list when nothing selected, show chat when something is selected\n const [mobileShowChat, setMobileShowChat] = useState(false);\n\n const handleSelectConversation = (id: string) => {\n setSelectedId(id);\n setMobileShowChat(true);\n };\n\n return (\n <PageLayout title=\"Messages\" description=\"Connect with property owners, tenants, and professionals\">\n <div className=\"flex h-[calc(100vh-220px)] min-h-[500px] overflow-hidden rounded-lg border border-border-subtle bg-bg-surface\">\n {/* Left Panel - Conversation List */}\n <div className={cn('w-full shrink-0 border-r border-border-subtle md:w-80', mobileShowChat && 'hidden md:block')}>\n {/* Search */}\n <div className=\"border-b border-border-subtle p-3\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"Search conversations...\"\n className=\"h-9 w-full rounded-md border border-border-subtle bg-bg-surface pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n\n {/* Conversation List */}\n <div className=\"overflow-y-auto\" style={{ height: 'calc(100% - 58px)' }}>\n {filteredConversations.length > 0 ? (\n <div className=\"space-y-0.5 p-2\">\n {filteredConversations.map((conversation) => (\n <ConversationItem\n key={conversation.id}\n conversation={conversation}\n isActive={conversation.id === selectedId}\n onClick={() => handleSelectConversation(conversation.id)}\n />\n ))}\n </div>\n ) : (\n <div className=\"flex flex-col items-center justify-center p-8 text-center\">\n <MessageCircle className=\"mb-2 h-8 w-8 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">No conversations found</p>\n </div>\n )}\n </div>\n </div>\n\n {/* Right Panel - Chat */}\n <div className={cn('flex flex-1 flex-col', !mobileShowChat && 'hidden md:flex')}>\n {activeConversation ? (\n <>\n {/* Chat Header */}\n <div className=\"flex items-center gap-3 border-b border-border-subtle px-4 py-3\">\n <button\n onClick={() => setMobileShowChat(false)}\n className=\"md:hidden rounded-md p-1 text-text-secondary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-5 w-5\" />\n </button>\n <div className=\"flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-primary\">\n {activeConversation.participants.find((p) => p.id !== CURRENT_USER_ID)?.name.charAt(0) ?? '?'}\n </div>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{activeConversation.title}</h3>\n <p className=\"text-xs text-text-secondary\">\n {activeConversation.participants.map((p) => p.name).join(', ')}\n </p>\n </div>\n </div>\n\n {/* Messages */}\n <div className=\"flex-1 overflow-y-auto p-4\">\n <div className=\"space-y-4\">\n {allMessages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} isOwn={msg.senderId === CURRENT_USER_ID} />\n ))}\n <div ref={messagesEndRef} />\n </div>\n </div>\n\n {/* Message Input */}\n <div className=\"border-t border-border-subtle p-3\">\n <div className=\"flex items-center gap-2\">\n <input\n ref={inputRef}\n type=\"text\"\n value={newMessage}\n onChange={(e) => setNewMessage(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Type a message...\"\n className=\"h-10 flex-1 rounded-md border border-border-subtle bg-bg-surface px-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <button\n onClick={handleSend}\n disabled={!newMessage.trim()}\n className={cn(\n 'flex h-10 w-10 items-center justify-center rounded-md transition-colors',\n newMessage.trim()\n ? 'bg-foreground text-background hover:bg-foreground/90'\n : 'bg-bg-sunken text-text-secondary cursor-not-allowed'\n )}\n >\n <Send className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </>\n ) : (\n <div className=\"flex flex-1 items-center justify-center\">\n <EmptyState\n icon={<MessageCircle className=\"h-12 w-12\" />}\n title=\"Select a conversation\"\n description=\"Choose a conversation from the list to start chatting\"\n />\n </div>\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatPage.js","names":[],"sources":["../../../../src/housingvista/pages/chat/ChatPage.tsx"],"sourcesContent":["import { useState, useRef, useEffect, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Send, Search, MessageCircle, ArrowLeft } from 'lucide-react';\nimport { EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useConversations } from '../../hooks';\nimport { cn, getRelativeTime } from '../../utils/helpers';\nimport type { ConversationListItem, ConversationMessage } from '../../types';\n\nconst CURRENT_USER_ID = '1'; // John Doe\n\nfunction ConversationItem({\n conversation,\n isActive,\n onClick,\n}: {\n conversation: ConversationListItem;\n isActive: boolean;\n onClick: () => void;\n}) {\n const otherParticipant =\n conversation.participants.find((p) => p.id !== CURRENT_USER_ID) ?? conversation.participants[0];\n\n return (\n <button\n onClick={onClick}\n className={cn(\n 'flex w-full items-start gap-3 rounded-lg p-3 text-left transition-colors',\n isActive ? 'bg-bg-sunken' : 'hover:bg-bg-sunken/50'\n )}\n >\n {/* Avatar */}\n <div className=\"relative shrink-0\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-primary\">\n {otherParticipant?.avatar ? (\n <img\n src={otherParticipant.avatar}\n alt={otherParticipant.name}\n className=\"h-full w-full rounded-full object-cover\"\n />\n ) : (\n (otherParticipant?.name.charAt(0) ?? '?')\n )}\n </div>\n </div>\n\n {/* Content */}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span\n className={cn(\n 'truncate text-sm font-medium',\n conversation.unreadCount > 0 ? 'text-text-primary' : 'text-text-primary'\n )}\n >\n {conversation.title}\n </span>\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {getRelativeTime(conversation.lastMessageTime)}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2 mt-0.5\">\n <p\n className={cn(\n 'truncate text-xs',\n conversation.unreadCount > 0 ? 'font-medium text-text-primary' : 'text-text-secondary'\n )}\n >\n {conversation.lastMessage}\n </p>\n {conversation.unreadCount > 0 && (\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-600 text-[10px] font-bold text-white\">\n {conversation.unreadCount}\n </span>\n )}\n </div>\n </div>\n </button>\n );\n}\n\nfunction MessageBubble({ message, isOwn }: { message: ConversationMessage; isOwn: boolean }) {\n return (\n <div className={cn('flex', isOwn ? 'justify-end' : 'justify-start')}>\n <div className={cn('max-w-[75%] space-y-1')}>\n {!isOwn && <span className=\"text-xs font-medium text-text-secondary\">{message.senderName}</span>}\n <div\n className={cn(\n 'rounded-xl px-4 py-2 text-sm',\n isOwn ? 'bg-foreground text-background rounded-br-sm' : 'bg-bg-sunken text-text-primary rounded-bl-sm'\n )}\n >\n {message.content}\n </div>\n <span className={cn('block text-[10px] text-text-secondary', isOwn && 'text-right')}>\n {new Date(message.timestamp).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}\n </span>\n </div>\n </div>\n );\n}\n\nexport function ChatPage() {\n const navigate = useNavigate();\n const { data: conversations } = useConversations();\n const [selectedId, setSelectedId] = useState<string | null>(conversations[0]?.id ?? null);\n const [searchQuery, setSearchQuery] = useState('');\n const [newMessage, setNewMessage] = useState('');\n const [localMessages, setLocalMessages] = useState<Record<string, ConversationMessage[]>>({});\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const filteredConversations = useMemo(() => {\n if (!searchQuery) return conversations;\n const q = searchQuery.toLowerCase();\n return conversations.filter(\n (c) =>\n c.title.toLowerCase().includes(q) ||\n c.lastMessage.toLowerCase().includes(q) ||\n c.participants.some((p) => p.name.toLowerCase().includes(q))\n );\n }, [conversations, searchQuery]);\n\n const activeConversation = conversations.find((c) => c.id === selectedId);\n\n const allMessages = useMemo(() => {\n if (!activeConversation) return [];\n const extras = localMessages[activeConversation.id] ?? [];\n return [...activeConversation.messages, ...extras];\n }, [activeConversation, localMessages]);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, [allMessages.length]);\n\n const handleSend = () => {\n if (!newMessage.trim() || !selectedId) return;\n const msg: ConversationMessage = {\n id: `local-${Date.now()}`,\n senderId: CURRENT_USER_ID,\n senderName: 'John Doe',\n content: newMessage.trim(),\n timestamp: new Date().toISOString(),\n type: 'text',\n };\n setLocalMessages((prev) => ({\n ...prev,\n [selectedId]: [...(prev[selectedId] ?? []), msg],\n }));\n setNewMessage('');\n inputRef.current?.focus();\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n };\n\n // Mobile: show list when nothing selected, show chat when something is selected\n const [mobileShowChat, setMobileShowChat] = useState(false);\n\n const handleSelectConversation = (id: string) => {\n setSelectedId(id);\n setMobileShowChat(true);\n };\n\n return (\n <PageLayout\n title=\"Messages\"\n description=\"Connect with property owners, tenants, and professionals\"\n purpose=\"One inbox for every property conversation — owners, tenants, agents, and service providers. Use it to negotiate deals, coordinate viewings and repairs, and keep a written record of what was agreed.\"\n >\n {/* Conversation workspace — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"flex h-[calc(100vh-220px)] min-h-[500px] overflow-hidden p-0\">\n {/* Left Panel - Conversation List */}\n <div className={cn('w-full shrink-0 border-r border-border-subtle md:w-80', mobileShowChat && 'hidden md:block')}>\n {/* Search */}\n <div className=\"border-b border-border-subtle p-3\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"Search conversations...\"\n className=\"h-9 w-full rounded-md border border-border-subtle bg-bg-surface pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n\n {/* Conversation List */}\n <div className=\"overflow-y-auto\" style={{ height: 'calc(100% - 58px)' }}>\n {filteredConversations.length > 0 ? (\n <div className=\"space-y-0.5 p-2\">\n {filteredConversations.map((conversation) => (\n <ConversationItem\n key={conversation.id}\n conversation={conversation}\n isActive={conversation.id === selectedId}\n onClick={() => handleSelectConversation(conversation.id)}\n />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-search\"\n title=\"No conversations found\"\n description=\"Try a different search, or start a chat from a property or service provider.\"\n className=\"border-0 bg-transparent p-8\"\n />\n )}\n </div>\n </div>\n\n {/* Right Panel - Chat */}\n <div className={cn('flex flex-1 flex-col', !mobileShowChat && 'hidden md:flex')}>\n {activeConversation ? (\n <>\n {/* Chat Header */}\n <div className=\"flex items-center gap-3 border-b border-border-subtle px-4 py-3\">\n <button\n onClick={() => setMobileShowChat(false)}\n className=\"md:hidden rounded-md p-1 text-text-secondary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-5 w-5\" />\n </button>\n <div className=\"flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-primary\">\n {activeConversation.participants.find((p) => p.id !== CURRENT_USER_ID)?.name.charAt(0) ?? '?'}\n </div>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{activeConversation.title}</h3>\n <p className=\"text-xs text-text-secondary\">\n {activeConversation.participants.map((p) => p.name).join(', ')}\n </p>\n </div>\n </div>\n\n {/* Messages */}\n <div className=\"flex-1 overflow-y-auto p-4\">\n <div className=\"space-y-4\">\n {allMessages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} isOwn={msg.senderId === CURRENT_USER_ID} />\n ))}\n <div ref={messagesEndRef} />\n </div>\n </div>\n\n {/* Message Input */}\n <div className=\"border-t border-border-subtle p-3\">\n <div className=\"flex items-center gap-2\">\n <input\n ref={inputRef}\n type=\"text\"\n value={newMessage}\n onChange={(e) => setNewMessage(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Type a message...\"\n className=\"h-10 flex-1 rounded-md border border-border-subtle bg-bg-surface px-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <button\n onClick={handleSend}\n disabled={!newMessage.trim()}\n className={cn(\n 'flex h-10 w-10 items-center justify-center rounded-md transition-colors',\n newMessage.trim()\n ? 'bg-foreground text-background hover:bg-foreground/90'\n : 'bg-bg-sunken text-text-secondary cursor-not-allowed'\n )}\n >\n <Send className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </>\n ) : (\n <div className=\"flex flex-1 items-center justify-center\">\n <EmptyState\n illustration=\"empty-generic\"\n icon={<MessageCircle className=\"h-12 w-12\" />}\n title=\"Select a conversation\"\n description=\"Choose a conversation from the list to start chatting\"\n />\n </div>\n )}\n </div>\n </EmphasisPanel>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|