From 5ce956b37a84e89222d4490ce9c1c3a871e43b8b Mon Sep 17 00:00:00 2001 From: Satyam Rastogi Date: Fri, 29 May 2026 20:01:33 +0530 Subject: [PATCH] feat(analytics): add subcontractor project scoping selectors (permitted-subset project view) --- src/data/selectors.js | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/data/selectors.js b/src/data/selectors.js index f18f21d..3af39d2 100644 --- a/src/data/selectors.js +++ b/src/data/selectors.js @@ -167,3 +167,55 @@ export function repLeaderboard(projects = [], resolvePerson) { } return Object.values(by).sort((a, b) => b.sales - a.sales); } + +// Projects a subcontractor is involved in — union of (a) projects listing them in subcontractorIds +// and (b) projects their tasks reference. Returns light summaries for a "My Projects" list. +export function subcontractorProjects(projects = [], tasks = [], subId) { + const fromTasks = new Set(tasks.filter((t) => t.subcontractorId === subId && t.projectId).map((t) => t.projectId)); + return projects + .filter((p) => (p.subcontractorIds || []).includes(subId) || fromTasks.has(p.id)) + .map((p) => { + const myTasks = tasks.filter((t) => t.subcontractorId === subId && t.projectId === p.id); + return { + id: p.id, + address: p.address, + customerName: p.customerName, + projectType: p.projectType, + status: p.status, + phase: p.phase, + completionPercentage: p.completionPercentage, + taskCount: myTasks.length, + openTaskCount: myTasks.filter((t) => !['Completed', 'Cancelled'].includes(t.status)).length, + }; + }); +} + +// The PERMITTED SUBSET of a single project for a subcontractor. Deliberately omits owner-private +// fields (budget, committedCost, margins, commission, other subs, all invoices, estimates, +// changeOrders, risks). Includes the schedule (milestones) read-only + the sub's own tasks/payments. +// This selector is the single guard that keeps owner-private data out of the subcontractor view. +export function scopedProjectForSub(project, subId, tasks = []) { + if (!project) return null; + const myTasks = tasks.filter((t) => t.subcontractorId === subId && t.projectId === project.id); + const sumFees = (t) => (t.fees || []).reduce((s, f) => s + (f.amount || 0), 0); + const sumExpenses = (t) => (t.expenses || []).reduce((s, e) => s + (e.amount || 0), 0); + const myEarnings = myTasks.reduce((s, t) => s + sumFees(t) + sumExpenses(t), 0); + const paid = myTasks.filter((t) => t.paymentStatus === 'Paid').reduce((s, t) => s + sumFees(t) + sumExpenses(t), 0); + return { + id: project.id, + address: project.address, + customerName: project.customerName, + projectType: project.projectType, + status: project.status, + phase: project.phase, + lifecycleStage: project.lifecycleStage, + completionPercentage: project.completionPercentage, + startDate: project.startDate, + endDate: project.endDate, + // schedule (read-only) — names/dates/status only; no costs + milestones: (project.milestones || []).map((m) => ({ id: m.id, name: m.name, dueDate: m.dueDate, status: m.status })), + // the sub's own scope on this job + myTasks: myTasks.map((t) => ({ id: t.id, title: t.title, status: t.status, dueDate: t.dueDate, category: t.category })), + myPayments: { earnings: myEarnings, paid, unpaid: myEarnings - paid }, + }; +}