- 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
293 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|