99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useTranslations } from 'next-intl';
|
|||
|
|
import { ChevronRight } from 'lucide-react';
|
|||
|
|
import { Link } from '@/i18n/navigation';
|
|||
|
|
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 type { TodaySummaryActions } from '@/types/today';
|
|||
|
|
|
|||
|
|
interface TodayUpcomingAppointmentsProps {
|
|||
|
|
actions: TodaySummaryActions;
|
|||
|
|
loading?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function TodayUpcomingAppointments({
|
|||
|
|
actions,
|
|||
|
|
loading = false,
|
|||
|
|
}: TodayUpcomingAppointmentsProps) {
|
|||
|
|
const t = useTranslations('today');
|
|||
|
|
const { currentOrganization } = useAuth();
|
|||
|
|
|
|||
|
|
if (!canAccessAppointmentsSection(currentOrganization)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const appointments = actions.upcomingAppointmentsToday ?? [];
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<Card>
|
|||
|
|
<h2 className="text-base font-semibold text-card-foreground">{t('upcomingAppointmentsTitle')}</h2>
|
|||
|
|
<div className="mt-4 space-y-3">
|
|||
|
|
{[0, 1, 2].map((key) => (
|
|||
|
|
<div key={key} className="h-12 animate-pulse rounded bg-background-secondary/60" />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Card>
|
|||
|
|
<div className="flex items-center justify-between gap-3 mb-4">
|
|||
|
|
<div>
|
|||
|
|
<h2 className="text-base font-semibold text-card-foreground">
|
|||
|
|
{t('upcomingAppointmentsTitle')}
|
|||
|
|
</h2>
|
|||
|
|
<p className="text-xs text-text-muted mt-1">{t('upcomingAppointmentsSubtitle')}</p>
|
|||
|
|
</div>
|
|||
|
|
<Link
|
|||
|
|
href="/appointments"
|
|||
|
|
className="text-xs font-medium text-primary hover:underline underline-offset-2 shrink-0"
|
|||
|
|
>
|
|||
|
|
{t('viewAllAppointments')}
|
|||
|
|
</Link>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{appointments.length === 0 ? (
|
|||
|
|
<p className="text-sm text-text-muted">{t('noUpcomingAppointments')}</p>
|
|||
|
|
) : (
|
|||
|
|
<ul className="divide-y divide-border/40">
|
|||
|
|
{appointments.map((appointment) => {
|
|||
|
|
const start = new Date(appointment.startAt);
|
|||
|
|
const end = new Date(appointment.endAt);
|
|||
|
|
const timeLabel = `${formatTimeForInput(start)} – ${formatTimeForInput(end)}`;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<li key={appointment.id}>
|
|||
|
|
<Link
|
|||
|
|
href="/appointments"
|
|||
|
|
className="flex items-center justify-between gap-3 py-3 -mx-2 px-2 rounded-[var(--radius-md)] hover:bg-background-secondary/45 transition-colors group"
|
|||
|
|
>
|
|||
|
|
<div className="min-w-0">
|
|||
|
|
<p className="text-sm font-medium text-text-primary truncate">
|
|||
|
|
{appointment.patientName}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-xs text-text-muted mt-0.5">
|
|||
|
|
{timeLabel}
|
|||
|
|
{appointment.purpose ? (
|
|||
|
|
<span className="text-text-secondary"> · {appointment.purpose}</span>
|
|||
|
|
) : null}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<ChevronRight
|
|||
|
|
className="h-4 w-4 shrink-0 text-text-muted opacity-0 group-hover:opacity-100 transition-opacity"
|
|||
|
|
aria-hidden
|
|||
|
|
/>
|
|||
|
|
</Link>
|
|||
|
|
</li>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</ul>
|
|||
|
|
)}
|
|||
|
|
</Card>
|
|||
|
|
);
|
|||
|
|
}
|