@burdenoff/microfe-housingvista 2026.626.2 → 2026.703.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 (74) hide show
  1. package/dist/housingvista/HousingVistaRoot.js +60 -59
  2. package/dist/housingvista/HousingVistaRoot.js.map +1 -1
  3. package/dist/housingvista/HousingVistaRoutes.js +25 -21
  4. package/dist/housingvista/HousingVistaRoutes.js.map +1 -1
  5. package/dist/housingvista/components/DataTable.js +1 -1
  6. package/dist/housingvista/components/DataTable.js.map +1 -1
  7. package/dist/housingvista/components/EmptyState.js.map +1 -1
  8. package/dist/housingvista/components/ErrorBoundary.js +3 -3
  9. package/dist/housingvista/components/ErrorBoundary.js.map +1 -1
  10. package/dist/housingvista/components/LoadingSkeleton.js +76 -1
  11. package/dist/housingvista/components/LoadingSkeleton.js.map +1 -0
  12. package/dist/housingvista/components/PageLayout.js.map +1 -1
  13. package/dist/housingvista/components/PropertyCard.js +49 -50
  14. package/dist/housingvista/components/PropertyCard.js.map +1 -1
  15. package/dist/housingvista/components/StatCard.js +3 -3
  16. package/dist/housingvista/components/StatCard.js.map +1 -1
  17. package/dist/housingvista/components/StatusBadge.js +8 -8
  18. package/dist/housingvista/components/StatusBadge.js.map +1 -1
  19. package/dist/housingvista/components/index.js +0 -2
  20. package/dist/housingvista/hooks/index.js +670 -198
  21. package/dist/housingvista/hooks/index.js.map +1 -1
  22. package/dist/housingvista/lib/toast.js +36 -0
  23. package/dist/housingvista/lib/toast.js.map +1 -0
  24. package/dist/housingvista/pages/HomePage.js +8 -8
  25. package/dist/housingvista/pages/HomePage.js.map +1 -1
  26. package/dist/housingvista/pages/amc/AMCManagementPage.js +254 -78
  27. package/dist/housingvista/pages/amc/AMCManagementPage.js.map +1 -1
  28. package/dist/housingvista/pages/analytics/AnalyticsPage.js +192 -161
  29. package/dist/housingvista/pages/analytics/AnalyticsPage.js.map +1 -1
  30. package/dist/housingvista/pages/assets/AssetManagementPage.js +4 -4
  31. package/dist/housingvista/pages/assets/AssetManagementPage.js.map +1 -1
  32. package/dist/housingvista/pages/chat/ChatPage.js +17 -209
  33. package/dist/housingvista/pages/chat/ChatPage.js.map +1 -1
  34. package/dist/housingvista/pages/finance/HomeLoansPage.js +11 -11
  35. package/dist/housingvista/pages/finance/HomeLoansPage.js.map +1 -1
  36. package/dist/housingvista/pages/investments/InvestmentDetailPage.js +243 -205
  37. package/dist/housingvista/pages/investments/InvestmentDetailPage.js.map +1 -1
  38. package/dist/housingvista/pages/investments/InvestmentsPage.js +348 -114
  39. package/dist/housingvista/pages/investments/InvestmentsPage.js.map +1 -1
  40. package/dist/housingvista/pages/legal/LegalPage.js +8 -8
  41. package/dist/housingvista/pages/legal/LegalPage.js.map +1 -1
  42. package/dist/housingvista/pages/market/AreaInsightsPage.js +81 -84
  43. package/dist/housingvista/pages/market/AreaInsightsPage.js.map +1 -1
  44. package/dist/housingvista/pages/market/MarketAnalyticsPage.js +83 -85
  45. package/dist/housingvista/pages/market/MarketAnalyticsPage.js.map +1 -1
  46. package/dist/housingvista/pages/marketplace/MarketplacePage.js +6 -6
  47. package/dist/housingvista/pages/marketplace/MarketplacePage.js.map +1 -1
  48. package/dist/housingvista/pages/notifications/NotificationsPage.js +19 -133
  49. package/dist/housingvista/pages/notifications/NotificationsPage.js.map +1 -1
  50. package/dist/housingvista/pages/properties/AddPropertyPage.js +279 -243
  51. package/dist/housingvista/pages/properties/AddPropertyPage.js.map +1 -1
  52. package/dist/housingvista/pages/properties/PropertiesPage.js +216 -165
  53. package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
  54. package/dist/housingvista/pages/properties/PropertyDetailPage.js +619 -424
  55. package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
  56. package/dist/housingvista/pages/rental/RentalManagementPage.js +493 -130
  57. package/dist/housingvista/pages/rental/RentalManagementPage.js.map +1 -1
  58. package/dist/housingvista/pages/settings/ProfilePage.js +102 -102
  59. package/dist/housingvista/pages/settings/ProfilePage.js.map +1 -1
  60. package/dist/housingvista/pages/settings/SettingsPage.js +99 -139
  61. package/dist/housingvista/pages/settings/SettingsPage.js.map +1 -1
  62. package/dist/housingvista/pages/shortlist/ShortlistPage.js +63 -0
  63. package/dist/housingvista/pages/shortlist/ShortlistPage.js.map +1 -0
  64. package/dist/housingvista/pages/transactions/TransactionDetailPage.js +140 -185
  65. package/dist/housingvista/pages/transactions/TransactionDetailPage.js.map +1 -1
  66. package/dist/housingvista/pages/transactions/TransactionsPage.js +269 -110
  67. package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
  68. package/dist/housingvista/pages/verification/VerificationPage.js +195 -116
  69. package/dist/housingvista/pages/verification/VerificationPage.js.map +1 -1
  70. package/dist/node_modules/sonner/dist/index.js +163 -0
  71. package/dist/node_modules/sonner/dist/index.js.map +1 -0
  72. package/package.json +3 -3
  73. package/dist/housingvista/components/Modal.js +0 -3
  74. package/dist/housingvista/components/Toast.js +0 -3
