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
This commit is contained in:
@@ -5,13 +5,15 @@ import { api } from "@/lib/api";
|
||||
import type { Case } from "@/lib/types";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { Plus, Search } from "lucide-react";
|
||||
import { Plus, Search, FolderOpen } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { SkeletonTable } from "@/components/ui/Skeleton";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: "", label: "Alle Status" },
|
||||
{ value: "active", label: "Aktiv" },
|
||||
{ value: "pending", label: "Anhangig" },
|
||||
{ value: "pending", label: "Anhängig" },
|
||||
{ value: "closed", label: "Geschlossen" },
|
||||
{ value: "archived", label: "Archiviert" },
|
||||
];
|
||||
@@ -20,9 +22,9 @@ const TYPE_OPTIONS = [
|
||||
{ value: "", label: "Alle Typen" },
|
||||
{ value: "INF", label: "Verletzungsklage" },
|
||||
{ value: "REV", label: "Widerruf" },
|
||||
{ value: "CCR", label: "Einstweilige Verfugung" },
|
||||
{ value: "CCR", label: "Einstweilige Verfügung" },
|
||||
{ value: "APP", label: "Berufung" },
|
||||
{ value: "PI", label: "Vorlaufiger Rechtsschutz" },
|
||||
{ value: "PI", label: "Vorläufiger Rechtsschutz" },
|
||||
{ value: "ZPO_CIVIL", label: "ZPO Zivilverfahren" },
|
||||
];
|
||||
|
||||
@@ -33,6 +35,16 @@ const STATUS_BADGE: Record<string, string> = {
|
||||
archived: "bg-neutral-100 text-neutral-400",
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
active: "Aktiv",
|
||||
pending: "Anhängig",
|
||||
closed: "Geschlossen",
|
||||
archived: "Archiviert",
|
||||
};
|
||||
|
||||
const inputClass =
|
||||
"rounded-md border border-neutral-200 bg-white px-2.5 py-1.5 text-sm outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
|
||||
|
||||
export default function CasesPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -59,24 +71,24 @@ export default function CasesPage() {
|
||||
const cases = data?.cases ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="animate-fade-in">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-neutral-900">Akten</h1>
|
||||
<p className="mt-0.5 text-sm text-neutral-500">
|
||||
{data ? `${data.total} Akten` : "Laden..."}
|
||||
{data ? `${data.total} Akten` : "\u00A0"}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/cases/new"
|
||||
className="inline-flex items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-neutral-800"
|
||||
className="inline-flex w-fit items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Neue Akte
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-400" />
|
||||
<input
|
||||
@@ -84,86 +96,113 @@ export default function CasesPage() {
|
||||
placeholder="Suchen nach Aktenzeichen, Titel..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full rounded-md border border-neutral-200 bg-white py-1.5 pl-9 pr-3 text-sm outline-none focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
|
||||
className={`w-full pl-9 pr-3 ${inputClass}`}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
className="rounded-md border border-neutral-200 bg-white px-2.5 py-1.5 text-sm outline-none focus:border-neutral-400"
|
||||
>
|
||||
{STATUS_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
className="rounded-md border border-neutral-200 bg-white px-2.5 py-1.5 text-sm outline-none focus:border-neutral-400"
|
||||
>
|
||||
{TYPE_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex gap-3">
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
className={inputClass}
|
||||
>
|
||||
{STATUS_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
className={inputClass}
|
||||
>
|
||||
{TYPE_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
{isLoading ? (
|
||||
<div className="py-12 text-center text-sm text-neutral-400">
|
||||
Laden...
|
||||
</div>
|
||||
<SkeletonTable rows={5} />
|
||||
) : cases.length === 0 ? (
|
||||
<div className="py-12 text-center text-sm text-neutral-400">
|
||||
Keine Akten gefunden.
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={FolderOpen}
|
||||
title="Keine Akten gefunden"
|
||||
description={
|
||||
search || status || type
|
||||
? "Versuchen Sie andere Suchkriterien."
|
||||
: "Erstellen Sie Ihre erste Akte, um loszulegen."
|
||||
}
|
||||
action={
|
||||
!search && !status && !type ? (
|
||||
<Link
|
||||
href="/cases/new"
|
||||
className="inline-flex items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Neue Akte anlegen
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-md border border-neutral-200 bg-white">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-neutral-100 text-left text-xs font-medium uppercase tracking-wider text-neutral-400">
|
||||
<th className="px-4 py-2.5">Aktenzeichen</th>
|
||||
<th className="px-4 py-2.5">Titel</th>
|
||||
<th className="px-4 py-2.5">Typ</th>
|
||||
<th className="px-4 py-2.5">Gericht</th>
|
||||
<th className="px-4 py-2.5">Status</th>
|
||||
<th className="px-4 py-2.5">Erstellt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-100">
|
||||
{cases.map((c) => (
|
||||
<tr
|
||||
key={c.id}
|
||||
onClick={() => router.push(`/cases/${c.id}`)}
|
||||
className="cursor-pointer hover:bg-neutral-50"
|
||||
>
|
||||
<td className="px-4 py-2.5 font-medium text-neutral-900">
|
||||
{c.case_number}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-neutral-700">{c.title}</td>
|
||||
<td className="px-4 py-2.5 text-neutral-500">
|
||||
{c.case_type ?? "-"}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-neutral-500">
|
||||
{c.court ?? "-"}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span
|
||||
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${STATUS_BADGE[c.status] ?? "bg-neutral-100 text-neutral-500"}`}
|
||||
<div className="-mx-4 overflow-x-auto sm:mx-0">
|
||||
<div className="min-w-[640px] sm:min-w-0">
|
||||
<div className="overflow-hidden rounded-md border border-neutral-200 bg-white">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-neutral-100 text-left text-xs font-medium uppercase tracking-wider text-neutral-400">
|
||||
<th className="px-4 py-2.5">Aktenzeichen</th>
|
||||
<th className="px-4 py-2.5">Titel</th>
|
||||
<th className="hidden px-4 py-2.5 md:table-cell">Typ</th>
|
||||
<th className="hidden px-4 py-2.5 lg:table-cell">
|
||||
Gericht
|
||||
</th>
|
||||
<th className="px-4 py-2.5">Status</th>
|
||||
<th className="hidden px-4 py-2.5 sm:table-cell">
|
||||
Erstellt
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-100">
|
||||
{cases.map((c) => (
|
||||
<tr
|
||||
key={c.id}
|
||||
onClick={() => router.push(`/cases/${c.id}`)}
|
||||
className="cursor-pointer transition-colors hover:bg-neutral-50"
|
||||
>
|
||||
{c.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-neutral-400">
|
||||
{new Date(c.created_at).toLocaleDateString("de-DE")}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-medium text-neutral-900">
|
||||
{c.case_number}
|
||||
</td>
|
||||
<td className="max-w-[200px] truncate px-4 py-2.5 text-neutral-700">
|
||||
{c.title}
|
||||
</td>
|
||||
<td className="hidden px-4 py-2.5 text-neutral-500 md:table-cell">
|
||||
{c.case_type ?? "-"}
|
||||
</td>
|
||||
<td className="hidden px-4 py-2.5 text-neutral-500 lg:table-cell">
|
||||
{c.court ?? "-"}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span
|
||||
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${STATUS_BADGE[c.status] ?? "bg-neutral-100 text-neutral-500"}`}
|
||||
>
|
||||
{STATUS_LABEL[c.status] ?? c.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="hidden whitespace-nowrap px-4 py-2.5 text-neutral-400 sm:table-cell">
|
||||
{new Date(c.created_at).toLocaleDateString("de-DE")}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user