Shitty gadgets removed. Some useful gadgets added.

This commit is contained in:
2026-07-11 03:12:56 +03:30
parent 2f8c9f0f4d
commit ed8ff61205
31 changed files with 1489 additions and 170 deletions

View File

@@ -1,13 +1,19 @@
'use client';
import { useEffect, useState } from 'react';
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 { purposeLabel } from '@/components/ui/appointments/appointmentPurposeStyles';
import { treatmentAppointmentHref } from '@/components/shared/treatmentSelection';
import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay';
import { canViewTreatment } from '@/components/shared/permissions';
import { useAuth } from '@/lib/hooks/useAuth';
import { treatmentCatalogApi } from '@/lib/api/treatment-catalog';
import { ListRowSkeleton } from '@/components/today/TodaySkeleton';
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
import type { TodaySummaryActions } from '@/types/today';
interface TodayUpcomingAppointmentsProps {
@@ -16,6 +22,8 @@ interface TodayUpcomingAppointmentsProps {
isInitialLoad?: boolean;
}
const MAX_VISIBLE = 3;
export function TodayUpcomingAppointments({
actions,
loading = false,
@@ -23,23 +31,35 @@ export function TodayUpcomingAppointments({
}: TodayUpcomingAppointmentsProps) {
const t = useTranslations('today');
const { currentOrganization } = useAuth();
const [treatmentCatalog, setTreatmentCatalog] = useState<TreatmentCatalogEntry[]>([]);
if (!canAccessAppointmentsSection(currentOrganization)) {
useEffect(() => {
void treatmentCatalogApi
.list()
.then((response) => setTreatmentCatalog(response.data))
.catch(() => {});
}, []);
if (
!currentOrganization ||
currentOrganization.type !== 'CLINIC' ||
!canViewTreatment(currentOrganization)
) {
return null;
}
const appointments = actions.upcomingAppointmentsToday ?? [];
const appointments = (actions.upcomingAppointmentsToday ?? []).slice(0, MAX_VISIBLE);
if (isInitialLoad) {
return (
<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" />
<Card className="min-h-[140px] p-3">
<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" />
</div>
<div className="space-y-3">
{[0, 1, 2].map((key) => (
<ListRowSkeleton key={key} />
<div className="space-y-2">
{[0, 1].map((key) => (
<ListRowSkeleton key={key} compact />
))}
</div>
</Card>
@@ -47,25 +67,25 @@ export function TodayUpcomingAppointments({
}
return (
<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">
<Card className="min-h-[140px] p-3">
<div className="mb-2 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 className="text-base font-semibold text-card-foreground">
<h2 className="text-sm font-semibold text-card-foreground">
{t('upcomingAppointmentsTitle')}
</h2>
<p className="text-xs text-text-muted mt-1">{t('upcomingAppointmentsSubtitle')}</p>
<p className="mt-0.5 text-[11px] text-text-muted">{t('upcomingAppointmentsSubtitle')}</p>
</div>
<Link
href="/appointments"
className="text-xs font-medium text-primary hover:underline underline-offset-2 shrink-0"
href={treatmentAppointmentHref()}
className="shrink-0 text-[11px] font-medium text-primary hover:underline underline-offset-2"
>
{t('viewAllAppointments')}
</Link>
</div>
{appointments.length === 0 ? (
<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 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>
</div>
) : (
<ul className="divide-y divide-border/40">
@@ -73,26 +93,32 @@ export function TodayUpcomingAppointments({
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);
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"
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="text-sm font-medium text-text-primary truncate">
<p className="truncate text-xs font-medium text-text-primary">
{appointment.patientName}
</p>
<p className="text-xs text-text-muted mt-0.5 truncate">
<p className="mt-0.5 truncate text-[11px] text-text-muted">
{timeLabel}
{appointment.purpose ? (
<span className="text-text-secondary"> · {appointment.purpose}</span>
<span style={{ color: purposeTextColor }}> · {purposeDisplay}</span>
) : null}
</p>
</div>
<ChevronRight
className="h-4 w-4 shrink-0 text-text-muted opacity-0 group-hover:opacity-100 transition-opacity"
className="h-3.5 w-3.5 shrink-0 text-text-muted opacity-0 transition-opacity group-hover:opacity-100"
aria-hidden
/>
</Link>