feat(owner): ground Financial Overview cards + drill-down modal in real records (collected/AR/payouts/vendor-spend)

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