feat(reminders): preventive-care surface — wann, was, wo

Add the Vorsorge page and section: an upcoming-reminders list sorted by
server-computed next-due, with a create form (measurement-linked or
free-text, cadence, notify hour, location) and a per-card Erledigt and
delete action. Cards stay neutral-coloured regardless of due state — the
status reads through a discreet badge only, never an alarming tint. Copy
ships in all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 23:33:37 +02:00
1 parent 8058d81590
commit c6045482c3
11 files changed
+826

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
"use client";
/**
* v1.17.1 — data hook for Vorsorge (measurement) reminders.
*
* Wraps the `/api/measurement-reminders` CRUD + satisfy surface behind
* TanStack Query. Every mutation invalidates the one
* `measurementReminders()` root so the section list and the dashboard
* tile repaint in lockstep. The DTO is the server-authoritative shape —
* the client renders `nextDueAt` as-is, never recomputing.
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiDelete, apiGet, apiPatch, apiPost } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
export interface MeasurementReminder {
id: string;
label: string;
measurementType: string | null;
intervalDays: number | null;
rrule: string | null;
anchorDate: string | null;
notifyHour: number;
location: string | null;
nextDueAt: string | null;
lastSatisfiedAt: string | null;
enabled: boolean;
createdAt: string;
updatedAt: string;
}
export interface CreateMeasurementReminderBody {
label: string;
measurementType?: string | null;
intervalDays?: number | null;
rrule?: string | null;
anchorDate?: string | null;
notifyHour?: number;
location?: string | null;
enabled?: boolean;
}
export type UpdateMeasurementReminderBody = Partial<
CreateMeasurementReminderBody
>;
const BASE = "/api/measurement-reminders";
export function useMeasurementReminders(enabled = true) {
return useQuery({
queryKey: queryKeys.measurementReminders(),
queryFn: () => apiGet<MeasurementReminder[]>(BASE),
enabled,
});
}
export function useMeasurementReminderMutations() {
const qc = useQueryClient();
const invalidate = () =>
qc.invalidateQueries({ queryKey: queryKeys.measurementReminders() });
const create = useMutation({
mutationKey: queryKeys.measurementReminderCreate(),
mutationFn: (body: CreateMeasurementReminderBody) =>
apiPost<MeasurementReminder>(BASE, body),
onSuccess: invalidate,
});
const update = useMutation({
mutationKey: queryKeys.measurementReminderUpdate(),
mutationFn: ({
id,
body,
}: {
id: string;
body: UpdateMeasurementReminderBody;
}) => apiPatch<MeasurementReminder>(`${BASE}/${id}`, body),
onSuccess: invalidate,
});
const remove = useMutation({
mutationKey: queryKeys.measurementReminderDelete(),
mutationFn: (id: string) =>
apiDelete<{ deleted: boolean }>(`${BASE}/${id}`),
onSuccess: invalidate,
});
const satisfy = useMutation({
mutationKey: queryKeys.measurementReminderSatisfy(),
mutationFn: (id: string) =>
apiPost<MeasurementReminder>(`${BASE}/${id}/satisfy`),
onSuccess: invalidate,
});
return { create, update, remove, satisfy };
}