diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index aa7380c..30bccef 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -2,26 +2,16 @@ // 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 { useAuth } from "@/hooks/useAuth"; 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 import { PatientRegistrationForm } from "@/components/features/forms/patient-registration-form"; @@ -33,11 +23,24 @@ 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"); - + 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([]); @@ -48,15 +51,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"); }); }, []); @@ -92,18 +88,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); @@ -152,10 +153,6 @@ export default function AgendamentoPage() { } }; - const handleNotifyPatient = (patientId: string) => { - console.log(`Notificando paciente ${patientId}`); - }; - const handleAddEvent = (event: CalendarEvent) => { setThreeDEvents((prev) => [...prev, event]); }; @@ -178,26 +175,10 @@ export default function AgendamentoPage() { Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).

-
- - - Opções » - - - - Agendamento - - - Procedimento - - - Financeiro - - - - +
+
+
+
- + {/* Legenda de status (estilo Google Calendar) */} +
+
+
+ + Solicitado +
+
+ + Confirmado
@@ -251,14 +239,7 @@ export default function AgendamentoPage() { onOpenAddPatientForm={() => setShowPatientForm(true)} /> - ) : ( - // A Lista de Espera foi MANTIDA - {}} - /> - )} + ) : null} {/* Formulário de Registro de Paciente */} diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index d44be28..a6e8ccb 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 removed by request */} {consulta.status !== 'Cancelada' && ( )} + + {/* 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...
+ )} +
+
+ +
+ +
+
+
+
+ + {/* 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() { - - - {/* 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) */}
+ + diff --git a/susconecta/components/event-manager.tsx b/susconecta/components/event-manager.tsx new file mode 100644 index 0000000..1a19417 --- /dev/null +++ b/susconecta/components/event-manager.tsx @@ -0,0 +1,1485 @@ +"use client" + +import React, { useState, useCallback, useMemo } from "react" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Grid3x3, List, Search, Filter, X } from "lucide-react" +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuCheckboxItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export interface Event { + id: string + title: string + description?: string + startTime: Date + endTime: Date + color: string + category?: string + attendees?: string[] + tags?: string[] +} + +export interface EventManagerProps { + events?: Event[] + onEventCreate?: (event: Omit) => void + onEventUpdate?: (id: string, event: Partial) => void + onEventDelete?: (id: string) => void + categories?: string[] + colors?: { name: string; value: string; bg: string; text: string }[] + defaultView?: "month" | "week" | "day" | "list" + className?: string + availableTags?: string[] +} + +const defaultColors = [ + { name: "Blue", value: "blue", bg: "bg-blue-500", text: "text-blue-700" }, + { name: "Green", value: "green", bg: "bg-green-500", text: "text-green-700" }, + { name: "Purple", value: "purple", bg: "bg-purple-500", text: "text-purple-700" }, + { name: "Orange", value: "orange", bg: "bg-orange-500", text: "text-orange-700" }, + { name: "Pink", value: "pink", bg: "bg-pink-500", text: "text-pink-700" }, + { name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" }, +] + +export function EventManager({ + events: initialEvents = [], + onEventCreate, + onEventUpdate, + onEventDelete, + categories = ["Meeting", "Task", "Reminder", "Personal"], + colors = defaultColors, + defaultView = "month", + className, + availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"], +}: EventManagerProps) { + const [events, setEvents] = useState(initialEvents) + const [currentDate, setCurrentDate] = useState(new Date()) + const [view, setView] = useState<"month" | "week" | "day" | "list">(defaultView) + const [selectedEvent, setSelectedEvent] = useState(null) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [isCreating, setIsCreating] = useState(false) + const [draggedEvent, setDraggedEvent] = useState(null) + const [newEvent, setNewEvent] = useState>({ + title: "", + description: "", + color: colors[0].value, + category: categories[0], + tags: [], + }) + + const [searchQuery, setSearchQuery] = useState("") + const [selectedColors, setSelectedColors] = useState([]) + const [selectedTags, setSelectedTags] = useState([]) + const [selectedCategories, setSelectedCategories] = useState([]) + + const filteredEvents = useMemo(() => { + return events.filter((event) => { + // Search filter + if (searchQuery) { + const query = searchQuery.toLowerCase() + const matchesSearch = + event.title.toLowerCase().includes(query) || + event.description?.toLowerCase().includes(query) || + event.category?.toLowerCase().includes(query) || + event.tags?.some((tag) => tag.toLowerCase().includes(query)) + + if (!matchesSearch) return false + } + + // Color filter + if (selectedColors.length > 0 && !selectedColors.includes(event.color)) { + return false + } + + // Tag filter + if (selectedTags.length > 0) { + const hasMatchingTag = event.tags?.some((tag) => selectedTags.includes(tag)) + if (!hasMatchingTag) return false + } + + // Category filter + if (selectedCategories.length > 0 && event.category && !selectedCategories.includes(event.category)) { + return false + } + + return true + }) + }, [events, searchQuery, selectedColors, selectedTags, selectedCategories]) + + const hasActiveFilters = selectedColors.length > 0 || selectedTags.length > 0 || selectedCategories.length > 0 + + const clearFilters = () => { + setSelectedColors([]) + setSelectedTags([]) + setSelectedCategories([]) + setSearchQuery("") + } + + const handleCreateEvent = useCallback(() => { + if (!newEvent.title || !newEvent.startTime || !newEvent.endTime) return + + const event: Event = { + id: Math.random().toString(36).substr(2, 9), + title: newEvent.title, + description: newEvent.description, + startTime: newEvent.startTime, + endTime: newEvent.endTime, + color: newEvent.color || colors[0].value, + category: newEvent.category, + attendees: newEvent.attendees, + tags: newEvent.tags || [], + } + + setEvents((prev) => [...prev, event]) + onEventCreate?.(event) + setIsDialogOpen(false) + setIsCreating(false) + setNewEvent({ + title: "", + description: "", + color: colors[0].value, + category: categories[0], + tags: [], + }) + }, [newEvent, colors, categories, onEventCreate]) + + const handleUpdateEvent = useCallback(() => { + if (!selectedEvent) return + + setEvents((prev) => prev.map((e) => (e.id === selectedEvent.id ? selectedEvent : e))) + onEventUpdate?.(selectedEvent.id, selectedEvent) + setIsDialogOpen(false) + setSelectedEvent(null) + }, [selectedEvent, onEventUpdate]) + + const handleDeleteEvent = useCallback( + (id: string) => { + setEvents((prev) => prev.filter((e) => e.id !== id)) + onEventDelete?.(id) + setIsDialogOpen(false) + setSelectedEvent(null) + }, + [onEventDelete], + ) + + const handleDragStart = useCallback((event: Event) => { + setDraggedEvent(event) + }, []) + + const handleDragEnd = useCallback(() => { + setDraggedEvent(null) + }, []) + + const handleDrop = useCallback( + (date: Date, hour?: number) => { + if (!draggedEvent) return + + const duration = draggedEvent.endTime.getTime() - draggedEvent.startTime.getTime() + const newStartTime = new Date(date) + if (hour !== undefined) { + newStartTime.setHours(hour, 0, 0, 0) + } + const newEndTime = new Date(newStartTime.getTime() + duration) + + const updatedEvent = { + ...draggedEvent, + startTime: newStartTime, + endTime: newEndTime, + } + + setEvents((prev) => prev.map((e) => (e.id === draggedEvent.id ? updatedEvent : e))) + onEventUpdate?.(draggedEvent.id, updatedEvent) + setDraggedEvent(null) + }, + [draggedEvent, onEventUpdate], + ) + + const navigateDate = useCallback( + (direction: "prev" | "next") => { + setCurrentDate((prev) => { + const newDate = new Date(prev) + if (view === "month") { + newDate.setMonth(prev.getMonth() + (direction === "next" ? 1 : -1)) + } else if (view === "week") { + newDate.setDate(prev.getDate() + (direction === "next" ? 7 : -7)) + } else if (view === "day") { + newDate.setDate(prev.getDate() + (direction === "next" ? 1 : -1)) + } + return newDate + }) + }, + [view], + ) + + const getColorClasses = useCallback( + (colorValue: string) => { + const color = colors.find((c) => c.value === colorValue) + return color || colors[0] + }, + [colors], + ) + + const toggleTag = (tag: string, isCreating: boolean) => { + if (isCreating) { + setNewEvent((prev) => ({ + ...prev, + tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag], + })) + } else { + setSelectedEvent((prev) => + prev + ? { + ...prev, + tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag], + } + : null, + ) + } + } + + return ( +
+ {/* Header */} +
+
+

+ {view === "month" && + currentDate.toLocaleDateString("pt-BR", { + month: "long", + year: "numeric", + })} + {view === "week" && + `Semana de ${currentDate.toLocaleDateString("pt-BR", { + month: "short", + day: "numeric", + })}`} + {view === "day" && + currentDate.toLocaleDateString("pt-BR", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + })} + {view === "list" && "Todos os eventos"} +

+
+ + + +
+
+ +
+ {/* Mobile: Select dropdown */} +
+ +
+ + {/* Desktop: Button group */} +
+ + + + +
+ + +
+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> + {searchQuery && ( + + )} +
+ + {/* Mobile: Horizontal scroll with full-length buttons */} +
+
+ {/* Color Filter */} + + + + + + Filtrar por Cor + + {colors.map((color) => ( + { + setSelectedColors((prev) => + checked ? [...prev, color.value] : prev.filter((c) => c !== color.value), + ) + }} + > +
+
+ {color.name} +
+ + ))} + + + + {/* Tag Filter */} + + + + + + Filtrar por Tag + + {availableTags.map((tag) => ( + { + setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag))) + }} + > + {tag} + + ))} + + + + {/* Category Filter */} + + + + + + Filtrar por Categoria + + {categories.map((category) => ( + { + setSelectedCategories((prev) => + checked ? [...prev, category] : prev.filter((c) => c !== category), + ) + }} + > + {category} + + ))} + + + + {hasActiveFilters && ( + + )} +
+
+ + {/* Desktop: Original layout */} +
+ {/* Color Filter */} + + + + + + Filtrar por Cor + + {colors.map((color) => ( + { + setSelectedColors((prev) => + checked ? [...prev, color.value] : prev.filter((c) => c !== color.value), + ) + }} + > +
+
+ {color.name} +
+ + ))} + + + + {/* Tag Filter */} + + + + + + Filtrar por Tag + + {availableTags.map((tag) => ( + { + setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag))) + }} + > + {tag} + + ))} + + + + {/* Category Filter */} + + + + + + Filtrar por Categoria + + {categories.map((category) => ( + { + setSelectedCategories((prev) => + checked ? [...prev, category] : prev.filter((c) => c !== category), + ) + }} + > + {category} + + ))} + + + + {hasActiveFilters && ( + + )} +
+
+ + {hasActiveFilters && ( +
+ Filtros ativos: + {selectedColors.map((colorValue) => { + const color = getColorClasses(colorValue) + return ( + +
+ {color.name} + + + ) + })} + {selectedTags.map((tag) => ( + + {tag} + + + ))} + {selectedCategories.map((category) => ( + + {category} + + + ))} +
+ )} + + {/* Calendar Views - Pass filteredEvents instead of events */} + {view === "month" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "week" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "day" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "list" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + getColorClasses={getColorClasses} + /> + )} + + {/* Event Dialog */} + + + + {isCreating ? "Criar Evento" : "Detalhes do Evento"} + + {isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do evento"} + + + +
+
+ + + isCreating + ? setNewEvent((prev) => ({ ...prev, title: e.target.value })) + : setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null)) + } + placeholder="Título do evento" + /> +
+ +
+ +