339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { format } from "date-fns";
|
|
import { de } from "date-fns/locale";
|
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
import { useSession } from "next-auth/react";
|
|
import { toast } from "sonner";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Calendar } from "@/components/ui/calendar";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { createTaskSchema, updateTaskSchema } from "@/lib/validations";
|
|
import { useCreateTask, useUpdateTask, useTask } from "@/hooks/use-tasks";
|
|
import { useUsers } from "@/hooks/use-users";
|
|
import { canEditTask, canChangeStatus } from "@/lib/permissions";
|
|
import { TaskStatus } from "@prisma/client";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* Props für TaskModal Component
|
|
*/
|
|
interface TaskModalProps {
|
|
taskId?: string;
|
|
trigger: React.ReactNode;
|
|
mode?: "create" | "edit";
|
|
}
|
|
|
|
/**
|
|
* Task Modal Component
|
|
* Formular zum Erstellen/Bearbeiten von Tasks
|
|
* - Nur shadcn/ui Komponenten
|
|
* - React Hook Form mit Zod Validierung
|
|
* - Berechtigungsprüfung
|
|
*/
|
|
export function TaskModal({ taskId, trigger, mode = "create" }: TaskModalProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const { data: session } = useSession();
|
|
|
|
// Hooks
|
|
const { data: task, isLoading: taskLoading } = useTask(taskId || "");
|
|
const { data: users, isLoading: usersLoading } = useUsers();
|
|
const createTask = useCreateTask();
|
|
const updateTask = useUpdateTask();
|
|
|
|
// Form Setup
|
|
const form = useForm({
|
|
resolver: zodResolver(mode === "create" ? createTaskSchema : updateTaskSchema),
|
|
defaultValues: {
|
|
wo: "",
|
|
was: "",
|
|
bisWann: new Date(),
|
|
assigneeId: "",
|
|
status: TaskStatus.UNERLEDIGT,
|
|
beauftragtAm: undefined as Date | undefined,
|
|
firmaBeauftragt: "",
|
|
},
|
|
});
|
|
|
|
// Lade Task-Daten bei Edit-Mode
|
|
useEffect(() => {
|
|
if (task && mode === "edit") {
|
|
form.reset({
|
|
wo: task.wo,
|
|
was: task.was,
|
|
bisWann: new Date(task.bisWann),
|
|
assigneeId: task.assigneeId || "",
|
|
status: task.status,
|
|
beauftragtAm: task.beauftragtAm ? new Date(task.beauftragtAm) : undefined,
|
|
firmaBeauftragt: task.firmaBeauftragt || "",
|
|
});
|
|
}
|
|
}, [task, mode, form]);
|
|
|
|
/**
|
|
* Berechtigungsprüfung
|
|
*/
|
|
const canEdit =
|
|
mode === "create" ||
|
|
(task && session?.user
|
|
? canEditTask(session.user.role, task, session.user.id)
|
|
: false);
|
|
|
|
const canOnlyChangeStatus =
|
|
mode === "edit" &&
|
|
task &&
|
|
session?.user &&
|
|
!canEditTask(session.user.role, task, session.user.id) &&
|
|
canChangeStatus(session.user.role, task, session.user.id);
|
|
|
|
/**
|
|
* Form Submit Handler
|
|
*/
|
|
const onSubmit = async (data: any) => {
|
|
try {
|
|
if (mode === "create") {
|
|
await createTask.mutateAsync(data);
|
|
toast.success("Aufgabe erstellt", {
|
|
description: "Die Aufgabe wurde erfolgreich erstellt.",
|
|
});
|
|
} else if (taskId) {
|
|
// Bearbeiter können nur Status ändern
|
|
const updateData = canOnlyChangeStatus
|
|
? { status: data.status }
|
|
: data;
|
|
|
|
await updateTask.mutateAsync({ id: taskId, data: updateData });
|
|
toast.success("Aufgabe aktualisiert", {
|
|
description: "Die Aufgabe wurde erfolgreich aktualisiert.",
|
|
});
|
|
}
|
|
|
|
setOpen(false);
|
|
form.reset();
|
|
} catch (error) {
|
|
toast.error("Fehler", {
|
|
description: "Es ist ein Fehler aufgetreten.",
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{mode === "create" ? "Neue Aufgabe" : "Aufgabe bearbeiten"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{mode === "create"
|
|
? "Erstellen Sie eine neue Aufgabe"
|
|
: "Bearbeiten Sie die Aufgabe"}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
{/* Wo */}
|
|
<FormField
|
|
control={form.control}
|
|
name="wo"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Wo</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="Ort der Aufgabe"
|
|
{...field}
|
|
disabled={!canEdit || canOnlyChangeStatus}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* Was */}
|
|
<FormField
|
|
control={form.control}
|
|
name="was"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Was</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Beschreibung der Aufgabe"
|
|
{...field}
|
|
disabled={!canEdit || canOnlyChangeStatus}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* Bis wann */}
|
|
<FormField
|
|
control={form.control}
|
|
name="bisWann"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-col">
|
|
<FormLabel>Bis wann</FormLabel>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<FormControl>
|
|
<Button
|
|
variant="outline"
|
|
className={cn(
|
|
"w-full pl-3 text-left font-normal",
|
|
!field.value && "text-muted-foreground"
|
|
)}
|
|
disabled={!canEdit || canOnlyChangeStatus}
|
|
>
|
|
{field.value ? (
|
|
format(field.value, "dd.MM.yyyy", { locale: de })
|
|
) : (
|
|
<span>Datum wählen</span>
|
|
)}
|
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
</Button>
|
|
</FormControl>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0" align="start">
|
|
<Calendar
|
|
mode="single"
|
|
selected={field.value}
|
|
onSelect={field.onChange}
|
|
disabled={(date) =>
|
|
date < new Date(new Date().setHours(0, 0, 0, 0))
|
|
}
|
|
initialFocus
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* Zuständigkeit */}
|
|
<FormField
|
|
control={form.control}
|
|
name="assigneeId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Zuständigkeit</FormLabel>
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
defaultValue={field.value}
|
|
disabled={!canEdit || canOnlyChangeStatus || usersLoading}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Benutzer wählen" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{users?.filter(user => user.role === "BEARBEITER").map((user) => (
|
|
<SelectItem key={user.id} value={user.id}>
|
|
{user.name} ({user.role})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* Status - Nur bei Edit Mode */}
|
|
{mode === "edit" && (
|
|
<FormField
|
|
control={form.control}
|
|
name="status"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Status</FormLabel>
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
defaultValue={field.value}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Status wählen" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value={TaskStatus.UNERLEDIGT}>
|
|
Unerledigt
|
|
</SelectItem>
|
|
<SelectItem value={TaskStatus.IN_BEARBEITUNG}>
|
|
In Bearbeitung
|
|
</SelectItem>
|
|
<SelectItem value={TaskStatus.ERLEDIGT}>
|
|
Erledigt
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setOpen(false)}
|
|
>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={form.formState.isSubmitting || (!canEdit && !canOnlyChangeStatus)}
|
|
>
|
|
{form.formState.isSubmitting ? "Wird gespeichert..." : "Speichern"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|