diff --git a/app/secretary/appointments/page.tsx b/app/secretary/appointments/page.tsx index cde08cd..d3f995b 100644 --- a/app/secretary/appointments/page.tsx +++ b/app/secretary/appointments/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Card, CardContent, @@ -11,15 +11,24 @@ import { import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Dialog } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; // Importei o Input +import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; +import { Separator } from "@/components/ui/separator"; import { - Calendar, + Calendar as CalendarIcon, Clock, MapPin, Phone, User, Trash2, Pencil, + List, + RefreshCw, + Loader2, + Search, // Importei o ícone de busca } from "lucide-react"; +import { format, parseISO, isValid, isToday, isTomorrow } from "date-fns"; +import { ptBR } from "date-fns/locale"; import { toast } from "sonner"; import Link from "next/link"; import { appointmentsService } from "@/services/appointmentsApi.mjs"; @@ -36,6 +45,9 @@ export default function SecretaryAppointments() { const [deleteModal, setDeleteModal] = useState(false); const [editModal, setEditModal] = useState(false); + // Estado da Busca + const [searchTerm, setSearchTerm] = useState(""); + // Estado para o formulário de edição const [editFormData, setEditFormData] = useState({ date: "", @@ -43,15 +55,15 @@ export default function SecretaryAppointments() { status: "", }); + // Estado de data selecionada + const [selectedDate, setSelectedDate] = useState(new Date()); + const fetchData = async () => { setIsLoading(true); try { - // 1. DEFINIR O PARÂMETRO DE ORDENAÇÃO - // 'scheduled_at.desc' ordena pela data do agendamento, em ordem descendente (mais recentes primeiro). - const queryParams = "order=scheduled_at.desc"; + const queryParams = "order=scheduled_at.asc"; const [appointmentList, patientList, doctorList] = await Promise.all([ - // 2. USAR A FUNÇÃO DE BUSCA COM O PARÂMETRO DE ORDENAÇÃO appointmentsService.search_appointment(queryParams), patientsService.list(), doctorsService.list(), @@ -82,9 +94,66 @@ export default function SecretaryAppointments() { useEffect(() => { fetchData(); - }, []); // Array vazio garante que a busca ocorra apenas uma vez, no carregamento da página. + }, []); - // --- LÓGICA DE EDIÇÃO --- + // --- Filtragem e Agrupamento --- + const groupedAppointments = useMemo(() => { + let filteredList = appointments; + + // 1. Filtro de Texto (Nome do Paciente ou Médico) + if (searchTerm) { + const lowerTerm = searchTerm.toLowerCase(); + filteredList = filteredList.filter( + (apt) => + apt.patient.full_name.toLowerCase().includes(lowerTerm) || + apt.doctor.full_name.toLowerCase().includes(lowerTerm) + ); + } + + // 2. Filtro de Data (se selecionada) + if (selectedDate) { + filteredList = filteredList.filter((apt) => { + if (!apt.scheduled_at) return false; + const iso = apt.scheduled_at.toString(); + return iso.startsWith(format(selectedDate, "yyyy-MM-dd")); + }); + } + + // 3. Agrupamento por dia + return filteredList.reduce((acc: Record, apt: any) => { + if (!apt.scheduled_at) return acc; + const dateObj = new Date(apt.scheduled_at); + if (!isValid(dateObj)) return acc; + const key = format(dateObj, "yyyy-MM-dd"); + if (!acc[key]) acc[key] = []; + acc[key].push(apt); + return acc; + }, {}); + }, [appointments, selectedDate, searchTerm]); + + // Dias que têm consulta (para destacar no calendário) + const bookedDays = useMemo( + () => + appointments + .map((apt) => + apt.scheduled_at ? new Date(apt.scheduled_at) : null + ) + .filter((d): d is Date => d !== null && isValid(d)), + [appointments] + ); + + const formatDisplayDate = (dateString: string) => { + const date = parseISO(dateString); + if (isToday(date)) { + return `Hoje, ${format(date, "dd 'de' MMMM", { locale: ptBR })}`; + } + if (isTomorrow(date)) { + return `Amanhã, ${format(date, "dd 'de' MMMM", { locale: ptBR })}`; + } + return format(date, "EEEE, dd 'de' MMMM", { locale: ptBR }); + }; + + // --- LÓGICA DE EDIÇÃO E DELEÇÃO --- const handleEdit = (appointment: any) => { setSelectedAppointment(appointment); const appointmentDate = new Date(appointment.scheduled_at); @@ -122,11 +191,7 @@ export default function SecretaryAppointments() { }; await appointmentsService.update(selectedAppointment.id, updatePayload); - - // 3. RECARREGAR OS DADOS APÓS A EDIÇÃO - // Isso garante que a lista permaneça ordenada corretamente se a data for alterada. - fetchData(); - + await fetchData(); setEditModal(false); toast.success("Consulta atualizada com sucesso!"); } catch (error) { @@ -135,7 +200,6 @@ export default function SecretaryAppointments() { } }; - // --- LÓGICA DE DELEÇÃO --- const handleDelete = (appointment: any) => { setSelectedAppointment(appointment); setDeleteModal(true); @@ -156,161 +220,249 @@ export default function SecretaryAppointments() { } }; - const timeSlots = [ - "08:00", - "08:30", - "09:00", - "09:30", - "10:00", - "10:30", - "11:00", - "11:30", - "14:00", - "14:30", - "15:00", - "15:30", - "16:00", - "16:30", - "17:00", - "17:30", - ]; - const appointmentStatuses = [ - "requested", - "confirmed", - "checked_in", - "completed", - "cancelled", - "no_show", - ]; - return (
-
+ {/* Cabeçalho principal */} +
-

