Implement ST-404 day overview

This commit is contained in:
Janpeter Visser 2026-04-19 10:30:37 +02:00
parent 67b4461e8a
commit c5ab2a40e4
6 changed files with 319 additions and 7 deletions

View file

@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import { calculateDayOverviewSnapshot } from "./day-overview";
describe("calculateDayOverviewSnapshot", () => {
it("berekent gepland versus werkelijk met statusverdeling", () => {
const snapshot = calculateDayOverviewSnapshot([
{
source: "planned",
status: "completed",
durationMinutes: 30,
impactLevel: "midden",
},
{
source: "planned",
status: "adjusted",
durationMinutes: 60,
impactLevel: "laag",
},
{
source: "planned",
status: "skipped",
durationMinutes: 90,
impactLevel: "hoog",
},
{
source: "ad_hoc",
status: "completed",
durationMinutes: 15,
impactLevel: "laag",
},
{
source: "planned",
status: "planned",
durationMinutes: 45,
impactLevel: "midden",
},
]);
expect(snapshot.totalActivities).toBe(5);
expect(snapshot.plannedActivityCount).toBe(4);
expect(snapshot.adHocActivityCount).toBe(1);
expect(snapshot.openActivityCount).toBe(1);
expect(snapshot.completedActivityCount).toBe(2);
expect(snapshot.adjustedActivityCount).toBe(1);
expect(snapshot.skippedActivityCount).toBe(1);
expect(snapshot.executedActivityCount).toBe(3);
expect(snapshot.plannedPoints).toBe(10);
expect(snapshot.actualPoints).toBe(5);
expect(snapshot.pointDifference).toBe(-5);
});
it("laat ongeplande activiteiten meetellen in werkelijk, niet in gepland", () => {
const snapshot = calculateDayOverviewSnapshot([
{
source: "ad_hoc",
status: "completed",
durationMinutes: 120,
impactLevel: "hoog",
},
]);
expect(snapshot.plannedActivityCount).toBe(0);
expect(snapshot.adHocActivityCount).toBe(1);
expect(snapshot.plannedPoints).toBe(0);
expect(snapshot.actualPoints).toBe(5);
expect(snapshot.pointDifference).toBe(5);
});
});

View file

@ -0,0 +1,72 @@
import { deriveActivityEnergyPoints } from "./meter";
import type { ActivityRecord } from "./types";
type ActivityOverviewInput = Pick<
ActivityRecord,
"source" | "status" | "durationMinutes" | "impactLevel"
>;
export type DayOverviewSnapshot = {
totalActivities: number;
plannedActivityCount: number;
adHocActivityCount: number;
openActivityCount: number;
completedActivityCount: number;
adjustedActivityCount: number;
skippedActivityCount: number;
executedActivityCount: number;
plannedPoints: number;
actualPoints: number;
pointDifference: number;
};
export function calculateDayOverviewSnapshot(
activities: ActivityOverviewInput[],
): DayOverviewSnapshot {
return activities.reduce<DayOverviewSnapshot>(
(snapshot, activity) => {
snapshot.totalActivities += 1;
if (activity.source === "planned") {
snapshot.plannedActivityCount += 1;
// Planned points should reflect the intended activity load, regardless of later status.
snapshot.plannedPoints += deriveActivityEnergyPoints({
durationMinutes: activity.durationMinutes,
impactLevel: activity.impactLevel,
});
} else {
snapshot.adHocActivityCount += 1;
}
if (activity.status === "planned") {
snapshot.openActivityCount += 1;
} else if (activity.status === "completed") {
snapshot.completedActivityCount += 1;
snapshot.executedActivityCount += 1;
snapshot.actualPoints += deriveActivityEnergyPoints(activity);
} else if (activity.status === "adjusted") {
snapshot.adjustedActivityCount += 1;
snapshot.executedActivityCount += 1;
snapshot.actualPoints += deriveActivityEnergyPoints(activity);
} else if (activity.status === "skipped") {
snapshot.skippedActivityCount += 1;
}
snapshot.pointDifference = snapshot.actualPoints - snapshot.plannedPoints;
return snapshot;
},
{
totalActivities: 0,
plannedActivityCount: 0,
adHocActivityCount: 0,
openActivityCount: 0,
completedActivityCount: 0,
adjustedActivityCount: 0,
skippedActivityCount: 0,
executedActivityCount: 0,
plannedPoints: 0,
actualPoints: 0,
pointDifference: 0,
},
);
}