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:
@@ -6,11 +6,19 @@ import { api } from "@/lib/api";
|
||||
import type { Case, CaseEvent, Party, Deadline, Document } from "@/lib/types";
|
||||
import { CaseTimeline } from "@/components/cases/CaseTimeline";
|
||||
import { PartyList } from "@/components/cases/PartyList";
|
||||
import { ArrowLeft, Clock, FileText, Users, Activity } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
FileText,
|
||||
Users,
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Skeleton } from "@/components/ui/Skeleton";
|
||||
|
||||
interface CaseDetail extends Case {
|
||||
parties: Party[];
|
||||
@@ -25,6 +33,13 @@ 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 TABS = [
|
||||
{ key: "timeline", label: "Verlauf", icon: Activity },
|
||||
{ key: "deadlines", label: "Fristen", icon: Clock },
|
||||
@@ -34,11 +49,43 @@ const TABS = [
|
||||
|
||||
type TabKey = (typeof TABS)[number]["key"];
|
||||
|
||||
function CaseDetailSkeleton() {
|
||||
return (
|
||||
<div>
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<div className="mt-4 flex items-start justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="mt-2 h-4 w-64" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex gap-4 border-b border-neutral-200 pb-2.5">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton key={i} className="h-4 w-20" />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-14 rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CaseDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("timeline");
|
||||
|
||||
const { data: caseDetail, isLoading } = useQuery({
|
||||
const {
|
||||
data: caseDetail,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["case", id],
|
||||
queryFn: () => api.get<CaseDetail>(`/cases/${id}`),
|
||||
});
|
||||
@@ -59,17 +106,28 @@ export default function CaseDetailPage() {
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="py-12 text-center text-sm text-neutral-400">
|
||||
Laden...
|
||||
</div>
|
||||
);
|
||||
return <CaseDetailSkeleton />;
|
||||
}
|
||||
|
||||
if (!caseDetail) {
|
||||
if (error || !caseDetail) {
|
||||
return (
|
||||
<div className="py-12 text-center text-sm text-neutral-400">
|
||||
Akte nicht gefunden.
|
||||
<div className="py-12 text-center">
|
||||
<div className="mx-auto mb-3 w-fit rounded-xl bg-red-50 p-3">
|
||||
<AlertTriangle className="h-6 w-6 text-red-500" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-neutral-900">
|
||||
Akte nicht gefunden
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-neutral-500">
|
||||
Die Akte existiert nicht oder Sie haben keine Berechtigung.
|
||||
</p>
|
||||
<Link
|
||||
href="/cases"
|
||||
className="mt-4 inline-flex items-center gap-1 text-sm text-neutral-500 transition-colors hover:text-neutral-700"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
Zurück zu Akten
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -78,28 +136,28 @@ export default function CaseDetailPage() {
|
||||
const documents = documentsData ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="animate-fade-in">
|
||||
<Link
|
||||
href="/cases"
|
||||
className="mb-4 inline-flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700"
|
||||
className="mb-4 inline-flex items-center gap-1 text-sm text-neutral-500 transition-colors hover:text-neutral-700"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
Zuruck zu Akten
|
||||
Zurück zu Akten
|
||||
</Link>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="text-lg font-semibold text-neutral-900">
|
||||
{caseDetail.title}
|
||||
</h1>
|
||||
<span
|
||||
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${STATUS_BADGE[caseDetail.status] ?? "bg-neutral-100 text-neutral-500"}`}
|
||||
>
|
||||
{caseDetail.status}
|
||||
{STATUS_LABEL[caseDetail.status] ?? caseDetail.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex gap-4 text-sm text-neutral-500">
|
||||
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-sm text-neutral-500">
|
||||
<span>Az. {caseDetail.case_number}</span>
|
||||
{caseDetail.case_type && <span>{caseDetail.case_type}</span>}
|
||||
{caseDetail.court && <span>{caseDetail.court}</span>}
|
||||
@@ -129,12 +187,12 @@ export default function CaseDetailPage() {
|
||||
)}
|
||||
|
||||
<div className="mt-6 border-b border-neutral-200">
|
||||
<nav className="-mb-px flex gap-4">
|
||||
<nav className="-mb-px flex gap-1 overflow-x-auto sm:gap-4">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`inline-flex items-center gap-1.5 border-b-2 px-1 pb-2.5 text-sm font-medium transition-colors ${
|
||||
className={`inline-flex shrink-0 items-center gap-1.5 border-b-2 px-1 pb-2.5 text-sm font-medium transition-colors ${
|
||||
activeTab === tab.key
|
||||
? "border-neutral-900 text-neutral-900"
|
||||
: "border-transparent text-neutral-400 hover:text-neutral-600"
|
||||
@@ -181,9 +239,14 @@ export default function CaseDetailPage() {
|
||||
function DeadlinesList({ deadlines }: { deadlines: Deadline[] }) {
|
||||
if (deadlines.length === 0) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-neutral-400">
|
||||
Keine Fristen vorhanden.
|
||||
</p>
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<div className="rounded-xl bg-neutral-100 p-3">
|
||||
<Clock className="h-5 w-5 text-neutral-400" />
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-neutral-500">
|
||||
Keine Fristen vorhanden.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,12 +256,18 @@ function DeadlinesList({ deadlines }: { deadlines: Deadline[] }) {
|
||||
overdue: "bg-red-50 text-red-700",
|
||||
};
|
||||
|
||||
const DEADLINE_STATUS_LABEL: Record<string, string> = {
|
||||
pending: "Offen",
|
||||
completed: "Erledigt",
|
||||
overdue: "Überfällig",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{deadlines.map((d) => (
|
||||
<div
|
||||
key={d.id}
|
||||
className="flex items-center justify-between rounded-md border border-neutral-200 bg-white px-4 py-3"
|
||||
className="flex flex-col gap-2 rounded-md border border-neutral-200 bg-white px-4 py-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-neutral-900">{d.title}</p>
|
||||
@@ -212,9 +281,9 @@ function DeadlinesList({ deadlines }: { deadlines: Deadline[] }) {
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-medium ${DEADLINE_STATUS[d.status] ?? "bg-neutral-100 text-neutral-500"}`}
|
||||
>
|
||||
{d.status}
|
||||
{DEADLINE_STATUS_LABEL[d.status] ?? d.status}
|
||||
</span>
|
||||
<span className="text-sm text-neutral-500">
|
||||
<span className="whitespace-nowrap text-sm text-neutral-500">
|
||||
{format(new Date(d.due_date), "d. MMM yyyy", { locale: de })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -227,9 +296,14 @@ function DeadlinesList({ deadlines }: { deadlines: Deadline[] }) {
|
||||
function DocumentsList({ documents }: { documents: Document[] }) {
|
||||
if (documents.length === 0) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-neutral-400">
|
||||
Keine Dokumente vorhanden.
|
||||
</p>
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<div className="rounded-xl bg-neutral-100 p-3">
|
||||
<FileText className="h-5 w-5 text-neutral-400" />
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-neutral-500">
|
||||
Keine Dokumente vorhanden.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,7 +330,7 @@ function DocumentsList({ documents }: { documents: Document[] }) {
|
||||
</div>
|
||||
<a
|
||||
href={`/api/documents/${doc.id}`}
|
||||
className="text-sm text-neutral-500 hover:text-neutral-700"
|
||||
className="text-sm text-neutral-500 transition-colors hover:text-neutral-700"
|
||||
>
|
||||
Herunterladen
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user