From 8b0df9cc4e7fb2be9fb715b864d48dcb2967da6a Mon Sep 17 00:00:00 2001 From: Raffael Sheikh Date: Sun, 7 Jun 2026 11:35:52 +0200 Subject: [PATCH] =?UTF-8?q?UI:=20Fehler-States=20statt=20wei=C3=9Fer=20Lee?= =?UTF-8?q?rseiten=20bei=20Query-Fehlern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard/Projekt/Lead/Team/Historie rendern bei fehlgeschlagener Query nicht mehr ein stilles `return null` (leere weisse Seite), sondern einen Hinweis mit "Neu laden"-Button. - Neue Komponente ErrorState (Meldung + optionaler Retry) - 5 Routen: if (q.isError || !q.data) -> ErrorState mit q.refetch() Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/components/ui/ErrorState.tsx | 24 +++++++++++++++++++++++ apps/web/src/routes/dashboard.tsx | 9 ++++++++- apps/web/src/routes/historie.tsx | 9 ++++++++- apps/web/src/routes/lead.tsx | 9 ++++++++- apps/web/src/routes/projekt.tsx | 9 ++++++++- apps/web/src/routes/team-view.tsx | 9 ++++++++- 6 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/components/ui/ErrorState.tsx diff --git a/apps/web/src/components/ui/ErrorState.tsx b/apps/web/src/components/ui/ErrorState.tsx new file mode 100644 index 0000000..eae8ec1 --- /dev/null +++ b/apps/web/src/components/ui/ErrorState.tsx @@ -0,0 +1,24 @@ +import { Button } from './Button'; + +/** + * Einheitlicher Fehlerzustand für fehlgeschlagene Queries — ersetzt stilles + * `return null` (weiße Leerseite) durch einen Hinweis + optionalen Retry. + */ +export function ErrorState({ + message = 'Konnte nicht geladen werden.', + onRetry, +}: { + message?: string; + onRetry?: () => void; +}) { + return ( +
+

{message}

+ {onRetry && ( + + )} +
+ ); +} diff --git a/apps/web/src/routes/dashboard.tsx b/apps/web/src/routes/dashboard.tsx index 4bfb8c2..13a2807 100644 --- a/apps/web/src/routes/dashboard.tsx +++ b/apps/web/src/routes/dashboard.tsx @@ -4,6 +4,7 @@ import { voice } from '../lib/voice'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; import { SkeletonCard } from '../components/ui/SkeletonCard'; +import { ErrorState } from '../components/ui/ErrorState'; import { TeamTimeSection } from '../components/time/TeamTimeSection'; import { MODUS_META, type ProjectModus } from '../lib/project-meta'; import { AlertTriangle } from 'lucide-react'; @@ -23,7 +24,13 @@ export function DashboardRoute() { ); } - if (!q.data) return null; + if (q.isError || !q.data) + return ( + void q.refetch()} + /> + ); const isCheckedIn = today.data?.entry?.status !== undefined && today.data.entry.status !== 'absent'; diff --git a/apps/web/src/routes/historie.tsx b/apps/web/src/routes/historie.tsx index 2429fbe..b8a021a 100644 --- a/apps/web/src/routes/historie.tsx +++ b/apps/web/src/routes/historie.tsx @@ -6,6 +6,7 @@ import type { AppRouter } from '@cockpit/api/router'; import { trpc } from '../lib/trpc'; import { Card } from '../components/ui/Card'; import { SkeletonCard } from '../components/ui/SkeletonCard'; +import { ErrorState } from '../components/ui/ErrorState'; type HistoryPerson = inferRouterOutputs['dayEntry']['historyForDate']['entries'][number]; @@ -37,7 +38,13 @@ export function HistorieRoute() { const q = trpc.dayEntry.historyForDate.useQuery({ date }); if (q.isPending) return ; - if (!q.data) return null; + if (q.isError || !q.data) + return ( + void q.refetch()} + /> + ); const { date: current, prevDate, nextDate, entries, ohneEintrag } = q.data; return ( diff --git a/apps/web/src/routes/lead.tsx b/apps/web/src/routes/lead.tsx index 9af5c19..5884075 100644 --- a/apps/web/src/routes/lead.tsx +++ b/apps/web/src/routes/lead.tsx @@ -10,6 +10,7 @@ import { voice } from '../lib/voice'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; import { SkeletonCard } from '../components/ui/SkeletonCard'; +import { ErrorState } from '../components/ui/ErrorState'; import { useConfirm } from '../components/ui/ConfirmProvider'; import { LEAD_STATUS_META, @@ -72,7 +73,13 @@ export function LeadRoute() { const q = trpc.lead.byId.useQuery({ id }); if (q.isPending) return ; - if (!q.data) return null; + if (q.isError || !q.data) + return ( + void q.refetch()} + /> + ); const { lead, contacts, touchpoints } = q.data; function goBack() { diff --git a/apps/web/src/routes/projekt.tsx b/apps/web/src/routes/projekt.tsx index 55bf670..e537bbd 100644 --- a/apps/web/src/routes/projekt.tsx +++ b/apps/web/src/routes/projekt.tsx @@ -7,6 +7,7 @@ import { trpc } from '../lib/trpc'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; import { SkeletonCard } from '../components/ui/SkeletonCard'; +import { ErrorState } from '../components/ui/ErrorState'; import { useConfirm } from '../components/ui/ConfirmProvider'; import { useCurrentUser } from '../hooks/useCurrentUser'; import { voice } from '../lib/voice'; @@ -71,7 +72,13 @@ export function ProjektRoute() { const q = trpc.project.byId.useQuery({ id }); const [abschlussOpen, setAbschlussOpen] = useState(false); if (q.isPending) return ; - if (!q.data) return null; + if (q.isError || !q.data) + return ( + void q.refetch()} + /> + ); const { project: p, area: a, milestones } = q.data; const isP1 = me?.id === p.verantwortlicheUserId; const pflichtKeys: Array<{ key: string; label: string }> = [ diff --git a/apps/web/src/routes/team-view.tsx b/apps/web/src/routes/team-view.tsx index 0f01292..12b967f 100644 --- a/apps/web/src/routes/team-view.tsx +++ b/apps/web/src/routes/team-view.tsx @@ -5,6 +5,7 @@ import { trpc } from '../lib/trpc'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; import { SkeletonCard } from '../components/ui/SkeletonCard'; +import { ErrorState } from '../components/ui/ErrorState'; import { formatDurationCompact } from '../lib/format-duration'; import { TIME_CATEGORIES } from '../lib/time-categories'; @@ -124,7 +125,13 @@ export function TeamViewRoute() { }, [isToday]); if (q.isPending) return ; - if (!q.data) return null; + if (q.isError || !q.data) + return ( + void q.refetch()} + /> + ); const lanes = q.data.lanes; const nowMs = Date.now();