From c5ab2a40e4d0f281d42aa3f68f9427b240dce0f8 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 19 Apr 2026 10:30:37 +0200 Subject: [PATCH] Implement ST-404 day overview --- README.md | 8 +- app/planning/page.tsx | 5 + components/planning/day-overview-card.tsx | 167 ++++++++++++++++++++ docs/backlog/inspannings-monitor-backlog.md | 6 +- lib/planning/day-overview.test.ts | 68 ++++++++ lib/planning/day-overview.ts | 72 +++++++++ 6 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 components/planning/day-overview-card.tsx create mode 100644 lib/planning/day-overview.test.ts create mode 100644 lib/planning/day-overview.ts diff --git a/README.md b/README.md index 5a7936d..6a6a609 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ product, niet als medisch hulpmiddel. Release 1 blijft smal: - planningpagina voor vandaag met activiteit toevoegen en directe lijstweergave - statusflows voor activiteiten van vandaag (`gepland`, `uitgevoerd`, `overgeslagen`, `aangepast`) - contextuele evaluatievelden voor overgeslagen en aangepaste activiteiten +- dagoverzicht op planning met gepland versus werkelijk en statusverdeling - energiemeter met lopend totaal ten opzichte van het dagbudget - niet-blokkerende waarschuwing bij budgetoverschrijding in planning en dashboard - eerste unit tests voor budget- en meterlogica via `Vitest` @@ -133,7 +134,6 @@ zichtbaar als `NEXT_PUBLIC_ENABLE_TEST_WIZARD=true` staat. ## Eerstvolgende bouwstappen -1. `ST-403` Ongeplande activiteiten ondersteunen -2. `ST-404` Dagoverzicht bouwen -3. `ST-405` Dagaggregaties server-side implementeren -4. `ST-105` RLS-policy tests en hardening afronden +1. `ST-405` Dagaggregaties server-side implementeren +2. `ST-105` RLS-policy tests en hardening afronden +3. `npm run test` toevoegen aan CI diff --git a/app/planning/page.tsx b/app/planning/page.tsx index 794b7f4..3f64b26 100644 --- a/app/planning/page.tsx +++ b/app/planning/page.tsx @@ -4,6 +4,7 @@ import { StatusToastBridge } from "@/components/feedback/status-toast-bridge"; import { AdHocActivityForm } from "@/components/planning/ad-hoc-activity-form"; import { AppShell } from "@/components/navigation/app-shell"; import { PageIntro } from "@/components/navigation/page-intro"; +import { DayOverviewCard } from "@/components/planning/day-overview-card"; import { ActivityForm } from "@/components/planning/activity-form"; import { EnergyMeterCard } from "@/components/planning/energy-meter-card"; import { TodayActivitiesList } from "@/components/planning/today-activities-list"; @@ -18,6 +19,7 @@ import { sanitizeNextPath } from "@/lib/auth/navigation"; import { getAuthState } from "@/lib/auth/session"; import { getTodayCheckInForCurrentUser } from "@/lib/check-in/service"; import { getPlanningStatusToast } from "@/lib/feedback/status-messages"; +import { calculateDayOverviewSnapshot } from "@/lib/planning/day-overview"; import { getPlanningPageDataForCurrentUser } from "@/lib/planning/service"; import { calculatePlanningMeterSnapshot } from "@/lib/planning/meter"; import { getProfileBundleForCurrentUser } from "@/lib/profile/service"; @@ -68,6 +70,7 @@ export default async function PlanningPage({ searchParams }: PlanningPageProps) planningPageData.activities, checkInStatus?.todayCheckIn?.dailyBudget ?? null, ); + const dayOverview = calculateDayOverviewSnapshot(planningPageData.activities); return ( @@ -151,6 +154,8 @@ export default async function PlanningPage({ searchParams }: PlanningPageProps) + + 0) { + return `Je werkelijke dag kwam ${overview.pointDifference} punten boven je oorspronkelijke plan uit.`; + } + + if (overview.pointDifference < 0) { + return `Je werkelijke dag bleef ${Math.abs(overview.pointDifference)} punten onder je oorspronkelijke plan.`; + } + + return "Je werkelijke dag lag qua punten precies in lijn met je oorspronkelijke plan."; +} + +function getStatusAccentClassName(key: "planned" | "completed" | "adjusted" | "skipped") { + if (key === "completed") { + return "bg-success text-white"; + } + + if (key === "adjusted") { + return "bg-primary text-white"; + } + + if (key === "skipped") { + return "bg-warning text-foreground"; + } + + return "bg-secondary text-secondary-foreground"; +} + +export function DayOverviewCard({ overview }: DayOverviewCardProps) { + return ( + + +

