Files
KanzlAI-mGMT/frontend/src/components/cases/CaseForm.tsx
m dea49f6f8e feat: group proceeding types by jurisdiction in UI dropdowns
- DeadlineCalculator: use optgroup to group by UPC/DE
- DeadlineWizard: add section headers for each jurisdiction
- CaseForm: replace hardcoded TYPE_OPTIONS with API-fetched
  proceeding types grouped by jurisdiction
- Added 3 new DE proceeding types to DB: DE_PATENT,
  DE_NULLITY, DE_OPPOSITION
2026-03-30 14:29:42 +02:00

203 lines
6.4 KiB
TypeScript

"use client";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
import type { ProceedingType } from "@/lib/types";
const JURISDICTION_LABELS: Record<string, string> = {
UPC: "UPC-Verfahren",
DE: "Deutsche Patentverfahren",
};
export interface CaseFormData {
case_number: string;
title: string;
case_type?: string;
court?: string;
court_ref?: string;
status: string;
}
interface CaseFormProps {
initialData?: Partial<CaseFormData>;
onSubmit: (data: CaseFormData) => void;
isSubmitting?: boolean;
submitLabel?: string;
}
export function CaseForm({
initialData,
onSubmit,
isSubmitting,
submitLabel = "Akte anlegen",
}: CaseFormProps) {
const { data: proceedingTypes } = useQuery({
queryKey: ["proceeding-types"],
queryFn: () => api.get<ProceedingType[]>("/proceeding-types"),
});
const [form, setForm] = useState<CaseFormData>({
case_number: initialData?.case_number ?? "",
title: initialData?.title ?? "",
case_type: initialData?.case_type ?? "",
court: initialData?.court ?? "",
court_ref: initialData?.court_ref ?? "",
status: initialData?.status ?? "active",
});
const [errors, setErrors] = useState<Partial<Record<keyof CaseFormData, string>>>({});
function validate(): boolean {
const newErrors: Partial<Record<keyof CaseFormData, string>> = {};
if (!form.case_number.trim()) {
newErrors.case_number = "Aktenzeichen ist erforderlich";
}
if (!form.title.trim()) {
newErrors.title = "Titel ist erforderlich";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!validate()) return;
const data: CaseFormData = {
...form,
case_type: form.case_type || undefined,
court: form.court || undefined,
court_ref: form.court_ref || undefined,
};
onSubmit(data);
}
function update(field: keyof CaseFormData, value: string) {
setForm((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
}
const inputClass =
"w-full rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-sm outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700">
Aktenzeichen *
</label>
<input
type="text"
value={form.case_number}
onChange={(e) => update("case_number", e.target.value)}
placeholder="z.B. 2026/001"
className={`${inputClass} ${errors.case_number ? "border-red-300 focus:border-red-400 focus:ring-red-400" : ""}`}
/>
{errors.case_number && (
<p className="mt-1 text-xs text-red-600">{errors.case_number}</p>
)}
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700">
Status
</label>
<select
value={form.status}
onChange={(e) => update("status", e.target.value)}
className={inputClass}
>
<option value="active">Aktiv</option>
<option value="pending">Anhängig</option>
<option value="closed">Geschlossen</option>
</select>
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700">
Titel *
</label>
<input
type="text"
value={form.title}
onChange={(e) => update("title", e.target.value)}
placeholder="Bezeichnung der Akte"
className={`${inputClass} ${errors.title ? "border-red-300 focus:border-red-400 focus:ring-red-400" : ""}`}
/>
{errors.title && (
<p className="mt-1 text-xs text-red-600">{errors.title}</p>
)}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700">
Verfahrensart
</label>
<select
value={form.case_type}
onChange={(e) => update("case_type", e.target.value)}
className={inputClass}
>
<option value="">-- Typ wählen --</option>
{(() => {
const grouped = new Map<string, ProceedingType[]>();
for (const pt of proceedingTypes ?? []) {
const key = pt.jurisdiction ?? "Sonstige";
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key)!.push(pt);
}
return Array.from(grouped.entries()).map(([jurisdiction, types]) => (
<optgroup key={jurisdiction} label={JURISDICTION_LABELS[jurisdiction] ?? jurisdiction}>
{types.map((pt) => (
<option key={pt.id} value={pt.code}>
{pt.name} ({pt.code})
</option>
))}
</optgroup>
));
})()}
</select>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700">
Gericht
</label>
<input
type="text"
value={form.court}
onChange={(e) => update("court", e.target.value)}
placeholder="z.B. UPC München Zentralkammer"
className={inputClass}
/>
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700">
Gerichtliches Aktenzeichen
</label>
<input
type="text"
value={form.court_ref}
onChange={(e) => update("court_ref", e.target.value)}
placeholder="z.B. UPC_CFI_123/2026"
className={inputClass}
/>
</div>
<div className="flex justify-end pt-2">
<button
type="submit"
disabled={isSubmitting}
className="rounded-md bg-neutral-900 px-4 py-1.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
>
{isSubmitting ? "Speichern..." : submitLabel}
</button>
</div>
</form>
);
}