From 5dd0764f0e1616f20d595c3d45d64fa186612e0c Mon Sep 17 00:00:00 2001 From: M-Gabrielly Date: Thu, 6 Nov 2025 01:01:17 -0300 Subject: [PATCH] feat: improve patient registration auth, debugging in 3D calendar and register profiles --- .../app/(main-routes)/calendar/page.tsx | 18 + susconecta/app/globals.css | 44 ++ susconecta/app/paciente/page.tsx | 237 ++++++--- susconecta/app/profissional/page.tsx | 476 ++++++++++++------ .../forms/patient-registration-form.tsx | 12 +- .../components/ui/three-dwall-calendar.tsx | 16 +- susconecta/lib/api.ts | 41 +- 7 files changed, 622 insertions(+), 222 deletions(-) diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index c4afdea..aa7380c 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -13,6 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback 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 { @@ -22,6 +23,7 @@ import { 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"; const ListaEspera = dynamic( () => import("@/components/features/agendamento/ListaEspera"), @@ -29,6 +31,7 @@ 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"); @@ -39,6 +42,9 @@ export default function AgendamentoPage() { // 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) => { @@ -242,6 +248,7 @@ export default function AgendamentoPage() { events={threeDEvents} onAddEvent={handleAddEvent} onRemoveEvent={handleRemoveEvent} + onOpenAddPatientForm={() => setShowPatientForm(true)} /> ) : ( @@ -253,6 +260,17 @@ export default function AgendamentoPage() { /> )} + + {/* Formulário de Registro de Paciente */} + { + console.log('[Calendar] Novo paciente registrado:', newPaciente); + setShowPatientForm(false); + }} + /> ); diff --git a/susconecta/app/globals.css b/susconecta/app/globals.css index df339bd..d41795d 100644 --- a/susconecta/app/globals.css +++ b/susconecta/app/globals.css @@ -123,3 +123,47 @@ @apply bg-background text-foreground font-sans; } } + +/* Esconder botões com ícones de lixo */ +button:has(.lucide-trash2), +button:has(.lucide-trash), +button[class*="trash"] { + display: none !important; +} + +/* Esconder campos de input embaixo do calendário 3D */ +input[placeholder="Nome do paciente"], +input[placeholder^="dd/mm"], +input[type="date"][value=""] { + display: none !important; +} + +/* Esconder botão "Adicionar Paciente" */ +/* Removido seletor vazio - será tratado por outros seletores */ + +/* Afastar X do popup (dialog-close) para longe das setas */ +[data-slot="dialog-close"], +button[aria-label="Close"], +.fc button[aria-label*="Close"] { + right: 16px !important; + top: 8px !important; + position: absolute !important; +} + +/* Esconder footer/header extras do calendário que mostram os campos */ +.fc .fc-toolbar input, +.fc .fc-toolbar [type="date"], +.fc .fc-toolbar [placeholder*="paciente"] { + display: none !important; +} + +/* Esconder row com campos de pesquisa - estrutura mantida pelo calendário */ + +/* Esconder botões de trash/delete em todos os popups */ +[role="dialog"] button[class*="hover:text-destructive"], +[role="dialog"] button[aria-label*="delete"], +[role="dialog"] button[aria-label*="excluir"], +[role="dialog"] button[aria-label*="remove"] { + display: none !important; +} + diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index 697c02f..d44be28 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -1429,95 +1429,192 @@ export default function PacientePage() { function Perfil() { - const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep) return ( -
+
+ {/* Header com Título e Botão */}
-

Meu Perfil

+
+

Meu Perfil

+

Bem-vindo à sua área exclusiva.

+
{!isEditingProfile ? ( - ) : ( -
- - +
)}
-
- {/* Informações Pessoais */} -
-

Informações Pessoais

-
- -

{profileData.nome}

- Este campo não pode ser alterado + + {/* Grid de 3 colunas (2 + 1) */} +
+ {/* Coluna Esquerda - Informações Pessoais */} +
+ {/* Informações Pessoais */} +
+

Informações Pessoais

+ +
+ {/* Nome Completo */} +
+ +
+ {profileData.nome || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
+ + {/* Email */} +
+ +
+ {profileData.email || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
+ + {/* Telefone */} +
+ + {isEditingProfile ? ( + handleProfileChange('telefone', e.target.value)} + className="mt-2" + placeholder="(00) 00000-0000" + maxLength={15} + /> + ) : ( +
+ {profileData.telefone || "Não preenchido"} +
+ )} +
+
-
- - {isEditingProfile ? ( - handleProfileChange('email', e.target.value)} /> - ) : ( -

{profileData.email}

- )} + + {/* Endereço e Contato */} +
+

Endereço e Contato

+ +
+ {/* Logradouro */} +
+ + {isEditingProfile ? ( + handleProfileChange('endereco', e.target.value)} + className="mt-2" + placeholder="Rua, avenida, etc." + /> + ) : ( +
+ {profileData.endereco || "Não preenchido"} +
+ )} +
+ + {/* Cidade */} +
+ + {isEditingProfile ? ( + handleProfileChange('cidade', e.target.value)} + className="mt-2" + placeholder="São Paulo" + /> + ) : ( +
+ {profileData.cidade || "Não preenchido"} +
+ )} +
+ + {/* CEP */} +
+ + {isEditingProfile ? ( + handleProfileChange('cep', e.target.value)} + className="mt-2" + placeholder="00000-000" + /> + ) : ( +
+ {profileData.cep || "Não preenchido"} +
+ )} +
+
-
- +
+ + {/* Coluna Direita - Foto do Perfil */} +
+
+

