feat(calendar): integrar EventManager, manter FullCalendar como fallback e adicionar calendário 3D; compactar controles do cabeçalho
This commit is contained in:
commit
10b439056e
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 {
|
.fc-media-screen {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 74vh;
|
height: 74vh;
|
||||||
@ -38,4 +37,47 @@
|
|||||||
.fc-toolbar-title {
|
.fc-toolbar-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--color-gray-900);
|
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,13 +5,13 @@ 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 (ANTIGO) - REMOVIDOS ---
|
// --- Imports do FullCalendar (restaurados) ---
|
||||||
// import pt_br_locale from "@fullcalendar/core/locales/pt-br";
|
import pt_br_locale from "@fullcalendar/core/locales/pt-br";
|
||||||
// import FullCalendar from "@fullcalendar/react";
|
import FullCalendar from "@fullcalendar/react";
|
||||||
// import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
// import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
// import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
// import { EventInput } from "@fullcalendar/core/index.js";
|
import { EventInput } from "@fullcalendar/core/index.js";
|
||||||
|
|
||||||
// --- Imports do EventManager (NOVO) - ADICIONADOS ---
|
// --- Imports do EventManager (NOVO) - ADICIONADOS ---
|
||||||
import { EventManager, type Event } from "@/components/event-manager";
|
import { EventManager, type Event } from "@/components/event-manager";
|
||||||
@ -41,8 +41,8 @@ export default function AgendamentoPage() {
|
|||||||
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");
|
||||||
|
|
||||||
// O 'requestsList' do FullCalendar foi removido.
|
// Estado para alimentar o FullCalendar (restaurado)
|
||||||
// const [requestsList, setRequestsList] = useState<EventInput[]>();
|
const [requestsList, setRequestsList] = useState<EventInput[]>([]);
|
||||||
|
|
||||||
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
|
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
|
||||||
|
|
||||||
@ -158,27 +158,31 @@ export default function AgendamentoPage() {
|
|||||||
// });
|
// });
|
||||||
// setRequestsList(events || []);
|
// setRequestsList(events || []);
|
||||||
|
|
||||||
// Convert to 3D calendar events (MANTIDO)
|
// Convert to 3D calendar events
|
||||||
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';
|
||||||
const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim();
|
const appointmentType = obj.appointment_type ?? obj.type ?? 'Consulta';
|
||||||
return {
|
const title = `${patient}: ${appointmentType}`.trim();
|
||||||
id: obj.id || String(Date.now()),
|
return {
|
||||||
title,
|
id: obj.id || String(Date.now()),
|
||||||
date: scheduled ? new Date(scheduled).toISOString() : new Date().toISOString(),
|
title,
|
||||||
};
|
date: scheduled ? new Date(scheduled).toISOString() : new Date().toISOString(),
|
||||||
});
|
status: obj.status || 'pending',
|
||||||
setThreeDEvents(threeDEvents);
|
patient,
|
||||||
} catch (err) {
|
type: appointmentType,
|
||||||
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
|
};
|
||||||
setAppointments([]);
|
});
|
||||||
// setRequestsList([]); // Removido
|
setThreeDEvents(threeDEvents);
|
||||||
setThreeDEvents([]);
|
} catch (err) {
|
||||||
}
|
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
|
||||||
})();
|
setAppointments([]);
|
||||||
return () => { mounted = false; };
|
setRequestsList([]);
|
||||||
}, []);
|
setThreeDEvents([]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handlers mantidos
|
// Handlers mantidos
|
||||||
const handleSaveAppointment = (appointment: any) => {
|
const handleSaveAppointment = (appointment: any) => {
|
||||||
@ -267,31 +271,45 @@ export default function AgendamentoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* --- AQUI ESTÁ A MUDANÇA --- */}
|
{activeTab === "calendar" ? (
|
||||||
{activeTab === "calendar" ? (
|
<div className="flex w-full">
|
||||||
<div className="flex w-full">
|
<FullCalendar
|
||||||
{/* O FullCalendar antigo foi substituído por este */}
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||||
<EventManager events={demoEvents} />
|
initialView="dayGridMonth"
|
||||||
</div>
|
locale={pt_br_locale}
|
||||||
) : activeTab === "3d" ? (
|
timeZone={"America/Sao_Paulo"}
|
||||||
// O calendário 3D foi mantido intacto
|
events={requestsList}
|
||||||
<div className="flex w-full">
|
headerToolbar={{
|
||||||
<ThreeDWallCalendar
|
left: "prev,next today",
|
||||||
events={threeDEvents}
|
center: "title",
|
||||||
onAddEvent={handleAddEvent}
|
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
||||||
onRemoveEvent={handleRemoveEvent}
|
}}
|
||||||
/>
|
dateClick={(info) => {
|
||||||
</div>
|
info.view.calendar.changeView("timeGridDay", info.dateStr);
|
||||||
) : (
|
}}
|
||||||
// A Lista de Espera foi mantida intacta
|
selectable={true}
|
||||||
<ListaEspera
|
selectMirror={true}
|
||||||
patients={waitingList}
|
dayMaxEvents={true}
|
||||||
onNotify={handleNotifyPatient}
|
dayMaxEventRows={3}
|
||||||
onAddToWaitlist={() => {}}
|
/>
|
||||||
/>
|
</div>
|
||||||
)}
|
) : activeTab === "3d" ? (
|
||||||
</div>
|
<div className="flex w-full justify-center">
|
||||||
</div>
|
<ThreeDWallCalendar
|
||||||
</div>
|
events={threeDEvents}
|
||||||
);
|
onAddEvent={handleAddEvent}
|
||||||
|
onRemoveEvent={handleRemoveEvent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ListaEspera
|
||||||
|
patients={waitingList}
|
||||||
|
onNotify={handleNotifyPatient}
|
||||||
|
onAddToWaitlist={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -4,9 +4,10 @@ import * as React from "react"
|
|||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
|
|
||||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card"
|
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 { v4 as uuidv4 } from "uuid"
|
||||||
import { startOfMonth, endOfMonth, eachDayOfInterval, format } from "date-fns"
|
import { startOfMonth, endOfMonth, eachDayOfInterval, format } from "date-fns"
|
||||||
import { ptBR } from "date-fns/locale"
|
import { ptBR } from "date-fns/locale"
|
||||||
@ -15,6 +16,9 @@ export type CalendarEvent = {
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
date: string // ISO
|
date: string // ISO
|
||||||
|
status?: 'confirmed' | 'pending' | 'cancelled' | string
|
||||||
|
patient?: string
|
||||||
|
type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThreeDWallCalendarProps {
|
interface ThreeDWallCalendarProps {
|
||||||
@ -37,6 +41,8 @@ export function ThreeDWallCalendar({
|
|||||||
const [dateRef, setDateRef] = React.useState<Date>(new Date())
|
const [dateRef, setDateRef] = React.useState<Date>(new Date())
|
||||||
const [title, setTitle] = React.useState("")
|
const [title, setTitle] = React.useState("")
|
||||||
const [newDate, setNewDate] = 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)
|
const wallRef = React.useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
// 3D tilt state
|
// 3D tilt state
|
||||||
@ -44,6 +50,7 @@ export function ThreeDWallCalendar({
|
|||||||
const [tiltY, setTiltY] = React.useState(0)
|
const [tiltY, setTiltY] = React.useState(0)
|
||||||
const isDragging = React.useRef(false)
|
const isDragging = React.useRef(false)
|
||||||
const dragStart = React.useRef<{ x: number; y: number } | null>(null)
|
const dragStart = React.useRef<{ x: number; y: number } | null>(null)
|
||||||
|
const hasDragged = React.useRef(false)
|
||||||
|
|
||||||
// month days
|
// month days
|
||||||
const days = eachDayOfInterval({
|
const days = eachDayOfInterval({
|
||||||
@ -54,6 +61,16 @@ export function ThreeDWallCalendar({
|
|||||||
const eventsForDay = (d: Date) =>
|
const eventsForDay = (d: Date) =>
|
||||||
events.filter((ev) => format(new Date(ev.date), "yyyy-MM-dd") === format(d, "yyyy-MM-dd"))
|
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
|
// Add event handler
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (!title.trim() || !newDate) return
|
if (!title.trim() || !newDate) return
|
||||||
@ -75,18 +92,26 @@ export function ThreeDWallCalendar({
|
|||||||
// drag tilt
|
// drag tilt
|
||||||
const onPointerDown = (e: React.PointerEvent) => {
|
const onPointerDown = (e: React.PointerEvent) => {
|
||||||
isDragging.current = true
|
isDragging.current = true
|
||||||
|
hasDragged.current = false
|
||||||
dragStart.current = { x: e.clientX, y: e.clientY }
|
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) => {
|
const onPointerMove = (e: React.PointerEvent) => {
|
||||||
if (!isDragging.current || !dragStart.current) return
|
if (!isDragging.current || !dragStart.current) return
|
||||||
const dx = e.clientX - dragStart.current.x
|
const dx = e.clientX - dragStart.current.x
|
||||||
const dy = e.clientY - dragStart.current.y
|
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)))
|
setTiltY((t) => Math.max(-60, Math.min(60, t + dx * 0.1)))
|
||||||
setTiltX((t) => Math.max(0, Math.min(60, t - dy * 0.1)))
|
setTiltX((t) => Math.max(0, Math.min(60, t - dy * 0.1)))
|
||||||
dragStart.current = { x: e.clientX, y: e.clientY }
|
dragStart.current = { x: e.clientX, y: e.clientY }
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPointerUp = () => {
|
const onPointerUp = () => {
|
||||||
isDragging.current = false
|
isDragging.current = false
|
||||||
dragStart.current = null
|
dragStart.current = null
|
||||||
@ -98,27 +123,53 @@ export function ThreeDWallCalendar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-4 items-center justify-between flex-wrap">
|
||||||
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1))}>
|
<div className="flex gap-2 items-center">
|
||||||
Mês Anterior
|
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1))}>
|
||||||
</Button>
|
Mês Anterior
|
||||||
<div className="font-semibold">{format(dateRef, "MMMM yyyy", { locale: ptBR })}</div>
|
</Button>
|
||||||
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1))}>
|
<div className="font-semibold text-lg">{format(dateRef, "MMMM yyyy", { locale: ptBR })}</div>
|
||||||
Próximo Mês
|
<Button onClick={() => setDateRef((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1))}>
|
||||||
</Button>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Wall container */}
|
{/* Wall container */}
|
||||||
<div
|
<div className="relative">
|
||||||
ref={wallRef}
|
<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">
|
||||||
onWheel={onWheel}
|
💡 Arraste para rotacionar • Scroll para inclinar
|
||||||
onPointerDown={onPointerDown}
|
</div>
|
||||||
onPointerMove={onPointerMove}
|
<div
|
||||||
onPointerUp={onPointerUp}
|
ref={wallRef}
|
||||||
onPointerCancel={onPointerUp}
|
onWheel={onWheel}
|
||||||
className="w-full overflow-auto"
|
onPointerDown={onPointerDown}
|
||||||
style={{ perspective: 1200 }}
|
onPointerMove={onPointerMove}
|
||||||
>
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={onPointerUp}
|
||||||
|
className="w-full overflow-auto"
|
||||||
|
style={{ perspective: 1200 }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="mx-auto"
|
className="mx-auto"
|
||||||
style={{
|
style={{
|
||||||
@ -148,71 +199,111 @@ export function ThreeDWallCalendar({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={day.toISOString()}
|
key={day.toISOString()}
|
||||||
className="relative"
|
className="relative cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
transform: `translateZ(${z}px)`,
|
transform: `translateZ(${z}px)`,
|
||||||
zIndex: Math.round(100 - Math.abs(rowOffset)),
|
zIndex: Math.round(100 - Math.abs(rowOffset)),
|
||||||
}}
|
}}
|
||||||
|
onClick={() => handleDayClick(day)}
|
||||||
>
|
>
|
||||||
<Card className="h-full overflow-visible">
|
<Card className="h-full overflow-visible hover:shadow-lg transition-shadow">
|
||||||
<CardContent className="p-3 h-full flex flex-col">
|
<CardContent className="p-2 h-full flex flex-col">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start mb-1">
|
||||||
<div className="text-xs font-medium">{format(day, "d")}</div>
|
<div className="text-sm font-medium">{format(day, "d")}</div>
|
||||||
<div className="text-xs text-muted-foreground">{format(day, "EEE", { locale: ptBR })}</div>
|
<div className="text-[9px] text-muted-foreground">
|
||||||
|
{dayEvents.length > 0 && `${dayEvents.length} ${dayEvents.length === 1 ? 'paciente' : 'pacientes'}`}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-[10px] text-muted-foreground mb-1">{format(day, "EEE", { locale: ptBR })}</div>
|
||||||
|
|
||||||
{/* events */}
|
{/* events */}
|
||||||
<div className="relative mt-2 flex-1">
|
<div className="relative flex-1 min-h-0">
|
||||||
{dayEvents.map((ev, i) => {
|
{dayEvents.map((ev, i) => {
|
||||||
const left = 8 + (i * 34) % (panelWidth - 40)
|
// Calcular tamanho da bolinha baseado na quantidade de eventos
|
||||||
const top = 8 + Math.floor((i * 34) / (panelWidth - 40)) * 28
|
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 (
|
return (
|
||||||
<Popover key={ev.id}>
|
<HoverCard key={ev.id} openDelay={100}>
|
||||||
<PopoverTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<HoverCard>
|
<div
|
||||||
<HoverCardTrigger asChild>
|
className={`absolute rounded-full ${getStatusColor()} flex items-center justify-center text-white cursor-pointer shadow-sm hover:shadow-md hover:scale-110 transition-all`}
|
||||||
<div
|
style={{
|
||||||
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"
|
left,
|
||||||
style={{ left, top, transform: `translateZ(20px)` }}
|
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)}
|
||||||
>
|
>
|
||||||
•
|
<Trash2 className="h-3 w-3 mr-1" />
|
||||||
</div>
|
Remover
|
||||||
</HoverCardTrigger>
|
</Button>
|
||||||
<HoverCardContent className="text-xs font-medium">
|
)}
|
||||||
{ev.title}
|
</div>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</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>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 text-xs text-muted-foreground">
|
|
||||||
{dayEvents.length} evento(s)
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -220,13 +311,101 @@ export function ThreeDWallCalendar({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 */}
|
{/* Add event form */}
|
||||||
<div className="flex gap-2 items-center">
|
<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)} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user