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)/dashboard/relatorios/page.tsx b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx index c70081a..a15da37 100644 --- a/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx +++ b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx @@ -520,28 +520,30 @@ export default function RelatoriosPage() { {/* Performance por médico */}
-
+

Performance por Médico

- +
- - - - - - - - - - {(loading ? performancePorMedico : medicosPerformance).map((m) => ( - - - - +
+
MédicoConsultasAbsenteísmo (%)
{m.nome}{m.consultas}{m.absenteismo}
+ + + + + - ))} - -
MédicoConsultasAbsenteísmo (%)
+ + + {(loading ? performancePorMedico : medicosPerformance).map((m) => ( + + {m.nome} + {m.consultas} + {m.absenteismo} + + ))} + + +
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) */} + + +
+ + ) + } + + // Extract fields with fallbacks + const reportDate = new Date(report.report_date || report.created_at || Date.now()).toLocaleDateString('pt-BR') + const cid = report.cid ?? report.cid_code ?? report.cidCode ?? report.cie ?? '' + const exam = report.exam ?? report.exame ?? report.especialidade ?? report.report_type ?? '' + const diagnosis = report.diagnosis ?? report.diagnostico ?? report.diagnosis_text ?? report.diagnostico_text ?? '' + const conclusion = report.conclusion ?? report.conclusao ?? report.conclusion_text ?? report.conclusao_text ?? '' + const notesHtml = report.content_html ?? report.conteudo_html ?? report.contentHtml ?? null + const notesText = report.content ?? report.body ?? report.conteudo ?? report.notes ?? report.observacoes ?? '' + + // Extract doctor name with multiple fallbacks + let doctorName = '' + if (doctor) { + doctorName = doctor.full_name || doctor.name || doctor.fullName || doctor.doctor_name || '' + } + if (!doctorName) { + const rd = report as any + const tryKeys = [ + 'doctor_name', 'doctor_full_name', 'doctorFullName', 'doctorName', + 'requested_by_name', 'requested_by', 'requester_name', 'requester', + 'created_by_name', 'created_by', 'executante', 'executante_name', + ] + for (const k of tryKeys) { + const v = rd[k] + if (v !== undefined && v !== null && String(v).trim() !== '') { + doctorName = String(v) + break + } + } + } + + return ( + +
+ {/* Header Toolbar */} +
+
+ {/* Left Section */} +
+ +
+
+

Laudo Médico

+

+ {doctorName || 'Profissional'} +

+
+
+ + {/* Right Section */} +
+ + +
+
+
+ + {/* Main Content Area */} +
+ {/* Document Container */} +
+ {/* Document Content */} +
+ + {/* Title */} +
+

+ RELATÓRIO MÉDICO +

+
+

+ Data: {reportDate} +

+ {doctorName && ( +

+ Profissional:{' '} + {doctorName} +

+ )} +
+
+ + {/* Patient/Header Info */} +
+
+ {cid && ( +
+ +

+ {cid} +

+
+ )} + {exam && ( +
+ +

+ {exam} +

+
+ )} +
+
+ + {/* Diagnosis Section */} + {diagnosis && ( +
+

Diagnóstico

+
+ {diagnosis} +
+
+ )} + + {/* Conclusion Section */} + {conclusion && ( +
+

Conclusão

+
+ {conclusion} +
+
+ )} + + {/* Notes/Content Section */} + {(notesHtml || notesText) && ( +
+

Notas do Profissional

+ {notesHtml ? ( +
+ ) : ( +
+ {notesText} +
+ )} +
+ )} + + {/* Signature Section */} + {report.doctor_signature && ( +
+
+
+ Assinatura do profissional +
+ {doctorName && ( +
+

+ {doctorName} +

+ {doctor?.crm && ( +

+ CRM: {doctor.crm} +

+ )} +
+ )} +
+
+ )} + + {/* Footer */} +
+

+ Documento gerado em {new Date().toLocaleString('pt-BR')} +

