- 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
173 lines
5.3 KiB
TypeScript
173 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import { useDropzone } from "react-dropzone";
|
|
import { Upload, FileText, X, Loader2 } from "lucide-react";
|
|
import type { Case } from "@/lib/types";
|
|
|
|
interface ExtractionFormProps {
|
|
cases: Case[];
|
|
selectedCaseId: string;
|
|
onCaseChange: (caseId: string) => void;
|
|
onExtract: (file: File | null, text: string) => void;
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const inputClass =
|
|
"w-full 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";
|
|
|
|
export function ExtractionForm({
|
|
cases,
|
|
selectedCaseId,
|
|
onCaseChange,
|
|
onExtract,
|
|
isLoading,
|
|
}: ExtractionFormProps) {
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [text, setText] = useState("");
|
|
|
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
|
if (acceptedFiles.length > 0) {
|
|
setFile(acceptedFiles[0]);
|
|
setText("");
|
|
}
|
|
}, []);
|
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
onDrop,
|
|
accept: { "application/pdf": [".pdf"] },
|
|
maxFiles: 1,
|
|
disabled: isLoading,
|
|
});
|
|
|
|
function removeFile() {
|
|
setFile(null);
|
|
}
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!selectedCaseId || (!file && !text.trim())) return;
|
|
onExtract(file, text.trim());
|
|
}
|
|
|
|
const hasInput = file !== null || text.trim().length > 0;
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
{/* Case selector */}
|
|
<div>
|
|
<label
|
|
htmlFor="case-select"
|
|
className="mb-1.5 block text-sm font-medium text-neutral-700"
|
|
>
|
|
Akte
|
|
</label>
|
|
<select
|
|
id="case-select"
|
|
value={selectedCaseId}
|
|
onChange={(e) => onCaseChange(e.target.value)}
|
|
className={inputClass}
|
|
disabled={isLoading}
|
|
>
|
|
<option value="">Akte auswählen...</option>
|
|
{cases.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.case_number} - {c.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* PDF dropzone */}
|
|
<div>
|
|
<label className="mb-1.5 block text-sm font-medium text-neutral-700">
|
|
PDF hochladen
|
|
</label>
|
|
{file ? (
|
|
<div className="flex items-center gap-3 rounded-md border border-neutral-200 bg-neutral-50 px-4 py-3">
|
|
<FileText className="h-5 w-5 shrink-0 text-neutral-500" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium text-neutral-900">
|
|
{file.name}
|
|
</p>
|
|
<p className="text-xs text-neutral-500">
|
|
{(file.size / 1024).toFixed(0)} KB
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={removeFile}
|
|
disabled={isLoading}
|
|
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-200 hover:text-neutral-600"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div
|
|
{...getRootProps()}
|
|
className={`cursor-pointer rounded-md border-2 border-dashed px-6 py-8 text-center transition-colors ${
|
|
isDragActive
|
|
? "border-neutral-500 bg-neutral-50"
|
|
: "border-neutral-300 hover:border-neutral-400"
|
|
} ${isLoading ? "pointer-events-none opacity-50" : ""}`}
|
|
>
|
|
<input {...getInputProps()} />
|
|
<Upload className="mx-auto h-8 w-8 text-neutral-400" />
|
|
<p className="mt-2 text-sm text-neutral-600">
|
|
PDF hierher ziehen oder{" "}
|
|
<span className="font-medium text-neutral-900">durchsuchen</span>
|
|
</p>
|
|
<p className="mt-1 text-xs text-neutral-400">Nur PDF-Dateien</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-px flex-1 bg-neutral-200" />
|
|
<span className="text-xs text-neutral-400">oder</span>
|
|
<div className="h-px flex-1 bg-neutral-200" />
|
|
</div>
|
|
|
|
{/* Text input */}
|
|
<div>
|
|
<label
|
|
htmlFor="text-input"
|
|
className="mb-1.5 block text-sm font-medium text-neutral-700"
|
|
>
|
|
Text eingeben
|
|
</label>
|
|
<textarea
|
|
id="text-input"
|
|
value={text}
|
|
onChange={(e) => {
|
|
setText(e.target.value);
|
|
if (e.target.value.trim()) setFile(null);
|
|
}}
|
|
placeholder="Gerichtsschriftsatz, Beschluss oder sonstigen Text hier einfügen..."
|
|
rows={6}
|
|
disabled={isLoading}
|
|
className={`${inputClass} resize-y placeholder:text-neutral-400 disabled:opacity-50`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Submit */}
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || !hasInput || !selectedCaseId}
|
|
className="inline-flex items-center gap-2 rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Analysiere...
|
|
</>
|
|
) : (
|
|
"Analysieren"
|
|
)}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|