feat: Betreuung, Kinderprofile und Aufnahmeverwaltung implementieren

Dashboard mit Abholslots und Filter-Scrolling, Anwesenheit mit Audit-Verlauf sowie Elternanmeldung, Warteliste und bestätigte Aufnahme ergänzen.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul SchoeneckandCopilot committed 2026-11-03 08:01:25 +01:00
1 parent 10af6159b4
commit c1513c040b
36 files changed
+2593

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
"use client";
import { useState, useTransition } from "react";
import { Check, ShieldCheck } from "lucide-react";
import { correctAttendance, saveAloneRules, saveContact } from "@/app/profile-actions";
import type { AloneRule, AttendanceRecord, PickupContact } from "@/lib/profile";
import { displayDay, timeInZone } from "@/lib/time";
import type { ActionResult } from "@/lib/validation";
import { Modal } from "./modal";
export const weekdayNames = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"];
function useSave(onSaved: (message: string) => void) {
const [error, setError] = useState("");
const [pending, startTransition] = useTransition();
function run(action: () => Promise<ActionResult>, message: string) {
setError("");
startTransition(async () => {
try {
const result = await action();
if (result.error) setError(result.error);
else onSaved(message);
} catch (error) {
console.error("Profil speichern", error);
setError("Speichern fehlgeschlagen. Bitte die Verbindung prüfen und erneut versuchen.");
}
});
}
return { error, pending, run };
}
function FormFooter({ pending, onClose, label = "Speichern" }: {
pending: boolean; onClose: () => void; label?: string;
}) {
return <footer className="modal-footer"><button type="button" className="button secondary" onClick={onClose} disabled={pending}>Abbrechen</button>
<button className="button primary" disabled={pending}><Check size={16} />{pending ? "Wird gespeichert …" : label}</button></footer>;
}
export function CorrectionForm({ childId, record, timeZone, onClose, onSaved }: {
childId: string; record: AttendanceRecord; timeZone: string;
onClose: () => void; onSaved: (message: string) => void;
}) {
const [status, setStatus] = useState(record.status);
const { error, pending, run } = useSave(onSaved);
return <Modal title="Anwesenheit korrigieren" subtitle={`${displayDay(record.day)} · ${timeZone}`} onClose={onClose} busy={pending}>
<form className="stack" onSubmit={(event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
run(() => correctAttendance({
childId, day: record.day, status,
arrival: form.get("arrival") ?? "", pickup: form.get("pickup") ?? "",
pickupKind: form.get("pickupKind") ?? "planned", reason: form.get("reason"),
expectedVersion: record.version,
}), "Anwesenheit korrigiert und im Audit-Log dokumentiert.");
}}>
<fieldset disabled={pending} className="form-fields">
<label>Status<select value={status} onChange={(event) => {
const value = event.target.value;
if (value === "expected" || value === "arrived" || value === "sick" || value === "picked_up") setStatus(value);
}}>
<option value="expected">Kein Anwesenheitseintrag / zurücksetzen</option>
<option value="arrived">Angekommen, noch nicht ausgecheckt</option>
<option value="picked_up">Bereits gegangen / abgeholt</option>
<option value="sick">Krank gemeldet</option>
</select></label>
{(status === "arrived" || status === "picked_up") && <div className="form-grid">
<label>Tatsächliche Ankunft<input type="time" name="arrival" required defaultValue={record.arrivedAt ? timeInZone(record.arrivedAt, timeZone) : ""} /></label>
{status === "picked_up" && <label>Tatsächliche Abholung<input type="time" name="pickup" required defaultValue={record.pickedUpAt ? timeInZone(record.pickedUpAt, timeZone) : ""} /></label>}
</div>}
{status === "picked_up" && <label>Art des Verlassens<select name="pickupKind" defaultValue={record.pickupKind ?? "planned"}>
<option value="planned">Planmäßig abgeholt</option><option value="parents">Außerplanmäßig von Eltern abgeholt</option><option value="alone">Allein gegangen</option>
</select></label>}
<label>Grund der Korrektur<textarea name="reason" required minLength={5} maxLength={500} rows={3} placeholder="z. B. Abholung um 15:10 vergessen auszuchecken" /></label>
<div className="info-box"><ShieldCheck size={18} /><p>Alter und neuer Stand, dein Zugang und der Grund bleiben im Audit-Log erhalten. Historische Alleingänge werden als tatsächliches Ereignis dokumentiert, unabhängig von heutigen Berechtigungen.</p></div>
</fieldset>
{error && <p className="form-error" role="alert">{error}</p>}
<FormFooter pending={pending} onClose={onClose} label="Korrektur speichern" />
</form>
</Modal>;
}
export function ContactForm({ childId, contact, onClose, onSaved }: {
childId: string; contact?: PickupContact; onClose: () => void; onSaved: (message: string) => void;
}) {
const { error, pending, run } = useSave(onSaved);
return <Modal title={contact ? "Abholperson bearbeiten" : "Abholperson hinzufügen"} subtitle="Berechtigung und Kontakt für dieses Kind" onClose={onClose} busy={pending}>
<form className="stack" onSubmit={(event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
run(() => saveContact({ id: contact?.id, childId, name: form.get("name"), relationship: form.get("relationship"),
phone: form.get("phone"), authorized: form.get("authorized") === "on", notes: form.get("notes") }), "Abholperson gespeichert.");
}}>
<fieldset disabled={pending} className="form-fields">
<label>Name<input name="name" required maxLength={80} defaultValue={contact?.name} /></label>
<div className="form-grid"><label>Beziehung zum Kind<input name="relationship" required maxLength={80} placeholder="z. B. Mutter, Vater, Oma" defaultValue={contact?.relationship} /></label>
<label>Telefon (optional)<input name="phone" type="tel" maxLength={40} defaultValue={contact?.phone} /></label></div>
<label className="checkbox-label"><input type="checkbox" name="authorized" defaultChecked={contact?.authorized ?? true} />Darf dieses Kind abholen</label>
<label>Hinweise (optional)<textarea name="notes" rows={3} maxLength={500} defaultValue={contact?.notes} placeholder="z. B. Ausweis bei Abholung prüfen" /></label>
<p className="small muted">Eine Berechtigung kann entzogen werden. Frühere Angaben bleiben im Audit-Log nachvollziehbar.</p>
</fieldset>
{error && <p className="form-error" role="alert">{error}</p>}
<FormFooter pending={pending} onClose={onClose} />
</form>
</Modal>;
}
export function AloneRulesForm({ childId, initialRules, onClose, onSaved }: {
childId: string; initialRules: AloneRule[]; onClose: () => void; onSaved: (message: string) => void;
}) {
const [rules, setRules] = useState(initialRules);
const { error, pending, run } = useSave(onSaved);
return <Modal title="Alleingehen regeln" subtitle="An welchen Tagen und ab welcher Uhrzeit darf das Kind allein gehen?" onClose={onClose} busy={pending}>
<form className="stack" onSubmit={(event) => {
event.preventDefault();
run(() => saveAloneRules({ childId, rules }), "Regeln zum Alleingehen gespeichert.");
}}>
<fieldset disabled={pending} className="form-fields">
<p className="small muted">Nicht ausgewählt heißt: An diesem Wochentag ist kein Alleingang erlaubt. Zeiten ersetzen nicht den Betreuungsplan; es wird niemals automatisch ausgecheckt.</p>
<div className="alone-rule-table">{weekdayNames.map((name, index) => {
const weekday = index + 1;
const rule = rules.find((r) => r.weekday === weekday);
return <div key={name} className="alone-rule-row"><label className="checkbox-label">
<input type="checkbox" checked={!!rule} onChange={() => setRules((previous) => rule ? previous.filter((r) => r.weekday !== weekday) : [...previous, { weekday, earliest: "15:00" }])} />{name}</label>
<label>Ab<input type="time" required={!!rule} disabled={!rule} aria-label={`${name} allein ab`} value={rule?.earliest ?? ""}
onChange={(event) => setRules((previous) => previous.map((r) => r.weekday === weekday ? { ...r, earliest: event.target.value } : r))} /></label></div>;
})}</div>
</fieldset>
{error && <p className="form-error" role="alert">{error}</p>}
<FormFooter pending={pending} onClose={onClose} />
</form>
</Modal>;
}