2026-07-11 00:48:35 +03:30
|
|
|
|
'use client';
|
|
|
|
|
|
|
2026-07-11 03:12:56 +03:30
|
|
|
|
import { useEffect, useState } from 'react';
|
2026-07-11 00:48:35 +03:30
|
|
|
|
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';
|
2026-07-11 03:12:56 +03:30
|
|
|
|
import { purposeLabel } from '@/components/ui/appointments/appointmentPurposeStyles';
|
|
|
|
|
|
import { treatmentAppointmentHref } from '@/components/shared/treatmentSelection';
|
|
|
|
|
|
import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay';
|
2026-07-12 00:28:43 +03:30
|
|
|
|
import { canViewMyAppointmentsWeekChart } from '@/components/shared/permissions';
|
2026-07-11 00:48:35 +03:30
|
|
|
|
import { useAuth } from '@/lib/hooks/useAuth';
|
2026-07-11 03:12:56 +03:30
|
|
|
|
import { treatmentCatalogApi } from '@/lib/api/treatment-catalog';
|
2026-07-11 00:53:40 +03:30
|
|
|
|
import { ListRowSkeleton } from '@/components/today/TodaySkeleton';
|
2026-07-11 03:12:56 +03:30
|
|
|
|
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
|
2026-07-11 00:48:35 +03:30
|
|
|
|
import type { TodaySummaryActions } from '@/types/today';
|
|
|
|
|
|
|
|
|
|
|
|
interface TodayUpcomingAppointmentsProps {
|
|
|
|
|
|
actions: TodaySummaryActions;
|
|
|
|
|
|
loading?: boolean;
|
2026-07-11 00:53:40 +03:30
|
|
|
|
isInitialLoad?: boolean;
|
2026-07-11 00:48:35 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function TodayUpcomingAppointments({
|
|
|
|
|
|
actions,
|
|
|
|
|
|
loading = false,
|
2026-07-11 00:53:40 +03:30
|
|
|
|
isInitialLoad = false,
|
2026-07-11 00:48:35 +03:30
|
|
|
|
}: TodayUpcomingAppointmentsProps) {
|
|
|
|
|
|
const t = useTranslations('today');
|
|
|
|
|
|
const { currentOrganization } = useAuth();
|
2026-07-11 03:12:56 +03:30
|
|
|
|
const [treatmentCatalog, setTreatmentCatalog] = useState<TreatmentCatalogEntry[]>([]);
|
2026-07-11 00:48:35 +03:30
|
|
|
|
|
2026-07-11 03:12:56 +03:30
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
void treatmentCatalogApi
|
|
|
|
|
|
.list()
|
|
|
|
|
|
.then((response) => setTreatmentCatalog(response.data))
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
!currentOrganization ||
|
|
|
|
|
|
currentOrganization.type !== 'CLINIC' ||
|
2026-07-12 00:28:43 +03:30
|
|
|
|
!canViewMyAppointmentsWeekChart(currentOrganization)
|
2026-07-11 03:12:56 +03:30
|
|
|
|
) {
|
2026-07-11 00:48:35 +03:30
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 22:32:37 +03:30
|
|
|
|
const appointments = actions.upcomingAppointmentsToday ?? [];
|
2026-07-11 00:48:35 +03:30
|
|
|
|
|
2026-07-11 00:53:40 +03:30
|
|
|
|
if (isInitialLoad) {
|
2026-07-11 00:48:35 +03:30
|
|
|
|
return (
|
2026-07-11 22:32:37 +03:30
|
|
|
|
<Card className="flex h-full min-h-0 flex-col p-3">
|
2026-07-11 03:12:56 +03:30
|
|
|
|
<div className="mb-2 space-y-1.5">
|
|
|
|
|
|
<div className="h-3.5 w-32 animate-pulse rounded bg-background-secondary/60" />
|
|
|
|
|
|
<div className="h-3 w-44 animate-pulse rounded bg-background-secondary/60" />
|
2026-07-11 00:53:40 +03:30
|
|
|
|
</div>
|
2026-07-11 03:12:56 +03:30
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{[0, 1].map((key) => (
|
|
|
|
|
|
<ListRowSkeleton key={key} compact />
|
2026-07-11 00:48:35 +03:30
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-11 22:32:37 +03:30
|
|
|
|
<Card className="flex h-full min-h-0 flex-col p-3">
|
2026-07-11 03:12:56 +03:30
|
|
|
|
<div className="mb-2 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
|
2026-07-11 00:48:35 +03:30
|
|
|
|
<div>
|
2026-07-11 03:12:56 +03:30
|
|
|
|
<h2 className="text-sm font-semibold text-card-foreground">
|
2026-07-11 00:48:35 +03:30
|
|
|
|
{t('upcomingAppointmentsTitle')}
|
|
|
|
|
|
</h2>
|
2026-07-11 03:12:56 +03:30
|
|
|
|
<p className="mt-0.5 text-[11px] text-text-muted">{t('upcomingAppointmentsSubtitle')}</p>
|
2026-07-11 00:48:35 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
<Link
|
2026-07-11 03:12:56 +03:30
|
|
|
|
href={treatmentAppointmentHref()}
|
|
|
|
|
|
className="shrink-0 text-[11px] font-medium text-primary hover:underline underline-offset-2"
|
2026-07-11 00:48:35 +03:30
|
|
|
|
>
|
|
|
|
|
|
{t('viewAllAppointments')}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{appointments.length === 0 ? (
|
2026-07-11 03:12:56 +03:30
|
|
|
|
<div className="flex min-h-[72px] items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border/50 bg-background-secondary/20 px-3">
|
|
|
|
|
|
<p className="text-xs text-text-muted text-center">{t('noUpcomingAppointments')}</p>
|
2026-07-11 00:53:40 +03:30
|
|
|
|
</div>
|
2026-07-11 00:48:35 +03:30
|
|
|
|
) : (
|
2026-07-11 22:32:37 +03:30
|
|
|
|
<div className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto">
|
|
|
|
|
|
<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)}`;
|
|
|
|
|
|
const purposeIndex = treatmentCatalog.findIndex(
|
|
|
|
|
|
(entry) => entry.code === appointment.purpose,
|
|
|
|
|
|
);
|
|
|
|
|
|
const purposeTextColor = treatmentTypeColor(
|
|
|
|
|
|
appointment.purpose,
|
|
|
|
|
|
purposeIndex < 0 ? 0 : purposeIndex,
|
|
|
|
|
|
);
|
|
|
|
|
|
const purposeDisplay = purposeLabel(appointment.purpose, treatmentCatalog);
|
2026-07-11 00:48:35 +03:30
|
|
|
|
|
2026-07-11 22:32:37 +03:30
|
|
|
|
return (
|
|
|
|
|
|
<li key={appointment.id}>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={treatmentAppointmentHref(appointment.id)}
|
|
|
|
|
|
className="group -mx-1 flex items-center justify-between gap-2 rounded-[var(--radius-md)] px-1 py-2 transition-colors hover:bg-background-secondary/45"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
|
<p className="truncate text-xs font-medium text-text-primary">
|
|
|
|
|
|
{appointment.patientName}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="mt-0.5 truncate text-[11px] text-text-muted">
|
|
|
|
|
|
{timeLabel}
|
|
|
|
|
|
{appointment.purpose ? (
|
|
|
|
|
|
<span style={{ color: purposeTextColor }}> · {purposeDisplay}</span>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ChevronRight
|
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 text-text-muted opacity-0 transition-opacity group-hover:opacity-100"
|
|
|
|
|
|
aria-hidden
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-07-11 00:48:35 +03:30
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|