Project details: cost & breakdown section shows partial payment details
This commit is contained in:
@@ -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-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,
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
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 { SpotlightCard } from '../../components/SpotlightCard';
|
||||
import { AnimatedCounter } from '../../components/AnimatedCounter';
|
||||
import { motion } from 'framer-motion';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer
|
||||
} from 'recharts';
|
||||
@@ -150,27 +150,37 @@ const inferExpenseType = (category = '') => {
|
||||
};
|
||||
|
||||
function seedExpenseRowsMock(project) {
|
||||
const baseRows = (project?.budgetBreakdown || []).map((b, i) => ({
|
||||
id: `exp_base_${i}`,
|
||||
type: inferExpenseType(b.category),
|
||||
name: b.name || '—',
|
||||
category: b.category || '',
|
||||
allocated: Number(b.allocated) || 0,
|
||||
actual: Number(b.actual) || 0,
|
||||
paid: Number(b.actual) > 0 && Number(b.actual) >= Number(b.allocated) * 0.9,
|
||||
}));
|
||||
const baseRows = (project?.budgetBreakdown || []).map((b, i) => {
|
||||
const id = `exp_base_${i}`;
|
||||
const type = inferExpenseType(b.category);
|
||||
const name = b.name || '—';
|
||||
const category = b.category || '';
|
||||
const allocated = Number(b.allocated) || 0;
|
||||
const actual = Number(b.actual) || 0;
|
||||
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 teamRows = [
|
||||
{ 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_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 = [
|
||||
{ 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_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 },
|
||||
].slice(0, 3 + seed);
|
||||
].slice(0, 3 + seed).map(row => ({
|
||||
...row,
|
||||
paymentRequests: generateMockPaymentRequests(row.id, row.actual, row.paid)
|
||||
}));
|
||||
return [...baseRows, ...teamRows, ...subRows];
|
||||
}
|
||||
|
||||
@@ -222,6 +232,70 @@ const OwnerProjectDetail = () => {
|
||||
const [expenseRows, setExpenseRows] = useState([]);
|
||||
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);
|
||||
|
||||
// ── Commission config state ─────────────────────────────────────────────
|
||||
@@ -339,6 +413,7 @@ const OwnerProjectDetail = () => {
|
||||
allocated: amt,
|
||||
actual: amt,
|
||||
paid: false,
|
||||
paymentRequests: [],
|
||||
};
|
||||
setExpenseRows(prev => [newRow, ...prev]);
|
||||
};
|
||||
@@ -379,11 +454,11 @@ const OwnerProjectDetail = () => {
|
||||
const activeCommissionConfig = resolvedCommission;
|
||||
|
||||
const expenseTotals = useMemo(() => {
|
||||
const total = expenseRows.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 total = processedExpenseRows.reduce((s, r) => s + (Number(r.actual) || Number(r.allocated) || 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);
|
||||
return { total, paid, outstanding };
|
||||
}, [expenseRows]);
|
||||
}, [processedExpenseRows]);
|
||||
|
||||
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 };
|
||||
@@ -1072,47 +1147,120 @@ 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 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-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>
|
||||
</thead>
|
||||
<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 TypeIcon = cfg.icon;
|
||||
const isExpanded = expandedRowIds.has(row.id);
|
||||
return (
|
||||
<tr key={row.id} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
|
||||
<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}`}>
|
||||
<TypeIcon size={11} />
|
||||
{cfg.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 font-bold text-sm text-zinc-900 dark:text-white">{row.name || '—'}</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-[#00f0ff]">{formatCurrency(row.actual)}</td>
|
||||
<td className="px-5 py-4 text-center">
|
||||
<label className="inline-flex items-center justify-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!row.paid}
|
||||
onChange={() => togglePaid(row.id)}
|
||||
className="sr-only peer"
|
||||
aria-label={`Mark ${row.name || 'expense'} paid`}
|
||||
/>
|
||||
<span className={`w-5 h-5 rounded-md border flex items-center justify-center transition-colors ${row.paid
|
||||
? 'bg-emerald-500 border-emerald-500 dark:bg-[#39ff14] dark:border-[#39ff14]'
|
||||
: 'bg-white dark:bg-[#18181b] border-zinc-300 dark:border-white/20 hover:border-emerald-400'}`}>
|
||||
{row.paid && <CheckCircle size={12} className="text-white dark:text-black" strokeWidth={3} />}
|
||||
<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">
|
||||
<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} />
|
||||
{cfg.label}
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
<td className="px-5 py-4 font-bold text-sm text-zinc-900 dark:text-white">{row.name || '—'}</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-[#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">
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border ${
|
||||
row.status === 'Paid'
|
||||
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/20'
|
||||
: row.status === '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'
|
||||
}`}>
|
||||
{row.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-center">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
{expenseRows.length === 0 && (
|
||||
{processedExpenseRows.length === 0 && (
|
||||
<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.
|
||||
</td>
|
||||
</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>
|
||||
</td>
|
||||
<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 className="px-5 py-4 text-right font-mono text-sm font-bold text-[#00f0ff]">
|
||||
{formatCurrency(profitMetrics.totalExpenses)}
|
||||
</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">
|
||||
{expenseRows.filter(r => r.paid).length}/{expenseRows.length}
|
||||
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-zinc-600 dark:text-zinc-300">
|
||||
{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>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
Reference in New Issue
Block a user