Project details: cost & breakdown section shows partial payment details

This commit is contained in:
Mayur Shinde
2026-06-04 17:10:47 +05:30
parent 2f1703870a
commit 647bdddaf5
2 changed files with 310 additions and 50 deletions
+109
View File
@@ -10378,3 +10378,112 @@ export const MOCK_STORM_ATTRIBUTION_LEADS = [
{ id: 'SAL-034', firstName: 'Brittany', lastName: 'Baker', address: '12020 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Canvassers', status: 'Contacted', contractValue: null, createdAt: '2025-09-21T09:30:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } }, { id: 'SAL-034', firstName: 'Brittany', lastName: 'Baker', address: '12020 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Canvassers', status: 'Contacted', contractValue: null, createdAt: '2025-09-21T09:30:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } },
{ id: 'SAL-035', firstName: 'Dylan', lastName: 'Nelson', address: '12080 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Canvassers', status: 'New', contractValue: null, createdAt: '2025-09-22T10:45:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } }, { id: 'SAL-035', firstName: 'Dylan', lastName: 'Nelson', address: '12080 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Canvassers', status: 'New', contractValue: null, createdAt: '2025-09-22T10:45:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } },
]; ];
export const generateMockPaymentRequests = (rowId, actual, paid) => {
if (actual <= 0) return [];
// Extract a numeric seed from the rowId (e.g. 'exp_base_0' -> 0, 'exp_team_1' -> 1)
const seedNum = parseInt(rowId.replace(/[^0-9]/g, '')) || 0;
const seed = seedNum % 2;
if (paid) {
if (actual <= 1000) {
return [
{
id: `req_${rowId}_1`,
requestDate: '2026-05-02',
requestedAmount: actual,
amountPaid: actual,
paymentDate: '2026-05-05',
}
];
} else {
const part1 = Math.round(actual * 0.6);
const part2 = actual - part1;
return [
{
id: `req_${rowId}_1`,
requestDate: '2026-04-20',
requestedAmount: part1,
amountPaid: part1,
paymentDate: '2026-04-22',
},
{
id: `req_${rowId}_2`,
requestDate: '2026-05-05',
requestedAmount: part2,
amountPaid: part2,
paymentDate: '2026-05-07',
}
];
}
} else {
if (actual > 2000) {
const part1 = Math.round(actual * 0.5);
const part2 = actual - part1;
if (seed === 1) {
// Return one paid request and one partially paid request
const part2Paid = Math.round(part2 * 0.4);
return [
{
id: `req_${rowId}_1`,
requestDate: '2026-05-10',
requestedAmount: part1,
amountPaid: part1,
paymentDate: '2026-05-12',
},
{
id: `req_${rowId}_2`,
requestDate: '2026-05-20',
requestedAmount: part2,
amountPaid: part2Paid,
paymentDate: '2026-05-22',
}
];
} else {
// Return one paid request and one unpaid request
return [
{
id: `req_${rowId}_1`,
requestDate: '2026-05-10',
requestedAmount: part1,
amountPaid: part1,
paymentDate: '2026-05-12',
},
{
id: `req_${rowId}_2`,
requestDate: '2026-05-20',
requestedAmount: part2,
amountPaid: 0,
paymentDate: null,
}
];
}
} else {
if (seed === 1) {
// Return a single partially paid request
return [
{
id: `req_${rowId}_1`,
requestDate: '2026-05-15',
requestedAmount: actual,
amountPaid: Math.round(actual * 0.5),
paymentDate: '2026-05-18',
}
];
} else {
// Return a single unpaid request
return [
{
id: `req_${rowId}_1`,
requestDate: '2026-05-15',
requestedAmount: actual,
amountPaid: 0,
paymentDate: null,
}
];
}
}
}
};
+188 -37
View File
@@ -1,11 +1,11 @@
import React, { useState, useMemo, useRef, useEffect } from 'react'; import React, { useState, useMemo, useRef, useEffect } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useMockStore } from '../../data/mockStore'; import { useMockStore, generateMockPaymentRequests } from '../../data/mockStore';
import { LIFECYCLE_STAGES, REWORK_STAGE, nextStage, prevStage, stageToPct } from '../../data/lifecycle'; import { LIFECYCLE_STAGES, REWORK_STAGE, nextStage, prevStage, stageToPct } from '../../data/lifecycle';
import { SpotlightCard } from '../../components/SpotlightCard'; import { SpotlightCard } from '../../components/SpotlightCard';
import { AnimatedCounter } from '../../components/AnimatedCounter'; import { AnimatedCounter } from '../../components/AnimatedCounter';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer
} from 'recharts'; } from 'recharts';
@@ -150,27 +150,37 @@ const inferExpenseType = (category = '') => {
}; };
function seedExpenseRowsMock(project) { function seedExpenseRowsMock(project) {
const baseRows = (project?.budgetBreakdown || []).map((b, i) => ({ const baseRows = (project?.budgetBreakdown || []).map((b, i) => {
id: `exp_base_${i}`, const id = `exp_base_${i}`;
type: inferExpenseType(b.category), const type = inferExpenseType(b.category);
name: b.name || '—', const name = b.name || '—';
category: b.category || '', const category = b.category || '';
allocated: Number(b.allocated) || 0, const allocated = Number(b.allocated) || 0;
actual: Number(b.actual) || 0, const actual = Number(b.actual) || 0;
paid: Number(b.actual) > 0 && Number(b.actual) >= Number(b.allocated) * 0.9, const paid = Number(b.actual) > 0 && Number(b.actual) >= Number(b.allocated) * 0.9;
})); return {
id, type, name, category, allocated, actual, paid,
paymentRequests: generateMockPaymentRequests(id, actual, paid)
};
});
const seed = (project?.id || '').charCodeAt((project?.id || 'x').length - 1) % 3; const seed = (project?.id || '').charCodeAt((project?.id || 'x').length - 1) % 3;
const teamRows = [ const teamRows = [
{ id: 'exp_team_1', type: 'team', name: 'Jesus Gonzales', category: 'Sales Rep', allocated: 4200, actual: 4200, paid: true }, { id: 'exp_team_1', type: 'team', name: 'Jesus Gonzales', category: 'Sales Rep', allocated: 4200, actual: 4200, paid: true },
{ id: 'exp_team_2', type: 'team', name: 'Cody Tatum', category: 'Canvasser', allocated: 1500, actual: 1500, paid: false }, { id: 'exp_team_2', type: 'team', name: 'Cody Tatum', category: 'Canvasser', allocated: 1500, actual: 1500, paid: false },
{ id: 'exp_team_3', type: 'team', name: 'Sarah Calloway', category: 'Estimator', allocated: 1200, actual: 0, paid: false }, { id: 'exp_team_3', type: 'team', name: 'Sarah Calloway', category: 'Estimator', allocated: 1200, actual: 0, paid: false },
].slice(0, 2 + seed); ].slice(0, 2 + seed).map(row => ({
...row,
paymentRequests: generateMockPaymentRequests(row.id, row.actual, row.paid)
}));
const subRows = [ const subRows = [
{ id: 'exp_sub_1', type: 'subcontractor', name: 'PlumbPro Services', category: 'Plumbing', allocated: 1800, actual: 1800, paid: true }, { id: 'exp_sub_1', type: 'subcontractor', name: 'PlumbPro Services', category: 'Plumbing', allocated: 1800, actual: 1800, paid: true },
{ id: 'exp_sub_2', type: 'subcontractor', name: 'Bright Electric LLC', category: 'Electrical', allocated: 2400, actual: 2150, paid: false }, { id: 'exp_sub_2', type: 'subcontractor', name: 'Bright Electric LLC', category: 'Electrical', allocated: 2400, actual: 2150, paid: false },
{ id: 'exp_sub_3', type: 'subcontractor', name: 'Premium Painters', category: 'Painting', allocated: 950, actual: 950, paid: true }, { id: 'exp_sub_3', type: 'subcontractor', name: 'Premium Painters', category: 'Painting', allocated: 950, actual: 950, paid: true },
{ id: 'exp_sub_4', type: 'subcontractor', name: 'Solid Civil Works', category: 'Civil work', allocated: 3200, actual: 0, paid: false }, { id: 'exp_sub_4', type: 'subcontractor', name: 'Solid Civil Works', category: 'Civil work', allocated: 3200, actual: 0, paid: false },
].slice(0, 3 + seed); ].slice(0, 3 + seed).map(row => ({
...row,
paymentRequests: generateMockPaymentRequests(row.id, row.actual, row.paid)
}));
return [...baseRows, ...teamRows, ...subRows]; return [...baseRows, ...teamRows, ...subRows];
} }
@@ -222,6 +232,70 @@ const OwnerProjectDetail = () => {
const [expenseRows, setExpenseRows] = useState([]); const [expenseRows, setExpenseRows] = useState([]);
const [expensesSeeded, setExpensesSeeded] = useState(false); const [expensesSeeded, setExpensesSeeded] = useState(false);
// ── Row Expansion state for Cost Breakdown ─────────────────────────────────
const [expandedRowIds, setExpandedRowIds] = useState(new Set());
const toggleRowExpanded = (rowId) => {
setExpandedRowIds(prev => {
const next = new Set(prev);
if (next.has(rowId)) {
next.delete(rowId);
} else {
next.add(rowId);
}
return next;
});
};
// Helper functions for payment requests & status calculations
const getRequestRemaining = (req) => (Number(req.requestedAmount) || 0) - (Number(req.amountPaid) || 0);
const getRequestStatus = (req) => {
const remaining = getRequestRemaining(req);
const paid = Number(req.amountPaid) || 0;
if (remaining <= 0) return 'Paid';
if (paid > 0) return 'Partially Paid';
return 'Unpaid';
};
const getRowTotalPaid = (row) => {
const reqs = row.paymentRequests || [];
return reqs.reduce((sum, r) => sum + (Number(r.amountPaid) || 0), 0);
};
const getRowRemaining = (row) => {
const actual = Number(row.actual) || 0;
const totalPaid = getRowTotalPaid(row);
return Math.max(0, actual - totalPaid);
};
const getRowStatus = (row) => {
const actual = Number(row.actual) || 0;
const totalPaid = getRowTotalPaid(row);
const remaining = getRowRemaining(row);
if (actual === 0) {
return 'Unpaid';
}
if (remaining <= 0) {
return 'Paid';
}
if (totalPaid > 0) {
return 'Partially Paid';
}
return 'Unpaid';
};
const processedExpenseRows = useMemo(() => {
return expenseRows.map(row => {
const remaining = getRowRemaining(row);
const status = getRowStatus(row);
const paid = Number(row.actual) > 0 && remaining === 0;
return {
...row,
remaining,
status,
paid
};
});
}, [expenseRows]);
const [openTooltip, setOpenTooltip] = useState(null); const [openTooltip, setOpenTooltip] = useState(null);
// ── Commission config state ───────────────────────────────────────────── // ── Commission config state ─────────────────────────────────────────────
@@ -339,6 +413,7 @@ const OwnerProjectDetail = () => {
allocated: amt, allocated: amt,
actual: amt, actual: amt,
paid: false, paid: false,
paymentRequests: [],
}; };
setExpenseRows(prev => [newRow, ...prev]); setExpenseRows(prev => [newRow, ...prev]);
}; };
@@ -379,11 +454,11 @@ const OwnerProjectDetail = () => {
const activeCommissionConfig = resolvedCommission; const activeCommissionConfig = resolvedCommission;
const expenseTotals = useMemo(() => { const expenseTotals = useMemo(() => {
const total = expenseRows.reduce((s, r) => s + (Number(r.actual) || Number(r.allocated) || 0), 0); const total = processedExpenseRows.reduce((s, r) => s + (Number(r.actual) || Number(r.allocated) || 0), 0);
const paid = expenseRows.reduce((s, r) => s + (r.paid ? (Number(r.actual) || Number(r.allocated) || 0) : 0), 0); const paid = processedExpenseRows.reduce((s, r) => s + (r.paid ? (Number(r.actual) || Number(r.allocated) || 0) : 0), 0);
const outstanding = Math.max(0, total - paid); const outstanding = Math.max(0, total - paid);
return { total, paid, outstanding }; return { total, paid, outstanding };
}, [expenseRows]); }, [processedExpenseRows]);
const profitMetrics = useMemo(() => { const profitMetrics = useMemo(() => {
if (!project) return { totalCosts: 0, totalExpenses: 0, paidExpenses: 0, outstandingExpenses: 0, totalBudget: 0, commission: 0, commissionFormula: '', grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 }; if (!project) return { totalCosts: 0, totalExpenses: 0, paidExpenses: 0, outstandingExpenses: 0, totalBudget: 0, commission: 0, commissionFormula: '', grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
@@ -1072,15 +1147,22 @@ const OwnerProjectDetail = () => {
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Category / Role</th> <th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Category / Role</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right">Allocated</th> <th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right">Allocated</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right">Actual</th> <th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right">Actual</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-center">Paid</th> <th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right">Remaining</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-center">Status</th>
<th className="w-10"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-zinc-100 dark:divide-white/5"> <tbody className="divide-y divide-zinc-100 dark:divide-white/5">
{expenseRows.map((row) => { {processedExpenseRows.map((row) => {
const cfg = EXPENSE_TYPE_CONFIG[row.type] || EXPENSE_TYPE_CONFIG.other; const cfg = EXPENSE_TYPE_CONFIG[row.type] || EXPENSE_TYPE_CONFIG.other;
const TypeIcon = cfg.icon; const TypeIcon = cfg.icon;
const isExpanded = expandedRowIds.has(row.id);
return ( return (
<tr key={row.id} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors"> <React.Fragment key={row.id}>
<tr
onClick={() => toggleRowExpanded(row.id)}
className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors cursor-pointer"
>
<td className="px-5 py-4"> <td className="px-5 py-4">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border ${cfg.color} ${cfg.bg} ${cfg.border}`}> <span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border ${cfg.color} ${cfg.bg} ${cfg.border}`}>
<TypeIcon size={11} /> <TypeIcon size={11} />
@@ -1091,28 +1173,94 @@ const OwnerProjectDetail = () => {
<td className="px-5 py-4 text-sm text-zinc-500 dark:text-zinc-400">{row.category || '—'}</td> <td className="px-5 py-4 text-sm text-zinc-500 dark:text-zinc-400">{row.category || '—'}</td>
<td className="px-5 py-4 text-right font-mono text-sm text-zinc-600 dark:text-zinc-300">{formatCurrency(row.allocated)}</td> <td className="px-5 py-4 text-right font-mono text-sm text-zinc-600 dark:text-zinc-300">{formatCurrency(row.allocated)}</td>
<td className="px-5 py-4 text-right font-mono text-sm text-[#00f0ff]">{formatCurrency(row.actual)}</td> <td className="px-5 py-4 text-right font-mono text-sm text-[#00f0ff]">{formatCurrency(row.actual)}</td>
<td className="px-5 py-4 text-right font-mono text-sm text-zinc-600 dark:text-zinc-300">{formatCurrency(row.remaining)}</td>
<td className="px-5 py-4 text-center"> <td className="px-5 py-4 text-center">
<label className="inline-flex items-center justify-center cursor-pointer"> <span className={`inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border ${
<input row.status === 'Paid'
type="checkbox" ? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/20'
checked={!!row.paid} : row.status === 'Partially Paid'
onChange={() => togglePaid(row.id)} ? 'text-amber-600 dark:text-[#fda913] bg-amber-50 dark:bg-[#fda913]/10 border-amber-200 dark:border-[#fda913]/20'
className="sr-only peer" : 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border-red-200 dark:border-[#ff003c]/20'
aria-label={`Mark ${row.name || 'expense'} paid`} }`}>
/> {row.status}
<span className={`w-5 h-5 rounded-md border flex items-center justify-center transition-colors ${row.paid </span>
? 'bg-emerald-500 border-emerald-500 dark:bg-[#39ff14] dark:border-[#39ff14]' </td>
: 'bg-white dark:bg-[#18181b] border-zinc-300 dark:border-white/20 hover:border-emerald-400'}`}> <td className="px-5 py-4 text-center">
{row.paid && <CheckCircle size={12} className="text-white dark:text-black" strokeWidth={3} />} <ChevronDownIcon
size={14}
className={`shrink-0 text-zinc-400 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
/>
</td>
</tr>
<AnimatePresence initial={false}>
{isExpanded && (
<tr className="bg-zinc-50/50 dark:bg-white/[0.02]">
<td colSpan={8} className="p-0 border-t border-zinc-200 dark:border-white/5">
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="p-5 bg-zinc-50/40 dark:bg-zinc-950/20">
<h4 className="text-xs font-bold uppercase tracking-widest text-zinc-400 dark:text-zinc-500 mb-3">Payment Request History</h4>
{row.paymentRequests && row.paymentRequests.length > 0 ? (
<div className="overflow-x-auto rounded-xl border border-zinc-200 dark:border-white/5 shadow-sm bg-white dark:bg-[#121214]">
<table className="w-full text-left border-collapse text-xs">
<thead className="bg-zinc-50 dark:bg-zinc-900/50 border-b border-zinc-200 dark:border-white/5">
<tr>
<th className="px-4 py-2.5 font-bold text-zinc-500 dark:text-zinc-400">Request Date</th>
<th className="px-4 py-2.5 font-bold text-zinc-500 dark:text-zinc-400 text-right">Requested Amount</th>
<th className="px-4 py-2.5 font-bold text-zinc-500 dark:text-zinc-400 text-right">Amount Paid</th>
<th className="px-4 py-2.5 font-bold text-zinc-500 dark:text-zinc-400 text-right">Remaining</th>
<th className="px-4 py-2.5 font-bold text-zinc-500 dark:text-zinc-400">Payment Date</th>
<th className="px-4 py-2.5 font-bold text-zinc-500 dark:text-zinc-400 text-center">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
{row.paymentRequests.map((req) => {
const reqRemaining = getRequestRemaining(req);
const reqStatus = getRequestStatus(req);
return (
<tr key={req.id} className="hover:bg-zinc-50/50 dark:hover:bg-white/[0.01] transition-colors">
<td className="px-4 py-3 text-zinc-600 dark:text-zinc-300 font-mono">{req.requestDate || '—'}</td>
<td className="px-4 py-3 text-right font-mono text-zinc-600 dark:text-zinc-300">{formatCurrency(req.requestedAmount)}</td>
<td className="px-4 py-3 text-right font-mono text-[#39ff14]">{formatCurrency(req.amountPaid)}</td>
<td className="px-4 py-3 text-right font-mono text-zinc-500">{formatCurrency(reqRemaining)}</td>
<td className="px-4 py-3 text-zinc-600 dark:text-zinc-300 font-mono">{req.paymentDate || '—'}</td>
<td className="px-4 py-3 text-center">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-widest border ${
reqStatus === 'Paid'
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/20'
: reqStatus === 'Partially Paid'
? 'text-amber-600 dark:text-[#fda913] bg-amber-50 dark:bg-[#fda913]/10 border-amber-200 dark:border-[#fda913]/20'
: 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border-red-200 dark:border-[#ff003c]/20'
}`}>
{reqStatus}
</span> </span>
</label>
</td> </td>
</tr> </tr>
); );
})} })}
{expenseRows.length === 0 && ( </tbody>
</table>
</div>
) : (
<p className="text-zinc-500 dark:text-zinc-400 text-xs italic pl-1">No payment requests recorded for this expense.</p>
)}
</div>
</motion.div>
</td>
</tr>
)}
</AnimatePresence>
</React.Fragment>
);
})}
{processedExpenseRows.length === 0 && (
<tr> <tr>
<td colSpan={6} className="px-5 py-10 text-center text-sm text-zinc-500 dark:text-zinc-400"> <td colSpan={8} className="px-5 py-10 text-center text-sm text-zinc-500 dark:text-zinc-400">
No expenses recorded yet. No expenses recorded yet.
</td> </td>
</tr> </tr>
@@ -1124,13 +1272,16 @@ const OwnerProjectDetail = () => {
<span className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400">Totals</span> <span className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400">Totals</span>
</td> </td>
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-zinc-600 dark:text-zinc-300"> <td className="px-5 py-4 text-right font-mono text-sm font-bold text-zinc-600 dark:text-zinc-300">
{formatCurrency(expenseRows.reduce((s, r) => s + (Number(r.allocated) || 0), 0))} {formatCurrency(processedExpenseRows.reduce((s, r) => s + (Number(r.allocated) || 0), 0))}
</td> </td>
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-[#00f0ff]"> <td className="px-5 py-4 text-right font-mono text-sm font-bold text-[#00f0ff]">
{formatCurrency(profitMetrics.totalExpenses)} {formatCurrency(profitMetrics.totalExpenses)}
</td> </td>
<td className="px-5 py-4 text-center text-[10px] font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest"> <td className="px-5 py-4 text-right font-mono text-sm font-bold text-zinc-600 dark:text-zinc-300">
{expenseRows.filter(r => r.paid).length}/{expenseRows.length} {formatCurrency(processedExpenseRows.reduce((s, r) => s + r.remaining, 0))}
</td>
<td className="px-5 py-4 text-center text-[10px] font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest" colSpan={2}>
{processedExpenseRows.filter(r => r.paid).length}/{processedExpenseRows.length}
</td> </td>
</tr> </tr>
</tfoot> </tfoot>