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:
2026-07-11 00:53:40 +03:30
parent 3f2b332bd3
commit 2f8c9f0f4d
13 changed files with 312 additions and 45 deletions

View File

@@ -7,16 +7,19 @@ import { Card } from '@/components/ui/shared/Card';
import { formatTimeForInput } from '@/components/appointments/appointmentTime';
import { canAccessAppointmentsSection } from '@/components/shared/permissions';
import { useAuth } from '@/lib/hooks/useAuth';
import { ListRowSkeleton } from '@/components/today/TodaySkeleton';
import type { TodaySummaryActions } from '@/types/today';
interface TodayUpcomingAppointmentsProps {
actions: TodaySummaryActions;
loading?: boolean;
isInitialLoad?: boolean;
}
export function TodayUpcomingAppointments({
actions,
loading = false,
isInitialLoad = false,
}: TodayUpcomingAppointmentsProps) {
const t = useTranslations('today');
const { currentOrganization } = useAuth();
@@ -27,13 +30,16 @@ export function TodayUpcomingAppointments({
const appointments = actions.upcomingAppointmentsToday ?? [];
if (loading) {
if (isInitialLoad) {
return (
<Card>
<h2 className="text-base font-semibold text-card-foreground">{t('upcomingAppointmentsTitle')}</h2>
<div className="mt-4 space-y-3">
<Card className="min-h-[280px]">
<div className="mb-4 space-y-2">
<div className="h-4 w-40 animate-pulse rounded bg-background-secondary/60" />
<div className="h-3 w-56 animate-pulse rounded bg-background-secondary/60" />
</div>
<div className="space-y-3">
{[0, 1, 2].map((key) => (
<div key={key} className="h-12 animate-pulse rounded bg-background-secondary/60" />
<ListRowSkeleton key={key} />
))}
</div>
</Card>
@@ -41,8 +47,8 @@ export function TodayUpcomingAppointments({
}
return (
<Card>
<div className="flex items-center justify-between gap-3 mb-4">
<Card className={`min-h-[280px] ${loading ? 'opacity-70 transition-opacity' : ''}`}>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-4">
<div>
<h2 className="text-base font-semibold text-card-foreground">
{t('upcomingAppointmentsTitle')}
@@ -58,7 +64,9 @@ export function TodayUpcomingAppointments({
</div>
{appointments.length === 0 ? (
<p className="text-sm text-text-muted">{t('noUpcomingAppointments')}</p>
<div className="flex flex-1 min-h-[160px] items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border/50 bg-background-secondary/20 px-4">
<p className="text-sm text-text-muted text-center">{t('noUpcomingAppointments')}</p>
</div>
) : (
<ul className="divide-y divide-border/40">
{appointments.map((appointment) => {
@@ -76,7 +84,7 @@ export function TodayUpcomingAppointments({
<p className="text-sm font-medium text-text-primary truncate">
{appointment.patientName}
</p>
<p className="text-xs text-text-muted mt-0.5">
<p className="text-xs text-text-muted mt-0.5 truncate">
{timeLabel}
{appointment.purpose ? (
<span className="text-text-secondary"> · {appointment.purpose}</span>