From eea59f50633f4cd2831f15a58ef670cb84df60ce Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Sat, 8 Nov 2025 00:23:51 -0300 Subject: [PATCH] =?UTF-8?q?style(calend=C3=A1rio)=20corrigir=20o=20modal?= =?UTF-8?q?=20quando=20clica=20em=20um=20paciente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main-routes)/calendar/page.tsx | 213 +++++-- .../features/general/event-manager.tsx | 530 ++++++++++-------- 2 files changed, 456 insertions(+), 287 deletions(-) diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index f2bce90..cf86cd4 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -55,10 +55,16 @@ export default function AgendamentoPage() { return; } - const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean))); - const patients = (patientIds && patientIds.length) ? await api.buscarPacientesPorIds(patientIds) : []; - const patientsById: Record = {}; - (patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; }); + const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean))); + const patients = (patientIds && patientIds.length) ? await api.buscarPacientesPorIds(patientIds) : []; + const patientsById: Record = {}; + (patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; }); + + // Tentar enriquecer com médicos/profissionais quando houver doctor_id + const doctorIds = Array.from(new Set(arr.map((a: any) => a.doctor_id).filter(Boolean))); + const doctors = (doctorIds && doctorIds.length) ? await api.buscarMedicosPorIds(doctorIds) : []; + const doctorsById: Record = {}; + (doctors || []).forEach((d: any) => { if (d && d.id) doctorsById[String(d.id)] = d; }); setAppointments(arr || []); @@ -80,6 +86,13 @@ export default function AgendamentoPage() { else if (status === "canceled" || status === "cancelado" || status === "cancelled") color = "red"; else if (status === "requested" || status === "solicitado") color = "blue"; + const professional = (doctorsById[String(obj.doctor_id)]?.full_name) || obj.doctor_name || obj.professional_name || obj.professional || obj.executante || 'Profissional'; + const appointmentType = obj.appointment_type || obj.type || obj.appointmentType || ''; + const insurance = obj.insurance_provider || obj.insurance || obj.convenio || obj.insuranceProvider || null; + const completedAt = obj.completed_at || obj.completedAt || null; + const cancelledAt = obj.cancelled_at || obj.cancelledAt || null; + const cancellationReason = obj.cancellation_reason || obj.cancellationReason || obj.cancel_reason || null; + return { id: obj.id || uuidv4(), title, @@ -87,6 +100,15 @@ export default function AgendamentoPage() { startTime: start, endTime: end, color, + // Campos adicionais para visualização detalhada + patientName: patient, + professionalName: professional, + appointmentType, + status: obj.status || null, + insuranceProvider: insurance, + completedAt, + cancelledAt, + cancellationReason, }; }); setManagerEvents(newManagerEvents); @@ -130,6 +152,128 @@ export default function AgendamentoPage() { } }; + // Componente auxiliar: legenda dinâmica que lista as cores/statuss presentes nos agendamentos + function DynamicLegend({ events }: { events: Event[] }) { + // Mapa de classes para cores conhecidas + const colorClassMap: Record = { + blue: "bg-blue-500 ring-blue-500/20", + green: "bg-green-500 ring-green-500/20", + orange: "bg-orange-500 ring-orange-500/20", + red: "bg-red-500 ring-red-500/20", + purple: "bg-purple-500 ring-purple-500/20", + pink: "bg-pink-500 ring-pink-500/20", + teal: "bg-teal-400 ring-teal-400/20", + } + + const hashToColor = (s: string) => { + // gera cor hex simples a partir de hash da string + let h = 0 + for (let i = 0; i < s.length; i++) h = (h << 5) - h + s.charCodeAt(i) + const c = (h & 0x00ffffff).toString(16).toUpperCase() + return "#" + "00000".substring(0, 6 - c.length) + c + } + + // Agrupa por cor e coleta os status associados + const entries = new Map>() + for (const ev of events) { + const col = (ev.color || "blue").toString() + const st = (ev.status || statusFromColor(ev.color) || "").toString().toLowerCase() + if (!entries.has(col)) entries.set(col, new Set()) + if (st) entries.get(col)!.add(st) + } + + // Painel principal: sempre exibe os 3 status primários (Solicitado, Confirmado, Cancelado) + const statusDisplay = (s: string) => { + switch (s) { + case "requested": + case "request": + case "solicitado": + return "Solicitado" + case "confirmed": + case "confirmado": + return "Confirmado" + case "canceled": + case "cancelled": + case "cancelado": + return "Cancelado" + case "pending": + case "pendente": + return "Pendente" + case "governo": + case "government": + return "Governo" + default: + return s.charAt(0).toUpperCase() + s.slice(1) + } + } + + // Ordem preferencial para exibição (tenta manter Solicitação/Confirmado/Cancelado em primeiro) + const priorityList = [ + 'solicitado','requested', + 'confirmed','confirmado', + 'pending','pendente', + 'canceled','cancelled','cancelado', + 'governo','government' + ] + + const items = Array.from(entries.entries()).map(([col, statuses]) => { + const statusArr = Array.from(statuses) + let priority = 999 + for (const s of statusArr) { + const idx = priorityList.indexOf(s) + if (idx >= 0) priority = Math.min(priority, idx) + } + // if none matched, leave priority high so they appear after known statuses + return { col, statuses: statusArr, priority } + }) + + items.sort((a, b) => a.priority - b.priority || a.col.localeCompare(b.col)) + + // Separar itens extras (fora os três principais) para renderizar depois + const primaryColors = new Set(['blue', 'green', 'red']) + const extras = items.filter(i => !primaryColors.has(i.col.toLowerCase())) + + return ( +
+ {/* Bloco grande com os três status principais sempre visíveis e responsivos */} +
+
+ + Solicitado +
+
+ + Confirmado +
+
+ + Cancelado +
+
+ + {/* Itens extras detectados dinamicamente (menores) */} + {extras.length > 0 && ( +
+ {extras.map(({ col, statuses }) => { + const statusList = statuses.map(statusDisplay).filter(Boolean).join(', ') + const cls = colorClassMap[col.toLowerCase()] + return ( +
+ {cls ? ( + + ) : ( + + )} + {statusList || col} +
+ ) + })} +
+ )} +
+ ) + } + // Envia atualização para a API e atualiza UI const handleEventUpdate = async (id: string, partial: Partial) => { try { @@ -157,58 +301,31 @@ export default function AgendamentoPage() { return (
-
-
- {/* Cabeçalho simplificado (sem 3D) */} +
+
-