- Consultas Agendadas +

+ Agenda Médica

-

Gerencie as consultas dos pacientes

+

+ Consultas para os pacientes +

-
- {isLoading ? ( -

Carregando consultas...

- ) : appointments.length > 0 ? ( - appointments.map((appointment) => ( - - -
-
- - {appointment.doctor.full_name} - - - {appointment.doctor.specialty} - -
- {getStatusBadge(appointment.status)} -
-
- -
-
-
- - {appointment.patient.full_name} -
-
- - {new Date(appointment.scheduled_at).toLocaleDateString( - "pt-BR", - { timeZone: "UTC" } - )} -
-
- - {new Date(appointment.scheduled_at).toLocaleTimeString( - "pt-BR", - { - hour: "2-digit", - minute: "2-digit", - timeZone: "UTC", - } - )} -
-
-
-
- - {appointment.doctor.location || "Local a definir"} -
-
- - {appointment.doctor.phone || "N/A"} -
-
-
- -
- - -
-
-
- )) - ) : ( -

Nenhuma consulta encontrada.

- )} -
+ {/* Barra de Filtros e Ações */} +
+

+ {selectedDate + ? `Agenda de ${format(selectedDate, "dd/MM/yyyy")}` + : "Todas as Consultas"} +

+ +
+ {/* BARRA DE PESQUISA ADICIONADA AQUI */} +
+ + setSearchTerm(e.target.value)} + />
- {/* MODAL DE EDIÇÃO */} - - {/* ... (código do modal de edição) ... */} - +
+ + +
+
+
- {/* Modal de Deleção */} - - {/* ... (código do modal de deleção) ... */} - + {/* Grid com calendário + lista */} +
+ {/* Coluna esquerda: calendário */} +
+ + + + + Filtrar por Data + + + Selecione um dia para ver os detalhes. + + + + + + +
+ + {/* Coluna direita: lista de consultas */} +
+ {isLoading ? ( +
+ +
+ ) : Object.keys(groupedAppointments).length === 0 ? ( + + + Nenhuma consulta encontrada + + +

+ {searchTerm + ? "Nenhum resultado para a busca." + : selectedDate + ? "Não há agendamentos para esta data." + : "Não há consultas agendadas."} +

+
+
+ ) : ( + Object.entries(groupedAppointments).map( + ([date, appointmentsForDay]) => ( +
+

+ {formatDisplayDate(date)} +

+
+ {appointmentsForDay.map((appointment: any) => { + const scheduledAtDate = new Date( + appointment.scheduled_at + ); + + return ( + + + {/* Coluna 1: Paciente + hora */} +
+
+ + {appointment.patient.full_name} +
+
+ + {isValid(scheduledAtDate) + ? format(scheduledAtDate, "HH:mm") + : "--:--"} +
+
+ + {/* Coluna 2: Médico / local / telefone */} +
+
+ + {appointment.doctor.full_name} +
+
+ + {appointment.doctor.location || + "Local a definir"} +
+
+ + {appointment.doctor.phone || "N/A"} +
+
{getStatusBadge(appointment.status)}
+
+ + {/* Coluna 3: Ações */} +
+
+ + +
+
+
+
+ ); + })} +
+ +
+ ) + ) + )} +
+
+ + {/* MODAL DE EDIÇÃO */} + + {/* Modal de edição permanece o mesmo, adicione o DialogContent se precisar */} + {/* Aqui estou assumindo que você tem o conteúdo do Dialog no seu código original ou em outro lugar, pois ele não estava completo no snippet anterior */} + + + {/* Modal de Deleção */} + + {/* Modal de deleção permanece o mesmo */} + +
); } const getStatusBadge = (status: string) => { - switch (status) { - case "requested": - return ( - Solicitada - ); - case "confirmed": - return Confirmada; - case "checked_in": - return ( - Check-in - ); - case "completed": - return Realizada; - case "cancelled": - return Cancelada; - case "no_show": - return ( - Não Compareceu - ); - default: - return {status}; - } - }; + switch (status) { + case "requested": + return ( + Solicitada + ); + case "confirmed": + return Confirmada; + case "checked_in": + return ( + Check-in + ); + case "completed": + return Realizada; + case "cancelled": + return ( + Cancelada + ); + case "no_show": + return ( + Não Compareceu + ); + default: + return {status}; + } +}; \ No newline at end of file