@burdenoff/microfe-housingvista 2026.716.1 → 2026.720.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/housingvista/components/PropertyCard.d.ts +7 -0
- package/dist/housingvista/components/PropertyCard.d.ts.map +1 -1
- package/dist/housingvista/components/PropertyCard.js +83 -58
- package/dist/housingvista/components/PropertyCard.js.map +1 -1
- package/dist/housingvista/components/Sparkline.d.ts +15 -0
- package/dist/housingvista/components/Sparkline.d.ts.map +1 -0
- package/dist/housingvista/components/Sparkline.js +61 -0
- package/dist/housingvista/components/Sparkline.js.map +1 -0
- package/dist/housingvista/components/StatCard.d.ts +3 -0
- package/dist/housingvista/components/StatCard.d.ts.map +1 -1
- package/dist/housingvista/components/StatCard.js +33 -29
- package/dist/housingvista/components/StatCard.js.map +1 -1
- package/dist/housingvista/components/index.d.ts +2 -0
- package/dist/housingvista/components/index.d.ts.map +1 -1
- package/dist/housingvista/components/index.js +1 -0
- package/dist/housingvista/components/maps/PropertyMapView.d.ts +22 -0
- package/dist/housingvista/components/maps/PropertyMapView.d.ts.map +1 -0
- package/dist/housingvista/components/maps/PropertyMapView.js +175 -0
- package/dist/housingvista/components/maps/PropertyMapView.js.map +1 -0
- package/dist/housingvista/components/maps/index.d.ts +5 -0
- package/dist/housingvista/components/maps/index.d.ts.map +1 -0
- package/dist/housingvista/components/maps/index.js +2 -0
- package/dist/housingvista/components/maps/leaflet-setup.d.ts +27 -0
- package/dist/housingvista/components/maps/leaflet-setup.d.ts.map +1 -0
- package/dist/housingvista/components/maps/leaflet-setup.js +36 -0
- package/dist/housingvista/components/maps/leaflet-setup.js.map +1 -0
- package/dist/housingvista/hooks/index.d.ts +33 -2
- package/dist/housingvista/hooks/index.d.ts.map +1 -1
- package/dist/housingvista/hooks/index.js +177 -94
- package/dist/housingvista/hooks/index.js.map +1 -1
- package/dist/housingvista/pages/HomePage.d.ts.map +1 -1
- package/dist/housingvista/pages/HomePage.js +7 -3
- package/dist/housingvista/pages/HomePage.js.map +1 -1
- package/dist/housingvista/pages/analytics/AnalyticsPage.d.ts.map +1 -1
- package/dist/housingvista/pages/analytics/AnalyticsPage.js +146 -145
- package/dist/housingvista/pages/analytics/AnalyticsPage.js.map +1 -1
- package/dist/housingvista/pages/market/AreaInsightsPage.d.ts.map +1 -1
- package/dist/housingvista/pages/market/AreaInsightsPage.js +60 -7
- package/dist/housingvista/pages/market/AreaInsightsPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.d.ts.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.js +285 -191
- package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.d.ts.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.js +792 -599
- package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
- package/dist/housingvista/pages/shortlist/ShortlistPage.d.ts.map +1 -1
- package/dist/housingvista/pages/shortlist/ShortlistPage.js +172 -75
- package/dist/housingvista/pages/shortlist/ShortlistPage.js.map +1 -1
- package/dist/housingvista/types/index.d.ts +44 -1
- package/dist/housingvista/types/index.d.ts.map +1 -1
- package/dist/housingvista/utils/pageContext.d.ts.map +1 -1
- package/dist/housingvista/utils/pageContext.js +20 -17
- package/dist/housingvista/utils/pageContext.js.map +1 -1
- package/dist/microfe-housingvista.css +2 -0
- package/dist/node_modules/@react-leaflet/core/lib/attribution.js +12 -0
- package/dist/node_modules/@react-leaflet/core/lib/attribution.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/component.js +39 -0
- package/dist/node_modules/@react-leaflet/core/lib/component.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/context.js +23 -0
- package/dist/node_modules/@react-leaflet/core/lib/context.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/div-overlay.js +15 -0
- package/dist/node_modules/@react-leaflet/core/lib/div-overlay.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/element.js +30 -0
- package/dist/node_modules/@react-leaflet/core/lib/element.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/events.js +14 -0
- package/dist/node_modules/@react-leaflet/core/lib/events.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/generic.js +18 -0
- package/dist/node_modules/@react-leaflet/core/lib/generic.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/grid-layer.js +9 -0
- package/dist/node_modules/@react-leaflet/core/lib/grid-layer.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/layer.js +23 -0
- package/dist/node_modules/@react-leaflet/core/lib/layer.js.map +1 -0
- package/dist/node_modules/@react-leaflet/core/lib/pane.js +12 -0
- package/dist/node_modules/@react-leaflet/core/lib/pane.js.map +1 -0
- package/dist/node_modules/leaflet/dist/leaflet-src.js +4585 -0
- package/dist/node_modules/leaflet/dist/leaflet-src.js.map +1 -0
- package/dist/node_modules/react-leaflet/lib/MapContainer.js +33 -0
- package/dist/node_modules/react-leaflet/lib/MapContainer.js.map +1 -0
- package/dist/node_modules/react-leaflet/lib/Marker.js +16 -0
- package/dist/node_modules/react-leaflet/lib/Marker.js.map +1 -0
- package/dist/node_modules/react-leaflet/lib/Popup.js +37 -0
- package/dist/node_modules/react-leaflet/lib/Popup.js.map +1 -0
- package/dist/node_modules/react-leaflet/lib/TileLayer.js +18 -0
- package/dist/node_modules/react-leaflet/lib/TileLayer.js.map +1 -0
- package/dist/node_modules/react-leaflet/lib/hooks.js +18 -0
- package/dist/node_modules/react-leaflet/lib/hooks.js.map +1 -0
- package/package.json +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/housingvista/pages/analytics/AnalyticsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { TrendingUp, DollarSign, Building, Activity, Download, Clock, BarChart3 } from 'lucide-react';\nimport {\n AreaChart,\n Area,\n LineChart,\n Line,\n PieChart,\n Pie,\n Cell,\n XAxis,\n YAxis,\n CartesianGrid,\n Tooltip,\n ResponsiveContainer,\n} from 'recharts';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { ChartCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { EmptyState } from '../../components';\nimport { LoadingSkeleton } from '../../components';\nimport { useAnalyticsData } from '../../hooks';\nimport { useHousingVistaAnalytics, useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\nimport { notify } from '../../lib/toast';\nimport { cn, formatCurrency, formatNumber } from '../../utils/helpers';\n\n// Distribution slice colours drawn from the semantic chart palette (full oklch tokens).\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\nconst REVENUE_COLOR = 'var(--color-chart-2)';\nconst EXPENSE_COLOR = 'var(--color-chart-1)';\n\n// Shared, theme-aware tooltip styling for every recharts surface on this page.\nconst tooltipContentStyle: React.CSSProperties = {\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-subtle)',\n borderRadius: 10,\n fontSize: 12,\n boxShadow: '0 8px 24px rgb(0 0 0 / 0.12)',\n};\nconst tooltipLabelStyle: React.CSSProperties = { color: 'var(--color-text-primary)', fontWeight: 600 };\nconst axisTick = { fontSize: 11, fill: 'var(--color-text-secondary)' } as const;\n\n// The backend series is monthly; this range control slices to the most recent N\n// months so the same real data can be viewed at different horizons (0 = all).\nconst rangeOptions: { value: number; label: string }[] = [\n { value: 3, label: '3M' },\n { value: 6, label: '6M' },\n { value: 12, label: '12M' },\n { value: 0, label: 'All' },\n];\n\nexport function AnalyticsPage() {\n useTrackPageView('housingvista.analytics.viewed');\n const { emitExport } = useHousingVistaAnalytics();\n const { data: analyticsData, loading } = useAnalyticsData();\n const [range, setRange] = useState<number>(12);\n\n // Client-side CSV export of the loaded analytics data (KPIs + monthly revenue).\n const handleExportCsv = () => {\n if (!analyticsData) return;\n\n const escapeCsv = (val: string | number) => {\n const s = String(val);\n return /[\",\\n]/.test(s) ? `\"${s.replace(/\"/g, '\"\"')}\"` : s;\n };\n\n const lines: string[] = [];\n lines.push('KPI,Value,Unit,Change %');\n for (const kpi of analyticsData.kpis) {\n lines.push([kpi.label, kpi.value, kpi.unit, kpi.changePercent].map(escapeCsv).join(','));\n }\n lines.push('');\n lines.push('Month,Revenue,Expenses,Net Income');\n for (const row of analyticsData.revenueByMonth) {\n lines.push([row.month, row.revenue, row.expenses, row.revenue - row.expenses].map(escapeCsv).join(','));\n }\n\n const blob = new Blob([lines.join('\\n')], { type: 'text/csv;charset=utf-8;' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `housingvista-analytics-${new Date().toISOString().slice(0, 10)}.csv`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n\n emitExport('analytics');\n notify.success('Report exported', 'Your analytics data was downloaded as a CSV file.');\n };\n\n const revenueColumns: Column<{ month: string; revenue: number; expenses: number }>[] = [\n {\n key: 'month',\n header: 'Month',\n render: (item) => <span className=\"font-medium text-text-primary\">{item.month}</span>,\n },\n {\n key: 'revenue',\n header: 'Revenue',\n render: (item) => <span className=\"font-medium text-status-success-text\">{formatCurrency(item.revenue)}</span>,\n },\n {\n key: 'expenses',\n header: 'Expenses',\n render: (item) => <span className=\"font-medium text-status-error-text\">{formatCurrency(item.expenses)}</span>,\n },\n {\n key: 'net',\n header: 'Net Income',\n sortable: false,\n render: (item) => {\n const net = item.revenue - item.expenses;\n return (\n <span className={cn('font-medium', net >= 0 ? 'text-status-success-text' : 'text-status-error-text')}>\n {formatCurrency(net)}\n </span>\n );\n },\n },\n {\n key: 'margin',\n header: 'Margin',\n sortable: false,\n render: (item) => {\n const margin = item.revenue > 0 ? ((item.revenue - item.expenses) / item.revenue) * 100 : 0;\n return (\n <span className={cn('font-medium', margin >= 0 ? 'text-status-success-text' : 'text-status-error-text')}>\n {margin.toFixed(1)}%\n </span>\n );\n },\n },\n ];\n\n // ---- Loading ----\n if (loading) {\n return (\n <PageLayout title=\"Analytics\" description=\"Track your property portfolio performance and market insights\">\n <LoadingSkeleton className=\"h-28\" />\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <LoadingSkeleton className=\"h-64\" />\n <LoadingSkeleton className=\"h-64\" />\n </div>\n </PageLayout>\n );\n }\n\n // ---- Empty / not-yet-computed ----\n const hasData =\n analyticsData &&\n (analyticsData.kpis.length > 0 ||\n analyticsData.revenueByMonth.length > 0 ||\n analyticsData.propertyTypeDistribution.length > 0);\n\n if (!hasData) {\n return (\n <PageLayout title=\"Analytics\" description=\"Track your property portfolio performance and market insights\">\n <EmptyState\n illustration=\"empty-generic\"\n icon={<BarChart3 className=\"h-12 w-12\" />}\n title=\"No analytics yet\"\n description=\"Once you have properties, transactions, and rentals recorded, revenue, occupancy, and portfolio insights will appear here.\"\n />\n </PageLayout>\n );\n }\n\n const kpis = analyticsData.kpis;\n const propertyTypeDistribution = analyticsData.propertyTypeDistribution;\n\n // Range control slices the real monthly series to its most recent N entries.\n const sliceRange = <T,>(rows: T[]): T[] => (range > 0 ? rows.slice(-range) : rows);\n const revenueByMonth = sliceRange(analyticsData.revenueByMonth);\n const occupancyRate = sliceRange(analyticsData.occupancyRate);\n\n const revenueChartData = revenueByMonth.map((r) => ({\n month: r.month,\n revenue: r.revenue,\n expenses: r.expenses,\n net: r.revenue - r.expenses,\n }));\n const distributionTotal = propertyTypeDistribution.reduce((s, d) => s + d.value, 0);\n const distributionData = propertyTypeDistribution.map((d, idx) => ({\n name: d.type.charAt(0).toUpperCase() + d.type.slice(1),\n value: d.value,\n count: d.count,\n color: CHART_COLORS[idx % CHART_COLORS.length],\n }));\n\n const totalRevenue = revenueByMonth.reduce((s, r) => s + r.revenue, 0);\n const totalExpenses = revenueByMonth.reduce((s, r) => s + r.expenses, 0);\n const netIncome = totalRevenue - totalExpenses;\n const profitMargin = totalRevenue > 0 ? (netIncome / totalRevenue) * 100 : 0;\n\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Track your property portfolio performance and market insights\"\n purpose=\"Understand how your portfolio is actually performing — revenue versus expenses, occupancy, property-type mix, and profitability — so you can spot trends, compare periods, and decide where to buy, sell, or raise rents.\"\n actions={\n <button\n onClick={handleExportCsv}\n 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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Download className=\"h-4 w-4\" />\n Export Report\n </button>\n }\n >\n <NextSteps\n storageKey=\"housingvista-analytics\"\n steps={[\n {\n id: 'performance',\n label: 'Drill into monthly performance',\n description: 'Review revenue, expenses, net income, and margin month by month.',\n href: '#analytics-performance',\n },\n ]}\n />\n\n {/* Period range control — slices the real monthly series to a horizon */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"text-sm font-medium text-text-secondary\">Period</span>\n <div\n role=\"group\"\n aria-label=\"Select analytics period\"\n className=\"inline-flex rounded-lg border border-border-subtle bg-bg-sunken/60 p-1\"\n >\n {rangeOptions.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n aria-pressed={range === option.value}\n onClick={() => setRange(option.value)}\n className={cn(\n 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n range === option.value\n ? 'bg-action-primary-bg text-action-primary-text shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n )}\n >\n {option.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* Stats Overview Row — primary insight zone (one EmphasisPanel per page) */}\n {kpis.length > 0 && (\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {kpis.map((kpi) => {\n const icons: Record<string, typeof DollarSign> = {\n 'Portfolio Value': DollarSign,\n 'Monthly Revenue': TrendingUp,\n 'Occupancy Rate': Building,\n 'Avg. Days to Rent': Clock,\n };\n const IconComponent = icons[kpi.label] || Activity;\n\n return (\n <StatCard\n key={kpi.id}\n label={kpi.label}\n value={\n kpi.unit === 'USD'\n ? kpi.value\n : `${formatNumber(kpi.value)}${kpi.unit === '%' ? '%' : ` ${kpi.unit}`}`\n }\n prefix={kpi.unit === 'USD' ? '$' : undefined}\n icon={<IconComponent className=\"h-4 w-4\" />}\n trend={kpi.trend === 'up' ? 'up' : kpi.trend === 'down' ? 'down' : 'stable'}\n change={kpi.changePercent}\n />\n );\n })}\n </div>\n </EmphasisPanel>\n )}\n\n {/* Charts Section — revenue area chart (wide) + property-type donut (aside) */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {revenueChartData.length > 0 && (\n <ChartCard\n title=\"Revenue vs Expenses\"\n description=\"Real income against costs across the selected period\"\n className=\"lg:col-span-2\"\n actions={\n <div className=\"flex items-center gap-4 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full\" style={{ background: REVENUE_COLOR }} />\n Revenue\n </span>\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full\" style={{ background: EXPENSE_COLOR }} />\n Expenses\n </span>\n </div>\n }\n >\n <div className=\"h-72 w-full\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <AreaChart data={revenueChartData} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>\n <defs>\n <linearGradient id=\"revGrad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"5%\" stopColor={REVENUE_COLOR} stopOpacity={0.35} />\n <stop offset=\"95%\" stopColor={REVENUE_COLOR} stopOpacity={0.02} />\n </linearGradient>\n <linearGradient id=\"expGrad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"5%\" stopColor={EXPENSE_COLOR} stopOpacity={0.3} />\n <stop offset=\"95%\" stopColor={EXPENSE_COLOR} stopOpacity={0.02} />\n </linearGradient>\n </defs>\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--color-border-subtle)\" vertical={false} />\n <XAxis dataKey=\"month\" tick={axisTick} tickLine={false} axisLine={false} />\n <YAxis\n tick={axisTick}\n tickLine={false}\n axisLine={false}\n width={52}\n tickFormatter={(v: number) => formatCurrency(v)}\n />\n <Tooltip\n contentStyle={tooltipContentStyle}\n labelStyle={tooltipLabelStyle}\n formatter={(value: number, name) => [formatCurrency(value), name]}\n />\n <Area\n type=\"monotone\"\n dataKey=\"revenue\"\n name=\"Revenue\"\n stroke={REVENUE_COLOR}\n strokeWidth={2.5}\n fill=\"url(#revGrad)\"\n />\n <Area\n type=\"monotone\"\n dataKey=\"expenses\"\n name=\"Expenses\"\n stroke={EXPENSE_COLOR}\n strokeWidth={2.5}\n fill=\"url(#expGrad)\"\n />\n </AreaChart>\n </ResponsiveContainer>\n </div>\n </ChartCard>\n )}\n\n {distributionData.length > 0 && (\n <ChartCard title=\"Property-Type Distribution\" description=\"Portfolio value split by type\">\n <div className=\"relative h-56 w-full\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <PieChart>\n <Pie\n data={distributionData}\n dataKey=\"value\"\n nameKey=\"name\"\n innerRadius={58}\n outerRadius={84}\n paddingAngle={2}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={2}\n >\n {distributionData.map((entry) => (\n <Cell key={entry.name} fill={entry.color} />\n ))}\n </Pie>\n <Tooltip\n contentStyle={tooltipContentStyle}\n labelStyle={tooltipLabelStyle}\n formatter={(value: number, _n, item) => [\n `${formatCurrency(value)} · ${item?.payload?.count ?? 0} properties`,\n item?.payload?.name,\n ]}\n />\n </PieChart>\n </ResponsiveContainer>\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center\">\n <span className=\"text-xs text-text-secondary\">Total value</span>\n <span className=\"text-lg font-bold text-text-primary\">{formatCurrency(distributionTotal)}</span>\n </div>\n </div>\n <ul className=\"mt-3 space-y-2\">\n {distributionData.map((entry) => (\n <li key={entry.name} className=\"flex items-center justify-between text-sm\">\n <span className=\"flex items-center gap-2\">\n <span className=\"h-2.5 w-2.5 rounded-full\" style={{ background: entry.color }} />\n <span className=\"text-text-primary\">{entry.name}</span>\n </span>\n <span className=\"text-text-secondary\">\n {distributionTotal > 0 ? Math.round((entry.value / distributionTotal) * 100) : 0}%\n </span>\n </li>\n ))}\n </ul>\n </ChartCard>\n )}\n </div>\n\n {/* Occupancy trend (wide) + quick stats (aside) */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {occupancyRate.length > 0 && (\n <ChartCard\n title=\"Occupancy Trend\"\n description=\"Share of units occupied over the selected period\"\n className=\"lg:col-span-2\"\n >\n <div className=\"h-56 w-full\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <LineChart data={occupancyRate} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--color-border-subtle)\" vertical={false} />\n <XAxis dataKey=\"month\" tick={axisTick} tickLine={false} axisLine={false} />\n <YAxis\n tick={axisTick}\n tickLine={false}\n axisLine={false}\n width={40}\n domain={[0, 100]}\n tickFormatter={(v: number) => `${v}%`}\n />\n <Tooltip\n contentStyle={tooltipContentStyle}\n labelStyle={tooltipLabelStyle}\n formatter={(value: number) => [`${value}%`, 'Occupancy']}\n />\n <Line\n type=\"monotone\"\n dataKey=\"rate\"\n stroke=\"var(--color-chart-2)\"\n strokeWidth={2.5}\n dot={{ r: 3, fill: 'var(--color-chart-2)', strokeWidth: 0 }}\n activeDot={{ r: 5 }}\n />\n </LineChart>\n </ResponsiveContainer>\n </div>\n </ChartCard>\n )}\n\n {revenueByMonth.length > 0 && (\n <ChartCard title=\"Period Summary\" description=\"Totals across the selected range\">\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between rounded-lg bg-bg-sunken/50 p-3\">\n <div>\n <p className=\"text-sm text-text-secondary\">Total Revenue</p>\n <p className=\"text-lg font-bold text-text-primary\">{formatCurrency(totalRevenue)}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm text-text-secondary\">Total Expenses</p>\n <p className=\"text-lg font-bold text-text-primary\">{formatCurrency(totalExpenses)}</p>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg bg-status-success-bg-subtle p-3 text-center\">\n <p className=\"text-xl font-bold text-status-success-text\">{formatCurrency(netIncome)}</p>\n <p className=\"text-xs text-text-secondary\">Net Income</p>\n </div>\n <div className=\"rounded-lg bg-status-info-bg-subtle p-3 text-center\">\n <p className=\"text-xl font-bold text-status-info-text\">{profitMargin.toFixed(1)}%</p>\n <p className=\"text-xs text-text-secondary\">Profit Margin</p>\n </div>\n </div>\n </div>\n </ChartCard>\n )}\n </div>\n\n {/* Performance Metrics Table */}\n {revenueByMonth.length > 0 && (\n <div id=\"analytics-performance\" className=\"scroll-mt-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Monthly Performance</h2>\n <DataTable columns={revenueColumns} data={revenueByMonth} keyField=\"month\" pageSize={12} />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAM,IAAe;CACnB;CACA;CACA;CACA;CACA;CACD,EACK,IAAgB,wBAChB,IAAgB,wBAGhB,IAA2C;CAC/C,YAAY;CACZ,QAAQ;CACR,cAAc;CACd,UAAU;CACV,WAAW;CACZ,EACK,IAAyC;CAAE,OAAO;CAA6B,YAAY;CAAK,EAChG,IAAW;CAAE,UAAU;CAAI,MAAM;CAA+B,EAIhE,IAAmD;CACvD;EAAE,OAAO;EAAG,OAAO;EAAM;CACzB;EAAE,OAAO;EAAG,OAAO;EAAM;CACzB;EAAE,OAAO;EAAI,OAAO;EAAO;CAC3B;EAAE,OAAO;EAAG,OAAO;EAAO;CAC3B;AAED,SAAgB,IAAgB;AAC9B,GAAiB,gCAAgC;CACjD,IAAM,EAAE,kBAAe,GAA0B,EAC3C,EAAE,MAAM,GAAe,eAAY,GAAkB,EACrD,CAAC,GAAO,KAAY,EAAiB,GAAG,EAGxC,UAAwB;AAC5B,MAAI,CAAC,EAAe;EAEpB,IAAM,KAAa,MAAyB;GAC1C,IAAM,IAAI,OAAO,EAAI;AACrB,UAAO,SAAS,KAAK,EAAE,GAAG,IAAI,EAAE,QAAQ,MAAM,OAAK,CAAC,KAAK;KAGrD,IAAkB,EAAE;AAC1B,IAAM,KAAK,0BAA0B;AACrC,OAAK,IAAM,KAAO,EAAc,KAC9B,GAAM,KAAK;GAAC,EAAI;GAAO,EAAI;GAAO,EAAI;GAAM,EAAI;GAAc,CAAC,IAAI,EAAU,CAAC,KAAK,IAAI,CAAC;AAG1F,EADA,EAAM,KAAK,GAAG,EACd,EAAM,KAAK,oCAAoC;AAC/C,OAAK,IAAM,KAAO,EAAc,eAC9B,GAAM,KAAK;GAAC,EAAI;GAAO,EAAI;GAAS,EAAI;GAAU,EAAI,UAAU,EAAI;GAAS,CAAC,IAAI,EAAU,CAAC,KAAK,IAAI,CAAC;EAGzG,IAAM,IAAO,IAAI,KAAK,CAAC,EAAM,KAAK,KAAK,CAAC,EAAE,EAAE,MAAM,2BAA2B,CAAC,EACxE,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAO,SAAS,cAAc,IAAI;AASxC,EARA,EAAK,OAAO,GACZ,EAAK,WAAW,2CAA0B,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,OAChF,SAAS,KAAK,YAAY,EAAK,EAC/B,EAAK,OAAO,EACZ,SAAS,KAAK,YAAY,EAAK,EAC/B,IAAI,gBAAgB,EAAI,EAExB,EAAW,YAAY,EACvB,EAAO,QAAQ,mBAAmB,oDAAoD;IAGlF,IAAiF;EACrF;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MAAS,kBAAC,QAAD;IAAM,WAAU;cAAiC,EAAK;IAAa,CAAA;GACtF;EACD;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MAAS,kBAAC,QAAD;IAAM,WAAU;cAAwC,EAAe,EAAK,QAAQ;IAAQ,CAAA;GAC/G;EACD;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MAAS,kBAAC,QAAD;IAAM,WAAU;cAAsC,EAAe,EAAK,SAAS;IAAQ,CAAA;GAC9G;EACD;GACE,KAAK;GACL,QAAQ;GACR,UAAU;GACV,SAAS,MAAS;IAChB,IAAM,IAAM,EAAK,UAAU,EAAK;AAChC,WACE,kBAAC,QAAD;KAAM,WAAW,EAAG,eAAe,KAAO,IAAI,6BAA6B,yBAAyB;eACjG,EAAe,EAAI;KACf,CAAA;;GAGZ;EACD;GACE,KAAK;GACL,QAAQ;GACR,UAAU;GACV,SAAS,MAAS;IAChB,IAAM,IAAS,EAAK,UAAU,KAAM,EAAK,UAAU,EAAK,YAAY,EAAK,UAAW,MAAM;AAC1F,WACE,kBAAC,QAAD;KAAM,WAAW,EAAG,eAAe,KAAU,IAAI,6BAA6B,yBAAyB;eAAvG,CACG,EAAO,QAAQ,EAAE,EAAC,IACd;;;GAGZ;EACF;AAGD,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;EAAY,aAAY;YAA1C,CACE,kBAAC,GAAD,EAAiB,WAAU,QAAS,CAAA,EACpC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAiB,WAAU,QAAS,CAAA,EACpC,kBAAC,GAAD,EAAiB,WAAU,QAAS,CAAA,CAChC;KACK;;AAWjB,KAAI,EALF,MACC,EAAc,KAAK,SAAS,KAC3B,EAAc,eAAe,SAAS,KACtC,EAAc,yBAAyB,SAAS,IAGlD,QACE,kBAAC,GAAD;EAAY,OAAM;EAAY,aAAY;YACxC,kBAAC,IAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAW,WAAU,aAAc,CAAA;GACzC,OAAM;GACN,aAAY;GACZ,CAAA;EACS,CAAA;CAIjB,IAAM,IAAO,EAAc,MACrB,IAA2B,EAAc,0BAGzC,KAAkB,MAAoB,IAAQ,IAAI,EAAK,MAAM,CAAC,EAAM,GAAG,GACvE,IAAiB,EAAW,EAAc,eAAe,EACzD,IAAgB,EAAW,EAAc,cAAc,EAEvD,IAAmB,EAAe,KAAK,OAAO;EAClD,OAAO,EAAE;EACT,SAAS,EAAE;EACX,UAAU,EAAE;EACZ,KAAK,EAAE,UAAU,EAAE;EACpB,EAAE,EACG,IAAoB,EAAyB,QAAQ,GAAG,MAAM,IAAI,EAAE,OAAO,EAAE,EAC7E,IAAmB,EAAyB,KAAK,GAAG,OAAS;EACjE,MAAM,EAAE,KAAK,OAAO,EAAE,CAAC,aAAa,GAAG,EAAE,KAAK,MAAM,EAAE;EACtD,OAAO,EAAE;EACT,OAAO,EAAE;EACT,OAAO,EAAa,IAAM,EAAa;EACxC,EAAE,EAEG,IAAe,EAAe,QAAQ,GAAG,MAAM,IAAI,EAAE,SAAS,EAAE,EAChE,IAAgB,EAAe,QAAQ,GAAG,MAAM,IAAI,EAAE,UAAU,EAAE,EAClE,IAAY,IAAe,GAC3B,KAAe,IAAe,IAAK,IAAY,IAAgB,MAAM;AAE3E,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;EACR,SACE,kBAAC,UAAD;GACE,SAAS;GACT,WAAU;aAFZ,CAIE,kBAAC,IAAD,EAAU,WAAU,WAAY,CAAA,EAAA,gBAEzB;;YAXb;GAcE,kBAAC,GAAD;IACE,YAAW;IACX,OAAO,CACL;KACE,IAAI;KACJ,OAAO;KACP,aAAa;KACb,MAAM;KACP,CACF;IACD,CAAA;GAGF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA0C;KAAa,CAAA,EACvE,kBAAC,OAAD;KACE,MAAK;KACL,cAAW;KACX,WAAU;eAET,EAAa,KAAK,MACjB,kBAAC,UAAD;MAEE,MAAK;MACL,gBAAc,MAAU,EAAO;MAC/B,eAAe,EAAS,EAAO,MAAM;MACrC,WAAW,EACT,6JACA,MAAU,EAAO,QACb,4DACA,8CACL;gBAEA,EAAO;MACD,EAZF,EAAO,MAYL,CACT;KACE,CAAA,CACF;;GAGL,EAAK,SAAS,KACb,kBAAC,IAAD;IAAe,WAAU;cACvB,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAK,KAAK,MAAQ;MAOjB,IAAM,IAN2C;OAC/C,mBAAmB;OACnB,mBAAmB;OACnB,kBAAkB;OAClB,qBAAqB;OACtB,CAC2B,EAAI,UAAU;AAE1C,aACE,kBAAC,GAAD;OAEE,OAAO,EAAI;OACX,OACE,EAAI,SAAS,QACT,EAAI,QACJ,GAAG,EAAa,EAAI,MAAM,GAAG,EAAI,SAAS,MAAM,MAAM,IAAI,EAAI;OAEpE,QAAQ,EAAI,SAAS,QAAQ,MAAM,KAAA;OACnC,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;OAC3C,OAAO,EAAI,UAAU,OAAO,OAAO,EAAI,UAAU,SAAS,SAAS;OACnE,QAAQ,EAAI;OACZ,EAXK,EAAI,GAWT;OAEJ;KACE,CAAA;IACQ,CAAA;GAIlB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAiB,SAAS,KACzB,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,WAAU;KACV,SACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACE,kBAAC,QAAD;QAAM,WAAU;QAA2B,OAAO,EAAE,YAAY,GAAe;QAAI,CAAA,EAAA,UAE9E;UACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACE,kBAAC,QAAD;QAAM,WAAU;QAA2B,OAAO,EAAE,YAAY,GAAe;QAAI,CAAA,EAAA,WAE9E;SACH;;eAGR,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAqB,OAAM;OAAO,QAAO;iBACvC,kBAAC,GAAD;QAAW,MAAM;QAAkB,QAAQ;SAAE,KAAK;SAAG,OAAO;SAAG,MAAM;SAAG,QAAQ;SAAG;kBAAnF;SACE,kBAAC,QAAD,EAAA,UAAA,CACE,kBAAC,kBAAD;UAAgB,IAAG;UAAU,IAAG;UAAI,IAAG;UAAI,IAAG;UAAI,IAAG;oBAArD,CACE,kBAAC,QAAD;WAAM,QAAO;WAAK,WAAW;WAAe,aAAa;WAAQ,CAAA,EACjE,kBAAC,QAAD;WAAM,QAAO;WAAM,WAAW;WAAe,aAAa;WAAQ,CAAA,CACnD;aACjB,kBAAC,kBAAD;UAAgB,IAAG;UAAU,IAAG;UAAI,IAAG;UAAI,IAAG;UAAI,IAAG;oBAArD,CACE,kBAAC,QAAD;WAAM,QAAO;WAAK,WAAW;WAAe,aAAa;WAAO,CAAA,EAChE,kBAAC,QAAD;WAAM,QAAO;WAAM,WAAW;WAAe,aAAa;WAAQ,CAAA,CACnD;YACZ,EAAA,CAAA;SACP,kBAAC,GAAD;UAAe,iBAAgB;UAAM,QAAO;UAA6B,UAAU;UAAS,CAAA;SAC5F,kBAAC,GAAD;UAAO,SAAQ;UAAQ,MAAM;UAAU,UAAU;UAAO,UAAU;UAAS,CAAA;SAC3E,kBAAC,GAAD;UACE,MAAM;UACN,UAAU;UACV,UAAU;UACV,OAAO;UACP,gBAAgB,MAAc,EAAe,EAAE;UAC/C,CAAA;SACF,kBAAC,GAAD;UACE,cAAc;UACd,YAAY;UACZ,YAAY,GAAe,MAAS,CAAC,EAAe,EAAM,EAAE,EAAK;UACjE,CAAA;SACF,kBAAC,GAAD;UACE,MAAK;UACL,SAAQ;UACR,MAAK;UACL,QAAQ;UACR,aAAa;UACb,MAAK;UACL,CAAA;SACF,kBAAC,GAAD;UACE,MAAK;UACL,SAAQ;UACR,MAAK;UACL,QAAQ;UACR,aAAa;UACb,MAAK;UACL,CAAA;SACQ;;OACQ,CAAA;MAClB,CAAA;KACI,CAAA,EAGb,EAAiB,SAAS,KACzB,kBAAC,GAAD;KAAW,OAAM;KAA6B,aAAY;eAA1D,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OAAqB,OAAM;OAAO,QAAO;iBACvC,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;QACE,MAAM;QACN,SAAQ;QACR,SAAQ;QACR,aAAa;QACb,aAAa;QACb,cAAc;QACd,QAAO;QACP,aAAa;kBAEZ,EAAiB,KAAK,MACrB,kBAAC,GAAD,EAAuB,MAAM,EAAM,OAAS,EAAjC,EAAM,KAA2B,CAC5C;QACE,CAAA,EACN,kBAAC,GAAD;QACE,cAAc;QACd,YAAY;QACZ,YAAY,GAAe,GAAI,MAAS,CACtC,GAAG,EAAe,EAAM,CAAC,KAAK,GAAM,SAAS,SAAS,EAAE,cACxD,GAAM,SAAS,KAChB;QACD,CAAA,CACO,EAAA,CAAA;OACS,CAAA,EACtB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAA8B;QAAkB,CAAA,EAChE,kBAAC,QAAD;QAAM,WAAU;kBAAuC,EAAe,EAAkB;QAAQ,CAAA,CAC5F;SACF;SACN,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAiB,KAAK,MACrB,kBAAC,MAAD;OAAqB,WAAU;iBAA/B,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,QAAD;SAAM,WAAU;SAA2B,OAAO,EAAE,YAAY,EAAM,OAAO;SAAI,CAAA,EACjF,kBAAC,QAAD;SAAM,WAAU;mBAAqB,EAAM;SAAY,CAAA,CAClD;WACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACG,IAAoB,IAAI,KAAK,MAAO,EAAM,QAAQ,IAAqB,IAAI,GAAG,GAAE,IAC5E;UACJ;SARI,EAAM,KAQV,CACL;MACC,CAAA,CACK;OAEV;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAc,SAAS,KACtB,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,WAAU;eAEV,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAqB,OAAM;OAAO,QAAO;iBACvC,kBAAC,GAAD;QAAW,MAAM;QAAe,QAAQ;SAAE,KAAK;SAAG,OAAO;SAAI,MAAM;SAAG,QAAQ;SAAG;kBAAjF;SACE,kBAAC,GAAD;UAAe,iBAAgB;UAAM,QAAO;UAA6B,UAAU;UAAS,CAAA;SAC5F,kBAAC,GAAD;UAAO,SAAQ;UAAQ,MAAM;UAAU,UAAU;UAAO,UAAU;UAAS,CAAA;SAC3E,kBAAC,GAAD;UACE,MAAM;UACN,UAAU;UACV,UAAU;UACV,OAAO;UACP,QAAQ,CAAC,GAAG,IAAI;UAChB,gBAAgB,MAAc,GAAG,EAAE;UACnC,CAAA;SACF,kBAAC,GAAD;UACE,cAAc;UACd,YAAY;UACZ,YAAY,MAAkB,CAAC,GAAG,EAAM,IAAI,YAAY;UACxD,CAAA;SACF,kBAAC,GAAD;UACE,MAAK;UACL,SAAQ;UACR,QAAO;UACP,aAAa;UACb,KAAK;WAAE,GAAG;WAAG,MAAM;WAAwB,aAAa;WAAG;UAC3D,WAAW,EAAE,GAAG,GAAG;UACnB,CAAA;SACQ;;OACQ,CAAA;MAClB,CAAA;KACI,CAAA,EAGb,EAAe,SAAS,KACvB,kBAAC,GAAD;KAAW,OAAM;KAAiB,aAAY;eAC5C,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAA8B;QAAiB,CAAA,EAC5D,kBAAC,KAAD;QAAG,WAAU;kBAAuC,EAAe,EAAa;QAAK,CAAA,CACjF,EAAA,CAAA,EACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAA8B;SAAkB,CAAA,EAC7D,kBAAC,KAAD;SAAG,WAAU;mBAAuC,EAAe,EAAc;SAAK,CAAA,CAClF;UACF;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAA8C,EAAe,EAAU;SAAK,CAAA,EACzF,kBAAC,KAAD;SAAG,WAAU;mBAA8B;SAAc,CAAA,CACrD;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAb,CAAwD,GAAa,QAAQ,EAAE,EAAC,IAAK;YACrF,kBAAC,KAAD;SAAG,WAAU;mBAA8B;SAAiB,CAAA,CACxD;UACF;SACF;;KACI,CAAA,CAEV;;GAGL,EAAe,SAAS,KACvB,kBAAC,OAAD;IAAK,IAAG;IAAwB,WAAU;cAA1C,CACE,kBAAC,MAAD;KAAI,WAAU;eAA+C;KAAwB,CAAA,EACrF,kBAAC,IAAD;KAAW,SAAS;KAAgB,MAAM;KAAgB,UAAS;KAAQ,UAAU;KAAM,CAAA,CACvF;;GAEG"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/housingvista/pages/analytics/AnalyticsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { TrendingUp, DollarSign, Building, Activity, Download, Clock, BarChart3 } from 'lucide-react';\nimport {\n AreaChart,\n Area,\n LineChart,\n Line,\n PieChart,\n Pie,\n Cell,\n XAxis,\n YAxis,\n CartesianGrid,\n Tooltip,\n ResponsiveContainer,\n} from 'recharts';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { ChartCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { EmptyState } from '../../components';\nimport { LoadingSkeleton } from '../../components';\nimport { useAnalyticsData } from '../../hooks';\nimport type { AnalyticsPeriod } from '../../types';\nimport { useHousingVistaAnalytics, useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\nimport { notify } from '../../lib/toast';\nimport { cn, formatCurrency, formatNumber } from '../../utils/helpers';\n\n// Distribution slice colours drawn from the semantic chart palette (full oklch tokens).\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\nconst REVENUE_COLOR = 'var(--color-chart-2)';\nconst EXPENSE_COLOR = 'var(--color-chart-1)';\n\n// Shared, theme-aware tooltip styling for every recharts surface on this page.\nconst tooltipContentStyle: React.CSSProperties = {\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-subtle)',\n borderRadius: 10,\n fontSize: 12,\n boxShadow: '0 8px 24px rgb(0 0 0 / 0.12)',\n};\nconst tooltipLabelStyle: React.CSSProperties = { color: 'var(--color-text-primary)', fontWeight: 600 };\nconst axisTick = { fontSize: 11, fill: 'var(--color-text-secondary)' } as const;\n\n// Server-side bucket granularity for the analytics series (BOFF-3930). The\n// backend groups revenue/occupancy directly by this period — no client-side\n// slicing of a fixed monthly window anymore.\nconst periodOptions: { value: AnalyticsPeriod; label: string }[] = [\n { value: 'week', label: 'Week' },\n { value: 'month', label: 'Month' },\n { value: 'quarter', label: 'Quarter' },\n { value: 'year', label: 'Year' },\n];\n\nexport function AnalyticsPage() {\n useTrackPageView('housingvista.analytics.viewed');\n const { emitExport } = useHousingVistaAnalytics();\n const [period, setPeriod] = useState<AnalyticsPeriod>('month');\n const { data: analyticsData, loading } = useAnalyticsData(period);\n\n // Client-side CSV export of the loaded analytics data (KPIs + monthly revenue).\n const handleExportCsv = () => {\n if (!analyticsData) return;\n\n const escapeCsv = (val: string | number) => {\n const s = String(val);\n return /[\",\\n]/.test(s) ? `\"${s.replace(/\"/g, '\"\"')}\"` : s;\n };\n\n const lines: string[] = [];\n lines.push('KPI,Value,Unit,Change %');\n for (const kpi of analyticsData.kpis) {\n lines.push([kpi.label, kpi.value, kpi.unit, kpi.changePercent].map(escapeCsv).join(','));\n }\n lines.push('');\n lines.push('Month,Revenue,Expenses,Net Income');\n for (const row of analyticsData.revenueByMonth) {\n lines.push([row.month, row.revenue, row.expenses, row.revenue - row.expenses].map(escapeCsv).join(','));\n }\n\n const blob = new Blob([lines.join('\\n')], { type: 'text/csv;charset=utf-8;' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `housingvista-analytics-${new Date().toISOString().slice(0, 10)}.csv`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n\n emitExport('analytics');\n notify.success('Report exported', 'Your analytics data was downloaded as a CSV file.');\n };\n\n const revenueColumns: Column<{ month: string; revenue: number; expenses: number }>[] = [\n {\n key: 'month',\n header: 'Month',\n render: (item) => <span className=\"font-medium text-text-primary\">{item.month}</span>,\n },\n {\n key: 'revenue',\n header: 'Revenue',\n render: (item) => <span className=\"font-medium text-status-success-text\">{formatCurrency(item.revenue)}</span>,\n },\n {\n key: 'expenses',\n header: 'Expenses',\n render: (item) => <span className=\"font-medium text-status-error-text\">{formatCurrency(item.expenses)}</span>,\n },\n {\n key: 'net',\n header: 'Net Income',\n sortable: false,\n render: (item) => {\n const net = item.revenue - item.expenses;\n return (\n <span className={cn('font-medium', net >= 0 ? 'text-status-success-text' : 'text-status-error-text')}>\n {formatCurrency(net)}\n </span>\n );\n },\n },\n {\n key: 'margin',\n header: 'Margin',\n sortable: false,\n render: (item) => {\n const margin = item.revenue > 0 ? ((item.revenue - item.expenses) / item.revenue) * 100 : 0;\n return (\n <span className={cn('font-medium', margin >= 0 ? 'text-status-success-text' : 'text-status-error-text')}>\n {margin.toFixed(1)}%\n </span>\n );\n },\n },\n ];\n\n // ---- Loading ----\n if (loading) {\n return (\n <PageLayout title=\"Analytics\" description=\"Track your property portfolio performance and market insights\">\n <LoadingSkeleton className=\"h-28\" />\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <LoadingSkeleton className=\"h-64\" />\n <LoadingSkeleton className=\"h-64\" />\n </div>\n </PageLayout>\n );\n }\n\n // ---- Empty / not-yet-computed ----\n const hasData =\n analyticsData &&\n (analyticsData.kpis.length > 0 ||\n analyticsData.revenueByMonth.length > 0 ||\n analyticsData.propertyTypeDistribution.length > 0);\n\n if (!hasData) {\n return (\n <PageLayout title=\"Analytics\" description=\"Track your property portfolio performance and market insights\">\n <EmptyState\n illustration=\"empty-generic\"\n icon={<BarChart3 className=\"h-12 w-12\" />}\n title=\"No analytics yet\"\n description=\"Once you have properties, transactions, and rentals recorded, revenue, occupancy, and portfolio insights will appear here.\"\n />\n </PageLayout>\n );\n }\n\n const kpis = analyticsData.kpis;\n const propertyTypeDistribution = analyticsData.propertyTypeDistribution;\n\n // The server already buckets both series at the requested `period`.\n const revenueByMonth = analyticsData.revenueByMonth;\n const occupancyRate = analyticsData.occupancyRate;\n\n const revenueChartData = revenueByMonth.map((r) => ({\n month: r.month,\n revenue: r.revenue,\n expenses: r.expenses,\n net: r.revenue - r.expenses,\n }));\n const distributionTotal = propertyTypeDistribution.reduce((s, d) => s + d.value, 0);\n const distributionData = propertyTypeDistribution.map((d, idx) => ({\n name: d.type.charAt(0).toUpperCase() + d.type.slice(1),\n value: d.value,\n count: d.count,\n color: CHART_COLORS[idx % CHART_COLORS.length],\n }));\n\n const totalRevenue = revenueByMonth.reduce((s, r) => s + r.revenue, 0);\n const totalExpenses = revenueByMonth.reduce((s, r) => s + r.expenses, 0);\n const netIncome = totalRevenue - totalExpenses;\n const profitMargin = totalRevenue > 0 ? (netIncome / totalRevenue) * 100 : 0;\n\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Track your property portfolio performance and market insights\"\n purpose=\"Understand how your portfolio is actually performing — revenue versus expenses, occupancy, property-type mix, and profitability — so you can spot trends, compare periods, and decide where to buy, sell, or raise rents.\"\n actions={\n <button\n onClick={handleExportCsv}\n 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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Download className=\"h-4 w-4\" />\n Export Report\n </button>\n }\n >\n <NextSteps\n storageKey=\"housingvista-analytics\"\n steps={[\n {\n id: 'performance',\n label: 'Drill into monthly performance',\n description: 'Review revenue, expenses, net income, and margin month by month.',\n href: '#analytics-performance',\n },\n ]}\n />\n\n {/* Period bucket control (BOFF-3930) — re-queries the server at the selected granularity */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"text-sm font-medium text-text-secondary\">Period</span>\n <div\n role=\"group\"\n aria-label=\"Select analytics period\"\n className=\"inline-flex rounded-lg border border-border-subtle bg-bg-sunken/60 p-1\"\n >\n {periodOptions.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n aria-pressed={period === option.value}\n onClick={() => setPeriod(option.value)}\n disabled={loading}\n className={cn(\n 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n period === option.value\n ? 'bg-action-primary-bg text-action-primary-text shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n )}\n >\n {option.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* Stats Overview Row — primary insight zone (one EmphasisPanel per page) */}\n {kpis.length > 0 && (\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {kpis.map((kpi) => {\n const icons: Record<string, typeof DollarSign> = {\n 'Portfolio Value': DollarSign,\n 'Monthly Revenue': TrendingUp,\n 'Occupancy Rate': Building,\n 'Avg. Days to Rent': Clock,\n };\n const IconComponent = icons[kpi.label] || Activity;\n\n return (\n <StatCard\n key={kpi.id}\n label={kpi.label}\n value={\n kpi.unit === 'USD'\n ? kpi.value\n : `${formatNumber(kpi.value)}${kpi.unit === '%' ? '%' : ` ${kpi.unit}`}`\n }\n prefix={kpi.unit === 'USD' ? '$' : undefined}\n icon={<IconComponent className=\"h-4 w-4\" />}\n trend={kpi.trend === 'up' ? 'up' : kpi.trend === 'down' ? 'down' : 'stable'}\n change={kpi.changePercent}\n />\n );\n })}\n </div>\n </EmphasisPanel>\n )}\n\n {/* Charts Section — revenue area chart (wide) + property-type donut (aside) */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {revenueChartData.length > 0 && (\n <ChartCard\n title=\"Revenue vs Expenses\"\n description=\"Real income against costs across the selected period\"\n className=\"lg:col-span-2\"\n actions={\n <div className=\"flex items-center gap-4 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full\" style={{ background: REVENUE_COLOR }} />\n Revenue\n </span>\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full\" style={{ background: EXPENSE_COLOR }} />\n Expenses\n </span>\n </div>\n }\n >\n <div className=\"h-72 w-full\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <AreaChart data={revenueChartData} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>\n <defs>\n <linearGradient id=\"revGrad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"5%\" stopColor={REVENUE_COLOR} stopOpacity={0.35} />\n <stop offset=\"95%\" stopColor={REVENUE_COLOR} stopOpacity={0.02} />\n </linearGradient>\n <linearGradient id=\"expGrad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"5%\" stopColor={EXPENSE_COLOR} stopOpacity={0.3} />\n <stop offset=\"95%\" stopColor={EXPENSE_COLOR} stopOpacity={0.02} />\n </linearGradient>\n </defs>\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--color-border-subtle)\" vertical={false} />\n <XAxis dataKey=\"month\" tick={axisTick} tickLine={false} axisLine={false} />\n <YAxis\n tick={axisTick}\n tickLine={false}\n axisLine={false}\n width={52}\n tickFormatter={(v: number) => formatCurrency(v)}\n />\n <Tooltip\n contentStyle={tooltipContentStyle}\n labelStyle={tooltipLabelStyle}\n formatter={(value: number, name) => [formatCurrency(value), name]}\n />\n <Area\n type=\"monotone\"\n dataKey=\"revenue\"\n name=\"Revenue\"\n stroke={REVENUE_COLOR}\n strokeWidth={2.5}\n fill=\"url(#revGrad)\"\n />\n <Area\n type=\"monotone\"\n dataKey=\"expenses\"\n name=\"Expenses\"\n stroke={EXPENSE_COLOR}\n strokeWidth={2.5}\n fill=\"url(#expGrad)\"\n />\n </AreaChart>\n </ResponsiveContainer>\n </div>\n </ChartCard>\n )}\n\n {distributionData.length > 0 && (\n <ChartCard title=\"Property-Type Distribution\" description=\"Portfolio value split by type\">\n <div className=\"relative h-56 w-full\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <PieChart>\n <Pie\n data={distributionData}\n dataKey=\"value\"\n nameKey=\"name\"\n innerRadius={58}\n outerRadius={84}\n paddingAngle={2}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={2}\n >\n {distributionData.map((entry) => (\n <Cell key={entry.name} fill={entry.color} />\n ))}\n </Pie>\n <Tooltip\n contentStyle={tooltipContentStyle}\n labelStyle={tooltipLabelStyle}\n formatter={(value: number, _n, item) => [\n `${formatCurrency(value)} · ${item?.payload?.count ?? 0} properties`,\n item?.payload?.name,\n ]}\n />\n </PieChart>\n </ResponsiveContainer>\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center\">\n <span className=\"text-xs text-text-secondary\">Total value</span>\n <span className=\"text-lg font-bold text-text-primary\">{formatCurrency(distributionTotal)}</span>\n </div>\n </div>\n <ul className=\"mt-3 space-y-2\">\n {distributionData.map((entry) => (\n <li key={entry.name} className=\"flex items-center justify-between text-sm\">\n <span className=\"flex items-center gap-2\">\n <span className=\"h-2.5 w-2.5 rounded-full\" style={{ background: entry.color }} />\n <span className=\"text-text-primary\">{entry.name}</span>\n </span>\n <span className=\"text-text-secondary\">\n {distributionTotal > 0 ? Math.round((entry.value / distributionTotal) * 100) : 0}%\n </span>\n </li>\n ))}\n </ul>\n </ChartCard>\n )}\n </div>\n\n {/* Occupancy trend (wide) + quick stats (aside) */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {occupancyRate.length > 0 && (\n <ChartCard\n title=\"Occupancy Trend\"\n description=\"Share of units occupied over the selected period\"\n className=\"lg:col-span-2\"\n >\n <div className=\"h-56 w-full\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <LineChart data={occupancyRate} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--color-border-subtle)\" vertical={false} />\n <XAxis dataKey=\"month\" tick={axisTick} tickLine={false} axisLine={false} />\n <YAxis\n tick={axisTick}\n tickLine={false}\n axisLine={false}\n width={40}\n domain={[0, 100]}\n tickFormatter={(v: number) => `${v}%`}\n />\n <Tooltip\n contentStyle={tooltipContentStyle}\n labelStyle={tooltipLabelStyle}\n formatter={(value: number) => [`${value}%`, 'Occupancy']}\n />\n <Line\n type=\"monotone\"\n dataKey=\"rate\"\n stroke=\"var(--color-chart-2)\"\n strokeWidth={2.5}\n dot={{ r: 3, fill: 'var(--color-chart-2)', strokeWidth: 0 }}\n activeDot={{ r: 5 }}\n />\n </LineChart>\n </ResponsiveContainer>\n </div>\n </ChartCard>\n )}\n\n {revenueByMonth.length > 0 && (\n <ChartCard title=\"Period Summary\" description=\"Totals across the selected range\">\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between rounded-lg bg-bg-sunken/50 p-3\">\n <div>\n <p className=\"text-sm text-text-secondary\">Total Revenue</p>\n <p className=\"text-lg font-bold text-text-primary\">{formatCurrency(totalRevenue)}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm text-text-secondary\">Total Expenses</p>\n <p className=\"text-lg font-bold text-text-primary\">{formatCurrency(totalExpenses)}</p>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg bg-status-success-bg-subtle p-3 text-center\">\n <p className=\"text-xl font-bold text-status-success-text\">{formatCurrency(netIncome)}</p>\n <p className=\"text-xs text-text-secondary\">Net Income</p>\n </div>\n <div className=\"rounded-lg bg-status-info-bg-subtle p-3 text-center\">\n <p className=\"text-xl font-bold text-status-info-text\">{profitMargin.toFixed(1)}%</p>\n <p className=\"text-xs text-text-secondary\">Profit Margin</p>\n </div>\n </div>\n </div>\n </ChartCard>\n )}\n </div>\n\n {/* Performance Metrics Table */}\n {revenueByMonth.length > 0 && (\n <div id=\"analytics-performance\" className=\"scroll-mt-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Monthly Performance</h2>\n <DataTable columns={revenueColumns} data={revenueByMonth} keyField=\"month\" pageSize={12} />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AreaInsightsPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/market/AreaInsightsPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AreaInsightsPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/market/AreaInsightsPage.tsx"],"names":[],"mappings":"AAqPA,wBAAgB,gBAAgB,gCA8I/B"}
|
|
@@ -203,14 +203,55 @@ function E({ area: r, isComparing: i, isSelected: a, isTopPick: o, onToggleCompa
|
|
|
203
203
|
})]
|
|
204
204
|
})]
|
|
205
205
|
}),
|
|
206
|
+
(r.avg2BhkPrice != null || r.avg3BhkPrice != null) && /* @__PURE__ */ x("div", {
|
|
207
|
+
className: "mt-2 grid grid-cols-2 gap-2",
|
|
208
|
+
children: [r.avg2BhkPrice != null && /* @__PURE__ */ x("div", {
|
|
209
|
+
className: "rounded-md bg-bg-sunken/50 p-2 text-xs",
|
|
210
|
+
children: [/* @__PURE__ */ b("span", {
|
|
211
|
+
className: "text-text-secondary",
|
|
212
|
+
children: "Avg 2BHK: "
|
|
213
|
+
}), /* @__PURE__ */ b("span", {
|
|
214
|
+
className: "font-semibold text-text-primary",
|
|
215
|
+
children: t(r.avg2BhkPrice)
|
|
216
|
+
})]
|
|
217
|
+
}), r.avg3BhkPrice != null && /* @__PURE__ */ x("div", {
|
|
218
|
+
className: "rounded-md bg-bg-sunken/50 p-2 text-xs",
|
|
219
|
+
children: [/* @__PURE__ */ b("span", {
|
|
220
|
+
className: "text-text-secondary",
|
|
221
|
+
children: "Avg 3BHK: "
|
|
222
|
+
}), /* @__PURE__ */ b("span", {
|
|
223
|
+
className: "font-semibold text-text-primary",
|
|
224
|
+
children: t(r.avg3BhkPrice)
|
|
225
|
+
})]
|
|
226
|
+
})]
|
|
227
|
+
}),
|
|
228
|
+
(r.nearbyHubs.length > 0 || r.transportLinks.length > 0) && /* @__PURE__ */ x("div", {
|
|
229
|
+
className: "mt-3 flex flex-wrap gap-1.5",
|
|
230
|
+
children: [r.nearbyHubs.slice(0, 3).map((e) => /* @__PURE__ */ b("span", {
|
|
231
|
+
className: "rounded-full border border-border-subtle bg-bg-surface px-2 py-0.5 text-[10px] text-text-secondary",
|
|
232
|
+
children: e
|
|
233
|
+
}, e)), r.transportLinks.slice(0, 2).map((e) => /* @__PURE__ */ b("span", {
|
|
234
|
+
className: "rounded-full border border-border-subtle bg-bg-surface px-2 py-0.5 text-[10px] text-text-secondary",
|
|
235
|
+
children: e
|
|
236
|
+
}, e))]
|
|
237
|
+
}),
|
|
206
238
|
r.upcomingProjects > 0 && /* @__PURE__ */ x("div", {
|
|
207
|
-
className: "mt-3
|
|
208
|
-
children: [
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
239
|
+
className: "mt-3 text-xs text-text-secondary",
|
|
240
|
+
children: [/* @__PURE__ */ x("div", {
|
|
241
|
+
className: "flex items-center gap-1.5",
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ b(d, { className: "h-3 w-3" }),
|
|
244
|
+
r.upcomingProjects,
|
|
245
|
+
" upcoming project",
|
|
246
|
+
r.upcomingProjects > 1 ? "s" : ""
|
|
247
|
+
]
|
|
248
|
+
}), r.futureProjects.length > 0 && /* @__PURE__ */ b("ul", {
|
|
249
|
+
className: "mt-1 space-y-0.5 pl-4",
|
|
250
|
+
children: r.futureProjects.slice(0, 3).map((e) => /* @__PURE__ */ x("li", {
|
|
251
|
+
className: "list-disc",
|
|
252
|
+
children: [e.name, e.expectedCompletion ? ` · ${e.expectedCompletion}` : ""]
|
|
253
|
+
}, e.name))
|
|
254
|
+
})]
|
|
214
255
|
})
|
|
215
256
|
]
|
|
216
257
|
});
|
|
@@ -263,6 +304,14 @@ function D() {
|
|
|
263
304
|
label: "Avg Price",
|
|
264
305
|
render: (e) => t(e.avgPropertyPrice)
|
|
265
306
|
},
|
|
307
|
+
{
|
|
308
|
+
label: "Avg 2BHK",
|
|
309
|
+
render: (e) => e.avg2BhkPrice == null ? "—" : t(e.avg2BhkPrice)
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
label: "Avg 3BHK",
|
|
313
|
+
render: (e) => e.avg3BhkPrice == null ? "—" : t(e.avg3BhkPrice)
|
|
314
|
+
},
|
|
266
315
|
{
|
|
267
316
|
label: "Rental Yield",
|
|
268
317
|
render: (e) => `${e.avgRentalYield}%`
|
|
@@ -282,6 +331,10 @@ function D() {
|
|
|
282
331
|
{
|
|
283
332
|
label: "Amenities",
|
|
284
333
|
render: (e) => `${e.amenitiesScore}/100`
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
label: "Amenities Rating",
|
|
337
|
+
render: (e) => e.amenitiesRating == null ? "—" : `${e.amenitiesRating}/5`
|
|
285
338
|
}
|
|
286
339
|
].map((e) => /* @__PURE__ */ x("tr", { children: [/* @__PURE__ */ b("td", {
|
|
287
340
|
className: "py-2 pr-4 text-text-secondary",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AreaInsightsPage.js","names":[],"sources":["../../../../src/housingvista/pages/market/AreaInsightsPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport {\n MapPin,\n GraduationCap,\n Heart,\n Users,\n TrendingUp,\n Building,\n ArrowUpRight,\n GitCompare,\n Check,\n} from 'lucide-react';\nimport { EmphasisPanel, Pagination } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useAreaInsights } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { cn, formatINRCompact, formatNumber } from '../../utils/helpers';\nimport type { AreaInsight } from '../../types';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nfunction gaugeColor(pct: number): string {\n if (pct >= 0.8) return 'text-status-success-text';\n if (pct >= 0.6) return 'text-status-info-text';\n if (pct >= 0.4) return 'text-status-warning-text';\n return 'text-status-error-text';\n}\n\n/**\n * Semicircle score gauge (0–max) — the arc is normalised via `pathLength=100`\n * so the fill is radius-independent. Matches the concept's gauge cluster.\n */\nfunction ScoreGauge({\n score,\n label,\n suffix = '',\n max = 100,\n}: {\n score: number;\n label: string;\n suffix?: string;\n max?: number;\n}) {\n const pct = Math.max(0, Math.min(score / max, 1));\n const arc = 'M5 46 A41 41 0 0 1 87 46';\n return (\n <div className=\"flex flex-col items-center\">\n <div className=\"relative\">\n <svg viewBox=\"0 0 92 50\" className=\"h-11 w-[64px]\" aria-hidden=\"true\">\n <path d={arc} fill=\"none\" strokeWidth={8} strokeLinecap=\"round\" className=\"stroke-bg-sunken\" />\n <path\n d={arc}\n fill=\"none\"\n strokeWidth={8}\n strokeLinecap=\"round\"\n stroke=\"currentColor\"\n pathLength={100}\n strokeDasharray={100}\n strokeDashoffset={100 * (1 - pct)}\n className={cn('transition-[stroke-dashoffset]', gaugeColor(pct))}\n />\n </svg>\n <span className=\"absolute inset-x-0 bottom-0 text-center text-sm font-bold tabular-nums text-text-primary\">\n {score}\n {suffix}\n </span>\n </div>\n <span className=\"mt-1 text-[10px] font-medium text-text-secondary\">{label}</span>\n </div>\n );\n}\n\nfunction AreaCard({\n area,\n isComparing,\n isSelected,\n isTopPick,\n onToggleCompare,\n}: {\n area: AreaInsight;\n isComparing: boolean;\n isSelected: boolean;\n isTopPick: boolean;\n onToggleCompare: () => void;\n}) {\n const overallScore = Math.round((area.safetyScore + area.connectivityScore + area.amenitiesScore) / 3);\n const overallPct = overallScore / 100;\n\n return (\n <div\n className={cn(\n 'rounded-card border bg-bg-surface p-5 shadow-elevation-1 transition-[box-shadow,border-color]',\n isSelected\n ? 'border-action-primary-border ring-2 ring-action-primary-border/20'\n : 'border-border-seam hover:border-border-strong hover:shadow-elevation-2'\n )}\n >\n {/* Header */}\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-base font-semibold text-text-primary\">{area.areaName}</h3>\n {isTopPick && (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-status-success-text\">\n Top Pick\n </span>\n )}\n </div>\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {area.city}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Overall score{' '}\n <span className={cn('font-semibold', gaugeColor(overallPct))}>{(overallScore / 10).toFixed(1)}/10</span>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {isComparing && (\n <button\n onClick={onToggleCompare}\n aria-pressed={isSelected}\n aria-label={isSelected ? `Remove ${area.areaName} from comparison` : `Add ${area.areaName} to comparison`}\n className={cn(\n 'flex h-7 w-7 items-center justify-center rounded-md border transition-colors',\n isSelected\n ? 'border-action-primary-border bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-bg-sunken'\n )}\n >\n {isSelected ? <Check className=\"h-3.5 w-3.5\" /> : <GitCompare className=\"h-3.5 w-3.5\" />}\n </button>\n )}\n </div>\n </div>\n\n {/* Score Gauges (mock parity) */}\n <div className=\"mb-4 grid grid-cols-4 gap-1 rounded-lg bg-bg-sunken/40 py-3\">\n <ScoreGauge score={area.safetyScore} label=\"Safety\" />\n <ScoreGauge score={area.connectivityScore} label=\"Transport\" />\n <ScoreGauge score={area.amenitiesScore} label=\"Amenities\" />\n <ScoreGauge score={area.greenCoverPercent} label=\"Green\" suffix=\"%\" />\n </div>\n\n {/* Nearby Counts */}\n <div className=\"mb-4 grid grid-cols-2 gap-2\">\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <GraduationCap className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Schools:</span>\n <span className=\"font-semibold text-text-primary\">{area.schoolsNearby}</span>\n </div>\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <Heart className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Hospitals:</span>\n <span className=\"font-semibold text-text-primary\">{area.hospitalsNearby}</span>\n </div>\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <Users className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Population:</span>\n <span className=\"font-semibold text-text-primary\">{formatNumber(area.population)}</span>\n </div>\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <TrendingUp className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Yield:</span>\n <span className=\"font-semibold text-text-primary\">{area.avgRentalYield}%</span>\n </div>\n </div>\n\n {/* Price + Growth */}\n <div className=\"flex items-center justify-between rounded-md bg-bg-sunken/50 p-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">Avg Property Price</p>\n <p className=\"text-sm font-bold text-text-primary\">{formatINRCompact(area.avgPropertyPrice)}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-xs text-text-secondary\">Appreciation</p>\n <p className=\"flex items-center justify-end gap-0.5 text-sm font-bold text-status-success-text\">\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n {area.appreciationRate}% YoY\n </p>\n </div>\n </div>\n\n {/* Upcoming Projects */}\n {area.upcomingProjects > 0 && (\n <div className=\"mt-3 flex items-center gap-1.5 text-xs text-text-secondary\">\n <Building className=\"h-3 w-3\" />\n {area.upcomingProjects} upcoming project{area.upcomingProjects > 1 ? 's' : ''}\n </div>\n )}\n </div>\n );\n}\n\nexport function AreaInsightsPage() {\n useTrackPageView('housingvista.market_analytics.area_viewed');\n const pagination = useServerPagination(10);\n const { data: areas, loading } = useAreaInsights(pagination.pagination);\n const [isComparing, setIsComparing] = useState(false);\n const [selectedForCompare, setSelectedForCompare] = useState<Set<string>>(new Set());\n\n const toggleCompare = (id: string) => {\n setSelectedForCompare((prev) => {\n const next = new Set(prev);\n if (next.has(id)) {\n next.delete(id);\n } else if (next.size < 3) {\n next.add(id);\n }\n return next;\n });\n };\n\n const comparedAreas = useMemo(() => {\n return areas.filter((a) => selectedForCompare.has(a.id));\n }, [areas, selectedForCompare]);\n\n // Highlight the highest-scoring area on this page as the \"Top Pick\".\n const topPickId = useMemo(() => {\n if (areas.length === 0) return null;\n const scoreOf = (a: AreaInsight) => a.safetyScore + a.connectivityScore + a.amenitiesScore;\n return areas.reduce((best, a) => (scoreOf(a) > scoreOf(best) ? a : best)).id;\n }, [areas]);\n\n return (\n <PageLayout\n title=\"Area Insights\"\n description=\"Explore neighborhoods, compare areas, and make informed decisions\"\n purpose=\"Decide WHERE to buy or rent, not just what. Compare neighborhoods on safety, connectivity, amenities, schools, hospitals, green cover, prices, and appreciation — then pick up to three areas to compare side by side before you commit.\"\n actions={\n <button\n onClick={() => {\n setIsComparing(!isComparing);\n if (isComparing) setSelectedForCompare(new Set());\n }}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors',\n isComparing\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-subtle text-text-primary hover:bg-bg-sunken'\n )}\n >\n <GitCompare className=\"h-3.5 w-3.5\" />\n {isComparing ? `Comparing (${selectedForCompare.size})` : 'Compare Areas'}\n </button>\n }\n >\n {/* Comparison Summary — primary emphasis zone when comparing (one per page) */}\n {isComparing && comparedAreas.length >= 2 && (\n <EmphasisPanel className=\"p-4\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Comparison Summary</h3>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"border-b border-border-subtle\">\n <th className=\"pb-2 pr-4 text-left text-text-secondary\">Metric</th>\n {comparedAreas.map((a) => (\n <th key={a.id} className=\"pb-2 pr-4 text-left font-semibold text-text-primary\">\n {a.areaName}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {[\n { label: 'Avg Price', render: (a: AreaInsight) => formatINRCompact(a.avgPropertyPrice) },\n { label: 'Rental Yield', render: (a: AreaInsight) => `${a.avgRentalYield}%` },\n { label: 'Appreciation', render: (a: AreaInsight) => `${a.appreciationRate}%` },\n { label: 'Safety Score', render: (a: AreaInsight) => `${a.safetyScore}/100` },\n { label: 'Connectivity', render: (a: AreaInsight) => `${a.connectivityScore}/100` },\n { label: 'Amenities', render: (a: AreaInsight) => `${a.amenitiesScore}/100` },\n ].map((row) => (\n <tr key={row.label}>\n <td className=\"py-2 pr-4 text-text-secondary\">{row.label}</td>\n {comparedAreas.map((a) => (\n <td key={a.id} className=\"py-2 pr-4 font-medium text-text-primary\">\n {row.render(a)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </EmphasisPanel>\n )}\n\n {/* Area Cards */}\n {areas.length > 0 ? (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {areas.map((area) => (\n <AreaCard\n key={area.id}\n area={area}\n isComparing={isComparing}\n isSelected={selectedForCompare.has(area.id)}\n isTopPick={area.id === topPickId}\n onToggleCompare={() => toggleCompare(area.id)}\n />\n ))}\n </div>\n\n {/* Pagination (server-side): working prev/next + page-size, no silent truncation */}\n <Pagination\n page={pagination.page}\n pageSize={pagination.pageSize}\n hasNextPage={areas.length >= pagination.pageSize}\n hasPreviousPage={pagination.page > 1}\n loading={loading}\n onPageChange={pagination.setPage}\n onPageSizeChange={pagination.setPageSize}\n />\n </>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<MapPin className=\"h-12 w-12\" />}\n title=\"No area insights available\"\n description=\"Area insight data will appear here once available.\"\n />\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AreaInsightsPage.js","names":[],"sources":["../../../../src/housingvista/pages/market/AreaInsightsPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport {\n MapPin,\n GraduationCap,\n Heart,\n Users,\n TrendingUp,\n Building,\n ArrowUpRight,\n GitCompare,\n Check,\n} from 'lucide-react';\nimport { EmphasisPanel, Pagination } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useAreaInsights } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { cn, formatINRCompact, formatNumber } from '../../utils/helpers';\nimport type { AreaInsight } from '../../types';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nfunction gaugeColor(pct: number): string {\n if (pct >= 0.8) return 'text-status-success-text';\n if (pct >= 0.6) return 'text-status-info-text';\n if (pct >= 0.4) return 'text-status-warning-text';\n return 'text-status-error-text';\n}\n\n/**\n * Semicircle score gauge (0–max) — the arc is normalised via `pathLength=100`\n * so the fill is radius-independent. Matches the concept's gauge cluster.\n */\nfunction ScoreGauge({\n score,\n label,\n suffix = '',\n max = 100,\n}: {\n score: number;\n label: string;\n suffix?: string;\n max?: number;\n}) {\n const pct = Math.max(0, Math.min(score / max, 1));\n const arc = 'M5 46 A41 41 0 0 1 87 46';\n return (\n <div className=\"flex flex-col items-center\">\n <div className=\"relative\">\n <svg viewBox=\"0 0 92 50\" className=\"h-11 w-[64px]\" aria-hidden=\"true\">\n <path d={arc} fill=\"none\" strokeWidth={8} strokeLinecap=\"round\" className=\"stroke-bg-sunken\" />\n <path\n d={arc}\n fill=\"none\"\n strokeWidth={8}\n strokeLinecap=\"round\"\n stroke=\"currentColor\"\n pathLength={100}\n strokeDasharray={100}\n strokeDashoffset={100 * (1 - pct)}\n className={cn('transition-[stroke-dashoffset]', gaugeColor(pct))}\n />\n </svg>\n <span className=\"absolute inset-x-0 bottom-0 text-center text-sm font-bold tabular-nums text-text-primary\">\n {score}\n {suffix}\n </span>\n </div>\n <span className=\"mt-1 text-[10px] font-medium text-text-secondary\">{label}</span>\n </div>\n );\n}\n\nfunction AreaCard({\n area,\n isComparing,\n isSelected,\n isTopPick,\n onToggleCompare,\n}: {\n area: AreaInsight;\n isComparing: boolean;\n isSelected: boolean;\n isTopPick: boolean;\n onToggleCompare: () => void;\n}) {\n const overallScore = Math.round((area.safetyScore + area.connectivityScore + area.amenitiesScore) / 3);\n const overallPct = overallScore / 100;\n\n return (\n <div\n className={cn(\n 'rounded-card border bg-bg-surface p-5 shadow-elevation-1 transition-[box-shadow,border-color]',\n isSelected\n ? 'border-action-primary-border ring-2 ring-action-primary-border/20'\n : 'border-border-seam hover:border-border-strong hover:shadow-elevation-2'\n )}\n >\n {/* Header */}\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-base font-semibold text-text-primary\">{area.areaName}</h3>\n {isTopPick && (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-status-success-text\">\n Top Pick\n </span>\n )}\n </div>\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {area.city}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Overall score{' '}\n <span className={cn('font-semibold', gaugeColor(overallPct))}>{(overallScore / 10).toFixed(1)}/10</span>\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {isComparing && (\n <button\n onClick={onToggleCompare}\n aria-pressed={isSelected}\n aria-label={isSelected ? `Remove ${area.areaName} from comparison` : `Add ${area.areaName} to comparison`}\n className={cn(\n 'flex h-7 w-7 items-center justify-center rounded-md border transition-colors',\n isSelected\n ? 'border-action-primary-border bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-bg-sunken'\n )}\n >\n {isSelected ? <Check className=\"h-3.5 w-3.5\" /> : <GitCompare className=\"h-3.5 w-3.5\" />}\n </button>\n )}\n </div>\n </div>\n\n {/* Score Gauges (mock parity) */}\n <div className=\"mb-4 grid grid-cols-4 gap-1 rounded-lg bg-bg-sunken/40 py-3\">\n <ScoreGauge score={area.safetyScore} label=\"Safety\" />\n <ScoreGauge score={area.connectivityScore} label=\"Transport\" />\n <ScoreGauge score={area.amenitiesScore} label=\"Amenities\" />\n <ScoreGauge score={area.greenCoverPercent} label=\"Green\" suffix=\"%\" />\n </div>\n\n {/* Nearby Counts */}\n <div className=\"mb-4 grid grid-cols-2 gap-2\">\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <GraduationCap className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Schools:</span>\n <span className=\"font-semibold text-text-primary\">{area.schoolsNearby}</span>\n </div>\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <Heart className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Hospitals:</span>\n <span className=\"font-semibold text-text-primary\">{area.hospitalsNearby}</span>\n </div>\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <Users className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Population:</span>\n <span className=\"font-semibold text-text-primary\">{formatNumber(area.population)}</span>\n </div>\n <div className=\"flex items-center gap-2 rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <TrendingUp className=\"h-3.5 w-3.5 text-text-secondary\" />\n <span className=\"text-text-secondary\">Yield:</span>\n <span className=\"font-semibold text-text-primary\">{area.avgRentalYield}%</span>\n </div>\n </div>\n\n {/* Price + Growth */}\n <div className=\"flex items-center justify-between rounded-md bg-bg-sunken/50 p-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">Avg Property Price</p>\n <p className=\"text-sm font-bold text-text-primary\">{formatINRCompact(area.avgPropertyPrice)}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-xs text-text-secondary\">Appreciation</p>\n <p className=\"flex items-center justify-end gap-0.5 text-sm font-bold text-status-success-text\">\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n {area.appreciationRate}% YoY\n </p>\n </div>\n </div>\n\n {/* Bedroom-tier pricing (BOFF-3932) — only rendered once captured for the area */}\n {(area.avg2BhkPrice != null || area.avg3BhkPrice != null) && (\n <div className=\"mt-2 grid grid-cols-2 gap-2\">\n {area.avg2BhkPrice != null && (\n <div className=\"rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <span className=\"text-text-secondary\">Avg 2BHK: </span>\n <span className=\"font-semibold text-text-primary\">{formatINRCompact(area.avg2BhkPrice)}</span>\n </div>\n )}\n {area.avg3BhkPrice != null && (\n <div className=\"rounded-md bg-bg-sunken/50 p-2 text-xs\">\n <span className=\"text-text-secondary\">Avg 3BHK: </span>\n <span className=\"font-semibold text-text-primary\">{formatINRCompact(area.avg3BhkPrice)}</span>\n </div>\n )}\n </div>\n )}\n\n {/* Named nearby landmarks (BOFF-3932) */}\n {(area.nearbyHubs.length > 0 || area.transportLinks.length > 0) && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {area.nearbyHubs.slice(0, 3).map((hub) => (\n <span\n key={hub}\n className=\"rounded-full border border-border-subtle bg-bg-surface px-2 py-0.5 text-[10px] text-text-secondary\"\n >\n {hub}\n </span>\n ))}\n {area.transportLinks.slice(0, 2).map((link) => (\n <span\n key={link}\n className=\"rounded-full border border-border-subtle bg-bg-surface px-2 py-0.5 text-[10px] text-text-secondary\"\n >\n {link}\n </span>\n ))}\n </div>\n )}\n\n {/* Upcoming Projects — count plus named detail when captured */}\n {area.upcomingProjects > 0 && (\n <div className=\"mt-3 text-xs text-text-secondary\">\n <div className=\"flex items-center gap-1.5\">\n <Building className=\"h-3 w-3\" />\n {area.upcomingProjects} upcoming project{area.upcomingProjects > 1 ? 's' : ''}\n </div>\n {area.futureProjects.length > 0 && (\n <ul className=\"mt-1 space-y-0.5 pl-4\">\n {area.futureProjects.slice(0, 3).map((project) => (\n <li key={project.name} className=\"list-disc\">\n {project.name}\n {project.expectedCompletion ? ` · ${project.expectedCompletion}` : ''}\n </li>\n ))}\n </ul>\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport function AreaInsightsPage() {\n useTrackPageView('housingvista.market_analytics.area_viewed');\n const pagination = useServerPagination(10);\n const { data: areas, loading } = useAreaInsights(pagination.pagination);\n const [isComparing, setIsComparing] = useState(false);\n const [selectedForCompare, setSelectedForCompare] = useState<Set<string>>(new Set());\n\n const toggleCompare = (id: string) => {\n setSelectedForCompare((prev) => {\n const next = new Set(prev);\n if (next.has(id)) {\n next.delete(id);\n } else if (next.size < 3) {\n next.add(id);\n }\n return next;\n });\n };\n\n const comparedAreas = useMemo(() => {\n return areas.filter((a) => selectedForCompare.has(a.id));\n }, [areas, selectedForCompare]);\n\n // Highlight the highest-scoring area on this page as the \"Top Pick\".\n const topPickId = useMemo(() => {\n if (areas.length === 0) return null;\n const scoreOf = (a: AreaInsight) => a.safetyScore + a.connectivityScore + a.amenitiesScore;\n return areas.reduce((best, a) => (scoreOf(a) > scoreOf(best) ? a : best)).id;\n }, [areas]);\n\n return (\n <PageLayout\n title=\"Area Insights\"\n description=\"Explore neighborhoods, compare areas, and make informed decisions\"\n purpose=\"Decide WHERE to buy or rent, not just what. Compare neighborhoods on safety, connectivity, amenities, schools, hospitals, green cover, prices, and appreciation — then pick up to three areas to compare side by side before you commit.\"\n actions={\n <button\n onClick={() => {\n setIsComparing(!isComparing);\n if (isComparing) setSelectedForCompare(new Set());\n }}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors',\n isComparing\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-subtle text-text-primary hover:bg-bg-sunken'\n )}\n >\n <GitCompare className=\"h-3.5 w-3.5\" />\n {isComparing ? `Comparing (${selectedForCompare.size})` : 'Compare Areas'}\n </button>\n }\n >\n {/* Comparison Summary — primary emphasis zone when comparing (one per page) */}\n {isComparing && comparedAreas.length >= 2 && (\n <EmphasisPanel className=\"p-4\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Comparison Summary</h3>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"border-b border-border-subtle\">\n <th className=\"pb-2 pr-4 text-left text-text-secondary\">Metric</th>\n {comparedAreas.map((a) => (\n <th key={a.id} className=\"pb-2 pr-4 text-left font-semibold text-text-primary\">\n {a.areaName}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {[\n { label: 'Avg Price', render: (a: AreaInsight) => formatINRCompact(a.avgPropertyPrice) },\n {\n label: 'Avg 2BHK',\n render: (a: AreaInsight) => (a.avg2BhkPrice != null ? formatINRCompact(a.avg2BhkPrice) : '—'),\n },\n {\n label: 'Avg 3BHK',\n render: (a: AreaInsight) => (a.avg3BhkPrice != null ? formatINRCompact(a.avg3BhkPrice) : '—'),\n },\n { label: 'Rental Yield', render: (a: AreaInsight) => `${a.avgRentalYield}%` },\n { label: 'Appreciation', render: (a: AreaInsight) => `${a.appreciationRate}%` },\n { label: 'Safety Score', render: (a: AreaInsight) => `${a.safetyScore}/100` },\n { label: 'Connectivity', render: (a: AreaInsight) => `${a.connectivityScore}/100` },\n { label: 'Amenities', render: (a: AreaInsight) => `${a.amenitiesScore}/100` },\n {\n label: 'Amenities Rating',\n render: (a: AreaInsight) => (a.amenitiesRating != null ? `${a.amenitiesRating}/5` : '—'),\n },\n ].map((row) => (\n <tr key={row.label}>\n <td className=\"py-2 pr-4 text-text-secondary\">{row.label}</td>\n {comparedAreas.map((a) => (\n <td key={a.id} className=\"py-2 pr-4 font-medium text-text-primary\">\n {row.render(a)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </EmphasisPanel>\n )}\n\n {/* Area Cards */}\n {areas.length > 0 ? (\n <>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {areas.map((area) => (\n <AreaCard\n key={area.id}\n area={area}\n isComparing={isComparing}\n isSelected={selectedForCompare.has(area.id)}\n isTopPick={area.id === topPickId}\n onToggleCompare={() => toggleCompare(area.id)}\n />\n ))}\n </div>\n\n {/* Pagination (server-side): working prev/next + page-size, no silent truncation */}\n <Pagination\n page={pagination.page}\n pageSize={pagination.pageSize}\n hasNextPage={areas.length >= pagination.pageSize}\n hasPreviousPage={pagination.page > 1}\n loading={loading}\n onPageChange={pagination.setPage}\n onPageSizeChange={pagination.setPageSize}\n />\n </>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<MapPin className=\"h-12 w-12\" />}\n title=\"No area insights available\"\n description=\"Area insight data will appear here once available.\"\n />\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PropertiesPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/PropertiesPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"PropertiesPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/PropertiesPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAyCH,eAAO,MAAM,cAAc,mCAmkB1B,CAAC"}
|