From 661105521117f6f47819cb4e9518c013b0f2b9a5 Mon Sep 17 00:00:00 2001 From: M-Gabrielly Date: Thu, 6 Nov 2025 16:09:52 -0300 Subject: [PATCH 01/12] fix(profissional): uncomment Communication --- susconecta/app/profissional/page.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index bcaafb9..adb9607 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -2941,8 +2941,8 @@ const ProfissionalPage = () => { ); case 'laudos': return renderLaudosSection(); - // case 'comunicacao': - // return renderComunicacaoSection(); + case 'comunicacao': + return renderComunicacaoSection(); case 'perfil': return renderPerfilSection(); default: @@ -3013,15 +3013,14 @@ const ProfissionalPage = () => { Laudos - {/* Comunicação removida - campos embaixo do calendário */} - {/* */} + SMS + +
- - + { + try { + const [y, m, d] = String(formData.appointmentDate).split('-'); + return `${d}/${m}/${y}`; + } catch (e) { + return ''; + } + })() : ''} + readOnly + /> + {showDatePicker && ( +
+ { + if (date) { + const dateStr = date.toISOString().split('T')[0]; + onFormChange({ ...formData, appointmentDate: dateStr }); + setShowDatePicker(false); + } + }} + disabled={(date) => date < new Date(new Date().toISOString().split('T')[0] + 'T00:00:00')} + /> +
+ )}
@@ -1011,8 +1092,8 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
{loadingSlots ? (
Carregando horários...
- ) : availableSlots && availableSlots.length ? ( - availableSlots.map((s) => { + ) : filteredAvailableSlots && filteredAvailableSlots.length ? ( + filteredAvailableSlots.map((s) => { const dt = new Date(s.datetime); const hh = String(dt.getHours()).padStart(2, '0'); const mm = String(dt.getMinutes()).padStart(2, '0'); From add30c54a3771c95cabe02b1fcdbf4c2fb05b528 Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Thu, 6 Nov 2025 16:50:40 -0300 Subject: [PATCH 04/12] =?UTF-8?q?feat(doutores/pacientes):=20adiciona=20fi?= =?UTF-8?q?ltros=20e=20ordena=C3=A7=C3=A3o=20avan=C3=A7ados=20na=20listage?= =?UTF-8?q?m=20de=20m=C3=A9dicos=20e=20pacientes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main-routes)/doutores/page.tsx | 176 ++++++++++++++---- .../app/(main-routes)/pacientes/page.tsx | 143 +++++++++++--- 2 files changed, 256 insertions(+), 63 deletions(-) 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) */} + + - - -
-
+ {/* REMOVIDO: botões de abas Calendário/3D */}
- {/* Legenda de status (estilo Google Calendar) */} + {/* Legenda de status (aplica-se ao EventManager) */}
@@ -210,49 +147,31 @@ 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/components/ui/three-dwall-calendar.tsx b/susconecta/components/ui/three-dwall-calendar.tsx deleted file mode 100644 index aa1a7e9..0000000 --- a/susconecta/components/ui/three-dwall-calendar.tsx +++ /dev/null @@ -1,467 +0,0 @@ -"use client" - -import * as React from "react" -import { Card, CardContent } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" -import { Badge } from "@/components/ui/badge" -import { Trash2, Calendar, Clock, User } from "lucide-react" -import { v4 as uuidv4 } from "uuid" -import { startOfMonth, endOfMonth, eachDayOfInterval, format } from "date-fns" -import { ptBR } from "date-fns/locale" - -export type CalendarEvent = { - id: string - title: string - date: string // ISO - status?: 'confirmed' | 'pending' | 'cancelled' | string - patient?: string - type?: string -} - -interface ThreeDWallCalendarProps { - events: CalendarEvent[] - onAddEvent?: (e: CalendarEvent) => void - onRemoveEvent?: (id: string) => void - onOpenAddPatientForm?: () => void - panelWidth?: number - panelHeight?: number - columns?: number -} - -export function ThreeDWallCalendar({ - events, - onAddEvent, - onRemoveEvent, - onOpenAddPatientForm, - panelWidth = 160, - panelHeight = 120, - columns = 7, -}: ThreeDWallCalendarProps) { - const [dateRef, setDateRef] = React.useState(new Date()) - const [title, setTitle] = React.useState("") - const [newDate, setNewDate] = React.useState("") - const [selectedDay, setSelectedDay] = React.useState(null) - const [isDialogOpen, setIsDialogOpen] = React.useState(false) - const wallRef = React.useRef(null) - - // 3D tilt state - const [tiltX, setTiltX] = React.useState(18) - const [tiltY, setTiltY] = React.useState(0) - const isDragging = React.useRef(false) - const dragStart = React.useRef<{ x: number; y: number } | null>(null) - const hasDragged = React.useRef(false) - const clickStart = React.useRef<{ x: number; y: number } | null>(null) - - // month days - const days = eachDayOfInterval({ - start: startOfMonth(dateRef), - end: endOfMonth(dateRef), - }) - - const eventsForDay = (d: Date) => - events.filter((ev) => format(new Date(ev.date), "yyyy-MM-dd") === format(d, "yyyy-MM-dd")) - - const selectedDayEvents = selectedDay ? eventsForDay(selectedDay) : [] - - const handleDayClick = (day: Date) => { - console.log('Day clicked:', format(day, 'dd/MM/yyyy')) - setSelectedDay(day) - setIsDialogOpen(true) - } - - // Add event handler - const handleAdd = () => { - if (!title.trim() || !newDate) return - onAddEvent?.({ - id: uuidv4(), - title: title.trim(), - date: new Date(newDate).toISOString(), - }) - setTitle("") - setNewDate("") - } - - // wheel tilt - const onWheel = (e: React.WheelEvent) => { - setTiltX((t) => Math.max(0, Math.min(50, t + e.deltaY * 0.02))) - setTiltY((t) => Math.max(-45, Math.min(45, t + e.deltaX * 0.05))) - } - - // drag tilt - const onPointerDown = (e: React.PointerEvent) => { - isDragging.current = true - hasDragged.current = false - dragStart.current = { x: e.clientX, y: e.clientY } - ;(e.currentTarget as Element).setPointerCapture(e.pointerId) - } - - const onPointerMove = (e: React.PointerEvent) => { - if (!isDragging.current || !dragStart.current) return - const dx = e.clientX - dragStart.current.x - const dy = e.clientY - dragStart.current.y - - // Se moveu mais de 5 pixels, considera como drag - if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { - hasDragged.current = true - } - - setTiltY((t) => Math.max(-60, Math.min(60, t + dx * 0.1))) - setTiltX((t) => Math.max(0, Math.min(60, t - dy * 0.1))) - dragStart.current = { x: e.clientX, y: e.clientY } - } - - const onPointerUp = () => { - isDragging.current = false - dragStart.current = null - // Reset hasDragged após um curto delay para permitir o clique ser processado - setTimeout(() => { - hasDragged.current = false - }, 100) - } - - const gap = 12 - const rowCount = Math.ceil(days.length / columns) - const wallCenterRow = (rowCount - 1) / 2 - - return ( -
-
-
- -
{format(dateRef, "MMMM yyyy", { locale: ptBR })}
- - {/* Botão Pacientes de hoje */} - -
- - {/* Legenda de cores */} -
-
-
- Confirmado -
-
-
- Pendente -
-
-
- Cancelado -
-
-
- Outros -
-
-
- - {/* Wall container */} -
-
- 💡 Arraste para rotacionar • Scroll para inclinar -
-
-
-
- {days.map((day, idx) => { - const row = Math.floor(idx / columns) - const rowOffset = row - wallCenterRow - const z = Math.max(-80, 40 - Math.abs(rowOffset) * 20) - const dayEvents = eventsForDay(day) - - return ( -
{ - clickStart.current = { x: e.clientX, y: e.clientY } - }} - onPointerUp={(e) => { - if (clickStart.current) { - const dx = Math.abs(e.clientX - clickStart.current.x) - const dy = Math.abs(e.clientY - clickStart.current.y) - // Se moveu menos de 5 pixels, é um clique - if (dx < 5 && dy < 5) { - e.stopPropagation() - handleDayClick(day) - } - clickStart.current = null - } - }} - > - - -
-
{format(day, "d")}
-
- {dayEvents.length > 0 && `${dayEvents.length} ${dayEvents.length === 1 ? 'paciente' : 'pacientes'}`} -
-
-
{format(day, "EEE", { locale: ptBR })}
- - {/* events */} -
- {dayEvents.map((ev, i) => { - // Calcular tamanho da bolinha baseado na quantidade de eventos - const eventCount = dayEvents.length - const ballSize = eventCount <= 3 ? 20 : - eventCount <= 6 ? 16 : - eventCount <= 10 ? 14 : - eventCount <= 15 ? 12 : 10 - - const spacing = ballSize + 4 - const maxPerRow = Math.floor((panelWidth - 16) / spacing) - const col = i % maxPerRow - const row = Math.floor(i / maxPerRow) - const left = 4 + (col * spacing) - const top = 4 + (row * spacing) - - // Cores baseadas no status - const getStatusColor = () => { - switch(ev.status) { - case 'confirmed': return 'bg-green-500 dark:bg-green-600' - case 'pending': return 'bg-yellow-500 dark:bg-yellow-600' - case 'cancelled': return 'bg-red-500 dark:bg-red-600' - default: return 'bg-blue-500 dark:bg-blue-600' - } - } - - return ( - - -
- • -
-
- -
-
{ev.title}
- {ev.patient && ev.type && ( -
-
Paciente: {ev.patient}
-
Tipo: {ev.type}
-
- )} -
- {format(new Date(ev.date), "PPP 'às' p", { locale: ptBR })} -
- {ev.status && ( -
- Status:{' '} - - {ev.status === 'confirmed' ? 'Confirmado' : - ev.status === 'pending' ? 'Pendente' : - ev.status === 'cancelled' ? 'Cancelado' : ev.status} - -
- )} - {onRemoveEvent && ( - - )} -
-
-
- ) - })} -
-
-
-
- ) - })} -
-
-
-
- - {/* Dialog de detalhes do dia */} - - - - {/* Navegação de dias */} -
- - - {selectedDay && format(selectedDay, "dd 'de' MMMM 'de' yyyy", { locale: ptBR })} - - -
- - {selectedDayEvents.length} {selectedDayEvents.length === 1 ? 'paciente agendado' : 'pacientes agendados'} - -
-
- {selectedDayEvents.length === 0 ? ( -
- Nenhum paciente agendado para este dia -
- ) : ( - selectedDayEvents.map((ev) => { - const getStatusColor = () => { - switch(ev.status) { - case 'confirmed': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' - case 'pending': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' - case 'cancelled': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' - default: return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' - } - } - - const getStatusText = () => { - switch(ev.status) { - case 'confirmed': return 'Confirmado' - case 'pending': return 'Pendente' - case 'cancelled': return 'Cancelado' - default: return ev.status || 'Sem status' - } - } - - return ( - - -
-
-
- -

{ev.patient || ev.title}

-
- - {ev.type && ( -
- - {ev.type} -
- )} - -
- - {format(new Date(ev.date), "HH:mm", { locale: ptBR })} -
- - - {getStatusText()} - -
- - {onRemoveEvent && ( - - )} -
-
-
- ) - }) - )} -
-
-
- - {/* Add event form */} -
- {onOpenAddPatientForm ? ( - - ) : ( - <> - setTitle(e.target.value)} /> - setNewDate(e.target.value)} /> - - - )} -
-
- ) -} From 280d314b5dafd74bdf455b6cdeebaf9c122afdd0 Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Thu, 6 Nov 2025 17:30:53 -0300 Subject: [PATCH 06/12] =?UTF-8?q?WeekView:=20limita=20renderiza=C3=A7?= =?UTF-8?q?=C3=A3o=20=C3=A0s=20horas=20do=20menor/maior=20hor=C3=A1rio=20d?= =?UTF-8?q?a=20semana=20vis=C3=ADvel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main-routes)/calendar/page.tsx | 40 +++++++++++++- .../features/general/event-manager.tsx | 54 ++++++++++++++++--- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index 519ba4b..f6f4b68 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -121,6 +121,40 @@ export default function AgendamentoPage() { } }; + // 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"; + } + }; + + // 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 (
@@ -164,7 +198,11 @@ export default function AgendamentoPage() {
) : (
- +
)}
diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx index 697b2e6..e974ca9 100644 --- a/susconecta/components/features/general/event-manager.tsx +++ b/susconecta/components/features/general/event-manager.tsx @@ -515,7 +515,7 @@ export function EventManager({ {/* Event Dialog */} - + {isCreating ? "Criar Evento" : "Detalhes do Evento"} @@ -528,7 +528,7 @@ export function EventManager({ isCreating ? setNewEvent((prev) => ({ ...prev, title: e.target.value })) @@ -542,7 +542,7 @@ export function EventManager({