Fix live test follow-up issues

This commit is contained in:
t.indorf
2026-05-19 14:28:32 +02:00
parent 3fa3b9b108
commit 99ff978116
9 changed files with 79 additions and 24 deletions
+2
View File
@@ -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,
}: {
+27 -8
View File
@@ -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." };
@@ -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<Set<string>>(
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({
</div>
<div className="divide-y">
{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);
+6 -4
View File
@@ -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 (
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-8">
@@ -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();
});
}
+9 -5
View File
@@ -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
+3 -1
View File
@@ -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 (
<form action={formAction} className="space-y-4">
<input type="hidden" name="callbackUrl" value={callbackUrl} />
<div className="space-y-1.5">
<Label htmlFor="email">E-Mail</Label>
<Input
+17 -2
View File
@@ -15,11 +15,26 @@ import { LoginForm } from "./login-form";
export const metadata = { title: "Anmelden · Kita-Planer" };
export default async function LoginPage() {
type LoginPageProps = {
searchParams?: Promise<{ callbackUrl?: string | string[] }>;
};
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 (
<div className="flex min-h-screen items-center justify-center bg-muted/30 px-4 py-12">
@@ -29,7 +44,7 @@ export default async function LoginPage() {
<CardDescription>Willkommen zurück.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<LoginForm />
<LoginForm callbackUrl={callbackUrl} />
<p className="text-center text-sm text-muted-foreground">
Noch keinen Account?{" "}
<Link href="/register" className="font-medium text-foreground underline">
+4
View File
@@ -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");
}