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:
1 parent
10af6159b4
commit
c1513c040b
36 files changed
+2593
No files matched your search
@@ -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>;
|
||||
}
|
||||
Reference in new issue
Block a user