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);
|
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