Polish Today dashboard loading, errors, and layout (Phase 4).
Add section error boundaries, retry banner with stale-while-revalidate, shared skeletons, responsive action layout, and org-switch refetching. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,24 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { useAuth } from '@/lib/hooks/useAuth';
|
||||
import {
|
||||
canAccessAppointmentsSection,
|
||||
canViewTasks,
|
||||
canViewTreatment,
|
||||
} from '@/components/shared/permissions';
|
||||
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
|
||||
import { TodayKpiGrid } from '@/components/today/TodayKpiGrid';
|
||||
import { TodayChartsSection } from '@/components/today/TodayChartsSection';
|
||||
import { TodayUpcomingAppointments } from '@/components/today/TodayUpcomingAppointments';
|
||||
import { TodayLoadErrorBanner } from '@/components/today/TodayLoadErrorBanner';
|
||||
import { TodaySectionErrorFallback } from '@/components/today/TodaySectionErrorFallback';
|
||||
import { TodayWidgetErrorBoundary } from '@/components/today/TodayWidgetErrorBoundary';
|
||||
import { useTodaySummary } from '@/lib/hooks/useTodaySummary';
|
||||
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
|
||||
|
||||
export default function TodayPage() {
|
||||
const t = useTranslations('today');
|
||||
const { currentOrganization } = useAuth();
|
||||
const { data, loading, error } = useTodaySummary(Boolean(currentOrganization?.id));
|
||||
const orgId = currentOrganization?.id;
|
||||
const { data, loading, isInitialLoad, error, reload } = useTodaySummary(orgId);
|
||||
|
||||
const showNoSubscriptionNotice =
|
||||
Boolean(currentOrganization?.isOwner) && !currentOrganization?.plan;
|
||||
|
||||
const showUpcoming = canAccessAppointmentsSection(currentOrganization);
|
||||
const showCharts =
|
||||
(currentOrganization?.type === 'CLINIC' && canViewTreatment(currentOrganization)) ||
|
||||
(currentOrganization?.type === 'LAB' && canViewTasks(currentOrganization));
|
||||
|
||||
const actionLayoutClass = useMemo(() => {
|
||||
if (showUpcoming && showCharts) {
|
||||
return 'grid grid-cols-1 xl:grid-cols-2 gap-4 items-start';
|
||||
}
|
||||
return 'grid grid-cols-1 gap-4';
|
||||
}, [showUpcoming, showCharts]);
|
||||
|
||||
const sectionErrorMessage = t('sectionLoadError');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-semibold">{t('welcomeBack')}</h1>
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
|
||||
<h1 className="text-2xl font-semibold">{t('welcomeBack')}</h1>
|
||||
{data?.generatedAt && !isInitialLoad ? (
|
||||
<p className="text-xs text-text-muted">
|
||||
{t('lastUpdated', {
|
||||
time: new Intl.DateTimeFormat(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(data.generatedAt)),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showNoSubscriptionNotice && (
|
||||
<div className="rounded-[var(--radius-md)] border border-amber-500/30 bg-amber-500/10 p-4">
|
||||
@@ -33,18 +70,53 @@ export default function TodayPage() {
|
||||
)}
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-[var(--radius-md)] border border-badge-danger-border bg-badge-danger-bg/40 p-4">
|
||||
<p className="text-sm text-badge-danger-fg">
|
||||
{formatApiErrorMessage(error, t('loadError'))}
|
||||
</p>
|
||||
</div>
|
||||
<TodayLoadErrorBanner
|
||||
message={formatApiErrorMessage(error, t('loadError'))}
|
||||
retryLabel={t('retryLoad')}
|
||||
onRetry={() => void reload()}
|
||||
isRetrying={loading && Boolean(data)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TodayKpiGrid widgets={data?.widgets ?? {}} loading={loading} />
|
||||
<TodayWidgetErrorBoundary
|
||||
fallback={<TodaySectionErrorFallback message={sectionErrorMessage} />}
|
||||
>
|
||||
<TodayKpiGrid
|
||||
widgets={data?.widgets ?? {}}
|
||||
loading={loading}
|
||||
isInitialLoad={isInitialLoad}
|
||||
hasError={Boolean(error)}
|
||||
/>
|
||||
</TodayWidgetErrorBoundary>
|
||||
|
||||
<TodayUpcomingAppointments actions={data?.actions ?? {}} loading={loading} />
|
||||
{(showUpcoming || showCharts) && (!error || data) ? (
|
||||
<div className={actionLayoutClass}>
|
||||
{showUpcoming ? (
|
||||
<TodayWidgetErrorBoundary
|
||||
fallback={<TodaySectionErrorFallback message={sectionErrorMessage} />}
|
||||
>
|
||||
<TodayUpcomingAppointments
|
||||
actions={data?.actions ?? {}}
|
||||
loading={loading}
|
||||
isInitialLoad={isInitialLoad}
|
||||
/>
|
||||
</TodayWidgetErrorBoundary>
|
||||
) : null}
|
||||
|
||||
<TodayChartsSection charts={data?.charts ?? {}} loading={loading} />
|
||||
{showCharts ? (
|
||||
<TodayWidgetErrorBoundary
|
||||
fallback={<TodaySectionErrorFallback message={sectionErrorMessage} />}
|
||||
>
|
||||
<TodayChartsSection
|
||||
charts={data?.charts ?? {}}
|
||||
loading={loading}
|
||||
isInitialLoad={isInitialLoad}
|
||||
className={showUpcoming ? '' : 'max-w-none'}
|
||||
/>
|
||||
</TodayWidgetErrorBoundary>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user