244 lines
11 KiB
TypeScript
244 lines
11 KiB
TypeScript
export interface AssistantInsightMetric {
|
|
label: string;
|
|
value: string;
|
|
tone?: "neutral" | "good" | "warn" | "danger" | "info";
|
|
}
|
|
|
|
export interface AssistantInsightSection {
|
|
title: string;
|
|
metrics: AssistantInsightMetric[];
|
|
}
|
|
|
|
export interface AssistantInsight {
|
|
kind: "chargeability" | "resource_match" | "holiday_region" | "resource_holidays";
|
|
title: string;
|
|
subtitle?: string;
|
|
metrics: AssistantInsightMetric[];
|
|
sections?: AssistantInsightSection[];
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function asString(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim() ? value : null;
|
|
}
|
|
|
|
function asNumber(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
function formatHours(value: unknown): string | null {
|
|
const num = asNumber(value);
|
|
return num == null ? null : `${num.toFixed(num % 1 === 0 ? 0 : 1)} h`;
|
|
}
|
|
|
|
function formatDays(value: unknown): string | null {
|
|
const num = asNumber(value);
|
|
return num == null ? null : `${num.toFixed(num % 1 === 0 ? 0 : 1)} d`;
|
|
}
|
|
|
|
function pushMetric(
|
|
metrics: AssistantInsightMetric[],
|
|
label: string,
|
|
value: string | null,
|
|
tone?: AssistantInsightMetric["tone"],
|
|
) {
|
|
if (!value) return;
|
|
metrics.push({ label, value, ...(tone ? { tone } : {}) });
|
|
}
|
|
|
|
function createLocationLabel(locationContext: Record<string, unknown> | undefined): string | null {
|
|
if (!locationContext) return null;
|
|
const parts = [
|
|
asString(locationContext.metroCity),
|
|
asString(locationContext.federalState),
|
|
asString(locationContext.country),
|
|
asString(locationContext.countryCode),
|
|
].filter(Boolean);
|
|
return parts.length > 0 ? parts.join(", ") : null;
|
|
}
|
|
|
|
function buildChargeabilityInsight(data: Record<string, unknown>): AssistantInsight | null {
|
|
const resource = asString(data.resource);
|
|
const month = asString(data.month);
|
|
if (!resource || !month) return null;
|
|
|
|
const holidaySummary = isRecord(data.holidaySummary) ? data.holidaySummary : undefined;
|
|
const absenceSummary = isRecord(data.absenceSummary) ? data.absenceSummary : undefined;
|
|
const capacityBreakdown = isRecord(data.capacityBreakdown) ? data.capacityBreakdown : undefined;
|
|
const locationContext = isRecord(data.locationContext) ? data.locationContext : undefined;
|
|
const chargeabilityPct = asNumber(data.chargeabilityPct);
|
|
const targetPct = asNumber(data.targetPct);
|
|
|
|
const metrics: AssistantInsightMetric[] = [];
|
|
pushMetric(metrics, "Chargeability", asString(data.chargeability), chargeabilityPct == null || targetPct == null
|
|
? "info"
|
|
: chargeabilityPct >= targetPct ? "good" : "warn");
|
|
pushMetric(metrics, "Available", formatHours(data.availableHours));
|
|
pushMetric(metrics, "Booked", formatHours(data.bookedHours));
|
|
pushMetric(metrics, "Unassigned", formatHours(data.unassignedHours));
|
|
pushMetric(metrics, "Target", formatHours(data.targetHours));
|
|
pushMetric(metrics, "Holidays", formatDays(holidaySummary?.workdayCount ?? holidaySummary?.count));
|
|
|
|
const sections: AssistantInsightSection[] = [];
|
|
|
|
const basisMetrics: AssistantInsightMetric[] = [];
|
|
pushMetric(basisMetrics, "Location", createLocationLabel(locationContext), "info");
|
|
pushMetric(basisMetrics, "Base working days", formatDays(data.baseWorkingDays));
|
|
pushMetric(basisMetrics, "Effective working days", formatDays(data.workingDays));
|
|
pushMetric(basisMetrics, "Base capacity", formatHours(data.baseAvailableHours));
|
|
if (basisMetrics.length > 0) {
|
|
sections.push({ title: "Basis", metrics: basisMetrics });
|
|
}
|
|
|
|
const deductionMetrics: AssistantInsightMetric[] = [];
|
|
pushMetric(deductionMetrics, "Holiday deduction", formatHours(holidaySummary?.hoursDeduction ?? capacityBreakdown?.holidayHoursDeduction), "warn");
|
|
pushMetric(deductionMetrics, "Absence deduction", formatHours(absenceSummary?.hoursDeduction ?? capacityBreakdown?.absenceHoursDeduction), "warn");
|
|
pushMetric(deductionMetrics, "Absence days", formatDays(absenceSummary?.dayEquivalent));
|
|
if (deductionMetrics.length > 0) {
|
|
sections.push({ title: "Deductions", metrics: deductionMetrics });
|
|
}
|
|
|
|
return {
|
|
kind: "chargeability",
|
|
title: `${resource} · ${month}`,
|
|
subtitle: "Holiday-aware monthly capacity",
|
|
metrics,
|
|
...(sections.length > 0 ? { sections } : {}),
|
|
};
|
|
}
|
|
|
|
function buildHolidayRegionInsight(data: Record<string, unknown>): AssistantInsight | null {
|
|
const locationContext = isRecord(data.locationContext) ? data.locationContext : undefined;
|
|
const periodStart = asString(data.periodStart);
|
|
const periodEnd = asString(data.periodEnd);
|
|
|
|
const metrics: AssistantInsightMetric[] = [];
|
|
pushMetric(metrics, "Region", createLocationLabel(locationContext), "info");
|
|
pushMetric(metrics, "Resolved holidays", asNumber(data.count)?.toString() ?? null);
|
|
pushMetric(metrics, "Period", periodStart && periodEnd ? `${periodStart} to ${periodEnd}` : null);
|
|
|
|
const summary = isRecord(data.summary) ? data.summary : undefined;
|
|
const scopeItems = Array.isArray(summary?.byScope) ? summary.byScope : [];
|
|
const scopeMetrics = scopeItems
|
|
.map((item) => {
|
|
if (!isRecord(item)) return null;
|
|
const scope = asString(item.scope);
|
|
const count = asNumber(item.count);
|
|
if (!scope || count == null) return null;
|
|
return { label: scope, value: String(count) } satisfies AssistantInsightMetric;
|
|
})
|
|
.filter((item): item is AssistantInsightMetric => item !== null);
|
|
|
|
return {
|
|
kind: "holiday_region",
|
|
title: createLocationLabel(locationContext) ?? "Regional holidays",
|
|
subtitle: "Resolved public holiday set",
|
|
metrics,
|
|
...(scopeMetrics.length > 0 ? { sections: [{ title: "Scopes", metrics: scopeMetrics }] } : {}),
|
|
};
|
|
}
|
|
|
|
function buildResourceHolidayInsight(data: Record<string, unknown>): AssistantInsight | null {
|
|
const resource = isRecord(data.resource) ? data.resource : undefined;
|
|
const summary = isRecord(data.summary) ? data.summary : undefined;
|
|
const periodStart = asString(data.periodStart);
|
|
const periodEnd = asString(data.periodEnd);
|
|
|
|
const metrics: AssistantInsightMetric[] = [];
|
|
pushMetric(metrics, "Employee", asString(resource?.name) ?? asString(resource?.eid));
|
|
pushMetric(metrics, "Location", createLocationLabel(resource), "info");
|
|
pushMetric(metrics, "Resolved holidays", asNumber(data.count)?.toString() ?? null);
|
|
pushMetric(metrics, "Period", periodStart && periodEnd ? `${periodStart} to ${periodEnd}` : null);
|
|
|
|
const scopeItems = Array.isArray(summary?.byScope) ? summary.byScope : [];
|
|
const scopeMetrics = scopeItems
|
|
.map((item) => {
|
|
if (!isRecord(item)) return null;
|
|
const scope = asString(item.scope);
|
|
const count = asNumber(item.count);
|
|
if (!scope || count == null) return null;
|
|
return { label: scope, value: String(count) } satisfies AssistantInsightMetric;
|
|
})
|
|
.filter((item): item is AssistantInsightMetric => item !== null);
|
|
|
|
return {
|
|
kind: "resource_holidays",
|
|
title: `${asString(resource?.name) ?? "Resource"} holidays`,
|
|
subtitle: "Location-specific holiday resolution",
|
|
metrics,
|
|
...(scopeMetrics.length > 0 ? { sections: [{ title: "Scopes", metrics: scopeMetrics }] } : {}),
|
|
};
|
|
}
|
|
|
|
function buildResourceMatchInsight(data: Record<string, unknown>): AssistantInsight | null {
|
|
const project = isRecord(data.project) ? data.project : undefined;
|
|
const period = isRecord(data.period) ? data.period : undefined;
|
|
const bestMatch = isRecord(data.bestMatch) ? data.bestMatch : undefined;
|
|
if (!project || !period || !bestMatch) return null;
|
|
|
|
const remainingHours = asNumber(bestMatch.remainingHours);
|
|
const remainingHoursPerDay = asNumber(bestMatch.remainingHoursPerDay);
|
|
const lcr = asString(bestMatch.lcr);
|
|
const holidaySummary = isRecord(bestMatch.holidaySummary) ? bestMatch.holidaySummary : undefined;
|
|
const absenceSummary = isRecord(bestMatch.absenceSummary) ? bestMatch.absenceSummary : undefined;
|
|
const capacityBreakdown = isRecord(bestMatch.capacityBreakdown) ? bestMatch.capacityBreakdown : undefined;
|
|
|
|
const metrics: AssistantInsightMetric[] = [];
|
|
pushMetric(metrics, "Best match", asString(bestMatch.name) ?? asString(bestMatch.eid), "good");
|
|
pushMetric(metrics, "Project", asString(project.name) ?? asString(project.shortCode));
|
|
pushMetric(metrics, "Remaining", formatHours(remainingHours), remainingHours != null && remainingHours > 0 ? "good" : "warn");
|
|
pushMetric(metrics, "Per workday", formatHours(remainingHoursPerDay));
|
|
pushMetric(metrics, "LCR", lcr);
|
|
pushMetric(metrics, "Holiday deduction", formatHours(holidaySummary?.hoursDeduction), "warn");
|
|
|
|
const sections: AssistantInsightSection[] = [];
|
|
|
|
const profileMetrics: AssistantInsightMetric[] = [];
|
|
pushMetric(profileMetrics, "Role", asString(bestMatch.role));
|
|
pushMetric(profileMetrics, "Chapter", asString(bestMatch.chapter));
|
|
pushMetric(profileMetrics, "Location", createLocationLabel(bestMatch), "info");
|
|
pushMetric(profileMetrics, "Candidate pool", asNumber(data.candidateCount)?.toString() ?? null);
|
|
if (profileMetrics.length > 0) {
|
|
sections.push({ title: "Selection", metrics: profileMetrics });
|
|
}
|
|
|
|
const basisMetrics: AssistantInsightMetric[] = [];
|
|
pushMetric(basisMetrics, "Window", asString(period.startDate) && asString(period.endDate) ? `${asString(period.startDate)} to ${asString(period.endDate)}` : null);
|
|
pushMetric(basisMetrics, "Ranking", asString(period.rankingMode));
|
|
pushMetric(basisMetrics, "Min/day", formatHours(period.minHoursPerDay));
|
|
pushMetric(basisMetrics, "Base capacity", formatHours(capacityBreakdown?.baseAvailableHours ?? bestMatch.baseAvailableHours));
|
|
pushMetric(basisMetrics, "Effective capacity", formatHours(bestMatch.availableHours));
|
|
pushMetric(basisMetrics, "Absence deduction", formatHours(absenceSummary?.hoursDeduction ?? capacityBreakdown?.absenceHoursDeduction), "warn");
|
|
if (basisMetrics.length > 0) {
|
|
sections.push({ title: "Capacity basis", metrics: basisMetrics });
|
|
}
|
|
|
|
return {
|
|
kind: "resource_match",
|
|
title: `${asString(project.shortCode) ?? asString(project.name) ?? "Project"} staffing`,
|
|
subtitle: "Holiday-aware best-fit resource",
|
|
metrics,
|
|
...(sections.length > 0 ? { sections } : {}),
|
|
};
|
|
}
|
|
|
|
export function buildAssistantInsight(toolName: string, data: unknown): AssistantInsight | null {
|
|
if (!isRecord(data)) return null;
|
|
|
|
switch (toolName) {
|
|
case "get_chargeability":
|
|
return buildChargeabilityInsight(data);
|
|
case "find_best_project_resource":
|
|
return buildResourceMatchInsight(data);
|
|
case "list_holidays_by_region":
|
|
return buildHolidayRegionInsight(data);
|
|
case "get_resource_holidays":
|
|
return buildResourceHolidayInsight(data);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|