140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { Link } from '@/i18n/navigation';
|
|
import { useAuth } from '@/lib/hooks/useAuth';
|
|
import {
|
|
canAccessAppointmentsSection,
|
|
canViewAppointmentsTab,
|
|
canViewCases,
|
|
canViewLabCasesOrTasks,
|
|
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';
|
|
|
|
export default function TodayPage() {
|
|
const t = useTranslations('today');
|
|
const { currentOrganization } = useAuth();
|
|
const orgId = currentOrganization?.id;
|
|
const { data, loading, isInitialLoad, error, reload } = useTodaySummary(orgId);
|
|
|
|
const showNoSubscriptionNotice =
|
|
Boolean(currentOrganization?.isOwner) && !currentOrganization?.plan;
|
|
|
|
const showUpcoming =
|
|
currentOrganization?.type === 'CLINIC' && canViewTreatment(currentOrganization);
|
|
const showCharts = useMemo(() => {
|
|
const orgType = currentOrganization?.type;
|
|
if (!orgType) return false;
|
|
|
|
if (orgType === 'CLINIC') {
|
|
return (
|
|
canAccessAppointmentsSection(currentOrganization) ||
|
|
canViewAppointmentsTab(currentOrganization) ||
|
|
canViewTreatment(currentOrganization)
|
|
);
|
|
}
|
|
|
|
return (
|
|
canViewCases(currentOrganization) ||
|
|
canViewTasks(currentOrganization) ||
|
|
canViewLabCasesOrTasks(currentOrganization)
|
|
);
|
|
}, [currentOrganization]);
|
|
|
|
const sectionErrorMessage = t('sectionLoadError');
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<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">
|
|
<p className="text-sm text-amber-200">
|
|
{t('noSubscriptionNotice')}{' '}
|
|
<Link href="/settings/subscriptions" className="font-medium underline underline-offset-2">
|
|
{t('choosePlanLink')}
|
|
</Link>{' '}
|
|
{t('noSubscriptionCta')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{error ? (
|
|
<TodayLoadErrorBanner
|
|
message={formatApiErrorMessage(error, t('loadError'))}
|
|
retryLabel={t('retryLoad')}
|
|
onRetry={() => void reload()}
|
|
isRetrying={loading && Boolean(data)}
|
|
/>
|
|
) : null}
|
|
|
|
<TodayWidgetErrorBoundary
|
|
fallback={<TodaySectionErrorFallback message={sectionErrorMessage} />}
|
|
>
|
|
<TodayKpiGrid
|
|
widgets={data?.widgets ?? {}}
|
|
loading={loading}
|
|
isInitialLoad={isInitialLoad}
|
|
hasError={Boolean(error)}
|
|
/>
|
|
</TodayWidgetErrorBoundary>
|
|
|
|
{showUpcoming && (!error || data) ? (
|
|
<TodayWidgetErrorBoundary
|
|
fallback={<TodaySectionErrorFallback message={sectionErrorMessage} />}
|
|
>
|
|
<div
|
|
className={`grid grid-cols-1 gap-4 lg:grid-cols-2 ${loading ? 'opacity-70 transition-opacity' : ''}`}
|
|
>
|
|
<TodayUpcomingAppointments
|
|
actions={data?.actions ?? {}}
|
|
loading={loading}
|
|
isInitialLoad={isInitialLoad}
|
|
/>
|
|
{showCharts ? (
|
|
<TodayChartsSection
|
|
charts={data?.charts ?? {}}
|
|
loading={loading}
|
|
isInitialLoad={isInitialLoad}
|
|
embedded
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</TodayWidgetErrorBoundary>
|
|
) : showCharts && (!error || data) ? (
|
|
<TodayWidgetErrorBoundary
|
|
fallback={<TodaySectionErrorFallback message={sectionErrorMessage} />}
|
|
>
|
|
<TodayChartsSection
|
|
charts={data?.charts ?? {}}
|
|
loading={loading}
|
|
isInitialLoad={isInitialLoad}
|
|
/>
|
|
</TodayWidgetErrorBoundary>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|