Files
KanzlAI-mGMT/frontend/src/components/ai/ExtractionResults.tsx
m f81a2492c6 feat: UI polish — responsive, loading/empty/error states, German fixes (Phase 3Q)
- 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
2026-03-25 14:16:30 +01:00

293 lines
11 KiB
TypeScript

"use client";
import { useState } from "react";
import { Trash2, Check, Pencil, X, Loader2, Brain } from "lucide-react";
import type { ExtractedDeadline } from "@/lib/types";
interface ExtractionResultsProps {
deadlines: ExtractedDeadline[];
onAdopt: (deadlines: ExtractedDeadline[]) => void;
isAdopting: boolean;
}
function confidenceColor(confidence: number): string {
if (confidence >= 0.8) return "bg-green-100 text-green-800";
if (confidence >= 0.5) return "bg-yellow-100 text-yellow-800";
return "bg-red-100 text-red-800";
}
function confidenceLabel(confidence: number): string {
if (confidence >= 0.8) return "Hoch";
if (confidence >= 0.5) return "Mittel";
return "Niedrig";
}
const editInputClass =
"w-full rounded border border-neutral-300 px-2 py-1 text-sm outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
export function ExtractionResults({
deadlines: initialDeadlines,
onAdopt,
isAdopting,
}: ExtractionResultsProps) {
const [deadlines, setDeadlines] = useState(initialDeadlines);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editForm, setEditForm] = useState<ExtractedDeadline | null>(null);
function removeDeadline(index: number) {
setDeadlines((prev) => prev.filter((_, i) => i !== index));
}
function startEdit(index: number) {
setEditingIndex(index);
setEditForm({ ...deadlines[index] });
}
function cancelEdit() {
setEditingIndex(null);
setEditForm(null);
}
function saveEdit() {
if (editingIndex === null || !editForm) return;
setDeadlines((prev) =>
prev.map((d, i) => (i === editingIndex ? editForm : d)),
);
setEditingIndex(null);
setEditForm(null);
}
if (deadlines.length === 0) {
return (
<div className="flex flex-col items-center py-8 text-center">
<div className="rounded-xl bg-neutral-100 p-3">
<Brain className="h-5 w-5 text-neutral-400" />
</div>
<p className="mt-2 text-sm text-neutral-500">
Keine Fristen gefunden. Alle extrahierten Fristen wurden entfernt.
</p>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h3 className="text-sm font-medium text-neutral-900">
{deadlines.length} Frist{deadlines.length !== 1 ? "en" : ""} erkannt
</h3>
<button
onClick={() => onAdopt(deadlines)}
disabled={isAdopting || deadlines.length === 0}
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"
>
{isAdopting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Übernehme...
</>
) : (
<>
<Check className="h-4 w-4" />
Fristen übernehmen
</>
)}
</button>
</div>
{/* Mobile: card layout, Desktop: table */}
<div className="hidden overflow-hidden rounded-md border border-neutral-200 sm:block">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-200 bg-neutral-50">
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
Frist
</th>
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
Fälligkeitsdatum
</th>
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
Rechtsgrundlage
</th>
<th className="px-4 py-2.5 text-left font-medium text-neutral-700">
Konfidenz
</th>
<th className="hidden px-4 py-2.5 text-left font-medium text-neutral-700 lg:table-cell">
Quellenangabe
</th>
<th className="px-4 py-2.5 text-right font-medium text-neutral-700">
Aktionen
</th>
</tr>
</thead>
<tbody>
{deadlines.map((d, i) => (
<tr
key={i}
className="border-b border-neutral-100 transition-colors last:border-b-0 hover:bg-neutral-50"
>
{editingIndex === i && editForm ? (
<>
<td className="px-4 py-2">
<input
value={editForm.title}
onChange={(e) =>
setEditForm({ ...editForm, title: e.target.value })
}
className={editInputClass}
/>
</td>
<td className="px-4 py-2">
<input
type="date"
value={editForm.due_date ?? ""}
onChange={(e) =>
setEditForm({
...editForm,
due_date: e.target.value || null,
})
}
className={editInputClass}
/>
</td>
<td className="px-4 py-2">
<input
value={editForm.rule_reference}
onChange={(e) =>
setEditForm({
...editForm,
rule_reference: e.target.value,
})
}
className={editInputClass}
/>
</td>
<td className="px-4 py-2">
<span
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${confidenceColor(editForm.confidence)}`}
>
{confidenceLabel(editForm.confidence)}
</span>
</td>
<td className="hidden px-4 py-2 text-xs text-neutral-500 lg:table-cell">
{editForm.source_quote}
</td>
<td className="px-4 py-2 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={saveEdit}
className="rounded p-1 text-green-600 transition-colors hover:bg-green-50"
title="Speichern"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={cancelEdit}
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100"
title="Abbrechen"
>
<X className="h-4 w-4" />
</button>
</div>
</td>
</>
) : (
<>
<td className="px-4 py-2.5 font-medium text-neutral-900">
{d.title}
</td>
<td className="px-4 py-2.5 text-neutral-700">
{d.due_date
? new Date(d.due_date).toLocaleDateString("de-DE")
: `${d.duration_value} ${d.duration_unit}`}
</td>
<td className="px-4 py-2.5 text-neutral-600">
{d.rule_reference || "-"}
</td>
<td className="px-4 py-2.5">
<span
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${confidenceColor(d.confidence)}`}
>
{confidenceLabel(d.confidence)}{" "}
{Math.round(d.confidence * 100)}%
</span>
</td>
<td className="hidden max-w-48 truncate px-4 py-2.5 text-xs text-neutral-500 lg:table-cell">
{d.source_quote || "-"}
</td>
<td className="px-4 py-2.5 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => startEdit(i)}
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
title="Bearbeiten"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
onClick={() => removeDeadline(i)}
className="rounded p-1 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-600"
title="Entfernen"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</td>
</>
)}
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile card layout */}
<div className="space-y-3 sm:hidden">
{deadlines.map((d, i) => (
<div
key={i}
className="rounded-md border border-neutral-200 bg-white p-4"
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-neutral-900">{d.title}</p>
<div className="flex shrink-0 items-center gap-1">
<button
onClick={() => startEdit(i)}
className="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
onClick={() => removeDeadline(i)}
className="rounded p-1 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-600"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className="mt-2 flex flex-wrap gap-2 text-xs text-neutral-500">
<span>
{d.due_date
? new Date(d.due_date).toLocaleDateString("de-DE")
: `${d.duration_value} ${d.duration_unit}`}
</span>
{d.rule_reference && (
<>
<span>·</span>
<span>{d.rule_reference}</span>
</>
)}
<span
className={`rounded-full px-2 py-0.5 font-medium ${confidenceColor(d.confidence)}`}
>
{confidenceLabel(d.confidence)} {Math.round(d.confidence * 100)}
%
</span>
</div>
</div>
))}
</div>
</div>
);
}