diff --git a/frontend/src/app/(app)/fristen/rechner/page.tsx b/frontend/src/app/(app)/fristen/rechner/page.tsx index f4c3f75..29ac2b3 100644 --- a/frontend/src/app/(app)/fristen/rechner/page.tsx +++ b/frontend/src/app/(app)/fristen/rechner/page.tsx @@ -1,61 +1,29 @@ "use client"; -import { DeadlineCalculator } from "@/components/deadlines/DeadlineCalculator"; -import { DeadlineWizard } from "@/components/deadlines/DeadlineWizard"; +import { FristenRechner } from "@/components/deadlines/FristenRechner"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; export default function FristenrechnerPage() { - const [mode, setMode] = useState<"wizard" | "quick">("wizard"); - return (
-
-
- - - Zurueck zu Fristen - -

- Fristenbestimmung -

-

- {mode === "wizard" - ? "Vollstaendige Verfahrens-Timeline mit automatischer Fristenberechnung" - : "Schnellberechnung einzelner Fristen nach Verfahrensart"} -

-
- - {/* Mode toggle */} -
- - -
+
+ + + Zurueck zu Fristen + +

+ Fristenrechner +

+

+ Verfahrensart waehlen, Fristen einsehen und Termine berechnen +

- {mode === "wizard" ? : } +
); } diff --git a/frontend/src/components/deadlines/FristenRechner.tsx b/frontend/src/components/deadlines/FristenRechner.tsx new file mode 100644 index 0000000..40ffccc --- /dev/null +++ b/frontend/src/components/deadlines/FristenRechner.tsx @@ -0,0 +1,602 @@ +"use client"; + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { api } from "@/lib/api"; +import type { + ProceedingType, + RuleTreeNode, + CalculateResponse, + Case, +} from "@/lib/types"; +import { format, parseISO } from "date-fns"; +import { de } from "date-fns/locale"; +import { + Scale, + Users, + Gavel, + FileText, + Clock, + CalendarDays, + AlertTriangle, + ChevronRight, + RotateCcw, + Loader2, + Check, + FolderOpen, +} from "lucide-react"; +import { useState, useMemo } from "react"; +import { toast } from "sonner"; + +// --- Helpers --- + +function formatDuration(value: number, unit: string): string { + if (value === 0) return ""; + const labels: Record = { + days: value === 1 ? "Tag" : "Tage", + weeks: value === 1 ? "Woche" : "Wochen", + months: value === 1 ? "Monat" : "Monate", + }; + return `${value} ${labels[unit] || unit}`; +} + +function getPartyIcon(party?: string) { + switch (party) { + case "claimant": + return ; + case "defendant": + return ; + case "court": + return ; + default: + return ; + } +} + +function getPartyLabel(party?: string): string { + switch (party) { + case "claimant": + return "Klaeger"; + case "defendant": + return "Beklagter"; + case "court": + return "Gericht"; + case "both": + return "Beide Parteien"; + default: + return ""; + } +} + +interface FlatRule { + id: string; + name: string; + duration_value: number; + duration_unit: string; + rule_code?: string; + primary_party?: string; + event_type?: string; + is_mandatory: boolean; + deadline_notes?: string; + description?: string; + depth: number; +} + +function flattenRuleTree(nodes: RuleTreeNode[], depth = 0): FlatRule[] { + const result: FlatRule[] = []; + for (const node of nodes) { + result.push({ + id: node.id, + name: node.name, + duration_value: node.duration_value, + duration_unit: node.duration_unit, + rule_code: node.rule_code, + primary_party: node.primary_party, + event_type: node.event_type, + is_mandatory: node.is_mandatory, + deadline_notes: node.deadline_notes, + description: node.description, + depth, + }); + if (node.children && node.children.length > 0) { + result.push(...flattenRuleTree(node.children, depth + 1)); + } + } + return result; +} + +// --- Group labels --- + +const categoryLabels: Record = { + hauptverfahren: "Hauptverfahren", + im_verfahren: "Verfahren im Verfahren", + rechtsbehelf: "Rechtsbehelfe", +}; + +const jurisdictionLabels: Record = { + UPC: "UPC", + DE: "Deutsche Patentverfahren", +}; + +interface TypeGroup { + key: string; + label: string; + items: ProceedingType[]; +} + +function groupProceedingTypes(types: ProceedingType[]): TypeGroup[] { + const groups: TypeGroup[] = []; + const seen = new Set(); + for (const pt of types) { + const j = pt.jurisdiction ?? "Sonstige"; + const c = pt.category ?? "hauptverfahren"; + const key = `${j}::${c}`; + if (!seen.has(key)) { + seen.add(key); + const jLabel = jurisdictionLabels[j] ?? j; + const cLabel = categoryLabels[c] ?? c; + const label = j === "DE" ? jLabel : `${jLabel} — ${cLabel}`; + groups.push({ key, label, items: [] }); + } + groups.find((g) => g.key === key)!.items.push(pt); + } + return groups; +} + +// --- Main Component --- + +export function FristenRechner() { + const [selectedType, setSelectedType] = useState(null); + const [expandedRuleId, setExpandedRuleId] = useState(null); + const [triggerDate, setTriggerDate] = useState( + new Date().toISOString().split("T")[0], + ); + const [calcResults, setCalcResults] = useState< + Record + >({}); + const [savingRuleId, setSavingRuleId] = useState(null); + const [selectedCaseId, setSelectedCaseId] = useState(""); + const queryClient = useQueryClient(); + + // Fetch proceeding types + const { data: proceedingTypes, isLoading: typesLoading } = useQuery({ + queryKey: ["proceeding-types"], + queryFn: () => api.get("/proceeding-types"), + }); + + // Fetch rule tree when type is selected + const { data: ruleTree, isLoading: rulesLoading } = useQuery({ + queryKey: ["deadline-rules", selectedType], + queryFn: () => api.get(`/deadline-rules/${selectedType}`), + enabled: !!selectedType, + }); + + // Fetch cases for "save to case" + const { data: cases } = useQuery({ + queryKey: ["cases"], + queryFn: () => api.get("/cases"), + enabled: savingRuleId !== null, + }); + + // Calculate single deadline + const calcMutation = useMutation({ + mutationFn: (params: { + proceeding_type: string; + trigger_event_date: string; + selected_rule_ids: string[]; + }) => api.post("/deadlines/calculate", params), + onSuccess: (data, variables) => { + if (data.deadlines && data.deadlines.length > 0) { + const d = data.deadlines[0]; + setCalcResults((prev) => ({ + ...prev, + [variables.selected_rule_ids[0]]: { + due_date: d.due_date, + original_due_date: d.original_due_date, + was_adjusted: d.was_adjusted, + }, + })); + } + }, + }); + + // Save deadline to case + const saveMutation = useMutation({ + mutationFn: (params: { + caseId: string; + deadline: { title: string; due_date: string; original_due_date?: string; rule_code?: string }; + }) => + api.post(`/cases/${params.caseId}/deadlines`, { + title: params.deadline.title, + due_date: params.deadline.due_date, + original_due_date: params.deadline.original_due_date, + rule_code: params.deadline.rule_code, + source: "calculator", + }), + onSuccess: () => { + toast.success("Frist auf Akte gespeichert"); + queryClient.invalidateQueries({ queryKey: ["deadlines"] }); + setSavingRuleId(null); + setSelectedCaseId(""); + }, + onError: () => { + toast.error("Fehler beim Speichern"); + }, + }); + + // Flat list of rules + const flatRules = useMemo(() => { + if (!ruleTree) return []; + return flattenRuleTree(ruleTree); + }, [ruleTree]); + + // Groups + const groups = useMemo(() => { + if (!proceedingTypes) return []; + return groupProceedingTypes(proceedingTypes); + }, [proceedingTypes]); + + const selectedPT = proceedingTypes?.find((pt) => pt.code === selectedType); + + function handleTypeSelect(code: string) { + setSelectedType(code); + setExpandedRuleId(null); + setCalcResults({}); + setSavingRuleId(null); + } + + function handleReset() { + setSelectedType(null); + setExpandedRuleId(null); + setCalcResults({}); + setSavingRuleId(null); + setSelectedCaseId(""); + } + + function handleRuleClick(ruleId: string) { + if (expandedRuleId === ruleId) { + setExpandedRuleId(null); + return; + } + setExpandedRuleId(ruleId); + setSavingRuleId(null); + // Auto-calculate with current date + if (selectedType && triggerDate) { + calcMutation.mutate({ + proceeding_type: selectedType, + trigger_event_date: triggerDate, + selected_rule_ids: [ruleId], + }); + } + } + + function handleDateChange(ruleId: string, date: string) { + setTriggerDate(date); + if (selectedType && date) { + calcMutation.mutate({ + proceeding_type: selectedType, + trigger_event_date: date, + selected_rule_ids: [ruleId], + }); + } + } + + return ( +
+ {/* Step 1: Proceeding Type Cards */} +
+
+

