@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.
Files changed (59) hide show
  1. package/dist/housingvista/components/ChartCard.js +11 -9
  2. package/dist/housingvista/components/ChartCard.js.map +1 -1
  3. package/dist/housingvista/components/DataTable.js +63 -60
  4. package/dist/housingvista/components/DataTable.js.map +1 -1
  5. package/dist/housingvista/components/EmptyState.js +16 -9
  6. package/dist/housingvista/components/EmptyState.js.map +1 -1
  7. package/dist/housingvista/components/PageLayout.js +21 -14
  8. package/dist/housingvista/components/PageLayout.js.map +1 -1
  9. package/dist/housingvista/components/PropertyCard.js +2 -2
  10. package/dist/housingvista/components/PropertyCard.js.map +1 -1
  11. package/dist/housingvista/components/StatCard.js +20 -17
  12. package/dist/housingvista/components/StatCard.js.map +1 -1
  13. package/dist/housingvista/components/StatusBadge.js +1 -1
  14. package/dist/housingvista/components/StatusBadge.js.map +1 -1
  15. package/dist/housingvista/pages/HomePage.js +101 -58
  16. package/dist/housingvista/pages/HomePage.js.map +1 -1
  17. package/dist/housingvista/pages/amc/AMCManagementPage.js +59 -48
  18. package/dist/housingvista/pages/amc/AMCManagementPage.js.map +1 -1
  19. package/dist/housingvista/pages/analytics/AnalyticsPage.js +113 -90
  20. package/dist/housingvista/pages/analytics/AnalyticsPage.js.map +1 -1
  21. package/dist/housingvista/pages/assets/AssetManagementPage.js +66 -60
  22. package/dist/housingvista/pages/assets/AssetManagementPage.js.map +1 -1
  23. package/dist/housingvista/pages/chat/ChatPage.js +60 -58
  24. package/dist/housingvista/pages/chat/ChatPage.js.map +1 -1
  25. package/dist/housingvista/pages/finance/HomeLoansPage.js +23 -20
  26. package/dist/housingvista/pages/finance/HomeLoansPage.js.map +1 -1
  27. package/dist/housingvista/pages/investments/InvestmentDetailPage.js +114 -81
  28. package/dist/housingvista/pages/investments/InvestmentDetailPage.js.map +1 -1
  29. package/dist/housingvista/pages/investments/InvestmentsPage.js +65 -59
  30. package/dist/housingvista/pages/investments/InvestmentsPage.js.map +1 -1
  31. package/dist/housingvista/pages/legal/LegalPage.js +17 -14
  32. package/dist/housingvista/pages/legal/LegalPage.js.map +1 -1
  33. package/dist/housingvista/pages/market/AreaInsightsPage.js +13 -10
  34. package/dist/housingvista/pages/market/AreaInsightsPage.js.map +1 -1
  35. package/dist/housingvista/pages/market/MarketAnalyticsPage.js +155 -123
  36. package/dist/housingvista/pages/market/MarketAnalyticsPage.js.map +1 -1
  37. package/dist/housingvista/pages/marketplace/MarketplacePage.js +15 -12
  38. package/dist/housingvista/pages/marketplace/MarketplacePage.js.map +1 -1
  39. package/dist/housingvista/pages/notifications/NotificationsPage.js +24 -21
  40. package/dist/housingvista/pages/notifications/NotificationsPage.js.map +1 -1
  41. package/dist/housingvista/pages/properties/AddPropertyPage.js +73 -71
  42. package/dist/housingvista/pages/properties/AddPropertyPage.js.map +1 -1
  43. package/dist/housingvista/pages/properties/PropertiesPage.js +141 -135
  44. package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
  45. package/dist/housingvista/pages/properties/PropertyDetailPage.js +192 -154
  46. package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
  47. package/dist/housingvista/pages/rental/RentalManagementPage.js +185 -160
  48. package/dist/housingvista/pages/rental/RentalManagementPage.js.map +1 -1
  49. package/dist/housingvista/pages/settings/ProfilePage.js +51 -49
  50. package/dist/housingvista/pages/settings/ProfilePage.js.map +1 -1
  51. package/dist/housingvista/pages/settings/SettingsPage.js +56 -54
  52. package/dist/housingvista/pages/settings/SettingsPage.js.map +1 -1
  53. package/dist/housingvista/pages/transactions/TransactionDetailPage.js +183 -142
  54. package/dist/housingvista/pages/transactions/TransactionDetailPage.js.map +1 -1
  55. package/dist/housingvista/pages/transactions/TransactionsPage.js +90 -84
  56. package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
  57. package/dist/housingvista/pages/verification/VerificationPage.js +49 -42
  58. package/dist/housingvista/pages/verification/VerificationPage.js.map +1 -1
  59. 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 T(e) {
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 E() {
26
- let E = S(), D = l(10), { data: O, loading: k } = c(D.pagination), [A, j] = d(""), M = u(() => {
27
- if (!A) return O;
28
- let e = A.toLowerCase();
29
- return O.filter((t) => t.title.toLowerCase().includes(e) || t.location.toLowerCase().includes(e) || t.type.toLowerCase().includes(e));
30
- }, [O, A]), N = u(() => {
31
- let e = O.reduce((e, t) => e + t.currentValue, 0), t = O.filter((e) => e.rentalYield).reduce((e, t) => e + t.currentValue * (t.rentalYield || 0) / 100 / 12, 0), n = O.length > 0 ? O.reduce((e, t) => e + t.appreciationPercent, 0) / O.length : 0, r = O.reduce((e, t) => e + t.purchasePrice, 0);
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
- }, [O]);
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__ */ w("div", {
54
- className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
55
- children: [
56
- /* @__PURE__ */ C(r, {
57
- label: "Total Value",
58
- value: N.totalValue,
59
- prefix: "$",
60
- icon: /* @__PURE__ */ C(g, { className: "h-4 w-4" }),
61
- trend: "up",
62
- change: 8
63
- }),
64
- /* @__PURE__ */ C(r, {
65
- label: "Monthly Income",
66
- value: Math.round(N.totalMonthlyIncome),
67
- prefix: "$",
68
- icon: /* @__PURE__ */ C(x, { className: "h-4 w-4" }),
69
- trend: "up",
70
- change: 5
71
- }),
72
- /* @__PURE__ */ C(r, {
73
- label: "Avg. Appreciation",
74
- value: `${N.avgAppreciation.toFixed(1)}%`,
75
- icon: /* @__PURE__ */ C(m, { className: "h-4 w-4" }),
76
- trend: "up",
77
- change: 2
78
- }),
79
- /* @__PURE__ */ C(r, {
80
- label: "Overall ROI",
81
- value: `${N.totalROI.toFixed(1)}%`,
82
- icon: /* @__PURE__ */ C(h, { className: "h-4 w-4" }),
83
- trend: "up",
84
- change: 3
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: O.map((n) => {
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: () => E(`/assets/${n.id}`),
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: T(n.status)
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: A,
161
- onSearchChange: j,
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: M.length > 0 ? /* @__PURE__ */ C(i, {
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: T(e.status)
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(), E(`/assets/${e.id}`);
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: M,
258
+ data: N,
254
259
  keyField: "id",
255
- onRowClick: (e) => E(`/assets/${e.id}`),
260
+ onRowClick: (e) => D(`/assets/${e.id}`),
256
261
  pageSize: 10,
257
262
  serverPagination: {
258
- page: D.page,
259
- pageSize: D.pageSize,
260
- hasNextPage: O.length >= D.pageSize,
261
- loading: k,
262
- onPageChange: D.setPage,
263
- onPageSizeChange: D.setPageSize
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 { E as AssetManagementPage };
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 _ = "1";
12
- function v({ conversation: n, isActive: r, onClick: i }) {
13
- let a = n.participants.find((e) => e.id !== _) ?? n.participants[0];
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 y({ message: t, isOwn: n }) {
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 b() {
77
+ function x() {
77
78
  p();
78
- let { data: t } = i(), [b, x] = c(t[0]?.id ?? null), [S, C] = c(""), [w, T] = c(""), [E, D] = c({}), O = s(null), k = s(null), A = o(() => {
79
- if (!S) return t;
80
- let e = S.toLowerCase();
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, S]), j = t.find((e) => e.id === b), M = o(() => {
83
- if (!j) return [];
84
- let e = E[j.id] ?? [];
85
- return [...j.messages, ...e];
86
- }, [j, E]);
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
- O.current?.scrollIntoView({ behavior: "smooth" });
89
- }, [M.length]);
90
- let N = () => {
91
- if (!w.trim() || !b) return;
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: w.trim(),
97
+ content: T.trim(),
97
98
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
98
99
  type: "text"
99
100
  };
100
- D((t) => ({
101
+ O((t) => ({
101
102
  ...t,
102
- [b]: [...t[b] ?? [], e]
103
- })), T(""), k.current?.focus();
104
- }, P = (e) => {
105
- e.key === "Enter" && !e.shiftKey && (e.preventDefault(), N());
106
- }, [F, I] = c(!1), L = (e) => {
107
- x(e), I(!0);
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
- children: /* @__PURE__ */ g("div", {
113
- className: "flex h-[calc(100vh-220px)] min-h-[500px] overflow-hidden rounded-lg border border-border-subtle bg-bg-surface",
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", F && "hidden md:block"),
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: S,
123
- onChange: (e) => C(e.target.value),
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: A.length > 0 ? /* @__PURE__ */ h("div", {
133
+ children: j.length > 0 ? /* @__PURE__ */ h("div", {
132
134
  className: "space-y-0.5 p-2",
133
- children: A.map((e) => /* @__PURE__ */ h(v, {
135
+ children: j.map((e) => /* @__PURE__ */ h(y, {
134
136
  conversation: e,
135
- isActive: e.id === b,
136
- onClick: () => L(e.id)
137
+ isActive: e.id === x,
138
+ onClick: () => R(e.id)
137
139
  }, e.id))
138
- }) : /* @__PURE__ */ g("div", {
139
- className: "flex flex-col items-center justify-center p-8 text-center",
140
- children: [/* @__PURE__ */ h(u, { className: "mb-2 h-8 w-8 text-text-secondary" }), /* @__PURE__ */ h("p", {
141
- className: "text-sm text-text-secondary",
142
- children: "No conversations found"
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", !F && "hidden md:flex"),
148
- children: j ? /* @__PURE__ */ g(m, { 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: () => I(!1),
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: j.participants.find((e) => e.id !== _)?.name.charAt(0) ?? "?"
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: j.title
166
+ children: M.title
166
167
  }), /* @__PURE__ */ h("p", {
167
168
  className: "text-xs text-text-secondary",
168
- children: j.participants.map((e) => e.name).join(", ")
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: [M.map((e) => /* @__PURE__ */ h(y, {
178
+ children: [N.map((e) => /* @__PURE__ */ h(b, {
178
179
  message: e,
179
- isOwn: e.senderId === _
180
- }, e.id)), /* @__PURE__ */ h("div", { ref: O })]
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: k,
189
+ ref: A,
189
190
  type: "text",
190
- value: w,
191
- onChange: (e) => T(e.target.value),
192
- onKeyDown: P,
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: N,
197
- disabled: !w.trim(),
198
- className: e("flex h-10 w-10 items-center justify-center rounded-md transition-colors", w.trim() ? "bg-foreground text-background hover:bg-foreground/90" : "bg-bg-sunken text-text-secondary cursor-not-allowed"),
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 { b as ChatPage };
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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"}