@burdenoff/microfe-housingvista 2026.624.2 → 2026.625.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/housingvista/components/ChartCard.js +11 -9
- package/dist/housingvista/components/ChartCard.js.map +1 -1
- package/dist/housingvista/components/DataTable.js +63 -60
- package/dist/housingvista/components/DataTable.js.map +1 -1
- package/dist/housingvista/components/EmptyState.js +16 -9
- package/dist/housingvista/components/EmptyState.js.map +1 -1
- package/dist/housingvista/components/PageLayout.js +21 -14
- package/dist/housingvista/components/PageLayout.js.map +1 -1
- package/dist/housingvista/components/PropertyCard.js +2 -2
- package/dist/housingvista/components/PropertyCard.js.map +1 -1
- package/dist/housingvista/components/StatCard.js +20 -17
- package/dist/housingvista/components/StatCard.js.map +1 -1
- package/dist/housingvista/components/StatusBadge.js +1 -1
- package/dist/housingvista/components/StatusBadge.js.map +1 -1
- package/dist/housingvista/pages/HomePage.js +101 -58
- package/dist/housingvista/pages/HomePage.js.map +1 -1
- package/dist/housingvista/pages/amc/AMCManagementPage.js +59 -48
- package/dist/housingvista/pages/amc/AMCManagementPage.js.map +1 -1
- package/dist/housingvista/pages/analytics/AnalyticsPage.js +113 -90
- package/dist/housingvista/pages/analytics/AnalyticsPage.js.map +1 -1
- package/dist/housingvista/pages/assets/AssetManagementPage.js +66 -60
- package/dist/housingvista/pages/assets/AssetManagementPage.js.map +1 -1
- package/dist/housingvista/pages/chat/ChatPage.js +60 -58
- package/dist/housingvista/pages/chat/ChatPage.js.map +1 -1
- package/dist/housingvista/pages/finance/HomeLoansPage.js +23 -20
- package/dist/housingvista/pages/finance/HomeLoansPage.js.map +1 -1
- package/dist/housingvista/pages/investments/InvestmentDetailPage.js +114 -81
- package/dist/housingvista/pages/investments/InvestmentDetailPage.js.map +1 -1
- package/dist/housingvista/pages/investments/InvestmentsPage.js +65 -59
- package/dist/housingvista/pages/investments/InvestmentsPage.js.map +1 -1
- package/dist/housingvista/pages/legal/LegalPage.js +17 -14
- package/dist/housingvista/pages/legal/LegalPage.js.map +1 -1
- package/dist/housingvista/pages/market/AreaInsightsPage.js +13 -10
- package/dist/housingvista/pages/market/AreaInsightsPage.js.map +1 -1
- package/dist/housingvista/pages/market/MarketAnalyticsPage.js +155 -123
- package/dist/housingvista/pages/market/MarketAnalyticsPage.js.map +1 -1
- package/dist/housingvista/pages/marketplace/MarketplacePage.js +15 -12
- package/dist/housingvista/pages/marketplace/MarketplacePage.js.map +1 -1
- package/dist/housingvista/pages/notifications/NotificationsPage.js +24 -21
- package/dist/housingvista/pages/notifications/NotificationsPage.js.map +1 -1
- package/dist/housingvista/pages/properties/AddPropertyPage.js +73 -71
- package/dist/housingvista/pages/properties/AddPropertyPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.js +141 -135
- package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.js +192 -154
- package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
- package/dist/housingvista/pages/rental/RentalManagementPage.js +185 -160
- package/dist/housingvista/pages/rental/RentalManagementPage.js.map +1 -1
- package/dist/housingvista/pages/settings/ProfilePage.js +51 -49
- package/dist/housingvista/pages/settings/ProfilePage.js.map +1 -1
- package/dist/housingvista/pages/settings/SettingsPage.js +56 -54
- package/dist/housingvista/pages/settings/SettingsPage.js.map +1 -1
- package/dist/housingvista/pages/transactions/TransactionDetailPage.js +183 -142
- package/dist/housingvista/pages/transactions/TransactionDetailPage.js.map +1 -1
- package/dist/housingvista/pages/transactions/TransactionsPage.js +90 -84
- package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
- package/dist/housingvista/pages/verification/VerificationPage.js +49 -42
- package/dist/housingvista/pages/verification/VerificationPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AMCManagementPage.js","names":[],"sources":["../../../../src/housingvista/pages/amc/AMCManagementPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport {\n Shield,\n FileText,\n DollarSign,\n Calendar,\n Plus,\n Download,\n CheckCircle,\n Clock,\n AlertTriangle,\n Wrench,\n RotateCcw,\n Eye,\n} from 'lucide-react';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { StatusBadge } from '../../components';\nimport { FilterBar } from '../../components';\nimport { EmptyState } from '../../components';\nimport { useAMCContracts } from '../../hooks';\nimport { cn, formatCurrency, formatDate } from '../../utils/helpers';\nimport type { AMCContract } from '../../types';\n\nfunction getStatusVariant(status: AMCContract['status']): 'success' | 'warning' | 'error' | 'muted' {\n switch (status) {\n case 'active':\n return 'success';\n case 'expiring_soon':\n return 'warning';\n case 'expired':\n return 'error';\n case 'cancelled':\n return 'muted';\n default:\n return 'muted';\n }\n}\n\nexport function AMCManagementPage() {\n const { data: amcContracts } = useAMCContracts();\n const [search, setSearch] = useState('');\n\n const filtered = useMemo(() => {\n if (!search) return amcContracts;\n const q = search.toLowerCase();\n return amcContracts.filter(\n (c) =>\n c.propertyTitle.toLowerCase().includes(q) ||\n c.providerName.toLowerCase().includes(q) ||\n c.packageName.toLowerCase().includes(q)\n );\n }, [amcContracts, search]);\n\n const stats = useMemo(() => {\n const activeContracts = amcContracts.filter((c) => c.status === 'active' || c.status === 'expiring_soon').length;\n const pendingRenewals = amcContracts.filter((c) => c.status === 'expiring_soon').length;\n const totalMonthlySpent = amcContracts\n .filter((c) => c.status === 'active' || c.status === 'expiring_soon')\n .reduce((sum, c) => sum + c.monthlyCost, 0);\n const upcomingServices = amcContracts.filter((c) => {\n if (!c.nextServiceDate) return false;\n const serviceDate = new Date(c.nextServiceDate);\n const now = new Date();\n const daysUntil = (serviceDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);\n return daysUntil >= 0 && daysUntil <= 30;\n }).length;\n return { activeContracts, pendingRenewals, totalMonthlySpent, upcomingServices };\n }, [amcContracts]);\n\n const contractColumns: Column<AMCContract>[] = [\n {\n key: 'packageName',\n header: 'Contract',\n render: (item) => (\n <div>\n <div className=\"font-medium text-text-primary\">{item.packageName}</div>\n <div className=\"text-xs text-text-secondary\">{item.propertyTitle}</div>\n </div>\n ),\n },\n {\n key: 'providerName',\n header: 'Provider',\n render: (item) => <span className=\"text-sm text-text-primary\">{item.providerName}</span>,\n },\n {\n key: 'servicesIncluded',\n header: 'Coverage',\n render: (item) => (\n <div className=\"max-w-[200px]\">\n <span className=\"text-sm text-text-primary\">{item.servicesIncluded.length} services</span>\n <p className=\"truncate text-xs text-text-secondary\">\n {item.servicesIncluded.slice(0, 2).join(', ')}\n {item.servicesIncluded.length > 2 && ` +${item.servicesIncluded.length - 2} more`}\n </p>\n </div>\n ),\n },\n {\n key: 'monthlyCost',\n header: 'Cost',\n render: (item) => (\n <div>\n <span className=\"font-medium text-text-primary\">{formatCurrency(item.monthlyCost)}</span>\n <span className=\"text-xs text-text-secondary\">/mo</span>\n </div>\n ),\n },\n {\n key: 'startDate',\n header: 'Start',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatDate(item.startDate)}</span>,\n },\n {\n key: 'endDate',\n header: 'End',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatDate(item.endDate)}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n render: (item) => (\n <StatusBadge label={item.status.replace('_', ' ')} variant={getStatusVariant(item.status)} dot />\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"AMC Management\"\n description=\"Manage Annual Maintenance Contracts and service packages\"\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 New Contract\n </button>\n </div>\n }\n >\n {/* Stats */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard label=\"Active Contracts\" value={stats.activeContracts} icon={<Shield className=\"h-4 w-4\" />} />\n <StatCard\n label=\"Pending Renewals\"\n value={stats.pendingRenewals}\n icon={<RotateCcw className=\"h-4 w-4\" />}\n trend={stats.pendingRenewals > 0 ? 'up' : 'stable'}\n change={stats.pendingRenewals > 0 ? stats.pendingRenewals : 0}\n />\n <StatCard\n label=\"Total Monthly Spent\"\n value={stats.totalMonthlySpent}\n prefix=\"$\"\n icon={<DollarSign className=\"h-4 w-4\" />}\n />\n <StatCard label=\"Upcoming Services\" value={stats.upcomingServices} icon={<Calendar className=\"h-4 w-4\" />} />\n </div>\n\n {/* Service Schedule Section */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-sm\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">Upcoming Service Schedule</h3>\n <div className=\"space-y-3\">\n {amcContracts\n .filter((c) => c.nextServiceDate && (c.status === 'active' || c.status === 'expiring_soon'))\n .sort((a, b) => new Date(a.nextServiceDate).getTime() - new Date(b.nextServiceDate).getTime())\n .map((contract) => {\n const serviceDate = new Date(contract.nextServiceDate);\n const now = new Date();\n const daysUntil = Math.ceil((serviceDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));\n const isUrgent = daysUntil <= 7;\n const isPast = daysUntil < 0;\n\n return (\n <div\n key={contract.id}\n className=\"flex flex-col gap-3 rounded-lg bg-bg-sunken/50 p-4 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"flex items-start gap-3\">\n <div\n className={cn(\n 'rounded-lg p-2',\n isPast\n ? 'bg-red-100 dark:bg-red-900/30'\n : isUrgent\n ? 'bg-amber-100 dark:bg-amber-900/30'\n : 'bg-blue-100 dark:bg-blue-900/30'\n )}\n >\n {isPast ? (\n <AlertTriangle className=\"h-4 w-4 text-red-600 dark:text-red-400\" />\n ) : isUrgent ? (\n <Clock className=\"h-4 w-4 text-amber-600 dark:text-amber-400\" />\n ) : (\n <Wrench className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n )}\n </div>\n <div>\n <p className=\"font-medium text-text-primary\">{contract.propertyTitle}</p>\n <p className=\"text-sm text-text-secondary\">\n {contract.packageName} · {contract.providerName}\n </p>\n <div className=\"mt-1 flex flex-wrap gap-1\">\n {contract.servicesIncluded.slice(0, 3).map((service, idx) => (\n <span\n key={idx}\n className=\"inline-block rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {service}\n </span>\n ))}\n {contract.servicesIncluded.length > 3 && (\n <span className=\"inline-block rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n +{contract.servicesIncluded.length - 3} more\n </span>\n )}\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right\">\n <p className=\"text-sm font-medium text-text-primary\">{formatDate(contract.nextServiceDate)}</p>\n <p\n className={cn(\n 'text-xs',\n isPast\n ? 'text-red-600 dark:text-red-400'\n : isUrgent\n ? 'text-amber-600 dark:text-amber-400'\n : 'text-text-secondary'\n )}\n >\n {isPast\n ? `${Math.abs(daysUntil)} days overdue`\n : daysUntil === 0\n ? 'Today'\n : `In ${daysUntil} days`}\n </p>\n </div>\n <StatusBadge\n label={contract.status.replace('_', ' ')}\n variant={getStatusVariant(contract.status)}\n />\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Contracts Table */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">All Contracts</h2>\n <FilterBar\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder=\"Search contracts, providers, properties...\"\n />\n <div className=\"mt-4\">\n {filtered.length > 0 ? (\n <DataTable columns={contractColumns} data={filtered} keyField=\"id\" pageSize={10} />\n ) : (\n <EmptyState\n icon={<Shield className=\"h-12 w-12\" />}\n title=\"No contracts found\"\n description=\"Try adjusting your search criteria or create a new contract.\"\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 New Contract\n </button>\n }\n />\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AMCManagementPage.js","names":[],"sources":["../../../../src/housingvista/pages/amc/AMCManagementPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport {\n Shield,\n FileText,\n DollarSign,\n Calendar,\n Plus,\n Download,\n CheckCircle,\n Clock,\n AlertTriangle,\n Wrench,\n RotateCcw,\n Eye,\n} from 'lucide-react';\nimport { EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { StatusBadge } from '../../components';\nimport { FilterBar } from '../../components';\nimport { EmptyState } from '../../components';\nimport { useAMCContracts } from '../../hooks';\nimport { cn, formatCurrency, formatDate } from '../../utils/helpers';\nimport type { AMCContract } from '../../types';\n\nfunction getStatusVariant(status: AMCContract['status']): 'success' | 'warning' | 'error' | 'muted' {\n switch (status) {\n case 'active':\n return 'success';\n case 'expiring_soon':\n return 'warning';\n case 'expired':\n return 'error';\n case 'cancelled':\n return 'muted';\n default:\n return 'muted';\n }\n}\n\nexport function AMCManagementPage() {\n const { data: amcContracts } = useAMCContracts();\n const [search, setSearch] = useState('');\n\n const filtered = useMemo(() => {\n if (!search) return amcContracts;\n const q = search.toLowerCase();\n return amcContracts.filter(\n (c) =>\n c.propertyTitle.toLowerCase().includes(q) ||\n c.providerName.toLowerCase().includes(q) ||\n c.packageName.toLowerCase().includes(q)\n );\n }, [amcContracts, search]);\n\n const stats = useMemo(() => {\n const activeContracts = amcContracts.filter((c) => c.status === 'active' || c.status === 'expiring_soon').length;\n const pendingRenewals = amcContracts.filter((c) => c.status === 'expiring_soon').length;\n const totalMonthlySpent = amcContracts\n .filter((c) => c.status === 'active' || c.status === 'expiring_soon')\n .reduce((sum, c) => sum + c.monthlyCost, 0);\n const upcomingServices = amcContracts.filter((c) => {\n if (!c.nextServiceDate) return false;\n const serviceDate = new Date(c.nextServiceDate);\n const now = new Date();\n const daysUntil = (serviceDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);\n return daysUntil >= 0 && daysUntil <= 30;\n }).length;\n return { activeContracts, pendingRenewals, totalMonthlySpent, upcomingServices };\n }, [amcContracts]);\n\n const scheduledServices = useMemo(\n () =>\n amcContracts\n .filter((c) => c.nextServiceDate && (c.status === 'active' || c.status === 'expiring_soon'))\n .sort((a, b) => new Date(a.nextServiceDate).getTime() - new Date(b.nextServiceDate).getTime()),\n [amcContracts]\n );\n\n const contractColumns: Column<AMCContract>[] = [\n {\n key: 'packageName',\n header: 'Contract',\n render: (item) => (\n <div>\n <div className=\"font-medium text-text-primary\">{item.packageName}</div>\n <div className=\"text-xs text-text-secondary\">{item.propertyTitle}</div>\n </div>\n ),\n },\n {\n key: 'providerName',\n header: 'Provider',\n render: (item) => <span className=\"text-sm text-text-primary\">{item.providerName}</span>,\n },\n {\n key: 'servicesIncluded',\n header: 'Coverage',\n render: (item) => (\n <div className=\"max-w-[200px]\">\n <span className=\"text-sm text-text-primary\">{item.servicesIncluded.length} services</span>\n <p className=\"truncate text-xs text-text-secondary\">\n {item.servicesIncluded.slice(0, 2).join(', ')}\n {item.servicesIncluded.length > 2 && ` +${item.servicesIncluded.length - 2} more`}\n </p>\n </div>\n ),\n },\n {\n key: 'monthlyCost',\n header: 'Cost',\n render: (item) => (\n <div>\n <span className=\"font-medium text-text-primary\">{formatCurrency(item.monthlyCost)}</span>\n <span className=\"text-xs text-text-secondary\">/mo</span>\n </div>\n ),\n },\n {\n key: 'startDate',\n header: 'Start',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatDate(item.startDate)}</span>,\n },\n {\n key: 'endDate',\n header: 'End',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatDate(item.endDate)}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n render: (item) => (\n <StatusBadge label={item.status.replace('_', ' ')} variant={getStatusVariant(item.status)} dot />\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"AMC Management\"\n description=\"Manage Annual Maintenance Contracts and service packages\"\n purpose=\"Keep your properties maintained without surprises. Track every Annual Maintenance Contract, see what each covers and costs, get ahead of renewals, and know which services are due in the next 30 days so nothing slips.\"\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 New Contract\n </button>\n </div>\n }\n >\n {/* 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 label=\"Active Contracts\" value={stats.activeContracts} icon={<Shield className=\"h-4 w-4\" />} />\n <StatCard\n label=\"Pending Renewals\"\n value={stats.pendingRenewals}\n icon={<RotateCcw className=\"h-4 w-4\" />}\n trend={stats.pendingRenewals > 0 ? 'up' : 'stable'}\n change={stats.pendingRenewals > 0 ? stats.pendingRenewals : 0}\n />\n <StatCard\n label=\"Total Monthly Spent\"\n value={stats.totalMonthlySpent}\n prefix=\"$\"\n icon={<DollarSign className=\"h-4 w-4\" />}\n />\n <StatCard label=\"Upcoming Services\" value={stats.upcomingServices} icon={<Calendar className=\"h-4 w-4\" />} />\n </div>\n </EmphasisPanel>\n\n {/* Service Schedule Section */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-sm\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">Upcoming Service Schedule</h3>\n {scheduledServices.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No services scheduled\"\n description=\"Once your active contracts have upcoming service dates, they'll be listed here in order of priority.\"\n className=\"border-0 bg-transparent p-6\"\n />\n ) : (\n <div className=\"space-y-3\">\n {scheduledServices\n .map((contract) => {\n const serviceDate = new Date(contract.nextServiceDate);\n const now = new Date();\n const daysUntil = Math.ceil((serviceDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));\n const isUrgent = daysUntil <= 7;\n const isPast = daysUntil < 0;\n\n return (\n <div\n key={contract.id}\n className=\"flex flex-col gap-3 rounded-lg bg-bg-sunken/50 p-4 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"flex items-start gap-3\">\n <div\n className={cn(\n 'rounded-lg p-2',\n isPast\n ? 'bg-red-100 dark:bg-red-900/30'\n : isUrgent\n ? 'bg-amber-100 dark:bg-amber-900/30'\n : 'bg-blue-100 dark:bg-blue-900/30'\n )}\n >\n {isPast ? (\n <AlertTriangle className=\"h-4 w-4 text-red-600 dark:text-red-400\" />\n ) : isUrgent ? (\n <Clock className=\"h-4 w-4 text-amber-600 dark:text-amber-400\" />\n ) : (\n <Wrench className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n )}\n </div>\n <div>\n <p className=\"font-medium text-text-primary\">{contract.propertyTitle}</p>\n <p className=\"text-sm text-text-secondary\">\n {contract.packageName} · {contract.providerName}\n </p>\n <div className=\"mt-1 flex flex-wrap gap-1\">\n {contract.servicesIncluded.slice(0, 3).map((service, idx) => (\n <span\n key={idx}\n className=\"inline-block rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {service}\n </span>\n ))}\n {contract.servicesIncluded.length > 3 && (\n <span className=\"inline-block rounded-md bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n +{contract.servicesIncluded.length - 3} more\n </span>\n )}\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right\">\n <p className=\"text-sm font-medium text-text-primary\">{formatDate(contract.nextServiceDate)}</p>\n <p\n className={cn(\n 'text-xs',\n isPast\n ? 'text-red-600 dark:text-red-400'\n : isUrgent\n ? 'text-amber-600 dark:text-amber-400'\n : 'text-text-secondary'\n )}\n >\n {isPast\n ? `${Math.abs(daysUntil)} days overdue`\n : daysUntil === 0\n ? 'Today'\n : `In ${daysUntil} days`}\n </p>\n </div>\n <StatusBadge\n label={contract.status.replace('_', ' ')}\n variant={getStatusVariant(contract.status)}\n />\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Contracts Table */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">All Contracts</h2>\n <FilterBar\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder=\"Search contracts, providers, properties...\"\n />\n <div className=\"mt-4\">\n {filtered.length > 0 ? (\n <DataTable columns={contractColumns} data={filtered} keyField=\"id\" pageSize={10} />\n ) : (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Shield className=\"h-12 w-12\" />}\n title=\"No contracts found\"\n description=\"Try adjusting your search criteria or create a new contract.\"\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 New Contract\n </button>\n }\n />\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -8,8 +8,9 @@ import { useAnalyticsData as s } from "../../hooks/index.js";
|
|
|
8
8
|
import { useState as c } from "react";
|
|
9
9
|
import { Activity as l, ArrowDownRight as u, ArrowUpRight as d, Building as f, Calendar as p, Clock as m, DollarSign as h, Download as g, TrendingUp as _ } from "lucide-react";
|
|
10
10
|
import { jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
11
|
+
import { EmphasisPanel as b, NextSteps as x } from "@burdenoff/fe-libs/ui";
|
|
11
12
|
//#region src/housingvista/pages/analytics/AnalyticsPage.tsx
|
|
12
|
-
var
|
|
13
|
+
var S = [
|
|
13
14
|
{
|
|
14
15
|
value: "week",
|
|
15
16
|
label: "Week"
|
|
@@ -27,16 +28,31 @@ var b = [
|
|
|
27
28
|
label: "Year"
|
|
28
29
|
}
|
|
29
30
|
];
|
|
30
|
-
function
|
|
31
|
-
let { data:
|
|
31
|
+
function C() {
|
|
32
|
+
let { data: C } = s(), [w, T] = c("month");
|
|
32
33
|
return /* @__PURE__ */ y(r, {
|
|
33
34
|
title: "Analytics",
|
|
34
35
|
description: "Track your property portfolio performance and market insights",
|
|
36
|
+
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.",
|
|
35
37
|
actions: /* @__PURE__ */ y("button", {
|
|
36
38
|
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",
|
|
37
39
|
children: [/* @__PURE__ */ v(g, { className: "h-4 w-4" }), "Export Report"]
|
|
38
40
|
}),
|
|
39
41
|
children: [
|
|
42
|
+
/* @__PURE__ */ v(x, {
|
|
43
|
+
storageKey: "housingvista-analytics",
|
|
44
|
+
steps: [{
|
|
45
|
+
id: "period-year",
|
|
46
|
+
label: "See the full-year picture",
|
|
47
|
+
description: "Switch the period to Year to view long-term trends.",
|
|
48
|
+
onClick: () => T("year")
|
|
49
|
+
}, {
|
|
50
|
+
id: "performance",
|
|
51
|
+
label: "Drill into monthly performance",
|
|
52
|
+
description: "Review revenue, expenses, net income, and margin month by month.",
|
|
53
|
+
href: "#analytics-performance"
|
|
54
|
+
}]
|
|
55
|
+
}),
|
|
40
56
|
/* @__PURE__ */ y("div", {
|
|
41
57
|
className: "flex flex-wrap items-center gap-2",
|
|
42
58
|
children: [
|
|
@@ -45,30 +61,33 @@ function x() {
|
|
|
45
61
|
className: "text-sm font-medium text-text-secondary",
|
|
46
62
|
children: "Period:"
|
|
47
63
|
}),
|
|
48
|
-
|
|
49
|
-
onClick: () =>
|
|
50
|
-
className: e("rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
|
|
64
|
+
S.map((t) => /* @__PURE__ */ v("button", {
|
|
65
|
+
onClick: () => T(t.value),
|
|
66
|
+
className: e("rounded-md px-3 py-1.5 text-sm font-medium transition-colors", w === t.value ? "bg-foreground text-background" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
|
|
51
67
|
children: t.label
|
|
52
68
|
}, t.value))
|
|
53
69
|
]
|
|
54
70
|
}),
|
|
55
|
-
/* @__PURE__ */ v(
|
|
56
|
-
className: "
|
|
57
|
-
children:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
/* @__PURE__ */ v(b, {
|
|
72
|
+
className: "p-4",
|
|
73
|
+
children: /* @__PURE__ */ v("div", {
|
|
74
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
75
|
+
children: C.kpis.map((e) => {
|
|
76
|
+
let t = {
|
|
77
|
+
"Portfolio Value": h,
|
|
78
|
+
"Monthly Revenue": _,
|
|
79
|
+
"Occupancy Rate": f,
|
|
80
|
+
"Avg. Days to Rent": m
|
|
81
|
+
}[e.label] || l;
|
|
82
|
+
return /* @__PURE__ */ v(i, {
|
|
83
|
+
label: e.label,
|
|
84
|
+
value: e.unit === "USD" ? e.value : `${n(e.value)}${e.unit === "%" ? "%" : ` ${e.unit}`}`,
|
|
85
|
+
prefix: e.unit === "USD" ? "$" : void 0,
|
|
86
|
+
icon: /* @__PURE__ */ v(t, { className: "h-4 w-4" }),
|
|
87
|
+
trend: e.trend === "up" ? "up" : e.trend === "down" ? "down" : "stable",
|
|
88
|
+
change: e.changePercent
|
|
89
|
+
}, e.id);
|
|
90
|
+
})
|
|
72
91
|
})
|
|
73
92
|
}),
|
|
74
93
|
/* @__PURE__ */ y("div", {
|
|
@@ -79,8 +98,8 @@ function x() {
|
|
|
79
98
|
description: "Monthly revenue and expense trends",
|
|
80
99
|
children: /* @__PURE__ */ y("div", {
|
|
81
100
|
className: "space-y-3",
|
|
82
|
-
children: [
|
|
83
|
-
let n = Math.max(...
|
|
101
|
+
children: [C.revenueByMonth.slice(-6).map((e) => {
|
|
102
|
+
let n = Math.max(...C.revenueByMonth.map((e) => e.revenue)), r = e.revenue / n * 100, i = e.expenses / n * 100;
|
|
84
103
|
return /* @__PURE__ */ y("div", {
|
|
85
104
|
className: "space-y-1",
|
|
86
105
|
children: [
|
|
@@ -131,8 +150,8 @@ function x() {
|
|
|
131
150
|
description: "Portfolio breakdown by property type",
|
|
132
151
|
children: /* @__PURE__ */ v("div", {
|
|
133
152
|
className: "space-y-4",
|
|
134
|
-
children:
|
|
135
|
-
let r =
|
|
153
|
+
children: C.propertyTypeDistribution.map((n) => {
|
|
154
|
+
let r = C.propertyTypeDistribution.reduce((e, t) => e + t.value, 0), i = r > 0 ? n.value / r * 100 : 0, a = {
|
|
136
155
|
Residential: "bg-blue-500",
|
|
137
156
|
Commercial: "bg-purple-500",
|
|
138
157
|
Agricultural: "bg-emerald-500"
|
|
@@ -173,7 +192,7 @@ function x() {
|
|
|
173
192
|
description: "Monthly occupancy rate percentage",
|
|
174
193
|
children: /* @__PURE__ */ v("div", {
|
|
175
194
|
className: "space-y-3",
|
|
176
|
-
children:
|
|
195
|
+
children: C.occupancyRate.slice(-6).map((t, n, r) => {
|
|
177
196
|
let i = n > 0 ? r[n - 1].rate : t.rate, a = t.rate - i;
|
|
178
197
|
return /* @__PURE__ */ y("div", {
|
|
179
198
|
className: "flex items-center justify-between",
|
|
@@ -216,7 +235,7 @@ function x() {
|
|
|
216
235
|
children: "Total Revenue (YTD)"
|
|
217
236
|
}), /* @__PURE__ */ v("p", {
|
|
218
237
|
className: "text-lg font-bold text-text-primary",
|
|
219
|
-
children: t(
|
|
238
|
+
children: t(C.revenueByMonth.reduce((e, t) => e + t.revenue, 0))
|
|
220
239
|
})] }), /* @__PURE__ */ y("div", {
|
|
221
240
|
className: "text-right",
|
|
222
241
|
children: [/* @__PURE__ */ v("p", {
|
|
@@ -224,7 +243,7 @@ function x() {
|
|
|
224
243
|
children: "Total Expenses (YTD)"
|
|
225
244
|
}), /* @__PURE__ */ v("p", {
|
|
226
245
|
className: "text-lg font-bold text-text-primary",
|
|
227
|
-
children: t(
|
|
246
|
+
children: t(C.revenueByMonth.reduce((e, t) => e + t.expenses, 0))
|
|
228
247
|
})]
|
|
229
248
|
})]
|
|
230
249
|
}), /* @__PURE__ */ y("div", {
|
|
@@ -233,7 +252,7 @@ function x() {
|
|
|
233
252
|
className: "rounded-lg bg-emerald-50 p-3 text-center dark:bg-emerald-900/20",
|
|
234
253
|
children: [/* @__PURE__ */ v("p", {
|
|
235
254
|
className: "text-xl font-bold text-emerald-600 dark:text-emerald-400",
|
|
236
|
-
children: t(
|
|
255
|
+
children: t(C.revenueByMonth.reduce((e, t) => e + t.revenue, 0) - C.revenueByMonth.reduce((e, t) => e + t.expenses, 0))
|
|
237
256
|
}), /* @__PURE__ */ v("p", {
|
|
238
257
|
className: "text-xs text-text-secondary",
|
|
239
258
|
children: "Net Income"
|
|
@@ -242,7 +261,7 @@ function x() {
|
|
|
242
261
|
className: "rounded-lg bg-blue-50 p-3 text-center dark:bg-blue-900/20",
|
|
243
262
|
children: [/* @__PURE__ */ y("p", {
|
|
244
263
|
className: "text-xl font-bold text-blue-600 dark:text-blue-400",
|
|
245
|
-
children: [((
|
|
264
|
+
children: [((C.revenueByMonth.reduce((e, t) => e + t.revenue, 0) - C.revenueByMonth.reduce((e, t) => e + t.expenses, 0)) / C.revenueByMonth.reduce((e, t) => e + t.revenue, 0) * 100).toFixed(1), "%"]
|
|
246
265
|
}), /* @__PURE__ */ v("p", {
|
|
247
266
|
className: "text-xs text-text-secondary",
|
|
248
267
|
children: "Profit Margin"
|
|
@@ -253,68 +272,72 @@ function x() {
|
|
|
253
272
|
})
|
|
254
273
|
]
|
|
255
274
|
}),
|
|
256
|
-
/* @__PURE__ */ y("div", {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
275
|
+
/* @__PURE__ */ y("div", {
|
|
276
|
+
id: "analytics-performance",
|
|
277
|
+
className: "scroll-mt-6",
|
|
278
|
+
children: [/* @__PURE__ */ v("h2", {
|
|
279
|
+
className: "mb-4 text-lg font-semibold text-text-primary",
|
|
280
|
+
children: "Monthly Performance"
|
|
281
|
+
}), /* @__PURE__ */ v(a, {
|
|
282
|
+
columns: [
|
|
283
|
+
{
|
|
284
|
+
key: "month",
|
|
285
|
+
header: "Month",
|
|
286
|
+
render: (e) => /* @__PURE__ */ v("span", {
|
|
287
|
+
className: "font-medium text-text-primary",
|
|
288
|
+
children: e.month
|
|
289
|
+
})
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
key: "revenue",
|
|
293
|
+
header: "Revenue",
|
|
294
|
+
render: (e) => /* @__PURE__ */ v("span", {
|
|
295
|
+
className: "font-medium text-emerald-600 dark:text-emerald-400",
|
|
296
|
+
children: t(e.revenue)
|
|
297
|
+
})
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
key: "expenses",
|
|
301
|
+
header: "Expenses",
|
|
302
|
+
render: (e) => /* @__PURE__ */ v("span", {
|
|
303
|
+
className: "font-medium text-red-600 dark:text-red-400",
|
|
304
|
+
children: t(e.expenses)
|
|
305
|
+
})
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
key: "net",
|
|
309
|
+
header: "Net Income",
|
|
310
|
+
sortable: !1,
|
|
311
|
+
render: (n) => {
|
|
312
|
+
let r = n.revenue - n.expenses;
|
|
313
|
+
return /* @__PURE__ */ v("span", {
|
|
314
|
+
className: e("font-medium", r >= 0 ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"),
|
|
315
|
+
children: t(r)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
key: "margin",
|
|
321
|
+
header: "Margin",
|
|
322
|
+
sortable: !1,
|
|
323
|
+
render: (t) => {
|
|
324
|
+
let n = t.revenue > 0 ? (t.revenue - t.expenses) / t.revenue * 100 : 0;
|
|
325
|
+
return /* @__PURE__ */ y("span", {
|
|
326
|
+
className: e("font-medium", n >= 0 ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"),
|
|
327
|
+
children: [n.toFixed(1), "%"]
|
|
328
|
+
});
|
|
329
|
+
}
|
|
307
330
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
})
|
|
331
|
+
],
|
|
332
|
+
data: C.revenueByMonth,
|
|
333
|
+
keyField: "month",
|
|
334
|
+
pageSize: 12
|
|
335
|
+
})]
|
|
336
|
+
})
|
|
314
337
|
]
|
|
315
338
|
});
|
|
316
339
|
}
|
|
317
340
|
//#endregion
|
|
318
|
-
export {
|
|
341
|
+
export { C as AnalyticsPage };
|
|
319
342
|
|
|
320
343
|
//# sourceMappingURL=AnalyticsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/housingvista/pages/analytics/AnalyticsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n BarChart3,\n TrendingUp,\n DollarSign,\n Building,\n Activity,\n Calendar,\n Download,\n PieChart,\n ArrowUpRight,\n ArrowDownRight,\n Clock,\n} from 'lucide-react';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { ChartCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { useAnalyticsData } from '../../hooks';\nimport { cn, formatCurrency, formatNumber } from '../../utils/helpers';\n\ntype TimePeriod = 'week' | 'month' | 'quarter' | 'year';\n\nconst periodTabs: { value: TimePeriod; 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 const { data: analyticsData } = useAnalyticsData();\n const [selectedPeriod, setSelectedPeriod] = useState<TimePeriod>('month');\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) => (\n <span className=\"font-medium text-emerald-600 dark:text-emerald-400\">{formatCurrency(item.revenue)}</span>\n ),\n },\n {\n key: 'expenses',\n header: 'Expenses',\n render: (item) => (\n <span className=\"font-medium text-red-600 dark:text-red-400\">{formatCurrency(item.expenses)}</span>\n ),\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\n className={cn(\n 'font-medium',\n net >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\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\n className={cn(\n 'font-medium',\n margin >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {margin.toFixed(1)}%\n </span>\n );\n },\n },\n ];\n\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Track your property portfolio performance and market insights\"\n actions={\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 Report\n </button>\n }\n >\n {/* Time Period Selector */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <span className=\"text-sm font-medium text-text-secondary\">Period:</span>\n {periodTabs.map((tab) => (\n <button\n key={tab.value}\n onClick={() => setSelectedPeriod(tab.value)}\n className={cn(\n 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',\n selectedPeriod === tab.value\n ? 'bg-foreground text-background'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {tab.label}\n </button>\n ))}\n </div>\n\n {/* Stats Overview Row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {analyticsData.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' ? kpi.value : `${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\n {/* Charts Section */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Revenue Chart */}\n <ChartCard title=\"Revenue vs Expenses\" description=\"Monthly revenue and expense trends\">\n <div className=\"space-y-3\">\n {/* Simple bar representation */}\n {analyticsData.revenueByMonth.slice(-6).map((item) => {\n const maxVal = Math.max(...analyticsData.revenueByMonth.map((r) => r.revenue));\n const revWidth = (item.revenue / maxVal) * 100;\n const expWidth = (item.expenses / maxVal) * 100;\n return (\n <div key={item.month} className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"font-medium text-text-primary\">{item.month}</span>\n <span className=\"text-text-secondary\">\n {formatCurrency(item.revenue)} / {formatCurrency(item.expenses)}\n </span>\n </div>\n <div className=\"flex gap-1\">\n <div className=\"h-2 rounded-full bg-emerald-500\" style={{ width: `${revWidth}%` }} />\n </div>\n <div className=\"flex gap-1\">\n <div className=\"h-2 rounded-full bg-red-400\" style={{ width: `${expWidth}%` }} />\n </div>\n </div>\n );\n })}\n <div className=\"flex items-center gap-4 pt-2 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-emerald-500\" />\n Revenue\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-red-400\" />\n Expenses\n </span>\n </div>\n </div>\n </ChartCard>\n\n {/* Property Distribution */}\n <ChartCard title=\"Property Type Distribution\" description=\"Portfolio breakdown by property type\">\n <div className=\"space-y-4\">\n {analyticsData.propertyTypeDistribution.map((item) => {\n const totalValue = analyticsData.propertyTypeDistribution.reduce((s, d) => s + d.value, 0);\n const pct = totalValue > 0 ? (item.value / totalValue) * 100 : 0;\n const colors: Record<string, string> = {\n Residential: 'bg-blue-500',\n Commercial: 'bg-purple-500',\n Agricultural: 'bg-emerald-500',\n };\n return (\n <div key={item.type} className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <span className={cn('h-3 w-3 rounded-full', colors[item.type] || 'bg-muted-foreground')} />\n <span className=\"font-medium text-text-primary\">{item.type}</span>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"text-text-secondary\">{item.count} properties</span>\n <span className=\"font-medium text-text-primary\">{formatCurrency(item.value)}</span>\n </div>\n </div>\n <div className=\"h-2 w-full rounded-full bg-bg-sunken\">\n <div\n className={cn('h-2 rounded-full', colors[item.type] || 'bg-muted-foreground')}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n })}\n </div>\n </ChartCard>\n\n {/* Occupancy Rate */}\n <ChartCard title=\"Occupancy Rate Trend\" description=\"Monthly occupancy rate percentage\">\n <div className=\"space-y-3\">\n {analyticsData.occupancyRate.slice(-6).map((item, idx, arr) => {\n const prev = idx > 0 ? arr[idx - 1].rate : item.rate;\n const change = item.rate - prev;\n return (\n <div key={item.month} className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">{item.month}</span>\n <div className=\"flex items-center gap-2\">\n <div className=\"h-2 w-24 rounded-full bg-bg-sunken\">\n <div className=\"h-2 rounded-full bg-blue-500\" style={{ width: `${item.rate}%` }} />\n </div>\n <span className=\"w-12 text-right text-sm font-semibold text-text-primary\">{item.rate}%</span>\n {change !== 0 && (\n <span\n className={cn('flex items-center text-xs', change > 0 ? 'text-emerald-600' : 'text-red-600')}\n >\n {change > 0 ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {Math.abs(change)}\n </span>\n )}\n </div>\n </div>\n );\n })}\n </div>\n </ChartCard>\n\n {/* Recent Activity */}\n <ChartCard title=\"Quick Stats\" description=\"Key performance indicators at a glance\">\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 (YTD)</p>\n <p className=\"text-lg font-bold text-text-primary\">\n {formatCurrency(analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0))}\n </p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm text-text-secondary\">Total Expenses (YTD)</p>\n <p className=\"text-lg font-bold text-text-primary\">\n {formatCurrency(analyticsData.revenueByMonth.reduce((s, r) => s + r.expenses, 0))}\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg bg-emerald-50 p-3 text-center dark:bg-emerald-900/20\">\n <p className=\"text-xl font-bold text-emerald-600 dark:text-emerald-400\">\n {formatCurrency(\n analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0) -\n analyticsData.revenueByMonth.reduce((s, r) => s + r.expenses, 0)\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">Net Income</p>\n </div>\n <div className=\"rounded-lg bg-blue-50 p-3 text-center dark:bg-blue-900/20\">\n <p className=\"text-xl font-bold text-blue-600 dark:text-blue-400\">\n {(\n ((analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0) -\n analyticsData.revenueByMonth.reduce((s, r) => s + r.expenses, 0)) /\n analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0)) *\n 100\n ).toFixed(1)}\n %\n </p>\n <p className=\"text-xs text-text-secondary\">Profit Margin</p>\n </div>\n </div>\n </div>\n </ChartCard>\n </div>\n\n {/* Performance Metrics Table */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Monthly Performance</h2>\n <DataTable columns={revenueColumns} data={analyticsData.revenueByMonth} keyField=\"month\" pageSize={12} />\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/housingvista/pages/analytics/AnalyticsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n BarChart3,\n TrendingUp,\n DollarSign,\n Building,\n Activity,\n Calendar,\n Download,\n PieChart,\n ArrowUpRight,\n ArrowDownRight,\n Clock,\n} from 'lucide-react';\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 { useAnalyticsData } from '../../hooks';\nimport { cn, formatCurrency, formatNumber } from '../../utils/helpers';\n\ntype TimePeriod = 'week' | 'month' | 'quarter' | 'year';\n\nconst periodTabs: { value: TimePeriod; 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 const { data: analyticsData } = useAnalyticsData();\n const [selectedPeriod, setSelectedPeriod] = useState<TimePeriod>('month');\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) => (\n <span className=\"font-medium text-emerald-600 dark:text-emerald-400\">{formatCurrency(item.revenue)}</span>\n ),\n },\n {\n key: 'expenses',\n header: 'Expenses',\n render: (item) => (\n <span className=\"font-medium text-red-600 dark:text-red-400\">{formatCurrency(item.expenses)}</span>\n ),\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\n className={cn(\n 'font-medium',\n net >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\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\n className={cn(\n 'font-medium',\n margin >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {margin.toFixed(1)}%\n </span>\n );\n },\n },\n ];\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 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 Report\n </button>\n }\n >\n <NextSteps\n storageKey=\"housingvista-analytics\"\n steps={[\n {\n id: 'period-year',\n label: 'See the full-year picture',\n description: 'Switch the period to Year to view long-term trends.',\n onClick: () => setSelectedPeriod('year'),\n },\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 {/* Time Period Selector */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <Calendar className=\"h-4 w-4 text-text-secondary\" />\n <span className=\"text-sm font-medium text-text-secondary\">Period:</span>\n {periodTabs.map((tab) => (\n <button\n key={tab.value}\n onClick={() => setSelectedPeriod(tab.value)}\n className={cn(\n 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',\n selectedPeriod === tab.value\n ? 'bg-foreground text-background'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {tab.label}\n </button>\n ))}\n </div>\n\n {/* Stats Overview Row — 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 {analyticsData.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' ? kpi.value : `${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 {/* Charts Section */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Revenue Chart */}\n <ChartCard title=\"Revenue vs Expenses\" description=\"Monthly revenue and expense trends\">\n <div className=\"space-y-3\">\n {/* Simple bar representation */}\n {analyticsData.revenueByMonth.slice(-6).map((item) => {\n const maxVal = Math.max(...analyticsData.revenueByMonth.map((r) => r.revenue));\n const revWidth = (item.revenue / maxVal) * 100;\n const expWidth = (item.expenses / maxVal) * 100;\n return (\n <div key={item.month} className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"font-medium text-text-primary\">{item.month}</span>\n <span className=\"text-text-secondary\">\n {formatCurrency(item.revenue)} / {formatCurrency(item.expenses)}\n </span>\n </div>\n <div className=\"flex gap-1\">\n <div className=\"h-2 rounded-full bg-emerald-500\" style={{ width: `${revWidth}%` }} />\n </div>\n <div className=\"flex gap-1\">\n <div className=\"h-2 rounded-full bg-red-400\" style={{ width: `${expWidth}%` }} />\n </div>\n </div>\n );\n })}\n <div className=\"flex items-center gap-4 pt-2 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-emerald-500\" />\n Revenue\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-red-400\" />\n Expenses\n </span>\n </div>\n </div>\n </ChartCard>\n\n {/* Property Distribution */}\n <ChartCard title=\"Property Type Distribution\" description=\"Portfolio breakdown by property type\">\n <div className=\"space-y-4\">\n {analyticsData.propertyTypeDistribution.map((item) => {\n const totalValue = analyticsData.propertyTypeDistribution.reduce((s, d) => s + d.value, 0);\n const pct = totalValue > 0 ? (item.value / totalValue) * 100 : 0;\n const colors: Record<string, string> = {\n Residential: 'bg-blue-500',\n Commercial: 'bg-purple-500',\n Agricultural: 'bg-emerald-500',\n };\n return (\n <div key={item.type} className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <span className={cn('h-3 w-3 rounded-full', colors[item.type] || 'bg-muted-foreground')} />\n <span className=\"font-medium text-text-primary\">{item.type}</span>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"text-text-secondary\">{item.count} properties</span>\n <span className=\"font-medium text-text-primary\">{formatCurrency(item.value)}</span>\n </div>\n </div>\n <div className=\"h-2 w-full rounded-full bg-bg-sunken\">\n <div\n className={cn('h-2 rounded-full', colors[item.type] || 'bg-muted-foreground')}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n })}\n </div>\n </ChartCard>\n\n {/* Occupancy Rate */}\n <ChartCard title=\"Occupancy Rate Trend\" description=\"Monthly occupancy rate percentage\">\n <div className=\"space-y-3\">\n {analyticsData.occupancyRate.slice(-6).map((item, idx, arr) => {\n const prev = idx > 0 ? arr[idx - 1].rate : item.rate;\n const change = item.rate - prev;\n return (\n <div key={item.month} className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">{item.month}</span>\n <div className=\"flex items-center gap-2\">\n <div className=\"h-2 w-24 rounded-full bg-bg-sunken\">\n <div className=\"h-2 rounded-full bg-blue-500\" style={{ width: `${item.rate}%` }} />\n </div>\n <span className=\"w-12 text-right text-sm font-semibold text-text-primary\">{item.rate}%</span>\n {change !== 0 && (\n <span\n className={cn('flex items-center text-xs', change > 0 ? 'text-emerald-600' : 'text-red-600')}\n >\n {change > 0 ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {Math.abs(change)}\n </span>\n )}\n </div>\n </div>\n );\n })}\n </div>\n </ChartCard>\n\n {/* Recent Activity */}\n <ChartCard title=\"Quick Stats\" description=\"Key performance indicators at a glance\">\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 (YTD)</p>\n <p className=\"text-lg font-bold text-text-primary\">\n {formatCurrency(analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0))}\n </p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm text-text-secondary\">Total Expenses (YTD)</p>\n <p className=\"text-lg font-bold text-text-primary\">\n {formatCurrency(analyticsData.revenueByMonth.reduce((s, r) => s + r.expenses, 0))}\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg bg-emerald-50 p-3 text-center dark:bg-emerald-900/20\">\n <p className=\"text-xl font-bold text-emerald-600 dark:text-emerald-400\">\n {formatCurrency(\n analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0) -\n analyticsData.revenueByMonth.reduce((s, r) => s + r.expenses, 0)\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">Net Income</p>\n </div>\n <div className=\"rounded-lg bg-blue-50 p-3 text-center dark:bg-blue-900/20\">\n <p className=\"text-xl font-bold text-blue-600 dark:text-blue-400\">\n {(\n ((analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0) -\n analyticsData.revenueByMonth.reduce((s, r) => s + r.expenses, 0)) /\n analyticsData.revenueByMonth.reduce((s, r) => s + r.revenue, 0)) *\n 100\n ).toFixed(1)}\n %\n </p>\n <p className=\"text-xs text-text-secondary\">Profit Margin</p>\n </div>\n </div>\n </div>\n </ChartCard>\n </div>\n\n {/* Performance Metrics Table */}\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={analyticsData.revenueByMonth} keyField=\"month\" pageSize={12} />\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|