Implement ST-402 activity evaluation fields

This commit is contained in:
Janpeter Visser 2026-04-19 09:51:20 +02:00
parent c3cd1de647
commit 57ade6a772
10 changed files with 344 additions and 6 deletions

View file

@ -0,0 +1,127 @@
"use client";
import { useActionState, useState } from "react";
import { saveActivityEvaluationAction } from "@/app/planning/actions";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { ActivityStatus, SkipReason } from "@/lib/planning/types";
type ActivityEvaluationFieldsProps = {
activityId: string;
status: ActivityStatus;
skipReasons: SkipReason[];
initialSkipReasonId: string | null;
initialNotes: string | null;
};
export function ActivityEvaluationFields({
activityId,
status,
skipReasons,
initialSkipReasonId,
initialNotes,
}: ActivityEvaluationFieldsProps) {
const [, formAction, isPending] = useActionState(saveActivityEvaluationAction, null);
const [skipReasonId, setSkipReasonId] = useState(
initialSkipReasonId ?? skipReasons[0]?.id ?? "",
);
const [notes, setNotes] = useState(initialNotes ?? "");
if (status !== "skipped" && status !== "adjusted") {
return null;
}
return (
<form action={formAction} className="space-y-4" aria-busy={isPending}>
<input type="hidden" name="activityId" value={activityId} />
{status === "skipped" ? (
<>
<input type="hidden" name="skipReasonId" value={skipReasonId} />
<div className="space-y-2">
<Label className="text-sm font-semibold text-foreground">
Waarom is deze activiteit overgeslagen?
</Label>
<Select
disabled={isPending}
value={skipReasonId}
onValueChange={(value) => setSkipReasonId(value ?? skipReasons[0]?.id ?? "")}
>
<SelectTrigger className="h-11 w-full rounded-[1.15rem] bg-background/80 px-4 text-sm">
<SelectValue placeholder="Kies een skip-reden" />
</SelectTrigger>
<SelectContent>
{skipReasons.map((skipReason) => (
<SelectItem key={skipReason.id} value={skipReason.id}>
{skipReason.labelNl}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor={`activity-notes-${activityId}`} className="text-sm font-semibold text-foreground">
Extra toelichting
</Label>
<textarea
id={`activity-notes-${activityId}`}
name="notes"
className="min-h-24 w-full rounded-[1.15rem] border border-input bg-background/80 px-4 py-3 text-sm text-foreground outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
disabled={isPending}
maxLength={500}
placeholder="Optioneel: wat speelde mee?"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
</>
) : (
<div className="space-y-2">
<Label htmlFor={`activity-notes-${activityId}`} className="text-sm font-semibold text-foreground">
Wat heb je aangepast?
</Label>
<textarea
id={`activity-notes-${activityId}`}
name="notes"
className="min-h-28 w-full rounded-[1.15rem] border border-input bg-background/80 px-4 py-3 text-sm text-foreground outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
disabled={isPending}
maxLength={500}
placeholder="Beschrijf kort wat je hebt aangepast aan duur, vorm of intensiteit."
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
)}
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs leading-6 text-muted-foreground" aria-live="polite">
{isPending
? "Evaluatie wordt opgeslagen..."
: status === "skipped"
? "Voeg optioneel context toe, zodat later duidelijker is waarom deze activiteit niet doorging."
: "Beschrijf kort wat je hebt aangepast, zodat dagreflectie later betekenisvoller wordt."}
</p>
<Button
type="submit"
size="sm"
disabled={
isPending ||
(status === "skipped" && !skipReasonId) ||
(status === "adjusted" && !notes.trim())
}
className="rounded-full px-4"
>
{isPending ? "Evaluatie opslaan..." : "Evaluatie opslaan"}
</Button>
</div>
</form>
);
}

View file

@ -17,7 +17,7 @@ const statusOptions: Array<{
}> = [
{ value: "planned", label: "Gepland" },
{ value: "completed", label: "Uitgevoerd" },
{ value: "skipped", label: "Geschipt" },
{ value: "skipped", label: "Overgeslagen" },
{ value: "adjusted", label: "Aangepast" },
];

View file

@ -5,14 +5,20 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ActivityEvaluationFields } from "@/components/planning/activity-evaluation-fields";
import { ActivityStatusActions } from "@/components/planning/activity-status-actions";
import { deriveActivityEnergyPoints } from "@/lib/planning/meter";
import type { ActivityCategory, ActivityRecord } from "@/lib/planning/types";
import type {
ActivityCategory,
ActivityRecord,
SkipReason,
} from "@/lib/planning/types";
import { cn } from "@/lib/utils";
type TodayActivitiesListProps = {
activities: ActivityRecord[];
categories: ActivityCategory[];
skipReasons: SkipReason[];
};
function getCategoryLabel(categories: ActivityCategory[], categoryId: string) {
@ -78,6 +84,7 @@ function getStatusBadgeClassName(value: ActivityRecord["status"]) {
export function TodayActivitiesList({
activities,
categories,
skipReasons,
}: TodayActivitiesListProps) {
return (
<Card className="py-0">
@ -141,6 +148,19 @@ export function TodayActivitiesList({
status={activity.status}
/>
</div>
{(activity.status === "skipped" || activity.status === "adjusted") ? (
<div className="mt-4 rounded-[var(--radius-lg)] border border-border/60 bg-card/60 p-4">
<ActivityEvaluationFields
key={`${activity.id}-${activity.status}`}
activityId={activity.id}
status={activity.status}
skipReasons={skipReasons}
initialSkipReasonId={activity.skipReasonId}
initialNotes={activity.notes}
/>
</div>
) : null}
</div>
))
)}