Foto do Perfil

+ {isEditingProfile ? ( - handleProfileChange('telefone', e.target.value)} /> +
+ handleProfileChange('foto_url', newUrl)} + userName={profileData.nome} + /> +
) : ( -

{profileData.telefone}

+
+ + + {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'} + + + +
+

+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'} +

+
+
)}
- {/* Endereço e Contato (render apenas se existir algum dado) */} - {hasAddress && ( -
-

Endereço

-
- - {isEditingProfile ? ( - handleProfileChange('endereco', e.target.value)} /> - ) : ( -

{profileData.endereco}

- )} -
-
- - {isEditingProfile ? ( - handleProfileChange('cidade', e.target.value)} /> - ) : ( -

{profileData.cidade}

- )} -
-
- - {isEditingProfile ? ( - handleProfileChange('cep', e.target.value)} /> - ) : ( -

{profileData.cep}

- )} -
- {/* Biografia removed: not used */} -
- )} -
- {/* Foto do Perfil */} -
-

Foto do Perfil

- handleProfileChange('foto_url', newUrl)} - userName={profileData.nome} - />
) diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index 485fc90..17e9860 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -25,7 +25,7 @@ import { } from "@/components/ui/table"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar" -import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, Trash2, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from "lucide-react" +import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from "lucide-react" import { Calendar as CalendarIcon, FileText, Settings } from "lucide-react"; import { Tooltip, @@ -41,6 +41,7 @@ import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import ptBrLocale from "@fullcalendar/core/locales/pt-br"; +import { PatientRegistrationForm } from "@/components/features/forms/patient-registration-form"; const FullCalendar = dynamic(() => import("@fullcalendar/react"), { ssr: false, @@ -230,7 +231,7 @@ const ProfissionalPage = () => { })(); return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [user?.email, user?.id]); @@ -378,6 +379,9 @@ const ProfissionalPage = () => { const [editingEvent, setEditingEvent] = useState(null); const [showPopup, setShowPopup] = useState(false); const [showActionModal, setShowActionModal] = useState(false); + const [showDayModal, setShowDayModal] = useState(false); + const [selectedDayDate, setSelectedDayDate] = useState(null); + const [showPatientForm, setShowPatientForm] = useState(false); const [step, setStep] = useState(1); const [newEvent, setNewEvent] = useState({ title: "", @@ -686,6 +690,13 @@ const ProfissionalPage = () => {

Agenda do Dia

