"use client"; import Link from "next/link"; import { useEffect, useState, useCallback, useMemo } from "react"; import { MoreHorizontal, PlusCircle, Search, Eye, Edit, Trash2, ArrowLeft, Loader2, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { mockProfessionals } from "@/lib/mocks/appointment-mocks"; import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds, atualizarAgendamento, buscarAgendamentoPorId, deletarAgendamento } from "@/lib/api"; import { CalendarRegistrationForm } from "@/components/features/forms/calendar-registration-form"; const formatDate = (date: string | Date) => { if (!date) return ""; return new Date(date).toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); }; const capitalize = (s: string) => { if (typeof s !== "string" || s.length === 0) return ""; return s.charAt(0).toUpperCase() + s.slice(1); }; export default function ConsultasPage() { const [appointments, setAppointments] = useState([]); const [originalAppointments, setOriginalAppointments] = useState([]); const [searchValue, setSearchValue] = useState(''); const [selectedStatus, setSelectedStatus] = useState('all'); const [filterDate, setFilterDate] = useState(''); const [isLoading, setIsLoading] = useState(true); const [showForm, setShowForm] = useState(false); const [editingAppointment, setEditingAppointment] = useState(null); const [viewingAppointment, setViewingAppointment] = useState(null); // Local form state used when editing. Keep hook at top-level to avoid Hooks order changes. const [localForm, setLocalForm] = useState(null); // Paginação const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const mapAppointmentToFormData = (appointment: any) => { // prefer scheduled_at (ISO) if available const scheduledBase = appointment.scheduled_at || appointment.time || appointment.created_at || null; const baseDate = scheduledBase ? new Date(scheduledBase) : new Date(); const duration = appointment.duration_minutes ?? appointment.duration ?? 30; // compute start and end times (HH:MM) const appointmentDateStr = baseDate.toISOString().split("T")[0]; const startTime = `${String(baseDate.getHours()).padStart(2, '0')}:${String(baseDate.getMinutes()).padStart(2, '0')}`; const endDate = new Date(baseDate.getTime() + duration * 60000); const endTime = `${String(endDate.getHours()).padStart(2, '0')}:${String(endDate.getMinutes()).padStart(2, '0')}`; return { id: appointment.id, patientName: appointment.patient, patientId: appointment.patient_id || appointment.patientId || null, // include doctor id so the form can run availability/exception checks when editing doctorId: appointment.doctor_id || appointment.doctorId || null, professionalName: appointment.professional || "", appointmentDate: appointmentDateStr, startTime, endTime, status: appointment.status, appointmentType: appointment.appointment_type || appointment.type, notes: appointment.notes || appointment.patient_notes || "", cpf: "", rg: "", birthDate: "", phoneCode: "+55", phoneNumber: "", email: "", unit: "nei", // API-editable fields (populate so the form shows existing values) duration_minutes: duration, chief_complaint: appointment.chief_complaint ?? null, patient_notes: appointment.patient_notes ?? null, insurance_provider: appointment.insurance_provider ?? null, checked_in_at: appointment.checked_in_at ?? null, completed_at: appointment.completed_at ?? null, cancelled_at: appointment.cancelled_at ?? null, cancellation_reason: appointment.cancellation_reason ?? appointment.cancellationReason ?? "", }; }; const handleDelete = async (appointmentId: string) => { if (!window.confirm("Tem certeza que deseja excluir esta consulta?")) return; try { // call server DELETE await deletarAgendamento(appointmentId); // remove from UI setAppointments((prev) => prev.filter((a) => a.id !== appointmentId)); // also update originalAppointments cache setOriginalAppointments((prev) => (prev || []).filter((a) => a.id !== appointmentId)); alert('Agendamento excluído com sucesso.'); } catch (err) { console.error('[ConsultasPage] Falha ao excluir agendamento', err); try { const msg = err instanceof Error ? err.message : String(err); alert('Falha ao excluir agendamento: ' + msg); } catch (e) { // ignore } } }; const handleEdit = (appointment: any) => { const formData = mapAppointmentToFormData(appointment); setEditingAppointment(formData); setShowForm(true); }; const handleView = (appointment: any) => { setViewingAppointment(appointment); }; const handleCancel = () => { setEditingAppointment(null); setShowForm(false); setLocalForm(null); }; const handleSave = async (formData: any) => { try { // build scheduled_at ISO (formData.startTime is 'HH:MM') const scheduled_at = new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(); // compute duration from start/end times when available let duration_minutes = 30; try { if (formData.startTime && formData.endTime) { const [sh, sm] = String(formData.startTime).split(":").map(Number); const [eh, em] = String(formData.endTime).split(":").map(Number); const start = (sh || 0) * 60 + (sm || 0); const end = (eh || 0) * 60 + (em || 0); if (!Number.isNaN(start) && !Number.isNaN(end) && end > start) duration_minutes = end - start; } } catch (e) { // fallback to default duration_minutes = 30; } const payload: any = { scheduled_at, duration_minutes, status: formData.status || undefined, notes: formData.notes ?? null, chief_complaint: formData.chief_complaint ?? null, patient_notes: formData.patient_notes ?? null, insurance_provider: formData.insurance_provider ?? null, // convert local datetime-local inputs (which may be in 'YYYY-MM-DDTHH:MM' format) to proper ISO if present checked_in_at: formData.checked_in_at ? new Date(formData.checked_in_at).toISOString() : null, completed_at: formData.completed_at ? new Date(formData.completed_at).toISOString() : null, cancelled_at: formData.cancelled_at ? new Date(formData.cancelled_at).toISOString() : null, cancellation_reason: formData.cancellation_reason ?? null, }; // Call PATCH endpoint const updated = await atualizarAgendamento(formData.id, payload); // Build UI-friendly row using server response and existing local fields const existing = appointments.find((a) => a.id === formData.id) || {}; const mapped = { id: updated.id, patient: formData.patientName || existing.patient || '', patient_id: existing.patient_id ?? null, // preserve doctor id so future edits retain the selected professional doctor_id: existing.doctor_id ?? (formData.doctorId || (formData as any).doctor_id) ?? null, // preserve server-side fields so future edits read them scheduled_at: updated.scheduled_at ?? scheduled_at, duration_minutes: updated.duration_minutes ?? duration_minutes, appointment_type: updated.appointment_type ?? formData.appointmentType ?? existing.type ?? 'presencial', status: updated.status ?? formData.status ?? existing.status, professional: existing.professional || formData.professionalName || '', notes: updated.notes ?? updated.patient_notes ?? formData.notes ?? existing.notes ?? '', chief_complaint: updated.chief_complaint ?? formData.chief_complaint ?? existing.chief_complaint ?? null, patient_notes: updated.patient_notes ?? formData.patient_notes ?? existing.patient_notes ?? null, insurance_provider: updated.insurance_provider ?? formData.insurance_provider ?? existing.insurance_provider ?? null, checked_in_at: updated.checked_in_at ?? formData.checked_in_at ?? existing.checked_in_at ?? null, completed_at: updated.completed_at ?? formData.completed_at ?? existing.completed_at ?? null, cancelled_at: updated.cancelled_at ?? formData.cancelled_at ?? existing.cancelled_at ?? null, cancellation_reason: updated.cancellation_reason ?? formData.cancellation_reason ?? existing.cancellation_reason ?? null, }; setAppointments((prev) => prev.map((a) => (a.id === mapped.id ? mapped : a))); handleCancel(); } catch (err) { console.error('[ConsultasPage] Falha ao atualizar agendamento', err); // Inform the user try { const msg = err instanceof Error ? err.message : String(err); alert('Falha ao salvar alterações: ' + msg); } catch (e) { // ignore } } }; // Fetch and map appointments (used at load and when clearing search) const fetchAndMapAppointments = async () => { const arr = await listarAgendamentos("select=*&order=scheduled_at.desc&limit=200"); // Collect unique patient_ids and doctor_ids const patientIds = new Set(); const doctorIds = new Set(); for (const a of arr || []) { if (a.patient_id) patientIds.add(String(a.patient_id)); if (a.doctor_id) doctorIds.add(String(a.doctor_id)); } // Batch fetch patients and doctors const patientsMap = new Map(); const doctorsMap = new Map(); try { if (patientIds.size) { const list = await buscarPacientesPorIds(Array.from(patientIds)); for (const p of list || []) patientsMap.set(String(p.id), p); } } catch (e) { console.warn("[ConsultasPage] Falha ao buscar pacientes em lote", e); } try { if (doctorIds.size) { const list = await buscarMedicosPorIds(Array.from(doctorIds)); for (const d of list || []) doctorsMap.set(String(d.id), d); } } catch (e) { console.warn("[ConsultasPage] Falha ao buscar médicos em lote", e); } // Map appointments using the maps const mapped = (arr || []).map((a: any) => { const patient = a.patient_id ? patientsMap.get(String(a.patient_id))?.full_name || String(a.patient_id) : ""; const professional = a.doctor_id ? doctorsMap.get(String(a.doctor_id))?.full_name || String(a.doctor_id) : ""; return { id: a.id, patient, patient_id: a.patient_id, // preserve the doctor's id so later edit flows can access it doctor_id: a.doctor_id ?? null, // keep some server-side fields so edit can access them later scheduled_at: a.scheduled_at ?? a.time ?? a.created_at ?? null, duration_minutes: a.duration_minutes ?? a.duration ?? null, appointment_type: a.appointment_type ?? a.type ?? null, status: a.status ?? "requested", professional, notes: a.notes || a.patient_notes || "", // additional editable fields chief_complaint: a.chief_complaint ?? null, patient_notes: a.patient_notes ?? null, insurance_provider: a.insurance_provider ?? null, checked_in_at: a.checked_in_at ?? null, completed_at: a.completed_at ?? null, cancelled_at: a.cancelled_at ?? null, cancellation_reason: a.cancellation_reason ?? a.cancellationReason ?? null, }; }); return mapped; }; useEffect(() => { let mounted = true; (async () => { try { const mapped = await fetchAndMapAppointments(); if (!mounted) return; setAppointments(mapped); setOriginalAppointments(mapped || []); setIsLoading(false); } catch (err) { console.warn("[ConsultasPage] Falha ao carregar agendamentos, usando mocks", err); if (!mounted) return; setAppointments([]); setIsLoading(false); } })(); return () => { mounted = false; }; }, []); // Search box: allow fetching a single appointment by ID when pressing Enter // Perform a local-only search against the already-loaded appointments. // This intentionally does not call the server — it filters the cached list. const applyFilters = (val?: string) => { const trimmed = String((val ?? searchValue) || '').trim(); let list = (originalAppointments || []).slice(); // search if (trimmed) { const q = trimmed.toLowerCase(); list = list.filter((a) => { const patient = String(a.patient || '').toLowerCase(); const professional = String(a.professional || '').toLowerCase(); const pid = String(a.patient_id || '').toLowerCase(); const aid = String(a.id || '').toLowerCase(); return ( patient.includes(q) || professional.includes(q) || pid === q || aid === q ); }); } // status filter if (selectedStatus && selectedStatus !== 'all') { list = list.filter((a) => String(a.status || '').toLowerCase() === String(selectedStatus).toLowerCase()); } // date filter (YYYY-MM-DD) if (filterDate) { list = list.filter((a) => { try { const sched = a.scheduled_at || a.time || a.created_at || null; if (!sched) return false; const iso = new Date(sched).toISOString().split('T')[0]; return iso === filterDate; } catch (e) { return false; } }); } setAppointments(list as any[]); }; const performSearch = (val: string) => { applyFilters(val); }; const handleSearchKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); // keep behavior consistent: perform a local filter immediately performSearch(searchValue); } else if (e.key === 'Escape') { setSearchValue(''); setAppointments(originalAppointments || []); } }; const handleClearSearch = async () => { setSearchValue(''); setIsLoading(true); try { // Reset to the original cached list without refetching from server setAppointments(originalAppointments || []); } catch (err) { setAppointments([]); } finally { setIsLoading(false); } }; // Debounce live filtering as the user types. Operates only on the cached originalAppointments. useEffect(() => { const t = setTimeout(() => { performSearch(searchValue); }, 250); return () => clearTimeout(t); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchValue, originalAppointments]); useEffect(() => { applyFilters(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedStatus, filterDate, originalAppointments]); // Dados paginados const paginatedAppointments = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; return appointments.slice(startIndex, endIndex); }, [appointments, currentPage, itemsPerPage]); const totalPages = Math.ceil(appointments.length / itemsPerPage); // Reset para página 1 quando mudar a busca ou itens por página useEffect(() => { setCurrentPage(1); }, [searchValue, selectedStatus, filterDate, itemsPerPage]); // Keep localForm synchronized with editingAppointment useEffect(() => { if (showForm && editingAppointment) { setLocalForm(editingAppointment); } if (!showForm) setLocalForm(null); }, [showForm, editingAppointment]); const onFormChange = (d: any) => setLocalForm(d); const saveLocal = async () => { if (!localForm) return; await handleSave(localForm); }; // If editing, render the edit form as a focused view (keeps hooks stable) if (showForm && localForm) { return (

Editar Consulta

); } return (
{/* Header responsivo */}

Consultas

Gerencie todas as consultas da clínica

{/* Filtros e busca responsivos */}
{/* Linha 1: Busca */}
setSearchValue(e.target.value)} onKeyDown={handleSearchKeyDown} />
{/* Linha 2: Selects responsivos */}
setFilterDate(e.target.value)} />
{/* Loading state */} {isLoading ? (
Carregando agendamentos...
) : ( <> {/* Desktop Table - Hidden on mobile */}
Paciente Médico Status Data e Hora Ações {paginatedAppointments.map((appointment) => { const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional); const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup ? appointment.professional : (professionalLookup ? professionalLookup.name : (appointment.professional || "Não encontrado")); return ( {appointment.patient} {professionalName} {capitalize(appointment.status)} {formatDate(appointment.scheduled_at ?? appointment.time)} handleView(appointment)}> Ver handleEdit(appointment)}> Editar handleDelete(appointment.id)} className="text-destructive"> Excluir ); })}
{/* Mobile Cards - Hidden on desktop */}
{paginatedAppointments.length > 0 ? ( paginatedAppointments.map((appointment) => { const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional); const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup ? appointment.professional : (professionalLookup ? professionalLookup.name : (appointment.professional || "Não encontrado")); return (
Paciente
{appointment.patient}
handleView(appointment)}> Ver handleEdit(appointment)}> Editar handleDelete(appointment.id)} className="text-destructive"> Excluir
Médico
{professionalName}
Status
{capitalize(appointment.status)}
Data e Hora
{formatDate(appointment.scheduled_at ?? appointment.time)}
); }) ) : (
Nenhuma consulta encontrada
)}
)} {/* Controles de paginação - Responsivos */}
Itens por página: Mostrando {paginatedAppointments.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "} {Math.min(currentPage * itemsPerPage, appointments.length)} de {appointments.length}
Pág {currentPage} de {totalPages || 1}
{viewingAppointment && ( setViewingAppointment(null)}> Detalhes da Consulta Informações detalhadas da consulta de {viewingAppointment?.patient}.
{viewingAppointment?.patient}
{viewingAppointment?.professional || 'Não encontrado'}
{(viewingAppointment?.scheduled_at ?? viewingAppointment?.time) ? formatDate(viewingAppointment?.scheduled_at ?? viewingAppointment?.time) : ''}
{capitalize(viewingAppointment?.status || "")}
{capitalize(viewingAppointment?.type || "")}
{viewingAppointment?.notes || "Nenhuma"}
)}
); }