feat(analytics): add subcontractor project scoping selectors (permitted-subset project view)
This commit is contained in:
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user