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:
m
2026-03-25 14:16:30 +01:00
parent 2cf01073a3
commit f81a2492c6
23 changed files with 834 additions and 288 deletions

View File

@@ -2,10 +2,19 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { api } from "@/lib/api";
import type { ProceedingType, CalculateResponse, CalculatedDeadline } from "@/lib/types";
import type {
ProceedingType,
CalculateResponse,
CalculatedDeadline,
} from "@/lib/types";
import { format, parseISO, isPast, isThisWeek } from "date-fns";
import { de } from "date-fns/locale";
import { Calculator, Calendar, ArrowRight, AlertTriangle } from "lucide-react";
import {
Calculator,
Calendar,
ArrowRight,
AlertTriangle,
} from "lucide-react";
import { useState } from "react";
function getTimelineUrgency(dueDate: string): "red" | "amber" | "green" {
@@ -21,6 +30,9 @@ const dotColors = {
green: "bg-green-500",
};
const inputClass =
"w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
export function DeadlineCalculator() {
const [proceedingType, setProceedingType] = useState("");
const [triggerDate, setTriggerDate] = useState("");
@@ -31,8 +43,10 @@ export function DeadlineCalculator() {
});
const calculateMutation = useMutation({
mutationFn: (params: { proceeding_type: string; trigger_event_date: string }) =>
api.post<CalculateResponse>("/api/deadlines/calculate", params),
mutationFn: (params: {
proceeding_type: string;
trigger_event_date: string;
}) => api.post<CalculateResponse>("/api/deadlines/calculate", params),
});
function handleCalculate(e: React.FormEvent) {
@@ -49,7 +63,10 @@ export function DeadlineCalculator() {
return (
<div className="space-y-6">
{/* Input form */}
<form onSubmit={handleCalculate} className="rounded-lg border border-neutral-200 bg-white p-5">
<form
onSubmit={handleCalculate}
className="rounded-lg border border-neutral-200 bg-white p-5"
>
<div className="flex items-center gap-2 text-sm font-medium text-neutral-900">
<Calculator className="h-4 w-4" />
Fristenberechnung
@@ -63,9 +80,9 @@ export function DeadlineCalculator() {
value={proceedingType}
onChange={(e) => setProceedingType(e.target.value)}
disabled={typesLoading}
className="w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900"
className={inputClass}
>
<option value="">Bitte wahlen...</option>
<option value="">Bitte wählen...</option>
{proceedingTypes?.map((pt) => (
<option key={pt.id} value={pt.code}>
{pt.name} ({pt.code})
@@ -75,19 +92,23 @@ export function DeadlineCalculator() {
</div>
<div>
<label className="mb-1 block text-xs font-medium text-neutral-500">
Auslosedatum
Auslösedatum
</label>
<input
type="date"
value={triggerDate}
onChange={(e) => setTriggerDate(e.target.value)}
className="w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900"
className={inputClass}
/>
</div>
<div className="flex items-end">
<button
type="submit"
disabled={!proceedingType || !triggerDate || calculateMutation.isPending}
disabled={
!proceedingType ||
!triggerDate ||
calculateMutation.isPending
}
className="flex w-full items-center justify-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"
>
{calculateMutation.isPending ? "Berechne..." : "Berechnen"}
@@ -101,20 +122,22 @@ export function DeadlineCalculator() {
{calculateMutation.isError && (
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<AlertTriangle className="h-4 w-4 shrink-0" />
Fehler bei der Berechnung. Bitte Eingaben prufen.
Fehler bei der Berechnung. Bitte Eingaben prüfen.
</div>
)}
{/* Results */}
{results && results.deadlines && (
<div className="space-y-3">
<div className="animate-fade-in space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-neutral-900">
Berechnete Fristen
</h3>
<span className="text-xs text-neutral-500">
{results.deadlines.length} Fristen ab{" "}
{format(parseISO(results.trigger_event_date), "dd. MMM yyyy", { locale: de })}
{format(parseISO(results.trigger_event_date), "dd. MMM yyyy", {
locale: de,
})}
</span>
</div>
@@ -129,15 +152,16 @@ export function DeadlineCalculator() {
key={d.rule_id}
className={`flex gap-3 px-4 py-3 ${!isLast ? "border-b border-neutral-100" : ""}`}
>
{/* Timeline dot + line */}
<div className="flex flex-col items-center pt-1">
<div className={`h-2.5 w-2.5 shrink-0 rounded-full ${dotColors[urgency]}`} />
{!isLast && <div className="mt-1 w-px flex-1 bg-neutral-200" />}
<div
className={`h-2.5 w-2.5 shrink-0 rounded-full ${dotColors[urgency]}`}
/>
{!isLast && (
<div className="mt-1 w-px flex-1 bg-neutral-200" />
)}
</div>
{/* Content */}
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
<span className="text-sm font-medium text-neutral-900">
{d.title}
</span>
@@ -145,13 +169,18 @@ export function DeadlineCalculator() {
{format(parseISO(d.due_date), "dd.MM.yyyy")}
</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">
{d.rule_code && <span>{d.rule_code}</span>}
{d.was_adjusted && (
<>
{d.rule_code && <span>·</span>}
<span className="text-amber-600">
Angepasst (Original: {format(parseISO(d.original_due_date), "dd.MM.yyyy")})
Angepasst (Original:{" "}
{format(
parseISO(d.original_due_date),
"dd.MM.yyyy",
)}
)
</span>
</>
)}
@@ -166,10 +195,12 @@ export function DeadlineCalculator() {
{/* Empty state */}
{!results && !calculateMutation.isPending && (
<div className="rounded-lg border border-neutral-200 bg-white p-8 text-center">
<Calendar className="mx-auto h-8 w-8 text-neutral-300" />
<p className="mt-2 text-sm text-neutral-500">
Verfahrensart und Auslosedatum wahlen, um Fristen zu berechnen
<div className="flex flex-col items-center rounded-lg border border-dashed border-neutral-300 bg-white px-6 py-12 text-center">
<div className="rounded-xl bg-neutral-100 p-3">
<Calendar className="h-6 w-6 text-neutral-400" />
</div>
<p className="mt-3 text-sm text-neutral-500">
Verfahrensart und Auslösedatum wählen, um Fristen zu berechnen
</p>
</div>
)}