diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index d2349f5..0052212 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -2,30 +2,27 @@ // 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"; import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback // Imports mantidos -import { Button } from "@/components/ui/button"; -import { useAuth } from "@/hooks/useAuth"; -import { mockWaitingList } from "@/lib/mocks/appointment-mocks"; import "./index.css"; -import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"; // Calendário 3D mantido -import { PatientRegistrationForm } from "@/components/features/forms/patient-registration-form"; - -const ListaEspera = dynamic( - () => import("@/components/features/agendamento/ListaEspera"), - { ssr: false } -); export default function AgendamentoPage() { - const { user, token } = useAuth(); const [appointments, setAppointments] = useState([]); - const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar"); - const [threeDEvents, setThreeDEvents] = useState([]); + // REMOVIDO: abas e 3D → não há mais alternância de abas + // const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar"); + + // REMOVIDO: estados do 3D e formulário do paciente (eram usados pelo 3D) + // const [threeDEvents, setThreeDEvents] = useState([]); + // const [showPatientForm, setShowPatientForm] = useState(false); + + // --- NOVO ESTADO --- + // Estado para alimentar o NOVO EventManager com dados da API + const [managerEvents, setManagerEvents] = useState([]); + const [managerLoading, setManagerLoading] = useState(true); // Padroniza idioma da página para pt-BR (afeta componentes que usam o lang do documento) useEffect(() => { @@ -42,21 +39,6 @@ export default function AgendamentoPage() { } }, []); - // --- NOVO ESTADO --- - // 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) => { - if (event.key === "c") setActiveTab("calendar"); - if (event.key === "3") setActiveTab("3d"); - }); - }, []); - useEffect(() => { let mounted = true; (async () => { @@ -67,8 +49,8 @@ export default function AgendamentoPage() { if (!mounted) return; if (!arr || !arr.length) { setAppointments([]); - setThreeDEvents([]); - setManagerEvents([]); // Limpa o novo calendário + // REMOVIDO: setThreeDEvents([]) + setManagerEvents([]); setManagerLoading(false); return; } @@ -86,12 +68,11 @@ export default function AgendamentoPage() { const start = scheduled ? new Date(scheduled) : new Date(); const duration = Number(obj.duration_minutes ?? obj.duration ?? 30) || 30; const end = new Date(start.getTime() + duration * 60 * 1000); - + const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente'; const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim(); - // Mapeamento de cores padronizado: - // azul = solicitado; verde = confirmado; laranja = pendente; vermelho = cancelado; azul como fallback + // Mapeamento de cores padronizado const status = String(obj.status || "").toLowerCase(); let color: Event["color"] = "blue"; if (status === "confirmed" || status === "confirmado") color = "green"; @@ -112,27 +93,12 @@ export default function AgendamentoPage() { setManagerLoading(false); // --- FIM DA LÓGICA --- - // Convert to 3D calendar events (MANTIDO 100%) - const threeDEvents: CalendarEvent[] = (arr || []).map((obj: any) => { - const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null; - const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente'; - const appointmentType = obj.appointment_type ?? obj.type ?? 'Consulta'; - const title = `${patient}: ${appointmentType}`.trim(); - return { - id: obj.id || String(Date.now()), - title, - date: scheduled ? new Date(scheduled).toISOString() : new Date().toISOString(), - status: obj.status || 'pending', - patient, - type: appointmentType, - }; - }); - setThreeDEvents(threeDEvents); + // REMOVIDO: conversão para 3D e setThreeDEvents } catch (err) { console.warn('[AgendamentoPage] falha ao carregar agendamentos', err); setAppointments([]); - setThreeDEvents([]); - setManagerEvents([]); // Limpa o novo calendário + // REMOVIDO: setThreeDEvents([]) + setManagerEvents([]); setManagerLoading(false); } })(); @@ -154,12 +120,38 @@ export default function AgendamentoPage() { } }; - const handleAddEvent = (event: CalendarEvent) => { - setThreeDEvents((prev) => [...prev, event]); + // Mapeia cor do calendário -> status da API + const statusFromColor = (color?: string) => { + switch ((color || "").toLowerCase()) { + case "green": return "confirmed"; + case "orange": return "pending"; + case "red": return "canceled"; + default: return "requested"; + } }; - const handleRemoveEvent = (id: string) => { - setThreeDEvents((prev) => prev.filter((e) => e.id !== id)); + // Envia atualização para a API e atualiza UI + const handleEventUpdate = async (id: string, partial: Partial) => { + try { + const payload: any = {}; + if (partial.startTime) payload.scheduled_at = partial.startTime.toISOString(); + if (partial.startTime && partial.endTime) { + const minutes = Math.max(1, Math.round((partial.endTime.getTime() - partial.startTime.getTime()) / 60000)); + payload.duration_minutes = minutes; + } + if (partial.color) payload.status = statusFromColor(partial.color); + if (typeof partial.description === "string") payload.notes = partial.description; + + if (Object.keys(payload).length) { + const api = await import('@/lib/api'); + await api.atualizarAgendamento(id, payload); + } + + // Otimista: reflete mudanças locais + setManagerEvents((prev) => prev.map((e) => (e.id === id ? { ...e, ...partial } : e))); + } catch (e) { + console.warn("[Calendário] Falha ao atualizar agendamento na API:", e); + } }; return ( @@ -167,39 +159,17 @@ export default function AgendamentoPage() {
- {/* Todo o cabeçalho foi mantido */} + {/* Cabeçalho simplificado (sem 3D) */}
-

