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.
+
+ )}
+
+ );
+}