diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..788806d --- /dev/null +++ b/next.config.mjs @@ -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; diff --git a/susconecta/app/(main-routes)/calendar/index.css b/susconecta/app/(main-routes)/calendar/index.css index c631baa..776c418 100644 --- a/susconecta/app/(main-routes)/calendar/index.css +++ b/susconecta/app/(main-routes)/calendar/index.css @@ -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); -} \ No newline at end of file +} + +/* 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; } \ No newline at end of file diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index d0cfb55..45cee52 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -5,13 +5,13 @@ 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 FullCalendar (restaurados) --- +import pt_br_locale from "@fullcalendar/core/locales/pt-br"; +import FullCalendar from "@fullcalendar/react"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import interactionPlugin from "@fullcalendar/interaction"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import { EventInput } from "@fullcalendar/core/index.js"; // --- Imports do EventManager (NOVO) - ADICIONADOS --- import { EventManager, type Event } from "@/components/event-manager"; @@ -41,8 +41,8 @@ export default function AgendamentoPage() {   const [waitingList, setWaitingList] = useState(mockWaitingList);   const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar");   - // O 'requestsList' do FullCalendar foi removido. - // const [requestsList, setRequestsList] = useState(); + // Estado para alimentar o FullCalendar (restaurado) + const [requestsList, setRequestsList] = useState([]);   const [threeDEvents, setThreeDEvents] = useState([]); @@ -158,27 +158,31 @@ export default function AgendamentoPage() {         // });         // setRequestsList(events || []); -        // 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; }; -  }, []); + // Convert to 3D calendar events + 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([]); + setRequestsList([]); + setThreeDEvents([]); + } + })(); + return () => { mounted = false; }; + }, []);   // Handlers mantidos   const handleSaveAppointment = (appointment: any) => { @@ -267,31 +271,45 @@ export default function AgendamentoPage() {                       - {/* --- AQUI ESTÁ A MUDANÇA --- */} -          {activeTab === "calendar" ? ( -           
- {/* O FullCalendar antigo foi substituído por este */} - -           
-          ) : activeTab === "3d" ? ( - // O calendário 3D foi mantido intacto -           
-              -           
-          ) : ( - // A Lista de Espera foi mantida intacta -            {}} -            /> -          )} -        -      -    -  ); + {activeTab === "calendar" ? ( +
+ { + info.view.calendar.changeView("timeGridDay", info.dateStr); + }} + selectable={true} + selectMirror={true} + dayMaxEvents={true} + dayMaxEventRows={3} + /> +
+ ) : activeTab === "3d" ? ( +
+ +
+ ) : ( + {}} + /> + )} + + + + ); } \ No newline at end of file diff --git a/susconecta/components/ui/three-dwall-calendar.tsx b/susconecta/components/ui/three-dwall-calendar.tsx index dc3cc90..2d671f8 100644 --- a/susconecta/components/ui/three-dwall-calendar.tsx +++ b/susconecta/components/ui/three-dwall-calendar.tsx @@ -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(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 @@ -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 (
-
- -
{format(dateRef, "MMMM yyyy", { locale: ptBR })}
- +
+
+ +
{format(dateRef, "MMMM yyyy", { locale: ptBR })}
+ +
+ + {/* Legenda de cores */} +
+
+
+ Confirmado +
+
+
+ Pendente +
+
+
+ Cancelado +
+
+
+ Outros +
+
{/* Wall container */} -
+
+
+ 💡 Arraste para rotacionar • Scroll para inclinar +
+
handleDayClick(day)} > - - -
-
{format(day, "d")}
-
{format(day, "EEE", { locale: ptBR })}
+ + +
+
{format(day, "d")}
+
+ {dayEvents.length > 0 && `${dayEvents.length} ${dayEvents.length === 1 ? 'paciente' : 'pacientes'}`} +
+
{format(day, "EEE", { locale: ptBR })}
{/* events */} -
+
{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 ( - - - - -
+ +
+ • +
+
+ +
+
{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 && ( +
- - - {ev.title} - - - - - - -
-
{ev.title}
-
- {format(new Date(ev.date), "PPP p", { locale: ptBR })} -
-
- {onRemoveEvent && ( - - )} -
-
-
- + + Remover + + )} +
+ +
) })}
- -
- {dayEvents.length} evento(s) -
@@ -220,13 +311,101 @@ export function ThreeDWallCalendar({ })}
+
+ {/* 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)} /> + setTitle(e.target.value)} /> setNewDate(e.target.value)} /> - +
)