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:
1 parent
90caa282ee
commit
8b0df9cc4e
6 files changed
+64
-5
No files matched your search
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { voice } from '../lib/voice';
|
|||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
|
import { ErrorState } from '../components/ui/ErrorState';
|
||||||
import { TeamTimeSection } from '../components/time/TeamTimeSection';
|
import { TeamTimeSection } from '../components/time/TeamTimeSection';
|
||||||
import { MODUS_META, type ProjectModus } from '../lib/project-meta';
|
import { MODUS_META, type ProjectModus } from '../lib/project-meta';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
@@ -23,7 +24,13 @@ export function DashboardRoute() {
|
|||||||
</div>
|
</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 =
|
const isCheckedIn =
|
||||||
today.data?.entry?.status !== undefined && today.data.entry.status !== 'absent';
|
today.data?.entry?.status !== undefined && today.data.entry.status !== 'absent';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { AppRouter } from '@cockpit/api/router';
|
|||||||
import { trpc } from '../lib/trpc';
|
import { trpc } from '../lib/trpc';
|
||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
|
import { ErrorState } from '../components/ui/ErrorState';
|
||||||
|
|
||||||
type HistoryPerson = inferRouterOutputs<AppRouter>['dayEntry']['historyForDate']['entries'][number];
|
type HistoryPerson = inferRouterOutputs<AppRouter>['dayEntry']['historyForDate']['entries'][number];
|
||||||
|
|
||||||
@@ -37,7 +38,13 @@ export function HistorieRoute() {
|
|||||||
const q = trpc.dayEntry.historyForDate.useQuery({ date });
|
const q = trpc.dayEntry.historyForDate.useQuery({ date });
|
||||||
|
|
||||||
if (q.isPending) return <SkeletonCard lines={6} />;
|
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;
|
const { date: current, prevDate, nextDate, entries, ohneEintrag } = q.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { voice } from '../lib/voice';
|
|||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
|
import { ErrorState } from '../components/ui/ErrorState';
|
||||||
import { useConfirm } from '../components/ui/ConfirmProvider';
|
import { useConfirm } from '../components/ui/ConfirmProvider';
|
||||||
import {
|
import {
|
||||||
LEAD_STATUS_META,
|
LEAD_STATUS_META,
|
||||||
@@ -72,7 +73,13 @@ export function LeadRoute() {
|
|||||||
const q = trpc.lead.byId.useQuery({ id });
|
const q = trpc.lead.byId.useQuery({ id });
|
||||||
|
|
||||||
if (q.isPending) return <SkeletonCard lines={8} />;
|
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;
|
const { lead, contacts, touchpoints } = q.data;
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { trpc } from '../lib/trpc';
|
|||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
|
import { ErrorState } from '../components/ui/ErrorState';
|
||||||
import { useConfirm } from '../components/ui/ConfirmProvider';
|
import { useConfirm } from '../components/ui/ConfirmProvider';
|
||||||
import { useCurrentUser } from '../hooks/useCurrentUser';
|
import { useCurrentUser } from '../hooks/useCurrentUser';
|
||||||
import { voice } from '../lib/voice';
|
import { voice } from '../lib/voice';
|
||||||
@@ -71,7 +72,13 @@ export function ProjektRoute() {
|
|||||||
const q = trpc.project.byId.useQuery({ id });
|
const q = trpc.project.byId.useQuery({ id });
|
||||||
const [abschlussOpen, setAbschlussOpen] = useState(false);
|
const [abschlussOpen, setAbschlussOpen] = useState(false);
|
||||||
if (q.isPending) return <SkeletonCard lines={6} />;
|
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 { project: p, area: a, milestones } = q.data;
|
||||||
const isP1 = me?.id === p.verantwortlicheUserId;
|
const isP1 = me?.id === p.verantwortlicheUserId;
|
||||||
const pflichtKeys: Array<{ key: string; label: string }> = [
|
const pflichtKeys: Array<{ key: string; label: string }> = [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { trpc } from '../lib/trpc';
|
|||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
|
import { ErrorState } from '../components/ui/ErrorState';
|
||||||
import { formatDurationCompact } from '../lib/format-duration';
|
import { formatDurationCompact } from '../lib/format-duration';
|
||||||
import { TIME_CATEGORIES } from '../lib/time-categories';
|
import { TIME_CATEGORIES } from '../lib/time-categories';
|
||||||
|
|
||||||
@@ -124,7 +125,13 @@ export function TeamViewRoute() {
|
|||||||
}, [isToday]);
|
}, [isToday]);
|
||||||
|
|
||||||
if (q.isPending) return <SkeletonCard lines={6} />;
|
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 lanes = q.data.lanes;
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user