Compare commits

...

7 Commits

Author SHA1 Message Date
m
3599e302df feat: redesign Fristenrechner — cards, no tabs, inline calculation 2026-03-30 21:00:14 +02:00
m
899b461833 feat: redesign Fristenrechner as single-flow card-based UI
Replace the Schnell/Wizard tab layout with a unified flow:
1. Proceeding type selection via compact clickable cards grouped
   by jurisdiction + category (UPC Hauptverfahren, im Verfahren,
   Rechtsbehelfe, Deutsche Patentverfahren)
2. Vertical deadline rule list for the selected type showing name,
   duration, rule code, and acting party
3. Inline expansion on click with date picker, auto-calculated due
   date (via selected_rule_ids API), holiday/weekend adjustment
   note, and save-to-case option

Old DeadlineCalculator.tsx and DeadlineWizard.tsx are no longer
imported but kept for reference.
2026-03-30 20:55:46 +02:00
m
260f65ea02 feat: auto-calculate deadlines on proceeding type selection (no click needed) 2026-03-30 19:41:02 +02:00
m
501b573967 fix: use typed category field instead of Record cast 2026-03-30 19:37:52 +02:00
m
23b8ef4bba chore: gitignore server binary and local state files 2026-03-30 19:34:34 +02:00
m
54c6eb8dae feat: 15 UPC proceeding types in 3 groups + category field
Added 10 new UPC types: DNI, EPO, AMD, CCI, EVP, DAM, COS, REH, DEF, RST.
Grouped as: Hauptverfahren / Verfahren im Verfahren / Rechtsbehelfe.
Frontend dropdown shows sub-groups within jurisdiction. German names throughout.
2026-03-30 19:34:07 +02:00
m
967f2f6d09 feat: direct SMTP email sending via Hostinger (replaces m CLI) 2026-03-30 17:28:40 +02:00
6 changed files with 676 additions and 67 deletions

6
.gitignore vendored
View File

@@ -46,3 +46,9 @@ tmp/
# TypeScript # TypeScript
*.tsbuildinfo *.tsbuildinfo
.worktrees/ .worktrees/
backend/server
backend/.m/
.m/inbox_lastread
backend/server
backend/.m/
.m/inbox_lastread

View File

@@ -39,6 +39,7 @@ type ProceedingType struct {
Name string `db:"name" json:"name"` Name string `db:"name" json:"name"`
Description *string `db:"description" json:"description,omitempty"` Description *string `db:"description" json:"description,omitempty"`
Jurisdiction *string `db:"jurisdiction" json:"jurisdiction,omitempty"` Jurisdiction *string `db:"jurisdiction" json:"jurisdiction,omitempty"`
Category *string `db:"category" json:"category,omitempty"`
DefaultColor string `db:"default_color" json:"default_color"` DefaultColor string `db:"default_color" json:"default_color"`
SortOrder int `db:"sort_order" json:"sort_order"` SortOrder int `db:"sort_order" json:"sort_order"`
IsActive bool `db:"is_active" json:"is_active"` IsActive bool `db:"is_active" json:"is_active"`

View File

@@ -1,17 +1,12 @@
"use client"; "use client";
import { DeadlineCalculator } from "@/components/deadlines/DeadlineCalculator"; import { FristenRechner } from "@/components/deadlines/FristenRechner";
import { DeadlineWizard } from "@/components/deadlines/DeadlineWizard";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
export default function FristenrechnerPage() { export default function FristenrechnerPage() {
const [mode, setMode] = useState<"wizard" | "quick">("wizard");
return ( return (
<div className="animate-fade-in space-y-4"> <div className="animate-fade-in space-y-4">
<div className="flex items-start justify-between">
<div> <div>
<Link <Link
href="/fristen" href="/fristen"
@@ -21,41 +16,14 @@ export default function FristenrechnerPage() {
Zurueck zu Fristen Zurueck zu Fristen
</Link> </Link>
<h1 className="text-lg font-semibold text-neutral-900"> <h1 className="text-lg font-semibold text-neutral-900">
Fristenbestimmung Fristenrechner
</h1> </h1>
<p className="mt-0.5 text-sm text-neutral-500"> <p className="mt-0.5 text-sm text-neutral-500">
{mode === "wizard" Verfahrensart waehlen, Fristen einsehen und Termine berechnen
? "Vollstaendige Verfahrens-Timeline mit automatischer Fristenberechnung"
: "Schnellberechnung einzelner Fristen nach Verfahrensart"}
</p> </p>
</div> </div>
{/* Mode toggle */} <FristenRechner />
<div className="flex rounded-md border border-neutral-200 bg-neutral-50 p-0.5">
<button
onClick={() => setMode("wizard")}
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
mode === "wizard"
? "bg-white text-neutral-900 shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
}`}
>
Verfahren
</button>
<button
onClick={() => setMode("quick")}
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
mode === "quick"
? "bg-white text-neutral-900 shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
}`}
>
Schnell
</button>
</div>
</div>
{mode === "wizard" ? <DeadlineWizard /> : <DeadlineCalculator />}
</div> </div>
); );
} }

