فهرست منبع

[hack] implement calendar por appointments

Matthew Trejo 2 ماه پیش
والد
کامیت
7934e77ed8

+ 97 - 0
src/app/appointments/calendar/page.tsx

@@ -0,0 +1,97 @@
+"use client";
+
+import { useSession } from "next-auth/react";
+import { redirect } from "next/navigation";
+import { Loader2 } from "lucide-react";
+import AuthenticatedLayout from "@/components/AuthenticatedLayout";
+import { useAppointments } from "@/hooks/useAppointments";
+import { useCalendar } from "@/hooks/useCalendar";
+import { CalendarHeader } from "@/components/appointments/calendar/CalendarHeader";
+import { CalendarView } from "@/components/appointments/calendar/CalendarView";
+import { CalendarAppointmentsList } from "@/components/appointments/calendar/CalendarAppointmentsList";
+import { CalendarFilters } from "@/components/appointments/calendar/CalendarFilters";
+import { CalendarStats } from "@/components/appointments/calendar/CalendarStats";
+import { COLOR_PALETTE } from "@/utils/palette";
+
+export default function AppointmentsCalendarPage() {
+  const { data: session, status } = useSession();
+  const { appointments, isLoading } = useAppointments();
+
+  const {
+    selectedDate,
+    setSelectedDate,
+    currentFilter,
+    setCurrentFilter,
+    appointmentsInSelectedDay,
+    daysWithAppointments,
+    monthStats,
+  } = useCalendar(appointments);
+
+  if (status === "loading" || isLoading) {
+    return (
+      <AuthenticatedLayout>
+        <div className="flex items-center justify-center min-h-screen">
+          <Loader2 
+            className="h-8 w-8 animate-spin" 
+            style={{ color: COLOR_PALETTE.primary[600] }}
+          />
+        </div>
+      </AuthenticatedLayout>
+    );
+  }
+
+  if (!session) {
+    redirect("/auth/login");
+  }
+
+  // Solo doctores pueden acceder
+  if (session.user.role !== "DOCTOR") {
+    redirect("/appointments");
+  }
+
+  return (
+    <AuthenticatedLayout>
+      <div className="container mx-auto px-4 py-6 space-y-6">
+        {/* Header */}
+        <CalendarHeader
+          selectedDate={selectedDate}
+          totalAppointments={monthStats.total}
+        />
+
+        {/* Stats */}
+        <CalendarStats
+          total={monthStats.total}
+          pending={monthStats.pending}
+          approved={monthStats.approved}
+          completed={monthStats.completed}
+        />
+
+        {/* Main Content */}
+        <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
+          {/* Sidebar - Calendario y Filtros */}
+          <div className="lg:col-span-4 space-y-4">
+            <CalendarView
+              selectedDate={selectedDate}
+              onSelectDate={setSelectedDate}
+              daysWithAppointments={daysWithAppointments}
+            />
+            
+            <CalendarFilters
+              currentFilter={currentFilter}
+              onFilterChange={setCurrentFilter}
+              counts={monthStats}
+            />
+          </div>
+
+          {/* Lista de Citas del Día Seleccionado */}
+          <div className="lg:col-span-8">
+            <CalendarAppointmentsList
+              appointments={appointmentsInSelectedDay}
+              selectedDate={selectedDate}
+            />
+          </div>
+        </div>
+      </div>
+    </AuthenticatedLayout>
+  );
+}

+ 208 - 0
src/components/appointments/calendar/CalendarAppointmentsList.tsx