+
{/* Navegação de Data */} @@ -718,7 +729,7 @@ const ProfissionalPage = () => {
{/* Lista de Pacientes do Dia */} -
+
{todayEvents.length === 0 ? (
@@ -2656,150 +2667,216 @@ const ProfissionalPage = () => { const renderPerfilSection = () => ( -
+
+ {/* Header com Título e Botão */}
-

Meu Perfil

+
+

Meu Perfil

+

Bem-vindo à sua área exclusiva.

+
{!isEditingProfile ? ( - ) : (
- -
)}
-
- {/* Informações Pessoais */} -
-

Informações Pessoais

- -
- -

{profileData.nome}

- Este campo não pode ser alterado -
+ {/* Grid de 3 colunas (2 + 1) */} +
+ {/* Coluna Esquerda - Informações Pessoais */} +
+ {/* Informações Pessoais */} +
+

Informações Pessoais

-
- - {isEditingProfile ? ( - handleProfileChange('email', e.target.value)} - /> - ) : ( -

{profileData.email}

- )} -
+
+ {/* Nome Completo */} +
+ +
+ {profileData.nome || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
-
- - {isEditingProfile ? ( - handleProfileChange('telefone', e.target.value)} - /> - ) : ( -

{profileData.telefone}

- )} -
+ {/* Email */} +
+ + {isEditingProfile ? ( + handleProfileChange('email', e.target.value)} + className="mt-2" + type="email" + /> + ) : ( +
+ {profileData.email || "Não preenchido"} +
+ )} +
-
- -

{profileData.crm}

- Este campo não pode ser alterado -
+ {/* Telefone */} +
+ + {isEditingProfile ? ( + handleProfileChange('telefone', e.target.value)} + className="mt-2" + placeholder="(00) 00000-0000" + /> + ) : ( +
+ {profileData.telefone || "Não preenchido"} +
+ )} +
-
- - {isEditingProfile ? ( - handleProfileChange('especialidade', e.target.value)} - /> - ) : ( -

{profileData.especialidade}

- )} -
-
+ {/* CRM */} +
+ +
+ {profileData.crm || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
- {/* Endereço e Contato */} -
-

Endereço e Contato

- -
- - {isEditingProfile ? ( - handleProfileChange('endereco', e.target.value)} - /> - ) : ( -

{profileData.endereco}

- )} -
- -
- - {isEditingProfile ? ( - handleProfileChange('cidade', e.target.value)} - /> - ) : ( -

{profileData.cidade}

- )} -
- -
- - {isEditingProfile ? ( - handleProfileChange('cep', e.target.value)} - /> - ) : ( -

{profileData.cep}

- )} -
- - {/* Biografia removida: não é um campo no registro de médico */} -
-
- - {/* Foto do Perfil */} -
-

Foto do Perfil

-
- - - {profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()} - - - {isEditingProfile && ( -
- -

- Formatos aceitos: JPG, PNG (máx. 2MB) -

+ {/* Especialidade */} +
+ + {isEditingProfile ? ( + handleProfileChange('especialidade', e.target.value)} + className="mt-2" + placeholder="Ex: Cardiologia" + /> + ) : ( +
+ {profileData.especialidade || "Não preenchido"} +
+ )} +
- )} +
+ + {/* Endereço e Contato */} +
+

Endereço e Contato

+ +
+ {/* Logradouro */} +
+ + {isEditingProfile ? ( + handleProfileChange('endereco', e.target.value)} + className="mt-2" + placeholder="Rua, avenida, etc." + /> + ) : ( +
+ {profileData.endereco || "Não preenchido"} +
+ )} +
+ + {/* Cidade */} +
+ + {isEditingProfile ? ( + handleProfileChange('cidade', e.target.value)} + className="mt-2" + placeholder="São Paulo" + /> + ) : ( +
+ {profileData.cidade || "Não preenchido"} +
+ )} +
+ + {/* CEP */} +
+ + {isEditingProfile ? ( + handleProfileChange('cep', e.target.value)} + className="mt-2" + placeholder="00000-000" + /> + ) : ( +
+ {profileData.cep || "Não preenchido"} +
+ )} +
+
+
+
+ + {/* Coluna Direita - Foto do Perfil */} +
+
+

