feat: Substitui calendário antigo pelo novo EventManager

This commit is contained in:
Jonas Francisco 2025-10-31 00:27:48 -03:00
parent 5b3faab1bd
commit 44ddc4d03a
4 changed files with 3265 additions and 214 deletions

View File

@ -1,246 +1,297 @@
"use client"; "use client";
// Imports mantidos
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import pt_br_locale from "@fullcalendar/core/locales/pt-br"; import Link from "next/link";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid"; // --- Imports do FullCalendar (ANTIGO) - REMOVIDOS ---
import interactionPlugin from "@fullcalendar/interaction"; // import pt_br_locale from "@fullcalendar/core/locales/pt-br";
import timeGridPlugin from "@fullcalendar/timegrid"; // import FullCalendar from "@fullcalendar/react";
import { EventInput } from "@fullcalendar/core/index.js"; // 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 { v4 as uuidv4 } from 'uuid';
// Imports mantidos
import { Sidebar } from "@/components/dashboard/sidebar"; import { Sidebar } from "@/components/dashboard/sidebar";
import { PagesHeader } from "@/components/dashboard/header"; import { PagesHeader } from "@/components/dashboard/header";
import { Button } from "@/components/ui/button"; 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 Link from "next/link";
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"; 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");
const [requestsList, setRequestsList] = useState<EventInput[]>();  
// O 'requestsList' do FullCalendar foi removido.
// const [requestsList, setRequestsList] = useState<EventInput[]>();
 
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]); const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
useEffect(() => { // --- Dados de Exemplo para o NOVO Calendário ---
document.addEventListener("keydown", (event) => { // (Colado do exemplo do 21st.dev)
if (event.key === "c") { const demoEvents: Event[] = [
setActiveTab("calendar"); {
} id: uuidv4(),
if (event.key === "f") { title: "Team Standup",
setActiveTab("espera"); description: "Daily sync with the engineering team.",
} startTime: new Date(2025, 9, 20, 9, 0, 0), // Mês 9 = Outubro
if (event.key === "3") { endTime: new Date(2025, 9, 20, 9, 30, 0),
setActiveTab("3d"); 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(() => {
// Fetch real appointments and map to calendar events     document.addEventListener("keydown", (event) => {
let mounted = true;       if (event.key === "c") {
(async () => {         setActiveTab("calendar");
try {       }
// listarAgendamentos accepts a query string; request a reasonable limit and order       if (event.key === "f") {
const api = await import('@/lib/api');         setActiveTab("espera");
const arr = await api.listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []);       }
if (!mounted) return;       if (event.key === "3") {
if (!arr || !arr.length) {         setActiveTab("3d");
setAppointments([]);       }
setRequestsList([]);     });
setThreeDEvents([]);   }, []);
return;
}
// Batch-fetch patient names for display   useEffect(() => {
const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean))); // Este useEffect foi mantido, pois ele busca dados para o Calendário 3D
const patients = (patientIds && patientIds.length) ? await api.buscarPacientesPorIds(patientIds) : [];     let mounted = true;
const patientsById: Record<string, any> = {};     (async () => {
(patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });       try {
        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([]);
          // setRequestsList([]); // Removido
          setThreeDEvents([]);
          return;
        }
setAppointments(arr || []);         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<string, any> = {};
        (patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });
const events: EventInput[] = (arr || []).map((obj: any) => {         setAppointments(arr || []);
const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null;
const start = scheduled ? new Date(scheduled) : null;
const duration = Number(obj.duration_minutes ?? obj.duration ?? 30) || 30;
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();
const color = obj.status === 'confirmed' ? '#68d68a' : obj.status === 'pending' ? '#ffe55f' : '#ff5f5fff';
return {
title,
start: start || new Date(),
end: start ? new Date(start.getTime() + duration * 60 * 1000) : undefined,
color,
extendedProps: { raw: obj },
} as EventInput;
});
setRequestsList(events || []);
// Convert to 3D calendar events // --- Mapeamento para o FullCalendar (ANTIGO) - REMOVIDO ---
const threeDEvents: CalendarEvent[] = (arr || []).map((obj: any) => {         // const events: EventInput[] = (arr || []).map((obj: any) => {
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 title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim();         // setRequestsList(events || []);
return {
id: obj.id || String(Date.now()),
title,
date: scheduled ? new Date(scheduled).toISOString() : new Date().toISOString(),
};
});
setThreeDEvents(threeDEvents);
} catch (err) {
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
setAppointments([]);
setRequestsList([]);
setThreeDEvents([]);
}
})();
return () => { mounted = false; };
}, []);
// mantive para caso a lógica de salvar consulta passe a funcionar         // Convert to 3D calendar events (MANTIDO)
const handleSaveAppointment = (appointment: any) => {         const threeDEvents: CalendarEvent[] = (arr || []).map((obj: any) => {
if (appointment.id) {           const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null;
setAppointments((prev) =>           const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente';
prev.map((a) => (a.id === appointment.id ? appointment : a))           const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim();
);           return {
} else {             id: obj.id || String(Date.now()),
const newAppointment = {             title,
...appointment,             date: scheduled ? new Date(scheduled).toISOString() : new Date().toISOString(),
id: Date.now().toString(),           };
};         });
setAppointments((prev) => [...prev, newAppointment]);         setThreeDEvents(threeDEvents);
}       } catch (err) {
};         console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
        setAppointments([]);
        // setRequestsList([]); // Removido
        setThreeDEvents([]);
      }
    })();
    return () => { mounted = false; };
  }, []);
