diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 4b39fd9..c1a641c 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -168,12 +168,12 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
{planningStatus?.activities.length
? `${planningStatus.activities.length} activiteiten voor vandaag`
- : "Nog niets gepland voor vandaag"}
+ : "Nog niets toegevoegd voor vandaag"}
- Plan kleine, concrete activiteiten voor vandaag en bouw daarna verder op budgetfeedback en evaluatie.
+ Plan kleine, concrete activiteiten voor vandaag en leg ook onverwachte activiteiten vast als je dag anders loopt dan gedacht.
{previewPoints === null
? "Kies een geldige duur en impact om te zien hoeveel punten deze activiteit ongeveer toevoegt."
- : `Deze activiteit telt voorlopig voor ${previewPoints} punten. Je totaal zou dan uitkomen op ${previewMeter?.plannedPoints ?? currentMeter.plannedPoints} geplande punten.`}
+ : `Deze activiteit telt voorlopig voor ${previewPoints} punten. Je totaal zou dan uitkomen op ${previewMeter?.totalPoints ?? currentMeter.totalPoints} punten in beeld.`}
{isPending
? "Je activiteit wordt opgeslagen..."
- : "Je activiteit wordt vandaag toegevoegd met status `gepland`, waarna de meter direct opnieuw wordt berekend."}
+ : "Je activiteit wordt vandaag toegevoegd met status `gepland`, waarna je dagtotaal direct opnieuw wordt berekend."}
diff --git a/docs/backlog/inspannings-monitor-backlog.md b/docs/backlog/inspannings-monitor-backlog.md
index fee9068..8a08a0c 100644
--- a/docs/backlog/inspannings-monitor-backlog.md
+++ b/docs/backlog/inspannings-monitor-backlog.md
@@ -92,14 +92,14 @@ 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` en `ST-402` zijn inmiddels gerealiseerd in de app. De volgende
-logische stap ligt nu in `ST-403`.
+Status: `ST-401`, `ST-402` en `ST-403` zijn inmiddels gerealiseerd in de app. De volgende
+logische stap ligt nu in `ST-404`.
| 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 | Ongeplande activiteit telt mee in werkelijke totalen |
+| 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-405 | Dagaggregaties server-side implementeren | Logic | Dagtotalen blijven consistent met individuele records |
diff --git a/lib/feedback/status-messages.ts b/lib/feedback/status-messages.ts
index 983005d..2c8af32 100644
--- a/lib/feedback/status-messages.ts
+++ b/lib/feedback/status-messages.ts
@@ -69,6 +69,11 @@ const planningStatusToasts: Record = {
title: "Activiteit gepland",
message: "Je activiteit staat nu in je dagplanning van vandaag.",
},
+ "ad-hoc-activity-saved": {
+ variant: "success",
+ title: "Ongeplande activiteit toegevoegd",
+ message: "Deze activiteit staat nu ook in je daglijst van vandaag.",
+ },
"activity-status-saved": {
variant: "success",
title: "Activiteit bijgewerkt",
@@ -88,6 +93,12 @@ const planningErrorToasts: Record = {
message:
"Controleer naam, categorie, duur, impact en prioriteit en probeer het opnieuw.",
},
+ "invalid-ad-hoc-activity-input": {
+ variant: "error",
+ title: "Ongeplande activiteit niet opgeslagen",
+ message:
+ "Controleer naam, categorie, duur en impact en probeer het opnieuw.",
+ },
"invalid-activity-status": {
variant: "error",
title: "Status niet opgeslagen",
@@ -109,6 +120,11 @@ const planningErrorToasts: Record = {
title: "Evaluatie niet opgeslagen",
message: "De extra context bij deze activiteit kon niet worden opgeslagen.",
},
+ "ad-hoc-activity-failed": {
+ variant: "error",
+ title: "Ongeplande activiteit niet opgeslagen",
+ message: "De ongeplande activiteit kon niet worden toegevoegd. Probeer het opnieuw.",
+ },
};
export function getDashboardStatusToast(status: string | null): StatusToast | null {
diff --git a/lib/planning/meter.test.ts b/lib/planning/meter.test.ts
index cc4af33..a4ead90 100644
--- a/lib/planning/meter.test.ts
+++ b/lib/planning/meter.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import {
- calculatePlannedPointsTotal,
+ calculateActivityPointsTotal,
calculatePlanningMeterSnapshot,
deriveActivityEnergyPoints,
} from "./meter";
@@ -38,7 +38,7 @@ describe("deriveActivityEnergyPoints", () => {
describe("calculatePlanningMeterSnapshot", () => {
it("somt punten van activiteiten op", () => {
expect(
- calculatePlannedPointsTotal([
+ calculateActivityPointsTotal([
{ durationMinutes: 30, impactLevel: "midden", status: "planned" },
{ durationMinutes: 90, impactLevel: "laag", status: "planned" },
]),
@@ -54,7 +54,7 @@ describe("calculatePlanningMeterSnapshot", () => {
8,
);
- expect(snapshot.plannedPoints).toBe(6);
+ expect(snapshot.totalPoints).toBe(6);
expect(snapshot.remainingBudget).toBe(2);
expect(snapshot.progressPercent).toBe(75);
expect(snapshot.isOverBudget).toBe(false);
@@ -66,7 +66,7 @@ describe("calculatePlanningMeterSnapshot", () => {
null,
);
- expect(snapshot.plannedPoints).toBe(2);
+ expect(snapshot.totalPoints).toBe(2);
expect(snapshot.dailyBudget).toBeNull();
expect(snapshot.remainingBudget).toBeNull();
expect(snapshot.progressPercent).toBeNull();
@@ -81,7 +81,7 @@ describe("calculatePlanningMeterSnapshot", () => {
6,
);
- expect(snapshot.plannedPoints).toBe(9);
+ expect(snapshot.totalPoints).toBe(9);
expect(snapshot.remainingBudget).toBe(-3);
expect(snapshot.isOverBudget).toBe(true);
expect(snapshot.progressPercent).toBe(100);
diff --git a/lib/planning/meter.ts b/lib/planning/meter.ts
index a88b882..d565d77 100644
--- a/lib/planning/meter.ts
+++ b/lib/planning/meter.ts
@@ -5,7 +5,7 @@ import type {
} from "@/lib/planning/types";
export type PlanningMeterSnapshot = {
- plannedPoints: number;
+ totalPoints: number;
activityCount: number;
dailyBudget: number | null;
remainingBudget: number | null;
@@ -59,7 +59,7 @@ export function deriveActivityEnergyPoints(input: ActivityMeterInput): number {
);
}
-export function calculatePlannedPointsTotal(
+export function calculateActivityPointsTotal(
activities: Pick[],
): number {
return activities.reduce(
@@ -72,11 +72,11 @@ export function calculatePlanningMeterSnapshot(
activities: Pick[],
dailyBudget: number | null,
): PlanningMeterSnapshot {
- const plannedPoints = calculatePlannedPointsTotal(activities);
+ const totalPoints = calculateActivityPointsTotal(activities);
if (dailyBudget === null) {
return {
- plannedPoints,
+ totalPoints,
activityCount: activities.length,
dailyBudget: null,
remainingBudget: null,
@@ -86,11 +86,11 @@ export function calculatePlanningMeterSnapshot(
};
}
- const remainingBudget = dailyBudget - plannedPoints;
- const progressRatio = dailyBudget > 0 ? plannedPoints / dailyBudget : 0;
+ const remainingBudget = dailyBudget - totalPoints;
+ const progressRatio = dailyBudget > 0 ? totalPoints / dailyBudget : 0;
return {
- plannedPoints,
+ totalPoints,
activityCount: activities.length,
dailyBudget,
remainingBudget,
diff --git a/lib/planning/service.ts b/lib/planning/service.ts
index eb711a9..0c212ae 100644
--- a/lib/planning/service.ts
+++ b/lib/planning/service.ts
@@ -1,6 +1,7 @@
import { getAuthenticatedUser } from "@/lib/auth/session";
import type {
ActivityCategory,
+ CreateAdHocActivitySubmission,
CreateActivitySubmission,
ActivityImpactLevel,
ActivityPriorityLevel,
@@ -328,6 +329,51 @@ export async function createActivityForTodayForCurrentUser(
return mapActivityRow(data);
}
+export async function createAdHocActivityForTodayForCurrentUser(
+ submission: CreateAdHocActivitySubmission,
+): Promise {
+ const user = await getAuthenticatedUser();
+
+ if (!user) {
+ throw new Error("Er is geen ingelogde gebruiker beschikbaar.");
+ }
+
+ const profileBundle = await ensureProfileBundleForCurrentUser();
+
+ if (!profileBundle) {
+ throw new Error("Profielbundle ontbreekt voor de huidige gebruiker.");
+ }
+
+ const activityDate = getLocalDateForTimezone(profileBundle.profile.timezone);
+ const supabase = await createClient();
+
+ await assertCategoryExists(supabase, submission.categoryId);
+
+ const { data, error } = await supabase
+ .from("activities")
+ .insert({
+ user_id: user.id,
+ activity_date: activityDate,
+ source: "ad_hoc",
+ status: "completed",
+ name: submission.name,
+ category_id: submission.categoryId,
+ duration_minutes: submission.durationMinutes,
+ impact_level: submission.impactLevel,
+ priority_level: "normaal",
+ skip_reason_id: null,
+ notes: null,
+ })
+ .select(ACTIVITY_COLUMNS)
+ .single();
+
+ if (error) {
+ throw new Error(`Ongeplande activiteit kon niet worden opgeslagen: ${error.message}`);
+ }
+
+ return mapActivityRow(data);
+}
+
export async function updateActivityStatusForTodayForCurrentUser(
submission: UpdateActivityStatusSubmission,
): Promise {
diff --git a/lib/planning/types.ts b/lib/planning/types.ts
index cb0ee82..f6e5daa 100644
--- a/lib/planning/types.ts
+++ b/lib/planning/types.ts
@@ -53,6 +53,13 @@ export type CreateActivitySubmission = {
priorityLevel: ActivityPriorityLevel;
};
+export type CreateAdHocActivitySubmission = {
+ name: string;
+ categoryId: string;
+ durationMinutes: number;
+ impactLevel: ActivityImpactLevel;
+};
+
export type UpdateActivityStatusSubmission = {
activityId: string;
status: ActivityStatus;