Compare commits
4 Commits
44ddc4d03a
...
ac66a68c04
| Author | SHA1 | Date | |
|---|---|---|---|
| ac66a68c04 | |||
| fbdeb7e462 | |||
| 10b439056e | |||
| d2e6d8948e |
14
next.config.mjs
Normal file
14
next.config.mjs
Normal file
@ -0,0 +1,14 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
async rewrites() {
|
||||
return [
|
||||
// Proxy local → Supabase (bypass CORS no navegador)
|
||||
{
|
||||
source: '/proxy/supabase/:path*',
|
||||
destination: 'https://yuanqfswhberkoevtmfr.supabase.co/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
.fc-media-screen {
|
||||
flex-grow: 1;
|
||||
height: 74vh;
|
||||
@ -38,4 +37,47 @@
|
||||
.fc-toolbar-title {
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact mode for embedded EventManager */
|
||||
.compact-event-manager {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.compact-event-manager h2 {
|
||||
font-size: 1rem; /* menor título */
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.compact-event-manager .sm\\:flex { /* reduz grupo de botões */
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.compact-event-manager .button,
|
||||
.compact-event-manager .btn,
|
||||
.compact-event-manager .chakra-button {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Inputs dentro do EventManager compactos */
|
||||
.compact-event-manager input,
|
||||
.compact-event-manager .input {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* reduzir padding dos cards e dos toolbars internos */
|
||||
.compact-event-manager .p-4 { padding: 0.5rem; }
|
||||
.compact-event-manager .p-3 { padding: 0.4rem; }
|
||||
|
||||
/* reduzir altura das linhas na vista semana/dia custom */
|
||||
.compact-event-manager .min-h-16 { min-height: 3.2rem; }
|
||||
.compact-event-manager .min-h-20 { min-height: 3.6rem; }
|
||||
|
||||
/* tornar os botões de filtro menores */
|
||||
.compact-event-manager .dropdown-trigger,
|
||||
.compact-event-manager .dropdown-menu-trigger {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* melhorar harmonia: menos margem entre header e calendário */
|
||||
.compact-event-manager { margin-top: 0.25rem; margin-bottom: 0.25rem; }
|
||||
@ -5,17 +5,9 @@ import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
|
||||
// --- Imports do FullCalendar (ANTIGO) - REMOVIDOS ---
|
||||
// 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 ---
|
||||
// --- Imports do EventManager (NOVO) - MANTIDOS ---
|
||||
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
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
@ -24,274 +16,244 @@ import { Button } from "@/components/ui/button";
|
||||
import { mockWaitingList } from "@/lib/mocks/appointment-mocks";
|
||||
import "./index.css";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"; // Calendário 3D mantido
|
||||
|
||||
const ListaEspera = dynamic(
|
||||
() => import("@/components/agendamento/ListaEspera"),
|
||||
{ ssr: false }
|
||||
() => import("@/components/agendamento/ListaEspera"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function AgendamentoPage() {
|
||||
const [appointments, setAppointments] = useState<any[]>([]);
|
||||
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
||||
const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar");
|
||||
|
||||
// O 'requestsList' do FullCalendar foi removido.
|
||||
// const [requestsList, setRequestsList] = useState<EventInput[]>();
|
||||
|
||||
const [appointments, setAppointments] = useState<any[]>([]);
|
||||
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
||||
const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar");
|
||||
|
||||
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
|
||||
|
||||
// --- Dados de Exemplo para o NOVO Calendário ---
|
||||
// (Colado do exemplo do 21st.dev)
|
||||
const demoEvents: 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 ---
|
||||
// --- NOVO ESTADO ---
|
||||
// Estado para alimentar o NOVO EventManager com dados da API
|
||||
const [managerEvents, setManagerEvents] = useState<Event[]>([]);
|
||||
const [managerLoading, setManagerLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "c") {
|
||||
setActiveTab("calendar");
|
||||
}
|
||||
if (event.key === "f") {
|
||||
setActiveTab("espera");
|
||||
}
|
||||
if (event.key === "3") {
|
||||
setActiveTab("3d");
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "c") {
|
||||
setActiveTab("calendar");
|
||||
}
|
||||
if (event.key === "f") {
|
||||
setActiveTab("espera");
|
||||
}
|
||||
if (event.key === "3") {
|
||||
setActiveTab("3d");
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Este useEffect foi mantido, pois ele busca dados para o Calendário 3D
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
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;
|
||||
}
|
||||
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([]);
|
||||
setThreeDEvents([]);
|
||||
setManagerEvents([]); // Limpa o novo calendário
|
||||
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<string, any> = {};
|
||||
(patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });
|
||||
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; });
|
||||
|
||||
setAppointments(arr || []);
|
||||
setAppointments(arr || []);
|
||||
|
||||
// --- Mapeamento para o FullCalendar (ANTIGO) - REMOVIDO ---
|
||||
// const events: EventInput[] = (arr || []).map((obj: any) => {
|
||||
// ...
|
||||
// });
|
||||
// setRequestsList(events || []);
|
||||
// --- 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();
|
||||
|
||||
let color = "gray"; // Cor padrão
|
||||
if (obj.status === 'confirmed') color = 'green';
|
||||
if (obj.status === 'pending') color = 'orange';
|
||||
|
||||
// Convert to 3D calendar events (MANTIDO)
|
||||
const threeDEvents: CalendarEvent[] = (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();
|
||||
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([]); // Removido
|
||||
setThreeDEvents([]);
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
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);
|
||||
setManagerLoading(false);
|
||||
// --- FIM DA LÓGICA ---
|
||||
|
||||
// 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]);
|
||||
}
|
||||
};
|
||||
// Convert to 3D calendar events (MANTIDO 100%)
|
||||
const threeDEvents: CalendarEvent[] = (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 appointmentType = obj.appointment_type ?? obj.type ?? 'Consulta';
|
||||
const title = `${patient}: ${appointmentType}`.trim();
|
||||
return {
|
||||
id: obj.id || String(Date.now()),
|
||||
title,
|
||||
date: scheduled ? new Date(scheduled).toISOString() : new Date().toISOString(),
|
||||
status: obj.status || 'pending',
|
||||
patient,
|
||||
type: appointmentType,
|
||||
};
|
||||
});
|
||||
setThreeDEvents(threeDEvents);
|
||||
} catch (err) {
|
||||
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
|
||||
setAppointments([]);
|
||||
setThreeDEvents([]);
|
||||
setManagerEvents([]); // Limpa o novo calendário
|
||||
setManagerLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
const handleNotifyPatient = (patientId: string) => {
|
||||
console.log(`Notificando paciente ${patientId}`);
|
||||
};
|
||||
// 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]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddEvent = (event: CalendarEvent) => {
|
||||
setThreeDEvents((prev) => [...prev, event]);
|
||||
};
|
||||
const handleNotifyPatient = (patientId: string) => {
|
||||
console.log(`Notificando paciente ${patientId}`);
|
||||
};
|
||||
|
||||
const handleRemoveEvent = (id: string) => {
|
||||
setThreeDEvents((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
const handleAddEvent = (event: CalendarEvent) => {
|
||||
setThreeDEvents((prev) => [...prev, event]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row bg-background">
|
||||
<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">
|
||||
const handleRemoveEvent = (id: string) => {
|
||||
setThreeDEvents((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row bg-background">
|
||||
<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">
|
||||
{/* Todo o cabeçalho foi mantido */}
|
||||
<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">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="bg-primary hover:bg-primary/90 px-5 py-1 text-primary-foreground rounded-sm">
|
||||
Opções »
|
||||
</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>
|
||||
<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 »
|
||||
</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">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-l-[100px] rounded-r-[0px]"
|
||||
onClick={() => setActiveTab("calendar")}
|
||||
>
|
||||
Calendário
|
||||
</Button>
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-l-[100px] rounded-r-[0px]"
|
||||
onClick={() => setActiveTab("calendar")}
|
||||
>
|
||||
Calendário
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-none"
|
||||
onClick={() => setActiveTab("3d")}
|
||||
>
|
||||
3D
|
||||
</Button>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-none"
|
||||
onClick={() => setActiveTab("3d")}
|
||||
>
|
||||
3D
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-r-[100px] rounded-l-[0px]"
|
||||
onClick={() => setActiveTab("espera")}
|
||||
>
|
||||
Lista de espera
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:!bg-primary hover:!text-white transition-colors rounded-r-[100px] rounded-l-[0px]"
|
||||
onClick={() => setActiveTab("espera")}
|
||||
>
|
||||
Lista de espera
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- AQUI ESTÁ A MUDANÇA --- */}
|
||||
{activeTab === "calendar" ? (
|
||||
<div className="flex w-full">
|
||||
{/* O FullCalendar antigo foi substituído por este */}
|
||||
<EventManager events={demoEvents} />
|
||||
</div>
|
||||
) : activeTab === "3d" ? (
|
||||
// O calendário 3D foi mantido intacto
|
||||
<div className="flex w-full">
|
||||
<ThreeDWallCalendar
|
||||
events={threeDEvents}
|
||||
onAddEvent={handleAddEvent}
|
||||
onRemoveEvent={handleRemoveEvent}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// A Lista de Espera foi mantida intacta
|
||||
<ListaEspera
|
||||
patients={waitingList}
|
||||
onNotify={handleNotifyPatient}
|
||||
onAddToWaitlist={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{/* --- AQUI ESTÁ A SUBSTITUIÇÃO --- */}
|
||||
{activeTab === "calendar" ? (
|
||||
<div className="flex w-full">
|
||||
{/* mostra loading até managerEvents ser preenchido (API integrada desde a entrada) */}
|
||||
<div className="w-full">
|
||||
{managerLoading ? (
|
||||
<div className="flex items-center justify-center w-full min-h-[70vh]">
|
||||
<div className="text-sm text-muted-foreground">Conectando ao calendário — carregando agendamentos...</div>
|
||||
</div>
|
||||
) : (
|
||||
// EventManager ocupa a área principal e já recebe events da API
|
||||
<div className="w-full min-h-[70vh]">
|
||||
<EventManager events={managerEvents} className="compact-event-manager" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : activeTab === "3d" ? (
|
||||
// O calendário 3D (ThreeDWallCalendar) foi MANTIDO 100%
|
||||
<div className="flex w-full justify-center">
|
||||
<ThreeDWallCalendar
|
||||
events={threeDEvents}
|
||||
onAddEvent={handleAddEvent}
|
||||
onRemoveEvent={handleRemoveEvent}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// A Lista de Espera foi MANTIDA
|
||||
<ListaEspera
|
||||
patients={waitingList}
|
||||
onNotify={handleNotifyPatient}
|
||||
onAddToWaitlist={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
118
susconecta/components/Calendario/Calendar.tsx
Normal file
118
susconecta/components/Calendario/Calendar.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useState, useCallback, useMemo } from "react";
|
||||
import { EventCard } from "./EventCard";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
// Types
|
||||
import { Event } from "@/components/event-manager";
|
||||
|
||||
// Week View Component
|
||||
export function WeekView({
|
||||
currentDate,
|
||||
events,
|
||||
onEventClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDrop,
|
||||
getColorClasses,
|
||||
}: {
|
||||
currentDate: Date;
|
||||
events: Event[];
|
||||
onEventClick: (event: Event) => void;
|
||||
onDragStart: (event: Event) => void;
|
||||
onDragEnd: () => void;
|
||||
onDrop: (date: Date, hour: number) => void;
|
||||
getColorClasses: (color: string) => { bg: string; text: string };
|
||||
}) {
|
||||
const startOfWeek = new Date(currentDate);
|
||||
startOfWeek.setDate(currentDate.getDay());
|
||||
|
||||
const weekDays = Array.from({ length: 7 }, (_, i) => {
|
||||
const day = new Date(startOfWeek);
|
||||
day.setDate(startOfWeek.getDate() + i);
|
||||
return day;
|
||||
});
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
|
||||
const getEventsForDayAndHour = (date: Date, hour: number) => {
|
||||
return events.filter((event) => {
|
||||
const eventDate = new Date(event.startTime);
|
||||
const eventHour = eventDate.getHours();
|
||||
return (
|
||||
eventDate.getDate() === date.getDate() &&
|
||||
eventDate.getMonth() === date.getMonth() &&
|
||||
eventDate.getFullYear() === date.getFullYear() &&
|
||||
eventHour === hour
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// dias da semana em pt-BR (abreviações)
|
||||
const weekDayNames = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
|
||||
|
||||
return (
|
||||
<Card className="overflow-auto">
|
||||
<div className="grid grid-cols-8 border-b">
|
||||
<div className="border-r p-2 text-center text-xs font-medium sm:text-sm">
|
||||
Hora
|
||||
</div>
|
||||
{weekDays.map((day) => (
|
||||
<div
|
||||
key={day.toISOString()}
|
||||
className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm"
|
||||
>
|
||||
<div className="hidden sm:block">
|
||||
{day.toLocaleDateString("pt-BR", { weekday: "short" })}
|
||||
</div>
|
||||
<div className="sm:hidden">
|
||||
{day.toLocaleDateString("pt-BR", { weekday: "narrow" })}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground sm:text-xs">
|
||||
{day.toLocaleDateString("pt-BR", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-8">
|
||||
{hours.map((hour) => (
|
||||
<React.Fragment key={`hour-${hour}`}>
|
||||
<div
|
||||
key={`time-${hour}`}
|
||||
className="border-b border-r p-1 text-[10px] text-muted-foreground sm:p-2 sm:text-xs"
|
||||
>
|
||||
{hour.toString().padStart(2, "0")}:00
|
||||
</div>
|
||||
{weekDays.map((day) => {
|
||||
const dayEvents = getEventsForDayAndHour(day, hour);
|
||||
return (
|
||||
<div
|
||||
key={`${day.toISOString()}-${hour}`}
|
||||
className="min-h-12 border-b border-r p-0.5 transition-colors hover:bg-accent/50 last:border-r-0 sm:min-h-16 sm:p-1"
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={() => onDrop(day, hour)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{dayEvents.map((event) => (
|
||||
<EventCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
onEventClick={onEventClick}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
getColorClasses={getColorClasses}
|
||||
variant="default"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
103
susconecta/components/Calendario/EventCard.tsx
Normal file
103
susconecta/components/Calendario/EventCard.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import React, { useState } from "react";
|
||||
import { Event } from "@/components/event-manager";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/*
|
||||
Componente leve para representar um evento no calendário.
|
||||
Compatível com o uso em Calendar.tsx (WeekView / DayView).
|
||||
*/
|
||||
|
||||
export function EventCard({
|
||||
event,
|
||||
onEventClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
getColorClasses,
|
||||
variant = "default",
|
||||
}: {
|
||||
event: Event;
|
||||
onEventClick: (e: Event) => void;
|
||||
onDragStart: (e: Event) => void;
|
||||
onDragEnd: () => void;
|
||||
getColorClasses: (color: string) => { bg: string; text: string };
|
||||
variant?: "default" | "compact" | "detailed";
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const color = getColorClasses?.(event.color) ?? { bg: "bg-slate-400", text: "text-white" };
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
e.dataTransfer.setData("text/plain", event.id);
|
||||
onDragStart && onDragStart(event);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
onEventClick && onEventClick(event);
|
||||
};
|
||||
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={() => onDragEnd && onDragEnd()}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
className={cn(
|
||||
"rounded px-2 py-0.5 text-xs font-medium truncate",
|
||||
color.bg,
|
||||
color.text,
|
||||
"cursor-pointer transition-all",
|
||||
hover && "shadow-md scale-105"
|
||||
)}
|
||||
>
|
||||
{event.title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "detailed") {
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={() => onDragEnd && onDragEnd()}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
className={cn(
|
||||
"rounded-lg p-2 text-sm cursor-pointer transition-all",
|
||||
color.bg,
|
||||
color.text,
|
||||
hover && "shadow-lg scale-[1.02]"
|
||||
)}
|
||||
>
|
||||
<div className="font-semibold">{event.title}</div>
|
||||
{event.description && <div className="text-xs opacity-90 mt-1 line-clamp-2">{event.description}</div>}
|
||||
<div className="mt-1 text-[11px] opacity-80">
|
||||
{event.startTime?.toLocaleTimeString?.("pt-BR", { hour: "2-digit", minute: "2-digit" }) ?? ""} - {event.endTime?.toLocaleTimeString?.("pt-BR", { hour: "2-digit", minute: "2-digit" }) ?? ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// default
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={() => onDragEnd && onDragEnd()}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
className={cn(
|
||||
"relative rounded px-2 py-1 text-xs font-medium cursor-pointer transition-all",
|
||||
color.bg,
|
||||
color.text,
|
||||
hover && "shadow-md scale-105"
|
||||
)}
|
||||
>
|
||||
<div className="truncate">{event.title}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback, useMemo } from "react"
|
||||
import React, { useState, useCallback, useMemo } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@ -263,30 +263,30 @@ export function EventManager({
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
|
||||
<h2 className="text-xl font-semibold sm:text-2xl">
|
||||
{view === "month" &&
|
||||
currentDate.toLocaleDateString("en-US", {
|
||||
currentDate.toLocaleDateString("pt-BR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
{view === "week" &&
|
||||
`Week of ${currentDate.toLocaleDateString("en-US", {
|
||||
`Semana de ${currentDate.toLocaleDateString("pt-BR", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}`}
|
||||
{view === "day" &&
|
||||
currentDate.toLocaleDateString("en-US", {
|
||||
currentDate.toLocaleDateString("pt-BR", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
{view === "list" && "All Events"}
|
||||
{view === "list" && "Todos os eventos"}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onClick={() => navigateDate("prev")} className="h-8 w-8">
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setCurrentDate(new Date())}>
|
||||
Today
|
||||
Hoje
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => navigateDate("next")} className="h-8 w-8">
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
@ -305,25 +305,25 @@ export function EventManager({
|
||||
<SelectItem value="month">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Month View
|
||||
Mês
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="week">
|
||||
<div className="flex items-center gap-2">
|
||||
<Grid3x3 className="h-4 w-4" />
|
||||
Week View
|
||||
Semana
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="day">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
Day View
|
||||
Dia
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="list">
|
||||
<div className="flex items-center gap-2">
|
||||
<List className="h-4 w-4" />
|
||||
List View
|
||||
Lista
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@ -339,7 +339,7 @@ export function EventManager({
|
||||
className="h-8"
|
||||
>
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span className="ml-1">Month</span>
|
||||
<span className="ml-1">Mês</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "week" ? "secondary" : "ghost"}
|
||||
@ -348,7 +348,7 @@ export function EventManager({
|
||||
className="h-8"
|
||||
>
|
||||
<Grid3x3 className="h-4 w-4" />
|
||||
<span className="ml-1">Week</span>
|
||||
<span className="ml-1">Semana</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "day" ? "secondary" : "ghost"}
|
||||
@ -357,7 +357,7 @@ export function EventManager({
|
||||
className="h-8"
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
<span className="ml-1">Day</span>
|
||||
<span className="ml-1">Dia</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "list" ? "secondary" : "ghost"}
|
||||
@ -366,7 +366,7 @@ export function EventManager({
|
||||
className="h-8"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
<span className="ml-1">List</span>
|
||||
<span className="ml-1">Lista</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -378,7 +378,7 @@ export function EventManager({
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Event
|
||||
Novo Evento
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -387,7 +387,7 @@ export function EventManager({
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search events..."
|
||||
placeholder="Buscar eventos..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
@ -412,7 +412,7 @@ export function EventManager({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
|
||||
<Filter className="h-4 w-4" />
|
||||
Colors
|
||||
Cores
|
||||
{selectedColors.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{selectedColors.length}
|
||||
@ -421,7 +421,7 @@ export function EventManager({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuLabel>Filter by Color</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Filtrar por Cor</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{colors.map((color) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@ -456,7 +456,7 @@ export function EventManager({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuLabel>Filter by Tag</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Filtrar por Tag</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{availableTags.map((tag) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@ -477,7 +477,7 @@ export function EventManager({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
|
||||
<Filter className="h-4 w-4" />
|
||||
Categories
|
||||
Categorias
|
||||
{selectedCategories.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
|
||||
{selectedCategories.length}
|
||||
@ -486,7 +486,7 @@ export function EventManager({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuLabel>Filter by Category</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Filtrar por Categoria</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{categories.map((category) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@ -512,7 +512,7 @@ export function EventManager({
|
||||
className="gap-2 whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Clear Filters
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -525,7 +525,7 @@ export function EventManager({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
|
||||
<Filter className="h-4 w-4" />
|
||||
Colors
|
||||
Cores
|
||||
{selectedColors.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1">
|
||||
{selectedColors.length}
|
||||
@ -534,7 +534,7 @@ export function EventManager({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>Filter by Color</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Filtrar por Cor</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{colors.map((color) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@ -569,7 +569,7 @@ export function EventManager({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>Filter by Tag</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Filtrar por Tag</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{availableTags.map((tag) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@ -590,7 +590,7 @@ export function EventManager({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
|
||||
<Filter className="h-4 w-4" />
|
||||
Categories
|
||||
Categorias
|
||||
{selectedCategories.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 px-1">
|
||||
{selectedCategories.length}
|
||||
@ -599,7 +599,7 @@ export function EventManager({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>Filter by Category</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Filtrar por Categoria</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{categories.map((category) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@ -620,7 +620,7 @@ export function EventManager({
|
||||
{hasActiveFilters && (
|
||||
<Button variant="ghost" size="sm" onClick={clearFilters} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
Clear
|
||||
Limpar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -628,7 +628,7 @@ export function EventManager({
|
||||
|
||||
{hasActiveFilters && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Active filters:</span>
|
||||
<span className="text-sm text-muted-foreground">Filtros ativos:</span>
|
||||
{selectedColors.map((colorValue) => {
|
||||
const color = getColorClasses(colorValue)
|
||||
return (
|
||||
@ -678,8 +678,8 @@ export function EventManager({
|
||||
setSelectedEvent(event)
|
||||
setIsDialogOpen(true)
|
||||
}}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={(event) => handleDragStart(event)}
|
||||
onDragEnd={() => handleDragEnd()}
|
||||
onDrop={handleDrop}
|
||||
getColorClasses={getColorClasses}
|
||||
/>
|
||||
@ -693,8 +693,8 @@ export function EventManager({
|
||||
setSelectedEvent(event)
|
||||
setIsDialogOpen(true)
|
||||
}}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={(event) => handleDragStart(event)}
|
||||
onDragEnd={() => handleDragEnd()}
|
||||
onDrop={handleDrop}
|
||||
getColorClasses={getColorClasses}
|
||||
/>
|
||||
@ -708,8 +708,8 @@ export function EventManager({
|
||||
setSelectedEvent(event)
|
||||
setIsDialogOpen(true)
|
||||
}}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={(event) => handleDragStart(event)}
|
||||
onDragEnd={() => handleDragEnd()}
|
||||
onDrop={handleDrop}
|
||||
getColorClasses={getColorClasses}
|
||||
/>
|
||||
@ -730,15 +730,15 @@ export function EventManager({
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isCreating ? "Create Event" : "Event Details"}</DialogTitle>
|
||||
<DialogTitle>{isCreating ? "Criar Evento" : "Detalhes do Evento"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isCreating ? "Add a new event to your calendar" : "View and edit event details"}
|
||||
{isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do evento"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title</Label>
|
||||
<Label htmlFor="title">Título</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={isCreating ? newEvent.title : selectedEvent?.title}
|
||||
@ -747,12 +747,12 @@ export function EventManager({
|
||||
? setNewEvent((prev) => ({ ...prev, title: e.target.value }))
|
||||
: setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null))
|
||||
}
|
||||
placeholder="Event title"
|
||||
placeholder="Título do evento"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Label htmlFor="description">Descrição</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={isCreating ? newEvent.description : selectedEvent?.description}
|
||||
@ -764,14 +764,14 @@ export function EventManager({
|
||||
}))
|
||||
: setSelectedEvent((prev) => (prev ? { ...prev, description: e.target.value } : null))
|
||||
}
|
||||
placeholder="Event description"
|
||||
placeholder="Descrição do evento"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="startTime">Start Time</Label>
|
||||
<Label htmlFor="startTime">Início</Label>
|
||||
<Input
|
||||
id="startTime"
|
||||
type="datetime-local"
|
||||
@ -800,7 +800,7 @@ export function EventManager({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endTime">End Time</Label>
|
||||
<Label htmlFor="endTime">Fim</Label>
|
||||
<Input
|
||||
id="endTime"
|
||||
type="datetime-local"
|
||||
@ -829,7 +829,7 @@ export function EventManager({
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Label htmlFor="category">Categoria</Label>
|
||||
<Select
|
||||
value={isCreating ? newEvent.category : selectedEvent?.category}
|
||||
onValueChange={(value) =>
|
||||
@ -839,7 +839,7 @@ export function EventManager({
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="category">
|
||||
<SelectValue placeholder="Select category" />
|
||||
<SelectValue placeholder="Selecione a categoria" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((cat) => (
|
||||
@ -852,7 +852,7 @@ export function EventManager({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="color">Color</Label>
|
||||
<Label htmlFor="color">Cor</Label>
|
||||
<Select
|
||||
value={isCreating ? newEvent.color : selectedEvent?.color}
|
||||
onValueChange={(value) =>
|
||||
@ -862,7 +862,7 @@ export function EventManager({
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="color">
|
||||
<SelectValue placeholder="Select color" />
|
||||
<SelectValue placeholder="Selecione a cor" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{colors.map((color) => (
|
||||
@ -901,7 +901,7 @@ export function EventManager({
|
||||
<DialogFooter>
|
||||
{!isCreating && (
|
||||
<Button variant="destructive" onClick={() => selectedEvent && handleDeleteEvent(selectedEvent.id)}>
|
||||
Delete
|
||||
Deletar
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@ -912,10 +912,10 @@ export function EventManager({
|
||||
setSelectedEvent(null)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={isCreating ? handleCreateEvent : handleUpdateEvent}>
|
||||
{isCreating ? "Create" : "Save"}
|
||||
{isCreating ? "Criar" : "Salvar"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@ -1160,7 +1160,7 @@ function MonthView({
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<div className="grid grid-cols-7 border-b">
|
||||
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => (
|
||||
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
|
||||
<div key={day} className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm">
|
||||
<span className="hidden sm:inline">{day}</span>
|
||||
<span className="sm:hidden">{day.charAt(0)}</span>
|
||||
@ -1204,8 +1204,8 @@ function MonthView({
|
||||
variant="compact"
|
||||
/>
|
||||
))}
|
||||
{dayEvents.length > 3 && (
|
||||
<div className="text-[10px] text-muted-foreground sm:text-xs">+{dayEvents.length - 3} more</div>
|
||||
{dayEvents.length > 3 && (
|
||||
<div className="text-[10px] text-muted-foreground sm:text-xs">+{dayEvents.length - 3} mais</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -1261,16 +1261,16 @@ function WeekView({
|
||||
return (
|
||||
<Card className="overflow-auto">
|
||||
<div className="grid grid-cols-8 border-b">
|
||||
<div className="border-r p-2 text-center text-xs font-medium sm:text-sm">Time</div>
|
||||
<div className="border-r p-2 text-center text-xs font-medium sm:text-sm">Hora</div>
|
||||
{weekDays.map((day) => (
|
||||
<div
|
||||
key={day.toISOString()}
|
||||
className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm"
|
||||
>
|
||||
<div className="hidden sm:block">{day.toLocaleDateString("en-US", { weekday: "short" })}</div>
|
||||
<div className="sm:hidden">{day.toLocaleDateString("en-US", { weekday: "narrow" })}</div>
|
||||
<div className="hidden sm:block">{day.toLocaleDateString("pt-BR", { weekday: "short" })}</div>
|
||||
<div className="sm:hidden">{day.toLocaleDateString("pt-BR", { weekday: "narrow" })}</div>
|
||||
<div className="text-[10px] text-muted-foreground sm:text-xs">
|
||||
{day.toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
||||
{day.toLocaleDateString("pt-BR", { month: "short", day: "numeric" })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -1401,7 +1401,7 @@ function ListView({
|
||||
|
||||
const groupedEvents = sortedEvents.reduce(
|
||||
(acc, event) => {
|
||||
const dateKey = event.startTime.toLocaleDateString("en-US", {
|
||||
const dateKey = event.startTime.toLocaleDateString("pt-BR", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
@ -1456,15 +1456,7 @@ function ListView({
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-[10px] text-muted-foreground sm:gap-4 sm:text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{event.startTime.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}{" "}
|
||||
-{" "}
|
||||
{event.endTime.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
{event.startTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} - {event.endTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
{event.tags && event.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@ -1485,7 +1477,7 @@ function ListView({
|
||||
</div>
|
||||
))}
|
||||
{sortedEvents.length === 0 && (
|
||||
<div className="py-12 text-center text-sm text-muted-foreground sm:text-base">No events found</div>
|
||||
<div className="py-12 text-center text-sm text-muted-foreground sm:text-base">Nenhum evento encontrado</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -4,9 +4,10 @@ import * as React from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
|
||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Trash2, Calendar, Clock, User } from "lucide-react"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { startOfMonth, endOfMonth, eachDayOfInterval, format } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
@ -15,6 +16,9 @@ export type CalendarEvent = {
|
||||
id: string
|
||||
title: string
|
||||
date: string // ISO
|
||||
status?: 'confirmed' | 'pending' | 'cancelled' | string
|
||||
patient?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface ThreeDWallCalendarProps {
|
||||
@ -37,6 +41,8 @@ export function ThreeDWallCalendar({
|
||||
const [dateRef, setDateRef] = React.useState<Date>(new Date())
|
||||
const [title, setTitle] = React.useState("")
|
||||
const [newDate, setNewDate] = React.useState("")
|
||||
const [selectedDay, setSelectedDay] = React.useState<Date | null>(null)
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false)
|
||||
const wallRef = React.useRef<HTMLDivElement | null>(null)
|
||||
|
||||
// 3D tilt state
|
||||
@ -44,6 +50,7 @@ export function ThreeDWallCalendar({
|
||||
const [tiltY, setTiltY] = React.useState(0)
|
||||
const isDragging = React.useRef(false)
|
||||
const dragStart = React.useRef<{ x: number; y: number } | null>(null)
|
||||
const hasDragged = React.useRef(false)
|
||||
|
||||
// month days
|
||||
const days = eachDayOfInterval({
|
||||
@ -54,6 +61,16 @@ export function ThreeDWallCalendar({
|
||||
const eventsForDay = (d: Date) =>
|
||||
events.filter((ev) => format(new Date(ev.date), "yyyy-MM-dd") === format(d, "yyyy-MM-dd"))
|
||||
|
||||
const selectedDayEvents = selectedDay ? eventsForDay(selectedDay) : []
|
||||
|
||||
const handleDayClick = (day: Date) => {
|
||||
// Só abre o dialog se não foi um drag
|
||||
if (!hasDragged.current) {
|
||||
setSelectedDay(day)
|
||||
setIsDialogOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Add event handler
|
||||
const handleAdd = () => {
|
||||
if (!title.trim() || !newDate) return
|
||||
@ -75,18 +92,26 @@ export function ThreeDWallCalendar({
|
||||
// drag tilt
|
||||
const onPointerDown = (e: React.PointerEvent) => {
|
||||
isDragging.current = true
|
||||
hasDragged.current = false
|
||||
dragStart.current = { x: e.clientX, y: e.clientY }
|
||||
;(e.currentTarget as Element).setPointerCapture(e.pointerId) // ✅ Correct element
|
||||
;(e.currentTarget as Element).setPointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
const onPointerMove = (e: React.PointerEvent) => {
|
||||
if (!isDragging.current || !dragStart.current) return
|
||||
const dx = e.clientX - dragStart.current.x
|
||||
const dy = e.clientY - dragStart.current.y
|
||||
|
||||
// Se moveu mais de 5 pixels, considera como drag
|
||||
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
||||
hasDragged.current = true
|
||||
}
|
||||
|
||||
setTiltY((t) => Math.max(-60, Math.min(60, t + dx * 0.1)))
|
||||
setTiltX((t) => Math.max(0, Math.min(60, t - dy * 0.1)))
|
||||
dragStart.current = { x: e.clientX, y: e.clientY }
|
||||
}
|
||||
|
||||
const onPointerUp = () => {
|
||||
isDragging.current = false
|
||||
dragStart.current = null
|
||||
@ -98,27 +123,53 @@ export function ThreeDWallCalendar({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1))}>
|
||||
Mês Anterior
|
||||
</Button>
|
||||
<div className="font-semibold">{format(dateRef, "MMMM yyyy", { locale: ptBR })}</div>
|
||||
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1))}>
|
||||
Próximo Mês
|
||||
</Button>
|
||||
<div className="flex gap-4 items-center justify-between flex-wrap">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1))}>
|
||||
Mês Anterior
|
||||
</Button>
|
||||
<div className="font-semibold text-lg">{format(dateRef, "MMMM yyyy", { locale: ptBR })}</div>
|
||||
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1))}>
|
||||
Próximo Mês
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Legenda de cores */}
|
||||
<div className="flex gap-3 items-center text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500 dark:bg-green-600"></div>
|
||||
<span>Confirmado</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500 dark:bg-yellow-600"></div>
|
||||
<span>Pendente</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500 dark:bg-red-600"></div>
|
||||
<span>Cancelado</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500 dark:bg-blue-600"></div>
|
||||
<span>Outros</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wall container */}
|
||||
<div
|
||||
ref={wallRef}
|
||||
onWheel={onWheel}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
className="w-full overflow-auto"
|
||||
style={{ perspective: 1200 }}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute top-2 left-2 z-10 bg-background/80 backdrop-blur-sm px-3 py-1.5 rounded-lg text-xs text-muted-foreground border border-border">
|
||||
💡 Arraste para rotacionar • Scroll para inclinar
|
||||
</div>
|
||||
<div
|
||||
ref={wallRef}
|
||||
onWheel={onWheel}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
className="w-full overflow-auto"
|
||||
style={{ perspective: 1200 }}
|
||||
>
|
||||
<div
|
||||
className="mx-auto"
|
||||
style={{
|
||||
@ -148,71 +199,111 @@ export function ThreeDWallCalendar({
|
||||
return (
|
||||
<div
|
||||
key={day.toISOString()}
|
||||
className="relative"
|
||||
className="relative cursor-pointer"
|
||||
style={{
|
||||
transform: `translateZ(${z}px)`,
|
||||
zIndex: Math.round(100 - Math.abs(rowOffset)),
|
||||
}}
|
||||
onClick={() => handleDayClick(day)}
|
||||
>
|
||||
<Card className="h-full overflow-visible">
|
||||
<CardContent className="p-3 h-full flex flex-col">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="text-xs font-medium">{format(day, "d")}</div>
|
||||
<div className="text-xs text-muted-foreground">{format(day, "EEE", { locale: ptBR })}</div>
|
||||
<Card className="h-full overflow-visible hover:shadow-lg transition-shadow">
|
||||
<CardContent className="p-2 h-full flex flex-col">
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<div className="text-sm font-medium">{format(day, "d")}</div>
|
||||
<div className="text-[9px] text-muted-foreground">
|
||||
{dayEvents.length > 0 && `${dayEvents.length} ${dayEvents.length === 1 ? 'paciente' : 'pacientes'}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mb-1">{format(day, "EEE", { locale: ptBR })}</div>
|
||||
|
||||
{/* events */}
|
||||
<div className="relative mt-2 flex-1">
|
||||
<div className="relative flex-1 min-h-0">
|
||||
{dayEvents.map((ev, i) => {
|
||||
const left = 8 + (i * 34) % (panelWidth - 40)
|
||||
const top = 8 + Math.floor((i * 34) / (panelWidth - 40)) * 28
|
||||
// Calcular tamanho da bolinha baseado na quantidade de eventos
|
||||
const eventCount = dayEvents.length
|
||||
const ballSize = eventCount <= 3 ? 20 :
|
||||
eventCount <= 6 ? 16 :
|
||||
eventCount <= 10 ? 14 :
|
||||
eventCount <= 15 ? 12 : 10
|
||||
|
||||
const spacing = ballSize + 4
|
||||
const maxPerRow = Math.floor((panelWidth - 16) / spacing)
|
||||
const col = i % maxPerRow
|
||||
const row = Math.floor(i / maxPerRow)
|
||||
const left = 4 + (col * spacing)
|
||||
const top = 4 + (row * spacing)
|
||||
|
||||
// Cores baseadas no status
|
||||
const getStatusColor = () => {
|
||||
switch(ev.status) {
|
||||
case 'confirmed': return 'bg-green-500 dark:bg-green-600'
|
||||
case 'pending': return 'bg-yellow-500 dark:bg-yellow-600'
|
||||
case 'cancelled': return 'bg-red-500 dark:bg-red-600'
|
||||
default: return 'bg-blue-500 dark:bg-blue-600'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover key={ev.id}>
|
||||
<PopoverTrigger asChild>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
className="absolute w-7 h-7 rounded-full bg-blue-500 dark:bg-blue-600 flex items-center justify-center text-white text-[10px] cursor-pointer shadow"
|
||||
style={{ left, top, transform: `translateZ(20px)` }}
|
||||
<HoverCard key={ev.id} openDelay={100}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
className={`absolute rounded-full ${getStatusColor()} flex items-center justify-center text-white cursor-pointer shadow-sm hover:shadow-md hover:scale-110 transition-all`}
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
width: ballSize,
|
||||
height: ballSize,
|
||||
fontSize: Math.max(6, ballSize / 3),
|
||||
transform: `translateZ(15px)`
|
||||
}}
|
||||
>
|
||||
•
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-64 p-3" side="top">
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-sm">{ev.title}</div>
|
||||
{ev.patient && ev.type && (
|
||||
<div className="text-xs space-y-1">
|
||||
<div><span className="font-medium">Paciente:</span> {ev.patient}</div>
|
||||
<div><span className="font-medium">Tipo:</span> {ev.type}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{format(new Date(ev.date), "PPP 'às' p", { locale: ptBR })}
|
||||
</div>
|
||||
{ev.status && (
|
||||
<div className="text-xs">
|
||||
<span className="font-medium">Status:</span>{' '}
|
||||
<span className={
|
||||
ev.status === 'confirmed' ? 'text-green-600 dark:text-green-400' :
|
||||
ev.status === 'pending' ? 'text-yellow-600 dark:text-yellow-400' :
|
||||
ev.status === 'cancelled' ? 'text-red-600 dark:text-red-400' :
|
||||
''
|
||||
}>
|
||||
{ev.status === 'confirmed' ? 'Confirmado' :
|
||||
ev.status === 'pending' ? 'Pendente' :
|
||||
ev.status === 'cancelled' ? 'Cancelado' : ev.status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{onRemoveEvent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full h-7 text-xs hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => onRemoveEvent(ev.id)}
|
||||
>
|
||||
•
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="text-xs font-medium">
|
||||
{ev.title}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48">
|
||||
<Card>
|
||||
<CardContent className="flex justify-between items-center p-2 text-sm">
|
||||
<div>
|
||||
<div className="font-medium">{ev.title}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{format(new Date(ev.date), "PPP p", { locale: ptBR })}
|
||||
</div>
|
||||
</div>
|
||||
{onRemoveEvent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => onRemoveEvent(ev.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
Remover
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
{dayEvents.length} evento(s)
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -220,13 +311,101 @@ export function ThreeDWallCalendar({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialog de detalhes do dia */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedDayEvents.length} {selectedDayEvents.length === 1 ? 'paciente agendado' : 'pacientes agendados'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 mt-4">
|
||||
{selectedDayEvents.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Nenhum paciente agendado para este dia
|
||||
</div>
|
||||
) : (
|
||||
selectedDayEvents.map((ev) => {
|
||||
const getStatusColor = () => {
|
||||
switch(ev.status) {
|
||||
case 'confirmed': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
case 'pending': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
case 'cancelled': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
default: return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = () => {
|
||||
switch(ev.status) {
|
||||
case 'confirmed': return 'Confirmado'
|
||||
case 'pending': return 'Pendente'
|
||||
case 'cancelled': return 'Cancelado'
|
||||
default: return ev.status || 'Sem status'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card key={ev.id} className="overflow-hidden">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="font-semibold">{ev.patient || ev.title}</h3>
|
||||
</div>
|
||||
|
||||
{ev.type && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
<span>{ev.type}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{format(new Date(ev.date), "HH:mm", { locale: ptBR })}</span>
|
||||
</div>
|
||||
|
||||
<Badge className={getStatusColor()}>
|
||||
{getStatusText()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{onRemoveEvent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRemoveEvent(ev.id)
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Add event form */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input placeholder="Título do evento" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
<Input placeholder="Nome do paciente" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
<Input type="date" value={newDate} onChange={(e) => setNewDate(e.target.value)} />
|
||||
<Button onClick={handleAdd}>Adicionar Evento</Button>
|
||||
<Button onClick={handleAdd}>Adicionar Paciente</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user