feat: Nearshore-Ratio indicator per project
Engine (packages/engine): - calculateShoringRatio() pure function: onshore/offshore hours, country breakdown, threshold check, weighted by hours not headcount - 12 unit tests: empty, 100% onshore/offshore, mixed ratios, custom threshold, case-insensitive, unknown country, FTE weighting Schema: - Project.shoringThreshold (default 55%) — per-project configurable - Project.onshoreCountryCode (default "DE") — configurable onshore country API (project router): - getShoringRatio query: loads assignments with resource.country, computes ratio, returns full breakdown - update mutation: accepts shoringThreshold + onshoreCountryCode UI: - ShoringIndicator: stacked horizontal bar with country segments, severity badge (green/yellow/red), hover tooltip, dark theme - ShoringBadge: mini colored dot + % for project list column - ProjectModal: "Max Offshore %" number input - Project detail: indicator after budget status card - Project list: "Shoring" column (default hidden, toggleable) AI Assistant: - get_shoring_ratio tool: human-readable breakdown with threshold alert Colors: green (<threshold-10), yellow (threshold-10 to threshold), red (>=threshold) Default: 55% offshore threshold, "DE" as onshore country Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -1385,6 +1385,20 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_shoring_ratio",
|
||||
description: "Get the onshore/offshore staffing ratio for a project. Shows the percentage of work hours allocated to each country, whether the project exceeds its nearshore threshold, and a full country breakdown.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
@@ -5520,6 +5534,52 @@ const executors = {
|
||||
|
||||
return `Change history for ${params.entityType} "${entityName}" (${entries.length} entries):\n\n${lines.join("\n")}`;
|
||||
},
|
||||
|
||||
async get_shoring_ratio(params: { projectId: string }, ctx: ToolContext) {
|
||||
const sel = { id: true, name: true, shortCode: true, shoringThreshold: true, onshoreCountryCode: true } as const;
|
||||
let project = await ctx.db.project.findUnique({ where: { id: params.projectId }, select: sel });
|
||||
if (!project) {
|
||||
project = await ctx.db.project.findUnique({ where: { shortCode: params.projectId }, select: sel });
|
||||
}
|
||||
if (!project) return { error: `Project not found: ${params.projectId}` };
|
||||
|
||||
const assignments = await ctx.db.assignment.findMany({
|
||||
where: { projectId: project.id, status: { not: "CANCELLED" } },
|
||||
include: { resource: { include: { country: { select: { code: true } } } } },
|
||||
});
|
||||
|
||||
if (assignments.length === 0) {
|
||||
return `Project "${project.name}" (${project.shortCode}): No active assignments — shoring ratio not available.`;
|
||||
}
|
||||
|
||||
const { calculateShoringRatio: calcShoring } = await import("@planarchy/engine/allocation");
|
||||
|
||||
const mapped = assignments.map((a) => {
|
||||
const start = new Date(a.startDate);
|
||||
const end = new Date(a.endDate);
|
||||
const diffDays = Math.max(1, Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1);
|
||||
const workingDays = Math.max(1, Math.round(diffDays / 7 * 5));
|
||||
return {
|
||||
resourceId: a.resourceId,
|
||||
countryCode: a.resource.country?.code ?? null,
|
||||
hoursPerDay: a.hoursPerDay,
|
||||
workingDays,
|
||||
};
|
||||
});
|
||||
|
||||
const threshold = project.shoringThreshold ?? 55;
|
||||
const onshoreCode = project.onshoreCountryCode ?? "DE";
|
||||
const result = calcShoring(mapped, threshold, onshoreCode);
|
||||
|
||||
const countryParts = Object.entries(result.byCountry)
|
||||
.sort((a, b) => b[1].pct - a[1].pct)
|
||||
.map(([code, info]) => `${code} ${info.pct}% (${info.resourceCount} people)`)
|
||||
.join(", ");
|
||||
|
||||
const warning = result.isAboveThreshold ? ` -- Above ${threshold}% offshore threshold!` : "";
|
||||
|
||||
return `Project "${project.name}" (${project.shortCode}): ${result.onshoreRatio}% onshore (${onshoreCode}), ${result.offshoreRatio}% offshore. Breakdown: ${countryParts}.${warning}${result.unknownCount > 0 ? ` (${result.unknownCount} resource(s) without country)` : ""}`;
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Executor ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user