+
+ +
+
+
+
+ + ) +} diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index a6e8ccb..ab0e481 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -932,6 +932,7 @@ export default function PacientePage() { const [selectedReport, setSelectedReport] = useState(null) function ExamesLaudos() { + const router = useRouter() const [reports, setReports] = useState(null) const [loadingReports, setLoadingReports] = useState(false) const [reportsError, setReportsError] = useState(null) @@ -1426,7 +1427,7 @@ export default function PacientePage() {
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
- +
@@ -1449,95 +1450,7 @@ export default function PacientePage() { - - !open && setSelectedReport(null)}> - - - - {selectedReport && ( - (() => { - const looksLikeIdStr = (s: any) => { - try { - const hexOnly = String(s || '').replace(/[^0-9a-fA-F]/g, ''); - const len = (typeof hexOnly === 'string') ? hexOnly.length : (Number(hexOnly) || 0); - return len >= 8; - } catch { return false; } - }; - const maybeId = selectedReport?.doctor_id || selectedReport?.created_by || selectedReport?.doctor || null; - const derived = reportDoctorName ? reportTitle(selectedReport, reportDoctorName) : reportTitle(selectedReport); - - if (looksLikeIdStr(derived)) { - return {strings.carregando}; - } - if (resolvingDoctors && maybeId && !doctorsMap[String(maybeId)]) { - return {strings.carregando}; - } - return {derived}; - })() - )} - - Detalhes do laudo -
- {selectedReport && ( - <> -
Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
- {reportDoctorName &&
Profissional: {reportDoctorName}
} - - {/* Standardized laudo sections */} - {(() => { - const cid = selectedReport.cid ?? selectedReport.cid_code ?? selectedReport.cidCode ?? selectedReport.cie ?? '-'; - const exam = selectedReport.exam ?? selectedReport.exame ?? selectedReport.especialidade ?? selectedReport.report_type ?? '-'; - const diagnosis = selectedReport.diagnosis ?? selectedReport.diagnostico ?? selectedReport.diagnosis_text ?? selectedReport.diagnostico_text ?? ''; - const conclusion = selectedReport.conclusion ?? selectedReport.conclusao ?? selectedReport.conclusion_text ?? selectedReport.conclusao_text ?? ''; - const notesHtml = selectedReport.content_html ?? selectedReport.conteudo_html ?? selectedReport.contentHtml ?? null; - const notesText = selectedReport.content ?? selectedReport.body ?? selectedReport.conteudo ?? selectedReport.notes ?? selectedReport.observacoes ?? ''; - return ( -
-
-
CID
-
{cid || '-'}
-
-
-
Exame
-
{exam || '-'}
-
-
-
Diagnóstico
-
{diagnosis || '-'}
-
-
-
Conclusão
-
{conclusion || '-'}
-
-
-
Notas do Profissional
- {notesHtml ? ( -
- ) : ( -
{notesText || '-'}
- )} -
-
- ); - })()} - {selectedReport.doctor_signature && ( -
Assinatura: assinatura
- )} - - )} -
- - - - - -
+ {/* Modal removed - now using dedicated page /app/laudos/[id] */} ) } diff --git a/susconecta/app/paciente/resultados/ResultadosClient.tsx b/susconecta/app/paciente/resultados/ResultadosClient.tsx index b471740..16e80a4 100644 --- a/susconecta/app/paciente/resultados/ResultadosClient.tsx +++ b/susconecta/app/paciente/resultados/ResultadosClient.tsx @@ -244,8 +244,12 @@ export default function ResultadosClient() { } const onlyAvail = (res?.slots || []).filter((s: any) => s.available) + const nowMs = Date.now() for (const s of onlyAvail) { const dt = new Date(s.datetime) + const dtMs = dt.getTime() + // Filtrar: só mostrar horários que são posteriores ao horário atual + if (dtMs < nowMs) continue const key = dt.toISOString().split('T')[0] const bucket = days.find(d => d.dateKey === key) if (!bucket) continue @@ -260,7 +264,6 @@ export default function ResultadosClient() { // compute nearest slot (earliest available in the returned window, but after now) let nearest: { iso: string; label: string } | null = null - const nowMs = Date.now() const allSlots = days.flatMap(d => d.horarios || []) const futureSorted = allSlots .map(s => ({ ...s, ms: new Date(s.iso).getTime() })) @@ -582,17 +585,24 @@ export default function ResultadosClient() { }) const merged = Array.from(mergedMap.values()).sort((a:any,b:any) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime()) - const formatted = (merged || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) })) + const nowMs = Date.now() + // Filtrar: só mostrar horários que são posteriores ao horário atual + const futureOnly = merged.filter((s: any) => new Date(s.datetime).getTime() >= nowMs) + const formatted = (futureOnly || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) })) setMoreTimesSlots(formatted) return formatted } else { - const slots = (av.slots || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) })) + const nowMs = Date.now() + // Filtrar: só mostrar horários que são posteriores ao horário atual + const slots = (av.slots || []).filter((s:any) => new Date(s.datetime).getTime() >= nowMs).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) })) setMoreTimesSlots(slots) return slots } } catch (e) { console.warn('[ResultadosClient] erro ao filtrar por disponibilidades', e) - const slots = (av.slots || []).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) })) + const nowMs = Date.now() + // Filtrar: só mostrar horários que são posteriores ao horário atual + const slots = (av.slots || []).filter((s:any) => new Date(s.datetime).getTime() >= nowMs).map((s:any) => ({ iso: s.datetime, label: new Date(s.datetime).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) })) setMoreTimesSlots(slots) return slots } diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index bcaafb9..27cfb7e 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -7,6 +7,7 @@ import ProtectedRoute from "@/components/shared/ProtectedRoute"; import { useAuth } from "@/hooks/useAuth"; import { useToast } from "@/hooks/use-toast"; import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api"; +import { ENV_CONFIG } from '@/lib/env-config'; import { useReports } from "@/hooks/useReports"; import { CreateReportData } from "@/types/report-types"; import { Button } from "@/components/ui/button"; @@ -36,7 +37,6 @@ import { import dynamic from "next/dynamic"; -import { ENV_CONFIG } from '@/lib/env-config'; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; @@ -182,7 +182,7 @@ const ProfissionalPage = () => { const q = `doctor_id=eq.${encodeURIComponent(String(resolvedDoctorId))}&select=patient_id&limit=200`; const appts = await listarAgendamentos(q).catch(() => []); for (const a of (appts || [])) { - const pid = a.patient_id ?? a.patient ?? a.patient_id_raw ?? null; + const pid = (a as any).patient_id ?? null; if (pid) patientIdSet.add(String(pid)); } } catch (e) { @@ -211,6 +211,7 @@ const ProfissionalPage = () => { })(); return () => { mounted = false; }; // Re-run when user id becomes available so patients assigned to the logged-in doctor are loaded + // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id]); // Carregar perfil do médico correspondente ao usuário logado @@ -354,9 +355,9 @@ const ProfissionalPage = () => { } } - const pid = a.patient_id || a.patient || a.patient_id_raw || a.patientId || null; + const pid = a.patient_id || (a as any).patient || a.patient_id_raw || a.patientId || null; const patientObj = pid ? patientMap.get(String(pid)) : null; - const patientName = patientObj?.full_name || a.patient || a.patient_name || String(pid) || 'Paciente'; + const patientName = patientObj?.full_name || (a as any).patient || a.patient_name || String(pid) || 'Paciente'; const patientIdVal = pid || null; return { @@ -429,6 +430,9 @@ const ProfissionalPage = () => { const [commPhoneNumber, setCommPhoneNumber] = useState(''); const [commMessage, setCommMessage] = useState(''); const [commPatientId, setCommPatientId] = useState(null); + const [commResponses, setCommResponses] = useState([]); + const [commResponsesLoading, setCommResponsesLoading] = useState(false); + const [commResponsesError, setCommResponsesError] = useState(null); const [smsSending, setSmsSending] = useState(false); const handleSave = async (event: React.MouseEvent) => { @@ -520,6 +524,68 @@ const ProfissionalPage = () => { } }; + const loadCommResponses = async (patientId?: string) => { + const pid = patientId ?? commPatientId; + if (!pid) { + setCommResponses([]); + setCommResponsesError('Selecione um paciente para ver respostas'); + return; + } + setCommResponsesLoading(true); + setCommResponsesError(null); + try { + // 1) tentar buscar por patient_id (o comportamento ideal) + const qs = new URLSearchParams(); + qs.set('patient_id', `eq.${String(pid)}`); + qs.set('order', 'created_at.desc'); + const url = `${(ENV_CONFIG as any).REST}/messages?${qs.toString()}`; + const headers: Record = { 'Accept': 'application/json' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + if ((ENV_CONFIG as any)?.SUPABASE_ANON_KEY) headers['apikey'] = (ENV_CONFIG as any).SUPABASE_ANON_KEY; + const r = await fetch(url, { method: 'GET', headers }); + let data = await r.json().catch(() => []); + data = Array.isArray(data) ? data : []; + + // 2) Se não houver mensagens por patient_id, tentar buscar por número (from/to) + if ((!data || data.length === 0) && commPhoneNumber) { + try { + const norm = normalizePhoneNumber(commPhoneNumber); + if (norm) { + // Primeiro tenta buscar mensagens onde `from` é o número + const qsFrom = new URLSearchParams(); + qsFrom.set('from', `eq.${String(norm)}`); + qsFrom.set('order', 'created_at.desc'); + const urlFrom = `${(ENV_CONFIG as any).REST}/messages?${qsFrom.toString()}`; + const rf = await fetch(urlFrom, { method: 'GET', headers }); + const dataFrom = await rf.json().catch(() => []); + if (Array.isArray(dataFrom) && dataFrom.length) { + data = dataFrom; + } else { + // se nada, tenta `to` (caso o provedor grave a direção inversa) + const qsTo = new URLSearchParams(); + qsTo.set('to', `eq.${String(norm)}`); + qsTo.set('order', 'created_at.desc'); + const urlTo = `${(ENV_CONFIG as any).REST}/messages?${qsTo.toString()}`; + const rt = await fetch(urlTo, { method: 'GET', headers }); + const dataTo = await rt.json().catch(() => []); + if (Array.isArray(dataTo) && dataTo.length) data = dataTo; + } + } + } catch (phoneErr) { + // não bloqueara o fluxo principal; apenas log + console.warn('[ProfissionalPage] fallback por telefone falhou', phoneErr); + } + } + + setCommResponses(Array.isArray(data) ? data : []); + } catch (e: any) { + setCommResponsesError(String(e?.message || e || 'Falha ao buscar respostas')); + setCommResponses([]); + } finally { + setCommResponsesLoading(false); + } + }; + const handleEditarLaudo = (paciente: any) => { @@ -720,14 +786,14 @@ const ProfissionalPage = () => { const todayEvents = getTodayEvents(); return ( -
+
{/* adicionada overflow-x-hidden */}
-

Agenda do Dia

+

Agenda do Dia

- {/* Navegação de Data */} -
-
+ {/* Navegação de Data - Responsiva */} +
+
-

+

{formatDate(currentCalendarDate)}

-
-
- {todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''} agendada{todayEvents.length !== 1 ? 's' : ''} +
+ {todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''}
{/* Lista de Pacientes do Dia */} -
+
{/* adicionada overflow-x-hidden */} {todayEvents.length === 0 ? ( -
- -

Nenhuma consulta agendada para este dia

-

Agenda livre para este dia

+
+ +

Nenhuma consulta agendada para este dia

+

Agenda livre para este dia

) : ( todayEvents.map((appointment) => { @@ -768,47 +833,46 @@ const ProfissionalPage = () => { return (
-
-
+
+
-
-
- - {appointment.title} +
+
+ + {appointment.title}
{paciente && ( -
+
CPF: {getPatientCpf(paciente)} • {getPatientAge(paciente)} anos
)}
-
- - {appointment.time} +
+ + {appointment.time}
{appointment.type}
-
+
Ver informações do paciente
-
@@ -1594,7 +1658,7 @@ const ProfissionalPage = () => { function LaudoViewer({ laudo, onClose }: { laudo: any; onClose: () => void }) { return (
-
+
{/* Header */}
@@ -2625,19 +2689,19 @@ const ProfissionalPage = () => { const renderComunicacaoSection = () => ( -
-

Comunicação com o Paciente

+
+

Comunicação com o Paciente

- + + +
- -