@@ -47,7 +47,7 @@ function O() {
47
47
  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",
48
48
  children: [/* @__PURE__ */ C(_, { className: "h-4 w-4" }), "Export"]
49
49
  }), /* @__PURE__ */ w("button", {
50
- 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",
50
+ className: "inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover",
51
51
  children: [/* @__PURE__ */ C(b, { className: "h-4 w-4" }), "Add Asset"]
52
52
  })]
53
53
  }),
@@ -156,7 +156,7 @@ function O() {
156
156
  className: "text-xs text-text-secondary",
157
157
  children: "Change"
158
158
  }), /* @__PURE__ */ w("p", {
159
- className: e("flex items-center gap-0.5 text-sm font-semibold", r ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"),
159
+ className: e("flex items-center gap-0.5 text-sm font-semibold", r ? "text-status-success-text" : "text-status-error-text"),
160
160
  children: [
161
161
  C(r ? p : f, { className: "h-3 w-3" }),
162
162
  n.appreciationPercent,
@@ -244,7 +244,7 @@ function O() {
244
244
  render: (n) => {
245
245
  let r = n.currentValue - n.purchasePrice, i = r >= 0;
246
246
  return /* @__PURE__ */ w("div", {
247
- className: e("flex items-center gap-1 text-sm font-medium", i ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"),
247
+ className: e("flex items-center gap-1 text-sm font-medium", i ? "text-status-success-text" : "text-status-error-text"),
248
248
  children: [
249
249
  C(i ? p : f, { className: "h-3 w-3" }),
250
250
  t(Math.abs(r)),
@@ -299,7 +299,7 @@ function O() {
299
299
  title: "No assets found",
300
300
  description: "Try adjusting your search criteria or add a new asset.",
301
301
  action: /* @__PURE__ */ w("button", {
302
- 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",
302
+ className: "inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover",
303
303
  children: [/* @__PURE__ */ C(b, { className: "h-4 w-4" }), "Add Asset"]
304
304
  })
305
305
  })
@@ -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 { EmphasisPanel, NextSteps } 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 <NextSteps\n storageKey=\"housingvista-assets\"\n steps={[\n {\n id: 'open-asset',\n label: 'Open an asset to see full performance',\n description: 'Drill into value history, yield, and ROI for any property.',\n onClick: () => {\n const first = assets[0];\n if (first) navigate(`/assets/${first.id}`);\n },\n },\n {\n id: 'compare-market',\n label: 'Compare against market trends',\n description: 'Check market analytics to benchmark your portfolio.',\n onClick: () => navigate('/market-analytics'),\n },\n {\n id: 'add-asset',\n label: 'Add a property you already own',\n description: 'Bring an existing holding into your portfolio view.',\n onClick: () => navigate('/properties/new'),\n },\n ]}\n />\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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"}
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, NextSteps } 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-status-success-text' : 'text-status-error-text'\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-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\">\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 <NextSteps\n storageKey=\"housingvista-assets\"\n steps={[\n {\n id: 'open-asset',\n label: 'Open an asset to see full performance',\n description: 'Drill into value history, yield, and ROI for any property.',\n onClick: () => {\n const first = assets[0];\n if (first) navigate(`/assets/${first.id}`);\n },\n },\n {\n id: 'compare-market',\n label: 'Compare against market trends',\n description: 'Check market analytics to benchmark your portfolio.',\n onClick: () => navigate('/market-analytics'),\n },\n {\n id: 'add-asset',\n label: 'Add a property you already own',\n description: 'Bring an existing holding into your portfolio view.',\n onClick: () => navigate('/properties/new'),\n },\n ]}\n />\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-status-success-text' : 'text-status-error-text'\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-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\">\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,220 +1,28 @@
1
- import { cn as e, getRelativeTime as t } from "../../utils/helpers.js";
2
- import { PageLayout as n } from "../../components/PageLayout.js";
3
- import { EmptyState as r } from "../../components/EmptyState.js";
1
+ import { PageLayout as e } from "../../components/PageLayout.js";
2
+ import { EmptyState as t } from "../../components/EmptyState.js";
4
3
  import "../../components/index.js";
5
- import { useConversations as i } from "../../hooks/index.js";
6
- import { useEffect as a, useMemo as o, useRef as s, useState as c } from "react";
7
- import { ArrowLeft as l, MessageCircle as u, Search as d, Send as f } from "lucide-react";
8
- import { useNavigate as p } from "react-router-dom";
9
- import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
10
- import { EmphasisPanel as _ } from "@burdenoff/fe-libs/ui";
4
+ import { MessageCircle as n } from "lucide-react";
5
+ import { jsx as r } from "react/jsx-runtime";
6
+ import { EmphasisPanel as i } from "@burdenoff/fe-libs/ui";
11
7
  //#region src/housingvista/pages/chat/ChatPage.tsx
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];
15
- return /* @__PURE__ */ g("button", {
16
- onClick: i,
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"),
18
- children: [/* @__PURE__ */ h("div", {
19
- className: "relative shrink-0",
20
- children: /* @__PURE__ */ h("div", {
21
- className: "flex h-10 w-10 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-primary",
22
- children: a?.avatar ? /* @__PURE__ */ h("img", {
23
- src: a.avatar,
24
- alt: a.name,
25
- className: "h-full w-full rounded-full object-cover"
26
- }) : a?.name.charAt(0) ?? "?"
27
- })
28
- }), /* @__PURE__ */ g("div", {
29
- className: "min-w-0 flex-1",
30
- children: [/* @__PURE__ */ g("div", {
31
- className: "flex items-center justify-between gap-2",
32
- children: [/* @__PURE__ */ h("span", {
33
- className: e("truncate text-sm font-medium", (n.unreadCount, "text-text-primary")),
34
- children: n.title
35
- }), /* @__PURE__ */ h("span", {
36
- className: "shrink-0 text-xs text-text-secondary",
37
- children: t(n.lastMessageTime)
38
- })]
39
- }), /* @__PURE__ */ g("div", {
40
- className: "flex items-center justify-between gap-2 mt-0.5",
41
- children: [/* @__PURE__ */ h("p", {
42
- className: e("truncate text-xs", n.unreadCount > 0 ? "font-medium text-text-primary" : "text-text-secondary"),
43
- children: n.lastMessage
44
- }), n.unreadCount > 0 && /* @__PURE__ */ h("span", {
45
- className: "flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-600 text-[10px] font-bold text-white",
46
- children: n.unreadCount
47
- })]
48
- })]
49
- })]
50
- });
51
- }
52
- function b({ message: t, isOwn: n }) {
53
- return /* @__PURE__ */ h("div", {
54
- className: e("flex", n ? "justify-end" : "justify-start"),
55
- children: /* @__PURE__ */ g("div", {
56
- className: e("max-w-[75%] space-y-1"),
57
- children: [
58
- !n && /* @__PURE__ */ h("span", {
59
- className: "text-xs font-medium text-text-secondary",
60
- children: t.senderName
61
- }),
62
- /* @__PURE__ */ h("div", {
63
- className: e("rounded-xl px-4 py-2 text-sm", n ? "bg-foreground text-background rounded-br-sm" : "bg-bg-sunken text-text-primary rounded-bl-sm"),
64
- children: t.content
65
- }),
66
- /* @__PURE__ */ h("span", {
67
- className: e("block text-[10px] text-text-secondary", n && "text-right"),
68
- children: new Date(t.timestamp).toLocaleTimeString("en-US", {
69
- hour: "numeric",
70
- minute: "2-digit"
71
- })
72
- })
73
- ]
74
- })
75
- });
76
- }
77
- function x() {
78
- p();
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();
82
- return t.filter((t) => t.title.toLowerCase().includes(e) || t.lastMessage.toLowerCase().includes(e) || t.participants.some((t) => t.name.toLowerCase().includes(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]);
88
- a(() => {
89
- k.current?.scrollIntoView({ behavior: "smooth" });
90
- }, [N.length]);
91
- let P = () => {
92
- if (!T.trim() || !x) return;
93
- let e = {
94
- id: `local-${Date.now()}`,
95
- senderId: v,
96
- senderName: "John Doe",
97
- content: T.trim(),
98
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
99
- type: "text"
100
- };
101
- O((t) => ({
102
- ...t,
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);
109
- };
110
- return /* @__PURE__ */ h(n, {
8
+ function a() {
9
+ return /* @__PURE__ */ r(e, {
111
10
  title: "Messages",
112
11
  description: "Connect with property owners, tenants, and professionals",
113
12
  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",
116
- children: [/* @__PURE__ */ g("div", {
117
- className: e("w-full shrink-0 border-r border-border-subtle md:w-80", I && "hidden md:block"),
118
- children: [/* @__PURE__ */ h("div", {
119
- className: "border-b border-border-subtle p-3",
120
- children: /* @__PURE__ */ g("div", {
121
- className: "relative",
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", {
123
- type: "text",
124
- value: C,
125
- onChange: (e) => w(e.target.value),
126
- placeholder: "Search conversations...",
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)]"
128
- })]
129
- })
130
- }), /* @__PURE__ */ h("div", {
131
- className: "overflow-y-auto",
132
- style: { height: "calc(100% - 58px)" },
133
- children: j.length > 0 ? /* @__PURE__ */ h("div", {
134
- className: "space-y-0.5 p-2",
135
- children: j.map((e) => /* @__PURE__ */ h(y, {
136
- conversation: e,
137
- isActive: e.id === x,
138
- onClick: () => R(e.id)
139
- }, e.id))
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"
145
- })
146
- })]
147
- }), /* @__PURE__ */ h("div", {
148
- className: e("flex flex-1 flex-col", !I && "hidden md:flex"),
149
- children: M ? /* @__PURE__ */ g(m, { children: [
150
- /* @__PURE__ */ g("div", {
151
- className: "flex items-center gap-3 border-b border-border-subtle px-4 py-3",
152
- children: [
153
- /* @__PURE__ */ h("button", {
154
- onClick: () => L(!1),
155
- className: "md:hidden rounded-md p-1 text-text-secondary hover:bg-bg-sunken",
156
- children: /* @__PURE__ */ h(l, { className: "h-5 w-5" })
157
- }),
158
- /* @__PURE__ */ h("div", {
159
- className: "flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-primary",
160
- children: M.participants.find((e) => e.id !== v)?.name.charAt(0) ?? "?"
161
- }),
162
- /* @__PURE__ */ g("div", {
163
- className: "min-w-0 flex-1",
164
- children: [/* @__PURE__ */ h("h3", {
165
- className: "truncate text-sm font-semibold text-text-primary",
166
- children: M.title
167
- }), /* @__PURE__ */ h("p", {
168
- className: "text-xs text-text-secondary",
169
- children: M.participants.map((e) => e.name).join(", ")
170
- })]
171
- })
172
- ]
173
- }),
174
- /* @__PURE__ */ h("div", {
175
- className: "flex-1 overflow-y-auto p-4",
176
- children: /* @__PURE__ */ g("div", {
177
- className: "space-y-4",
178
- children: [N.map((e) => /* @__PURE__ */ h(b, {
179
- message: e,
180
- isOwn: e.senderId === v
181
- }, e.id)), /* @__PURE__ */ h("div", { ref: k })]
182
- })
183
- }),
184
- /* @__PURE__ */ h("div", {
185
- className: "border-t border-border-subtle p-3",
186
- children: /* @__PURE__ */ g("div", {
187
- className: "flex items-center gap-2",
188
- children: [/* @__PURE__ */ h("input", {
189
- ref: A,
190
- type: "text",
191
- value: T,
192
- onChange: (e) => E(e.target.value),
193
- onKeyDown: F,
194
- placeholder: "Type a message...",
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)]"
196
- }), /* @__PURE__ */ h("button", {
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"),
200
- children: /* @__PURE__ */ h(f, { className: "h-4 w-4" })
201
- })]
202
- })
203
- })
204
- ] }) : /* @__PURE__ */ h("div", {
205
- className: "flex flex-1 items-center justify-center",
206
- children: /* @__PURE__ */ h(r, {
207
- illustration: "empty-generic",
208
- icon: /* @__PURE__ */ h(u, { className: "h-12 w-12" }),
209
- title: "Select a conversation",
210
- description: "Choose a conversation from the list to start chatting"
211
- })
212
- })
213
- })]
13
+ children: /* @__PURE__ */ r(i, {
14
+ className: "flex min-h-[400px] items-center justify-center p-6",
15
+ children: /* @__PURE__ */ r(t, {
16
+ illustration: "empty-generic",
17
+ icon: /* @__PURE__ */ r(n, { className: "h-12 w-12" }),
18
+ title: "Messaging is coming soon",
19
+ description: "Direct messages with owners, tenants, agents, and service providers are handled by the HousingVista platform and will appear here once enabled for your account.",
20
+ className: "border-0 bg-transparent"
21
+ })
214
22
  })
215
23
  });
216
24
  }
217
25
  //#endregion
218
- export { x as ChatPage };
26
+ export { a as ChatPage };
219
27
 
220
28
  //# 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 { 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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"}
1
+ {"version":3,"file":"ChatPage.js","names":[],"sources":["../../../../src/housingvista/pages/chat/ChatPage.tsx"],"sourcesContent":["import { MessageCircle } from 'lucide-react';\nimport { EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\n\nexport function ChatPage() {\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 {/* Messaging is delivered by the platform layer — no in-MFE mock inbox. */}\n <EmphasisPanel className=\"flex min-h-[400px] items-center justify-center p-6\">\n <EmptyState\n illustration=\"empty-generic\"\n icon={<MessageCircle className=\"h-12 w-12\" />}\n title=\"Messaging is coming soon\"\n description=\"Direct messages with owners, tenants, agents, and service providers are handled by the HousingVista platform and will appear here once enabled for your account.\"\n className=\"border-0 bg-transparent\"\n />\n </EmphasisPanel>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;AAIA,SAAgB,IAAW;AACzB,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;YAGR,kBAAC,GAAD;GAAe,WAAU;aACvB,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAe,WAAU,aAAc,CAAA;IAC7C,OAAM;IACN,aAAY;IACZ,WAAU;IACV,CAAA;GACY,CAAA;EACL,CAAA"}
@@ -17,7 +17,7 @@ function v(e, t, n) {
17
17
  function y({ rating: t }) {
18
18
  return /* @__PURE__ */ m("div", {
19
19
  className: "flex items-center gap-0.5",
20
- children: Array.from({ length: 5 }).map((n, r) => /* @__PURE__ */ m(f, { className: e("h-3 w-3", r < Math.floor(t) ? "fill-amber-400 text-amber-400" : "text-text-secondary/30") }, r))
20
+ children: Array.from({ length: 5 }).map((n, r) => /* @__PURE__ */ m(f, { className: e("h-3 w-3", r < Math.floor(t) ? "fill-status-warning text-status-warning" : "text-text-secondary/30") }, r))
21
21
  });
22
22
  }
23
23
  function b({ loan: n }) {
@@ -43,7 +43,7 @@ function b({ loan: n }) {
43
43
  })]
44
44
  })] })]