Calendário

-

- Navegue através do atalho: Calendário (C). -

+

Calendário

+

Navegue através do atalho: Calendário (C).

- {/* REMOVIDO: botões de abas Calendário/3D */} -
- {/* Legenda de status (aplica-se ao EventManager) */} -
-
-
- - Solicitado -
-
- - Confirmado -
- {/* Novo: Cancelado (vermelho) */} -
- - Cancelado -
+ {/* legenda dinâmica: mostra as cores presentes nos agendamentos do dia atual */} +
+
- {/* Apenas o EventManager */} -
-
- {managerLoading ? ( -
-
Conectando ao calendário — carregando agendamentos...
-
- ) : ( -
- -
- )} -
+
+ {managerLoading ? ( +
+
Conectando ao calendário — carregando agendamentos...
+
+ ) : ( +
+ +
+ )}
- - {/* REMOVIDO: PatientRegistrationForm (era acionado pelo 3D) */}
); diff --git a/susconecta/components/features/general/event-manager.tsx b/susconecta/components/features/general/event-manager.tsx index 7ffb8bd..38bac08 100644 --- a/susconecta/components/features/general/event-manager.tsx +++ b/susconecta/components/features/general/event-manager.tsx @@ -1,6 +1,7 @@ "use client" import React, { useState, useCallback, useMemo, useEffect } from "react" +import { buscarAgendamentoPorId, buscarPacientesPorIds, buscarMedicosPorIds } from "@/lib/api" import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" import { Input } from "@/components/ui/input" @@ -29,6 +30,15 @@ export interface Event { category?: string attendees?: string[] tags?: string[] + // Additional appointment fields (optional) + patientName?: string + professionalName?: string + appointmentType?: string + status?: string + insuranceProvider?: string | null + completedAt?: string | Date | null + cancelledAt?: string | Date | null + cancellationReason?: string | null } export interface EventManagerProps { @@ -230,6 +240,73 @@ export function EventManager({ } catch {} }, []) + // Quando um evento é selecionado para visualização, buscar dados completos do agendamento + // para garantir que patient/professional/tags/attendees/status estejam preenchidos. + useEffect(() => { + if (!selectedEvent || isCreating) return + let cancelled = false + + const enrich = async () => { + try { + const full = await buscarAgendamentoPorId(selectedEvent.id).catch(() => null) + if (cancelled || !full) return + + // Tentar resolver nomes de paciente e profissional a partir de IDs quando possível + let patientName = selectedEvent.patientName + if ((!patientName || patientName === "—") && full.patient_id) { + const pList = await buscarPacientesPorIds([full.patient_id as any]).catch(() => []) + if (pList && pList.length) patientName = (pList[0] as any).full_name || (pList[0] as any).fullName || (pList[0] as any).name + } + + let professionalName = selectedEvent.professionalName + if ((!professionalName || professionalName === "—") && full.doctor_id) { + const dList = await buscarMedicosPorIds([full.doctor_id as any]).catch(() => []) + if (dList && dList.length) professionalName = (dList[0] as any).full_name || (dList[0] as any).fullName || (dList[0] as any).name + } + + const merged: Event = { + ...selectedEvent, + // priorizar valores vindos do backend quando existirem + title: ((full as any).title as any) || selectedEvent.title, + description: ((full as any).notes as any) || ((full as any).patient_notes as any) || selectedEvent.description, + patientName: patientName || selectedEvent.patientName, + professionalName: professionalName || selectedEvent.professionalName, + appointmentType: ((full as any).appointment_type as any) || selectedEvent.appointmentType, + status: ((full as any).status as any) || selectedEvent.status, + insuranceProvider: ((full as any).insurance_provider as any) ?? selectedEvent.insuranceProvider, + completedAt: ((full as any).completed_at as any) ?? selectedEvent.completedAt, + cancelledAt: ((full as any).cancelled_at as any) ?? selectedEvent.cancelledAt, + cancellationReason: ((full as any).cancellation_reason as any) ?? selectedEvent.cancellationReason, + attendees: ((full as any).attendees as any) || ((full as any).participants as any) || selectedEvent.attendees, + tags: ((full as any).tags as any) || selectedEvent.tags, + } + + if (!cancelled) setSelectedEvent(merged) + } catch (err) { + // não bloquear UI em caso de falha + console.warn('[EventManager] Falha ao enriquecer agendamento:', err) + } + } + + enrich() + + return () => { + cancelled = true + } + }, [selectedEvent, isCreating]) + + // Remove trechos redundantes como "Status: requested." que às vezes vêm concatenados na descrição + const sanitizeDescription = (d?: string | null) => { + if (!d) return null + try { + // Remove qualquer segmento "Status: ..." seguido opcionalmente de ponto + const cleaned = String(d).replace(/Status:\s*[^\.\n]+\.?/gi, "").trim() + return cleaned || null + } catch (e) { + return d + } + } + return (
{/* Header */} @@ -504,7 +581,7 @@ export function EventManager({ {/* Event Dialog */} - + {isCreating ? "Criar Evento" : "Detalhes do Agendamento"} @@ -512,122 +589,179 @@ export function EventManager({ -
-
- - - isCreating - ? setNewEvent((prev) => ({ ...prev, title: e.target.value })) - : setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null)) - } - placeholder="Título do evento" - /> -
+ {/* Dialog content: form when creating; read-only view when viewing */} + {isCreating ? ( + <> +
+
+ + setNewEvent((prev) => ({ ...prev, title: e.target.value }))} + placeholder="Título do evento" + /> +
-
- -