forked from RiseUP/riseup-squad20
remove ThreeDWallCalendar import/componente
This commit is contained in:
parent
add30c54a3
commit
b302bf1c66
@ -2,7 +2,6 @@
|
||||
|
||||
// Imports mantidos
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// --- Imports do EventManager (NOVO) - MANTIDOS ---
|
||||
import { EventManager, type Event } from "@/components/features/general/event-manager";
|
||||
@ -10,22 +9,21 @@ import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback
|
||||
|
||||
// Imports mantidos
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { mockWaitingList } from "@/lib/mocks/appointment-mocks";
|
||||
import "./index.css";
|
||||
import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"; // Calendário 3D mantido
|
||||
import { PatientRegistrationForm } from "@/components/features/forms/patient-registration-form";
|
||||
|
||||
const ListaEspera = dynamic(
|
||||
() => import("@/components/features/agendamento/ListaEspera"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function AgendamentoPage() {
|
||||
const { user, token } = useAuth();
|
||||
const [appointments, setAppointments] = useState<any[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar");
|
||||
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
|
||||
// REMOVIDO: abas e 3D → não há mais alternância de abas
|
||||
// const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar");
|
||||
|
||||
// REMOVIDO: estados do 3D e formulário do paciente (eram usados pelo 3D)
|
||||
// const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
|
||||
// const [showPatientForm, setShowPatientForm] = useState(false);
|
||||
|
||||
// --- NOVO ESTADO ---
|
||||
// Estado para alimentar o NOVO EventManager com dados da API
|
||||
const [managerEvents, setManagerEvents] = useState<Event[]>([]);
|
||||
const [managerLoading, setManagerLoading] = useState<boolean>(true);
|
||||
|
||||
// Padroniza idioma da página para pt-BR (afeta componentes que usam o lang do documento)
|
||||
useEffect(() => {
|
||||
@ -42,21 +40,6 @@ export default function AgendamentoPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// --- NOVO ESTADO ---
|
||||
// Estado para alimentar o NOVO EventManager com dados da API
|
||||
const [managerEvents, setManagerEvents] = useState<Event[]>([]);
|
||||
const [managerLoading, setManagerLoading] = useState<boolean>(true);
|
||||
|
||||
// Estado para o formulário de registro de paciente
|
||||
const [showPatientForm, setShowPatientForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "c") setActiveTab("calendar");
|
||||
if (event.key === "3") setActiveTab("3d");
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
@ -67,8 +50,8 @@ export default function AgendamentoPage() {
|
||||
if (!mounted) return;
|
||||
if (!arr || !arr.length) {
|
||||
setAppointments([]);
|
||||
setThreeDEvents([]);
|
||||
setManagerEvents([]); // Limpa o novo calendário
|
||||
// REMOVIDO: setThreeDEvents([])
|
||||
setManagerEvents([]);
|
||||
setManagerLoading(false);
|
||||
return;
|
||||
}
|
||||
@ -86,12 +69,11 @@ export default function AgendamentoPage() {
|
||||
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();
|
||||
|
||||
// Mapeamento de cores padronizado:
|
||||
// azul = solicitado; verde = confirmado; laranja = pendente; vermelho = cancelado; azul como fallback
|
||||
// Mapeamento de cores padronizado
|
||||
const status = String(obj.status || "").toLowerCase();
|
||||
let color: Event["color"] = "blue";
|
||||
if (status === "confirmed" || status === "confirmado") color = "green";
|
||||
@ -112,27 +94,12 @@ export default function AgendamentoPage() {
|
||||
setManagerLoading(false);
|
||||
// --- FIM DA LÓGICA ---
|
||||
|
||||
// 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);
|
||||
// REMOVIDO: conversão para 3D e setThreeDEvents
|
||||
} catch (err) {
|
||||
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
|
||||
setAppointments([]);
|
||||
setThreeDEvents([]);
|
||||
setManagerEvents([]); // Limpa o novo calendário
|
||||
// REMOVIDO: setThreeDEvents([])
|
||||
setManagerEvents([]);
|
||||
setManagerLoading(false);
|
||||
}
|
||||
})();
|
||||
@ -154,52 +121,22 @@ export default function AgendamentoPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddEvent = (event: CalendarEvent) => {
|
||||
setThreeDEvents((prev) => [...prev, event]);
|
||||
};
|
||||
|
||||
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 */}
|
||||
{/* Cabeçalho simplificado (sem 3D) */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{activeTab === "calendar" ? "Calendário" : activeTab === "3d" ? "Calendário 3D" : "Lista de Espera"}
|
||||
</h1>
|
||||
<h1 className="text-2xl font-bold text-foreground">Calendário</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).
|
||||
Navegue através do atalho: Calendário (C).
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:bg-primary! hover:text-white! transition-colors rounded-l-[100px] rounded-r-none"
|
||||
onClick={() => setActiveTab("calendar")}
|
||||
>
|
||||
Calendário
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant={"outline"}
|
||||
className="bg-muted hover:bg-primary! hover:text-white! transition-colors rounded-r-[100px] rounded-l-none"
|
||||
onClick={() => setActiveTab("3d")}
|
||||
>
|
||||
3D
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* REMOVIDO: botões de abas Calendário/3D */}
|
||||
</div>
|
||||
|
||||
{/* Legenda de status (estilo Google Calendar) */}
|
||||
{/* Legenda de status (aplica-se ao EventManager) */}
|
||||
<div className="rounded-md border bg-card/60 p-2 sm:p-3 -mt-4">
|
||||
<div className="flex flex-wrap items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
@ -210,49 +147,31 @@ export default function AgendamentoPage() {
|
||||
<span aria-hidden className="h-3 w-3 rounded-full bg-green-500 ring-2 ring-green-500/30" />
|
||||
<span className="text-foreground">Confirmado</span>
|
||||
</div>
|
||||
{/* Novo: Cancelado (vermelho) */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span aria-hidden className="h-3 w-3 rounded-full bg-red-500 ring-2 ring-red-500/30" />
|
||||
<span className="text-foreground">Cancelado</span>
|
||||
</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>
|
||||
{/* Apenas o EventManager */}
|
||||
<div className="flex w-full">
|
||||
<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>
|
||||
) : (
|
||||
<div className="w-full min-h-[70vh]">
|
||||
<EventManager events={managerEvents} className="compact-event-manager" />
|
||||
</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}
|
||||
onOpenAddPatientForm={() => setShowPatientForm(true)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulário de Registro de Paciente */}
|
||||
<PatientRegistrationForm
|
||||
open={showPatientForm}
|
||||
onOpenChange={setShowPatientForm}
|
||||
mode="create"
|
||||
onSaved={(newPaciente) => {
|
||||
console.log('[Calendar] Novo paciente registrado:', newPaciente);
|
||||
setShowPatientForm(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* REMOVIDO: PatientRegistrationForm (era acionado pelo 3D) */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,467 +0,0 @@
|
||||
"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
|
||||
onOpenAddPatientForm?: () => void
|
||||
panelWidth?: number
|
||||
panelHeight?: number
|
||||
columns?: number
|
||||
}
|
||||
|
||||
export function ThreeDWallCalendar({
|
||||
events,
|
||||
onAddEvent,
|
||||
onRemoveEvent,
|
||||
onOpenAddPatientForm,
|
||||
panelWidth = 160,
|
||||
panelHeight = 120,
|
||||
columns = 7,
|
||||
}: ThreeDWallCalendarProps) {
|
||||
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
|
||||
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)
|
||||
const clickStart = React.useRef<{ x: number; y: number } | null>(null)
|
||||
|
||||
// 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) => {
|
||||
console.log('Day clicked:', format(day, 'dd/MM/yyyy'))
|
||||
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
|
||||
// Reset hasDragged após um curto delay para permitir o clique ser processado
|
||||
setTimeout(() => {
|
||||
hasDragged.current = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const gap = 12
|
||||
const rowCount = Math.ceil(days.length / columns)
|
||||
const wallCenterRow = (rowCount - 1) / 2
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
{/* Botão Pacientes de hoje */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedDay(new Date())
|
||||
setIsDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
Pacientes de hoje
|
||||
</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 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, maxWidth: 1100 }}
|
||||
>
|
||||
<div
|
||||
className="mx-auto"
|
||||
style={{
|
||||
width: Math.max(700, columns * (panelWidth + gap)),
|
||||
transformStyle: "preserve-3d",
|
||||
transform: `rotateX(${tiltX}deg) rotateY(${tiltY}deg)`,
|
||||
transition: "transform 120ms linear",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${columns}, ${panelWidth}px)`,
|
||||
gridAutoRows: `${panelHeight}px`,
|
||||
gap: `${gap}px`,
|
||||
transformStyle: "preserve-3d",
|
||||
padding: gap,
|
||||
}}
|
||||
>
|
||||
{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 (
|
||||
<div
|
||||
key={day.toISOString()}
|
||||
className="relative cursor-pointer"
|
||||
style={{
|
||||
transform: `translateZ(${z}px)`,
|
||||
zIndex: Math.round(100 - Math.abs(rowOffset)),
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
clickStart.current = { x: e.clientX, y: e.clientY }
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
if (clickStart.current) {
|
||||
const dx = Math.abs(e.clientX - clickStart.current.x)
|
||||
const dy = Math.abs(e.clientY - clickStart.current.y)
|
||||
// Se moveu menos de 5 pixels, é um clique
|
||||
if (dx < 5 && dy < 5) {
|
||||
e.stopPropagation()
|
||||
handleDayClick(day)
|
||||
}
|
||||
clickStart.current = null
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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 flex-1 min-h-0">
|
||||
{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 (
|
||||
<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)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
Remover
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
{/* Navegação de dias */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedDay((prev) => prev ? new Date(prev.getFullYear(), prev.getMonth(), prev.getDate() - 1) : new Date())}
|
||||
aria-label="Dia anterior"
|
||||
>
|
||||
❮
|
||||
</Button>
|
||||
<DialogTitle className="text-xl">
|
||||
{selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })}
|
||||
</DialogTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedDay((prev) => prev ? new Date(prev.getFullYear(), prev.getMonth(), prev.getDate() + 1) : new Date())}
|
||||
aria-label="Próximo dia"
|
||||
>
|
||||
❯
|
||||
</Button>
|
||||
</div>
|
||||
<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">
|
||||
{onOpenAddPatientForm ? (
|
||||
<Button onClick={onOpenAddPatientForm} className="w-full">
|
||||
Adicionar Paciente
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<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 Paciente</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user