From 99ff978116e96e9dc0df08262dd822db629cf933 Mon Sep 17 00:00:00 2001 From: "t.indorf" Date: Tue, 19 May 2026 14:28:32 +0200 Subject: [PATCH] Fix live test follow-up issues --- src/app/dashboard/kalender/page.tsx | 2 ++ src/app/dashboard/notdienst/actions.ts | 35 ++++++++++++++----- .../dashboard/notdienst/notdienst-form.tsx | 12 ++++--- src/app/dashboard/notdienst/page.tsx | 10 +++--- .../profil/_components/contact-form.tsx | 3 ++ src/app/login/actions.ts | 14 +++++--- src/app/login/login-form.tsx | 4 ++- src/app/login/page.tsx | 19 ++++++++-- src/lib/date-utils.ts | 4 +++ 9 files changed, 79 insertions(+), 24 deletions(-) diff --git a/src/app/dashboard/kalender/page.tsx b/src/app/dashboard/kalender/page.tsx index 3859b3e..8e08265 100644 --- a/src/app/dashboard/kalender/page.tsx +++ b/src/app/dashboard/kalender/page.tsx @@ -12,6 +12,8 @@ import { PendingAnfragen, type PendingTerminDto } from "./_components/pending-an import { TerminList, type CalendarListItemDto } from "./_components/termin-list"; import { TerminRequestModal } from "./_components/termin-request-modal"; +export const metadata = { title: "Terminkalender · Kita-Planer" }; + export default async function KalenderPage({ searchParams, }: { diff --git a/src/app/dashboard/notdienst/actions.ts b/src/app/dashboard/notdienst/actions.ts index 448e349..8c53ee0 100644 --- a/src/app/dashboard/notdienst/actions.ts +++ b/src/app/dashboard/notdienst/actions.ts @@ -15,11 +15,23 @@ import { getTargetMonthData } from "@/lib/date-utils"; // Die Termine werden Round-Robin auf die Kinder aufgeteilt. // ===================================================================== +const dateKeyPattern = /^\d{4}-\d{2}-\d{2}$/; + const payloadSchema = z.object({ childrenIds: z.array(z.string()).min(1), - dates: z.array(z.string().datetime()), // ISO strings + dates: z.array( + z.string().regex(dateKeyPattern).refine((value) => { + const date = dateKeyToUtcDate(value); + return value === date.toISOString().slice(0, 10); + }), + ), }); +function dateKeyToUtcDate(dateKey: string) { + const [year, month, day] = dateKey.split("-").map(Number); + return new Date(Date.UTC(year, month - 1, day)); +} + export async function saveNotdienstAvailabilities(payloadRaw: { childrenIds: string[]; dates: string[]; @@ -35,7 +47,8 @@ export async function saveNotdienstAvailabilities(payloadRaw: { return { error: "Ungültige Eingabedaten." }; } - const { childrenIds, dates } = parsed.data; + const { childrenIds } = parsed.data; + const dates = Array.from(new Set(parsed.data.dates)).sort(); // ── 1. Sperrfrist prüfen ─────────────────────────────────────────── const { targetYear, targetMonth, isLocked } = getTargetMonthData(); @@ -79,9 +92,15 @@ export async function saveNotdienstAvailabilities(payloadRaw: { } // Termine in Date-Objekte wandeln und sortieren - const parsedDates = dates - .map((d) => new Date(d)) - .sort((a, b) => a.getTime() - b.getTime()); + const parsedDates = dates.map(dateKeyToUtcDate); + const hasOutsideTargetMonth = parsedDates.some( + (date) => + date.getUTCFullYear() !== targetYear || + date.getUTCMonth() !== targetMonth - 1, + ); + if (hasOutsideTargetMonth) { + return { error: "Bitte wähle Termine im Zielmonat aus." }; + } // Round-Robin Zuweisung const inserts: { @@ -110,8 +129,8 @@ export async function saveNotdienstAvailabilities(payloadRaw: { kitaId, childId: { in: childrenIds }, date: { - gte: new Date(targetYear, targetMonth - 1, 1), - lt: new Date(targetYear, targetMonth, 1), + gte: new Date(Date.UTC(targetYear, targetMonth - 1, 1)), + lt: new Date(Date.UTC(targetYear, targetMonth, 1)), }, }, }); @@ -126,7 +145,7 @@ export async function saveNotdienstAvailabilities(payloadRaw: { }); revalidatePath("/dashboard/notdienst"); - return { success: true }; + return { success: true, dates }; } catch (error) { console.error("Fehler beim Speichern der Notdienste:", error); return { error: "Ein Fehler ist aufgetreten beim Speichern der Termine." }; diff --git a/src/app/dashboard/notdienst/notdienst-form.tsx b/src/app/dashboard/notdienst/notdienst-form.tsx index 76f0a7b..a942731 100644 --- a/src/app/dashboard/notdienst/notdienst-form.tsx +++ b/src/app/dashboard/notdienst/notdienst-form.tsx @@ -1,13 +1,14 @@ "use client"; import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { Info, Loader2, CheckCircle2 } from "lucide-react"; import { format } from "date-fns"; import { de } from "date-fns/locale"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; -import { getWorkingDaysOfMonth } from "@/lib/date-utils"; +import { formatDateKey, getWorkingDaysOfMonth } from "@/lib/date-utils"; import { Button } from "@/components/ui/button"; import { saveNotdienstAvailabilities } from "./actions"; @@ -23,7 +24,7 @@ interface NotdienstFormProps { targetMonth: number; isLocked: boolean; requiredDaysTotal: number; - initialSelectedDates: string[]; // ISO Strings + initialSelectedDates: string[]; childrenIds: string[]; } @@ -35,7 +36,8 @@ export function NotdienstForm({ initialSelectedDates, childrenIds, }: NotdienstFormProps) { - // Lokaler State für die Auswahl. Ein Datum liegt immer als ISO-String vor (z.B. "2026-06-01T00:00:00.000Z") + const router = useRouter(); + // Lokaler State für die Auswahl. Ein Datum liegt als stabiler yyyy-MM-dd-Schlüssel vor. const [selectedDates, setSelectedDates] = useState>( new Set(initialSelectedDates), ); @@ -78,7 +80,9 @@ export function NotdienstForm({ if (result?.error) { toast.error(result.error); } else { + setSelectedDates(new Set(result.dates)); toast.success("Verfügbarkeiten erfolgreich gespeichert."); + router.refresh(); } }); } @@ -134,7 +138,7 @@ export function NotdienstForm({
{workingDays.map((day) => { - const dateStr = day.toISOString(); + const dateStr = formatDateKey(day); const isSelected = selectedDates.has(dateStr); const isToday = day.getTime() === new Date().setHours(0, 0, 0, 0); diff --git a/src/app/dashboard/notdienst/page.tsx b/src/app/dashboard/notdienst/page.tsx index 1439842..79c886e 100644 --- a/src/app/dashboard/notdienst/page.tsx +++ b/src/app/dashboard/notdienst/page.tsx @@ -4,7 +4,7 @@ import { Info, Calendar, ArrowRight, ShieldAlert } from "lucide-react"; import { requireRole } from "@/lib/auth-utils"; import { prisma } from "@/lib/prisma"; -import { getTargetMonthData } from "@/lib/date-utils"; +import { formatDateKey, getTargetMonthData } from "@/lib/date-utils"; import { Button } from "@/components/ui/button"; import { NotdienstEntry } from "./notdienst-entry"; @@ -45,6 +45,8 @@ export default async function NotdienstPage() { const targetData = getTargetMonthData(); const { targetYear, targetMonth, monthName, isLocked } = targetData; + const targetMonthStart = new Date(Date.UTC(targetYear, targetMonth - 1, 1)); + const targetMonthEnd = new Date(Date.UTC(targetYear, targetMonth, 1)); const requiredDaysTotal = children.length * kita.notdienstMinPerChildPerMonth; const childIds = children.map((child) => child.id); @@ -58,8 +60,8 @@ export default async function NotdienstPage() { kitaId, childId: { in: childIds }, date: { - gte: new Date(targetYear, targetMonth - 1, 1), - lt: new Date(targetYear, targetMonth, 1), + gte: targetMonthStart, + lt: targetMonthEnd, }, }, select: { date: true }, @@ -77,7 +79,7 @@ export default async function NotdienstPage() { ]) : [[], 0]; - const selectedDates = availabilities.map((a) => a.date.toISOString()); + const selectedDates = availabilities.map((a) => formatDateKey(a.date)); return (
diff --git a/src/app/dashboard/profil/_components/contact-form.tsx b/src/app/dashboard/profil/_components/contact-form.tsx index f5275c9..a84809b 100644 --- a/src/app/dashboard/profil/_components/contact-form.tsx +++ b/src/app/dashboard/profil/_components/contact-form.tsx @@ -1,6 +1,7 @@ "use client"; import { useTransition } from "react"; +import { useRouter } from "next/navigation"; import { Loader2, Save } from "lucide-react"; import { toast } from "sonner"; @@ -19,6 +20,7 @@ type ContactFormProps = { }; export function ContactForm({ contact }: ContactFormProps) { + const router = useRouter(); const [isPending, startTransition] = useTransition(); function handleSubmit(formData: FormData) { @@ -36,6 +38,7 @@ export function ContactForm({ contact }: ContactFormProps) { } toast.success("Kontaktdaten gespeichert."); + router.refresh(); }); } diff --git a/src/app/login/actions.ts b/src/app/login/actions.ts index 37d1a25..98f5804 100644 --- a/src/app/login/actions.ts +++ b/src/app/login/actions.ts @@ -18,6 +18,7 @@ import { signIn } from "@/auth"; const loginSchema = z.object({ email: z.string().email("Bitte eine gültige E-Mail-Adresse angeben.").toLowerCase().trim(), password: z.string().min(1, "Passwort fehlt."), + callbackUrl: z.string().optional(), }); export type LoginState = { @@ -28,6 +29,12 @@ export type LoginState = { }; }; +function safeCallbackUrl(value: string | undefined) { + if (!value) return "/dashboard"; + if (!value.startsWith("/") || value.startsWith("//")) return "/dashboard"; + return value; +} + export async function loginAction( _prev: LoginState, formData: FormData, @@ -37,16 +44,13 @@ export async function loginAction( return { errors: parsed.error.flatten().fieldErrors }; } const { email, password } = parsed.data; + const redirectTo = safeCallbackUrl(parsed.data.callbackUrl); try { await signIn("credentials", { email, password, - // Auth-Utils entscheiden im Folge-Render, ob /onboarding oder /dashboard. - // Wir landen aus Sicherheitsgründen erstmal generisch auf der Wurzel. - // Der Proxy (proxy.ts) übernimmt das Routing: - // → /onboarding wenn kein kitaId, → /dashboard wenn vorhanden. - redirectTo: "/", + redirectTo, }); } catch (err) { // signIn wirft NEXT_REDIRECT bei Erfolg → das müssen wir bewusst diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx index 3ff7c43..e299c65 100644 --- a/src/app/login/login-form.tsx +++ b/src/app/login/login-form.tsx @@ -10,11 +10,13 @@ import { loginAction, type LoginState } from "./actions"; const initialState: LoginState = {}; -export function LoginForm() { +export function LoginForm({ callbackUrl }: { callbackUrl: string }) { const [state, formAction, pending] = useActionState(loginAction, initialState); return (
+ +
; +}; + +function getSafeCallbackUrl(value: string | string[] | undefined) { + const callbackUrl = Array.isArray(value) ? value[0] : value; + if (!callbackUrl) return "/dashboard"; + if (!callbackUrl.startsWith("/") || callbackUrl.startsWith("//")) { + return "/dashboard"; + } + return callbackUrl; +} + +export default async function LoginPage({ searchParams }: LoginPageProps) { const session = await auth(); if (session?.user?.id) { redirect(getPostLoginRedirect(session.user)); } + const resolvedSearchParams = await searchParams; + const callbackUrl = getSafeCallbackUrl(resolvedSearchParams?.callbackUrl); return (
@@ -29,7 +44,7 @@ export default async function LoginPage() { Willkommen zurück. - +

Noch keinen Account?{" "} diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts index f5b1c49..d9a3a8f 100644 --- a/src/lib/date-utils.ts +++ b/src/lib/date-utils.ts @@ -46,3 +46,7 @@ export function getWorkingDaysOfMonth(year: number, month: number) { .filter((day) => !isWeekend(day)) .map((day) => startOfDay(day)); } + +export function formatDateKey(date: Date) { + return format(date, "yyyy-MM-dd"); +}