diff --git a/susconecta/app/(auth)/login-admin/page-new.tsx b/susconecta/app/(auth)/login-admin/page-new.tsx new file mode 100644 index 0000000..6327e34 --- /dev/null +++ b/susconecta/app/(auth)/login-admin/page-new.tsx @@ -0,0 +1,17 @@ +'use client' +import { useEffect } from 'react' +import { useRouter } from 'next/navigation' + +export default function LoginAdminRedirect() { + const router = useRouter() + + useEffect(() => { + router.replace('/login') + }, [router]) + + return ( +
Redirecionando para a página de login...
- Entre com suas credenciais para acessar o sistema administrativo -
- Acesse sua área pessoal e gerencie suas consultas -
Redirecionando...
Entre com suas credenciais para acessar o sistema @@ -70,7 +124,7 @@ export default function LoginPage() { - Acesso ao Sistema + Login @@ -121,9 +175,8 @@ export default function LoginPage() { - - + Voltar ao Início 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/forms/doctor-registration-form.tsx b/susconecta/components/features/forms/doctor-registration-form.tsx index 189f0b3..2183868 100644 --- a/susconecta/components/features/forms/doctor-registration-form.tsx +++ b/susconecta/components/features/forms/doctor-registration-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useRef } from "react"; import { parse, parseISO, format } from 'date-fns'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -167,6 +167,7 @@ export function DoctorRegistrationForm({ userName: string; userType: 'médico' | 'paciente'; } | null>(null); + const savedDoctorRef = useRef(null); const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]); @@ -504,6 +505,11 @@ async function handleSubmit(ev: React.FormEvent) { // 1. Cria o perfil do médico na tabela doctors let savedDoctorProfile: any = await criarMedico(medicoPayload); console.log("✅ Perfil do médico criado:", savedDoctorProfile); + console.log("🔑 Senha no objeto retornado:", savedDoctorProfile?.password); + + // Salvar a senha ANTES de qualquer operação que possa sobrescrever o objeto + const senhaGerada = savedDoctorProfile?.password; + console.log("💾 Senha salva em variável:", senhaGerada); // Fallback: some create flows don't persist optional fields like birth_date/cep/sexo. // If the returned object is missing those but our payload included them, @@ -531,7 +537,9 @@ async function handleSubmit(ev: React.FormEvent) { const patched = await atualizarMedico(String(createdDoctorId), medicoPayload).catch((e) => { console.warn('[DoctorForm] fallback PATCH failed:', e); return null; }); if (patched) { console.debug('[DoctorForm] fallback PATCH result:', patched); - savedDoctorProfile = patched; + // Preservar a senha ao atualizar o objeto + savedDoctorProfile = { ...patched, password: senhaGerada }; + console.log("🔄 Senha preservada após PATCH:", savedDoctorProfile?.password); } } } catch (e) { @@ -547,6 +555,7 @@ async function handleSubmit(ev: React.FormEvent) { // { doctor, doctor_id, email, password, user_id } or similar shapes. const result = savedDoctorProfile as any; console.log('✅ Resultado de criarMedico:', result); + console.log('🔑 Senha no resultado final:', result?.password); // Determine the doctor id if available let createdDoctorId: string | null = null; @@ -559,13 +568,36 @@ async function handleSubmit(ev: React.FormEvent) { // If the function returned credentials, show them in the credentials dialog if (result && (result.password || result.email || result.user)) { - setCredentials({ + console.log('📧 Credenciais recebidas - configurando dialog...'); + console.log('📧 Email:', result.email || form.email); + console.log('🔑 Senha extraída:', result.password); + console.log('👤 Nome do usuário:', form.full_name); + + const credenciaisParaExibir = { email: result.email || form.email, - password: result.password || "", + password: result.password || senhaGerada || "", userName: form.full_name, - userType: 'médico', - }); + userType: 'médico' as const, + }; + + console.log('📋 Credenciais a serem definidas:', credenciaisParaExibir); + + // Salvar o médico no ref ANTES de abrir o dialog + savedDoctorRef.current = savedDoctorProfile; + + setCredentials(credenciaisParaExibir); setShowCredentialsDialog(true); + console.log('✅ Dialog de credenciais configurado e aberto'); + + // Verificar estados após 100ms + setTimeout(() => { + console.log('🔍 Verificando estados após 100ms:'); + console.log('- showCredentialsDialog:', showCredentialsDialog); + console.log('- credentials:', credentials); + }, 100); + + // NÃO fechar o formulário aqui - será fechado quando o usuário fechar o dialog de credenciais + return; // Sair da função para não executar o cleanup abaixo } // Upload photo if provided and we have an id @@ -800,8 +832,8 @@ async function handleSubmit(ev: React.FormEvent) { setField("data_nascimento", date || null)} + selected={form.data_nascimento ?? undefined} + onSelect={(date) => setField("data_nascimento", date ?? null)} initialFocus /> @@ -1061,28 +1093,37 @@ async function handleSubmit(ev: React.FormEvent) { <> {content} - {/* Dialog de credenciais */} - {credentials && ( - { - setShowCredentialsDialog(open); - if (!open) { - // Quando o dialog de credenciais fecha, fecha o formulário também - setCredentials(null); - if (inline) { - onClose?.(); - } else { - onOpenChange?.(false); - } + { + console.log('🔄 CredentialsDialog (inline) onOpenChange chamado com:', open); + setShowCredentialsDialog(open); + if (!open) { + // Dialog foi fechado - limpar estados e fechar formulário + console.log('✅ Dialog fechado - limpando formulário...'); + setCredentials(null); + + // Chamar onSaved se houver médico salvo + if (savedDoctorRef.current) { + onSaved?.(savedDoctorRef.current); + savedDoctorRef.current = null; } - }} - email={credentials.email} - password={credentials.password} - userName={credentials.userName} - userType={credentials.userType} - /> - )} + + // Limpar formulário + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + + // Fechar formulário + if (inline) onClose?.(); + else onOpenChange?.(false); + } + }} + email={credentials?.email || ''} + password={credentials?.password || ''} + userName={credentials?.userName || ''} + userType={credentials?.userType || 'médico'} + /> > ); } @@ -1100,23 +1141,36 @@ async function handleSubmit(ev: React.FormEvent) { - {/* Dialog de credenciais */} - {credentials && ( - { - setShowCredentialsDialog(open); - if (!open) { - setCredentials(null); - onOpenChange?.(false); + { + console.log('🔄 CredentialsDialog (dialog) onOpenChange chamado com:', open); + setShowCredentialsDialog(open); + if (!open) { + // Dialog foi fechado - limpar estados e fechar formulário + console.log('✅ Dialog fechado - limpando formulário...'); + setCredentials(null); + + // Chamar onSaved se houver médico salvo + if (savedDoctorRef.current) { + onSaved?.(savedDoctorRef.current); + savedDoctorRef.current = null; } - }} - email={credentials.email} - password={credentials.password} - userName={credentials.userName} - userType={credentials.userType} - /> - )} + + // Limpar formulário + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + + // Fechar formulário principal + onOpenChange?.(false); + } + }} + email={credentials?.email || ''} + password={credentials?.password || ''} + userName={credentials?.userName || ''} + userType={credentials?.userType || 'médico'} + /> > ); } diff --git a/susconecta/components/features/forms/patient-registration-form.tsx b/susconecta/components/features/forms/patient-registration-form.tsx index feafa89..aa11b20 100644 --- a/susconecta/components/features/forms/patient-registration-form.tsx +++ b/susconecta/components/features/forms/patient-registration-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useRef } from "react"; import { format, parseISO, parse } from "date-fns"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -138,6 +138,9 @@ export function PatientRegistrationForm({ userName: string; userType: 'médico' | 'paciente'; } | null>(null); + + // Ref para guardar o paciente salvo para chamar onSaved quando o dialog fechar + const savedPatientRef = useRef(null); const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]); @@ -276,7 +279,11 @@ export function PatientRegistrationForm({ setErrors((e) => ({ ...e, telefone: 'Telefone é obrigatório quando email é informado (fluxo de criação único).' })); setSubmitting(false); return; } let savedPatientProfile: any = await criarPaciente(patientPayload); - console.log('Perfil do paciente criado (via Function):', savedPatientProfile); + console.log('🎯 Paciente criado! Resposta completa:', savedPatientProfile); + console.log('🔑 Senha no objeto:', savedPatientProfile?.password); + + // Guardar a senha ANTES de qualquer operação que possa sobrescrever o objeto + const senhaGerada = savedPatientProfile?.password; // Fallback: some backend create flows (create-user-with-password) do not // persist optional patient fields like sex/cep/birth_date. The edit flow @@ -295,17 +302,56 @@ export function PatientRegistrationForm({ const patched = await atualizarPaciente(String(pacienteId), patientPayload).catch((e) => { console.warn('[PatientForm] fallback PATCH falhou:', e); return null; }); if (patched) { console.debug('[PatientForm] fallback PATCH result:', patched); - savedPatientProfile = patched; + // Preserva a senha ao fazer merge do patch + savedPatientProfile = { ...patched, password: senhaGerada }; } } } catch (e) { console.warn('[PatientForm] erro ao tentar fallback PATCH:', e); } - const maybePassword = (savedPatientProfile as any)?.password || (savedPatientProfile as any)?.generated_password; - if (maybePassword) { - setCredentials({ email: (savedPatientProfile as any).email || form.email, password: String(maybePassword), userName: form.nome, userType: 'paciente' }); + // Usar a senha que foi guardada ANTES do PATCH + const emailToDisplay = savedPatientProfile?.email || form.email; + console.log('📧 Email para exibir:', emailToDisplay); + console.log('🔐 Senha para exibir:', senhaGerada); + + if (senhaGerada && emailToDisplay) { + console.log('✅ Abrindo modal de credenciais...'); + const credentialsToShow = { + email: emailToDisplay, + password: String(senhaGerada), + userName: form.nome, + userType: 'paciente' as const + }; + console.log('📝 Credenciais a serem definidas:', credentialsToShow); + + // Guardar o paciente salvo no ref para usar quando o dialog fechar + savedPatientRef.current = savedPatientProfile; + + // Definir credenciais e abrir dialog + setCredentials(credentialsToShow); setShowCredentialsDialog(true); + + // NÃO limpar o formulário ou fechar ainda - aguardar o usuário fechar o dialog de credenciais + // O dialog de credenciais vai chamar onSaved e fechar quando o usuário clicar em "Fechar" + + // Verificar se foi setado + setTimeout(() => { + console.log('🔍 Verificando estados após 100ms:'); + console.log(' - showCredentialsDialog:', showCredentialsDialog); + console.log(' - credentials:', credentials); + }, 100); + } else { + console.error('❌ Não foi possível exibir credenciais:', { senhaGerada, emailToDisplay }); + alert(`Paciente criado!\n\nEmail: ${emailToDisplay}\n\nAVISO: A senha não pôde ser recuperada. Entre em contato com o suporte.`); + + // Se não há senha, limpar e fechar normalmente + onSaved?.(savedPatientProfile); + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + if (inline) onClose?.(); + else onOpenChange?.(false); } if (form.photo) { @@ -313,8 +359,6 @@ export function PatientRegistrationForm({ catch (upErr) { console.warn('[PatientForm] Falha ao enviar foto do paciente após criação:', upErr); alert('Paciente criado, mas falha ao enviar a foto. Você pode tentar novamente no perfil.'); } finally { setUploadingPhoto(false); } } - - onSaved?.(savedPatientProfile); setForm(initial); setPhotoPreview(null); setServerAnexos([]); if (inline) onClose?.(); else onOpenChange?.(false); } } catch (err: any) { console.error("❌ Erro no handleSubmit:", err); const userMessage = err?.message?.includes("toPayload") || err?.message?.includes("is not defined") ? "Erro ao processar os dados do formulário. Por favor, verifique os campos e tente novamente." : err?.message || "Erro ao salvar paciente. Por favor, tente novamente."; setErrors({ submit: userMessage }); } finally { setSubmitting(false); } @@ -519,8 +563,86 @@ export function PatientRegistrationForm({ ); if (inline) { - return (<>{content}{credentials && ( { setShowCredentialsDialog(open); if (!open) { setCredentials(null); if (inline) onClose?.(); else onOpenChange?.(false); } }} email={credentials.email} password={credentials.password} userName={credentials.userName} userType={credentials.userType} />)}>); + return ( + <> + {content} + { + console.log('🔄 CredentialsDialog onOpenChange chamado com:', open); + setShowCredentialsDialog(open); + if (!open) { + // Dialog foi fechado - limpar estados e fechar formulário + console.log('✅ Dialog fechado - limpando formulário...'); + setCredentials(null); + + // Chamar onSaved se houver paciente salvo + if (savedPatientRef.current) { + onSaved?.(savedPatientRef.current); + savedPatientRef.current = null; + } + + // Limpar formulário + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + + // Fechar formulário + if (inline) onClose?.(); + else onOpenChange?.(false); + } + }} + email={credentials?.email || ''} + password={credentials?.password || ''} + userName={credentials?.userName || ''} + userType={credentials?.userType || 'paciente'} + /> + > + ); } - return (<> {title}{content}{credentials && ( { setShowCredentialsDialog(open); if (!open) { setCredentials(null); onOpenChange?.(false); } }} email={credentials.email} password={credentials.password} userName={credentials.userName} userType={credentials.userType} />)}>); + return ( + <> + + + + + {title} + + + {content} + + + { + console.log('🔄 CredentialsDialog onOpenChange chamado com:', open); + setShowCredentialsDialog(open); + if (!open) { + // Dialog foi fechado - limpar estados e fechar formulário + console.log('✅ Dialog fechado - limpando formulário...'); + setCredentials(null); + + // Chamar onSaved se houver paciente salvo + if (savedPatientRef.current) { + onSaved?.(savedPatientRef.current); + savedPatientRef.current = null; + } + + // Limpar formulário + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + + // Fechar formulário principal + onOpenChange?.(false); + } + }} + email={credentials?.email || ''} + password={credentials?.password || ''} + userName={credentials?.userName || ''} + userType={credentials?.userType || 'paciente'} + /> + > + ); } \ No newline at end of file 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({ - - - Título - - 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 ? ( + <> + + + Título + setNewEvent((prev) => ({ ...prev, title: e.target.value }))} + placeholder="Título do evento" + /> + - - Descrição - - isCreating - ? setNewEvent((prev) => ({ - ...prev, - description: e.target.value, - })) - : setSelectedEvent((prev) => (prev ? { ...prev, description: e.target.value } : null)) - } - placeholder="Descrição do evento" - rows={3} - /> - + + Descrição + setNewEvent((prev) => ({ ...prev, description: e.target.value }))} + placeholder="Descrição do evento" + rows={3} + /> + - - - Início - { - const date = new Date(e.target.value) - isCreating - ? setNewEvent((prev) => ({ ...prev, startTime: date })) - : setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null)) - }} - /> + + + Início + setNewEvent((prev) => ({ ...prev, startTime: new Date(e.target.value) }))} + /> + + + + Fim + setNewEvent((prev) => ({ ...prev, endTime: new Date(e.target.value) }))} + /> + + - - Fim - { - const date = new Date(e.target.value) - isCreating - ? setNewEvent((prev) => ({ ...prev, endTime: date })) - : setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null)) + + { + setIsDialogOpen(false) + setIsCreating(false) + setSelectedEvent(null) }} - /> + > + Cancelar + + Criar + + > + ) : ( + <> + {/* Read-only compact view: title + stacked details + descrição abaixo */} + + + {selectedEvent?.title || "—"} + + + + + + Profissional + {selectedEvent?.professionalName || "—"} + + + + Paciente + {selectedEvent?.patientName || "—"} + + + + Tipo + {selectedEvent?.appointmentType || "—"} + + + + Status + {selectedEvent?.status || "—"} + + + + Data + {(() => { + const formatDate = (d?: string | Date) => { + if (!d) return "—" + try { + const dt = d instanceof Date ? d : new Date(d) + if (isNaN(dt.getTime())) return "—" + return dt.toLocaleString(LOCALE, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: TIMEZONE }) + } catch (e) { + return "—" + } + } + return formatDate(selectedEvent?.startTime) + })()} + + + {selectedEvent?.completedAt && ( + + Concluído em + {(() => { + const dt = selectedEvent.completedAt + try { + const d = dt instanceof Date ? dt : new Date(dt as any) + return isNaN(d.getTime()) ? "—" : d.toLocaleString(LOCALE, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: TIMEZONE }) + } catch { return "—" } + })()} + + )} + + {selectedEvent?.cancelledAt && ( + + Cancelado em + {(() => { + const dt = selectedEvent.cancelledAt + try { + const d = dt instanceof Date ? dt : new Date(dt as any) + return isNaN(d.getTime()) ? "—" : d.toLocaleString(LOCALE, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: TIMEZONE }) + } catch { return "—" } + })()} + Motivo do cancelamento + {selectedEvent?.cancellationReason || "—"} + + )} + + + + + Observações + + {sanitizeDescription(selectedEvent?.description) ?? "—"} + + + + + { + setIsDialogOpen(false) + setIsCreating(false) + setSelectedEvent(null) + }} + > + Fechar + + - - - {/* Campos de Categoria/Cor removidos */} - - {/* Campo de Tags removido */} - - - - {!isCreating && ( - selectedEvent && handleDeleteEvent(selectedEvent.id)}> - Deletar - - )} - { - setIsDialogOpen(false) - setIsCreating(false) - setSelectedEvent(null) - }} - > - Cancelar - - - {isCreating ? "Criar" : "Salvar"} - - + > + )} @@ -943,7 +1077,7 @@ function MonthView({ ) } -// Week View Component +// Week View Component (simplified and stable) function WeekView({ currentDate, events, @@ -958,7 +1092,7 @@ function WeekView({ onEventClick: (event: Event) => void onDragStart: (event: Event) => void onDragEnd: () => void - onDrop: (date: Date, hour: number) => void + onDrop: (date: Date, hour?: number) => void getColorClasses: (color: string) => { bg: string; text: string } }) { const startOfWeek = new Date(currentDate) @@ -970,103 +1104,55 @@ function WeekView({ return day }) - // NOVO: limita intervalo de horas ao 1º e último evento da semana - const [startHour, endHour] = React.useMemo(() => { - let minH = Infinity - let maxH = -Infinity - for (const ev of events) { - const d = ev.startTime - const sameWeekDay = weekDays.some(wd => - d.getFullYear() === wd.getFullYear() && - d.getMonth() === wd.getMonth() && - d.getDate() === wd.getDate() - ) - if (!sameWeekDay) continue - minH = Math.min(minH, d.getHours()) - maxH = Math.max(maxH, ev.endTime.getHours()) - } - if (!isFinite(minH) || !isFinite(maxH)) return [0, 23] as const - if (maxH < minH) maxH = minH - return [minH, maxH] as const - }, [events, weekDays]) - - const hours = React.useMemo( - () => Array.from({ length: (endHour - startHour + 1) }, (_, i) => startHour + i), - [startHour, endHour] - ) - - const getEventsForDayAndHour = (date: Date, hour: number) => { - return events.filter((event) => { - const eventDate = new Date(event.startTime) - const eventHour = eventDate.getHours() + const getEventsForDay = (date: Date) => + events.filter((event) => { + const d = new Date(event.startTime) return ( - eventDate.getDate() === date.getDate() && - eventDate.getMonth() === date.getMonth() && - eventDate.getFullYear() === date.getFullYear() && - eventHour === hour + d.getFullYear() === date.getFullYear() && + d.getMonth() === date.getMonth() && + d.getDate() === date.getDate() ) }) - } return ( - - Hora + {weekDays.map((day) => ( - - {day.toLocaleDateString(LOCALE, { weekday: "short", timeZone: TIMEZONE })} - {day.toLocaleDateString(LOCALE, { weekday: "narrow", timeZone: TIMEZONE })} - - {day.toLocaleDateString(LOCALE, { month: "short", day: "numeric", timeZone: TIMEZONE })} - + + {day.toLocaleDateString(LOCALE, { weekday: "short", timeZone: TIMEZONE })} + {day.toLocaleDateString(LOCALE, { weekday: "narrow", timeZone: TIMEZONE })} ))} - - {hours.map((hour) => ( - - - {hour.toString().padStart(2, "0")}:00 - - {weekDays.map((day) => { - const dayEvents = getEventsForDayAndHour(day, hour) - return ( - e.preventDefault()} - onDrop={() => onDrop(day, hour)} - > - - {dayEvents.map((event) => ( - - ))} + + + {weekDays.map((day, idx) => { + const dayEvents = getEventsForDay(day) + return ( + + + {dayEvents.map((ev) => ( + + - - ) - })} - - ))} + ))} + + + ) + })} ) } -// Day View Component +// Day View Component (simple hourly lanes) function DayView({ currentDate, events, @@ -1081,42 +1167,21 @@ function DayView({ onEventClick: (event: Event) => void onDragStart: (event: Event) => void onDragEnd: () => void - onDrop: (date: Date, hour: number) => void + onDrop: (date: Date, hour?: number) => void getColorClasses: (color: string) => { bg: string; text: string } }) { - // NOVO: calcula intervalo de horas do 1º ao último evento do dia - const [startHour, endHour] = React.useMemo(() => { - const sameDayEvents = events.filter((ev) => { - const d = ev.startTime + const hours = Array.from({ length: 24 }, (_, i) => i) + + const getEventsForHour = (hour: number) => + events.filter((event) => { + const d = new Date(event.startTime) return ( - d.getDate() === currentDate.getDate() && + d.getFullYear() === currentDate.getFullYear() && d.getMonth() === currentDate.getMonth() && - d.getFullYear() === currentDate.getFullYear() + d.getDate() === currentDate.getDate() && + d.getHours() === hour ) }) - if (!sameDayEvents.length) return [0, 23] as const - const minH = Math.min(...sameDayEvents.map((e) => e.startTime.getHours())) - const maxH = Math.max(...sameDayEvents.map((e) => e.endTime.getHours())) - return [minH, Math.max(maxH, minH)] as const - }, [events, currentDate]) - - const hours = React.useMemo( - () => Array.from({ length: (endHour - startHour + 1) }, (_, i) => startHour + i), - [startHour, endHour] - ) - - const getEventsForHour = (hour: number) => { - return events.filter((event) => { - const eventDate = new Date(event.startTime) - const eventHour = eventDate.getHours() - return ( - eventDate.getDate() === currentDate.getDate() && - eventDate.getMonth() === currentDate.getMonth() && - eventDate.getFullYear() === currentDate.getFullYear() && - eventHour === hour - ) - }) - } return ( @@ -1124,27 +1189,14 @@ function DayView({ {hours.map((hour) => { const hourEvents = getEventsForHour(hour) return ( - e.preventDefault()} - onDrop={() => onDrop(currentDate, hour)} - > + e.preventDefault()} onDrop={() => onDrop(currentDate, hour)}> {hour.toString().padStart(2, "0")}:00 {hourEvents.map((event) => ( - + ))} diff --git a/susconecta/components/features/general/hero-section.tsx b/susconecta/components/features/general/hero-section.tsx index 3fb27cc..107560a 100644 --- a/susconecta/components/features/general/hero-section.tsx +++ b/susconecta/components/features/general/hero-section.tsx @@ -23,24 +23,7 @@ export function HeroSection() { - {} - - - Portal do Paciente - - - Sou Profissional de Saúde - - + {} diff --git a/susconecta/components/layout/header.tsx b/susconecta/components/layout/header.tsx index e3f7ffc..6ea5cef 100644 --- a/susconecta/components/layout/header.tsx +++ b/susconecta/components/layout/header.tsx @@ -50,20 +50,8 @@ export function Header() { className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground" asChild > - - Sou Paciente + Entrar - - Sou Profissional de Saúde - - - - Sou Administrador de uma Clínica - - {} @@ -101,19 +89,8 @@ export function Header() { className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground" asChild > - Sou Paciente + Entrar - - Sou Profissional de Saúde - - - - Sou Administrador de uma Clínica - - diff --git a/susconecta/components/shared/ProtectedRoute.tsx b/susconecta/components/shared/ProtectedRoute.tsx index 31e9633..7c62b96 100644 --- a/susconecta/components/shared/ProtectedRoute.tsx +++ b/susconecta/components/shared/ProtectedRoute.tsx @@ -126,17 +126,6 @@ export default function ProtectedRoute({ Você não tem permissão para acessar esta página. - - Tipo de acesso necessário: {requiredUserType.join(' ou ')} - - Seu tipo de acesso: {user.userType} - - router.push(USER_TYPE_ROUTES[user.userType])} - className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 cursor-pointer" - > - Ir para minha área - ) diff --git a/susconecta/hooks/useAuth.tsx b/susconecta/hooks/useAuth.tsx index fd79c8b..1725b99 100644 --- a/susconecta/hooks/useAuth.tsx +++ b/susconecta/hooks/useAuth.tsx @@ -298,8 +298,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { throw error } + const errorMessage = error instanceof Error ? error.message : String(error) throw new AuthenticationError( - 'Erro inesperado durante o login', + errorMessage || 'Erro inesperado durante o login', 'UNKNOWN_ERROR', error ) diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index fe7c217..a3b80c1 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -2139,25 +2139,25 @@ export async function criarMedico(input: MedicoInput): Promise { // If server returned doctor_id, fetch the doctor if (parsed && parsed.doctor_id) { const doc = await buscarMedicoPorId(String(parsed.doctor_id)).catch(() => null); - if (doc) return Object.assign(doc, { password }); - if (parsed.doctor) return Object.assign(parsed.doctor, { password }); - return Object.assign({ id: parsed.doctor_id, full_name: input.full_name, cpf: cleanCpf, email: input.email } as Medico, { password }); + if (doc) return { ...doc, password } as any; + if (parsed.doctor) return { ...parsed.doctor, password } as any; + return { id: parsed.doctor_id, full_name: input.full_name, cpf: cleanCpf, email: input.email, password } as any; } // If server returned doctor object directly if (parsed && (parsed.id || parsed.full_name || parsed.cpf)) { - return Object.assign(parsed, { password }) as Medico; + return { ...parsed, password } as any; } // If server returned an envelope with user, try to locate doctor by email if (parsed && parsed.user && parsed.user.id) { const maybe = await fetch(`${REST}/doctors?email=eq.${encodeURIComponent(String(input.email))}&select=*`, { method: 'GET', headers: baseHeaders() }).then((r) => r.ok ? r.json().catch(() => []) : []); - if (Array.isArray(maybe) && maybe.length) return Object.assign(maybe[0] as Medico, { password }); - return Object.assign({ id: parsed.user.id, full_name: input.full_name, email: input.email } as Medico, { password }); + if (Array.isArray(maybe) && maybe.length) return { ...maybe[0], password } as any; + return { id: parsed.user.id, full_name: input.full_name, email: input.email, password } as any; } // otherwise return parsed with password as best-effort - return Object.assign(parsed || {}, { password }); + return { ...(parsed || {}), password } as any; } catch (err: any) { lastErr = err; const emsg = err && typeof err === 'object' && 'message' in err ? (err as any).message : String(err); diff --git a/susconecta/lib/http.ts b/susconecta/lib/http.ts index cd55f8f..1583e94 100644 --- a/susconecta/lib/http.ts +++ b/susconecta/lib/http.ts @@ -189,15 +189,7 @@ class HttpClient { // Redirecionar para login if (typeof window !== 'undefined') { - const userType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) || 'profissional' - const loginRoutes = { - profissional: '/login', - paciente: '/login-paciente', - administrador: '/login-admin' - } - - const loginRoute = loginRoutes[userType as keyof typeof loginRoutes] || '/login' - window.location.href = loginRoute + window.location.href = '/login' } } diff --git a/susconecta/types/auth.ts b/susconecta/types/auth.ts index e4bcd45..84a7adb 100644 --- a/susconecta/types/auth.ts +++ b/susconecta/types/auth.ts @@ -85,6 +85,6 @@ export const USER_TYPE_ROUTES: UserTypeRoutes = { export const LOGIN_ROUTES: LoginRoutes = { profissional: '/login', - paciente: '/login-paciente', - administrador: '/login-admin', + paciente: '/login', + administrador: '/login', } as const \ No newline at end of file
- Navegue através do atalho: Calendário (C). -
Navegue através do atalho: Calendário (C).
Você não tem permissão para acessar esta página.
- Tipo de acesso necessário: {requiredUserType.join(' ou ')} - - Seu tipo de acesso: {user.userType} -