diff --git a/src/data/selectors.js b/src/data/selectors.js index 2a6ad2a..805a342 100644 --- a/src/data/selectors.js +++ b/src/data/selectors.js @@ -31,13 +31,22 @@ export function winRate(kanbanLeads = []) { return { won, lost, decided, rate: decided ? Math.round((won / decided) * 100) : 0 }; } -// Revenue recognized = Σ client payments received (paymentSchedule status 'paid'); cost = Σ actualCost. +// 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. export function revenueSummary(projects = []) { - const revenue = sum(projects, (p) => - sum((p.paymentSchedule || []).filter((ps) => ps.status === 'paid'), (ps) => ps.amount)); - const cost = sum(projects, (p) => Number(p.actualCost ?? p.spent ?? 0)); - const commissions = sum(projects, (p) => Number(p.commission || 0)); - return { revenue, cost, grossProfit: revenue - cost, commissions, netProfit: revenue - cost - commissions }; + 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); + revenue += Math.round(pct * budget); + cost += Number(p.actualCost ?? p.spent ?? 0); + commissions += Math.round(pct * Number(p.commission || 0)); + received += sum((p.paymentSchedule || []).filter((ps) => ps.status === 'paid'), (ps) => ps.amount); + } + return { revenue, received, cost, grossProfit: revenue - cost, commissions, netProfit: revenue - cost - commissions }; } // Commission grouped by the project's sales rep. resolvePerson(id)->{name} optional, for display.