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