Foto do Perfil

+ +
+ + + {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'} + + + +
+

+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'} +

+
+
+
@@ -2838,8 +2915,8 @@ const ProfissionalPage = () => { ); case 'laudos': return renderLaudosSection(); - case 'comunicacao': - return renderComunicacaoSection(); + // case 'comunicacao': + // return renderComunicacaoSection(); case 'perfil': return renderPerfilSection(); default: @@ -2910,14 +2987,15 @@ const ProfissionalPage = () => { Laudos - + */} -
)} + + {/* Modal para visualizar pacientes de um dia específico */} + {showDayModal && selectedDayDate && ( +
+
+ {/* Header com navegação */} +
+ + +

+ {selectedDayDate.toLocaleDateString('pt-BR', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + })} +

+ + + +
+ + +
+ + {/* Content */} +
+ {(() => { + const dayStr = selectedDayDate.toISOString().split('T')[0]; + const dayEvents = events.filter(e => e.date === dayStr).sort((a, b) => a.time.localeCompare(b.time)); + + if (dayEvents.length === 0) { + return ( +
+ +

Nenhuma consulta agendada para este dia

+
+ ); + } + + return ( +
+

+ {dayEvents.length} consulta{dayEvents.length !== 1 ? 's' : ''} agendada{dayEvents.length !== 1 ? 's' : ''} +

+ {dayEvents.map((appointment) => { + const paciente = pacientes.find(p => p.nome === appointment.title); + return ( +
+
+
+

+ + {appointment.title} +

+ + {appointment.type} + +
+
+ + + {appointment.time} + + {paciente && ( + + CPF: {getPatientCpf(paciente)} • {getPatientAge(paciente)} anos + + )} +
+
+
+ ); + })} +
+ ); + })()} +
+
+
+ )} + + {/* Formulário para cadastro de paciente */} + { + // Adicionar o novo paciente à lista e recarregar + setPacientes((prev) => [...prev, newPaciente]); + }} + />
); diff --git a/susconecta/components/features/forms/patient-registration-form.tsx b/susconecta/components/features/forms/patient-registration-form.tsx index 1ff1dde..2e82998 100644 --- a/susconecta/components/features/forms/patient-registration-form.tsx +++ b/susconecta/components/features/forms/patient-registration-form.tsx @@ -264,7 +264,17 @@ export function PatientRegistrationForm({ } async function handleSubmit(ev: React.FormEvent) { - ev.preventDefault(); if (!validateLocal()) return; + ev.preventDefault(); + if (!validateLocal()) return; + + // Debug: verificar se token está disponível + const tokenCheck = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')) : null; + console.debug('[PatientForm] Token disponível?', !!tokenCheck ? 'SIM' : 'NÃO - Possível causa do erro!'); + if (!tokenCheck) { + setErrors({ submit: 'Sessão expirada. Por favor, faça login novamente.' }); + return; + } + try { if (!validarCPFLocal(form.cpf)) { setErrors((e) => ({ ...e, cpf: "CPF inválido" })); return; } if (mode === "create") { const existe = await verificarCpfDuplicado(form.cpf); if (existe) { setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" })); return; } } diff --git a/susconecta/components/ui/three-dwall-calendar.tsx b/susconecta/components/ui/three-dwall-calendar.tsx index 35c23f3..aa1a7e9 100644 --- a/susconecta/components/ui/three-dwall-calendar.tsx +++ b/susconecta/components/ui/three-dwall-calendar.tsx @@ -25,6 +25,7 @@ interface ThreeDWallCalendarProps { events: CalendarEvent[] onAddEvent?: (e: CalendarEvent) => void onRemoveEvent?: (id: string) => void + onOpenAddPatientForm?: () => void panelWidth?: number panelHeight?: number columns?: number @@ -34,6 +35,7 @@ export function ThreeDWallCalendar({ events, onAddEvent, onRemoveEvent, + onOpenAddPatientForm, panelWidth = 160, panelHeight = 120, columns = 7, @@ -448,9 +450,17 @@ export function ThreeDWallCalendar({ {/* Add event form */}
- setTitle(e.target.value)} /> - setNewDate(e.target.value)} /> - + {onOpenAddPatientForm ? ( + + ) : ( + <> + setTitle(e.target.value)} /> + setNewDate(e.target.value)} /> + + + )}
) diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 2b1cfd7..1cf4dd3 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1554,6 +1554,15 @@ export async function criarPaciente(input: PacienteInput): Promise { ]; let lastErr: any = null; + + // Debug: verificar token antes de tentar + const debugToken = getAuthToken(); + if (!debugToken) { + console.warn('[criarPaciente] ⚠️ AVISO: Nenhum token de autenticação encontrado no localStorage/sessionStorage! Tentando mesmo assim, mas possível causa do erro.'); + } else { + console.debug('[criarPaciente] ✓ Token encontrado, comprimento:', debugToken.length); + } + for (const u of fnUrls) { try { const headers = { ...baseHeaders(), 'Content-Type': 'application/json' } as Record; @@ -1562,7 +1571,7 @@ export async function criarPaciente(input: PacienteInput): Promise { const a = maskedHeaders.Authorization as string; maskedHeaders.Authorization = `${a.slice(0,6)}...${a.slice(-6)}`; } - // Log removido por segurança + console.debug('[criarPaciente] Tentando criar paciente em:', u.replace(/^https:\/\/[^\/]+/, 'https://[...host...]')); const res = await fetch(u, { method: 'POST', headers, @@ -1601,17 +1610,37 @@ export async function criarPaciente(input: PacienteInput): Promise { } catch (err: any) { lastErr = err; const emsg = err && typeof err === 'object' && 'message' in err ? (err as any).message : String(err); - console.warn('[criarPaciente] tentativa em', u, 'falhou:', emsg); - // If the underlying error is a network/CORS issue, add a helpful hint in the log - if (emsg && emsg.toLowerCase().includes('failed to fetch')) { - console.error('[criarPaciente] Falha de fetch (network/CORS). Verifique se você está autenticado no navegador (token presente em localStorage/sessionStorage) e se o endpoint permite requisições CORS do seu domínio. Também confirme que a função /create-user-with-password existe e está acessível.'); + console.warn('[criarPaciente] ❌ Tentativa em', u, 'falhou:', emsg); + + // Se o erro é uma falha de fetch (network/CORS) + if (emsg && (emsg.toLowerCase().includes('failed to fetch') || emsg.toLowerCase().includes('networkerror'))) { + console.error('[criarPaciente] ⚠️ FALHA DE REDE/CORS detectada. Possíveis causas:\n' + + '1. Função Supabase /create-user-with-password não existe ou está desativada\n' + + '2. CORS configurado incorretamente no Supabase\n' + + '3. Endpoint indisponível ou a rede está offline\n' + + '4. Token expirado ou inválido\n' + + 'URL que falhou:', u); } // try next } } const emsg = lastErr && typeof lastErr === 'object' && 'message' in lastErr ? (lastErr as any).message : String(lastErr ?? 'sem detalhes'); - throw new Error(`Falha ao criar paciente via create-user-with-password: ${emsg}. Verifique autenticação (token no localStorage/sessionStorage), CORS e se o endpoint /functions/v1/create-user-with-password está implementado e aceitando requisições do navegador.`); + + // Mensagem de erro mais detalhada e útil + let friendlyMsg = `Falha ao criar paciente.`; + if (emsg.toLowerCase().includes('networkerror') || emsg.toLowerCase().includes('failed to fetch')) { + friendlyMsg += ` Erro de rede/CORS detectado. `; + friendlyMsg += `Verifique se:\n`; + friendlyMsg += `• A função /create-user-with-password existe no Supabase\n`; + friendlyMsg += `• Você está autenticado (token no localStorage)\n`; + friendlyMsg += `• CORS está configurado corretamente\n`; + friendlyMsg += `• A rede está disponível`; + } else { + friendlyMsg += ` ${emsg}`; + } + + throw new Error(friendlyMsg); } export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise {