"use client" 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 { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card" 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" export type CalendarEvent = { id: string title: string date: string // ISO status?: 'confirmed' | 'pending' | 'cancelled' | string patient?: string type?: string } interface ThreeDWallCalendarProps { events: CalendarEvent[] onAddEvent?: (e: CalendarEvent) => void onRemoveEvent?: (id: string) => void panelWidth?: number panelHeight?: number columns?: number } export function ThreeDWallCalendar({ events, onAddEvent, onRemoveEvent, panelWidth = 160, panelHeight = 120, columns = 7, }: ThreeDWallCalendarProps) { const [dateRef, setDateRef] = React.useState(new Date()) const [title, setTitle] = React.useState("") const [newDate, setNewDate] = React.useState("") const [selectedDay, setSelectedDay] = React.useState(null) const [isDialogOpen, setIsDialogOpen] = React.useState(false) const wallRef = React.useRef(null) // 3D tilt state const [tiltX, setTiltX] = React.useState(18) 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({ start: startOfMonth(dateRef), end: endOfMonth(dateRef), }) 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 onAddEvent?.({ id: uuidv4(), title: title.trim(), date: new Date(newDate).toISOString(), }) setTitle("") setNewDate("") } // wheel tilt const onWheel = (e: React.WheelEvent) => { setTiltX((t) => Math.max(0, Math.min(50, t + e.deltaY * 0.02))) setTiltY((t) => Math.max(-45, Math.min(45, t + e.deltaX * 0.05))) } // 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) } 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 } const gap = 12 const rowCount = Math.ceil(days.length / columns) const wallCenterRow = (rowCount - 1) / 2 return (
{format(dateRef, "MMMM yyyy", { locale: ptBR })}
{/* Legenda de cores */}
Confirmado
Pendente
Cancelado
Outros
{/* Wall container */}
💡 Arraste para rotacionar • Scroll para inclinar
{days.map((day, idx) => { const row = Math.floor(idx / columns) const rowOffset = row - wallCenterRow const z = Math.max(-80, 40 - Math.abs(rowOffset) * 20) const dayEvents = eventsForDay(day) return (
handleDayClick(day)} >
{format(day, "d")}
{dayEvents.length > 0 && `${dayEvents.length} ${dayEvents.length === 1 ? 'paciente' : 'pacientes'}`}
{format(day, "EEE", { locale: ptBR })}
{/* events */}
{dayEvents.map((ev, i) => { // 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 (
{ev.title}
{ev.patient && ev.type && (
Paciente: {ev.patient}
Tipo: {ev.type}
)}
{format(new Date(ev.date), "PPP 'às' p", { locale: ptBR })}
{ev.status && (
Status:{' '} {ev.status === 'confirmed' ? 'Confirmado' : ev.status === 'pending' ? 'Pendente' : ev.status === 'cancelled' ? 'Cancelado' : ev.status}
)} {onRemoveEvent && ( )}
) })}
) })}
{/* Dialog de detalhes do dia */} {selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })} {selectedDayEvents.length} {selectedDayEvents.length === 1 ? 'paciente agendado' : 'pacientes agendados'}
{selectedDayEvents.length === 0 ? (
Nenhum paciente agendado para este dia
) : ( 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 (

{ev.patient || ev.title}

{ev.type && (
{ev.type}
)}
{format(new Date(ev.date), "HH:mm", { locale: ptBR })}
{getStatusText()}
{onRemoveEvent && ( )}
) }) )}
{/* Add event form */}
setTitle(e.target.value)} /> setNewDate(e.target.value)} />
) }