- {activeTab === "calendar" ? "Calendário" : activeTab === "3d" ? "Calendário 3D" : "Lista de Espera"} -

+

Calendário

- Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3). + Navegue através do atalho: Calendário (C).

-
-
- - - -
-
+ {/* REMOVIDO: botões de abas Calendário/3D */}
- {/* Legenda de status (estilo Google Calendar) */} + {/* Legenda de status (aplica-se ao EventManager) */}
@@ -210,49 +180,35 @@ export default function AgendamentoPage() { Confirmado
+ {/* Novo: Cancelado (vermelho) */} +
+ + Cancelado +
- {/* --- AQUI ESTÁ A SUBSTITUIÇÃO --- */} - {activeTab === "calendar" ? ( -
- {/* mostra loading até managerEvents ser preenchido (API integrada desde a entrada) */} -
- {managerLoading ? ( -
-
Conectando ao calendário — carregando agendamentos...
-
- ) : ( - // EventManager ocupa a área principal e já recebe events da API -
- -
- )} -
+ {/* Apenas o EventManager */} +
+
+ {managerLoading ? ( +
+
Conectando ao calendário — carregando agendamentos...
+
+ ) : ( +
+ +
+ )}
- ) : activeTab === "3d" ? ( - // O calendário 3D (ThreeDWallCalendar) foi MANTIDO 100% -
- setShowPatientForm(true)} - /> -
- ) : null} +
- - {/* Formulário de Registro de Paciente */} - { - console.log('[Calendar] Novo paciente registrado:', newPaciente); - setShowPatientForm(false); - }} - /> + + {/* REMOVIDO: PatientRegistrationForm (era acionado pelo 3D) */}
); diff --git a/susconecta/app/(main-routes)/doutores/page.tsx b/susconecta/app/(main-routes)/doutores/page.tsx index 5761f68..4adbd7d 100644 --- a/susconecta/app/(main-routes)/doutores/page.tsx +++ b/susconecta/app/(main-routes)/doutores/page.tsx @@ -145,7 +145,12 @@ export default function DoutoresPage() { const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); - + // NOVO: Ordenação e filtros + const [sortBy, setSortBy] = useState<"name_asc" | "name_desc" | "recent" | "oldest">("name_asc"); + const [stateFilter, setStateFilter] = useState(""); + const [cityFilter, setCityFilter] = useState(""); + const [specialtyFilter, setSpecialtyFilter] = useState(""); + async function load() { setLoading(true); try { @@ -272,47 +277,87 @@ export default function DoutoresPage() { }; }, [searchTimeout]); - // Lista de médicos a exibir (busca ou filtro local) + // NOVO: Opções dinâmicas + const stateOptions = useMemo( + () => + Array.from( + new Set((doctors || []).map((d) => (d.state || "").trim()).filter(Boolean)), + ).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })), + [doctors], + ); + + const cityOptions = useMemo(() => { + const base = (doctors || []).filter((d) => !stateFilter || String(d.state) === stateFilter); + return Array.from( + new Set(base.map((d) => (d.city || "").trim()).filter(Boolean)), + ).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })); + }, [doctors, stateFilter]); + + const specialtyOptions = useMemo( + () => + Array.from( + new Set((doctors || []).map((d) => (d.especialidade || "").trim()).filter(Boolean)), + ).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })), + [doctors], + ); + + // NOVO: Índice para ordenação por "tempo" (ordem de carregamento) + const indexById = useMemo(() => { + const map = new Map(); + (doctors || []).forEach((d, i) => map.set(String(d.id), i)); + return map; + }, [doctors]); + + // Lista de médicos a exibir com busca + filtros + ordenação const displayedDoctors = useMemo(() => { console.log('🔍 Filtro - search:', search, 'searchMode:', searchMode, 'doctors:', doctors.length, 'searchResults:', searchResults.length); - - // Se não tem busca, mostra todos os médicos - if (!search.trim()) return doctors; - + const q = search.toLowerCase().trim(); const qDigits = q.replace(/\D/g, ""); - - // Se estamos em modo de busca (servidor), filtra os resultados da busca const sourceList = searchMode ? searchResults : doctors; - console.log('🔍 Usando sourceList:', searchMode ? 'searchResults' : 'doctors', '- tamanho:', sourceList.length); - - const filtered = sourceList.filter((d) => { - // Busca por nome - const byName = (d.full_name || "").toLowerCase().includes(q); - - // Busca por CRM (remove formatação se necessário) - const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits); - - // Busca por ID (UUID completo ou parcial) - const byId = (d.id || "").toLowerCase().includes(q); - - // Busca por email - const byEmail = (d.email || "").toLowerCase().includes(q); - - // Busca por especialidade - const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q); - - const match = byName || byCrm || byId || byEmail || byEspecialidade; - if (match) { - console.log('✅ Match encontrado:', d.full_name, d.id, 'por:', { byName, byCrm, byId, byEmail, byEspecialidade }); - } - - return match; + + // 1) Busca + const afterSearch = !q + ? sourceList + : sourceList.filter((d) => { + const byName = (d.full_name || "").toLowerCase().includes(q); + const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits); + const byId = (d.id || "").toLowerCase().includes(q); + const byEmail = (d.email || "").toLowerCase().includes(q); + const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q); + const match = byName || byCrm || byId || byEmail || byEspecialidade; + if (match) console.log('✅ Match encontrado:', d.full_name, d.id); + return match; + }); + + // 2) Filtros de localização e especialidade + const afterFilters = afterSearch.filter((d) => { + if (stateFilter && String(d.state) !== stateFilter) return false; + if (cityFilter && String(d.city) !== cityFilter) return false; + if (specialtyFilter && String(d.especialidade) !== specialtyFilter) return false; + return true; }); - - console.log('🔍 Resultados filtrados:', filtered.length); - return filtered; - }, [doctors, search, searchMode, searchResults]); + + // 3) Ordenação + const sorted = [...afterFilters]; + if (sortBy === "name_asc" || sortBy === "name_desc") { + sorted.sort((a, b) => { + const an = (a.full_name || "").trim(); + const bn = (b.full_name || "").trim(); + const cmp = an.localeCompare(bn, "pt-BR", { sensitivity: "base" }); + return sortBy === "name_asc" ? cmp : -cmp; + }); + } else if (sortBy === "recent" || sortBy === "oldest") { + sorted.sort((a, b) => { + const ia = indexById.get(String(a.id)) ?? 0; + const ib = indexById.get(String(b.id)) ?? 0; + return sortBy === "recent" ? ia - ib : ib - ia; + }); + } + + console.log('🔍 Resultados filtrados:', sorted.length); + return sorted; + }, [doctors, search, searchMode, searchResults, stateFilter, cityFilter, specialtyFilter, sortBy, indexById]); // Dados paginados const paginatedDoctors = useMemo(() => { @@ -323,10 +368,10 @@ export default function DoutoresPage() { const totalPages = Math.ceil(displayedDoctors.length / itemsPerPage); - // Reset para página 1 quando mudar a busca ou itens por página + // Reset página ao mudar busca/filtros/ordenação useEffect(() => { setCurrentPage(1); - }, [search, itemsPerPage, searchMode]); + }, [search, itemsPerPage, searchMode, stateFilter, cityFilter, specialtyFilter, sortBy]); function handleAdd() { setEditingId(null); @@ -440,7 +485,7 @@ export default function DoutoresPage() {

