213 lines
6.9 KiB
TypeScript
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>
|
|
);
|
|
}
|