@@ -0,0 +1,208 @@
+"use client";
+
+import { format, parseISO } from "date-fns";
+import { es } from "date-fns/locale";
+import { Clock, User, FileText, Calendar as CalendarIcon } from "lucide-react";
+import { COLOR_PALETTE } from "@/utils/palette";
+import type { Appointment } from "@/types/appointments";
+import Link from "next/link";
+
+interface CalendarAppointmentsListProps {
+  appointments: Appointment[];
+  selectedDate: Date;
+}
+
+const estadoConfig = {
+  PENDIENTE: { 
+    label: "Pendiente", 
+    color: COLOR_PALETTE.warning[500],
+    bgColor: COLOR_PALETTE.warning[50],
+    borderColor: COLOR_PALETTE.warning[200]
+  },
+  APROBADA: { 
+    label: "Aprobada", 
+    color: COLOR_PALETTE.success[600],
+    bgColor: COLOR_PALETTE.success[50],
+    borderColor: COLOR_PALETTE.success[200]
+  },
+  COMPLETADA: { 
+    label: "Completada", 
+    color: COLOR_PALETTE.primary[600],
+    bgColor: COLOR_PALETTE.primary[50],
+    borderColor: COLOR_PALETTE.primary[200]
+  },
+  CANCELADA: { 
+    label: "Cancelada", 
+    color: COLOR_PALETTE.gray[500],
+    bgColor: COLOR_PALETTE.gray[50],
+    borderColor: COLOR_PALETTE.gray[200]
+  },
+  RECHAZADA: { 
+    label: "Rechazada", 
+    color: COLOR_PALETTE.error[600],
+    bgColor: COLOR_PALETTE.error[50],
+    borderColor: COLOR_PALETTE.error[200]
+  },
+};
+
+export function CalendarAppointmentsList({
+  appointments,
+  selectedDate,
+}: CalendarAppointmentsListProps) {
+  
+  if (appointments.length === 0) {
+    return (
+      <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
+        <div className="text-center">
+          <CalendarIcon 
+            className="mx-auto h-12 w-12 mb-3" 
+            style={{ color: COLOR_PALETTE.gray[400] }}
+          />
+          <h3 
+            className="text-lg font-medium mb-1"
+            style={{ color: COLOR_PALETTE.gray[900] }}
+          >
+            No hay citas programadas
+          </h3>
+          <p style={{ color: COLOR_PALETTE.gray[500] }}>
+            {format(selectedDate, "EEEE, d 'de' MMMM 'de' yyyy", { locale: es })}
+          </p>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center justify-between">
+        <h3 
+          className="text-lg font-semibold"
+          style={{ color: COLOR_PALETTE.gray[900] }}
+        >
+          Citas del {format(selectedDate, "d 'de' MMMM", { locale: es })}
+        </h3>
+        <span 
+          className="text-sm font-medium px-3 py-1 rounded-full"
+          style={{ 
+            backgroundColor: COLOR_PALETTE.primary[50],
+            color: COLOR_PALETTE.primary[600]
+          }}
+        >
+          {appointments.length} {appointments.length === 1 ? 'cita' : 'citas'}
+        </span>
+      </div>
+
+      <div className="space-y-3">
+        {appointments.map((appointment) => {
+          const config = estadoConfig[appointment.estado as keyof typeof estadoConfig];
+          const appointmentDate = appointment.fechaSolicitada 
+            ? (typeof appointment.fechaSolicitada === 'string' 
+                ? parseISO(appointment.fechaSolicitada) 
+                : appointment.fechaSolicitada)
+            : null;
+
+          return (
+            <Link
+              key={appointment.id}
+              href={`/appointments/${appointment.id}`}
+              className="block"
+            >
+              <div
+                className="bg-white rounded-lg shadow-sm border p-4 hover:shadow-md transition-all cursor-pointer"
+                style={{ 
+                  borderColor: COLOR_PALETTE.gray[200],
+                }}
+              >
+                <div className="flex items-start justify-between gap-4">
+                  <div className="flex-1 space-y-3">
+                    {/* Hora y estado */}
+                    <div className="flex items-center gap-3 flex-wrap">
+                      <div 
+                        className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg"
+                        style={{ backgroundColor: COLOR_PALETTE.gray[50] }}
+                      >
+                        <Clock className="h-4 w-4" style={{ color: COLOR_PALETTE.gray[600] }} />
+                        <span 
+                          className="font-semibold text-sm"
+                          style={{ color: COLOR_PALETTE.gray[900] }}
+                        >
+                          {appointmentDate ? format(appointmentDate, "HH:mm", { locale: es }) : 'Sin hora'}
+                        </span>
+                      </div>
+                      <span
+                        className="text-xs font-semibold px-3 py-1.5 rounded-lg"
+                        style={{
+                          backgroundColor: config.bgColor,
+                          color: config.color,
+                        }}
+                      >
+                        {config.label}
+                      </span>
+                    </div>
+
+                    {/* Paciente */}
+                    <div className="flex items-center gap-2">
+                      <div 
+                        className="p-1.5 rounded-lg"
+                        style={{ backgroundColor: COLOR_PALETTE.primary[50] }}
+                      >
+                        <User className="h-4 w-4" style={{ color: COLOR_PALETTE.primary[600] }} />
+                      </div>
+                      <div>
+                        <p className="text-xs" style={{ color: COLOR_PALETTE.gray[500] }}>
+                          Paciente
+                        </p>
+                        <p className="font-medium" style={{ color: COLOR_PALETTE.gray[900] }}>
+                          {appointment.paciente?.name} {appointment.paciente?.lastname}
+                        </p>
+                      </div>
+                    </div>
+
+                    {/* Motivo */}
+                    <div className="flex items-start gap-2">
+                      <div 
+                        className="p-1.5 rounded-lg mt-0.5"
+                        style={{ backgroundColor: COLOR_PALETTE.gray[50] }}
+                      >
+                        <FileText className="h-4 w-4" style={{ color: COLOR_PALETTE.gray[600] }} />
+                      </div>
+                      <div className="flex-1">
+                        <p className="text-xs mb-1" style={{ color: COLOR_PALETTE.gray[500] }}>
+                          Motivo de consulta
+                        </p>
+                        <p 
+                          className="text-sm line-clamp-2"
+                          style={{ color: COLOR_PALETTE.gray[700] }}
+                        >
+                          {appointment.motivoConsulta}
+                        </p>
+                      </div>
+                    </div>
+                  </div>
+
+                  {/* Flecha indicadora */}
+                  <div className="flex items-center">
+                    <div 
+                      className="p-2 rounded-lg"
+                      style={{ backgroundColor: COLOR_PALETTE.gray[50] }}
+                    >
+                      <svg
+                        className="h-5 w-5"
+                        style={{ color: COLOR_PALETTE.gray[400] }}
+                        fill="none"
+                        viewBox="0 0 24 24"
+                        stroke="currentColor"
+                      >
+                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+                      </svg>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </Link>
+          );
+        })}
+      </div>
+    </div>
+  );
+}

+ 73 - 0
src/components/appointments/calendar/CalendarFilters.tsx

@@ -0,0 +1,73 @@
+"use client";
+
+import { COLOR_PALETTE } from "@/utils/palette";
+import type { CalendarFilter } from "@/hooks/useCalendar";
+
+interface CalendarFiltersProps {
+  currentFilter: CalendarFilter;
+  onFilterChange: (filter: CalendarFilter) => void;
+  counts: {
+    total: number;
+    pending: number;
+    approved: number;
+    completed: number;
+    cancelled: number;
+  };
+}
+
+const filters: Array<{ id: CalendarFilter; label: string; key: keyof CalendarFiltersProps['counts'] }> = [
+  { id: "all", label: "Todas", key: "total" },
+  { id: "pending", label: "Pendientes", key: "pending" },
+  { id: "approved", label: "Aprobadas", key: "approved" },
+  { id: "completed", label: "Completadas", key: "completed" },
+  { id: "cancelled", label: "Canceladas", key: "cancelled" },
+];
+
+export function CalendarFilters({
+  currentFilter,
+  onFilterChange,
+  counts,
+}: CalendarFiltersProps) {
+  return (
+    <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
+      <h3 
+        className="text-sm font-semibold mb-3"
+        style={{ color: COLOR_PALETTE.gray[700] }}
+      >
+        Filtrar por estado
+      </h3>
+      
+      <div className="space-y-2">
+        {filters.map((filter) => {
+          const isActive = currentFilter === filter.id;
+          const count = counts[filter.key];
+          
+          return (
+            <button
+              key={filter.id}
+              onClick={() => onFilterChange(filter.id)}
+              className="w-full flex items-center justify-between px-3 py-2 rounded-lg transition-all text-left"
+              style={{
+                backgroundColor: isActive ? COLOR_PALETTE.primary[50] : 'transparent',
+                color: isActive ? COLOR_PALETTE.primary[700] : COLOR_PALETTE.gray[700],
+                fontWeight: isActive ? 600 : 400,
+              }}
+            >
+              <span className="text-sm">{filter.label}</span>
+              <span 
+                className="text-xs px-2 py-0.5 rounded-full"
+                style={{
+                  backgroundColor: isActive ? COLOR_PALETTE.primary[100] : COLOR_PALETTE.gray[100],
+                  color: isActive ? COLOR_PALETTE.primary[700] : COLOR_PALETTE.gray[600],
+                  fontWeight: 600,
+                }}
+              >
+                {count}
+              </span>
+            </button>
+          );
+        })}
+      </div>
+    </div>
+  );
+}

+ 89 - 0
src/components/appointments/calendar/CalendarHeader.tsx

@@ -0,0 +1,89 @@
+"use client";
+
+import { CalendarDays } from "lucide-react";
+import { COLOR_PALETTE } from "@/utils/palette";
+import { format } from "date-fns";
+import { es } from "date-fns/locale";
+
+interface CalendarHeaderProps {
+  selectedDate: Date;
+  totalAppointments: number;
+}
+
+export function CalendarHeader({ selectedDate, totalAppointments }: CalendarHeaderProps) {
+  return (
+    <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
+      <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
+        <div className="flex items-center gap-3 flex-1">
+          <div 
+            className="p-3 rounded-lg flex-shrink-0"
+            style={{ backgroundColor: COLOR_PALETTE.primary[50] }}
+          >
+            <CalendarDays 
+              className="h-6 w-6" 
+              style={{ color: COLOR_PALETTE.primary[600] }}
+            />
+          </div>
+          <div>
+            <h1 
+              className="text-2xl font-bold"
+              style={{ color: COLOR_PALETTE.gray[900] }}
+            >
+              Calendario de Citas
+            </h1>
+            <p 
+              className="text-sm mt-1"
+              style={{ color: COLOR_PALETTE.gray[500] }}
+            >
+              Gestiona y visualiza todas tus citas médicas
+            </p>
+          </div>
+        </div>
+
+        <div className="flex items-center gap-4 flex-shrink-0">
+          <div 
+            className="text-right px-4 py-3 rounded-lg border"
+            style={{ 
+              backgroundColor: COLOR_PALETTE.primary[50],
+              borderColor: COLOR_PALETTE.primary[200]
+            }}
+          >
+            <div 
+              className="text-2xl font-bold"
+              style={{ color: COLOR_PALETTE.primary[700] }}
+            >
+              {totalAppointments}
+            </div>
+            <div 
+              className="text-xs font-medium"
+              style={{ color: COLOR_PALETTE.primary[600] }}
+            >
+              Citas del mes
+            </div>
+          </div>
+          
+          <div 
+            className="text-right px-4 py-3 rounded-lg border"
+            style={{ 
+              backgroundColor: COLOR_PALETTE.gray[50],
+              borderColor: COLOR_PALETTE.gray[200]
+            }}
+          >
+            <div 
+              className="text-sm font-semibold"
+              style={{ color: COLOR_PALETTE.gray[900] }}
+            >
+              {format(selectedDate, "MMMM", { locale: es })}
+            </div>
+            <div 
+              className="text-xs"
+              style={{ color: COLOR_PALETTE.gray[600] }}
+            >
+              {format(selectedDate, "yyyy", { locale: es })}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 86 - 0
src/components/appointments/calendar/CalendarStats.tsx

@@ -0,0 +1,86 @@
+"use client";
+
+import { COLOR_PALETTE } from "@/utils/palette";
+import { Calendar, Clock, CheckCircle2, XCircle } from "lucide-react";
+
+interface CalendarStatsProps {
+  total: number;
+  pending: number;
+  approved: number;
+  completed: number;
+}
+
+export function CalendarStats({
+  total,
+  pending,
+  approved,
+  completed,
+}: CalendarStatsProps) {
+  const stats = [
+    {
+      label: "Total",
+      value: total,
+      icon: Calendar,
+      color: COLOR_PALETTE.primary[600],
+      bgColor: COLOR_PALETTE.primary[50],
+    },
+    {
+      label: "Pendientes",
+      value: pending,
+      icon: Clock,
+      color: COLOR_PALETTE.warning[600],
+      bgColor: COLOR_PALETTE.warning[50],
+    },
+    {
+      label: "Aprobadas",
+      value: approved,
+      icon: CheckCircle2,
+      color: COLOR_PALETTE.success[600],
+      bgColor: COLOR_PALETTE.success[50],
+    },
+    {
+      label: "Completadas",
+      value: completed,
+      icon: CheckCircle2,
+      color: COLOR_PALETTE.primary[600],
+      bgColor: COLOR_PALETTE.primary[50],
+    },
+  ];
+
+  return (
+    <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
+      {stats.map((stat) => {
+        const Icon = stat.icon;
+        return (
+          <div
+            key={stat.label}
+            className="bg-white rounded-lg shadow-sm border border-gray-200 p-4"
+          >
+            <div className="flex items-center gap-3">
+              <div
+                className="p-2 rounded-lg"
+                style={{ backgroundColor: stat.bgColor }}
+              >
+                <Icon className="h-5 w-5" style={{ color: stat.color }} />
+              </div>
+              <div>
+                <p 
+                  className="text-2xl font-bold"
+                  style={{ color: COLOR_PALETTE.gray[900] }}
+                >
+                  {stat.value}
+                </p>
+                <p 
+                  className="text-sm"
+                  style={{ color: COLOR_PALETTE.gray[500] }}
+                >
+                  {stat.label}
+                </p>
+              </div>
+            </div>
+          </div>
+        );
+      })}
+    </div>
+  );
+}

+ 54 - 0
src/components/appointments/calendar/CalendarView.tsx

@@ -0,0 +1,54 @@
+"use client";
+
+import { Calendar } from "@/components/ui/calendar";
+import { Card } from "@/components/ui/card";
+import { format, parseISO } from "date-fns";
+import { es } from "date-fns/locale";
+
+interface CalendarViewProps {
+  selectedDate: Date;
+  onSelectDate: (date: Date) => void;
+  daysWithAppointments: Set<string>;
+}
+
+export function CalendarView({
+  selectedDate,
+  onSelectDate,
+  daysWithAppointments,
+}: CalendarViewProps) {
+  
+  const modifiers = {
+    hasAppointments: (date: Date) => {
+      const dateStr = date.toISOString().split('T')[0];
+      return daysWithAppointments.has(dateStr);
+    },
+  };
+
+  const modifiersClassNames = {
+    hasAppointments: "font-semibold underline text-primary",
+  };
+
+  return (
+    <Card className="p-4 sm:p-6">
+      <Calendar
+        className="w-full"
+        mode="single"
+        selected={selectedDate}
+        onSelect={(date) => date && onSelectDate(date)}
+        locale={es}
+        modifiers={modifiers}
+        modifiersClassNames={modifiersClassNames}
+        showOutsideDays
+      />
+      
+      <div className="mt-4 pt-4 border-t">
+        <div className="flex items-center gap-2 text-sm text-muted-foreground">
+          <div className="flex items-center gap-1.5">
+            <div className="w-3 h-3 rounded-full bg-primary" />
+            <span className="text-xs sm:text-sm">Días con citas</span>
+          </div>
+        </div>
+      </div>
+    </Card>
+  );
+}

+ 7 - 1
src/components/sidebar/SidebarNavigation.tsx

@@ -14,7 +14,8 @@ import {
   Calendar,
   Sparkles,
   BookOpen,
-  User
+  User,
+  CalendarDays
 } from "lucide-react"
 import { COLOR_PALETTE } from "@/utils/palette"
 import { useAppointmentsBadge } from "@/hooks/useAppointmentsBadge"
@@ -144,6 +145,11 @@ export default function SidebarNavigation({ onItemClick, isCollapsed = false }:
               href: "/appointments/doctor",
               icon: Calendar,
               badge: pendingCount > 0 ? pendingCount.toString() : undefined
+            },
+            {
+              title: "Calendario",
+              href: "/appointments/calendar",
+              icon: CalendarDays
             }
           ]
         }

+ 93 - 0
src/hooks/useCalendar.ts

@@ -0,0 +1,93 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import { startOfMonth, endOfMonth, isSameDay, parseISO, isWithinInterval, startOfDay, endOfDay } from "date-fns";
+import type { Appointment } from "@/types/appointments";
+
+export type CalendarView = "month" | "week" | "day";
+export type CalendarFilter = "all" | "pending" | "approved" | "completed" | "cancelled";
+
+export const useCalendar = (appointments: Appointment[]) => {
+  const [selectedDate, setSelectedDate] = useState<Date>(new Date());
+  const [currentFilter, setCurrentFilter] = useState<CalendarFilter>("all");
+  const [currentView, setCurrentView] = useState<CalendarView>("month");
+
+  // Filtrar citas por estado
+  const filteredByStatus = useMemo(() => {
+    if (currentFilter === "all") return appointments;
+    if (currentFilter === "pending") return appointments.filter(a => a.estado === "PENDIENTE");
+    if (currentFilter === "approved") return appointments.filter(a => a.estado === "APROBADA");
+    if (currentFilter === "completed") return appointments.filter(a => a.estado === "COMPLETADA");
+    if (currentFilter === "cancelled") return appointments.filter(a => a.estado === "CANCELADA" || a.estado === "RECHAZADA");
+    return appointments;
+  }, [appointments, currentFilter]);
+
+  // Obtener citas del mes actual
+  const appointmentsInMonth = useMemo(() => {
+    const monthStart = startOfMonth(selectedDate);
+    const monthEnd = endOfMonth(selectedDate);
+
+    return filteredByStatus.filter(appointment => {
+      if (!appointment.fechaSolicitada) return false;
+      const appointmentDate = typeof appointment.fechaSolicitada === 'string' 
+        ? parseISO(appointment.fechaSolicitada) 
+        : appointment.fechaSolicitada;
+      
+      return isWithinInterval(appointmentDate, { start: monthStart, end: monthEnd });
+    });
+  }, [filteredByStatus, selectedDate]);
+
+  // Obtener citas del día seleccionado
+  const appointmentsInSelectedDay = useMemo(() => {
+    return filteredByStatus.filter(appointment => {
+      if (!appointment.fechaSolicitada) return false;
+      const appointmentDate = typeof appointment.fechaSolicitada === 'string'
+        ? parseISO(appointment.fechaSolicitada)
+        : appointment.fechaSolicitada;
+      
+      return isSameDay(appointmentDate, selectedDate);
+    }).sort((a, b) => {
+      const dateA = typeof a.fechaSolicitada === 'string' ? parseISO(a.fechaSolicitada) : a.fechaSolicitada;
+      const dateB = typeof b.fechaSolicitada === 'string' ? parseISO(b.fechaSolicitada) : b.fechaSolicitada;
+      return dateA!.getTime() - dateB!.getTime();
+    });
+  }, [filteredByStatus, selectedDate]);
+
+  // Obtener días con citas en el mes
+  const daysWithAppointments = useMemo(() => {
+    const days = new Set<string>();
+    appointmentsInMonth.forEach(appointment => {
+      if (appointment.fechaSolicitada) {
+        const date = typeof appointment.fechaSolicitada === 'string'
+          ? parseISO(appointment.fechaSolicitada)
+          : appointment.fechaSolicitada;
+        days.add(date.toISOString().split('T')[0]);
+      }
+    });
+    return days;
+  }, [appointmentsInMonth]);
+
+  // Estadísticas del mes
+  const monthStats = useMemo(() => {
+    return {
+      total: appointmentsInMonth.length,
+      pending: appointmentsInMonth.filter(a => a.estado === "PENDIENTE").length,
+      approved: appointmentsInMonth.filter(a => a.estado === "APROBADA").length,
+      completed: appointmentsInMonth.filter(a => a.estado === "COMPLETADA").length,
+      cancelled: appointmentsInMonth.filter(a => a.estado === "CANCELADA" || a.estado === "RECHAZADA").length,
+    };
+  }, [appointmentsInMonth]);
+
+  return {
+    selectedDate,
+    setSelectedDate,
+    currentFilter,
+    setCurrentFilter,
+    currentView,
+    setCurrentView,
+    appointmentsInMonth,
+    appointmentsInSelectedDay,
+    daysWithAppointments,
+    monthStats,
+  };
+};