45
45
  }), n.specialOffer && /* @__PURE__ */ h("span", {
46
- className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
46
+ className: "inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text",
47
47
  children: [/* @__PURE__ */ m(d, { className: "h-3 w-3" }), "Offer"]
48
48
  })]
49
49
  }),
@@ -108,24 +108,24 @@ function b({ loan: n }) {
108
108
  className: "text-text-secondary",
109
109
  children: "Prepayment Charges"
110
110
  }), /* @__PURE__ */ m("span", {
111
- className: e("font-medium", n.prepaymentCharges === "None" ? "text-emerald-600 dark:text-emerald-400" : "text-text-primary"),
111
+ className: e("font-medium", n.prepaymentCharges === "None" ? "text-status-success-text" : "text-text-primary"),
112
112
  children: n.prepaymentCharges
113
113
  })]
114
114
  }),
115
115
  n.specialOffer && /* @__PURE__ */ m("div", {
116
- className: "mb-3 rounded-md bg-amber-50 p-2.5 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-300",
116
+ className: "mb-3 rounded-md bg-status-warning-bg-subtle p-2.5 text-xs text-status-warning-text",
117
117
  children: n.specialOffer
118
118
  }),
119
119
  /* @__PURE__ */ m("div", {
120
120
  className: "mb-4 space-y-1.5",
121
121
  children: n.features.map((e, t) => /* @__PURE__ */ h("div", {
122
122
  className: "flex items-center gap-1.5 text-xs text-text-secondary",
123
- children: [/* @__PURE__ */ m(u, { className: "h-3 w-3 shrink-0 text-emerald-500" }), e]
123
+ children: [/* @__PURE__ */ m(u, { className: "h-3 w-3 shrink-0 text-status-success" }), e]
124
124
  }, t))
125
125
  }),
