Add non-destructive dev seed on startup

This commit is contained in:
t.indorf
2026-05-06 22:31:07 +02:00
parent 9f452fccac
commit b686e714ff
77 changed files with 10862 additions and 87 deletions
+186
View File
@@ -0,0 +1,186 @@
"use client";
import { useState, useTransition } from "react";
import { Plus, X, Shield, Trash2 } from "lucide-react";
import { ParentDuty, ParentDutyAssignment, User } from "@prisma/client";
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 { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { createDuty, assignDuty, removeDutyAssignment, deleteDuty } from "./duty-actions";
type DutyWithAssignments = ParentDuty & {
assignments: ParentDutyAssignment[];
};
export function DutyManager({
user,
allDuties,
userAssignments,
}: {
user: User;
allDuties: DutyWithAssignments[];
userAssignments: (ParentDutyAssignment & { duty: ParentDuty })[];
}) {
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-4 w-4" />
<span className="hidden sm:inline">Ämter</span>
{userAssignments.length > 0 && (
<Badge variant="secondary" className="ml-1 px-1 py-0 h-4">
{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-sm font-semibold">Aktuelle Ämter</h4>
{userAssignments.length === 0 ? (
<p className="text-sm text-muted-foreground italic">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 bg-primary">
{assignment.duty.name}
<button
className="ml-1 rounded-full hover:bg-primary-foreground/20 p-0.5"
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 pt-4">
<h4 className="text-sm font-semibold">Vorhandenes Amt zuweisen</h4>
{unassignedDuties.length === 0 ? (
<p className="text-sm text-muted-foreground italic">Keine weiteren Ämter verfügbar.</p>
) : (
<div className="flex flex-wrap gap-2">
{unassignedDuties.map((duty) => (
<Badge
key={duty.id}
variant="outline"
className="cursor-pointer hover:bg-muted gap-1 pr-1 group relative"
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 hover:bg-destructive/20 text-destructive p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
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 pt-4">
<h4 className="text-sm font-semibold">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>
);
}