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

@ -74,6 +74,11 @@ const planningStatusToasts: Record<string, StatusToast> = {
title: "Activiteit bijgewerkt",
message: "De status van je activiteit is opgeslagen.",
},
"activity-evaluation-saved": {
variant: "success",
title: "Evaluatie opgeslagen",
message: "De extra context bij deze activiteit is bijgewerkt.",
},
};
const planningErrorToasts: Record<string, StatusToast> = {
@ -93,6 +98,17 @@ const planningErrorToasts: Record<string, StatusToast> = {
title: "Status niet opgeslagen",
message: "De activiteitstatus kon niet worden bijgewerkt. Probeer het opnieuw.",
},
"invalid-activity-evaluation": {
variant: "error",
title: "Evaluatie niet opgeslagen",
message:
"Controleer de skip-reden of toelichting en probeer het opnieuw.",
},
"activity-evaluation-failed": {
variant: "error",
title: "Evaluatie niet opgeslagen",
message: "De extra context bij deze activiteit kon niet worden opgeslagen.",
},
};
export function getDashboardStatusToast(status: string | null): StatusToast | null {

View file

@ -169,3 +169,21 @@ export function getUuidValue(
return value;
}
export function getOptionalUuidValue(
formData: FormData,
key: string,
errorCode: string,
): string | null {
const value = getOptionalString(formData, key);
if (!value) {
return null;
}
if (!UUID_VALUE_PATTERN.test(value)) {
fail(errorCode);
}
return value;
}

View file

@ -10,6 +10,7 @@ import type {
ActivitiesForDateStatus,
ActivityStatus,
SkipReason,
UpdateActivityEvaluationSubmission,
UpdateActivityStatusSubmission,
} from "@/lib/planning/types";
import { ensureProfileBundleForCurrentUser } from "@/lib/profile/service";
@ -166,6 +167,26 @@ async function assertCategoryExists(
}
}
async function assertSkipReasonExists(
supabase: SupabaseServerClient,
skipReasonId: string,
): Promise<void> {
const { data, error } = await supabase
.from("skip_reasons")
.select("id")
.eq("id", skipReasonId)
.eq("is_active", true)
.maybeSingle();
if (error) {
throw new Error(`Skip-reden kon niet worden gevalideerd: ${error.message}`);
}
if (!data) {
throw new Error("Ongeldige skip-reden.");
}
}
export async function listActivityCategories(): Promise<ActivityCategory[]> {
const supabase = await createClient();
const { data, error } = await supabase
@ -246,8 +267,9 @@ export async function getPlanningPageDataForCurrentUser(): Promise<PlanningPageD
return null;
}
const [categories, activitiesStatus] = await Promise.all([
const [categories, skipReasons, activitiesStatus] = await Promise.all([
listActivityCategories(),
listSkipReasons(),
getTodayActivitiesForCurrentUser(),
]);
@ -256,6 +278,7 @@ export async function getPlanningPageDataForCurrentUser(): Promise<PlanningPageD
activityDate:
activitiesStatus?.activityDate ?? getLocalDateForTimezone(profileBundle.profile.timezone),
categories,
skipReasons,
activities: activitiesStatus?.activities ?? [],
};
}
@ -344,3 +367,77 @@ export async function updateActivityStatusForTodayForCurrentUser(
return mapActivityRow(data);
}
export async function updateActivityEvaluationForTodayForCurrentUser(
submission: UpdateActivityEvaluationSubmission,
): Promise<ActivityRecord> {
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();
const { data: existingActivity, error: existingActivityError } = await supabase
.from("activities")
.select(ACTIVITY_COLUMNS)
.eq("id", submission.activityId)
.eq("user_id", user.id)
.eq("activity_date", activityDate)
.maybeSingle();
if (existingActivityError) {
throw new Error(
`Activiteit voor evaluatie kon niet worden geladen: ${existingActivityError.message}`,
);
}
if (!existingActivity) {
throw new Error("Ongeldige of niet-beschikbare activiteit.");
}
let nextSkipReasonId: string | null = null;
let nextNotes: string | null = null;
if (existingActivity.status === "skipped") {
if (!submission.skipReasonId) {
throw new Error("Skip-reden is verplicht voor een overgeslagen activiteit.");
}
await assertSkipReasonExists(supabase, submission.skipReasonId);
nextSkipReasonId = submission.skipReasonId;
nextNotes = submission.notes;
} else if (existingActivity.status === "adjusted") {
if (!submission.notes) {
throw new Error("Toelichting is verplicht voor een aangepaste activiteit.");
}
nextNotes = submission.notes;
}
const { data, error } = await supabase
.from("activities")
.update({
skip_reason_id: nextSkipReasonId,
notes: nextNotes,
})
.eq("id", existingActivity.id)
.eq("user_id", user.id)
.eq("activity_date", activityDate)
.select(ACTIVITY_COLUMNS)
.single();
if (error) {
throw new Error(`Evaluatiecontext kon niet worden opgeslagen: ${error.message}`);
}
return mapActivityRow(data);
}

View file

@ -58,6 +58,12 @@ export type UpdateActivityStatusSubmission = {
status: ActivityStatus;
};
export type UpdateActivityEvaluationSubmission = {
activityId: string;
skipReasonId: string | null;
notes: string | null;
};
export type ActivitiesForDateStatus = {
timezone: string;
activityDate: string;
@ -68,5 +74,6 @@ export type PlanningPageData = {
timezone: string;
activityDate: string;
categories: ActivityCategory[];
skipReasons: SkipReason[];
activities: ActivityRecord[];
};