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:
@@ -8,6 +8,7 @@ import { de } from "date-fns/locale";
|
||||
import { Check, Clock, Filter } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useState, useMemo } from "react";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
type StatusFilter = "all" | "pending" | "completed" | "overdue";
|
||||
|
||||
@@ -25,7 +26,7 @@ const urgencyConfig = {
|
||||
border: "border-red-200",
|
||||
badge: "bg-red-100 text-red-700",
|
||||
dot: "bg-red-500",
|
||||
label: "Uberschritten",
|
||||
label: "Überfällig",
|
||||
},
|
||||
amber: {
|
||||
bg: "bg-amber-50",
|
||||
@@ -43,6 +44,9 @@ const urgencyConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const selectClass =
|
||||
"rounded-md border border-neutral-200 bg-white px-2.5 py-1 text-sm text-neutral-700 transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400 outline-none";
|
||||
|
||||
export function DeadlineList() {
|
||||
const queryClient = useQueryClient();
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
@@ -66,7 +70,7 @@ export function DeadlineList() {
|
||||
toast.success("Frist als erledigt markiert");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Fehler beim Abschliessen der Frist");
|
||||
toast.error("Fehler beim Abschließen der Frist");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,7 +84,8 @@ export function DeadlineList() {
|
||||
if (!deadlines) return [];
|
||||
return deadlines.filter((d) => {
|
||||
if (statusFilter === "pending" && d.status !== "pending") return false;
|
||||
if (statusFilter === "completed" && d.status !== "completed") return false;
|
||||
if (statusFilter === "completed" && d.status !== "completed")
|
||||
return false;
|
||||
if (statusFilter === "overdue") {
|
||||
if (d.status === "completed") return false;
|
||||
if (!isPast(parseISO(d.due_date))) return false;
|
||||
@@ -92,7 +97,9 @@ export function DeadlineList() {
|
||||
|
||||
const counts = useMemo(() => {
|
||||
if (!deadlines) return { overdue: 0, thisWeek: 0, ok: 0 };
|
||||
let overdue = 0, thisWeek = 0, ok = 0;
|
||||
let overdue = 0,
|
||||
thisWeek = 0,
|
||||
ok = 0;
|
||||
for (const d of deadlines) {
|
||||
if (d.status === "completed") continue;
|
||||
const urgency = getUrgency(d);
|
||||
@@ -107,7 +114,10 @@ export function DeadlineList() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="h-16 animate-pulse rounded-lg bg-neutral-100" />
|
||||
<div
|
||||
key={i}
|
||||
className="h-16 animate-pulse rounded-lg bg-neutral-100"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -118,42 +128,52 @@ export function DeadlineList() {
|
||||
{/* Summary cards */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
onClick={() => setStatusFilter(statusFilter === "overdue" ? "all" : "overdue")}
|
||||
className={`rounded-lg border p-3 text-left transition-colors ${
|
||||
onClick={() =>
|
||||
setStatusFilter(statusFilter === "overdue" ? "all" : "overdue")
|
||||
}
|
||||
className={`rounded-lg border p-3 text-left transition-all ${
|
||||
statusFilter === "overdue"
|
||||
? "border-red-300 bg-red-50"
|
||||
? "border-red-300 bg-red-50 ring-1 ring-red-200"
|
||||
: "border-neutral-200 bg-white hover:bg-neutral-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl font-semibold text-red-600">{counts.overdue}</div>
|
||||
<div className="text-xs text-neutral-500">Uberschritten</div>
|
||||
<div className="text-2xl font-semibold tabular-nums text-red-600">
|
||||
{counts.overdue}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-500">Überfällig</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter(statusFilter === "pending" ? "all" : "pending")}
|
||||
className={`rounded-lg border p-3 text-left transition-colors ${
|
||||
onClick={() =>
|
||||
setStatusFilter(statusFilter === "pending" ? "all" : "pending")
|
||||
}
|
||||
className={`rounded-lg border p-3 text-left transition-all ${
|
||||
statusFilter === "pending"
|
||||
? "border-amber-300 bg-amber-50"
|
||||
? "border-amber-300 bg-amber-50 ring-1 ring-amber-200"
|
||||
: "border-neutral-200 bg-white hover:bg-neutral-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl font-semibold text-amber-600">{counts.thisWeek}</div>
|
||||
<div className="text-2xl font-semibold tabular-nums text-amber-600">
|
||||
{counts.thisWeek}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-500">Diese Woche</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter("all")}
|
||||
className={`rounded-lg border p-3 text-left transition-colors ${
|
||||
className={`rounded-lg border p-3 text-left transition-all ${
|
||||
statusFilter === "all"
|
||||
? "border-green-300 bg-green-50"
|
||||
? "border-green-300 bg-green-50 ring-1 ring-green-200"
|
||||
: "border-neutral-200 bg-white hover:bg-neutral-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl font-semibold text-green-600">{counts.ok}</div>
|
||||
<div className="text-2xl font-semibold tabular-nums text-green-600">
|
||||
{counts.ok}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-500">OK</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-1.5 text-sm text-neutral-500">
|
||||
<Filter className="h-3.5 w-3.5" />
|
||||
<span>Filter:</span>
|
||||
@@ -161,18 +181,18 @@ export function DeadlineList() {
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as StatusFilter)}
|
||||
className="rounded-md border border-neutral-200 bg-white px-2.5 py-1 text-sm text-neutral-700"
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="all">Alle Status</option>
|
||||
<option value="pending">Offen</option>
|
||||
<option value="completed">Erledigt</option>
|
||||
<option value="overdue">Uberschritten</option>
|
||||
<option value="overdue">Überfällig</option>
|
||||
</select>
|
||||
{cases && cases.length > 0 && (
|
||||
<select
|
||||
value={caseFilter}
|
||||
onChange={(e) => setCaseFilter(e.target.value)}
|
||||
className="rounded-md border border-neutral-200 bg-white px-2.5 py-1 text-sm text-neutral-700"
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="all">Alle Akten</option>
|
||||
{cases.map((c) => (
|
||||
@@ -186,10 +206,15 @@ export function DeadlineList() {
|
||||
|
||||
{/* Deadline list */}
|
||||
{filtered.length === 0 ? (
|
||||
<div className="rounded-lg border border-neutral-200 bg-white p-8 text-center">
|
||||
<Clock className="mx-auto h-8 w-8 text-neutral-300" />
|
||||
<p className="mt-2 text-sm text-neutral-500">Keine Fristen gefunden</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Clock}
|
||||
title="Keine Fristen gefunden"
|
||||
description={
|
||||
statusFilter !== "all" || caseFilter !== "all"
|
||||
? "Versuchen Sie andere Filtereinstellungen."
|
||||
: "Es sind noch keine Fristen vorhanden."
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filtered.map((deadline) => {
|
||||
@@ -200,15 +225,19 @@ export function DeadlineList() {
|
||||
return (
|
||||
<div
|
||||
key={deadline.id}
|
||||
className={`flex items-center gap-3 rounded-lg border px-4 py-3 ${config.bg} ${config.border}`}
|
||||
className={`flex items-center gap-3 rounded-lg border px-4 py-3 transition-colors ${config.bg} ${config.border}`}
|
||||
>
|
||||
<div className={`h-2.5 w-2.5 shrink-0 rounded-full ${config.dot}`} />
|
||||
<div
|
||||
className={`h-2.5 w-2.5 shrink-0 rounded-full ${config.dot}`}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="truncate text-sm font-medium text-neutral-900">
|
||||
{deadline.title}
|
||||
</span>
|
||||
<span className={`shrink-0 rounded px-1.5 py-0.5 text-xs font-medium ${config.badge}`}>
|
||||
<span
|
||||
className={`shrink-0 rounded px-1.5 py-0.5 text-xs font-medium ${config.badge}`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
{deadline.status === "completed" && (
|
||||
@@ -217,9 +246,11 @@ export function DeadlineList() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-neutral-500">
|
||||
<span>
|
||||
{format(parseISO(deadline.due_date), "dd. MMM yyyy", { locale: de })}
|
||||
{format(parseISO(deadline.due_date), "dd. MMM yyyy", {
|
||||
locale: de,
|
||||
})}
|
||||
</span>
|
||||
{caseInfo && (
|
||||
<>
|
||||
@@ -242,7 +273,7 @@ export function DeadlineList() {
|
||||
onClick={() => completeMutation.mutate(deadline.id)}
|
||||
disabled={completeMutation.isPending}
|
||||
title="Als erledigt markieren"
|
||||
className="shrink-0 rounded-md p-1.5 text-neutral-400 hover:bg-white hover:text-green-600"
|
||||
className="shrink-0 rounded-md p-1.5 text-neutral-400 transition-colors hover:bg-white hover:text-green-600"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user