diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index c4afdea..a6d3b5c 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -2,39 +2,36 @@
// Imports mantidos
import { useEffect, useState } from "react";
-import dynamic from "next/dynamic";
-import Link from "next/link";
// --- Imports do EventManager (NOVO) - MANTIDOS ---
import { EventManager, type Event } from "@/components/features/general/event-manager";
import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback
// Imports mantidos
-import { Sidebar } from "@/components/layout/sidebar";
-import { PagesHeader } from "@/components/features/dashboard/header";
import { Button } from "@/components/ui/button";
-import { mockWaitingList } from "@/lib/mocks/appointment-mocks";
import "./index.css";
-import {
- 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/features/agendamento/ListaEspera"),
- { ssr: false }
-);
-
export default function AgendamentoPage() {
const [appointments, setAppointments] = useState([]);
- const [waitingList, setWaitingList] = useState(mockWaitingList);
- const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar");
-
+ const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar");
const [threeDEvents, setThreeDEvents] = useState([]);
+ // Padroniza idioma da página para pt-BR (afeta componentes que usam o lang do documento)
+ useEffect(() => {
+ try {
+ // Atributos no
+ document.documentElement.lang = "pt-BR";
+ document.documentElement.setAttribute("xml:lang", "pt-BR");
+ document.documentElement.setAttribute("data-lang", "pt-BR");
+ // Cookie de locale (usado por apps com i18n)
+ const oneYear = 60 * 60 * 24 * 365;
+ document.cookie = `NEXT_LOCALE=pt-BR; Path=/; Max-Age=${oneYear}; SameSite=Lax`;
+ } catch {
+ // ignore
+ }
+ }, []);
+
// --- NOVO ESTADO ---
// Estado para alimentar o NOVO EventManager com dados da API
const [managerEvents, setManagerEvents] = useState([]);
@@ -42,15 +39,8 @@ export default function AgendamentoPage() {
useEffect(() => {
document.addEventListener("keydown", (event) => {
- if (event.key === "c") {
- setActiveTab("calendar");
- }
- if (event.key === "f") {
- setActiveTab("espera");
- }
- if (event.key === "3") {
- setActiveTab("3d");
- }
+ if (event.key === "c") setActiveTab("calendar");
+ if (event.key === "3") setActiveTab("3d");
});
}, []);
@@ -86,18 +76,23 @@ export default function AgendamentoPage() {
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';
+
+ // Mapeamento de cores padronizado:
+ // azul = solicitado; verde = confirmado; laranja = pendente; vermelho = cancelado; azul como fallback
+ const status = String(obj.status || "").toLowerCase();
+ let color: Event["color"] = "blue";
+ if (status === "confirmed" || status === "confirmado") color = "green";
+ else if (status === "pending" || status === "pendente") color = "orange";
+ else if (status === "canceled" || status === "cancelado" || status === "cancelled") color = "red";
+ else if (status === "requested" || status === "solicitado") color = "blue";
return {
- id: obj.id || uuidv4(), // Usa ID da API ou gera um
- title: title,
- description: `Agendamento para ${patient}. Status: ${obj.status || 'N/A'}.`,
+ id: obj.id || uuidv4(),
+ title,
+ description: `Agendamento para ${patient}. Status: ${obj.status || 'N/A'}.`,
startTime: start,
endTime: end,
- color: color,
+ color,
};
});
setManagerEvents(newManagerEvents);
@@ -146,10 +141,6 @@ export default function AgendamentoPage() {
}
};
- const handleNotifyPatient = (patientId: string) => {
- console.log(`Notificando paciente ${patientId}`);
- };
-
const handleAddEvent = (event: CalendarEvent) => {
setThreeDEvents((prev) => [...prev, event]);
};
@@ -172,26 +163,10 @@ export default function AgendamentoPage() {
Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).
-
-
-
- Opções »
-
-
-
- Agendamento
-
-
- Procedimento
-
-
- Financeiro
-
-
-
-
+
+
+
+
-
+ {/* Legenda de status (estilo Google Calendar) */}
+
+
+
+
+ Solicitado
+
+
+
+ Confirmado
@@ -244,14 +226,7 @@ export default function AgendamentoPage() {
onRemoveEvent={handleRemoveEvent}
/>
- ) : (
- // A Lista de Espera foi MANTIDA
- {}}
- />
- )}
+ ) : null}
diff --git a/susconecta/components/event-manager.tsx b/susconecta/components/event-manager.tsx
new file mode 100644
index 0000000..1a19417
--- /dev/null
+++ b/susconecta/components/event-manager.tsx
@@ -0,0 +1,1485 @@
+"use client"
+
+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"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Grid3x3, List, Search, Filter, X } from "lucide-react"
+import { cn } from "@/lib/utils"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuCheckboxItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+export interface Event {
+ id: string
+ title: string
+ description?: string
+ startTime: Date
+ endTime: Date
+ color: string
+ category?: string
+ attendees?: string[]
+ tags?: string[]
+}
+
+export interface EventManagerProps {
+ events?: Event[]
+ onEventCreate?: (event: Omit) => void
+ onEventUpdate?: (id: string, event: Partial) => void
+ onEventDelete?: (id: string) => void
+ categories?: string[]
+ colors?: { name: string; value: string; bg: string; text: string }[]
+ defaultView?: "month" | "week" | "day" | "list"
+ className?: string
+ availableTags?: string[]
+}
+
+const defaultColors = [
+ { name: "Blue", value: "blue", bg: "bg-blue-500", text: "text-blue-700" },
+ { name: "Green", value: "green", bg: "bg-green-500", text: "text-green-700" },
+ { name: "Purple", value: "purple", bg: "bg-purple-500", text: "text-purple-700" },
+ { name: "Orange", value: "orange", bg: "bg-orange-500", text: "text-orange-700" },
+ { name: "Pink", value: "pink", bg: "bg-pink-500", text: "text-pink-700" },
+ { name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" },
+]
+
+export function EventManager({
+ events: initialEvents = [],
+ onEventCreate,
+ onEventUpdate,
+ onEventDelete,
+ categories = ["Meeting", "Task", "Reminder", "Personal"],
+ colors = defaultColors,
+ defaultView = "month",
+ className,
+ availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
+}: EventManagerProps) {
+ const [events, setEvents] = useState(initialEvents)
+ const [currentDate, setCurrentDate] = useState(new Date())
+ const [view, setView] = useState<"month" | "week" | "day" | "list">(defaultView)
+ const [selectedEvent, setSelectedEvent] = useState(null)
+ const [isDialogOpen, setIsDialogOpen] = useState(false)
+ const [isCreating, setIsCreating] = useState(false)
+ const [draggedEvent, setDraggedEvent] = useState(null)
+ const [newEvent, setNewEvent] = useState>({
+ title: "",
+ description: "",
+ color: colors[0].value,
+ category: categories[0],
+ tags: [],
+ })
+
+ const [searchQuery, setSearchQuery] = useState("")
+ const [selectedColors, setSelectedColors] = useState([])
+ const [selectedTags, setSelectedTags] = useState([])
+ const [selectedCategories, setSelectedCategories] = useState([])
+
+ const filteredEvents = useMemo(() => {
+ return events.filter((event) => {
+ // Search filter
+ if (searchQuery) {
+ const query = searchQuery.toLowerCase()
+ const matchesSearch =
+ event.title.toLowerCase().includes(query) ||
+ event.description?.toLowerCase().includes(query) ||
+ event.category?.toLowerCase().includes(query) ||
+ event.tags?.some((tag) => tag.toLowerCase().includes(query))
+
+ if (!matchesSearch) return false
+ }
+
+ // Color filter
+ if (selectedColors.length > 0 && !selectedColors.includes(event.color)) {
+ return false
+ }
+
+ // Tag filter
+ if (selectedTags.length > 0) {
+ const hasMatchingTag = event.tags?.some((tag) => selectedTags.includes(tag))
+ if (!hasMatchingTag) return false
+ }
+
+ // Category filter
+ if (selectedCategories.length > 0 && event.category && !selectedCategories.includes(event.category)) {
+ return false
+ }
+
+ return true
+ })
+ }, [events, searchQuery, selectedColors, selectedTags, selectedCategories])
+
+ const hasActiveFilters = selectedColors.length > 0 || selectedTags.length > 0 || selectedCategories.length > 0
+
+ const clearFilters = () => {
+ setSelectedColors([])
+ setSelectedTags([])
+ setSelectedCategories([])
+ setSearchQuery("")
+ }
+
+ const handleCreateEvent = useCallback(() => {
+ if (!newEvent.title || !newEvent.startTime || !newEvent.endTime) return
+
+ const event: Event = {
+ id: Math.random().toString(36).substr(2, 9),
+ title: newEvent.title,
+ description: newEvent.description,
+ startTime: newEvent.startTime,
+ endTime: newEvent.endTime,
+ color: newEvent.color || colors[0].value,
+ category: newEvent.category,
+ attendees: newEvent.attendees,
+ tags: newEvent.tags || [],
+ }
+
+ setEvents((prev) => [...prev, event])
+ onEventCreate?.(event)
+ setIsDialogOpen(false)
+ setIsCreating(false)
+ setNewEvent({
+ title: "",
+ description: "",
+ color: colors[0].value,
+ category: categories[0],
+ tags: [],
+ })
+ }, [newEvent, colors, categories, onEventCreate])
+
+ const handleUpdateEvent = useCallback(() => {
+ if (!selectedEvent) return
+
+ setEvents((prev) => prev.map((e) => (e.id === selectedEvent.id ? selectedEvent : e)))
+ onEventUpdate?.(selectedEvent.id, selectedEvent)
+ setIsDialogOpen(false)
+ setSelectedEvent(null)
+ }, [selectedEvent, onEventUpdate])
+
+ const handleDeleteEvent = useCallback(
+ (id: string) => {
+ setEvents((prev) => prev.filter((e) => e.id !== id))
+ onEventDelete?.(id)
+ setIsDialogOpen(false)
+ setSelectedEvent(null)
+ },
+ [onEventDelete],
+ )
+
+ const handleDragStart = useCallback((event: Event) => {
+ setDraggedEvent(event)
+ }, [])
+
+ const handleDragEnd = useCallback(() => {
+ setDraggedEvent(null)
+ }, [])
+
+ const handleDrop = useCallback(
+ (date: Date, hour?: number) => {
+ if (!draggedEvent) return
+
+ const duration = draggedEvent.endTime.getTime() - draggedEvent.startTime.getTime()
+ const newStartTime = new Date(date)
+ if (hour !== undefined) {
+ newStartTime.setHours(hour, 0, 0, 0)
+ }
+ const newEndTime = new Date(newStartTime.getTime() + duration)
+
+ const updatedEvent = {
+ ...draggedEvent,
+ startTime: newStartTime,
+ endTime: newEndTime,
+ }
+
+ setEvents((prev) => prev.map((e) => (e.id === draggedEvent.id ? updatedEvent : e)))
+ onEventUpdate?.(draggedEvent.id, updatedEvent)
+ setDraggedEvent(null)
+ },
+ [draggedEvent, onEventUpdate],
+ )
+
+ const navigateDate = useCallback(
+ (direction: "prev" | "next") => {
+ setCurrentDate((prev) => {
+ const newDate = new Date(prev)
+ if (view === "month") {
+ newDate.setMonth(prev.getMonth() + (direction === "next" ? 1 : -1))
+ } else if (view === "week") {
+ newDate.setDate(prev.getDate() + (direction === "next" ? 7 : -7))
+ } else if (view === "day") {
+ newDate.setDate(prev.getDate() + (direction === "next" ? 1 : -1))
+ }
+ return newDate
+ })
+ },
+ [view],
+ )
+
+ const getColorClasses = useCallback(
+ (colorValue: string) => {
+ const color = colors.find((c) => c.value === colorValue)
+ return color || colors[0]
+ },
+ [colors],
+ )
+
+ const toggleTag = (tag: string, isCreating: boolean) => {
+ if (isCreating) {
+ setNewEvent((prev) => ({
+ ...prev,
+ tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag],
+ }))
+ } else {
+ setSelectedEvent((prev) =>
+ prev
+ ? {
+ ...prev,
+ tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag],
+ }
+ : null,
+ )
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ {view === "month" &&
+ currentDate.toLocaleDateString("pt-BR", {
+ month: "long",
+ year: "numeric",
+ })}
+ {view === "week" &&
+ `Semana de ${currentDate.toLocaleDateString("pt-BR", {
+ month: "short",
+ day: "numeric",
+ })}`}
+ {view === "day" &&
+ currentDate.toLocaleDateString("pt-BR", {
+ weekday: "long",
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ })}
+ {view === "list" && "Todos os eventos"}
+
+
+
+
+
+
+
+
+
+ {/* Mobile: Select dropdown */}
+
+
+
+
+ {/* Desktop: Button group */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-9"
+ />
+ {searchQuery && (
+
+ )}
+
+
+ {/* Mobile: Horizontal scroll with full-length buttons */}
+
+
+ {/* Color Filter */}
+
+
+
+
+
+ Filtrar por Cor
+
+ {colors.map((color) => (
+ {
+ setSelectedColors((prev) =>
+ checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
+ )
+ }}
+ >
+
+
+ ))}
+
+
+
+ {/* Tag Filter */}
+
+
+
+
+
+ Filtrar por Tag
+
+ {availableTags.map((tag) => (
+ {
+ setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
+ }}
+ >
+ {tag}
+
+ ))}
+
+
+
+ {/* Category Filter */}
+
+
+
+
+
+ Filtrar por Categoria
+
+ {categories.map((category) => (
+ {
+ setSelectedCategories((prev) =>
+ checked ? [...prev, category] : prev.filter((c) => c !== category),
+ )
+ }}
+ >
+ {category}
+
+ ))}
+
+
+
+ {hasActiveFilters && (
+
+ )}
+
+
+
+ {/* Desktop: Original layout */}
+
+ {/* Color Filter */}
+
+
+
+
+
+ Filtrar por Cor
+
+ {colors.map((color) => (
+ {
+ setSelectedColors((prev) =>
+ checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
+ )
+ }}
+ >
+
+
+ ))}
+
+
+
+ {/* Tag Filter */}
+
+
+
+
+
+ Filtrar por Tag
+
+ {availableTags.map((tag) => (
+ {
+ setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
+ }}
+ >
+ {tag}
+
+ ))}
+
+
+
+ {/* Category Filter */}
+
+
+
+
+
+ Filtrar por Categoria
+
+ {categories.map((category) => (
+ {
+ setSelectedCategories((prev) =>
+ checked ? [...prev, category] : prev.filter((c) => c !== category),
+ )
+ }}
+ >
+ {category}
+
+ ))}
+
+
+
+ {hasActiveFilters && (
+
+ )}
+
+
+
+ {hasActiveFilters && (
+
+
Filtros ativos:
+ {selectedColors.map((colorValue) => {
+ const color = getColorClasses(colorValue)
+ return (
+
+
+ {color.name}
+
+
+ )
+ })}
+ {selectedTags.map((tag) => (
+
+ {tag}
+
+
+ ))}
+ {selectedCategories.map((category) => (
+
+ {category}
+
+
+ ))}
+
+ )}
+
+ {/* Calendar Views - Pass filteredEvents instead of events */}
+ {view === "month" && (
+
{
+ setSelectedEvent(event)
+ setIsDialogOpen(true)
+ }}
+ onDragStart={(event) => handleDragStart(event)}
+ onDragEnd={() => handleDragEnd()}
+ onDrop={handleDrop}
+ getColorClasses={getColorClasses}
+ />
+ )}
+
+ {view === "week" && (
+ {
+ setSelectedEvent(event)
+ setIsDialogOpen(true)
+ }}
+ onDragStart={(event) => handleDragStart(event)}
+ onDragEnd={() => handleDragEnd()}
+ onDrop={handleDrop}
+ getColorClasses={getColorClasses}
+ />
+ )}
+
+ {view === "day" && (
+ {
+ setSelectedEvent(event)
+ setIsDialogOpen(true)
+ }}
+ onDragStart={(event) => handleDragStart(event)}
+ onDragEnd={() => handleDragEnd()}
+ onDrop={handleDrop}
+ getColorClasses={getColorClasses}
+ />
+ )}
+
+ {view === "list" && (
+ {
+ setSelectedEvent(event)
+ setIsDialogOpen(true)
+ }}
+ getColorClasses={getColorClasses}
+ />
+ )}
+
+ {/* Event Dialog */}
+
+
+ )
+}
+
+// EventCard component with hover effect
+function EventCard({
+ event,
+ onEventClick,
+ onDragStart,
+ onDragEnd,
+ getColorClasses,
+ variant = "default",
+}: {
+ event: Event
+ onEventClick: (event: Event) => void
+ onDragStart: (event: Event) => void
+ onDragEnd: () => void
+ getColorClasses: (color: string) => { bg: string; text: string }
+ variant?: "default" | "compact" | "detailed"
+}) {
+ const [isHovered, setIsHovered] = useState(false)
+ const colorClasses = getColorClasses(event.color)
+
+ const formatTime = (date: Date) => {
+ return date.toLocaleTimeString("en-US", {
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ }
+
+ const getDuration = () => {
+ const diff = event.endTime.getTime() - event.startTime.getTime()
+ const hours = Math.floor(diff / (1000 * 60 * 60))
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`
+ }
+ return `${minutes}m`
+ }
+
+ if (variant === "compact") {
+ return (
+ onDragStart(event)}
+ onDragEnd={onDragEnd}
+ onClick={() => onEventClick(event)}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ className="relative cursor-pointer"
+ >
+
+ {event.title}
+
+ {isHovered && (
+
+
+
+
+ {event.description &&
{event.description}
}
+
+
+
+ {formatTime(event.startTime)} - {formatTime(event.endTime)}
+
+ ({getDuration()})
+
+
+ {event.category && (
+
+ {event.category}
+
+ )}
+ {event.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+ )}
+
+ )
+ }
+
+ if (variant === "detailed") {
+ return (
+ onDragStart(event)}
+ onDragEnd={onDragEnd}
+ onClick={() => onEventClick(event)}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ className={cn(
+ "cursor-pointer rounded-lg p-3 transition-all duration-300",
+ colorClasses.bg,
+ "text-white animate-in fade-in slide-in-from-left-2",
+ isHovered && "scale-[1.03] shadow-2xl ring-2 ring-white/50",
+ )}
+ >
+
{event.title}
+ {event.description &&
{event.description}
}
+
+
+ {formatTime(event.startTime)} - {formatTime(event.endTime)}
+
+ {isHovered && (
+
+ {event.category && (
+
+ {event.category}
+
+ )}
+ {event.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+ )
+ }
+
+ return (
+ onDragStart(event)}
+ onDragEnd={onDragEnd}
+ onClick={() => onEventClick(event)}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ className="relative"
+ >
+
+ {isHovered && (
+
+
+
+
+ {event.description &&
{event.description}
}
+
+
+
+
+ {formatTime(event.startTime)} - {formatTime(event.endTime)}
+
+ ({getDuration()})
+
+
+ {event.category && (
+
+ {event.category}
+
+ )}
+ {event.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+
+ )}
+
+ )
+}
+
+// Month View Component
+function MonthView({
+ currentDate,
+ events,
+ onEventClick,
+ onDragStart,
+ onDragEnd,
+ onDrop,
+ getColorClasses,
+}: {
+ currentDate: Date
+ events: Event[]
+ onEventClick: (event: Event) => void
+ onDragStart: (event: Event) => void
+ onDragEnd: () => void
+ onDrop: (date: Date) => void
+ getColorClasses: (color: string) => { bg: string; text: string }
+}) {
+ const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
+ const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0)
+ const startDate = new Date(firstDayOfMonth)
+ startDate.setDate(startDate.getDate() - startDate.getDay())
+
+ const days = []
+ const currentDay = new Date(startDate)
+
+ for (let i = 0; i < 42; i++) {
+ days.push(new Date(currentDay))
+ currentDay.setDate(currentDay.getDate() + 1)
+ }
+
+ const getEventsForDay = (date: Date) => {
+ return events.filter((event) => {
+ const eventDate = new Date(event.startTime)
+ return (
+ eventDate.getDate() === date.getDate() &&
+ eventDate.getMonth() === date.getMonth() &&
+ eventDate.getFullYear() === date.getFullYear()
+ )
+ })
+ }
+
+ return (
+
+
+ {["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
+
+ {day}
+ {day.charAt(0)}
+
+ ))}
+
+
+ {days.map((day, index) => {
+ const dayEvents = getEventsForDay(day)
+ const isCurrentMonth = day.getMonth() === currentDate.getMonth()
+ const isToday = day.toDateString() === new Date().toDateString()
+
+ return (
+
e.preventDefault()}
+ onDrop={() => onDrop(day)}
+ >
+
+ {day.getDate()}
+
+
+ {dayEvents.slice(0, 3).map((event) => (
+
+ ))}
+ {dayEvents.length > 3 && (
+
+{dayEvents.length - 3} mais
+ )}
+
+
+ )
+ })}
+
+
+ )
+}
+
+// Week View Component
+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
+ )
+ })
+ }
+
+ return (
+
+
+
Hora
+ {weekDays.map((day) => (
+
+
{day.toLocaleDateString("pt-BR", { weekday: "short" })}
+
{day.toLocaleDateString("pt-BR", { weekday: "narrow" })}
+
+ {day.toLocaleDateString("pt-BR", { month: "short", day: "numeric" })}
+
+
+ ))}
+
+
+ {hours.map((hour) => (
+ <>
+
+ {hour.toString().padStart(2, "0")}:00
+
+ {weekDays.map((day) => {
+ const dayEvents = getEventsForDayAndHour(day, hour)
+ return (
+
e.preventDefault()}
+ onDrop={() => onDrop(day, hour)}
+ >
+
+ {dayEvents.map((event) => (
+
+ ))}
+
+
+ )
+ })}
+ >
+ ))}
+
+
+ )
+}
+
+// Day View Component
+function DayView({
+ 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 hours = Array.from({ length: 24 }, (_, i) => i)
+
+ const getEventsForHour = (hour: number) => {
+ return events.filter((event) => {
+ const eventDate = new Date(event.startTime)
+ const eventHour = eventDate.getHours()
+ return (
+ eventDate.getDate() === currentDate.getDate() &&
+ eventDate.getMonth() === currentDate.getMonth() &&
+ eventDate.getFullYear() === currentDate.getFullYear() &&
+ eventHour === hour
+ )
+ })
+ }
+
+ return (
+
+
+ {hours.map((hour) => {
+ const hourEvents = getEventsForHour(hour)
+ return (
+
e.preventDefault()}
+ onDrop={() => onDrop(currentDate, hour)}
+ >
+
+ {hour.toString().padStart(2, "0")}:00
+
+
+
+ {hourEvents.map((event) => (
+
+ ))}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+// List View Component
+function ListView({
+ events,
+ onEventClick,
+ getColorClasses,
+}: {
+ events: Event[]
+ onEventClick: (event: Event) => void
+ getColorClasses: (color: string) => { bg: string; text: string }
+}) {
+ const sortedEvents = [...events].sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
+
+ const groupedEvents = sortedEvents.reduce(
+ (acc, event) => {
+ const dateKey = event.startTime.toLocaleDateString("pt-BR", {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ })
+ if (!acc[dateKey]) {
+ acc[dateKey] = []
+ }
+ acc[dateKey].push(event)
+ return acc
+ },
+ {} as Record,
+ )
+
+ return (
+
+
+ {Object.entries(groupedEvents).map(([date, dateEvents]) => (
+
+
{date}
+
+ {dateEvents.map((event) => {
+ const colorClasses = getColorClasses(event.color)
+ return (
+
onEventClick(event)}
+ className="group cursor-pointer rounded-lg border bg-card p-3 transition-all hover:shadow-md hover:scale-[1.01] animate-in fade-in slide-in-from-bottom-2 duration-300 sm:p-4"
+ >
+
+
+
+
+
+
+ {event.title}
+
+ {event.description && (
+
+ {event.description}
+
+ )}
+
+
+ {event.category && (
+
+ {event.category}
+
+ )}
+
+
+
+
+
+ {event.startTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} - {event.endTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}
+
+ {event.tags && event.tags.length > 0 && (
+
+ {event.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+
+ )
+ })}
+
+
+ ))}
+ {sortedEvents.length === 0 && (
+
Nenhum evento encontrado
+ )}
+
+
+ )
+}
diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx
index 1a19417..697b2e6 100644
--- a/susconecta/components/features/general/event-manager.tsx
+++ b/susconecta/components/features/general/event-manager.tsx
@@ -1,6 +1,6 @@
"use client"
-import React, { useState, useCallback, useMemo } from "react"
+import React, { useState, useCallback, useMemo, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
@@ -16,16 +16,8 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
-import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Grid3x3, List, Search, Filter, X } from "lucide-react"
+import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Grid3x3, List, Search, X } from "lucide-react"
import { cn } from "@/lib/utils"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuCheckboxItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
export interface Event {
id: string
@@ -60,6 +52,10 @@ const defaultColors = [
{ name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" },
]
+// Locale/timezone padrão BR
+const LOCALE = "pt-BR"
+const TIMEZONE = "America/Sao_Paulo"
+
export function EventManager({
events: initialEvents = [],
onEventCreate,
@@ -87,13 +83,19 @@ export function EventManager({
})
const [searchQuery, setSearchQuery] = useState("")
- const [selectedColors, setSelectedColors] = useState([])
- const [selectedTags, setSelectedTags] = useState([])
- const [selectedCategories, setSelectedCategories] = useState([])
+
+ // Dialog: lista completa de pacientes do dia
+ const [dayDialogEvents, setDayDialogEvents] = useState(null)
+ const [isDayDialogOpen, setIsDayDialogOpen] = useState(false)
+ const openDayDialog = useCallback((eventsForDay: Event[]) => {
+ // ordena por horário antes de abrir
+ const ordered = [...eventsForDay].sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
+ setDayDialogEvents(ordered)
+ setIsDayDialogOpen(true)
+ }, [])
const filteredEvents = useMemo(() => {
return events.filter((event) => {
- // Search filter
if (searchQuery) {
const query = searchQuery.toLowerCase()
const matchesSearch =
@@ -101,36 +103,15 @@ export function EventManager({
event.description?.toLowerCase().includes(query) ||
event.category?.toLowerCase().includes(query) ||
event.tags?.some((tag) => tag.toLowerCase().includes(query))
-
if (!matchesSearch) return false
}
-
- // Color filter
- if (selectedColors.length > 0 && !selectedColors.includes(event.color)) {
- return false
- }
-
- // Tag filter
- if (selectedTags.length > 0) {
- const hasMatchingTag = event.tags?.some((tag) => selectedTags.includes(tag))
- if (!hasMatchingTag) return false
- }
-
- // Category filter
- if (selectedCategories.length > 0 && event.category && !selectedCategories.includes(event.category)) {
- return false
- }
-
return true
})
- }, [events, searchQuery, selectedColors, selectedTags, selectedCategories])
+ }, [events, searchQuery])
- const hasActiveFilters = selectedColors.length > 0 || selectedTags.length > 0 || selectedCategories.length > 0
+ const hasActiveFilters = false
const clearFilters = () => {
- setSelectedColors([])
- setSelectedTags([])
- setSelectedCategories([])
setSearchQuery("")
}
@@ -238,23 +219,16 @@ export function EventManager({
[colors],
)
- const toggleTag = (tag: string, isCreating: boolean) => {
- if (isCreating) {
- setNewEvent((prev) => ({
- ...prev,
- tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag],
- }))
- } else {
- setSelectedEvent((prev) =>
- prev
- ? {
- ...prev,
- tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag],
- }
- : null,
- )
- }
- }
+ // Força lang/cookie pt-BR no documento (reforço local)
+ useEffect(() => {
+ try {
+ document.documentElement.lang = "pt-BR"
+ document.documentElement.setAttribute("xml:lang", "pt-BR")
+ document.documentElement.setAttribute("data-lang", "pt-BR")
+ const oneYear = 60 * 60 * 24 * 365
+ document.cookie = `NEXT_LOCALE=pt-BR; Path=/; Max-Age=${oneYear}; SameSite=Lax`
+ } catch {}
+ }, [])
return (
@@ -263,21 +237,24 @@ export function EventManager({
{view === "month" &&
- currentDate.toLocaleDateString("pt-BR", {
+ currentDate.toLocaleDateString(LOCALE, {
month: "long",
year: "numeric",
+ timeZone: TIMEZONE,
})}
{view === "week" &&
- `Semana de ${currentDate.toLocaleDateString("pt-BR", {
+ `Semana de ${currentDate.toLocaleDateString(LOCALE, {
month: "short",
day: "numeric",
+ timeZone: TIMEZONE,
})}`}
{view === "day" &&
- currentDate.toLocaleDateString("pt-BR", {
+ currentDate.toLocaleDateString(LOCALE, {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
+ timeZone: TIMEZONE,
})}
{view === "list" && "Todos os eventos"}
@@ -285,9 +262,6 @@ export function EventManager({
-
@@ -385,290 +359,46 @@ export function EventManager({
-
-
setSearchQuery(e.target.value)}
- className="pl-9"
- />
- {searchQuery && (
-
- {/* Mobile: Horizontal scroll with full-length buttons */}
-
-
- {/* Color Filter */}
-
-
-
-
- Cores
- {selectedColors.length > 0 && (
-
- {selectedColors.length}
-
- )}
-
-
-
- Filtrar por Cor
-
- {colors.map((color) => (
- {
- setSelectedColors((prev) =>
- checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
- )
- }}
- >
-
-
- ))}
-
-
+ {/* Input central com altura consistente e foco visível */}
+
setSearchQuery(e.target.value)}
+ className={cn(
+ "flex-1 h-10 px-3 border border-border focus:ring-2 focus:ring-primary/20 outline-none",
+ searchQuery ? "rounded-l-md rounded-r-none" : "rounded-md"
+ )}
+ />
- {/* Tag Filter */}
-
-
-
-
- Tags
- {selectedTags.length > 0 && (
-
- {selectedTags.length}
-
- )}
-
-
-
- Filtrar por Tag
-
- {availableTags.map((tag) => (
- {
- setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
- }}
- >
- {tag}
-
- ))}
-
-
-
- {/* Category Filter */}
-
-
-
-
- Categorias
- {selectedCategories.length > 0 && (
-
- {selectedCategories.length}
-
- )}
-
-
-
- Filtrar por Categoria
-
- {categories.map((category) => (
- {
- setSelectedCategories((prev) =>
- checked ? [...prev, category] : prev.filter((c) => c !== category),
- )
- }}
- >
- {category}
-
- ))}
-
-
-
- {hasActiveFilters && (
-
setSearchQuery("")}
>
-
- Limpar Filtros
-
- )}
+
+
+ ) : null}
-
- {/* Desktop: Original layout */}
-
- {/* Color Filter */}
-
-
-
-
- Cores
- {selectedColors.length > 0 && (
-
- {selectedColors.length}
-
- )}
-
-
-
- Filtrar por Cor
-
- {colors.map((color) => (
- {
- setSelectedColors((prev) =>
- checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
- )
- }}
- >
-
-
- ))}
-
-
-
- {/* Tag Filter */}
-
-
-
-
- Tags
- {selectedTags.length > 0 && (
-
- {selectedTags.length}
-
- )}
-
-
-
- Filtrar por Tag
-
- {availableTags.map((tag) => (
- {
- setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
- }}
- >
- {tag}
-
- ))}
-
-
-
- {/* Category Filter */}
-
-
-
-
- Categorias
- {selectedCategories.length > 0 && (
-
- {selectedCategories.length}
-
- )}
-
-
-
- Filtrar por Categoria
-
- {categories.map((category) => (
- {
- setSelectedCategories((prev) =>
- checked ? [...prev, category] : prev.filter((c) => c !== category),
- )
- }}
- >
- {category}
-
- ))}
-
-
-
- {hasActiveFilters && (
-
-
- Limpar
-
- )}
-
- {hasActiveFilters && (
-
-
Filtros ativos:
- {selectedColors.map((colorValue) => {
- const color = getColorClasses(colorValue)
- return (
-
-
- {color.name}
- setSelectedColors((prev) => prev.filter((c) => c !== colorValue))}
- className="ml-1 hover:text-foreground"
- >
-
-
-
- )
- })}
- {selectedTags.map((tag) => (
-
- {tag}
- setSelectedTags((prev) => prev.filter((t) => t !== tag))}
- className="ml-1 hover:text-foreground"
- >
-
-
-
- ))}
- {selectedCategories.map((category) => (
-
- {category}
- setSelectedCategories((prev) => prev.filter((c) => c !== category))}
- className="ml-1 hover:text-foreground"
- >
-
-
-
- ))}
-
- )}
-
{/* Calendar Views - Pass filteredEvents instead of events */}
{view === "month" && (
handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
+ openDayDialog={openDayDialog}
/>
)}
+ {/* Dialog com todos os pacientes do dia */}
+
+
{view === "week" && (
-
+
{isCreating ? "Criar Evento" : "Detalhes do Evento"}
@@ -827,75 +614,9 @@ export function EventManager({
-
-
-
-
-
+ {/* Campos de Categoria/Cor removidos */}
-
-
-
-
-
-
-
-
-
- {availableTags.map((tag) => {
- const isSelected = isCreating ? newEvent.tags?.includes(tag) : selectedEvent?.tags?.includes(tag)
- return (
- toggleTag(tag, isCreating)}
- >
- {tag}
-
- )
- })}
-
-
+ {/* Campo de Tags removido */}
@@ -944,9 +665,11 @@ function EventCard({
const colorClasses = getColorClasses(event.color)
const formatTime = (date: Date) => {
- return date.toLocaleTimeString("en-US", {
+ return date.toLocaleTimeString(LOCALE, {
hour: "2-digit",
minute: "2-digit",
+ hour12: false,
+ timeZone: TIMEZONE,
})
}
@@ -1124,6 +847,7 @@ function MonthView({
onDragEnd,
onDrop,
getColorClasses,
+ openDayDialog,
}: {
currentDate: Date
events: Event[]
@@ -1132,6 +856,7 @@ function MonthView({
onDragEnd: () => void
onDrop: (date: Date) => void
getColorClasses: (color: string) => { bg: string; text: string }
+ openDayDialog: (eventsForDay: Event[]) => void
}) {
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0)
@@ -1170,6 +895,15 @@ function MonthView({
{days.map((day, index) => {
const dayEvents = getEventsForDay(day)
+ // dedup por título para evitar repetidos
+ const uniqueMap = new Map
()
+ dayEvents.forEach((ev) => {
+ const k = (ev.title || "").trim().toLowerCase()
+ if (!uniqueMap.has(k)) uniqueMap.set(k, ev)
+ })
+ const uniqueEvents = Array.from(uniqueMap.values())
+ const eventsToShow = uniqueEvents.slice(0, 3)
+ const moreCount = Math.max(0, uniqueEvents.length - 3)
const isCurrentMonth = day.getMonth() === currentDate.getMonth()
const isToday = day.toDateString() === new Date().toDateString()
@@ -1184,16 +918,11 @@ function MonthView({
onDragOver={(e) => e.preventDefault()}
onDrop={() => onDrop(day)}
>
-
- {day.getDate()}
-
+ {/* Número do dia padronizado (sem destaque azul no 'hoje') */}
+ {day.getDate()}
+
- {dayEvents.slice(0, 3).map((event) => (
+ {eventsToShow.map((event) => (
))}
- {dayEvents.length > 3 && (
-
+{dayEvents.length - 3} mais
+ {moreCount > 0 && (
+
+ openDayDialog(uniqueEvents)}
+ className="text-primary underline"
+ >
+ +{moreCount} mais
+
+
)}
@@ -1267,17 +1004,17 @@ function WeekView({
key={day.toISOString()}
className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm"
>
- {day.toLocaleDateString("pt-BR", { weekday: "short" })}
- {day.toLocaleDateString("pt-BR", { weekday: "narrow" })}
+ {day.toLocaleDateString(LOCALE, { weekday: "short", timeZone: TIMEZONE })}
+ {day.toLocaleDateString(LOCALE, { weekday: "narrow", timeZone: TIMEZONE })}
- {day.toLocaleDateString("pt-BR", { month: "short", day: "numeric" })}
+ {day.toLocaleDateString(LOCALE, { month: "short", day: "numeric", timeZone: TIMEZONE })}
))}
{hours.map((hour) => (
- <>
+
)
})}
- >
+
))}
@@ -1401,15 +1138,14 @@ function ListView({
const groupedEvents = sortedEvents.reduce(
(acc, event) => {
- const dateKey = event.startTime.toLocaleDateString("pt-BR", {
+ const dateKey = event.startTime.toLocaleDateString(LOCALE, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
+ timeZone: TIMEZONE,
})
- if (!acc[dateKey]) {
- acc[dateKey] = []
- }
+ if (!acc[dateKey]) acc[dateKey] = []
acc[dateKey].push(event)
return acc
},
@@ -1426,11 +1162,7 @@ function ListView({
{dateEvents.map((event) => {
const colorClasses = getColorClasses(event.color)
return (
- onEventClick(event)}
- className="group cursor-pointer rounded-lg border bg-card p-3 transition-all hover:shadow-md hover:scale-[1.01] animate-in fade-in slide-in-from-bottom-2 duration-300 sm:p-4"
- >
+
onEventClick(event)} className="group cursor-pointer rounded-lg border bg-card p-3 transition-all hover:shadow-md hover:scale-[1.01] animate-in fade-in slide-in-from-bottom-2 duration-300 sm:p-4">
@@ -1456,7 +1188,9 @@ function ListView({
- {event.startTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} - {event.endTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}
+ {event.startTime.toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: TIMEZONE })}
+ {" - "}
+ {event.endTime.toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: TIMEZONE })}
{event.tags && event.tags.length > 0 && (