const handleNotifyPatient = (patientId: string) => {   // Handlers mantidos
console.log(`Notificando paciente ${patientId}`);   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]);
    }
  };
const handleAddEvent = (event: CalendarEvent) => {   const handleNotifyPatient = (patientId: string) => {
setThreeDEvents((prev) => [...prev, event]);     console.log(`Notificando paciente ${patientId}`);
};   };
const handleRemoveEvent = (id: string) => {   const handleAddEvent = (event: CalendarEvent) => {
setThreeDEvents((prev) => prev.filter((e) => e.id !== id));     setThreeDEvents((prev) => [...prev, event]);
};   };
return (   const handleRemoveEvent = (id: string) => {
<div className="flex flex-row bg-background">     setThreeDEvents((prev) => prev.filter((e) => e.id !== id));
<div className="flex w-full flex-col">   };
<div className="flex w-full flex-col gap-10 p-6">
<div className="flex flex-row justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-foreground">
{activeTab === "calendar" ? "Calendário" : activeTab === "3d" ? "Calendário 3D" : "Lista de Espera"}
</h1>
<p className="text-muted-foreground">
Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).
</p>
</div>
<div className="flex space-x-2">
{/* <Link href={"/agenda"}>
<Button className="bg-blue-600 hover:bg-blue-700">
Agenda
</Button>
</Link> */}
<DropdownMenu>
<DropdownMenuTrigger className="bg-primary hover:bg-primary/90 px-5 py-1 text-primary-foreground rounded-sm">
Opções &#187;
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link href={"/agenda"}>
<DropdownMenuItem>Agendamento</DropdownMenuItem>
</Link>
<Link href={"/procedimento"}>
<DropdownMenuItem>Procedimento</DropdownMenuItem>
</Link>
<Link href={"/financeiro"}>
<DropdownMenuItem>Financeiro</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex flex-row">   return (
<Button     <div className="flex flex-row bg-background">
variant={"outline"}       <div className="flex w-full flex-col">
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-l-[100px] rounded-r-[0px]"         <div className="flex w-full flex-col gap-10 p-6">
onClick={() => setActiveTab("calendar")}           <div className="flex flex-row justify-between items-center">
> {/* Todo o cabeçalho foi mantido */}
Calendário             <div>
</Button>               <h1 className="text-2xl font-bold text-foreground">
                {activeTab === "calendar" ? "Calendário" : activeTab === "3d" ? "Calendário 3D" : "Lista de Espera"}
              </h1>
              <p className="text-muted-foreground">
                Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).
              </p>
            </div>
            <div className="flex space-x-2">
              <DropdownMenu>
                <DropdownMenuTrigger className="bg-primary hover:bg-primary/90 px-5 py-1 text-primary-foreground rounded-sm">
                  Opções &#187;
                </DropdownMenuTrigger>
                <DropdownMenuContent>
                  <Link href={"/agenda"}>
                    <DropdownMenuItem>Agendamento</DropdownMenuItem>
                  </Link>
                  <Link href={"/procedimento"}>
                    <DropdownMenuItem>Procedimento</DropdownMenuItem>
                  </Link>
                  <Link href={"/financeiro"}>
                    <DropdownMenuItem>Financeiro</DropdownMenuItem>
                  </Link>
                </DropdownMenuContent>
              </DropdownMenu>
