diff --git a/src/data/selectors.js b/src/data/selectors.js index 731bb3e..f18f21d 100644 --- a/src/data/selectors.js +++ b/src/data/selectors.js @@ -31,18 +31,22 @@ export function winRate(kanbanLeads = []) { return { won, lost, decided, rate: decided ? Math.round((won / decided) * 100) : 0 }; } -// Earned (percentage-of-completion) P&L. Revenue and commission are recognized in proportion to -// work done (completionPercentage); cost is what's actually been incurred. This is the correct -// basis for in-progress construction: the previous approach compared cash-collected client -// payments against fully-incurred cost, which made gross profit spuriously negative mid-project. -// `received` is still exposed for any cash-basis view that wants money actually collected. +// Earned P&L on a COST-TO-COST percentage-of-completion basis (the construction-accounting standard): +// for each job the earned fraction = cost incurred / projected total cost (committedCost), so revenue +// and commission are recognized at the job's TRUE margin. This avoids the distortion of comparing +// work-%-recognized revenue against front-loaded cost (which made gross profit spuriously thin/negative +// mid-project). A genuinely over-budget job (committed > budget) still earns < its cost → real loss. +// `received` is also exposed for any cash-basis view that wants money actually collected. export function revenueSummary(projects = []) { let revenue = 0, cost = 0, commissions = 0, received = 0; for (const p of projects) { - const pct = Math.min(Math.max(Number(p.completionPercentage || 0) / 100, 0), 1); const budget = Number(p.approvedBudget ?? p.budget ?? 0); + const ac = Number(p.actualCost ?? p.spent ?? 0); + const committed = Number(p.committedCost ?? ac) || ac; + // earned fraction: cost-to-cost (capped at 1); fall back to work-% only if no committed cost exists. + const pct = committed > 0 ? Math.min(ac / committed, 1) : Math.min(Math.max(Number(p.completionPercentage || 0) / 100, 0), 1); revenue += Math.round(pct * budget); - cost += Number(p.actualCost ?? p.spent ?? 0); + cost += ac; commissions += Math.round(pct * Number(p.commission || 0)); received += sum((p.paymentSchedule || []).filter((ps) => ps.status === 'paid'), (ps) => ps.amount); }