diff --git a/frontend/src/app/(app)/cases/[id]/ereignisse/[eventId]/page.tsx b/frontend/src/app/(app)/cases/[id]/ereignisse/[eventId]/page.tsx new file mode 100644 index 0000000..7a214f5 --- /dev/null +++ b/frontend/src/app/(app)/cases/[id]/ereignisse/[eventId]/page.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useParams } from "next/navigation"; +import { api } from "@/lib/api"; +import type { CaseEvent, Case } from "@/lib/types"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; +import { NotesList } from "@/components/notes/NotesList"; +import { Skeleton } from "@/components/ui/Skeleton"; +import { format, parseISO } from "date-fns"; +import { de } from "date-fns/locale"; +import { + AlertTriangle, + FileText, + Scale, + ArrowRightLeft, + Calendar, + MessageSquare, + Gavel, + Info, +} from "lucide-react"; +import Link from "next/link"; + +const EVENT_TYPE_CONFIG: Record< + string, + { label: string; icon: typeof Info; color: string } +> = { + status_changed: { + label: "Statusaenderung", + icon: ArrowRightLeft, + color: "bg-blue-50 text-blue-700", + }, + deadline_created: { + label: "Frist erstellt", + icon: Calendar, + color: "bg-amber-50 text-amber-700", + }, + deadline_completed: { + label: "Frist erledigt", + icon: Calendar, + color: "bg-emerald-50 text-emerald-700", + }, + document_uploaded: { + label: "Dokument hochgeladen", + icon: FileText, + color: "bg-violet-50 text-violet-700", + }, + hearing_scheduled: { + label: "Verhandlung angesetzt", + icon: Gavel, + color: "bg-rose-50 text-rose-700", + }, + note_added: { + label: "Notiz hinzugefuegt", + icon: MessageSquare, + color: "bg-neutral-100 text-neutral-700", + }, + case_created: { + label: "Akte erstellt", + icon: Scale, + color: "bg-emerald-50 text-emerald-700", + }, +}; + +const DEFAULT_EVENT_CONFIG = { + label: "Ereignis", + icon: Info, + color: "bg-neutral-100 text-neutral-600", +}; + +function DetailSkeleton() { + return ( +
+ +
+ + + + +
+
+ ); +} + +export default function CaseEventDetailPage() { + const { id: caseId, eventId } = useParams<{ + id: string; + eventId: string; + }>(); + + const { data: caseData } = useQuery({ + queryKey: ["case", caseId], + queryFn: () => api.get(`/cases/${caseId}`), + }); + + const { + data: event, + isLoading, + error, + } = useQuery({ + queryKey: ["case-event", eventId], + queryFn: () => api.get(`/case-events/${eventId}`), + }); + + if (isLoading) return ; + + if (error || !event) { + return ( +
+
+ +
+

+ Ereignis nicht gefunden +

+

+ Das Ereignis existiert nicht oder Sie haben keine Berechtigung. +

+ + Zurueck zur Akte + +
+ ); + } + + const typeConfig = + EVENT_TYPE_CONFIG[event.event_type ?? ""] ?? DEFAULT_EVENT_CONFIG; + const TypeIcon = typeConfig.icon; + + return ( +
+ + + {/* Header */} +
+
+ +
+
+

+ {event.title} +

+

+ {event.event_date + ? format(parseISO(event.event_date), "d. MMMM yyyy, HH:mm", { + locale: de, + }) + : format(parseISO(event.created_at), "d. MMMM yyyy, HH:mm", { + locale: de, + })} +

+
+
+ + {/* Description */} + {event.description && ( +
+

+ Beschreibung +

+

+ {event.description} +

+
+ )} + + {/* Metadata */} +
+

+ Metadaten +

+
+
+
Typ:
+
+ + {typeConfig.label} + +
+
+ {event.created_by && ( +
+
Erstellt von:
+
{event.created_by}
+
+ )} +
+
Erstellt am:
+
+ {format(parseISO(event.created_at), "d. MMMM yyyy, HH:mm", { + locale: de, + })} +
+
+ {event.metadata && + Object.keys(event.metadata).length > 0 && + Object.entries(event.metadata).map(([key, value]) => ( +
+
{key}:
+
{String(value)}
+
+ ))} +
+
+ + {/* Notes */} +
+ +
+
+ ); +} diff --git a/frontend/src/app/(app)/fristen/[id]/page.tsx b/frontend/src/app/(app)/fristen/[id]/page.tsx new file mode 100644 index 0000000..5068a24 --- /dev/null +++ b/frontend/src/app/(app)/fristen/[id]/page.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useParams, useRouter } from "next/navigation"; +import { api } from "@/lib/api"; +import type { Deadline } from "@/lib/types"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; +import { NotesList } from "@/components/notes/NotesList"; +import { Skeleton } from "@/components/ui/Skeleton"; +import { format, parseISO, formatDistanceToNow, isPast } from "date-fns"; +import { de } from "date-fns/locale"; +import { + AlertTriangle, + CheckCircle2, + Clock, + ExternalLink, +} from "lucide-react"; +import Link from "next/link"; +import { toast } from "sonner"; + +interface DeadlineDetail extends Deadline { + case_number?: string; + case_title?: string; +} + +const STATUS_CONFIG: Record< + string, + { label: string; bg: string; icon: typeof Clock } +> = { + pending: { label: "Offen", bg: "bg-amber-50 text-amber-700", icon: Clock }, + completed: { + label: "Erledigt", + bg: "bg-emerald-50 text-emerald-700", + icon: CheckCircle2, + }, + overdue: { + label: "Ueberfaellig", + bg: "bg-red-50 text-red-700", + icon: AlertTriangle, + }, +}; + +function getEffectiveStatus(d: DeadlineDetail): string { + if (d.status === "completed") return "completed"; + if (isPast(parseISO(d.due_date))) return "overdue"; + return "pending"; +} + +function DetailSkeleton() { + return ( +
+ +
+ + + + +
+
+ ); +} + +export default function DeadlineDetailPage() { + const { id } = useParams<{ id: string }>(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const { + data: deadline, + isLoading, + error, + } = useQuery({ + queryKey: ["deadline", id], + queryFn: () => api.get(`/deadlines/${id}`), + }); + + const completeMutation = useMutation({ + mutationFn: () => api.patch(`/deadlines/${id}/complete`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["deadline", id] }); + queryClient.invalidateQueries({ queryKey: ["deadlines"] }); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); + toast.success("Frist als erledigt markiert"); + }, + onError: () => toast.error("Fehler beim Abschliessen der Frist"), + }); + + if (isLoading) return ; + + if (error || !deadline) { + return ( +
+
+ +
+

+ Frist nicht gefunden +

+

+ Die Frist existiert nicht oder Sie haben keine Berechtigung. +

+ + Zurueck zu Fristen + +
+ ); + } + + const status = getEffectiveStatus(deadline); + const config = STATUS_CONFIG[status] ?? STATUS_CONFIG.pending; + const StatusIcon = config.icon; + const dueDate = parseISO(deadline.due_date); + const relativeTime = formatDistanceToNow(dueDate, { + addSuffix: true, + locale: de, + }); + + return ( +
+ + + {/* Header */} +
+
+
+ + + {config.label} + +

+ {deadline.title} +

+
+ {deadline.description && ( +

+ {deadline.description} +

+ )} +
+ {deadline.status !== "completed" && ( + + )} +
+ + {/* Due date */} +
+
+ + Faellig: {format(dueDate, "d. MMMM yyyy", { locale: de })} + + + ({relativeTime}) + +
+ {deadline.warning_date && ( +

+ Warnung am:{" "} + {format(parseISO(deadline.warning_date), "d. MMMM yyyy", { + locale: de, + })} +

+ )} + {deadline.original_due_date && + deadline.original_due_date !== deadline.due_date && ( +

+ Urspruengliches Datum:{" "} + {format(parseISO(deadline.original_due_date), "d. MMMM yyyy", { + locale: de, + })} +

+ )} + {deadline.completed_at && ( +

+ Erledigt am:{" "} + {format(parseISO(deadline.completed_at), "d. MMMM yyyy, HH:mm", { + locale: de, + })} +

+ )} +
+ + {/* Case context */} + {deadline.case_id && ( +
+
+
+

+ Akte +

+

+ {deadline.case_number + ? `Az. ${deadline.case_number}` + : "Verknuepfte Akte"} + {deadline.case_title && ` — ${deadline.case_title}`} +

+
+ + Zur Akte + + +
+
+ )} + + {/* Source info */} + {deadline.source && deadline.source !== "manual" && ( +
+

+ Quelle +

+

+ {deadline.source === "calculated" + ? "Berechnet" + : deadline.source === "caldav" + ? "CalDAV Sync" + : deadline.source} + {deadline.rule_id && ` (Regel: ${deadline.rule_id})`} +

+
+ )} + + {/* Notes */} +
+ +
+
+ ); +} diff --git a/frontend/src/app/(app)/fristen/neu/page.tsx b/frontend/src/app/(app)/fristen/neu/page.tsx new file mode 100644 index 0000000..e6eec13 --- /dev/null +++ b/frontend/src/app/(app)/fristen/neu/page.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { api } from "@/lib/api"; +import type { Case, Deadline } from "@/lib/types"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; +import { useState } from "react"; +import { toast } from "sonner"; + +const inputClass = + "w-full rounded-md border border-neutral-200 px-3 py-1.5 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"; +const labelClass = "mb-1 block text-xs font-medium text-neutral-600"; + +export default function NewDeadlinePage() { + const router = useRouter(); + const queryClient = useQueryClient(); + + const [caseId, setCaseId] = useState(""); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [dueDate, setDueDate] = useState(""); + const [warningDate, setWarningDate] = useState(""); + const [notes, setNotes] = useState(""); + + const { data: casesData } = useQuery({ + queryKey: ["cases"], + queryFn: () => api.get<{ cases: Case[]; total: number } | Case[]>("/cases"), + }); + + const cases = Array.isArray(casesData) + ? casesData + : Array.isArray(casesData?.cases) + ? casesData.cases + : []; + + const createMutation = useMutation({ + mutationFn: (body: Record) => + api.post(`/cases/${caseId}/deadlines`, body), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["deadlines"] }); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); + toast.success("Frist erstellt"); + router.push(`/fristen/${data.id}`); + }, + onError: () => toast.error("Fehler beim Erstellen der Frist"), + }); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!caseId || !title.trim() || !dueDate) return; + + const body: Record = { + title: title.trim(), + due_date: new Date(dueDate).toISOString(), + source: "manual", + }; + if (description.trim()) body.description = description.trim(); + if (warningDate) body.warning_date = new Date(warningDate).toISOString(); + if (notes.trim()) body.notes = notes.trim(); + + createMutation.mutate(body); + } + + return ( +
+ + +

+ Neue Frist anlegen +

+

+ Erstellen Sie eine neue Frist fuer eine Akte. +

+ +
+
+ + +
+ +
+ + setTitle(e.target.value)} + required + className={inputClass} + placeholder="z.B. Klageschrift einreichen" + /> +
+ +
+ + setDescription(e.target.value)} + className={inputClass} + placeholder="Optionale Beschreibung" + /> +
+ +
+
+ + setDueDate(e.target.value)} + required + className={inputClass} + /> +
+
+ + setWarningDate(e.target.value)} + className={inputClass} + /> +
+
+ +
+ +