- 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
203 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|