Files
kita-planer/src/app/dashboard/families/duty-manager.tsx
T
2026-05-20 22:47:52 +02:00

213 lines
6.9 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { Plus, X, Shield, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { createDuty, assignDuty, removeDutyAssignment, deleteDuty } from "./duty-actions";
type DutyWithAssignments = {
id: string;
name: string;
assignments: {
id: string;
}[];
};
type DutyUser = {
id: string;
firstName: string;
lastName: string;
};
type UserDutyAssignment = {
id: string;
dutyId: string;
duty: {
name: string;
};
};
export function DutyManager({
user,
allDuties,
userAssignments,
}: {
user: DutyUser;
allDuties: DutyWithAssignments[];
userAssignments: UserDutyAssignment[];
}) {
const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const handleCreateDuty = (formData: FormData) => {
const name = formData.get("name") as string;
if (!name) return;
startTransition(async () => {
const res = await createDuty({ name });
if (res.error) {
toast.error(res.error);
} else {
toast.success("Amt erstellt.");
}
});
};
const handleAssign = (dutyId: string) => {
startTransition(async () => {
const res = await assignDuty(user.id, dutyId);
if (res.error) {
toast.error(res.error);
} else {
toast.success("Amt zugewiesen.");
}
});
};
const handleRemove = (assignmentId: string) => {
startTransition(async () => {
const res = await removeDutyAssignment(assignmentId);
if (res.error) {
toast.error(res.error);
} else {
toast.success("Zuweisung entfernt.");
}
});
};
const handleDeleteDuty = (dutyId: string) => {
startTransition(async () => {
const res = await deleteDuty(dutyId);
if (res.error) {
toast.error(res.error);
} else {
toast.success("Amt komplett gelöscht.");
}
});
};
const unassignedDuties = allDuties.filter(
(d) => !userAssignments.some((a) => a.dutyId === d.id)
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 gap-1">
<Shield className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Ämter</span>
{userAssignments.length > 0 && (
<Badge variant="secondary" className="ml-1 h-4 px-1 py-0 tabular-nums">
{userAssignments.length}
</Badge>
)}
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Ämter verwalten</DialogTitle>
<DialogDescription>
Ämter und Dienste für {user.firstName} {user.lastName}.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-6 py-4">
{/* Current Assignments */}
<div className="flex flex-col gap-2">
<h4 className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Aktuelle Ämter
</h4>
{userAssignments.length === 0 ? (
<p className="rounded-lg border border-dashed border-[var(--hairline)] bg-[var(--surface-2)] px-4 py-5 text-sm italic text-[var(--ink-faint)]">
Keine Ämter zugewiesen.
</p>
) : (
<div className="flex flex-wrap gap-2">
{userAssignments.map((assignment) => (
<Badge key={assignment.id} variant="default" className="gap-1 pr-1">
{assignment.duty.name}
<button
className="ml-1 rounded-full p-0.5 transition-colors hover:bg-[oklch(0.98_0.005_45_/_0.18)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[oklch(0.98_0.005_45)]"
onClick={() => handleRemove(assignment.id)}
disabled={isPending}
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
</div>
{/* Assign new duty */}
<div className="flex flex-col gap-2 border-t border-[var(--hairline-soft)] pt-4">
<h4 className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Vorhandenes Amt zuweisen
</h4>
{unassignedDuties.length === 0 ? (
<p className="text-sm italic text-[var(--ink-faint)]">
Keine weiteren Ämter verfügbar.
</p>
) : (
<div className="flex flex-wrap gap-2">
{unassignedDuties.map((duty) => (
<Badge
key={duty.id}
variant="outline"
className="group relative cursor-pointer gap-1 pr-1 hover:border-[var(--accent-mid)] hover:bg-[var(--accent-soft)] hover:text-[var(--accent-deep)]"
onClick={() => handleAssign(duty.id)}
>
<Plus className="h-3 w-3" />
{duty.name}
{/* Only show delete if no one is assigned to it across the entire DB */}
{duty.assignments.length === 0 && (
<button
className="ml-1 rounded-full p-0.5 text-[var(--danger)] opacity-0 transition-opacity hover:bg-[var(--danger-soft)] group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
handleDeleteDuty(duty.id);
}}
disabled={isPending}
title="Amt löschen"
>
<Trash2 className="h-3 w-3" />
</button>
)}
</Badge>
))}
</div>
)}
</div>
{/* Create new duty */}
<div className="flex flex-col gap-2 border-t border-[var(--hairline-soft)] pt-4">
<h4 className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Neues Amt anlegen
</h4>
<form action={handleCreateDuty} className="flex gap-2">
<Input name="name" placeholder="z.B. Wäschedienst" className="h-8" required />
<Button size="sm" className="h-8" disabled={isPending}>
Erstellen
</Button>
</form>
</div>
</div>
</DialogContent>
</Dialog>
);
}