feat(platform): checkpoint current implementation state
This commit is contained in:
@@ -36,14 +36,49 @@ export interface ProjectHealthRow {
|
||||
assignmentCount: number;
|
||||
spentCents: number;
|
||||
}>;
|
||||
derivation?: {
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
calendarContextCount: number;
|
||||
holidayAwareAssignmentCount: number;
|
||||
fallbackAssignmentCount: number;
|
||||
baseSpentCents: number;
|
||||
adjustedSpentCents: number;
|
||||
publicHolidayDayEquivalent: number;
|
||||
publicHolidayCostDeductionCents: number;
|
||||
absenceDayEquivalent: number;
|
||||
absenceCostDeductionCents: number;
|
||||
};
|
||||
}
|
||||
|
||||
const DAY_KEYS: (keyof WeekdayAvailability)[] = [
|
||||
"sunday",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
];
|
||||
|
||||
function hasAvailability<T extends { availability?: unknown }>(
|
||||
resource: T | null | undefined,
|
||||
): resource is T & { availability: WeekdayAvailability } {
|
||||
return resource !== null && resource !== undefined && resource.availability !== null && resource.availability !== undefined;
|
||||
}
|
||||
|
||||
function toIsoDate(value: Date): string {
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getDailyAvailabilityHours(
|
||||
availability: WeekdayAvailability,
|
||||
date: Date,
|
||||
): number {
|
||||
const dayKey = DAY_KEYS[date.getUTCDay()];
|
||||
return dayKey ? (availability[dayKey] ?? 0) : 0;
|
||||
}
|
||||
|
||||
function toUtcDayStart(value: Date): Date {
|
||||
return new Date(Date.UTC(
|
||||
value.getUTCFullYear(),
|
||||
@@ -66,6 +101,75 @@ function buildLocationKey(input: {
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeSpentDerivation(input: {
|
||||
availability: WeekdayAvailability;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
dailyCostCents: number;
|
||||
context: Awaited<ReturnType<typeof loadDailyAvailabilityContexts>> extends Map<string, infer TValue>
|
||||
? TValue | undefined
|
||||
: never;
|
||||
}) {
|
||||
const baseSpentCents = calculateEffectiveAllocationCostCents({
|
||||
availability: input.availability,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
dailyCostCents: input.dailyCostCents,
|
||||
periodStart: input.startDate,
|
||||
periodEnd: input.endDate,
|
||||
context: undefined,
|
||||
});
|
||||
const adjustedSpentCents = calculateEffectiveAllocationCostCents({
|
||||
availability: input.availability,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
dailyCostCents: input.dailyCostCents,
|
||||
periodStart: input.startDate,
|
||||
periodEnd: input.endDate,
|
||||
context: input.context,
|
||||
});
|
||||
|
||||
let publicHolidayDayEquivalent = 0;
|
||||
let publicHolidayCostDeductionCents = 0;
|
||||
let absenceDayEquivalent = 0;
|
||||
let absenceCostDeductionCents = 0;
|
||||
|
||||
const cursor = new Date(input.startDate);
|
||||
cursor.setUTCHours(0, 0, 0, 0);
|
||||
const end = new Date(input.endDate);
|
||||
end.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
while (cursor <= end) {
|
||||
const baseHours = getDailyAvailabilityHours(input.availability, cursor);
|
||||
if (baseHours > 0) {
|
||||
const isoDate = toIsoDate(cursor);
|
||||
if (input.context?.holidayDates.has(isoDate)) {
|
||||
publicHolidayDayEquivalent += 1;
|
||||
publicHolidayCostDeductionCents += input.dailyCostCents;
|
||||
} else {
|
||||
const vacationFraction = Math.min(
|
||||
1,
|
||||
Math.max(0, input.context?.vacationFractionsByDate.get(isoDate) ?? 0),
|
||||
);
|
||||
if (vacationFraction > 0) {
|
||||
absenceDayEquivalent += vacationFraction;
|
||||
absenceCostDeductionCents += input.dailyCostCents * vacationFraction;
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
baseSpentCents,
|
||||
adjustedSpentCents,
|
||||
publicHolidayDayEquivalent,
|
||||
publicHolidayCostDeductionCents: Math.round(publicHolidayCostDeductionCents),
|
||||
absenceDayEquivalent,
|
||||
absenceCostDeductionCents: Math.round(absenceCostDeductionCents),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDashboardProjectHealth(
|
||||
db: PrismaClient,
|
||||
): Promise<ProjectHealthRow[]> {
|
||||
@@ -147,20 +251,67 @@ export async function getDashboardProjectHealth(
|
||||
contextEnd,
|
||||
);
|
||||
const spentByProject = new Map<string, number>();
|
||||
const derivationByProject = new Map<string, {
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
calendarContextCount: number;
|
||||
holidayAwareAssignmentCount: number;
|
||||
fallbackAssignmentCount: number;
|
||||
baseSpentCents: number;
|
||||
adjustedSpentCents: number;
|
||||
publicHolidayDayEquivalent: number;
|
||||
publicHolidayCostDeductionCents: number;
|
||||
absenceDayEquivalent: number;
|
||||
absenceCostDeductionCents: number;
|
||||
}>();
|
||||
const calendarLocationsByProject = new Map<string, Map<string, NonNullable<ProjectHealthRow["calendarLocations"]>[number]>>();
|
||||
for (const a of assignments) {
|
||||
const cost = hasAvailability(a.resource)
|
||||
? calculateEffectiveAllocationCostCents({
|
||||
const derivation = hasAvailability(a.resource)
|
||||
? summarizeSpentDerivation({
|
||||
availability: a.resource.availability as unknown as WeekdayAvailability,
|
||||
startDate: a.startDate,
|
||||
endDate: a.endDate,
|
||||
dailyCostCents: a.dailyCostCents ?? 0,
|
||||
periodStart: a.startDate,
|
||||
periodEnd: a.endDate,
|
||||
context: contexts.get(a.resource.id),
|
||||
})
|
||||
: (a.dailyCostCents ?? 0) * calculateInclusiveDays(a.startDate, a.endDate);
|
||||
: null;
|
||||
const cost = derivation?.adjustedSpentCents
|
||||
?? (a.dailyCostCents ?? 0) * calculateInclusiveDays(a.startDate, a.endDate);
|
||||
spentByProject.set(a.projectId, (spentByProject.get(a.projectId) ?? 0) + cost);
|
||||
const existingDerivation = derivationByProject.get(a.projectId) ?? {
|
||||
periodStart: toIsoDate(a.startDate),
|
||||
periodEnd: toIsoDate(a.endDate),
|
||||
calendarContextCount: 0,
|
||||
holidayAwareAssignmentCount: 0,
|
||||
fallbackAssignmentCount: 0,
|
||||
baseSpentCents: 0,
|
||||
adjustedSpentCents: 0,
|
||||
publicHolidayDayEquivalent: 0,
|
||||
publicHolidayCostDeductionCents: 0,
|
||||
absenceDayEquivalent: 0,
|
||||
absenceCostDeductionCents: 0,
|
||||
};
|
||||
if (a.startDate < new Date(existingDerivation.periodStart)) {
|
||||
existingDerivation.periodStart = toIsoDate(a.startDate);
|
||||
}
|
||||
if (a.endDate > new Date(existingDerivation.periodEnd)) {
|
||||
existingDerivation.periodEnd = toIsoDate(a.endDate);
|
||||
}
|
||||
if (derivation) {
|
||||
existingDerivation.calendarContextCount += 1;
|
||||
existingDerivation.holidayAwareAssignmentCount += 1;
|
||||
existingDerivation.baseSpentCents += derivation.baseSpentCents;
|
||||
existingDerivation.adjustedSpentCents += derivation.adjustedSpentCents;
|
||||
existingDerivation.publicHolidayDayEquivalent += derivation.publicHolidayDayEquivalent;
|
||||
existingDerivation.publicHolidayCostDeductionCents += derivation.publicHolidayCostDeductionCents;
|
||||
existingDerivation.absenceDayEquivalent += derivation.absenceDayEquivalent;
|
||||
existingDerivation.absenceCostDeductionCents += derivation.absenceCostDeductionCents;
|
||||
} else {
|
||||
existingDerivation.fallbackAssignmentCount += 1;
|
||||
existingDerivation.baseSpentCents += cost;
|
||||
existingDerivation.adjustedSpentCents += cost;
|
||||
}
|
||||
derivationByProject.set(a.projectId, existingDerivation);
|
||||
if (a.resource) {
|
||||
const projectLocations = calendarLocationsByProject.get(a.projectId) ?? new Map();
|
||||
const locationKey = buildLocationKey({
|
||||
@@ -229,6 +380,7 @@ export async function getDashboardProjectHealth(
|
||||
(budgetHealth + staffingHealth + timelineHealth) / 3,
|
||||
);
|
||||
const remainingBudgetCents = p.budgetCents == null ? null : p.budgetCents - spentCents;
|
||||
const derivation = derivationByProject.get(p.id);
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
@@ -254,6 +406,7 @@ export async function getDashboardProjectHealth(
|
||||
timelineStatus,
|
||||
calendarLocations: Array.from(calendarLocationsByProject.get(p.id)?.values() ?? [])
|
||||
.sort((left, right) => right.spentCents - left.spentCents),
|
||||
...(derivation ? { derivation } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ export async function createEstimateExport(
|
||||
? (targetVersion.projectSnapshot as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const payload = serializeEstimateExport(
|
||||
const payload = await serializeEstimateExport(
|
||||
{
|
||||
estimate,
|
||||
version: targetVersion,
|
||||
|
||||
Reference in New Issue
Block a user