From 6a5b857de3369d4d9bc1a4eaeaf1f76e6294f3b9 Mon Sep 17 00:00:00 2001 From: Satyam Rastogi Date: Fri, 29 May 2026 18:49:11 +0530 Subject: [PATCH] feat(owner): ground Financial Overview cards + drill-down modal in real records (collected/AR/payouts/vendor-spend) --- .../owner/FinancialDetailsModal.jsx | 99 +++++++++---------- src/components/owner/FinancialKPICards.jsx | 64 ++++-------- 2 files changed, 68 insertions(+), 95 deletions(-) diff --git a/src/components/owner/FinancialDetailsModal.jsx b/src/components/owner/FinancialDetailsModal.jsx index 6fdbd54..3e14875 100644 --- a/src/components/owner/FinancialDetailsModal.jsx +++ b/src/components/owner/FinancialDetailsModal.jsx @@ -2,9 +2,11 @@ import React, { useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { X, ArrowUpRight, ArrowDownRight, Filter, Download, Search, CheckCircle, Clock, AlertCircle } from 'lucide-react'; import { useMockStore } from '../../data/mockStore'; +import { DEMO_TODAY } from '../../data/mockStore'; +import { financialOverview } from '../../data/selectors'; const FinancialDetailsModal = ({ isOpen, onClose, type, ownerId }) => { - const { projects: allProjects, vendors } = useMockStore(); + const { projects: allProjects } = useMockStore(); const projects = ownerId ? allProjects.filter(p => p.ownerId === ownerId) : allProjects; const [searchTerm, setSearchTerm] = useState(''); const [sortConfig, setSortConfig] = useState({ key: 'amount', direction: 'desc' }); @@ -25,71 +27,63 @@ const FinancialDetailsModal = ({ isOpen, onClose, type, ownerId }) => { // --- Data Derivation Logic --- + const fin = useMemo(() => financialOverview(projects, DEMO_TODAY), [projects]); + const data = useMemo(() => { let items = []; let summary = { total: 0, label: '' }; switch (type) { case 'revenue': - summary.label = 'Total Revenue'; - // Get all active/completed projects - items = projects - .filter(p => p.status === 'active' || p.status === 'completed') - .map(p => ({ - id: p.id, - name: p.address, // Project Name - entity: 'Client', - date: p.startDate, - status: p.status, - amount: p.budget, - type: 'Project Budget' - })); + summary.label = 'Total Collected'; + items = fin.collected.items.map((it, idx) => ({ + id: `${it.projectId}-R${idx}`, + name: it.project, + entity: 'Client', + date: it.date, + status: 'Paid', + amount: it.amount, + type: it.label + })); break; case 'ar': summary.label = 'Outstanding AR'; - // Mock invoices from projects - items = projects.flatMap(p => - (p.invoices || []).filter(i => i.status === 'pending').map(i => ({ - id: i.id, - name: p.address, - entity: 'Client', - date: i.dueDate, - status: 'Overdue', // Mock status - amount: i.amount, - type: 'Invoice' - })) - ); + items = fin.outstandingAR.items.map((it, idx) => ({ + id: `${it.projectId}-AR${idx}`, + name: it.project, + entity: 'Client', + date: it.dueDate, + status: 'Pending', + amount: it.amount, + type: it.label + })); break; case 'payouts': summary.label = 'Pending Payouts'; - // Mock vendor bills from project milestones - items = projects.flatMap(p => - (p.milestones || []).filter(m => m.status === 'completed').map(m => ({ - id: `BILL-${m.id}`, - name: vendors.find(v => v.id === m.assignedTo)?.vendorName || 'Unknown Vendor', - entity: 'Vendor', - date: m.dueDate, - status: 'Pending Approval', - amount: p.budget * 0.05, // Mock 5% per milestone - type: 'Milestone Payment' - })) - ); + items = fin.pendingPayouts.items.map((it, idx) => ({ + id: `${it.projectId}-P${idx}`, + name: it.label, + entity: 'Vendor', + date: it.dueDate, + status: 'Pending', + amount: it.amount, + type: 'Vendor Invoice' + })); break; case 'spend': summary.label = 'YTD Vendor Spend'; - // Mock spend from vendors - items = vendors.map(v => ({ - id: v.id, - name: v.vendorName, - entity: v.type, - date: '2024-YTD', + items = fin.vendorSpend.items.map((it, idx) => ({ + id: `${it.projectId}-S${idx}`, + name: it.label, + entity: 'Vendor', + date: it.date, status: 'Paid', - amount: v.spend?.totalSpend || 0, - type: 'Total Spend' - })).filter(i => i.amount > 0); + amount: it.amount, + type: 'Vendor Invoice' + })); break; default: @@ -98,13 +92,14 @@ const FinancialDetailsModal = ({ isOpen, onClose, type, ownerId }) => { summary.total = items.reduce((sum, item) => sum + item.amount, 0); return { items, summary }; - }, [type, projects, vendors]); + }, [type, fin]); // --- Filtering & Sorting --- - const filteredItems = data.items.filter(item => - item.name.toLowerCase().includes(searchTerm.toLowerCase()) || - item.id.toLowerCase().includes(searchTerm.toLowerCase()) - ); + const filteredItems = data.items.filter(item => { + const lc = searchTerm.toLowerCase(); + return (item.name ?? '').toLowerCase().includes(lc) || + (item.id ?? '').toLowerCase().includes(lc); + }); const sortedItems = [...filteredItems].sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { diff --git a/src/components/owner/FinancialKPICards.jsx b/src/components/owner/FinancialKPICards.jsx index e38bf3c..fbf2591 100644 --- a/src/components/owner/FinancialKPICards.jsx +++ b/src/components/owner/FinancialKPICards.jsx @@ -1,48 +1,24 @@ import React, { useMemo } from 'react'; import { useMockStore } from '../../data/mockStore'; +import { DEMO_TODAY } from '../../data/mockStore'; +import { financialOverview } from '../../data/selectors'; import { DollarSign, TrendingUp, TrendingDown, CreditCard } from 'lucide-react'; import { SpotlightCard } from '../SpotlightCard'; const FinancialKPICards = ({ onCardClick, ownerId }) => { - const { projects, vendors } = useMockStore(); + const { projects } = useMockStore(); - // Calculate KPIs derived from mock data — filtered by ownerId when provided - const metrics = useMemo(() => { - const ownerProjects = ownerId ? projects.filter(p => p.ownerId === ownerId) : projects; - - let totalRevenue = 0; - let pendingPayouts = 0; - - ownerProjects.forEach(project => { - if (project.status === 'active' || project.status === 'completed') { - totalRevenue += project.budget; - } - - if (project.invoices) { - project.invoices.forEach(inv => { - if (inv.status === 'pending') { - pendingPayouts += inv.amount; - } - }); - } - }); - - const totalVendorSpend = vendors.reduce((acc, v) => acc + (v.spend?.totalSpend || 0), 0); - - return { - totalRevenue, - outstandingAR: totalRevenue * 0.3, - pendingPayouts, - totalVendorSpend - }; - }, [projects, vendors, ownerId]); + const fin = useMemo( + () => financialOverview(ownerId ? projects.filter(p => p.ownerId === ownerId) : projects, DEMO_TODAY), + [projects, ownerId] + ); const cards = [ { id: 'revenue', - title: 'Revenue This Month', - value: `$${(metrics.totalRevenue / 1000).toFixed(1)}k`, - change: '+12%', + title: 'Collected to Date', + value: `$${(fin.collected.total / 1000).toFixed(1)}k`, + change: `${fin.collected.items.length} payments`, trend: 'up', icon: DollarSign, color: 'text-emerald-400', @@ -52,10 +28,10 @@ const FinancialKPICards = ({ onCardClick, ownerId }) => { { id: 'ar', title: 'Outstanding AR', - value: `$${(metrics.outstandingAR / 1000).toFixed(1)}k`, - change: '5 Pending', + value: `$${(fin.outstandingAR.total / 1000).toFixed(1)}k`, + change: `${fin.outstandingAR.count} pending`, trend: 'neutral', - icon: TrendingUp, // Invoicing icon + icon: TrendingUp, color: 'text-blue-400', bg: 'bg-blue-400/10', border: 'border-blue-400/20' @@ -63,9 +39,11 @@ const FinancialKPICards = ({ onCardClick, ownerId }) => { { id: 'payouts', title: 'Pending Payouts', - value: `$${(metrics.pendingPayouts / 1000).toFixed(1)}k`, - change: 'Due in 7 days', - trend: 'down', // Money going out + value: `$${(fin.pendingPayouts.total / 1000).toFixed(1)}k`, + change: fin.pendingPayouts.dueSoonCount > 0 + ? `${fin.pendingPayouts.dueSoonCount} due in 7d` + : `${fin.pendingPayouts.items.length} pending`, + trend: 'down', icon: CreditCard, color: 'text-amber-400', bg: 'bg-amber-400/10', @@ -74,9 +52,9 @@ const FinancialKPICards = ({ onCardClick, ownerId }) => { { id: 'spend', title: 'YTD Vendor Spend', - value: `$${(metrics.totalVendorSpend / 1000).toFixed(1)}k`, - change: '+8% vs LY', - trend: 'up', // Spending more + value: `$${(fin.vendorSpend.total / 1000).toFixed(1)}k`, + change: `${fin.vendorSpend.items.length} invoices`, + trend: 'up', icon: TrendingDown, color: 'text-purple-400', bg: 'bg-purple-400/10',