feat(api): explain chargeability derivation inputs
This commit is contained in:
@@ -132,6 +132,38 @@ describe("chargeability report router", () => {
|
|||||||
|
|
||||||
expect(strictMonth).toBeDefined();
|
expect(strictMonth).toBeDefined();
|
||||||
expect(proposedMonth).toBeDefined();
|
expect(proposedMonth).toBeDefined();
|
||||||
|
expect(strict.explainability).toEqual({
|
||||||
|
locationFields: [
|
||||||
|
"country",
|
||||||
|
"federalState",
|
||||||
|
"city",
|
||||||
|
"orgUnit",
|
||||||
|
"managementLevelGroup",
|
||||||
|
"managementLevel",
|
||||||
|
],
|
||||||
|
monthDerivationFields: [
|
||||||
|
"baseAvailableHours",
|
||||||
|
"publicHolidayCount",
|
||||||
|
"publicHolidayWorkdayCount",
|
||||||
|
"publicHolidayHoursDeduction",
|
||||||
|
"absenceDayEquivalent",
|
||||||
|
"absenceHoursDeduction",
|
||||||
|
"effectiveAvailableHours",
|
||||||
|
],
|
||||||
|
activeFilters: [],
|
||||||
|
formulas: {
|
||||||
|
sah: "baseAvailableHours - publicHolidayHoursDeduction - absenceHoursDeduction = effectiveAvailableHours",
|
||||||
|
chargeabilityPct: "chargeabilityHours / sahHours",
|
||||||
|
targetHours: "sahHours * targetPct",
|
||||||
|
gapHours: "chargeabilityHours - targetHours",
|
||||||
|
},
|
||||||
|
notes: [
|
||||||
|
"Location fields explain why two resources can have different SAH in the same month because country, federal state, and city holidays may differ.",
|
||||||
|
"Holiday deductions and absence deductions are tracked separately; absence does not deduct days that are already public holidays.",
|
||||||
|
"Include proposed work changes chargeability ratios and hours, but it does not change holiday or absence-based SAH derivation.",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(withProposed.explainability.activeFilters).toEqual(["includeProposed"]);
|
||||||
expect(strictMonth?.chg).toBeGreaterThan(0);
|
expect(strictMonth?.chg).toBeGreaterThan(0);
|
||||||
expect(proposedMonth?.chg).toBeGreaterThan(strictMonth?.chg ?? 0);
|
expect(proposedMonth?.chg).toBeGreaterThan(strictMonth?.chg ?? 0);
|
||||||
expect(proposedMonth?.chg).toBeCloseTo((strictMonth?.chg ?? 0) * 2, 5);
|
expect(proposedMonth?.chg).toBeCloseTo((strictMonth?.chg ?? 0) * 2, 5);
|
||||||
@@ -352,6 +384,13 @@ describe("chargeability report router", () => {
|
|||||||
const augsburg = report.resources.find((resource) => resource.city === "Augsburg");
|
const augsburg = report.resources.find((resource) => resource.city === "Augsburg");
|
||||||
const munich = report.resources.find((resource) => resource.city === "Munich");
|
const munich = report.resources.find((resource) => resource.city === "Munich");
|
||||||
|
|
||||||
|
expect(augsburg?.federalState).toBe("BY");
|
||||||
|
expect(augsburg?.months[0]?.derivation?.publicHolidayCount).toBe(
|
||||||
|
(munich?.months[0]?.derivation?.publicHolidayCount ?? 0) + 1,
|
||||||
|
);
|
||||||
|
expect(augsburg?.months[0]?.derivation?.publicHolidayHoursDeduction).toBe(
|
||||||
|
(munich?.months[0]?.derivation?.publicHolidayHoursDeduction ?? 0) + 8,
|
||||||
|
);
|
||||||
expect(augsburg?.months[0]?.sah).toBe((munich?.months[0]?.sah ?? 0) - 8);
|
expect(augsburg?.months[0]?.sah).toBe((munich?.months[0]?.sah ?? 0) - 8);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -513,11 +552,43 @@ describe("chargeability report router", () => {
|
|||||||
expect(result.resourceCount).toBe(1);
|
expect(result.resourceCount).toBe(1);
|
||||||
expect(result.returnedResourceCount).toBe(1);
|
expect(result.returnedResourceCount).toBe(1);
|
||||||
expect(result.truncated).toBe(false);
|
expect(result.truncated).toBe(false);
|
||||||
|
expect(result.explainability).toEqual({
|
||||||
|
locationFields: [
|
||||||
|
"country",
|
||||||
|
"federalState",
|
||||||
|
"city",
|
||||||
|
"orgUnit",
|
||||||
|
"managementLevelGroup",
|
||||||
|
"managementLevel",
|
||||||
|
],
|
||||||
|
monthDerivationFields: [
|
||||||
|
"baseAvailableHours",
|
||||||
|
"publicHolidayCount",
|
||||||
|
"publicHolidayWorkdayCount",
|
||||||
|
"publicHolidayHoursDeduction",
|
||||||
|
"absenceDayEquivalent",
|
||||||
|
"absenceHoursDeduction",
|
||||||
|
"effectiveAvailableHours",
|
||||||
|
],
|
||||||
|
activeFilters: ["resourceQuery"],
|
||||||
|
formulas: {
|
||||||
|
sah: "baseAvailableHours - publicHolidayHoursDeduction - absenceHoursDeduction = effectiveAvailableHours",
|
||||||
|
chargeabilityPct: "chargeabilityHours / sahHours",
|
||||||
|
targetHours: "sahHours * targetPct",
|
||||||
|
gapHours: "chargeabilityHours - targetHours",
|
||||||
|
},
|
||||||
|
notes: [
|
||||||
|
"Location fields explain why two resources can have different SAH in the same month because country, federal state, and city holidays may differ.",
|
||||||
|
"Holiday deductions and absence deductions are tracked separately; absence does not deduct days that are already public holidays.",
|
||||||
|
"Include proposed work changes chargeability ratios and hours, but it does not change holiday or absence-based SAH derivation.",
|
||||||
|
],
|
||||||
|
});
|
||||||
expect(result.resources).toEqual([
|
expect(result.resources).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
targetPct: 80,
|
targetPct: 80,
|
||||||
country: "ES",
|
country: "ES",
|
||||||
|
federalState: null,
|
||||||
city: "Barcelona",
|
city: "Barcelona",
|
||||||
managementLevelGroup: "Senior",
|
managementLevelGroup: "Senior",
|
||||||
managementLevel: "L7",
|
managementLevel: "L7",
|
||||||
@@ -527,6 +598,10 @@ describe("chargeability report router", () => {
|
|||||||
sah: expect.any(Number),
|
sah: expect.any(Number),
|
||||||
chargeabilityPct: expect.any(Number),
|
chargeabilityPct: expect.any(Number),
|
||||||
gapPct: expect.any(Number),
|
gapPct: expect.any(Number),
|
||||||
|
derivation: expect.objectContaining({
|
||||||
|
baseAvailableHours: expect.any(Number),
|
||||||
|
effectiveAvailableHours: expect.any(Number),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -168,12 +168,62 @@ export const chargeabilityReportDetailInputSchema = chargeabilityReportInputSche
|
|||||||
|
|
||||||
type ChargeabilityReportInput = z.infer<typeof chargeabilityReportInputSchema>;
|
type ChargeabilityReportInput = z.infer<typeof chargeabilityReportInputSchema>;
|
||||||
type ChargeabilityReportDetailInput = z.infer<typeof chargeabilityReportDetailInputSchema>;
|
type ChargeabilityReportDetailInput = z.infer<typeof chargeabilityReportDetailInputSchema>;
|
||||||
|
type ChargeabilityExplainabilityInput = ChargeabilityReportInput & {
|
||||||
|
resourceQuery?: string | undefined;
|
||||||
|
resourceLimit?: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
type ChargeabilityReportDbClient = Pick<
|
type ChargeabilityReportDbClient = Pick<
|
||||||
PrismaClient,
|
PrismaClient,
|
||||||
"assignment" | "resource" | "project" | "vacation" | "holidayCalendar" | "systemSettings"
|
"assignment" | "resource" | "project" | "vacation" | "holidayCalendar" | "systemSettings"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const CHARGEABILITY_LOCATION_FIELDS = [
|
||||||
|
"country",
|
||||||
|
"federalState",
|
||||||
|
"city",
|
||||||
|
"orgUnit",
|
||||||
|
"managementLevelGroup",
|
||||||
|
"managementLevel",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const CHARGEABILITY_DERIVATION_FIELDS = [
|
||||||
|
"baseAvailableHours",
|
||||||
|
"publicHolidayCount",
|
||||||
|
"publicHolidayWorkdayCount",
|
||||||
|
"publicHolidayHoursDeduction",
|
||||||
|
"absenceDayEquivalent",
|
||||||
|
"absenceHoursDeduction",
|
||||||
|
"effectiveAvailableHours",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function buildChargeabilityExplainability(input: ChargeabilityExplainabilityInput) {
|
||||||
|
const activeFilters = [
|
||||||
|
...(input.orgUnitId ? ["orgUnitId"] : []),
|
||||||
|
...(input.managementLevelGroupId ? ["managementLevelGroupId"] : []),
|
||||||
|
...(input.countryId ? ["countryId"] : []),
|
||||||
|
...(input.resourceQuery ? ["resourceQuery"] : []),
|
||||||
|
...(input.includeProposed ? ["includeProposed"] : []),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
locationFields: [...CHARGEABILITY_LOCATION_FIELDS],
|
||||||
|
monthDerivationFields: [...CHARGEABILITY_DERIVATION_FIELDS],
|
||||||
|
activeFilters,
|
||||||
|
formulas: {
|
||||||
|
sah: "baseAvailableHours - publicHolidayHoursDeduction - absenceHoursDeduction = effectiveAvailableHours",
|
||||||
|
chargeabilityPct: "chargeabilityHours / sahHours",
|
||||||
|
targetHours: "sahHours * targetPct",
|
||||||
|
gapHours: "chargeabilityHours - targetHours",
|
||||||
|
},
|
||||||
|
notes: [
|
||||||
|
"Location fields explain why two resources can have different SAH in the same month because country, federal state, and city holidays may differ.",
|
||||||
|
"Holiday deductions and absence deductions are tracked separately; absence does not deduct days that are already public holidays.",
|
||||||
|
"Include proposed work changes chargeability ratios and hours, but it does not change holiday or absence-based SAH derivation.",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function queryChargeabilityReport(
|
async function queryChargeabilityReport(
|
||||||
db: ChargeabilityReportDbClient,
|
db: ChargeabilityReportDbClient,
|
||||||
input: ChargeabilityReportInput,
|
input: ChargeabilityReportInput,
|
||||||
@@ -227,6 +277,7 @@ async function queryChargeabilityReport(
|
|||||||
chargeabilityRatio: 0,
|
chargeabilityRatio: 0,
|
||||||
targetRatio: 0,
|
targetRatio: 0,
|
||||||
})),
|
})),
|
||||||
|
explainability: buildChargeabilityExplainability(input),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +439,7 @@ async function queryChargeabilityReport(
|
|||||||
monthKeys,
|
monthKeys,
|
||||||
resources: anonymizeResources(resourceRows, directory),
|
resources: anonymizeResources(resourceRows, directory),
|
||||||
groupTotals,
|
groupTotals,
|
||||||
|
explainability: buildChargeabilityExplainability(input),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +514,7 @@ export function buildChargeabilityReportDetail(
|
|||||||
resourceCount: matchingResources.length,
|
resourceCount: matchingResources.length,
|
||||||
returnedResourceCount: resources.length,
|
returnedResourceCount: resources.length,
|
||||||
truncated: resources.length < matchingResources.length,
|
truncated: resources.length < matchingResources.length,
|
||||||
|
explainability: buildChargeabilityExplainability(input),
|
||||||
resources,
|
resources,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user