Fix live test follow-up issues
This commit is contained in:
@@ -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,
|
||||
}: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user