View File

@@ -51,13 +51,28 @@ export function DeadlineCalculator() {
}) => api.post<CalculateResponse>("/deadlines/calculate", params), }) => api.post<CalculateResponse>("/deadlines/calculate", params),
}); });
// Auto-calculate when proceeding type changes (using current trigger date)
function doCalculate(type: string, date: string) {
if (!type || !date) return;
calculateMutation.mutate({
proceeding_type: type,
trigger_event_date: date,
});
}
function handleProceedingChange(newType: string) {
setProceedingType(newType);
doCalculate(newType, triggerDate);
}
function handleDateChange(newDate: string) {
setTriggerDate(newDate);
if (proceedingType) doCalculate(proceedingType, newDate);
}
function handleCalculate(e: React.FormEvent) { function handleCalculate(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
if (!proceedingType || !triggerDate) return; doCalculate(proceedingType, triggerDate);
calculateMutation.mutate({
proceeding_type: proceedingType,
trigger_event_date: triggerDate,
});
} }
const results = calculateMutation.data; const results = calculateMutation.data;
@@ -80,27 +95,43 @@ export function DeadlineCalculator() {
</label> </label>
<select <select
value={proceedingType} value={proceedingType}
onChange={(e) => setProceedingType(e.target.value)} onChange={(e) => handleProceedingChange(e.target.value)}
disabled={typesLoading} disabled={typesLoading}
className={inputClass} className={inputClass}
> >
<option value="">Bitte wählen...</option> <option value="">Bitte wählen...</option>
{(() => { {(() => {
const grouped = new Map<string, ProceedingType[]>(); const types = proceedingTypes ?? [];
for (const pt of proceedingTypes ?? []) { const categoryLabels: Record<string, string> = {
const key = pt.jurisdiction ?? "Sonstige"; hauptverfahren: "Hauptverfahren",
if (!grouped.has(key)) grouped.set(key, []); im_verfahren: "Verfahren im Verfahren",
grouped.get(key)!.push(pt); rechtsbehelf: "Rechtsbehelfe",
} };
const labels: Record<string, string> = { const jurisdictionLabels: Record<string, string> = {
UPC: "UPC-Verfahren", UPC: "UPC",
DE: "Deutsche Patentverfahren", DE: "Deutsche Patentverfahren",
}; };
return Array.from(grouped.entries()).map(([jurisdiction, types]) => ( // Group by jurisdiction + category
<optgroup key={jurisdiction} label={labels[jurisdiction] ?? jurisdiction}> const groups: { key: string; label: string; items: typeof types }[] = [];
{types.map((pt) => ( const seen = new Set<string>();
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.map((g) => (
<optgroup key={g.key} label={g.label}>
{g.items.map((pt) => (
<option key={pt.id} value={pt.code}> <option key={pt.id} value={pt.code}>
{pt.name} ({pt.code}) {pt.name}
</option> </option>
))} ))}
</optgroup> </optgroup>
@@ -115,7 +146,7 @@ export function DeadlineCalculator() {
<input <input
type="date" type="date"
value={triggerDate} value={triggerDate}
onChange={(e) => setTriggerDate(e.target.value)} onChange={(e) => handleDateChange(e.target.value)}
className={inputClass} className={inputClass}
/> />
</div> </div>

View File

@@ -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<string, string> = {
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 <Scale className="h-3.5 w-3.5" />;
case "defendant":
return <Users className="h-3.5 w-3.5" />;
case "court":
return <Gavel className="h-3.5 w-3.5" />;
default:
return <FileText className="h-3.5 w-3.5" />;
}
}
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<string, string> = {
hauptverfahren: "Hauptverfahren",
im_verfahren: "Verfahren im Verfahren",
rechtsbehelf: "Rechtsbehelfe",
};
const jurisdictionLabels: Record<string, string> = {
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<string>();
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<string | null>(null);
const [expandedRuleId, setExpandedRuleId] = useState<string | null>(null);
const [triggerDate, setTriggerDate] = useState(
new Date().toISOString().split("T")[0],
);
const [calcResults, setCalcResults] = useState<
Record<string, { due_date: string; original_due_date: string; was_adjusted: boolean }>
>({});
const [savingRuleId, setSavingRuleId] = useState<string | null>(null);
const [selectedCaseId, setSelectedCaseId] = useState<string>("");
const queryClient = useQueryClient();
// Fetch proceeding types
const { data: proceedingTypes, isLoading: typesLoading } = useQuery({
queryKey: ["proceeding-types"],
queryFn: () => api.get<ProceedingType[]>("/proceeding-types"),
});
// Fetch rule tree when type is selected
const { data: ruleTree, isLoading: rulesLoading } = useQuery({
queryKey: ["deadline-rules", selectedType],
queryFn: () => api.get<RuleTreeNode[]>(`/deadline-rules/${selectedType}`),
enabled: !!selectedType,
});
// Fetch cases for "save to case"
const { data: cases } = useQuery({
queryKey: ["cases"],
queryFn: () => api.get<Case[]>("/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<CalculateResponse>("/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 (
<div className="space-y-6">
{/* Step 1: Proceeding Type Cards */}
<div>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-sm font-medium text-neutral-900">
Verfahrensart waehlen
</h2>
{selectedType && (
<button
onClick={handleReset}
className="flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-700"
>
<RotateCcw className="h-3 w-3" />
Zuruecksetzen
</button>
)}
</div>
{typesLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-neutral-400" />
</div>
) : (
<div className="space-y-5">
{groups.map((group) => (
<div key={group.key}>
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400">
{group.label}
</div>
<div className="flex flex-wrap gap-2">
{group.items.map((pt) => {
const isSelected = selectedType === pt.code;
return (
<button
key={pt.id}
onClick={() => handleTypeSelect(pt.code)}
className={`rounded-lg border px-3 py-2 text-sm transition-all ${
isSelected
? "border-neutral-900 bg-neutral-900 text-white shadow-sm"
: "border-neutral-200 bg-white text-neutral-700 hover:border-neutral-300 hover:shadow-sm"
}`}
>
{pt.name}
</button>
);
})}
</div>
</div>
))}
</div>
)}
</div>
{/* Step 2: Deadline Rules for Selected Type */}
{selectedType && (
<div className="animate-fade-in">
<div className="mb-3 flex items-center justify-between">
<div>
<h2 className="text-sm font-medium text-neutral-900">
Fristen: {selectedPT?.name}
</h2>
{flatRules.length > 0 && (
<p className="mt-0.5 text-xs text-neutral-500">
{flatRules.length} Fristen &mdash; Frist anklicken zum Berechnen
</p>
)}
</div>
</div>
{rulesLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-neutral-400" />
</div>
) : flatRules.length === 0 ? (
<div className="rounded-lg border border-dashed border-neutral-300 bg-white px-6 py-8 text-center text-sm text-neutral-500">
Keine Fristenregeln fuer diesen Verfahrenstyp hinterlegt.
</div>
) : (
<div className="rounded-lg border border-neutral-200 bg-white divide-y divide-neutral-100">
{flatRules.map((rule, i) => {
const isExpanded = expandedRuleId === rule.id;
const result = calcResults[rule.id];
const duration = formatDuration(rule.duration_value, rule.duration_unit);
return (
<div key={rule.id}>
{/* Rule Row */}
<button
onClick={() => handleRuleClick(rule.id)}
className={`flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-neutral-50 ${
isExpanded ? "bg-neutral-50" : ""
}`}
style={{ paddingLeft: `${16 + rule.depth * 20}px` }}
>
{/* Timeline dot + connector */}
<div className="flex flex-col items-center">
<div
className={`h-2.5 w-2.5 shrink-0 rounded-full border-2 ${
isExpanded
? "border-neutral-900 bg-neutral-900"
: "border-neutral-300 bg-white"
}`}
/>
{i < flatRules.length - 1 && (
<div className="mt-0.5 h-3 w-px bg-neutral-200" />
)}
</div>
{/* Content */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-neutral-900">
{rule.name}
</span>
{!rule.is_mandatory && (
<span className="rounded bg-neutral-100 px-1 py-0.5 text-[10px] text-neutral-400">
optional
</span>
)}
</div>
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-neutral-500">
{duration && (
<span className="flex items-center gap-0.5">
<Clock className="h-3 w-3" />
{duration}
</span>
)}
{rule.rule_code && (
<>
<span className="text-neutral-300">&middot;</span>
<span className="rounded bg-neutral-100 px-1 py-0.5 font-mono text-[10px]">
{rule.rule_code}
</span>
</>
)}
{rule.primary_party && (
<>
<span className="text-neutral-300">&middot;</span>
<span className="flex items-center gap-0.5">
{getPartyIcon(rule.primary_party)}
{getPartyLabel(rule.primary_party)}
</span>
</>
)}
</div>
</div>
{/* Chevron */}
<ChevronRight
className={`h-4 w-4 shrink-0 text-neutral-400 transition-transform ${
isExpanded ? "rotate-90" : ""
}`}
/>
</button>
{/* Step 3: Expanded Calculation Panel */}
{isExpanded && (
<div className="border-t border-neutral-100 bg-neutral-50 px-4 py-4 animate-fade-in"
style={{ paddingLeft: `${36 + rule.depth * 20}px` }}
>
<div className="flex flex-col gap-4 sm:flex-row sm:items-end">
{/* Date picker */}
<div>
<label className="mb-1 block text-xs font-medium text-neutral-500">
Ausloesedatum
</label>
<input
type="date"
value={triggerDate}
onChange={(e) =>
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"
/>
</div>
{/* Result */}
{calcMutation.isPending &&
calcMutation.variables?.selected_rule_ids[0] === rule.id ? (
<div className="flex items-center gap-2 text-sm text-neutral-500">
<Loader2 className="h-4 w-4 animate-spin" />
Berechne...
</div>
) : result ? (
<div className="flex items-center gap-4">
<div>
<div className="text-xs font-medium text-neutral-500">
Fristende
</div>
<div className="flex items-center gap-2">
<CalendarDays className="h-4 w-4 text-neutral-700" />
<span className="text-lg font-semibold tabular-nums text-neutral-900">
{format(
parseISO(result.due_date),
"dd. MMMM yyyy",
{ locale: de },
)}
</span>
</div>
{result.was_adjusted && (
<div className="mt-0.5 flex items-center gap-1 text-xs text-amber-600">
<AlertTriangle className="h-3 w-3" />
Angepasst (Original:{" "}
{format(
parseISO(result.original_due_date),
"dd.MM.yyyy",
)}
)
</div>
)}
</div>
{/* Save to case button */}
<button
onClick={(e) => {
e.stopPropagation();
setSavingRuleId(
savingRuleId === rule.id ? null : rule.id,
);
}}
className="flex items-center gap-1 rounded-md border border-neutral-200 bg-white px-2.5 py-1.5 text-xs font-medium text-neutral-700 transition-colors hover:bg-neutral-50"
>
<FolderOpen className="h-3.5 w-3.5" />
Auf Akte
</button>
</div>
) : null}
</div>
{/* Save to case panel */}
{savingRuleId === rule.id && result && (
<div className="mt-3 flex items-center gap-2 animate-fade-in">
<select
value={selectedCaseId}
onChange={(e) =>
setSelectedCaseId(e.target.value)
}
className="flex-1 rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-xs text-neutral-900 outline-none focus:border-neutral-400"
>
<option value="">Akte waehlen...</option>
{cases
?.filter((c) => c.status !== "closed")
.map((c) => (
<option key={c.id} value={c.id}>
{c.case_number} {c.title}
</option>
))}
</select>
<button
disabled={
!selectedCaseId || saveMutation.isPending
}
onClick={() => {
saveMutation.mutate({
caseId: selectedCaseId,
deadline: {
title: rule.name,
due_date: result.due_date,
original_due_date: result.was_adjusted
? result.original_due_date
: undefined,
rule_code: rule.rule_code,
},
});
}}
className="flex items-center gap-1 rounded-md bg-neutral-900 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
>
{saveMutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
Speichern
</button>
</div>
)}
{/* Notes */}
{rule.deadline_notes && (
<p className="mt-2 text-xs italic text-neutral-400">
{rule.deadline_notes}
</p>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
)}
{/* Empty state */}
{!selectedType && !typesLoading && (
<div className="flex flex-col items-center rounded-lg border border-dashed border-neutral-300 bg-white px-6 py-12 text-center">
<div className="rounded-xl bg-neutral-100 p-3">
<Scale className="h-6 w-6 text-neutral-400" />
</div>
<p className="mt-3 text-sm font-medium text-neutral-700">
Fristenrechner
</p>
<p className="mt-1 max-w-sm text-xs text-neutral-500">
Waehlen Sie oben eine Verfahrensart, um alle zugehoerigen Fristen
anzuzeigen und einzelne Termine zu berechnen.
</p>
</div>
)}
{/* Calculation error */}
{calcMutation.isError && (
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<AlertTriangle className="h-4 w-4 shrink-0" />
Fehler bei der Berechnung. Bitte Eingaben pruefen.
</div>
)}
</div>
);
}

View File

@@ -197,6 +197,7 @@ export interface ProceedingType {
name: string; name: string;
description?: string; description?: string;
jurisdiction?: string; jurisdiction?: string;
category?: string;
default_color: string; default_color: string;
sort_order: number; sort_order: number;
is_active: boolean; is_active: boolean;