126
126
  /* @__PURE__ */ h("button", {
127
127
  onClick: () => r(`/home-loans/${n.id}/apply`),
128
- className: "flex w-full items-center justify-center gap-1.5 rounded-md bg-foreground px-4 py-2.5 text-xs font-medium text-background transition-colors hover:bg-foreground/90",
128
+ className: "flex w-full items-center justify-center gap-1.5 rounded-md bg-action-primary-bg px-4 py-2.5 text-xs font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover",
129
129
  children: ["Apply Now", /* @__PURE__ */ m(s, { className: "h-3.5 w-3.5" })]
130
130
  })
131
131
  ]
@@ -259,7 +259,7 @@ function x() {
259
259
  className: "text-xs text-text-secondary",
260
260
  children: "Total Interest"
261
261
  }), /* @__PURE__ */ m("p", {
262
- className: "text-sm font-semibold text-red-600 dark:text-red-400",
262
+ className: "text-sm font-semibold text-status-error-text",
263
263
  children: t(T)
264
264
  })]
265
265
  })]
@@ -269,20 +269,20 @@ function x() {
269
269
  children: [/* @__PURE__ */ h("div", {
270
270
  className: "flex h-3 w-full overflow-hidden rounded-full",
271
271
  children: [/* @__PURE__ */ m("div", {
272
- className: "bg-foreground transition-all",
272
+ className: "bg-chart-1 transition-all",
273
273
  style: { width: `${u / w * 100}%` }
274
274
  }), /* @__PURE__ */ m("div", {
275
- className: "bg-red-400 dark:bg-red-600 transition-all",
275
+ className: "bg-chart-negative transition-all",
276
276
  style: { width: `${T / w * 100}%` }
277
277
  })]
278
278
  }), /* @__PURE__ */ h("div", {
279
279
  className: "mt-1.5 flex justify-between text-xs text-text-secondary",
280
280
  children: [/* @__PURE__ */ h("span", {
281
281
  className: "flex items-center gap-1",
282
- children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-foreground" }), " Principal"]
282
+ children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-chart-1" }), " Principal"]
283
283
  }), /* @__PURE__ */ h("span", {
284
284
  className: "flex items-center gap-1",
285
- children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-red-400 dark:bg-red-600" }), " Interest"]
285
+ children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-chart-negative" }), " Interest"]
286
286
  })]
287
287
  })]
288
288
  })