<Button               <div className="flex flex-row">
variant={"outline"}                 <Button
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-none"                   variant={"outline"}
onClick={() => setActiveTab("3d")}                   className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-l-[100px] rounded-r-[0px]"
>                   onClick={() => setActiveTab("calendar")}
3D                 >
</Button>                   Calendário
                </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-none"
onClick={() => setActiveTab("espera")}                   onClick={() => setActiveTab("3d")}
>                 >
Lista de espera                   3D
</Button>                 </Button>
</div>
</div>
</div>
{activeTab === "calendar" ? (                 <Button
<div className="flex w-full">                   variant={"outline"}
<FullCalendar                   className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-r-[100px] rounded-l-[0px]"
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}                   onClick={() => setActiveTab("espera")}
initialView="dayGridMonth"                 >
locale={pt_br_locale}                   Lista de espera
timeZone={"America/Sao_Paulo"}                 </Button>
events={requestsList}               </div>
headerToolbar={{             </div>
left: "prev,next today",           </div>
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay", {/* --- AQUI ESTÁ A MUDANÇA --- */}
}}           {activeTab === "calendar" ? (
dateClick={(info) => {             <div className="flex w-full">
info.view.calendar.changeView("timeGridDay", info.dateStr); {/* O FullCalendar antigo foi substituído por este */}
}} <EventManager events={demoEvents} />
selectable={true}             </div>
selectMirror={true}           ) : activeTab === "3d" ? (
dayMaxEvents={true} // O calendário 3D foi mantido intacto
dayMaxEventRows={3}             <div className="flex w-full">
/>               <ThreeDWallCalendar
</div>                 events={threeDEvents}
) : activeTab === "3d" ? (                 onAddEvent={handleAddEvent}
<div className="flex w-full">                 onRemoveEvent={handleRemoveEvent}
<ThreeDWallCalendar               />
events={threeDEvents}             </div>
onAddEvent={handleAddEvent}           ) : (
onRemoveEvent={handleRemoveEvent} // A Lista de Espera foi mantida intacta
/>             <ListaEspera
</div>               patients={waitingList}
) : (               onNotify={handleNotifyPatient}
<ListaEspera               onAddToWaitlist={() => {}}
patients={waitingList}             />
onNotify={handleNotifyPatient}           )}
onAddToWaitlist={() => {}}         </div>
/>       </div>
)}     </div>
</div>   );
</div> }
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,7 @@
"sonner": "latest", "sonner": "latest",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"uuid": "^13.0.0",
"vaul": "latest", "vaul": "latest",
"zod": "3.25.67" "zod": "3.25.67"
}, },
@ -9175,6 +9176,19 @@
"base64-arraybuffer": "^1.0.2" "base64-arraybuffer": "^1.0.2"
} }
}, },
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/vaul": { "node_modules/vaul": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",