Implement ST-401 activity status flows

This commit is contained in:
Janpeter Visser 2026-04-19 09:35:05 +02:00
parent 4966d493cc
commit d0739736aa
7 changed files with 228 additions and 5 deletions

View file

@ -0,0 +1,68 @@
"use client";
import { useActionState } from "react";
import { updateActivityStatusAction } from "@/app/planning/actions";
import { Button } from "@/components/ui/button";
import type { ActivityStatus } from "@/lib/planning/types";
import { cn } from "@/lib/utils";
type ActivityStatusActionsProps = {
activityId: string;
status: ActivityStatus;
};
const statusOptions: Array<{
value: ActivityStatus;
label: string;
}> = [
{ value: "planned", label: "Gepland" },
{ value: "completed", label: "Uitgevoerd" },
{ value: "skipped", label: "Geschipt" },
{ value: "adjusted", label: "Aangepast" },
];
export function ActivityStatusActions({
activityId,
status,
}: ActivityStatusActionsProps) {
const [, formAction, isPending] = useActionState(updateActivityStatusAction, null);
return (
<form action={formAction} className="space-y-3" aria-busy={isPending}>
<input type="hidden" name="activityId" value={activityId} />
<div
className="flex flex-wrap gap-2"
role="group"
aria-label="Status van deze activiteit wijzigen"
>
{statusOptions.map((option) => {
const isCurrent = option.value === status;
return (
<Button
key={option.value}
type="submit"
name="status"
value={option.value}
size="sm"
variant={isCurrent ? "default" : "outline"}
disabled={isPending}
aria-pressed={isCurrent}
className={cn(
"rounded-full px-3",
isPending && "pointer-events-none opacity-70",
)}
>
{option.label}
</Button>
);
})}
</div>
<p className="text-xs leading-6 text-muted-foreground" aria-live="polite">
{isPending
? "Status wordt opgeslagen..."
: "Je kunt de status vandaag direct aanpassen zonder de activiteit te verwijderen."}
</p>
</form>
);
}

View file

@ -5,8 +5,10 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ActivityStatusActions } from "@/components/planning/activity-status-actions";
import { deriveActivityEnergyPoints } from "@/lib/planning/meter";
import type { ActivityCategory, ActivityRecord } from "@/lib/planning/types";
import { cn } from "@/lib/utils";
type TodayActivitiesListProps = {
activities: ActivityRecord[];
@ -41,6 +43,38 @@ function formatPriorityLabel(value: ActivityRecord["priorityLevel"]) {
return "Normaal";
}
function formatStatusLabel(value: ActivityRecord["status"]) {
if (value === "completed") {
return "Uitgevoerd";
}
if (value === "skipped") {
return "Overgeslagen";
}
if (value === "adjusted") {
return "Aangepast";
}
return "Gepland";
}
function getStatusBadgeClassName(value: ActivityRecord["status"]) {
if (value === "completed") {
return "bg-success text-primary-foreground";
}
if (value === "skipped") {
return "bg-warning text-foreground";
}
if (value === "adjusted") {
return "bg-secondary text-secondary-foreground";
}
return "bg-secondary text-secondary-foreground";
}
export function TodayActivitiesList({
activities,
categories,
@ -75,8 +109,13 @@ export function TodayActivitiesList({
{getCategoryLabel(categories, activity.categoryId)}
</p>
</div>
<span className="rounded-full bg-secondary px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-secondary-foreground">
Gepland
<span
className={cn(
"rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em]",
getStatusBadgeClassName(activity.status),
)}
>
{formatStatusLabel(activity.status)}
</span>
</div>
@ -95,6 +134,13 @@ export function TodayActivitiesList({
{deriveActivityEnergyPoints(activity)}
</p>
</div>
<div className="mt-5 border-t border-border/60 pt-4">
<ActivityStatusActions
activityId={activity.id}
status={activity.status}
/>
</div>
</div>
))
)}