feat(analytics): add subcontractor project scoping selectors (permitted-subset project view)

This commit is contained in:
Satyam Rastogi
2026-05-29 20:01:33 +05:30
parent 777af1ed70
commit 5ce956b37a
+52
View File
@@ -167,3 +167,55 @@ export function repLeaderboard(projects = [], resolvePerson) {
} }
return Object.values(by).sort((a, b) => b.sales - a.sales); 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 },
};
}