Add non-destructive dev seed on startup
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import { saveNotdienstAvailabilities } from "./actions";
|
||||
|
||||
// =====================================================================
|
||||
// NotdienstForm · Client Component
|
||||
// ---------------------------------------------------------------------
|
||||
// Erlaubt das Auswählen von N Tagen aus der Liste der Werktage des
|
||||
// Zielmonats. Speichert als Array von Datum-Strings.
|
||||
// =====================================================================
|
||||
|
||||
interface NotdienstFormProps {
|
||||
targetYear: number;
|
||||
targetMonth: number;
|
||||
isLocked: boolean;
|
||||
requiredDaysTotal: number;
|
||||
initialSelectedDates: string[]; // ISO Strings
|
||||
childrenIds: string[];
|
||||
}
|
||||
|
||||
export function NotdienstForm({
|
||||
targetYear,
|
||||
targetMonth,
|
||||
isLocked,
|
||||
requiredDaysTotal,
|
||||
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 [selectedDates, setSelectedDates] = useState<Set<string>>(
|
||||
new Set(initialSelectedDates),
|
||||
);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const workingDays = getWorkingDaysOfMonth(targetYear, targetMonth);
|
||||
const selectedCount = selectedDates.size;
|
||||
const isFulfilled = selectedCount >= requiredDaysTotal;
|
||||
|
||||
function toggleDate(dateStr: string) {
|
||||
if (isLocked || isPending) return;
|
||||
|
||||
const newSet = new Set(selectedDates);
|
||||
if (newSet.has(dateStr)) {
|
||||
newSet.delete(dateStr);
|
||||
} else {
|
||||
newSet.add(dateStr);
|
||||
}
|
||||
setSelectedDates(newSet);
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (isLocked) return;
|
||||
|
||||
if (!isFulfilled) {
|
||||
toast.error(
|
||||
`Bitte wähle noch ${requiredDaysTotal - selectedCount} weitere Termine aus.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const payload = {
|
||||
dates: Array.from(selectedDates),
|
||||
childrenIds,
|
||||
};
|
||||
|
||||
const result = await saveNotdienstAvailabilities(payload);
|
||||
|
||||
if (result?.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success("Verfügbarkeiten erfolgreich gespeichert.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Fortschritt / Sperr-Hinweis */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between",
|
||||
isLocked ? "bg-destructive/5" : "bg-card",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{isLocked ? (
|
||||
<Info className="mt-0.5 h-5 w-5 shrink-0 text-destructive" />
|
||||
) : isFulfilled ? (
|
||||
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-emerald-500" />
|
||||
) : (
|
||||
<Info className="mt-0.5 h-5 w-5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
{isLocked
|
||||
? "Planung abgeschlossen"
|
||||
: "Deine ausgewählten Termine"}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isLocked
|
||||
? "Die Frist für diesen Monat ist abgelaufen. Änderungen sind nicht mehr möglich."
|
||||
: `Du hast ${selectedCount} von ${requiredDaysTotal} benötigten Tagen markiert.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isLocked || isPending || !isFulfilled}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
{isLocked ? "Gesperrt" : "Speichern"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Datumsliste */}
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<p className="text-sm font-medium">Werktage im {format(new Date(targetYear, targetMonth - 1, 1), "MMMM", { locale: de })}</p>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{workingDays.map((day) => {
|
||||
const dateStr = day.toISOString();
|
||||
const isSelected = selectedDates.has(dateStr);
|
||||
const isToday = day.getTime() === new Date().setHours(0, 0, 0, 0);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={dateStr}
|
||||
type="button"
|
||||
disabled={isLocked || isPending}
|
||||
onClick={() => toggleDate(dateStr)}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between px-4 py-3 text-left transition-colors",
|
||||
!isLocked && !isPending && "hover:bg-muted/50",
|
||||
isSelected && "bg-primary/5 hover:bg-primary/10",
|
||||
(isLocked || isPending) && "cursor-not-allowed opacity-70",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Custom Checkbox Indicator */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-5 w-5 items-center justify-center rounded border",
|
||||
isSelected
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-input bg-background",
|
||||
)}
|
||||
>
|
||||
{isSelected && <CheckCircle2 className="h-3.5 w-3.5" />}
|
||||
</div>
|
||||
<div>
|
||||
<span className={cn("font-medium", isSelected && "text-primary")}>
|
||||
{format(day, "EEEE, dd. MMMM", { locale: de })}
|
||||
</span>
|
||||
{isToday && (
|
||||
<span className="ml-2 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
||||
Heute
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user