From 3022cbfc4b1d4b0109bf47fe14dcb33f5bec2b97 Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Wed, 5 Nov 2025 22:23:41 -0300
Subject: [PATCH 1/9] =?UTF-8?q?fix(search):=20mover=20=C3=ADcone=20de=20bu?=
=?UTF-8?q?sca=20para=20fora=20do=20input=20e=20estilizar=20limpeza?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
susconecta/components/event-manager.tsx | 1485 +++++++++++++++++
.../features/general/event-manager.tsx | 52 +-
2 files changed, 1521 insertions(+), 16 deletions(-)
create mode 100644 susconecta/components/event-manager.tsx
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"}
+
+
+ navigateDate("prev")} className="h-8 w-8">
+
+
+ setCurrentDate(new Date())}>
+ Hoje
+
+ navigateDate("next")} className="h-8 w-8">
+
+
+
+
+
+
+ {/* Mobile: Select dropdown */}
+
+
setView(value)}>
+
+
+
+
+
+
+
+ Mês
+
+
+
+
+
+ Semana
+
+
+
+
+
+ Dia
+
+
+
+
+
+ Lista
+
+
+
+
+
+
+ {/* Desktop: Button group */}
+
+ setView("month")}
+ className="h-8"
+ >
+
+ Mês
+
+ setView("week")}
+ className="h-8"
+ >
+
+ Semana
+
+ setView("day")}
+ className="h-8"
+ >
+
+ Dia
+
+ setView("list")}
+ className="h-8"
+ >
+
+ Lista
+
+
+
+
{
+ setIsCreating(true)
+ setIsDialogOpen(true)
+ }}
+ className="w-full sm:w-auto"
+ >
+
+ Novo Evento
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-9"
+ />
+ {searchQuery && (
+ setSearchQuery("")}
+ >
+
+
+ )}
+
+
+ {/* 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),
+ )
+ }}
+ >
+
+
+ ))}
+
+
+
+ {/* 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 Filtros
+
+ )}
+
+
+
+ {/* 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" && (
+
{
+ 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 */}
+
+
+
+ {isCreating ? "Criar Evento" : "Detalhes do Evento"}
+
+ {isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do evento"}
+
+
+
+
+
+ Título
+
+ isCreating
+ ? setNewEvent((prev) => ({ ...prev, title: e.target.value }))
+ : setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null))
+ }
+ placeholder="Título do evento"
+ />
+
+
+
+ Descrição
+
+
+
+
+
+
+ Categoria
+
+ isCreating
+ ? setNewEvent((prev) => ({ ...prev, category: value }))
+ : setSelectedEvent((prev) => (prev ? { ...prev, category: value } : null))
+ }
+ >
+
+
+
+
+ {categories.map((cat) => (
+
+ {cat}
+
+ ))}
+
+
+
+
+
+
Cor
+
+ isCreating
+ ? setNewEvent((prev) => ({ ...prev, color: value }))
+ : setSelectedEvent((prev) => (prev ? { ...prev, color: value } : null))
+ }
+ >
+
+
+
+
+ {colors.map((color) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
Tags
+
+ {availableTags.map((tag) => {
+ const isSelected = isCreating ? newEvent.tags?.includes(tag) : selectedEvent?.tags?.includes(tag)
+ return (
+ toggleTag(tag, isCreating)}
+ >
+ {tag}
+
+ )
+ })}
+
+
+
+
+
+ {!isCreating && (
+ selectedEvent && handleDeleteEvent(selectedEvent.id)}>
+ Deletar
+
+ )}
+ {
+ setIsDialogOpen(false)
+ setIsCreating(false)
+ setSelectedEvent(null)
+ }}
+ >
+ Cancelar
+
+
+ {isCreating ? "Criar" : "Salvar"}
+
+
+
+
+
+ )
+}
+
+// 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..1a9cac6 100644
--- a/susconecta/components/features/general/event-manager.tsx
+++ b/susconecta/components/features/general/event-manager.tsx
@@ -385,23 +385,43 @@ export function EventManager({
{/* Mobile: Horizontal scroll with full-length buttons */}
--
2.47.2
From 2729fdc86899cb3d2155b08f289a3343b9e4a517 Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Wed, 5 Nov 2025 22:59:35 -0300
Subject: [PATCH 2/9] =?UTF-8?q?Remove=20ListaEspera=20e=20bot=C3=B5es=20Pr?=
=?UTF-8?q?ocedimento/Financeiro?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/(main-routes)/calendar/page.tsx | 81 +++----------------
.../features/general/event-manager.tsx | 62 ++++++++++++--
2 files changed, 64 insertions(+), 79 deletions(-)
diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index c4afdea..8d261c9 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -2,37 +2,19 @@
// 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([]);
// --- NOVO ESTADO ---
@@ -42,15 +24,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");
});
}, []);
@@ -146,10 +121,6 @@ export default function AgendamentoPage() {
}
};
- const handleNotifyPatient = (patientId: string) => {
- console.log(`Notificando paciente ${patientId}`);
- };
-
const handleAddEvent = (event: CalendarEvent) => {
setThreeDEvents((prev) => [...prev, event]);
};
@@ -173,23 +144,6 @@ export default function AgendamentoPage() {
-
-
- Opções »
-
-
-
- Agendamento
-
-
- Procedimento
-
-
- Financeiro
-
-
-
-
setActiveTab("3d")}
>
3D
-
- setActiveTab("espera")}
- >
- Lista de espera
-
@@ -244,16 +190,9 @@ export default function AgendamentoPage() {
onRemoveEvent={handleRemoveEvent}
/>
- ) : (
- // A Lista de Espera foi MANTIDA
- {}}
- />
- )}
-
-
-
- );
-}
\ No newline at end of file
+ ) : null}
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx
index 1a9cac6..e8e0bf3 100644
--- a/susconecta/components/features/general/event-manager.tsx
+++ b/susconecta/components/features/general/event-manager.tsx
@@ -72,6 +72,11 @@ export function EventManager({
availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
}: EventManagerProps) {
const [events, setEvents] = useState(initialEvents)
+ // controla dias expandidos no MonthView (key = YYYY-MM-DD)
+ const [expandedDays, setExpandedDays] = useState>({})
+ const toggleExpandedDay = useCallback((dayKey: string) => {
+ setExpandedDays((prev) => ({ ...prev, [dayKey]: !prev[dayKey] }))
+ }, [])
const [currentDate, setCurrentDate] = useState(new Date())
const [view, setView] = useState<"month" | "week" | "day" | "list">(defaultView)
const [selectedEvent, setSelectedEvent] = useState(null)
@@ -702,6 +707,8 @@ export function EventManager({
onDragEnd={() => handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
+ expandedDays={expandedDays}
+ toggleExpandedDay={toggleExpandedDay}
/>
)}
@@ -1144,6 +1151,8 @@ function MonthView({
onDragEnd,
onDrop,
getColorClasses,
+ expandedDays,
+ toggleExpandedDay,
}: {
currentDate: Date
events: Event[]
@@ -1152,6 +1161,8 @@ function MonthView({
onDragEnd: () => void
onDrop: (date: Date) => void
getColorClasses: (color: string) => { bg: string; text: string }
+ expandedDays: Record
+ toggleExpandedDay: (dayKey: string) => void
}) {
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0)
@@ -1177,6 +1188,9 @@ function MonthView({
})
}
+ const formatTime = (date: Date) =>
+ date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
+
return (
@@ -1224,16 +1238,48 @@ function MonthView({
variant="compact"
/>
))}
- {dayEvents.length > 3 && (
+ {dayEvents.length > 3 && (
+{dayEvents.length - 3} mais
)}
-
-
- )
- })}
-
-
- )
+ {(() => {
+ const dayKey = day.toISOString().slice(0, 10)
+ const isExpanded = !!expandedDays?.[dayKey]
+ const eventsToShow = isExpanded ? dayEvents : dayEvents.slice(0, 3)
+ return (
+ <>
+ {eventsToShow.map((event) => (
+
+ ))}
+
+ {dayEvents.length > 3 && (
+
+ toggleExpandedDay(dayKey)}
+ className="text-primary underline hover:text-primary/80"
+ >
+ {isExpanded ? "Mostrar menos" : `+${dayEvents.length - 3} mais`}
+
+
+ )}
+ >
+ )
+ })()}
+
+
+ )
+ })}
+
+
+ )
}
// Week View Component
--
2.47.2
From 93e7b203903caada30883083f4191772fa4c22a7 Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Wed, 5 Nov 2025 23:22:41 -0300
Subject: [PATCH 3/9] ajuste no calendario
---
.../features/general/event-manager.tsx | 150 ++++++++++--------
1 file changed, 84 insertions(+), 66 deletions(-)
diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx
index e8e0bf3..e493641 100644
--- a/susconecta/components/features/general/event-manager.tsx
+++ b/susconecta/components/features/general/event-manager.tsx
@@ -72,11 +72,6 @@ export function EventManager({
availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
}: EventManagerProps) {
const [events, setEvents] = useState(initialEvents)
- // controla dias expandidos no MonthView (key = YYYY-MM-DD)
- const [expandedDays, setExpandedDays] = useState>({})
- const toggleExpandedDay = useCallback((dayKey: string) => {
- setExpandedDays((prev) => ({ ...prev, [dayKey]: !prev[dayKey] }))
- }, [])
const [currentDate, setCurrentDate] = useState(new Date())
const [view, setView] = useState<"month" | "week" | "day" | "list">(defaultView)
const [selectedEvent, setSelectedEvent] = useState(null)
@@ -96,6 +91,16 @@ export function EventManager({
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
@@ -707,11 +712,49 @@ export function EventManager({
onDragEnd={() => handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
- expandedDays={expandedDays}
- toggleExpandedDay={toggleExpandedDay}
+ openDayDialog={openDayDialog}
/>
)}
+ {/* Dialog com todos os pacientes do dia */}
+
+
+
+ Pacientes do dia
+ Todos os agendamentos do dia selecionado.
+
+
+ {dayDialogEvents?.map((ev) => (
+
+
+
+
+
{ev.title}
+
+ {ev.startTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
+ {" - "}
+ {ev.endTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
+
+
+ {ev.description && (
+
{ev.description}
+ )}
+
+ {ev.category && {ev.category} }
+ {ev.tags?.map((t) => (
+ {t}
+ ))}
+
+
+
+ ))}
+ {!dayDialogEvents?.length && (
+
Nenhum evento
+ )}
+
+
+
+
{view === "week" && (
void
onDrop: (date: Date) => void
getColorClasses: (color: string) => { bg: string; text: string }
- expandedDays: Record
- toggleExpandedDay: (dayKey: string) => void
+ openDayDialog: (eventsForDay: Event[]) => void
}) {
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0)
@@ -1188,9 +1229,6 @@ function MonthView({
})
}
- const formatTime = (date: Date) =>
- date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
-
return (
@@ -1204,6 +1242,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()
@@ -1218,16 +1265,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
+
+
)}
- {(() => {
- const dayKey = day.toISOString().slice(0, 10)
- const isExpanded = !!expandedDays?.[dayKey]
- const eventsToShow = isExpanded ? dayEvents : dayEvents.slice(0, 3)
- return (
- <>
- {eventsToShow.map((event) => (
-
- ))}
-
- {dayEvents.length > 3 && (
-
- toggleExpandedDay(dayKey)}
- className="text-primary underline hover:text-primary/80"
- >
- {isExpanded ? "Mostrar menos" : `+${dayEvents.length - 3} mais`}
-
-
- )}
- >
- )
- })()}
-
-
- )
- })}
-
-
- )
+
+
+ )
+ })}
+
+
+ )
}
// Week View Component
@@ -1343,7 +1361,7 @@ function WeekView({
{hours.map((hour) => (
- <>
+
)
})}
- >
+
))}
--
2.47.2
From 334adb5ba1c86cad503b969c0807c02e66fd7a22 Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Wed, 5 Nov 2025 23:58:53 -0300
Subject: [PATCH 4/9] ajuste visual calendario
---
.../app/(main-routes)/calendar/page.tsx | 15 +
.../features/general/event-manager.tsx | 444 ++----------------
2 files changed, 65 insertions(+), 394 deletions(-)
diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index 8d261c9..292c8ad 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -17,6 +17,21 @@ export default function AgendamentoPage() {
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([]);
diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx
index e493641..1089eb9 100644
--- a/susconecta/components/features/general/event-manager.tsx
+++ b/susconecta/components/features/general/event-manager.tsx
@@ -60,16 +60,20 @@ const defaultColors = [
{ name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" },
]
+// Helper para padrão pt-BR (data e hora)
+const formatDateTimeBR = (date?: Date) =>
+ date ? new Intl.DateTimeFormat("pt-BR", { dateStyle: "short", timeStyle: "short" }).format(date) : ""
+
export function EventManager({
events: initialEvents = [],
onEventCreate,
onEventUpdate,
onEventDelete,
- categories = ["Meeting", "Task", "Reminder", "Personal"],
+ categories: _categories = ["Meeting", "Task", "Reminder", "Personal"],
colors = defaultColors,
defaultView = "month",
className,
- availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
+ availableTags: _availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
}: EventManagerProps) {
const [events, setEvents] = useState(initialEvents)
const [currentDate, setCurrentDate] = useState(new Date())
@@ -82,10 +86,9 @@ export function EventManager({
title: "",
description: "",
color: colors[0].value,
- category: categories[0],
+ category: _categories[0],
tags: [],
})
-
const [searchQuery, setSearchQuery] = useState("")
const [selectedColors, setSelectedColors] = useState([])
const [selectedTags, setSelectedTags] = useState([])
@@ -108,9 +111,7 @@ export function EventManager({
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))
+ event.description?.toLowerCase().includes(query)
if (!matchesSearch) return false
}
@@ -156,7 +157,6 @@ export function EventManager({
color: newEvent.color || colors[0].value,
category: newEvent.category,
attendees: newEvent.attendees,
- tags: newEvent.tags || [],
}
setEvents((prev) => [...prev, event])
@@ -167,10 +167,10 @@ export function EventManager({
title: "",
description: "",
color: colors[0].value,
- category: categories[0],
+ category: _categories[0],
tags: [],
})
- }, [newEvent, colors, categories, onEventCreate])
+ }, [newEvent, colors, _categories, onEventCreate])
const handleUpdateEvent = useCallback(() => {
if (!selectedEvent) return
@@ -248,24 +248,6 @@ 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,
- )
- }
- }
-
return (
{/* Header */}
@@ -295,9 +277,6 @@ export function EventManager({
navigateDate("prev")} className="h-8 w-8">
- setCurrentDate(new Date())}>
- Hoje
-
navigateDate("next")} className="h-8 w-8">
@@ -434,123 +413,8 @@ export function EventManager({
- {/* 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),
- )
- }}
- >
-
-
- ))}
-
-
-
- {/* 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 Filtros
-
- )}
-
-
-
- {/* Desktop: Original layout */}
-
- {/* Color Filter */}
+ {/* Filtro de Cores (único), logo abaixo do input */}
+
@@ -563,7 +427,7 @@ export function EventManager({
)}
-
+
Filtrar por Cor
{colors.map((color) => (
@@ -584,69 +448,6 @@ export function EventManager({
))}
-
- {/* 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 && (
@@ -699,7 +500,7 @@ export function EventManager({
)}
- {/* Calendar Views - Pass filteredEvents instead of events */}
+ {/* Calendar Views - Month */}
{view === "month" && (
{ev.title}
- {ev.startTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
- {" - "}
- {ev.endTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
+ {formatDateTimeBR(ev.startTime)} - {formatDateTimeBR(ev.endTime)}
{ev.description && (
{ev.description}
)}
-
- {ev.category && {ev.category} }
- {ev.tags?.map((t) => (
- {t}
- ))}
-
))}
@@ -755,47 +548,6 @@ export function EventManager({
- {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 */}
@@ -867,6 +619,10 @@ export function EventManager({
: setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null))
}}
/>
+ {/* Exibe também em pt-BR para usuários com SO/teclado em inglês */}
+
+ {formatDateTimeBR(isCreating ? newEvent.startTime : selectedEvent?.startTime)}
+
@@ -894,77 +650,37 @@ export function EventManager({
: setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null))
}}
/>
-
-
-
-
-
- Categoria
-
- isCreating
- ? setNewEvent((prev) => ({ ...prev, category: value }))
- : setSelectedEvent((prev) => (prev ? { ...prev, category: value } : null))
- }
- >
-
-
-
-
- {categories.map((cat) => (
-
- {cat}
-
- ))}
-
-
-
-
-
-
Cor
-
- isCreating
- ? setNewEvent((prev) => ({ ...prev, color: value }))
- : setSelectedEvent((prev) => (prev ? { ...prev, color: value } : null))
- }
- >
-
-
-
-
- {colors.map((color) => (
-
-
-
- ))}
-
-
+ {/* Exibe também em pt-BR para usuários com SO/teclado em inglês */}
+
+ {formatDateTimeBR(isCreating ? newEvent.endTime : selectedEvent?.endTime)}
+
-
Tags
-
- {availableTags.map((tag) => {
- const isSelected = isCreating ? newEvent.tags?.includes(tag) : selectedEvent?.tags?.includes(tag)
- return (
- toggleTag(tag, isCreating)}
- >
- {tag}
-
- )
- })}
-
+
Cor
+
+ isCreating
+ ? setNewEvent((prev) => ({ ...prev, color: value }))
+ : setSelectedEvent((prev) => (prev ? { ...prev, color: value } : null))
+ }
+ >
+
+
+
+
+ {colors.map((color) => (
+
+
+
+ ))}
+
+
@@ -1013,12 +729,8 @@ function EventCard({
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 formatTime = (date: Date) =>
+ date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
const getDuration = () => {
const diff = event.endTime.getTime() - event.startTime.getTime()
@@ -1067,18 +779,6 @@ function EventCard({
({getDuration()})
-
- {event.category && (
-
- {event.category}
-
- )}
- {event.tags?.map((tag) => (
-
- {tag}
-
- ))}
-
@@ -1109,20 +809,6 @@ function EventCard({
{formatTime(event.startTime)} - {formatTime(event.endTime)}
- {isHovered && (
-
- {event.category && (
-
- {event.category}
-
- )}
- {event.tags?.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
)
}
@@ -1164,18 +850,6 @@ function EventCard({
({getDuration()})
-
- {event.category && (
-
- {event.category}
-
- )}
- {event.tags?.map((tag) => (
-
- {tag}
-
- ))}
-
@@ -1491,13 +1165,11 @@ function ListView({
month: "long",
day: "numeric",
})
- if (!acc[dateKey]) {
- acc[dateKey] = []
- }
+ if (!acc[dateKey]) acc[dateKey] = []
acc[dateKey].push(event)
return acc
},
- {} as Record,
+ {} as Record
)
return (
@@ -1529,28 +1201,12 @@ function ListView({
)}
-
- {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}
-
- ))}
-
- )}
--
2.47.2
From e2a6b280803966febc19d0cbceb2c58c78ef8fc1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?=
<166467972+JoaoGustavo-dev@users.noreply.github.com>
Date: Thu, 6 Nov 2025 00:35:59 -0300
Subject: [PATCH 5/9] fixing-patient-page
---
susconecta/app/paciente/page.tsx | 160 +++++++++++++++---
.../paciente/resultados/ResultadosClient.tsx | 66 +++++---
2 files changed, 183 insertions(+), 43 deletions(-)
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index 697c02f..2fc841b 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -18,7 +18,8 @@ import Link from 'next/link'
import ProtectedRoute from '@/components/shared/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById } from '@/lib/api'
+import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById, atualizarAgendamento, deletarAgendamento } from '@/lib/api'
+import { CalendarRegistrationForm } from '@/components/features/forms/calendar-registration-form'
import { buscarRelatorioPorId, listarRelatoriosPorMedico } from '@/lib/reports'
import { ENV_CONFIG } from '@/lib/env-config'
import { listarRelatoriosPorPaciente } from '@/lib/reports'
@@ -35,7 +36,6 @@ const strings = {
ultimosExames: 'Últimos Exames',
mensagensNaoLidas: 'Mensagens Não Lidas',
agendar: 'Agendar',
- reagendar: 'Reagendar',
cancelar: 'Cancelar',
detalhes: 'Detalhes',
adicionarCalendario: 'Adicionar ao calendário',
@@ -445,11 +445,10 @@ export default function PacientePage() {
-
+
)
}
- // Consultas fictícias
const [currentDate, setCurrentDate] = useState(new Date())
// helper: produce a local YYYY-MM-DD key (uses local timezone, not toISOString UTC)
@@ -519,10 +518,15 @@ export default function PacientePage() {
const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0);
const isSelectedDateToday = selectedDate.getTime() === today.getTime()
- // Appointments state (loaded when component mounts)
- const [appointments, setAppointments] = useState(null)
- const [loadingAppointments, setLoadingAppointments] = useState(false)
- const [appointmentsError, setAppointmentsError] = useState(null)
+ // Appointments state (loaded when component mounts)
+ const [appointments, setAppointments] = useState(null)
+ const [doctorsMap, setDoctorsMap] = useState>({}) // Store doctor info by ID
+ const [loadingAppointments, setLoadingAppointments] = useState(false)
+ const [appointmentsError, setAppointmentsError] = useState(null)
+ // expanded appointment id for inline details (kept for possible fallback)
+ const [expandedId, setExpandedId] = useState(null)
+ // selected appointment for modal details
+ const [selectedAppointment, setSelectedAppointment] = useState(null)
useEffect(() => {
let mounted = true
@@ -608,6 +612,7 @@ export default function PacientePage() {
}
})
+ setDoctorsMap(doctorsMap)
setAppointments(mapped)
} catch (err: any) {
console.warn('[Consultas] falha ao carregar agendamentos', err)
@@ -638,6 +643,60 @@ export default function PacientePage() {
const _dialogSource = (appointments !== null ? appointments : consultasFicticias)
const _todaysAppointments = (_dialogSource || []).filter((c: any) => c.data === todayStr)
+ // helper: present a localized label for appointment status
+ const statusLabel = (s: any) => {
+ const raw = (s === null || s === undefined) ? '' : String(s)
+ const key = raw.toLowerCase()
+ const map: Record = {
+ 'requested': 'Solicitado',
+ 'request': 'Solicitado',
+ 'confirmed': 'Confirmado',
+ 'confirmada': 'Confirmada',
+ 'confirmado': 'Confirmado',
+ 'completed': 'Concluído',
+ 'concluído': 'Concluído',
+ 'cancelled': 'Cancelado',
+ 'cancelada': 'Cancelada',
+ 'cancelado': 'Cancelado',
+ 'pending': 'Pendente',
+ 'pendente': 'Pendente',
+ 'checked_in': 'Registrado',
+ 'in_progress': 'Em andamento',
+ 'no_show': 'Não compareceu'
+ }
+ return map[key] || raw
+ }
+
+ // map an appointment (row) to the CalendarRegistrationForm's formData shape
+ const mapAppointmentToFormData = (appointment: any) => {
+ // Use the raw appointment with all fields: doctor_id, scheduled_at, appointment_type, etc.
+ const schedIso = appointment.scheduled_at || (appointment.data && appointment.hora ? `${appointment.data}T${appointment.hora}` : null) || null
+ const baseDate = schedIso ? new Date(schedIso) : new Date()
+ const appointmentDate = schedIso ? baseDate.toISOString().split('T')[0] : ''
+ const startTime = schedIso ? baseDate.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : (appointment.hora || '')
+ const duration = appointment.duration_minutes ?? appointment.duration ?? 30
+
+ // Get doctor name from doctorsMap if available
+ const docName = appointment.medico || (appointment.doctor_id ? doctorsMap[String(appointment.doctor_id)]?.full_name : null) || appointment.doctor_name || appointment.professional_name || '---'
+
+ return {
+ id: appointment.id,
+ patientName: docName,
+ patientId: null,
+ doctorId: appointment.doctor_id ?? null,
+ professionalName: docName,
+ appointmentDate,
+ startTime,
+ endTime: '',
+ status: appointment.status || undefined,
+ appointmentType: appointment.appointment_type || appointment.type || (appointment.local ? 'presencial' : 'teleconsulta'),
+ duration_minutes: duration,
+ notes: appointment.notes || '',
+ }
+ }
+
+
+
return (
{/* Hero Section */}
@@ -771,7 +830,7 @@ export default function PacientePage() {
? 'bg-linear-to-r from-amber-500 to-amber-600 shadow-amber-500/20'
: 'bg-linear-to-r from-red-500 to-red-600 shadow-red-500/20'
}`}>
- {consulta.status}
+ {statusLabel(consulta.status)}
@@ -781,28 +840,43 @@ export default function PacientePage() {
type="button"
size="sm"
className="border border-primary/30 text-primary bg-primary/5 hover:bg-primary! hover:text-white! hover:border-primary! transition-all duration-200 focus-visible:ring-2 focus-visible:ring-primary/40 active:scale-95 text-xs font-semibold flex-1"
+ onClick={() => setSelectedAppointment(consulta)}
>
Detalhes
- {consulta.status !== 'Cancelada' && (
-
- Reagendar
-
- )}
+ {/* Reagendar removed by request */}
{consulta.status !== 'Cancelada' && (
{
+ try {
+ const ok = typeof window !== 'undefined' ? window.confirm('Deseja realmente cancelar esta consulta?') : true
+ if (!ok) return
+ // call API to delete
+ await deletarAgendamento(consulta.id)
+ // remove from local list
+ setAppointments((prev) => {
+ if (!prev) return prev
+ return prev.filter((a: any) => String(a.id) !== String(consulta.id))
+ })
+ // if modal open for this appointment, close it
+ if (selectedAppointment && String(selectedAppointment.id) === String(consulta.id)) setSelectedAppointment(null)
+ setToast({ type: 'success', msg: 'Consulta cancelada.' })
+ } catch (err: any) {
+ console.error('[Consultas] falha ao cancelar agendamento', err)
+ try { setToast({ type: 'error', msg: err?.message || 'Falha ao cancelar a consulta.' }) } catch (e) {}
+ }
+ }}
>
Cancelar
)}
+
+ {/* Inline detalhes removed: modal will show details instead */}
+
))
@@ -811,6 +885,45 @@ export default function PacientePage() {
+
+
+
+ !open && setSelectedAppointment(null)}>
+
+
+ Detalhes da Consulta
+ Detalhes da consulta
+
+ {selectedAppointment ? (
+ <>
+
+
Profissional: {selectedAppointment.medico || '-'}
+
Especialidade: {selectedAppointment.especialidade || '-'}
+
+
+
+
Data: {(function(d:any,h:any){ try{ const dt = new Date(String(d) + 'T' + String(h||'00:00')); return formatDatePt(dt) }catch(e){ return String(d||'-') } })(selectedAppointment.data, selectedAppointment.hora)}
+
Hora: {selectedAppointment.hora || '-'}
+
Status: {statusLabel(selectedAppointment.status) || '-'}
+
+ >
+ ) : (
+
Carregando...
+ )}
+
+
+
+
+ setSelectedAppointment(null)} className="transition duration-200 hover:bg-primary/10 hover:text-primary min-w-[110px]">
+ Fechar
+
+
+
+
+
+
+ {/* Reagendar feature removed */}
+
)
}
@@ -1262,7 +1375,7 @@ export default function PacientePage() {
setReportsPage(1)
}, [reports])
- return (
+ return (<>
Laudos
@@ -1334,10 +1447,13 @@ export default function PacientePage() {
)}
+
+
+
!open && setSelectedReport(null)}>
-
-
+
+
{selectedReport && (
(() => {
const looksLikeIdStr = (s: any) => {
@@ -1422,7 +1538,7 @@ export default function PacientePage() {
-
+ >
)
}
diff --git a/susconecta/app/paciente/resultados/ResultadosClient.tsx b/susconecta/app/paciente/resultados/ResultadosClient.tsx
index 1fa8519..b471740 100644
--- a/susconecta/app/paciente/resultados/ResultadosClient.tsx
+++ b/susconecta/app/paciente/resultados/ResultadosClient.tsx
@@ -148,7 +148,7 @@ export default function ResultadosClient() {
try {
setLoadingMedicos(true)
console.log('[ResultadosClient] Initial doctors fetch starting')
- const list = await buscarMedicos('medico').catch((err) => {
+ const list = await buscarMedicos('').catch((err) => {
console.error('[ResultadosClient] Initial fetch error:', err)
return []
})
@@ -175,7 +175,7 @@ export default function ResultadosClient() {
setAgendaByDoctor({})
setAgendasExpandida({})
// termo de busca: usar a especialidade escolhida
- const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : 'medico'
+ const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : ''
console.log('[ResultadosClient] Fetching doctors with term:', termo)
const list = await buscarMedicos(termo).catch((err) => {
console.error('[ResultadosClient] buscarMedicos error:', err)
@@ -219,9 +219,9 @@ export default function ResultadosClient() {
}, [searchQuery])
// 3) Carregar horários disponíveis para um médico (próximos 7 dias) e agrupar por dia
- async function loadAgenda(doctorId: string) {
- if (!doctorId) return
- if (agendaLoading[doctorId]) return
+ async function loadAgenda(doctorId: string): Promise<{ iso: string; label: string } | null> {
+ if (!doctorId) return null
+ if (agendaLoading[doctorId]) return null
setAgendaLoading((s) => ({ ...s, [doctorId]: true }))
try {
// janela de 7 dias
@@ -271,10 +271,12 @@ export default function ResultadosClient() {
nearest = { iso: s.iso, label: s.label }
}
- setAgendaByDoctor((prev) => ({ ...prev, [doctorId]: days }))
- setNearestSlotByDoctor((prev) => ({ ...prev, [doctorId]: nearest }))
+ setAgendaByDoctor((prev) => ({ ...prev, [doctorId]: days }))
+ setNearestSlotByDoctor((prev) => ({ ...prev, [doctorId]: nearest }))
+ return nearest
} catch (e: any) {
showToast('error', e?.message || 'Falha ao buscar horários')
+ return null
} finally {
setAgendaLoading((s) => ({ ...s, [doctorId]: false }))
}
@@ -752,19 +754,7 @@ export default function ResultadosClient() {
-
-
-
-
-
- Todos os bairros
- Centro
- Jardins
- Farolândia
-
-
-
- {/* Search input para buscar médico por nome */}
+ {/* Search input para buscar médico por nome (movido antes do Select de bairro para ficar ao lado visualmente) */}
+
+
+
+
+
+ Todos os bairros
+ Centro
+ Jardins
+ Farolândia
+
+
+
{ if (!agendaByDoctor[id]) loadAgenda(id) }}
+ onClick={async () => {
+ // If we don't have the agenda loaded, load it and try to open the nearest slot.
+ if (!agendaByDoctor[id]) {
+ const nearest = await loadAgenda(id)
+ if (nearest) {
+ openConfirmDialog(id, nearest.iso)
+ return
+ }
+ // fallback: open the "more times" modal to let the user pick a date/time
+ setMoreTimesForDoctor(id)
+ void fetchSlotsForDate(id, moreTimesDate)
+ return
+ }
+
+ // If agenda already loaded, try nearest known slot
+ const nearest = nearestSlotByDoctor[id]
+ if (nearest) {
+ openConfirmDialog(id, nearest.iso)
+ } else {
+ setMoreTimesForDoctor(id)
+ void fetchSlotsForDate(id, moreTimesDate)
+ }
+ }}
>
Agendar consulta
--
2.47.2
From 1aed4c6164e0cfbfc12fcc4e6e87ba4a1313a934 Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Thu, 6 Nov 2025 00:40:07 -0300
Subject: [PATCH 6/9] ajust calendario
---
.../app/(main-routes)/calendar/page.tsx | 24 +-
.../features/general/event-manager.tsx | 323 ++++++++----------
2 files changed, 172 insertions(+), 175 deletions(-)
diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index 292c8ad..4e427a2 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -144,6 +144,17 @@ export default function AgendamentoPage() {
setThreeDEvents((prev) => prev.filter((e) => e.id !== id));
};
+ // Tenta clicar no botão de filtro correspondente (procura por texto do botão)
+ const clickFilter = (label: string) => {
+ try {
+ const buttons = Array.from(document.querySelectorAll("button"));
+ const match = buttons.find((b) => b.textContent?.trim().toLowerCase().includes(label.toLowerCase()));
+ if (match) match.click();
+ } catch {
+ // ignore
+ }
+ };
+
return (
@@ -158,9 +169,17 @@ export default function AgendamentoPage() {
Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).
-
+
+ {/* Botões rápidos de filtros (acionam os triggers se existirem no DOM) */}
+
+ clickFilter("Cores")}>Cores
+ clickFilter("Tags")}>Tags
+ clickFilter("Categorias")}>Categorias
+
+
setActiveTab("calendar")}
@@ -169,6 +188,7 @@ export default function AgendamentoPage() {
setActiveTab("3d")}
@@ -177,7 +197,7 @@ export default function AgendamentoPage() {
-
+
{/* --- AQUI ESTÁ A SUBSTITUIÇÃO --- */}
{activeTab === "calendar" ? (
diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx
index 1089eb9..a009277 100644
--- a/susconecta/components/features/general/event-manager.tsx
+++ b/susconecta/components/features/general/event-manager.tsx
@@ -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,20 +52,16 @@ const defaultColors = [
{ name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" },
]
-// Helper para padrão pt-BR (data e hora)
-const formatDateTimeBR = (date?: Date) =>
- date ? new Intl.DateTimeFormat("pt-BR", { dateStyle: "short", timeStyle: "short" }).format(date) : ""
-
export function EventManager({
events: initialEvents = [],
onEventCreate,
onEventUpdate,
onEventDelete,
- categories: _categories = ["Meeting", "Task", "Reminder", "Personal"],
+ categories = ["Meeting", "Task", "Reminder", "Personal"],
colors = defaultColors,
defaultView = "month",
className,
- availableTags: _availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
+ availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
}: EventManagerProps) {
const [events, setEvents] = useState(initialEvents)
const [currentDate, setCurrentDate] = useState(new Date())
@@ -86,13 +74,11 @@ export function EventManager({
title: "",
description: "",
color: colors[0].value,
- category: _categories[0],
+ category: categories[0],
tags: [],
})
+
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)
@@ -106,42 +92,22 @@ export function EventManager({
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.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("")
}
@@ -157,6 +123,7 @@ export function EventManager({
color: newEvent.color || colors[0].value,
category: newEvent.category,
attendees: newEvent.attendees,
+ tags: newEvent.tags || [],
}
setEvents((prev) => [...prev, event])
@@ -167,10 +134,10 @@ export function EventManager({
title: "",
description: "",
color: colors[0].value,
- category: _categories[0],
+ category: categories[0],
tags: [],
})
- }, [newEvent, colors, _categories, onEventCreate])
+ }, [newEvent, colors, categories, onEventCreate])
const handleUpdateEvent = useCallback(() => {
if (!selectedEvent) return
@@ -412,95 +379,9 @@ export function EventManager({
) : null}
-
- {/* Filtro de Cores (único), logo abaixo do input */}
-
-
-
-
-
- Cores
- {selectedColors.length > 0 && (
-
- {selectedColors.length}
-
- )}
-
-
-
- Filtrar por Cor
-
- {colors.map((color) => (
- {
- setSelectedColors((prev) =>
- checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
- )
- }}
- >
-
-
- ))}
-
-
- {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 - Month */}
+ {/* Calendar Views - Pass filteredEvents instead of events */}
{view === "month" && (
{dayDialogEvents?.map((ev) => (
-
+
{
+ setSelectedEvent(ev)
+ setIsDialogOpen(true)
+ setIsDayDialogOpen(false)
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ setSelectedEvent(ev)
+ setIsDialogOpen(true)
+ setIsDayDialogOpen(false)
+ }
+ }}
+ className="flex items-start gap-3 p-2 border-b last:border-b-0 rounded-md cursor-pointer hover:bg-accent/40 focus:outline-none focus:ring-2 focus:ring-primary/30"
+ >
{ev.title}
- {formatDateTimeBR(ev.startTime)} - {formatDateTimeBR(ev.endTime)}
+ {ev.startTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
+ {" - "}
+ {ev.endTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
{ev.description && (
{ev.description}
)}
+
+ {ev.category && {ev.category} }
+ {ev.tags?.map((t) => (
+ {t}
+ ))}
+
))}
@@ -548,9 +454,50 @@ export function EventManager({
+ {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 */}
-
+
{isCreating ? "Criar Evento" : "Detalhes do Evento"}
@@ -619,10 +566,6 @@ export function EventManager({
: setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null))
}}
/>
- {/* Exibe também em pt-BR para usuários com SO/teclado em inglês */}
-
- {formatDateTimeBR(isCreating ? newEvent.startTime : selectedEvent?.startTime)}
-
@@ -650,38 +593,12 @@ export function EventManager({
: setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null))
}}
/>
- {/* Exibe também em pt-BR para usuários com SO/teclado em inglês */}
-
- {formatDateTimeBR(isCreating ? newEvent.endTime : selectedEvent?.endTime)}
-
-
-
Cor
-
- isCreating
- ? setNewEvent((prev) => ({ ...prev, color: value }))
- : setSelectedEvent((prev) => (prev ? { ...prev, color: value } : null))
- }
- >
-
-
-
-
- {colors.map((color) => (
-
-
-
- ))}
-
-
-
+ {/* Campos de Categoria/Cor removidos */}
+
+ {/* Campo de Tags removido */}
@@ -729,8 +646,12 @@ function EventCard({
const [isHovered, setIsHovered] = useState(false)
const colorClasses = getColorClasses(event.color)
- const formatTime = (date: Date) =>
- date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
+ 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()
@@ -779,6 +700,18 @@ function EventCard({
({getDuration()})
+
+ {event.category && (
+
+ {event.category}
+
+ )}
+ {event.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
@@ -809,6 +742,20 @@ function EventCard({
{formatTime(event.startTime)} - {formatTime(event.endTime)}
+ {isHovered && (
+
+ {event.category && (
+
+ {event.category}
+
+ )}
+ {event.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
)
}
@@ -850,6 +797,18 @@ function EventCard({
({getDuration()})
+
+ {event.category && (
+
+ {event.category}
+
+ )}
+ {event.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
@@ -1165,11 +1124,13 @@ function ListView({
month: "long",
day: "numeric",
})
- if (!acc[dateKey]) acc[dateKey] = []
+ if (!acc[dateKey]) {
+ acc[dateKey] = []
+ }
acc[dateKey].push(event)
return acc
},
- {} as Record
+ {} as Record,
)
return (
@@ -1201,12 +1162,28 @@ function ListView({
)}
+
+ {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}
+
+ ))}
+
+ )}
--
2.47.2
From 214da568c3814892408b86c79f1a21ee8868ed30 Mon Sep 17 00:00:00 2001
From: Jonas Francisco
Date: Thu, 6 Nov 2025 00:54:58 -0300
Subject: [PATCH 7/9] ajustes finais no calendario
---
.../app/(main-routes)/calendar/page.tsx | 65 ++++++++++---------
.../features/general/event-manager.tsx | 57 ++++++++++------
2 files changed, 70 insertions(+), 52 deletions(-)
diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index 4e427a2..a6d3b5c 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -76,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);
@@ -144,17 +149,6 @@ export default function AgendamentoPage() {
setThreeDEvents((prev) => prev.filter((e) => e.id !== id));
};
- // Tenta clicar no botão de filtro correspondente (procura por texto do botão)
- const clickFilter = (label: string) => {
- try {
- const buttons = Array.from(document.querySelectorAll("button"));
- const match = buttons.find((b) => b.textContent?.trim().toLowerCase().includes(label.toLowerCase()));
- if (match) match.click();
- } catch {
- // ignore
- }
- };
-
return (
@@ -170,13 +164,6 @@ export default function AgendamentoPage() {
- {/* Botões rápidos de filtros (acionam os triggers se existirem no DOM) */}
-
- clickFilter("Cores")}>Cores
- clickFilter("Tags")}>Tags
- clickFilter("Categorias")}>Categorias
-
-
-
+
+
+ {/* Legenda de status (estilo Google Calendar) */}
+
+
+
+
+ Solicitado
+
+
+
+ Confirmado
+
+
+
{/* --- AQUI ESTÁ A SUBSTITUIÇÃO --- */}
{activeTab === "calendar" ? (
@@ -226,8 +227,8 @@ export default function AgendamentoPage() {
/>
) : null}
-
-
-
- );
- }
\ No newline at end of file
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx
index a009277..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"
@@ -52,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,
@@ -215,6 +219,17 @@ export function EventManager({
[colors],
)
+ // 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 (
{/* Header */}
@@ -222,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"}
@@ -430,9 +448,9 @@ export function EventManager({
{ev.title}
- {ev.startTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
+ {ev.startTime.toLocaleTimeString(LOCALE,{hour:"2-digit",minute:"2-digit",hour12:false,timeZone:TIMEZONE})}
{" - "}
- {ev.endTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
+ {ev.endTime.toLocaleTimeString(LOCALE,{hour:"2-digit",minute:"2-digit",hour12:false,timeZone:TIMEZONE})}
{ev.description && (
@@ -647,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,
})
}
@@ -984,10 +1004,10 @@ 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 })}
))}
@@ -1118,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
},
@@ -1143,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">
@@ -1173,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 && (
--
2.47.2
From 5dd0764f0e1616f20d595c3d45d64fa186612e0c Mon Sep 17 00:00:00 2001
From: M-Gabrielly
Date: Thu, 6 Nov 2025 01:01:17 -0300
Subject: [PATCH 8/9] feat: improve patient registration auth, debugging in 3D
calendar and register profiles
---
.../app/(main-routes)/calendar/page.tsx | 18 +
susconecta/app/globals.css | 44 ++
susconecta/app/paciente/page.tsx | 237 ++++++---
susconecta/app/profissional/page.tsx | 476 ++++++++++++------
.../forms/patient-registration-form.tsx | 12 +-
.../components/ui/three-dwall-calendar.tsx | 16 +-
susconecta/lib/api.ts | 41 +-
7 files changed, 622 insertions(+), 222 deletions(-)
diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index c4afdea..aa7380c 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -13,6 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback
import { Sidebar } from "@/components/layout/sidebar";
import { PagesHeader } from "@/components/features/dashboard/header";
import { Button } from "@/components/ui/button";
+import { useAuth } from "@/hooks/useAuth";
import { mockWaitingList } from "@/lib/mocks/appointment-mocks";
import "./index.css";
import {
@@ -22,6 +23,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
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"),
@@ -29,6 +31,7 @@ const ListaEspera = dynamic(
);
export default function AgendamentoPage() {
+ const { user, token } = useAuth();
const [appointments, setAppointments] = useState([]);
const [waitingList, setWaitingList] = useState(mockWaitingList);
const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar");
@@ -39,6 +42,9 @@ export default function AgendamentoPage() {
// Estado para alimentar o NOVO EventManager com dados da API
const [managerEvents, setManagerEvents] = useState([]);
const [managerLoading, setManagerLoading] = useState(true);
+
+ // Estado para o formulário de registro de paciente
+ const [showPatientForm, setShowPatientForm] = useState(false);
useEffect(() => {
document.addEventListener("keydown", (event) => {
@@ -242,6 +248,7 @@ export default function AgendamentoPage() {
events={threeDEvents}
onAddEvent={handleAddEvent}
onRemoveEvent={handleRemoveEvent}
+ onOpenAddPatientForm={() => setShowPatientForm(true)}
/>
) : (
@@ -253,6 +260,17 @@ export default function AgendamentoPage() {
/>
)}
+
+ {/* Formulário de Registro de Paciente */}
+
{
+ console.log('[Calendar] Novo paciente registrado:', newPaciente);
+ setShowPatientForm(false);
+ }}
+ />
);
diff --git a/susconecta/app/globals.css b/susconecta/app/globals.css
index df339bd..d41795d 100644
--- a/susconecta/app/globals.css
+++ b/susconecta/app/globals.css
@@ -123,3 +123,47 @@
@apply bg-background text-foreground font-sans;
}
}
+
+/* Esconder botões com ícones de lixo */
+button:has(.lucide-trash2),
+button:has(.lucide-trash),
+button[class*="trash"] {
+ display: none !important;
+}
+
+/* Esconder campos de input embaixo do calendário 3D */
+input[placeholder="Nome do paciente"],
+input[placeholder^="dd/mm"],
+input[type="date"][value=""] {
+ display: none !important;
+}
+
+/* Esconder botão "Adicionar Paciente" */
+/* Removido seletor vazio - será tratado por outros seletores */
+
+/* Afastar X do popup (dialog-close) para longe das setas */
+[data-slot="dialog-close"],
+button[aria-label="Close"],
+.fc button[aria-label*="Close"] {
+ right: 16px !important;
+ top: 8px !important;
+ position: absolute !important;
+}
+
+/* Esconder footer/header extras do calendário que mostram os campos */
+.fc .fc-toolbar input,
+.fc .fc-toolbar [type="date"],
+.fc .fc-toolbar [placeholder*="paciente"] {
+ display: none !important;
+}
+
+/* Esconder row com campos de pesquisa - estrutura mantida pelo calendário */
+
+/* Esconder botões de trash/delete em todos os popups */
+[role="dialog"] button[class*="hover:text-destructive"],
+[role="dialog"] button[aria-label*="delete"],
+[role="dialog"] button[aria-label*="excluir"],
+[role="dialog"] button[aria-label*="remove"] {
+ display: none !important;
+}
+
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx
index 697c02f..d44be28 100644
--- a/susconecta/app/paciente/page.tsx
+++ b/susconecta/app/paciente/page.tsx
@@ -1429,95 +1429,192 @@ export default function PacientePage() {
function Perfil() {
- const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep)
return (
-
+
+ {/* Header com Título e Botão */}
-
Meu Perfil
+
+
Meu Perfil
+
Bem-vindo à sua área exclusiva.
+
{!isEditingProfile ? (
-
setIsEditingProfile(true)} className="flex items-center gap-2">
- Editar Perfil
+ setIsEditingProfile(true)}
+ >
+ ✏️ Editar Perfil
) : (
-
- Salvar
-
+
+ ✓ Salvar
+
+
- Cancelar
+ ✕ Cancelar
)}
-
- {/* Informações Pessoais */}
-
-
Informações Pessoais
-
-
Nome Completo
-
{profileData.nome}
-
Este campo não pode ser alterado
+
+ {/* Grid de 3 colunas (2 + 1) */}
+
+ {/* Coluna Esquerda - Informações Pessoais */}
+
+ {/* Informações Pessoais */}
+
+
Informações Pessoais
+
+
+ {/* Nome Completo */}
+
+
+ Nome Completo
+
+
+ {profileData.nome || "Não preenchido"}
+
+
+ Este campo não pode ser alterado
+
+
+
+ {/* Email */}
+
+
+ Email
+
+
+ {profileData.email || "Não preenchido"}
+
+
+ Este campo não pode ser alterado
+
+
+
+ {/* Telefone */}
+
+
+ Telefone
+
+ {isEditingProfile ? (
+
handleProfileChange('telefone', e.target.value)}
+ className="mt-2"
+ placeholder="(00) 00000-0000"
+ maxLength={15}
+ />
+ ) : (
+
+ {profileData.telefone || "Não preenchido"}
+
+ )}
+
+
-
-
Email
- {isEditingProfile ? (
-
handleProfileChange('email', e.target.value)} />
- ) : (
-
{profileData.email}
- )}
+
+ {/* Endereço e Contato */}
+
+
Endereço e Contato
+
+
+ {/* Logradouro */}
+
+
+ Logradouro
+
+ {isEditingProfile ? (
+
handleProfileChange('endereco', e.target.value)}
+ className="mt-2"
+ placeholder="Rua, avenida, etc."
+ />
+ ) : (
+
+ {profileData.endereco || "Não preenchido"}
+
+ )}
+
+
+ {/* Cidade */}
+
+
+ Cidade
+
+ {isEditingProfile ? (
+
handleProfileChange('cidade', e.target.value)}
+ className="mt-2"
+ placeholder="São Paulo"
+ />
+ ) : (
+
+ {profileData.cidade || "Não preenchido"}
+
+ )}
+
+
+ {/* CEP */}
+
+
+ CEP
+
+ {isEditingProfile ? (
+
handleProfileChange('cep', e.target.value)}
+ className="mt-2"
+ placeholder="00000-000"
+ />
+ ) : (
+
+ {profileData.cep || "Não preenchido"}
+
+ )}
+
+
-
- Telefone
+
+
+ {/* Coluna Direita - Foto do Perfil */}
+
+
+
Foto do Perfil
+
{isEditingProfile ? (
-
handleProfileChange('telefone', e.target.value)} />
+
+ handleProfileChange('foto_url', newUrl)}
+ userName={profileData.nome}
+ />
+
) : (
-
{profileData.telefone}
+
+
+
+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'}
+
+
+
+
+
+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'}
+
+
+
)}
- {/* Endereço e Contato (render apenas se existir algum dado) */}
- {hasAddress && (
-
-
Endereço
-
-
Endereço
- {isEditingProfile ? (
-
handleProfileChange('endereco', e.target.value)} />
- ) : (
-
{profileData.endereco}
- )}
-
-
-
Cidade
- {isEditingProfile ? (
-
handleProfileChange('cidade', e.target.value)} />
- ) : (
-
{profileData.cidade}
- )}
-
-
-
CEP
- {isEditingProfile ? (
-
handleProfileChange('cep', e.target.value)} />
- ) : (
-
{profileData.cep}
- )}
-
- {/* Biografia removed: not used */}
-
- )}
-
- {/* Foto do Perfil */}
-
-
Foto do Perfil
- handleProfileChange('foto_url', newUrl)}
- userName={profileData.nome}
- />
)
diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx
index 485fc90..17e9860 100644
--- a/susconecta/app/profissional/page.tsx
+++ b/susconecta/app/profissional/page.tsx
@@ -25,7 +25,7 @@ import {
} from "@/components/ui/table";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
-import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, Trash2, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from "lucide-react"
+import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from "lucide-react"
import { Calendar as CalendarIcon, FileText, Settings } from "lucide-react";
import {
Tooltip,
@@ -41,6 +41,7 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
+import { PatientRegistrationForm } from "@/components/features/forms/patient-registration-form";
const FullCalendar = dynamic(() => import("@fullcalendar/react"), {
ssr: false,
@@ -230,7 +231,7 @@ const ProfissionalPage = () => {
})();
return () => { mounted = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [user?.email, user?.id]);
@@ -378,6 +379,9 @@ const ProfissionalPage = () => {
const [editingEvent, setEditingEvent] = useState
(null);
const [showPopup, setShowPopup] = useState(false);
const [showActionModal, setShowActionModal] = useState(false);
+ const [showDayModal, setShowDayModal] = useState(false);
+ const [selectedDayDate, setSelectedDayDate] = useState(null);
+ const [showPatientForm, setShowPatientForm] = useState(false);
const [step, setStep] = useState(1);
const [newEvent, setNewEvent] = useState({
title: "",
@@ -686,6 +690,13 @@ const ProfissionalPage = () => {
Agenda do Dia
+
setShowPatientForm(true)}
+ className="flex items-center gap-2"
+ >
+
+ Adicionar Paciente
+
{/* Navegação de Data */}
@@ -718,7 +729,7 @@ const ProfissionalPage = () => {
{/* Lista de Pacientes do Dia */}
-
+
{todayEvents.length === 0 ? (
@@ -2656,150 +2667,216 @@ const ProfissionalPage = () => {
const renderPerfilSection = () => (
-
+
+ {/* Header com Título e Botão */}
-
Meu Perfil
+
+
Meu Perfil
+
Bem-vindo à sua área exclusiva.
+
{!isEditingProfile ? (
-
setIsEditingProfile(true)} className="flex items-center gap-2">
-
- Editar Perfil
+ setIsEditingProfile(true)}
+ >
+ ✏️ Editar Perfil
) : (
-
- Salvar
+
+ ✓ Salvar
-
- Cancelar
+
+ ✕ Cancelar
)}
-
- {/* Informações Pessoais */}
-
-
Informações Pessoais
-
-
-
Nome Completo
-
{profileData.nome}
-
Este campo não pode ser alterado
-
+ {/* Grid de 3 colunas (2 + 1) */}
+
+ {/* Coluna Esquerda - Informações Pessoais */}
+
+ {/* Informações Pessoais */}
+
+
Informações Pessoais
-
-
Email
- {isEditingProfile ? (
-
handleProfileChange('email', e.target.value)}
- />
- ) : (
-
{profileData.email}
- )}
-
+
+ {/* Nome Completo */}
+
+
+ Nome Completo
+
+
+ {profileData.nome || "Não preenchido"}
+
+
+ Este campo não pode ser alterado
+
+
-
-
Telefone
- {isEditingProfile ? (
-
handleProfileChange('telefone', e.target.value)}
- />
- ) : (
-
{profileData.telefone}
- )}
-
+ {/* Email */}
+
+
+ Email
+
+ {isEditingProfile ? (
+
handleProfileChange('email', e.target.value)}
+ className="mt-2"
+ type="email"
+ />
+ ) : (
+
+ {profileData.email || "Não preenchido"}
+
+ )}
+
-
-
CRM
-
{profileData.crm}
-
Este campo não pode ser alterado
-
+ {/* Telefone */}
+
+
+ Telefone
+
+ {isEditingProfile ? (
+
handleProfileChange('telefone', e.target.value)}
+ className="mt-2"
+ placeholder="(00) 00000-0000"
+ />
+ ) : (
+
+ {profileData.telefone || "Não preenchido"}
+
+ )}
+
-
-
Especialidade
- {isEditingProfile ? (
-
handleProfileChange('especialidade', e.target.value)}
- />
- ) : (
-
{profileData.especialidade}
- )}
-
-
+ {/* CRM */}
+
+
+ CRM
+
+
+ {profileData.crm || "Não preenchido"}
+
+
+ Este campo não pode ser alterado
+
+
- {/* Endereço e Contato */}
-
-
Endereço e Contato
-
-
-
Endereço
- {isEditingProfile ? (
-
handleProfileChange('endereco', e.target.value)}
- />
- ) : (
-
{profileData.endereco}
- )}
-
-
-
-
Cidade
- {isEditingProfile ? (
-
handleProfileChange('cidade', e.target.value)}
- />
- ) : (
-
{profileData.cidade}
- )}
-
-
-
-
CEP
- {isEditingProfile ? (
-
handleProfileChange('cep', e.target.value)}
- />
- ) : (
-
{profileData.cep}
- )}
-
-
- {/* Biografia removida: não é um campo no registro de médico */}
-
-
-
- {/* Foto do Perfil */}
-
-
Foto do Perfil
-
-
-
- {profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
-
-
- {isEditingProfile && (
-
-
- Alterar Foto
-
-
- Formatos aceitos: JPG, PNG (máx. 2MB)
-
+ {/* Especialidade */}
+
+
+ Especialidade
+
+ {isEditingProfile ? (
+
handleProfileChange('especialidade', e.target.value)}
+ className="mt-2"
+ placeholder="Ex: Cardiologia"
+ />
+ ) : (
+
+ {profileData.especialidade || "Não preenchido"}
+
+ )}
+
- )}
+
+
+ {/* Endereço e Contato */}
+
+
Endereço e Contato
+
+
+ {/* Logradouro */}
+
+
+ Logradouro
+
+ {isEditingProfile ? (
+
handleProfileChange('endereco', e.target.value)}
+ className="mt-2"
+ placeholder="Rua, avenida, etc."
+ />
+ ) : (
+
+ {profileData.endereco || "Não preenchido"}
+
+ )}
+
+
+ {/* Cidade */}
+
+
+ Cidade
+
+ {isEditingProfile ? (
+
handleProfileChange('cidade', e.target.value)}
+ className="mt-2"
+ placeholder="São Paulo"
+ />
+ ) : (
+
+ {profileData.cidade || "Não preenchido"}
+
+ )}
+
+
+ {/* CEP */}
+
+
+ CEP
+
+ {isEditingProfile ? (
+
handleProfileChange('cep', e.target.value)}
+ className="mt-2"
+ placeholder="00000-000"
+ />
+ ) : (
+
+ {profileData.cep || "Não preenchido"}
+
+ )}
+
+
+
+
+
+ {/* Coluna Direita - Foto do Perfil */}
+
+
+
Foto do Perfil
+
+
+
+
+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'}
+
+
+
+
+
+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'}
+
+
+
+
@@ -2838,8 +2915,8 @@ const ProfissionalPage = () => {
);
case 'laudos':
return renderLaudosSection();
- case 'comunicacao':
- return renderComunicacaoSection();
+ // case 'comunicacao':
+ // return renderComunicacaoSection();
case 'perfil':
return renderPerfilSection();
default:
@@ -2910,14 +2987,15 @@ const ProfissionalPage = () => {
Laudos
-
setActiveSection('comunicacao')}
>
Comunicação
-
+ */}
{
Editar
-
-
- Excluir
-
{
)}
+
+ {/* Modal para visualizar pacientes de um dia específico */}
+ {showDayModal && selectedDayDate && (
+
+
+ {/* Header com navegação */}
+
+
{
+ const prev = new Date(selectedDayDate);
+ prev.setDate(prev.getDate() - 1);
+ setSelectedDayDate(prev);
+ }}
+ className="p-2 hover:bg-muted rounded transition-colors"
+ aria-label="Dia anterior"
+ >
+
+
+
+
+ {selectedDayDate.toLocaleDateString('pt-BR', {
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric'
+ })}
+
+
+
{
+ const next = new Date(selectedDayDate);
+ next.setDate(next.getDate() + 1);
+ setSelectedDayDate(next);
+ }}
+ className="p-2 hover:bg-muted rounded transition-colors"
+ aria-label="Próximo dia"
+ >
+
+
+
+
+
+
setShowDayModal(false)}
+ className="p-2 hover:bg-muted rounded transition-colors ml-2"
+ aria-label="Fechar"
+ >
+
+
+
+
+ {/* Content */}
+
+ {(() => {
+ const dayStr = selectedDayDate.toISOString().split('T')[0];
+ const dayEvents = events.filter(e => e.date === dayStr).sort((a, b) => a.time.localeCompare(b.time));
+
+ if (dayEvents.length === 0) {
+ return (
+
+
+
Nenhuma consulta agendada para este dia
+
+ );
+ }
+
+ return (
+
+
+ {dayEvents.length} consulta{dayEvents.length !== 1 ? 's' : ''} agendada{dayEvents.length !== 1 ? 's' : ''}
+
+ {dayEvents.map((appointment) => {
+ const paciente = pacientes.find(p => p.nome === appointment.title);
+ return (
+
+
+
+
+
+ {appointment.title}
+
+
+ {appointment.type}
+
+
+
+
+
+ {appointment.time}
+
+ {paciente && (
+
+ CPF: {getPatientCpf(paciente)} • {getPatientAge(paciente)} anos
+
+ )}
+
+
+
+ );
+ })}
+
+ );
+ })()}
+
+
+
+ )}
+
+ {/* Formulário para cadastro de paciente */}
+
{
+ // Adicionar o novo paciente à lista e recarregar
+ setPacientes((prev) => [...prev, newPaciente]);
+ }}
+ />
);
diff --git a/susconecta/components/features/forms/patient-registration-form.tsx b/susconecta/components/features/forms/patient-registration-form.tsx
index 1ff1dde..2e82998 100644
--- a/susconecta/components/features/forms/patient-registration-form.tsx
+++ b/susconecta/components/features/forms/patient-registration-form.tsx
@@ -264,7 +264,17 @@ export function PatientRegistrationForm({
}
async function handleSubmit(ev: React.FormEvent) {
- ev.preventDefault(); if (!validateLocal()) return;
+ ev.preventDefault();
+ if (!validateLocal()) return;
+
+ // Debug: verificar se token está disponível
+ const tokenCheck = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')) : null;
+ console.debug('[PatientForm] Token disponível?', !!tokenCheck ? 'SIM' : 'NÃO - Possível causa do erro!');
+ if (!tokenCheck) {
+ setErrors({ submit: 'Sessão expirada. Por favor, faça login novamente.' });
+ return;
+ }
+
try {
if (!validarCPFLocal(form.cpf)) { setErrors((e) => ({ ...e, cpf: "CPF inválido" })); return; }
if (mode === "create") { const existe = await verificarCpfDuplicado(form.cpf); if (existe) { setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" })); return; } }
diff --git a/susconecta/components/ui/three-dwall-calendar.tsx b/susconecta/components/ui/three-dwall-calendar.tsx
index 35c23f3..aa1a7e9 100644
--- a/susconecta/components/ui/three-dwall-calendar.tsx
+++ b/susconecta/components/ui/three-dwall-calendar.tsx
@@ -25,6 +25,7 @@ interface ThreeDWallCalendarProps {
events: CalendarEvent[]
onAddEvent?: (e: CalendarEvent) => void
onRemoveEvent?: (id: string) => void
+ onOpenAddPatientForm?: () => void
panelWidth?: number
panelHeight?: number
columns?: number
@@ -34,6 +35,7 @@ export function ThreeDWallCalendar({
events,
onAddEvent,
onRemoveEvent,
+ onOpenAddPatientForm,
panelWidth = 160,
panelHeight = 120,
columns = 7,
@@ -448,9 +450,17 @@ export function ThreeDWallCalendar({
{/* Add event form */}
- setTitle(e.target.value)} />
- setNewDate(e.target.value)} />
- Adicionar Paciente
+ {onOpenAddPatientForm ? (
+
+ Adicionar Paciente
+
+ ) : (
+ <>
+ setTitle(e.target.value)} />
+ setNewDate(e.target.value)} />
+ Adicionar Paciente
+ >
+ )}
)
diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts
index 2b1cfd7..1cf4dd3 100644
--- a/susconecta/lib/api.ts
+++ b/susconecta/lib/api.ts
@@ -1554,6 +1554,15 @@ export async function criarPaciente(input: PacienteInput): Promise
{
];
let lastErr: any = null;
+
+ // Debug: verificar token antes de tentar
+ const debugToken = getAuthToken();
+ if (!debugToken) {
+ console.warn('[criarPaciente] ⚠️ AVISO: Nenhum token de autenticação encontrado no localStorage/sessionStorage! Tentando mesmo assim, mas possível causa do erro.');
+ } else {
+ console.debug('[criarPaciente] ✓ Token encontrado, comprimento:', debugToken.length);
+ }
+
for (const u of fnUrls) {
try {
const headers = { ...baseHeaders(), 'Content-Type': 'application/json' } as Record;
@@ -1562,7 +1571,7 @@ export async function criarPaciente(input: PacienteInput): Promise {
const a = maskedHeaders.Authorization as string;
maskedHeaders.Authorization = `${a.slice(0,6)}...${a.slice(-6)}`;
}
- // Log removido por segurança
+ console.debug('[criarPaciente] Tentando criar paciente em:', u.replace(/^https:\/\/[^\/]+/, 'https://[...host...]'));
const res = await fetch(u, {
method: 'POST',
headers,
@@ -1601,17 +1610,37 @@ export async function criarPaciente(input: PacienteInput): Promise {
} catch (err: any) {
lastErr = err;
const emsg = err && typeof err === 'object' && 'message' in err ? (err as any).message : String(err);
- console.warn('[criarPaciente] tentativa em', u, 'falhou:', emsg);
- // If the underlying error is a network/CORS issue, add a helpful hint in the log
- if (emsg && emsg.toLowerCase().includes('failed to fetch')) {
- console.error('[criarPaciente] Falha de fetch (network/CORS). Verifique se você está autenticado no navegador (token presente em localStorage/sessionStorage) e se o endpoint permite requisições CORS do seu domínio. Também confirme que a função /create-user-with-password existe e está acessível.');
+ console.warn('[criarPaciente] ❌ Tentativa em', u, 'falhou:', emsg);
+
+ // Se o erro é uma falha de fetch (network/CORS)
+ if (emsg && (emsg.toLowerCase().includes('failed to fetch') || emsg.toLowerCase().includes('networkerror'))) {
+ console.error('[criarPaciente] ⚠️ FALHA DE REDE/CORS detectada. Possíveis causas:\n' +
+ '1. Função Supabase /create-user-with-password não existe ou está desativada\n' +
+ '2. CORS configurado incorretamente no Supabase\n' +
+ '3. Endpoint indisponível ou a rede está offline\n' +
+ '4. Token expirado ou inválido\n' +
+ 'URL que falhou:', u);
}
// try next
}
}
const emsg = lastErr && typeof lastErr === 'object' && 'message' in lastErr ? (lastErr as any).message : String(lastErr ?? 'sem detalhes');
- throw new Error(`Falha ao criar paciente via create-user-with-password: ${emsg}. Verifique autenticação (token no localStorage/sessionStorage), CORS e se o endpoint /functions/v1/create-user-with-password está implementado e aceitando requisições do navegador.`);
+
+ // Mensagem de erro mais detalhada e útil
+ let friendlyMsg = `Falha ao criar paciente.`;
+ if (emsg.toLowerCase().includes('networkerror') || emsg.toLowerCase().includes('failed to fetch')) {
+ friendlyMsg += ` Erro de rede/CORS detectado. `;
+ friendlyMsg += `Verifique se:\n`;
+ friendlyMsg += `• A função /create-user-with-password existe no Supabase\n`;
+ friendlyMsg += `• Você está autenticado (token no localStorage)\n`;
+ friendlyMsg += `• CORS está configurado corretamente\n`;
+ friendlyMsg += `• A rede está disponível`;
+ } else {
+ friendlyMsg += ` ${emsg}`;
+ }
+
+ throw new Error(friendlyMsg);
}
export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise {
--
2.47.2
From c6aacc7df46ae2494d196cbee937ed188abb7117 Mon Sep 17 00:00:00 2001
From: M-Gabrielly
Date: Thu, 6 Nov 2025 01:15:24 -0300
Subject: [PATCH 9/9] fix(calendar): fix import
---
susconecta/app/(main-routes)/calendar/page.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx
index 30bccef..d2349f5 100644
--- a/susconecta/app/(main-routes)/calendar/page.tsx
+++ b/susconecta/app/(main-routes)/calendar/page.tsx
@@ -2,6 +2,7 @@
// 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";
--
2.47.2