- Responsive sidebar: collapses on mobile with hamburger menu, slide-in animation - Skeleton loaders: dashboard cards, case table, case detail page - Empty states: friendly messages with icons for cases, deadlines, parties, documents - Error states: retry button on dashboard, proper error message on case not found - Form validation: inline error messages on case creation form - German language: fix all missing umlauts (Zurück, wählen, Anhängig, Verfügung, etc.) - Status labels: display German translations instead of raw status values - Transitions: fade-in animations on page load, hover/transition-colors on all interactive elements - Focus states: focus-visible ring for keyboard accessibility - Mobile layout: stacking for filters, forms, tabs; horizontal scroll for tables - Extraction results: card layout on mobile, table on desktop - Missing types: add DashboardData, DeadlineSummary, CaseSummary, ExtractedDeadline etc. - Fix QuickActions links to use correct routes (/cases/new, /ai/extract) - Consistent input focus styles across all forms
188 lines
5.7 KiB
TypeScript
188 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
|
|
const TYPE_OPTIONS = [
|
|
{ value: "", label: "-- Typ wählen --" },
|
|
{ value: "INF", label: "Verletzungsklage (INF)" },
|
|
{ value: "REV", label: "Widerruf (REV)" },
|
|
{ value: "CCR", label: "Einstweilige Verfügung (CCR)" },
|
|
{ value: "APP", label: "Berufung (APP)" },
|
|
{ value: "PI", label: "Vorläufiger Rechtsschutz (PI)" },
|
|
{ value: "ZPO_CIVIL", label: "ZPO Zivilverfahren" },
|
|
];
|
|
|
|
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 [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}
|
|
>
|
|
{TYPE_OPTIONS.map((o) => (
|
|
<option key={o.value} value={o.value}>
|
|
{o.label}
|
|
</option>
|
|
))}
|
|
</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>
|
|
);
|
|
}
|