Gerencie os médicos da sua clínica

-
+
@@ -473,6 +518,59 @@ export default function DoutoresPage() { )}
+ + {/* NOVO: Ordenar por */} + + + {/* NOVO: Especialidade */} + + + {/* NOVO: Estado (UF) */} + + + {/* NOVO: Cidade (dependente do estado) */} + +
-
+
+ {/* Busca */}
e.key === "Enter" && handleBuscarServidor()} />
- + + + {/* Ordenar por */} + + + {/* Estado (UF) */} + + + {/* Cidade (dependente do estado) */} + +
- {/* Lista de Pacientes do Dia - Responsiva */} -
+ {/* Lista de Pacientes do Dia */} +
{/* adicionada overflow-x-hidden */} {todayEvents.length === 0 ? (
@@ -1658,7 +1658,7 @@ const ProfissionalPage = () => { function LaudoViewer({ laudo, onClose }: { laudo: any; onClose: () => void }) { return (
-
+
{/* Header */}
diff --git a/susconecta/components/features/agenda/HeaderAgenda.tsx b/susconecta/components/features/agenda/HeaderAgenda.tsx index 928976d..f73a3f8 100644 --- a/susconecta/components/features/agenda/HeaderAgenda.tsx +++ b/susconecta/components/features/agenda/HeaderAgenda.tsx @@ -1,72 +1,10 @@ "use client"; -import { RotateCcw } from "lucide-react"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; - export default function HeaderAgenda() { - const pathname = usePathname(); - const router = useRouter(); - - const isAg = pathname?.startsWith("/agenda"); - const isPr = pathname?.startsWith("/procedimento"); - const isFi = pathname?.startsWith("/financeiro"); - return (

Novo Agendamento

- -
- - - -
); diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx index 697b2e6..a31663b 100644 --- a/susconecta/components/features/general/event-manager.tsx +++ b/susconecta/components/features/general/event-manager.tsx @@ -16,7 +16,7 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" -import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Grid3x3, List, Search, X } from "lucide-react" +import { ChevronLeft, ChevronRight, Calendar, Clock, Grid3x3, List, Search, X } from "lucide-react" import { cn } from "@/lib/utils" export interface Event { @@ -343,17 +343,6 @@ export function EventManager({ Lista
- -
@@ -515,11 +504,11 @@ export function EventManager({ {/* Event Dialog */} - + - {isCreating ? "Criar Evento" : "Detalhes do Evento"} + {isCreating ? "Criar Evento" : "Detalhes do Agendamento"} - {isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do evento"} + {isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do agendamento"} @@ -528,7 +517,7 @@ export function EventManager({ isCreating ? setNewEvent((prev) => ({ ...prev, title: e.target.value })) @@ -542,7 +531,7 @@ export function EventManager({