+ Verfahrensart waehlen +

+ {selectedType && ( + + )} +
+ + {typesLoading ? ( +
+ +
+ ) : ( +
+ {groups.map((group) => ( +
+
+ {group.label} +
+
+ {group.items.map((pt) => { + const isSelected = selectedType === pt.code; + return ( + + ); + })} +
+
+ ))} +
+ )} +
+ + {/* Step 2: Deadline Rules for Selected Type */} + {selectedType && ( +
+
+
+

+ Fristen: {selectedPT?.name} +

+ {flatRules.length > 0 && ( +

+ {flatRules.length} Fristen — Frist anklicken zum Berechnen +

+ )} +
+
+ + {rulesLoading ? ( +
+ +
+ ) : flatRules.length === 0 ? ( +
+ Keine Fristenregeln fuer diesen Verfahrenstyp hinterlegt. +
+ ) : ( +
+ {flatRules.map((rule, i) => { + const isExpanded = expandedRuleId === rule.id; + const result = calcResults[rule.id]; + const duration = formatDuration(rule.duration_value, rule.duration_unit); + + return ( +
+ {/* Rule Row */} + + + {/* Step 3: Expanded Calculation Panel */} + {isExpanded && ( +
+
+ {/* Date picker */} +
+ + + handleDateChange(rule.id, e.target.value) + } + className="rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400" + /> +
+ + {/* Result */} + {calcMutation.isPending && + calcMutation.variables?.selected_rule_ids[0] === rule.id ? ( +
+ + Berechne... +
+ ) : result ? ( +
+
+
+ Fristende +
+
+ + + {format( + parseISO(result.due_date), + "dd. MMMM yyyy", + { locale: de }, + )} + +
+ {result.was_adjusted && ( +
+ + Angepasst (Original:{" "} + {format( + parseISO(result.original_due_date), + "dd.MM.yyyy", + )} + ) +
+ )} +
+ + {/* Save to case button */} + +
+ ) : null} +
+ + {/* Save to case panel */} + {savingRuleId === rule.id && result && ( +
+ + +
+ )} + + {/* Notes */} + {rule.deadline_notes && ( +

+ {rule.deadline_notes} +

+ )} +
+ )} +
+ ); + })} +
+ )} +
+ )} + + {/* Empty state */} + {!selectedType && !typesLoading && ( +
+
+ +
+

+ Fristenrechner +

+

+ Waehlen Sie oben eine Verfahrensart, um alle zugehoerigen Fristen + anzuzeigen und einzelne Termine zu berechnen. +

+
+ )} + + {/* Calculation error */} + {calcMutation.isError && ( +
+ + Fehler bei der Berechnung. Bitte Eingaben pruefen. +
+ )} +
+ ); +}