feat: Substitui FullCalendar pelo EventManager conectado à API
This commit is contained in:
Jonas Francisco 2025-10-31 01:09:44 -03:00
parent 10b439056e
commit fbdeb7e462

View File

@ -5,17 +5,9 @@ import { useEffect, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
// --- Imports do FullCalendar (restaurados) --- // --- Imports do EventManager (NOVO) - MANTIDOS ---
import pt_br_locale from "@fullcalendar/core/locales/pt-br";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import { EventInput } from "@fullcalendar/core/index.js";
// --- Imports do EventManager (NOVO) - ADICIONADOS ---
import { EventManager, type Event } from "@/components/event-manager"; import { EventManager, type Event } from "@/components/event-manager";
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback
// Imports mantidos // Imports mantidos
import { Sidebar } from "@/components/dashboard/sidebar"; import { Sidebar } from "@/components/dashboard/sidebar";
@ -24,141 +16,91 @@ import { Button } from "@/components/ui/button";
import { mockWaitingList } from "@/lib/mocks/appointment-mocks"; import { mockWaitingList } from "@/lib/mocks/appointment-mocks";
import "./index.css"; import "./index.css";
import { import {
  DropdownMenu, DropdownMenu,
  DropdownMenuContent, DropdownMenuContent,
  DropdownMenuItem, DropdownMenuItem,
  DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"; // Calendário 3D mantido import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"; // Calendário 3D mantido
const ListaEspera = dynamic( const ListaEspera = dynamic(
  () => import("@/components/agendamento/ListaEspera"), () => import("@/components/agendamento/ListaEspera"),
  { ssr: false } { ssr: false }
); );
export default function AgendamentoPage() { export default function AgendamentoPage() {
  const [appointments, setAppointments] = useState<any[]>([]); const [appointments, setAppointments] = useState<any[]>([]);
  const [waitingList, setWaitingList] = useState(mockWaitingList); const [waitingList, setWaitingList] = useState(mockWaitingList);
  const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar"); const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar");
 
// Estado para alimentar o FullCalendar (restaurado)
const [requestsList, setRequestsList] = useState<EventInput[]>([]);
 
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]); const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
// --- Dados de Exemplo para o NOVO Calendário --- // --- NOVO ESTADO ---
// (Colado do exemplo do 21st.dev) // Estado para alimentar o NOVO EventManager com dados da API
const demoEvents: Event[] = [ const [managerEvents, setManagerEvents] = useState<Event[]>([]);
{
id: uuidv4(),
title: "Team Standup",
description: "Daily sync with the engineering team.",
startTime: new Date(2025, 9, 20, 9, 0, 0), // Mês 9 = Outubro
endTime: new Date(2025, 9, 20, 9, 30, 0),
color: "blue",
},
{
id: uuidv4(),
title: "Code Review",
description: "Review PRs for the new feature.",
startTime: new Date(2025, 9, 21, 14, 0, 0),
endTime: new Date(2025, 9, 21, 15, 0, 0),
color: "green",
},
{
id: uuidv4(),
title: "Client Presentation",
description: "Present the new designs to the client.",
startTime: new Date(2025, 9, 22, 11, 0, 0),
endTime: new Date(2025, 9, 22, 12, 0, 0),
color: "orange",
},
{
id: uuidv4(),
title: "Sprint Planning",
description: "Plan the next sprint tasks.",
startTime: new Date(2025, 9, 23, 10, 0, 0),
endTime: new Date(2025, 9, 23, 11, 30, 0),
color: "purple",
},
{
id: uuidv4(),
title: "Doctor Appointment",
description: "Annual check-up.",
startTime: new Date(2025, 9, 24, 16, 0, 0),
endTime: new Date(2025, 9, 24, 17, 0, 0),
color: "red",
},
{
id: uuidv4(),
title: "Deploy to Production",
description: "Deploy the new release.",
startTime: new Date(2025, 9, 25, 15, 0, 0),
endTime: new Date(2025, 9, 25, 16, 0, 0),
color: "teal",
},
{
id: uuidv4(),
title: "Product Design Review",
description: "Review the new product design mockups.",
startTime: new Date(2025, 9, 20, 13, 0, 0),
endTime: new Date(2025, 9, 20, 14, 30, 0),
color: "pink",
},
{
id: uuidv4(),
title: "Gym Session",
description: "Leg day.",
startTime: new Date(2025, 9, 20, 18, 0, 0),
endTime: new Date(2025, 9, 20, 19, 0, 0),
color: "gray",
},
];
// --- Fim dos Dados de Exemplo ---
  useEffect(() => { useEffect(() => {
    document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
      if (event.key === "c") { if (event.key === "c") {
        setActiveTab("calendar"); setActiveTab("calendar");
      } }
      if (event.key === "f") { if (event.key === "f") {
        setActiveTab("espera"); setActiveTab("espera");
      } }
      if (event.key === "3") { if (event.key === "3") {
        setActiveTab("3d"); setActiveTab("3d");
      } }
    }); });
  }, []); }, []);
  useEffect(() => { useEffect(() => {
// Este useEffect foi mantido, pois ele busca dados para o Calendário 3D let mounted = true;
    let mounted = true; (async () => {
    (async () => { try {
      try { const api = await import('@/lib/api');
        const api = await import('@/lib/api'); const arr = await api.listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []);
        const arr = await api.listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []); if (!mounted) return;
        if (!mounted) return; if (!arr || !arr.length) {
        if (!arr || !arr.length) { setAppointments([]);
          setAppointments([]); setThreeDEvents([]);
          // setRequestsList([]); // Removido setManagerEvents([]); // Limpa o novo calendário
          setThreeDEvents([]); return;
          return; }
        }
        const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean))); 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 patients = (patientIds && patientIds.length) ? await api.buscarPacientesPorIds(patientIds) : [];
        const patientsById: Record<string, any> = {}; const patientsById: Record<string, any> = {};
        (patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; }); (patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });
        setAppointments(arr || []); setAppointments(arr || []);
// --- Mapeamento para o FullCalendar (ANTIGO) - REMOVIDO --- // --- LÓGICA DE TRANSFORMAÇÃO PARA O NOVO EVENTMANAGER ---
        // const events: EventInput[] = (arr || []).map((obj: any) => { 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();
        // setRequestsList(events || []); const duration = Number(obj.duration_minutes ?? obj.duration ?? 30) || 30;
const end = new Date(start.getTime() + duration * 60 * 1000);
// Convert to 3D calendar events 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();
let color = "gray"; // Cor padrão
if (obj.status === 'confirmed') color = 'green';
if (obj.status === 'pending') color = 'orange';
return {
id: obj.id || uuidv4(), // Usa ID da API ou gera um
title: title,
description: `Agendamento para ${patient}. Status: ${obj.status || 'N/A'}.`,
startTime: start,
endTime: end,
color: color,
};
});
setManagerEvents(newManagerEvents);
// --- FIM DA LÓGICA ---
// Convert to 3D calendar events (MANTIDO 100%)
const threeDEvents: CalendarEvent[] = (arr || []).map((obj: any) => { const threeDEvents: CalendarEvent[] = (arr || []).map((obj: any) => {
const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null; const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null;
const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente'; const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente';
@ -177,123 +119,109 @@ export default function AgendamentoPage() {
} catch (err) { } catch (err) {
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err); console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
setAppointments([]); setAppointments([]);
setRequestsList([]);
setThreeDEvents([]); setThreeDEvents([]);
setManagerEvents([]); // Limpa o novo calendário
} }
})(); })();
return () => { mounted = false; }; return () => { mounted = false; };
}, []); }, []);
  // Handlers mantidos // Handlers mantidos
  const handleSaveAppointment = (appointment: any) => { const handleSaveAppointment = (appointment: any) => {
    if (appointment.id) { if (appointment.id) {
      setAppointments((prev) => setAppointments((prev) =>
        prev.map((a) => (a.id === appointment.id ? appointment : a)) prev.map((a) => (a.id === appointment.id ? appointment : a))
      ); );
    } else { } else {
      const newAppointment = { const newAppointment = {
        ...appointment, ...appointment,
        id: Date.now().toString(), id: Date.now().toString(),
      }; };
      setAppointments((prev) => [...prev, newAppointment]); setAppointments((prev) => [...prev, newAppointment]);
    } }
  }; };
  const handleNotifyPatient = (patientId: string) => { const handleNotifyPatient = (patientId: string) => {
    console.log(`Notificando paciente ${patientId}`); console.log(`Notificando paciente ${patientId}`);
  }; };
  const handleAddEvent = (event: CalendarEvent) => { const handleAddEvent = (event: CalendarEvent) => {
    setThreeDEvents((prev) => [...prev, event]); setThreeDEvents((prev) => [...prev, event]);
  }; };
  const handleRemoveEvent = (id: string) => { const handleRemoveEvent = (id: string) => {
    setThreeDEvents((prev) => prev.filter((e) => e.id !== id)); setThreeDEvents((prev) => prev.filter((e) => e.id !== id));
  }; };
  return ( return (
    <div className="flex flex-row bg-background"> <div className="flex flex-row bg-background">
      <div className="flex w-full flex-col"> <div className="flex w-full flex-col">
        <div className="flex w-full flex-col gap-10 p-6"> <div className="flex w-full flex-col gap-10 p-6">
          <div className="flex flex-row justify-between items-center"> <div className="flex flex-row justify-between items-center">
{/* Todo o cabeçalho foi mantido */} {/* Todo o cabeçalho foi mantido */}
            <div> <div>
              <h1 className="text-2xl font-bold text-foreground"> <h1 className="text-2xl font-bold text-foreground">
                {activeTab === "calendar" ? "Calendário" : activeTab === "3d" ? "Calendário 3D" : "Lista de Espera"} {activeTab === "calendar" ? "Calendário" : activeTab === "3d" ? "Calendário 3D" : "Lista de Espera"}
              </h1> </h1>
              <p className="text-muted-foreground"> <p className="text-muted-foreground">
                Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3). Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).
              </p> </p>
            </div> </div>
            <div className="flex space-x-2"> <div className="flex space-x-2">
              <DropdownMenu> <DropdownMenu>
                <DropdownMenuTrigger className="bg-primary hover:bg-primary/90 px-5 py-1 text-primary-foreground rounded-sm"> <DropdownMenuTrigger className="bg-primary hover:bg-primary/90 px-5 py-1 text-primary-foreground rounded-sm">
                  Opções &#187; Opções &#187;
                </DropdownMenuTrigger> </DropdownMenuTrigger>
                <DropdownMenuContent> <DropdownMenuContent>
                  <Link href={"/agenda"}> <Link href={"/agenda"}>
                    <DropdownMenuItem>Agendamento</DropdownMenuItem> <DropdownMenuItem>Agendamento</DropdownMenuItem>
                  </Link> </Link>
                  <Link href={"/procedimento"}> <Link href={"/procedimento"}>
                    <DropdownMenuItem>Procedimento</DropdownMenuItem> <DropdownMenuItem>Procedimento</DropdownMenuItem>
                  </Link> </Link>
                  <Link href={"/financeiro"}> <Link href={"/financeiro"}>
                    <DropdownMenuItem>Financeiro</DropdownMenuItem> <DropdownMenuItem>Financeiro</DropdownMenuItem>
                  </Link> </Link>
                </DropdownMenuContent> </DropdownMenuContent>
              </DropdownMenu> </DropdownMenu>
              <div className="flex flex-row"> <div className="flex flex-row">
                <Button <Button
                  variant={"outline"} variant={"outline"}
                  className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-l-[100px] rounded-r-[0px]" className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-l-[100px] rounded-r-[0px]"
                  onClick={() => setActiveTab("calendar")} onClick={() => setActiveTab("calendar")}
                > >
                  Calendário Calendário
                </Button> </Button>
                <Button <Button
                  variant={"outline"} variant={"outline"}
                  className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-none" className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-none"
                  onClick={() => setActiveTab("3d")} onClick={() => setActiveTab("3d")}
                > >
                  3D 3D
                </Button> </Button>
                <Button <Button
                  variant={"outline"} variant={"outline"}
                  className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-r-[100px] rounded-l-[0px]" className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-r-[100px] rounded-l-[0px]"
                  onClick={() => setActiveTab("espera")} onClick={() => setActiveTab("espera")}
                > >
                  Lista de espera Lista de espera
                </Button> </Button>
              </div> </div>
            </div> </div>
          </div> </div>
{/* --- AQUI ESTÁ A SUBSTITUIÇÃO --- */}
{activeTab === "calendar" ? ( {activeTab === "calendar" ? (
<div className="flex w-full"> <div className="flex w-full">
<FullCalendar {/* O FullCalendar foi substituído pelo EventManager,
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} agora alimentado pelo estado dinâmico 'managerEvents' */}
initialView="dayGridMonth" <EventManager events={managerEvents} />
locale={pt_br_locale}
timeZone={"America/Sao_Paulo"}
events={requestsList}
headerToolbar={{
left: "prev,next today",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
dateClick={(info) => {
info.view.calendar.changeView("timeGridDay", info.dateStr);
}}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
dayMaxEventRows={3}
/>
</div> </div>
) : activeTab === "3d" ? ( ) : activeTab === "3d" ? (
// O calendário 3D (ThreeDWallCalendar) foi MANTIDO 100%
<div className="flex w-full justify-center"> <div className="flex w-full justify-center">
<ThreeDWallCalendar <ThreeDWallCalendar
events={threeDEvents} events={threeDEvents}
@ -302,6 +230,7 @@ export default function AgendamentoPage() {
/> />
</div> </div>
) : ( ) : (
// A Lista de Espera foi MANTIDA
<ListaEspera <ListaEspera
patients={waitingList} patients={waitingList}
onNotify={handleNotifyPatient} onNotify={handleNotifyPatient}