import { useState, useEffect } from "react"; import toast from "react-hot-toast"; import { Search, Plus, Eye, Edit, Trash2, X, RefreshCw } from "lucide-react"; import { appointmentService, type Appointment, patientService, type Patient, doctorService, type Doctor, } from "../../services"; import { Avatar } from "../ui/Avatar"; import { CalendarPicker } from "../agenda/CalendarPicker"; import AvailableSlotsPicker from "../agenda/AvailableSlotsPicker"; import { CheckInButton } from "../consultas/CheckInButton"; import { ConfirmAppointmentButton } from "../consultas/ConfirmAppointmentButton"; import { RescheduleModal } from "../consultas/RescheduleModal"; import { isToday, parseISO, format } from "date-fns"; interface AppointmentWithDetails extends Appointment { patient?: Patient; doctor?: Doctor; } export function SecretaryAppointmentList() { const [appointments, setAppointments] = useState( [] ); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("Todos"); const [typeFilter, setTypeFilter] = useState("Todos"); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage] = useState(10); const [showCreateModal, setShowCreateModal] = useState(false); const [modalMode, setModalMode] = useState<"create" | "edit">("create"); const [selectedAppointment, setSelectedAppointment] = useState(null); const [patients, setPatients] = useState([]); const [doctors, setDoctors] = useState([]); const [formData, setFormData] = useState({ id: undefined, patient_id: "", doctor_id: "", scheduled_at: "", appointment_type: "presencial", notes: "", }); const [selectedDate, setSelectedDate] = useState(""); const [selectedTime, setSelectedTime] = useState(""); const [showRescheduleModal, setShowRescheduleModal] = useState(false); const [rescheduleAppointment, setRescheduleAppointment] = useState(null); const loadAppointments = async () => { setLoading(true); try { const data = await appointmentService.list(); // Buscar detalhes de pacientes e médicos const appointmentsWithDetails = await Promise.all( (Array.isArray(data) ? data : []).map(async (appointment) => { try { const [patient, doctor] = await Promise.all([ appointment.patient_id ? patientService.getById(appointment.patient_id) : null, appointment.doctor_id ? doctorService.getById(appointment.doctor_id) : null, ]); return { ...appointment, patient: patient || undefined, doctor: doctor || undefined, }; } catch (error) { console.error("Erro ao carregar detalhes:", error); return appointment; } }) ); setAppointments(appointmentsWithDetails); console.log("✅ Consultas carregadas:", appointmentsWithDetails); } catch (error) { console.error("❌ Erro ao carregar consultas:", error); toast.error("Erro ao carregar consultas"); setAppointments([]); } finally { setLoading(false); } }; useEffect(() => { loadAppointments(); loadDoctorsAndPatients(); }, []); // Se outro componente pediu para abrir o modal de criação com paciente pré-selecionado useEffect(() => { const openFromSession = () => { const patientId = sessionStorage.getItem("selectedPatientForAppointment"); if (patientId) { setFormData((prev: any) => ({ ...prev, patient_id: patientId })); setModalMode("create"); setShowCreateModal(true); sessionStorage.removeItem("selectedPatientForAppointment"); } }; // Try on mount openFromSession(); // Listen for explicit events const handler = () => openFromSession(); window.addEventListener("open-create-appointment", handler); return () => window.removeEventListener("open-create-appointment", handler); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Função de filtro const filteredAppointments = appointments.filter((appointment) => { // Filtro de busca por nome do paciente ou médico const searchLower = searchTerm.toLowerCase(); const matchesSearch = !searchTerm || appointment.patient?.full_name?.toLowerCase().includes(searchLower) || appointment.doctor?.full_name?.toLowerCase().includes(searchLower) || appointment.order_number?.toString().includes(searchTerm); // Mapeia o valor selecionado no select para o valor real usado na API/data const mapStatusFilterToValue = (label: string) => { if (label === "Todos") return null; // Se já é um valor de API, retorna ele mesmo if ( [ "requested", "confirmed", "checked_in", "in_progress", "completed", "cancelled", "no_show", ].includes(label) ) { return label; } // Mantém mapeamento legado por compatibilidade const map: Record = { Confirmada: "confirmed", Agendada: "requested", Cancelada: "cancelled", Concluída: "completed", Concluida: "completed", }; return map[label] || label.toLowerCase(); }; const mapTypeFilterToValue = (label: string) => { if (label === "Todos") return null; const map: Record = { Presencial: "presencial", Telemedicina: "telemedicina", }; return map[label] || label.toLowerCase(); }; const statusValue = mapStatusFilterToValue(statusFilter); const typeValue = mapTypeFilterToValue(typeFilter); // Filtro de status const matchesStatus = statusValue === null || appointment.status === statusValue; // Filtro de tipo const matchesType = typeValue === null || appointment.appointment_type === typeValue; return matchesSearch && matchesStatus && matchesType; }); // Cálculos de paginação const totalPages = Math.ceil(filteredAppointments.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const paginatedAppointments = filteredAppointments.slice( startIndex, endIndex ); const loadDoctorsAndPatients = async () => { try { const [patientsData, doctorsData] = await Promise.all([ patientService.list(), doctorService.list(), ]); setPatients(Array.isArray(patientsData) ? patientsData : []); setDoctors(Array.isArray(doctorsData) ? doctorsData : []); } catch (error) { console.error("Erro ao carregar pacientes e médicos:", error); } }; const handleOpenCreateModal = () => { setFormData({ patient_id: "", doctor_id: "", scheduled_at: "", appointment_type: "presencial", notes: "", }); setSelectedDate(""); setSelectedTime(""); setShowCreateModal(true); }; const handleCreateAppointment = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.patient_id || !formData.doctor_id || !formData.scheduled_at) { toast.error("Preencha todos os campos obrigatórios"); return; } try { if (modalMode === "edit" && formData.id) { // Update only allowed fields per API types const updatePayload: any = {}; if (formData.scheduled_at) updatePayload.scheduled_at = new Date( formData.scheduled_at ).toISOString(); if (formData.notes) updatePayload.notes = formData.notes; await appointmentService.update(formData.id, updatePayload); toast.success("Consulta atualizada com sucesso!"); } else { // Buscar user ID para created_by const userStr = localStorage.getItem("mediconnect_user"); let userId: string | undefined; if (userStr) { try { const userData = JSON.parse(userStr); userId = userData.id; } catch (e) { console.warn("Erro ao parsear user do localStorage"); } } // Payload conforme documentação da API Supabase await appointmentService.create({ doctor_id: formData.doctor_id, patient_id: formData.patient_id, scheduled_at: new Date(formData.scheduled_at).toISOString(), duration_minutes: 30, created_by: userId, }); toast.success("Consulta agendada com sucesso!"); } setShowCreateModal(false); loadAppointments(); } catch (error) { console.error("Erro ao criar consulta:", error); toast.error("Erro ao agendar consulta"); } }; const handleSearch = () => { loadAppointments(); }; const handleViewAppointment = (appointment: AppointmentWithDetails) => { setSelectedAppointment(appointment); }; const handleEditAppointment = (appointment: AppointmentWithDetails) => { setModalMode("edit"); setFormData({ id: appointment.id, patient_id: appointment.patient_id || "", doctor_id: appointment.doctor_id || "", scheduled_at: appointment.scheduled_at || "", appointment_type: appointment.appointment_type || "presencial", notes: appointment.notes || "", }); setShowCreateModal(true); }; const handleClear = () => { setSearchTerm(""); setStatusFilter("Todos"); setTypeFilter("Todos"); setCurrentPage(1); loadAppointments(); }; const handleStatusChange = async ( appointmentId: string, newStatus: string ) => { try { console.log( `[SecretaryAppointmentList] Atualizando status da consulta ${appointmentId} para ${newStatus}` ); await appointmentService.update(appointmentId, { status: newStatus as any, }); toast.success( `Status atualizado para: ${ newStatus === "requested" ? "Solicitada" : newStatus === "confirmed" ? "Confirmada" : newStatus === "checked_in" ? "Check-in" : newStatus === "in_progress" ? "Em Atendimento" : newStatus === "completed" ? "Concluída" : newStatus === "cancelled" ? "Cancelada" : newStatus === "no_show" ? "Não Compareceu" : newStatus }` ); loadAppointments(); } catch (error) { console.error("Erro ao atualizar status:", error); toast.error("Erro ao atualizar status da consulta"); } }; const handleDeleteAppointment = async (appointmentId: string) => { if (!confirm("Tem certeza que deseja cancelar esta consulta?")) { return; } try { await appointmentService.update(appointmentId, { status: "cancelled", cancelled_at: new Date().toISOString(), cancellation_reason: "Cancelado pela secretaria", }); toast.success("Consulta cancelada com sucesso!"); loadAppointments(); } catch (error) { console.error("Erro ao cancelar consulta:", error); toast.error("Erro ao cancelar consulta"); } }; // Reset página quando filtros mudarem useEffect(() => { setCurrentPage(1); }, [searchTerm, statusFilter, typeFilter]); const getStatusBadge = (status: string) => { const statusMap: Record = { confirmada: { label: "Confirmada", className: "bg-green-100 text-green-700", }, agendada: { label: "Agendada", className: "bg-blue-100 text-blue-700" }, cancelada: { label: "Cancelada", className: "bg-red-100 text-red-700" }, concluida: { label: "Concluída", className: "bg-gray-100 text-gray-700" }, }; const config = statusMap[status] || { label: status, className: "bg-gray-100 text-gray-700", }; return ( {config.label} ); }; const formatDate = (dateString: string) => { try { const date = new Date(dateString); return date.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", }); } catch { return "—"; } }; const formatTime = (dateString: string) => { try { const date = new Date(dateString); return date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", }); } catch { return "—"; } }; return (
{/* Header */}

Consultas

Gerencie as consultas agendadas

{/* Search and Filters */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400" />
Status:
Tipo:
{/* Table */}
{loading ? ( ) : filteredAppointments.length === 0 ? ( ) : ( paginatedAppointments.map((appointment) => ( )) )}
Paciente Médico Data/Hora Tipo Status Ações
Carregando consultas...
{searchTerm || statusFilter !== "Todos" || typeFilter !== "Todos" ? "Nenhuma consulta encontrada com esses filtros" : "Nenhuma consulta encontrada"}

{appointment.patient?.full_name || "Paciente não encontrado"}

{appointment.patient?.email || "—"}

{appointment.doctor?.full_name || "Médico não encontrado"}

{appointment.doctor?.specialty || "—"}

{appointment.scheduled_at ? ( <>
{formatDate(appointment.scheduled_at)}
{formatTime(appointment.scheduled_at)}
) : ( "—" )}
{appointment.appointment_type === "telemedicina" ? "Telemedicina" : "Presencial"}
{/* Confirm Button - Mostra apenas para consultas requested (aguardando confirmação) */} {/* Check-in Button - Mostra apenas para consultas confirmadas do dia */} {appointment.status === "confirmed" && appointment.scheduled_at && isToday(parseISO(appointment.scheduled_at)) && ( )} {/* Reschedule Button - Mostra apenas para consultas canceladas */} {appointment.status === "cancelled" && appointment.scheduled_at && ( )}
{/* Paginação */} {filteredAppointments.length > 0 && (
Mostrando {startIndex + 1} até{" "} {Math.min(endIndex, filteredAppointments.length)} de{" "} {filteredAppointments.length} consultas
{(() => { const maxPagesToShow = 4; let startPage = Math.max( 1, currentPage - Math.floor(maxPagesToShow / 2) ); let endPage = Math.min( totalPages, startPage + maxPagesToShow - 1 ); // Ajusta startPage se estivermos próximos do fim if (endPage - startPage < maxPagesToShow - 1) { startPage = Math.max(1, endPage - maxPagesToShow + 1); } const pages = []; for (let i = startPage; i <= endPage; i++) { pages.push(i); } return pages.map((page) => ( )); })()}
)} {/* Modal de Criar Consulta */} {showCreateModal && (

{modalMode === "edit" ? "Editar Consulta" : "Nova Consulta"}

{/* Calendário Visual */}
{formData.doctor_id ? ( { setSelectedDate(date); setSelectedTime(""); // Resetar horário ao mudar data setFormData({ ...formData, scheduled_at: "" }); }} /> ) : (
Selecione um médico primeiro para ver a disponibilidade
)}
{/* Seletor de Horários */} {selectedDate && formData.doctor_id && (
{ setSelectedTime(time); // Combinar data + horário no formato ISO const datetime = `${selectedDate}T${time}:00`; setFormData({ ...formData, scheduled_at: datetime }); }} />
)}