195 lines
6.3 KiB
TypeScript
195 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState, useId } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
import { completeOnboardingAction, type OnboardingState } from "./actions";
|
|
|
|
const initialOnboardingState: OnboardingState = { attempt: 0 };
|
|
|
|
export function OnboardingForm() {
|
|
const [state, formAction, pending] = useActionState(
|
|
completeOnboardingAction,
|
|
initialOnboardingState,
|
|
);
|
|
|
|
// Bumping the key on each attempt forces uncontrolled inputs to remount with
|
|
// the echoed defaultValue from the server — otherwise React's <form action>
|
|
// semantics would wipe them after a failed submit.
|
|
const attemptKey = state.attempt;
|
|
|
|
const kitaError = state.errors?.kitaName?.[0];
|
|
const minError = state.errors?.notdienstMinPerChildPerMonth?.[0];
|
|
|
|
return (
|
|
<form action={formAction} className="space-y-8">
|
|
{/* ----- Schritt 1: Name ----- */}
|
|
<fieldset className="space-y-4">
|
|
<legend className="text-sm font-semibold text-muted-foreground">
|
|
Schritt 1 · Grunddaten
|
|
</legend>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="kitaName">Name des Elternvereins / der Kita</Label>
|
|
<Input
|
|
key={`kitaName-${attemptKey}`}
|
|
id="kitaName"
|
|
name="kitaName"
|
|
required
|
|
placeholder="z.B. Waldameisen e.V."
|
|
defaultValue={state.values?.kitaName ?? ""}
|
|
aria-invalid={!!kitaError}
|
|
aria-describedby={kitaError ? "kitaName-error" : undefined}
|
|
/>
|
|
{kitaError && (
|
|
<p
|
|
id="kitaName-error"
|
|
role="alert"
|
|
aria-live="polite"
|
|
className="text-xs text-destructive"
|
|
>
|
|
{kitaError}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</fieldset>
|
|
|
|
{/* ----- Schritt 2: Module ----- */}
|
|
<fieldset className="space-y-3">
|
|
<legend className="text-sm font-semibold text-muted-foreground">
|
|
Schritt 2 · Module aktivieren
|
|
</legend>
|
|
<ModuleCheckbox
|
|
key={`notdienstModuleEnabled-${attemptKey}`}
|
|
name="notdienstModuleEnabled"
|
|
label="Notdienst-Planung"
|
|
description="Verfügbarkeiten erfassen, Plan generieren, bei Krankheitsausfall alarmieren."
|
|
defaultChecked={state.values?.notdienstModuleEnabled ?? true}
|
|
/>
|
|
<ModuleCheckbox
|
|
key={`terminModuleEnabled-${attemptKey}`}
|
|
name="terminModuleEnabled"
|
|
label="Terminkalender"
|
|
description="Kita-Feste, Schließtage und private Anfragen koordinieren."
|
|
defaultChecked={state.values?.terminModuleEnabled ?? true}
|
|
/>
|
|
<ModuleCheckbox
|
|
key={`adressbuchModuleEnabled-${attemptKey}`}
|
|
name="adressbuchModuleEnabled"
|
|
label="Eltern-Adressbuch"
|
|
description="Eltern können sich auf Opt-In-Basis untereinander finden."
|
|
defaultChecked={state.values?.adressbuchModuleEnabled ?? true}
|
|
/>
|
|
</fieldset>
|
|
|
|
{/* ----- Schritt 3: Notdienst-Regeln ----- */}
|
|
<fieldset className="space-y-4">
|
|
<legend className="text-sm font-semibold text-muted-foreground">
|
|
Schritt 3 · Notdienst-Regel
|
|
</legend>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="notdienstMinPerChildPerMonth">
|
|
Mindest-Verfügbarkeiten pro Kind und Monat
|
|
</Label>
|
|
<Input
|
|
key={`notdienstMinPerChildPerMonth-${attemptKey}`}
|
|
id="notdienstMinPerChildPerMonth"
|
|
name="notdienstMinPerChildPerMonth"
|
|
type="number"
|
|
min={0}
|
|
max={31}
|
|
defaultValue={state.values?.notdienstMinPerChildPerMonth ?? 2}
|
|
required
|
|
aria-invalid={!!minError}
|
|
aria-describedby={
|
|
minError
|
|
? "notdienstMinPerChildPerMonth-error"
|
|
: "notdienstMinPerChildPerMonth-helper"
|
|
}
|
|
/>
|
|
{!minError && (
|
|
<p
|
|
id="notdienstMinPerChildPerMonth-helper"
|
|
className="text-xs text-muted-foreground"
|
|
>
|
|
Wie viele Tage müssen Eltern pro Monat als verfügbar markieren?
|
|
Diesen Wert kannst du später jederzeit ändern.
|
|
</p>
|
|
)}
|
|
{minError && (
|
|
<p
|
|
id="notdienstMinPerChildPerMonth-error"
|
|
role="alert"
|
|
aria-live="polite"
|
|
className="text-xs text-destructive"
|
|
>
|
|
{minError}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</fieldset>
|
|
|
|
{state.errors?._form?.[0] && (
|
|
<p
|
|
role="alert"
|
|
aria-live="polite"
|
|
className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"
|
|
>
|
|
{state.errors._form[0]}
|
|
</p>
|
|
)}
|
|
|
|
<Button type="submit" className="w-full" disabled={pending} size="lg">
|
|
{pending ? "Wird eingerichtet…" : "Kita einrichten & loslegen"}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function ModuleCheckbox({
|
|
name,
|
|
label,
|
|
description,
|
|
defaultChecked,
|
|
}: {
|
|
name: string;
|
|
label: string;
|
|
description: string;
|
|
defaultChecked?: boolean;
|
|
}) {
|
|
const id = useId();
|
|
const titleId = `${id}-title`;
|
|
const descId = `${id}-desc`;
|
|
return (
|
|
// The wrapping <label htmlFor> keeps the whole card clickable to toggle
|
|
// the checkbox. aria-labelledby narrows the accessible name to just the
|
|
// module title, with the description exposed via aria-describedby so
|
|
// screen readers can present it as secondary info rather than as part of
|
|
// the primary label.
|
|
<label
|
|
htmlFor={id}
|
|
className="flex cursor-pointer items-start gap-3 rounded-md border p-4 transition-colors hover:bg-muted/40"
|
|
>
|
|
<Checkbox
|
|
id={id}
|
|
name={name}
|
|
defaultChecked={defaultChecked}
|
|
aria-labelledby={titleId}
|
|
aria-describedby={descId}
|
|
className="mt-0.5"
|
|
/>
|
|
<div className="grid gap-1 leading-none">
|
|
<span id={titleId} className="text-sm font-medium">
|
|
{label}
|
|
</span>
|
|
<span id={descId} className="text-xs text-muted-foreground">
|
|
{description}
|
|
</span>
|
|
</div>
|
|
</label>
|
|
);
|
|
}
|