Merge branch 'codex/replace-alert-dialogs' into codex/notdienst-month-settings

This commit is contained in:
t.indorf
2026-05-21 10:46:31 +02:00
4 changed files with 159 additions and 21 deletions
+22 -11
View File
@@ -1,10 +1,11 @@
"use client"; "use client";
import { useTransition } from "react"; import { useState, useTransition } from "react";
import { Loader2, BellRing } from "lucide-react"; import { Loader2, BellRing } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { triggerAlertAction } from "./actions"; import { triggerAlertAction } from "./actions";
export function AlertButton({ export function AlertButton({
@@ -12,11 +13,10 @@ export function AlertButton({
}: { }: {
assignmentId: string; assignmentId: string;
}) { }) {
const [confirmOpen, setConfirmOpen] = useState(false);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const handleAlert = () => { const handleAlert = () => {
if (!confirm("Bist du sicher? Dies löst den Notdienst-Alarm aus.")) return;
startTransition(async () => { startTransition(async () => {
const result = await triggerAlertAction(assignmentId); const result = await triggerAlertAction(assignmentId);
if ("error" in result && result.error) { if ("error" in result && result.error) {
@@ -28,13 +28,24 @@ export function AlertButton({
}; };
return ( return (
<Button variant="destructive" onClick={handleAlert} disabled={isPending}> <ConfirmDialog
{isPending ? ( open={confirmOpen}
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> onOpenChange={setConfirmOpen}
) : ( title="Notdienst-Alarm auslösen?"
<BellRing className="mr-2 h-4 w-4" /> description="Der Alarm informiert die hinterlegten Kontakte per E-Mail über den Notdienst."
)} confirmLabel="Alarm auslösen"
Alarm auslösen pendingLabel="Alarm wird ausgelöst..."
</Button> isPending={isPending}
onConfirm={handleAlert}
>
<Button variant="destructive" disabled={isPending}>
{isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<BellRing className="mr-2 h-4 w-4" />
)}
Alarm auslösen
</Button>
</ConfirmDialog>
); );
} }
@@ -5,6 +5,7 @@ import { Plus, Pencil, Search, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -40,6 +41,9 @@ type EducatorDto = {
export function ErzieherList({ educators }: { educators: EducatorDto[] }) { export function ErzieherList({ educators }: { educators: EducatorDto[] }) {
const [openCreate, setOpenCreate] = useState(false); const [openCreate, setOpenCreate] = useState(false);
const [editingEducator, setEditingEducator] = useState<EducatorDto | null>(null); const [editingEducator, setEditingEducator] = useState<EducatorDto | null>(null);
const [educatorToDelete, setEducatorToDelete] = useState<EducatorDto | null>(
null,
);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const handleCreate = (formData: FormData) => { const handleCreate = (formData: FormData) => {
@@ -79,14 +83,14 @@ export function ErzieherList({ educators }: { educators: EducatorDto[] }) {
}); });
}; };
const handleDelete = (id: string) => { const handleDelete = (educator: EducatorDto) => {
if (!confirm("Wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.")) return;
startTransition(async () => { startTransition(async () => {
const res = await deleteEducator(id); const res = await deleteEducator(educator.id);
if (res.error) { if (res.error) {
toast.error(res.error); toast.error(res.error);
} else { } else {
toast.success("ErzieherIn gelöscht."); toast.success("ErzieherIn gelöscht.");
setEducatorToDelete(null);
} }
}); });
}; };
@@ -193,8 +197,9 @@ export function ErzieherList({ educators }: { educators: EducatorDto[] }) {
variant="ghost" variant="ghost"
size="icon" size="icon"
className="text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]" className="text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]"
onClick={() => handleDelete(ed.id)} onClick={() => setEducatorToDelete(ed)}
disabled={isPending} disabled={isPending}
aria-label={`${ed.firstName} ${ed.lastName} löschen`}
> >
<Trash2 className="h-4 w-4 [stroke-width:1.7]" /> <Trash2 className="h-4 w-4 [stroke-width:1.7]" />
</Button> </Button>
@@ -237,6 +242,25 @@ export function ErzieherList({ educators }: { educators: EducatorDto[] }) {
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<ConfirmDialog
open={!!educatorToDelete}
onOpenChange={(open) => {
if (!open) setEducatorToDelete(null);
}}
title="ErzieherIn löschen?"
description={
educatorToDelete
? `${educatorToDelete.firstName} ${educatorToDelete.lastName} wird aus dem Personalstamm entfernt.`
: "Dieser Eintrag wird aus dem Personalstamm entfernt."
}
confirmLabel="Löschen"
pendingLabel="Wird gelöscht..."
isPending={isPending}
onConfirm={() => {
if (educatorToDelete) handleDelete(educatorToDelete);
}}
/>
</div> </div>
); );
} }
@@ -5,6 +5,7 @@ import { Pencil, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -51,6 +52,7 @@ export function MyChildrenManager({
const [editingChild, setEditingChild] = useState<MyChild | null>(null); const [editingChild, setEditingChild] = useState<MyChild | null>(null);
const [editPayload, setEditPayload] = const [editPayload, setEditPayload] =
useState<ChildPayload>(emptyChildPayload); useState<ChildPayload>(emptyChildPayload);
const [childToDelete, setChildToDelete] = useState<MyChild | null>(null);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
function openEditor(child: MyChild) { function openEditor(child: MyChild) {
@@ -91,11 +93,6 @@ export function MyChildrenManager({
} }
function handleDelete(child: MyChild) { function handleDelete(child: MyChild) {
const confirmed = confirm(
`${child.firstName} ${child.lastName} wirklich entfernen?`,
);
if (!confirmed) return;
startTransition(async () => { startTransition(async () => {
const result = await deleteMyChild(child.id); const result = await deleteMyChild(child.id);
if (result.error) { if (result.error) {
@@ -104,6 +101,7 @@ export function MyChildrenManager({
} }
toast.success("Kind entfernt."); toast.success("Kind entfernt.");
setChildToDelete(null);
}); });
} }
@@ -157,7 +155,7 @@ export function MyChildrenManager({
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]" className="h-7 w-7 text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]"
onClick={() => handleDelete(child)} onClick={() => setChildToDelete(child)}
disabled={isPending || !canManage} disabled={isPending || !canManage}
aria-label="Kind löschen" aria-label="Kind löschen"
> >
@@ -250,6 +248,25 @@ export function MyChildrenManager({
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<ConfirmDialog
open={!!childToDelete}
onOpenChange={(open) => {
if (!open) setChildToDelete(null);
}}
title="Kind entfernen?"
description={
childToDelete
? `${childToDelete.firstName} ${childToDelete.lastName} wird aus deinem Haushalt entfernt.`
: "Dieses Kind wird aus deinem Haushalt entfernt."
}
confirmLabel="Entfernen"
pendingLabel="Wird entfernt..."
isPending={isPending}
onConfirm={() => {
if (childToDelete) handleDelete(childToDelete);
}}
/>
</div> </div>
); );
} }
+86
View File
@@ -0,0 +1,86 @@
"use client";
import { type ReactNode } from "react";
import { AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
type ConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: ReactNode;
confirmLabel: string;
cancelLabel?: string;
pendingLabel?: string;
isPending?: boolean;
children?: ReactNode;
onConfirm: () => void | Promise<void>;
};
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel,
cancelLabel = "Abbrechen",
pendingLabel = "Wird ausgeführt...",
isPending = false,
children,
onConfirm,
}: ConfirmDialogProps) {
async function handleConfirm() {
await onConfirm();
onOpenChange(false);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
{children ? <DialogTrigger asChild>{children}</DialogTrigger> : null}
<DialogContent className="border-[var(--danger)]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-[var(--danger)]">
<AlertTriangle className="h-5 w-5" />
{title}
</DialogTitle>
<DialogDescription className="pt-1 text-sm leading-6">
{description}
</DialogDescription>
</DialogHeader>
<div className="rounded-lg border border-[var(--danger)] bg-[var(--danger-soft)] p-3 text-sm text-[var(--danger)]">
Diese Aktion kann nicht rückgängig gemacht werden.
</div>
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
{cancelLabel}
</Button>
<Button
type="button"
variant="destructive"
onClick={handleConfirm}
disabled={isPending}
>
{isPending ? pendingLabel : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}