"use client"; // Imports mantidos import { useEffect, useState } from "react"; // --- Imports do EventManager (NOVO) - MANTIDOS --- import { EventManager, type Event } from "@/components/features/general/event-manager"; import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback // Imports mantidos import "./index.css"; export default function AgendamentoPage() { const [appointments, setAppointments] = useState([]); // REMOVIDO: abas e 3D → não há mais alternância de abas // const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar"); // REMOVIDO: estados do 3D e formulário do paciente (eram usados pelo 3D) // const [threeDEvents, setThreeDEvents] = useState([]); // const [showPatientForm, setShowPatientForm] = useState(false); // --- NOVO ESTADO --- // Estado para alimentar o NOVO EventManager com dados da API const [managerEvents, setManagerEvents] = useState([]); const [managerLoading, setManagerLoading] = useState(true); // Padroniza idioma da página para pt-BR (afeta componentes que usam o lang do documento) useEffect(() => { try { // Atributos no document.documentElement.lang = "pt-BR"; document.documentElement.setAttribute("xml:lang", "pt-BR"); document.documentElement.setAttribute("data-lang", "pt-BR"); // Cookie de locale (usado por apps com i18n) const oneYear = 60 * 60 * 24 * 365; document.cookie = `NEXT_LOCALE=pt-BR; Path=/; Max-Age=${oneYear}; SameSite=Lax`; } catch { // ignore } }, []); useEffect(() => { let mounted = true; (async () => { try { setManagerLoading(true); const api = await import('@/lib/api'); const arr = await api.listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []); if (!mounted) return; if (!arr || !arr.length) { setAppointments([]); // REMOVIDO: setThreeDEvents([]) setManagerEvents([]); setManagerLoading(false); return; } const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean))); const patients = (patientIds && patientIds.length) ? await api.buscarPacientesPorIds(patientIds) : []; const patientsById: Record = {}; (patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; }); // Tentar enriquecer com médicos/profissionais quando houver doctor_id const doctorIds = Array.from(new Set(arr.map((a: any) => a.doctor_id).filter(Boolean))); const doctors = (doctorIds && doctorIds.length) ? await api.buscarMedicosPorIds(doctorIds) : []; const doctorsById: Record = {}; (doctors || []).forEach((d: any) => { if (d && d.id) doctorsById[String(d.id)] = d; }); setAppointments(arr || []); // --- LÓGICA DE TRANSFORMAÇÃO PARA O NOVO EVENTMANAGER --- const newManagerEvents: Event[] = (arr || []).map((obj: any) => { const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null; const start = scheduled ? new Date(scheduled) : new Date(); const duration = Number(obj.duration_minutes ?? obj.duration ?? 30) || 30; const end = new Date(start.getTime() + duration * 60 * 1000); const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente'; const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim(); // Mapeamento de cores padronizado const status = String(obj.status || "").toLowerCase(); let color: Event["color"] = "blue"; if (status === "confirmed" || status === "confirmado") color = "green"; else if (status === "pending" || status === "pendente") color = "orange"; else if (status === "canceled" || status === "cancelado" || status === "cancelled") color = "red"; else if (status === "requested" || status === "solicitado") color = "blue"; const professional = (doctorsById[String(obj.doctor_id)]?.full_name) || obj.doctor_name || obj.professional_name || obj.professional || obj.executante || 'Profissional'; const appointmentType = obj.appointment_type || obj.type || obj.appointmentType || ''; const insurance = obj.insurance_provider || obj.insurance || obj.convenio || obj.insuranceProvider || null; const completedAt = obj.completed_at || obj.completedAt || null; const cancelledAt = obj.cancelled_at || obj.cancelledAt || null; const cancellationReason = obj.cancellation_reason || obj.cancellationReason || obj.cancel_reason || null; return { id: obj.id || uuidv4(), title, description: `Agendamento para ${patient}. Status: ${obj.status || 'N/A'}.`, startTime: start, endTime: end, color, // Campos adicionais para visualização detalhada patientName: patient, professionalName: professional, appointmentType, status: obj.status || null, insuranceProvider: insurance, completedAt, cancelledAt, cancellationReason, }; }); setManagerEvents(newManagerEvents); setManagerLoading(false); // --- FIM DA LÓGICA --- // REMOVIDO: conversão para 3D e setThreeDEvents } catch (err) { console.warn('[AgendamentoPage] falha ao carregar agendamentos', err); setAppointments([]); // REMOVIDO: setThreeDEvents([]) setManagerEvents([]); setManagerLoading(false); } })(); return () => { mounted = false; }; }, []); // Handlers mantidos const handleSaveAppointment = (appointment: any) => { if (appointment.id) { setAppointments((prev) => prev.map((a) => (a.id === appointment.id ? appointment : a)) ); } else { const newAppointment = { ...appointment, id: Date.now().toString(), }; setAppointments((prev) => [...prev, newAppointment]); } }; // Mapeia cor do calendário -> status da API const statusFromColor = (color?: string) => { switch ((color || "").toLowerCase()) { case "green": return "confirmed"; case "orange": return "pending"; case "red": return "canceled"; default: return "requested"; } }; // Componente auxiliar: legenda dinâmica que lista as cores/statuss presentes nos agendamentos function DynamicLegend({ events }: { events: Event[] }) { // Mapa de classes para cores conhecidas const colorClassMap: Record = { blue: "bg-blue-500 ring-blue-500/20", green: "bg-green-500 ring-green-500/20", orange: "bg-orange-500 ring-orange-500/20", red: "bg-red-500 ring-red-500/20", purple: "bg-purple-500 ring-purple-500/20", pink: "bg-pink-500 ring-pink-500/20", teal: "bg-teal-400 ring-teal-400/20", } const hashToColor = (s: string) => { // gera cor hex simples a partir de hash da string let h = 0 for (let i = 0; i < s.length; i++) h = (h << 5) - h + s.charCodeAt(i) const c = (h & 0x00ffffff).toString(16).toUpperCase() return "#" + "00000".substring(0, 6 - c.length) + c } // Agrupa por cor e coleta os status associados const entries = new Map>() for (const ev of events) { const col = (ev.color || "blue").toString() const st = (ev.status || statusFromColor(ev.color) || "").toString().toLowerCase() if (!entries.has(col)) entries.set(col, new Set()) if (st) entries.get(col)!.add(st) } // Painel principal: sempre exibe os 3 status primários (Solicitado, Confirmado, Cancelado) const statusDisplay = (s: string) => { switch (s) { case "requested": case "request": case "solicitado": return "Solicitado" case "confirmed": case "confirmado": return "Confirmado" case "canceled": case "cancelled": case "cancelado": return "Cancelado" case "pending": case "pendente": return "Pendente" case "governo": case "government": return "Governo" default: return s.charAt(0).toUpperCase() + s.slice(1) } } // Ordem preferencial para exibição (tenta manter Solicitação/Confirmado/Cancelado em primeiro) const priorityList = [ 'solicitado','requested', 'confirmed','confirmado', 'pending','pendente', 'canceled','cancelled','cancelado', 'governo','government' ] const items = Array.from(entries.entries()).map(([col, statuses]) => { const statusArr = Array.from(statuses) let priority = 999 for (const s of statusArr) { const idx = priorityList.indexOf(s) if (idx >= 0) priority = Math.min(priority, idx) } // if none matched, leave priority high so they appear after known statuses return { col, statuses: statusArr, priority } }) items.sort((a, b) => a.priority - b.priority || a.col.localeCompare(b.col)) // Separar itens extras (fora os três principais) para renderizar depois const primaryColors = new Set(['blue', 'green', 'red']) const extras = items.filter(i => !primaryColors.has(i.col.toLowerCase())) return (
{/* Bloco grande com os três status principais sempre visíveis e responsivos */}
Solicitado
Confirmado
Cancelado
{/* Itens extras detectados dinamicamente (menores) */} {extras.length > 0 && (
{extras.map(({ col, statuses }) => { const statusList = statuses.map(statusDisplay).filter(Boolean).join(', ') const cls = colorClassMap[col.toLowerCase()] return (
{cls ? ( ) : ( )} {statusList || col}
) })}
)}
) } // Envia atualização para a API e atualiza UI const handleEventUpdate = async (id: string, partial: Partial) => { try { const payload: any = {}; if (partial.startTime) payload.scheduled_at = partial.startTime.toISOString(); if (partial.startTime && partial.endTime) { const minutes = Math.max(1, Math.round((partial.endTime.getTime() - partial.startTime.getTime()) / 60000)); payload.duration_minutes = minutes; } if (partial.color) payload.status = statusFromColor(partial.color); if (typeof partial.description === "string") payload.notes = partial.description; if (Object.keys(payload).length) { const api = await import('@/lib/api'); await api.atualizarAgendamento(id, payload); } // Otimista: reflete mudanças locais setManagerEvents((prev) => prev.map((e) => (e.id === id ? { ...e, ...partial } : e))); } catch (e) { console.warn("[Calendário] Falha ao atualizar agendamento na API:", e); } }; return (

Calendário

Navegue através do atalho: Calendário (C).

{/* legenda dinâmica: mostra as cores presentes nos agendamentos do dia atual */}
{managerLoading ? (
Conectando ao calendário — carregando agendamentos...
) : (
)}
); }