@@ -1 +1 @@
1
- {"version":3,"file":"HomeLoansPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoansPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Building2,\n Calculator,\n Star,\n CheckCircle,\n TrendingDown,\n Percent,\n DollarSign,\n Clock,\n ArrowRight,\n Gift,\n} from 'lucide-react';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useHomeLoans } from '../../hooks';\nimport { cn, formatCurrency, formatNumber } from '../../utils/helpers';\nimport type { HomeLoan } from '../../types';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-3 w-3',\n i < Math.floor(rating) ? 'fill-amber-400 text-amber-400' : 'text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nfunction LoanCard({ loan }: { loan: HomeLoan }) {\n const navigate = useNavigate();\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-sm transition-shadow hover:shadow-md\">\n {/* Header */}\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken\">\n <Building2 className=\"h-6 w-6 text-text-secondary\" />\n </div>\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">{loan.bankName}</h3>\n <div className=\"flex items-center gap-1.5 mt-0.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-xs text-text-secondary\">{loan.rating}</span>\n </div>\n </div>\n </div>\n {loan.specialOffer && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400\">\n <Gift className=\"h-3 w-3\" />\n Offer\n </span>\n )}\n </div>\n\n {/* Key Stats Grid */}\n <div className=\"mb-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Interest Rate</p>\n <p className=\"text-lg font-bold text-text-primary\">{loan.interestRate}%</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">EMI / Lakh</p>\n <p className=\"text-lg font-bold text-text-primary\">${loan.emiPerLakh}</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Tenure</p>\n <p className=\"text-sm font-semibold text-text-primary\">{loan.maxTenure} years</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Amount</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loan.maxAmount)}</p>\n </div>\n </div>\n\n {/* Processing Fee */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Processing Fee</span>\n <span className=\"font-medium text-text-primary\">{loan.processingFee}</span>\n </div>\n\n {/* Prepayment Charges */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Prepayment Charges</span>\n <span\n className={cn(\n 'font-medium',\n loan.prepaymentCharges === 'None' ? 'text-emerald-600 dark:text-emerald-400' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges}\n </span>\n </div>\n\n {/* Special Offer */}\n {loan.specialOffer && (\n <div className=\"mb-3 rounded-md bg-amber-50 p-2.5 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-300\">\n {loan.specialOffer}\n </div>\n )}\n\n {/* Features */}\n <div className=\"mb-4 space-y-1.5\">\n {loan.features.map((feature, i) => (\n <div key={i} className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CheckCircle className=\"h-3 w-3 shrink-0 text-emerald-500\" />\n {feature}\n </div>\n ))}\n </div>\n\n {/* Apply Button */}\n <button\n onClick={() => navigate(`/home-loans/${loan.id}/apply`)}\n className=\"flex w-full items-center justify-center gap-1.5 rounded-md bg-foreground px-4 py-2.5 text-xs font-medium text-background transition-colors hover:bg-foreground/90\"\n >\n Apply Now\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n );\n}\n\nexport function HomeLoansPage() {\n const navigate = useNavigate();\n const { data: loans } = useHomeLoans();\n\n // EMI Calculator state\n const [loanAmount, setLoanAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n const [interestRate, setInterestRate] = useState(6.75);\n\n const emi = useMemo(() => calculateEMI(loanAmount, interestRate, tenure), [loanAmount, interestRate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - loanAmount;\n\n return (\n <PageLayout\n title=\"Home Loans\"\n description=\"Compare rates, calculate EMI, and apply for home loans\"\n purpose=\"Work out what a home loan will really cost before you commit. Use the EMI calculator to model your monthly payment and total interest, then compare offers from partner banks and apply to the one that fits.\"\n >\n {/* EMI Calculator — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Calculator</h2>\n </div>\n\n <div className=\"grid gap-6 md:grid-cols-2\">\n {/* Inputs */}\n <div className=\"space-y-5\">\n {/* Loan Amount */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loanAmount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={5000000}\n step={10000}\n value={loanAmount}\n onChange={(e) => setLoanAmount(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>$50K</span>\n <span>$5M</span>\n </div>\n </div>\n\n {/* Tenure */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={30}\n step={1}\n value={tenure}\n onChange={(e) => setTenure(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>1 yr</span>\n <span>30 yrs</span>\n </div>\n </div>\n\n {/* Interest Rate */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Interest Rate (%)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{interestRate}%</span>\n </div>\n <input\n type=\"range\"\n min={3}\n max={15}\n step={0.05}\n value={interestRate}\n onChange={(e) => setInterestRate(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>3%</span>\n <span>15%</span>\n </div>\n </div>\n </div>\n\n {/* Results */}\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-red-600 dark:text-red-400\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n {/* Visual breakdown bar */}\n <div className=\"mt-4 w-full\">\n <div className=\"flex h-3 w-full overflow-hidden rounded-full\">\n <div\n className=\"bg-foreground transition-all\"\n style={{ width: `${(loanAmount / totalPayment) * 100}%` }}\n />\n <div\n className=\"bg-red-400 dark:bg-red-600 transition-all\"\n style={{ width: `${(totalInterest / totalPayment) * 100}%` }}\n />\n </div>\n <div className=\"mt-1.5 flex justify-between text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-foreground\" /> Principal\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-red-400 dark:bg-red-600\" /> Interest\n </span>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <NextSteps\n storageKey=\"housingvista-home-loans\"\n steps={[\n {\n id: 'apply-best',\n label: 'Apply to the best-rated offer',\n description: 'Start an application with a top partner bank.',\n onClick: () => {\n const best = [...loans].sort((a, b) => b.rating - a.rating)[0];\n if (best) navigate(`/home-loans/${best.id}/apply`);\n },\n },\n {\n id: 'browse-properties',\n label: 'Find a property to finance',\n description: 'Browse listings that fit the amount you just modeled.',\n onClick: () => navigate('/properties'),\n },\n {\n id: 'track-transactions',\n label: 'Track your purchase',\n description: 'See ongoing deals where this loan would apply.',\n onClick: () => navigate('/transactions'),\n },\n ]}\n />\n\n {/* Loan Offers */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Available Loan Offers</h2>\n {loans.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {loans.map((loan) => (\n <LoanCard key={loan.id} loan={loan} />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"No loan offers available\"\n description=\"Check back later for new loan offers from our partner banks.\"\n />\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HomeLoansPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoansPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { Building2, Calculator, Star, CheckCircle, ArrowRight, Gift } from 'lucide-react';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useHomeLoans } from '../../hooks';\nimport { cn, formatCurrency } from '../../utils/helpers';\nimport type { HomeLoan } from '../../types';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-3 w-3',\n i < Math.floor(rating) ? 'fill-status-warning text-status-warning' : 'text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nfunction LoanCard({ loan }: { loan: HomeLoan }) {\n const navigate = useNavigate();\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-sm transition-shadow hover:shadow-md\">\n {/* Header */}\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken\">\n <Building2 className=\"h-6 w-6 text-text-secondary\" />\n </div>\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">{loan.bankName}</h3>\n <div className=\"flex items-center gap-1.5 mt-0.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-xs text-text-secondary\">{loan.rating}</span>\n </div>\n </div>\n </div>\n {loan.specialOffer && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text\">\n <Gift className=\"h-3 w-3\" />\n Offer\n </span>\n )}\n </div>\n\n {/* Key Stats Grid */}\n <div className=\"mb-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Interest Rate</p>\n <p className=\"text-lg font-bold text-text-primary\">{loan.interestRate}%</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">EMI / Lakh</p>\n <p className=\"text-lg font-bold text-text-primary\">${loan.emiPerLakh}</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Tenure</p>\n <p className=\"text-sm font-semibold text-text-primary\">{loan.maxTenure} years</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Amount</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loan.maxAmount)}</p>\n </div>\n </div>\n\n {/* Processing Fee */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Processing Fee</span>\n <span className=\"font-medium text-text-primary\">{loan.processingFee}</span>\n </div>\n\n {/* Prepayment Charges */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Prepayment Charges</span>\n <span\n className={cn(\n 'font-medium',\n loan.prepaymentCharges === 'None' ? 'text-status-success-text' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges}\n </span>\n </div>\n\n {/* Special Offer */}\n {loan.specialOffer && (\n <div className=\"mb-3 rounded-md bg-status-warning-bg-subtle p-2.5 text-xs text-status-warning-text\">\n {loan.specialOffer}\n </div>\n )}\n\n {/* Features */}\n <div className=\"mb-4 space-y-1.5\">\n {loan.features.map((feature, i) => (\n <div key={i} className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CheckCircle className=\"h-3 w-3 shrink-0 text-status-success\" />\n {feature}\n </div>\n ))}\n </div>\n\n {/* Apply Button */}\n <button\n onClick={() => navigate(`/home-loans/${loan.id}/apply`)}\n className=\"flex w-full items-center justify-center gap-1.5 rounded-md bg-action-primary-bg px-4 py-2.5 text-xs font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n Apply Now\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n );\n}\n\nexport function HomeLoansPage() {\n const navigate = useNavigate();\n const { data: loans } = useHomeLoans();\n\n // EMI Calculator state\n const [loanAmount, setLoanAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n const [interestRate, setInterestRate] = useState(6.75);\n\n const emi = useMemo(() => calculateEMI(loanAmount, interestRate, tenure), [loanAmount, interestRate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - loanAmount;\n\n return (\n <PageLayout\n title=\"Home Loans\"\n description=\"Compare rates, calculate EMI, and apply for home loans\"\n purpose=\"Work out what a home loan will really cost before you commit. Use the EMI calculator to model your monthly payment and total interest, then compare offers from partner banks and apply to the one that fits.\"\n >\n {/* EMI Calculator — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Calculator</h2>\n </div>\n\n <div className=\"grid gap-6 md:grid-cols-2\">\n {/* Inputs */}\n <div className=\"space-y-5\">\n {/* Loan Amount */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loanAmount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={5000000}\n step={10000}\n value={loanAmount}\n onChange={(e) => setLoanAmount(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>$50K</span>\n <span>$5M</span>\n </div>\n </div>\n\n {/* Tenure */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={30}\n step={1}\n value={tenure}\n onChange={(e) => setTenure(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>1 yr</span>\n <span>30 yrs</span>\n </div>\n </div>\n\n {/* Interest Rate */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Interest Rate (%)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{interestRate}%</span>\n </div>\n <input\n type=\"range\"\n min={3}\n max={15}\n step={0.05}\n value={interestRate}\n onChange={(e) => setInterestRate(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>3%</span>\n <span>15%</span>\n </div>\n </div>\n </div>\n\n {/* Results */}\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-status-error-text\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n {/* Visual breakdown bar */}\n <div className=\"mt-4 w-full\">\n <div className=\"flex h-3 w-full overflow-hidden rounded-full\">\n <div className=\"bg-chart-1 transition-all\" style={{ width: `${(loanAmount / totalPayment) * 100}%` }} />\n <div\n className=\"bg-chart-negative transition-all\"\n style={{ width: `${(totalInterest / totalPayment) * 100}%` }}\n />\n </div>\n <div className=\"mt-1.5 flex justify-between text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-chart-1\" /> Principal\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-chart-negative\" /> Interest\n </span>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <NextSteps\n storageKey=\"housingvista-home-loans\"\n steps={[\n {\n id: 'apply-best',\n label: 'Apply to the best-rated offer',\n description: 'Start an application with a top partner bank.',\n onClick: () => {\n const best = [...loans].sort((a, b) => b.rating - a.rating)[0];\n if (best) navigate(`/home-loans/${best.id}/apply`);\n },\n },\n {\n id: 'browse-properties',\n label: 'Find a property to finance',\n description: 'Browse listings that fit the amount you just modeled.',\n onClick: () => navigate('/properties'),\n },\n {\n id: 'track-transactions',\n label: 'Track your purchase',\n description: 'See ongoing deals where this loan would apply.',\n onClick: () => navigate('/transactions'),\n },\n ]}\n />\n\n {/* Loan Offers */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Available Loan Offers</h2>\n {loans.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {loans.map((loan) => (\n <LoanCard key={loan.id} loan={loan} />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"No loan offers available\"\n description=\"Check back later for new loan offers from our partner banks.\"\n />\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}