+ Dagoverzicht +

+ Gepland versus werkelijk + + Dit overzicht laat zien wat je vooraf van plan was, wat er echt gebeurde en hoe de statussen van vandaag verdeeld zijn. + +
+ +
+ + +

+ Vooraf gepland +

+

+ {overview.plannedActivityCount} +

+

+ {overview.plannedPoints} punten oorspronkelijk in plan +

+
+
+ + + +

+ Werkelijk gedaan +

+

+ {overview.executedActivityCount} +

+

+ {overview.actualPoints} punten uitgevoerd of aangepast +

+
+
+ + + +

+ Ongepland erbij +

+

+ {overview.adHocActivityCount} +

+

+ Activiteiten die onderweg aan je dag zijn toegevoegd +

+
+
+ + + +

+ Nog open +

+

+ {overview.openActivityCount} +

+

+ Activiteiten die nog op `gepland` staan +

+
+
+
+ +
+
+ {[ + { + key: "planned" as const, + label: "Gepland", + value: overview.openActivityCount, + }, + { + key: "completed" as const, + label: "Uitgevoerd", + value: overview.completedActivityCount, + }, + { + key: "adjusted" as const, + label: "Aangepast", + value: overview.adjustedActivityCount, + }, + { + key: "skipped" as const, + label: "Overgeslagen", + value: overview.skippedActivityCount, + }, + ].map((item) => ( + + {item.label} + + {item.value} + + + ))} +
+

+ {getDifferenceCopy(overview)} +

+
+
+
+ ); +} diff --git a/docs/backlog/inspannings-monitor-backlog.md b/docs/backlog/inspannings-monitor-backlog.md index 8a08a0c..c61e2f4 100644 --- a/docs/backlog/inspannings-monitor-backlog.md +++ b/docs/backlog/inspannings-monitor-backlog.md @@ -92,15 +92,15 @@ Status: `ST-301`, `ST-302`, `ST-304` en `ST-305` zijn inmiddels gerealiseerd in Doel: de kernloop afronden door geplande activiteiten te evalueren en terug te zien. -Status: `ST-401`, `ST-402` en `ST-403` zijn inmiddels gerealiseerd in de app. De volgende -logische stap ligt nu in `ST-404`. +Status: `ST-401`, `ST-402`, `ST-403` en `ST-404` zijn inmiddels gerealiseerd in de app. De volgende +logische stap ligt nu in `ST-405`. | Story ID | Titel | Type | Definition of done | | --- | --- | --- | --- | | ST-401 | Statusflows voor uitgevoerd, geskipt en aangepast bouwen | Build | Afgerond: activiteiten van vandaag kunnen direct tussen de vier statussen wisselen | | ST-402 | Evaluatievelden toevoegen | UI | Afgerond: skip-reden en toelichting verschijnen passend per status en worden opgeslagen | | ST-403 | Ongeplande activiteiten ondersteunen | Build | Afgerond: ongeplande activiteit kan als ad-hoc item worden opgeslagen en telt mee in het dagtotaal | -| ST-404 | Dagoverzicht bouwen | UI | Gepland versus uitgevoerd en statusverdeling zijn zichtbaar | +| ST-404 | Dagoverzicht bouwen | UI | Afgerond: planning toont nu gepland versus werkelijk en een directe statusverdeling van de dag | | ST-405 | Dagaggregaties server-side implementeren | Logic | Dagtotalen blijven consistent met individuele records | ## EPIC-06 Weekoverzicht en inzichten diff --git a/lib/planning/day-overview.test.ts b/lib/planning/day-overview.test.ts new file mode 100644 index 0000000..66b3897 --- /dev/null +++ b/lib/planning/day-overview.test.ts @@ -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); + }); +}); diff --git a/lib/planning/day-overview.ts b/lib/planning/day-overview.ts new file mode 100644 index 0000000..a49ad8f --- /dev/null +++ b/lib/planning/day-overview.ts @@ -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( + (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, + }, + ); +}