UI: Fehler-States statt weißer Leerseiten bei Query-Fehlern

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) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 11:35:52 +02:00
1 parent 90caa282ee
commit 8b0df9cc4e
6 files changed
+64 -5

No files matched your search

+24
View File
@@ -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 (
<div className="mx-auto max-w-md rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-bg)] p-6 text-center">
<p className="text-sm text-[var(--color-ink-muted)]">{message}</p>
{onRetry && (
<Button variant="secondary" size="sm" className="mt-3" onClick={onRetry}>
Neu laden
</Button>
)}
</div>
);
}
+8 -1
View File
@@ -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() {
</div>
);
}
if (!q.data) return null;
if (q.isError || !q.data)
return (
<ErrorState
message="Dashboard konnte nicht geladen werden."
onRetry={() => void q.refetch()}
/>
);
const isCheckedIn =
today.data?.entry?.status !== undefined && today.data.entry.status !== 'absent';
+8 -1
View File
@@ -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<AppRouter>['dayEntry']['historyForDate']['entries'][number];
@@ -37,7 +38,13 @@ export function HistorieRoute() {
const q = trpc.dayEntry.historyForDate.useQuery({ date });
if (q.isPending) return <SkeletonCard lines={6} />;
if (!q.data) return null;
if (q.isError || !q.data)
return (
<ErrorState
message="Historie konnte nicht geladen werden."
onRetry={() => void q.refetch()}
/>
);
const { date: current, prevDate, nextDate, entries, ohneEintrag } = q.data;
return (
+8 -1
View File
@@ -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 <SkeletonCard lines={8} />;
if (!q.data) return null;
if (q.isError || !q.data)
return (
<ErrorState
message="Lead konnte nicht geladen werden."
onRetry={() => void q.refetch()}
/>
);
const { lead, contacts, touchpoints } = q.data;
function goBack() {
+8 -1
View File
@@ -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 <SkeletonCard lines={6} />;
if (!q.data) return null;
if (q.isError || !q.data)
return (
<ErrorState
message="Projekt konnte nicht geladen werden."
onRetry={() => void q.refetch()}
/>
);
const { project: p, area: a, milestones } = q.data;
const isP1 = me?.id === p.verantwortlicheUserId;
const pflichtKeys: Array<{ key: string; label: string }> = [
+8 -1
View File
@@ -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 <SkeletonCard lines={6} />;
if (!q.data) return null;
if (q.isError || !q.data)
return (
<ErrorState
message="Team-Ansicht konnte nicht geladen werden."
onRetry={() => void q.refetch()}
/>
);
const lanes = q.data.lanes;
const nowMs = Date.now();