From c8813749c60a76258b876eb9ade074eaf03138a7 Mon Sep 17 00:00:00 2001 From: M-Gabrielly Date: Sat, 8 Nov 2025 00:05:31 -0300 Subject: [PATCH 1/3] feat(auth): unify login page and redirect by role --- .../app/(auth)/login-admin/page-new.tsx | 17 +++ susconecta/app/(auth)/login-admin/page.tsx | 125 +--------------- susconecta/app/(auth)/login-paciente/page.tsx | 135 +----------------- .../app/(auth)/login-profissional/page.tsx | 17 +++ susconecta/app/(auth)/login/page.tsx | 111 ++++++++++---- .../features/general/hero-section.tsx | 19 +-- susconecta/components/layout/header.tsx | 27 +--- .../components/shared/ProtectedRoute.tsx | 11 -- susconecta/hooks/useAuth.tsx | 3 +- susconecta/lib/http.ts | 10 +- susconecta/types/auth.ts | 4 +- 11 files changed, 138 insertions(+), 341 deletions(-) create mode 100644 susconecta/app/(auth)/login-admin/page-new.tsx create mode 100644 susconecta/app/(auth)/login-profissional/page.tsx 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...

+
+ ) +} diff --git a/susconecta/app/(auth)/login-admin/page.tsx b/susconecta/app/(auth)/login-admin/page.tsx index 03260c8..571ee04 100644 --- a/susconecta/app/(auth)/login-admin/page.tsx +++ b/susconecta/app/(auth)/login-admin/page.tsx @@ -1,128 +1,17 @@ 'use client' -import { useState } from 'react' +import { useEffect } from 'react' import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { useAuth } from '@/hooks/useAuth' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { AuthenticationError } from '@/lib/auth' -export default function LoginAdminPage() { - const [credentials, setCredentials] = useState({ email: '', password: '' }) - const [error, setError] = useState('') - - const [loading, setLoading] = useState(false) +export default function LoginAdminRedirect() { const router = useRouter() - const { login } = useAuth() - const handleLogin = async (e: React.FormEvent) => { - e.preventDefault() - setLoading(true) - setError('') - - try { - // Tentar fazer login usando o contexto com tipo administrador - const success = await login(credentials.email, credentials.password, 'administrador') - - if (success) { - console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...') - - // Redirecionamento direto - solução que funcionou - window.location.href = '/dashboard' - } - } catch (err) { - console.error('[LOGIN-ADMIN] Erro no login:', err) - - if (err instanceof AuthenticationError) { - setError(err.message) - } else { - setError('Erro inesperado. Tente novamente.') - } - } finally { - setLoading(false) - } - } - - + useEffect(() => { + router.replace('/login') + }, [router]) return ( -
-
-
-

- Login Administrador de Clínica -

-

- Entre com suas credenciais para acessar o sistema administrativo -

-
- - - - Acesso Administrativo - - -
-
- - setCredentials({...credentials, email: e.target.value})} - required - className="mt-1" - disabled={loading} - /> -
- -
- - setCredentials({...credentials, password: e.target.value})} - required - className="mt-1" - disabled={loading} - /> -
- - {error && ( - - {error} - - )} - - -
- - -
- -
-
-
-
+
+

Redirecionando para a página de login...

) } \ No newline at end of file diff --git a/susconecta/app/(auth)/login-paciente/page.tsx b/susconecta/app/(auth)/login-paciente/page.tsx index 0927014..18c43bf 100644 --- a/susconecta/app/(auth)/login-paciente/page.tsx +++ b/susconecta/app/(auth)/login-paciente/page.tsx @@ -1,138 +1,17 @@ 'use client' -import { useState } from 'react' +import { useEffect } from 'react' import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { useAuth } from '@/hooks/useAuth' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { AuthenticationError } from '@/lib/auth' -export default function LoginPacientePage() { - const [credentials, setCredentials] = useState({ email: '', password: '' }) - const [error, setError] = useState('') - - const [loading, setLoading] = useState(false) +export default function LoginPacienteRedirect() { const router = useRouter() - const { login } = useAuth() - const handleLogin = async (e: React.FormEvent) => { - e.preventDefault() - setLoading(true) - setError('') - - try { - // Tentar fazer login usando o contexto com tipo paciente - const success = await login(credentials.email, credentials.password, 'paciente') - - if (success) { - // Redirecionar para a página do paciente - router.push('/paciente') - } - } catch (err) { - console.error('[LOGIN-PACIENTE] Erro no login:', err) - - if (err instanceof AuthenticationError) { - // Verificar se é erro de credenciais inválidas (pode ser email não confirmado) - if (err.code === '400' || err.details?.error_code === 'invalid_credentials') { - setError( - '⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' + - 'verifique sua caixa de entrada e clique no link de confirmação ' + - 'que foi enviado para ' + credentials.email - ) - } else { - setError(err.message) - } - } else { - setError('Erro inesperado. Tente novamente.') - } - } finally { - setLoading(false) - } - } - - - - // Auto-cadastro foi removido (UI + client-side endpoint call) + useEffect(() => { + router.replace('/login') + }, [router]) return ( -
-
-
-

- Sou Paciente -

-

- Acesse sua área pessoal e gerencie suas consultas -

-
- - - - Entrar como Paciente - - -
-
- - setCredentials({...credentials, email: e.target.value})} - required - className="mt-1" - disabled={loading} - /> -
- -
- - setCredentials({...credentials, password: e.target.value})} - required - className="mt-1" - disabled={loading} - /> -
- - {error && ( - - {error} - - )} - - -
- - -
- -
- {/* Auto-cadastro UI removed */} -
-
-
+
+

Redirecionando...

) } \ No newline at end of file diff --git a/susconecta/app/(auth)/login-profissional/page.tsx b/susconecta/app/(auth)/login-profissional/page.tsx new file mode 100644 index 0000000..5a42390 --- /dev/null +++ b/susconecta/app/(auth)/login-profissional/page.tsx @@ -0,0 +1,17 @@ +'use client' +import { useEffect } from 'react' +import { useRouter } from 'next/navigation' + +export default function LoginProfissionalRedirect() { + const router = useRouter() + + useEffect(() => { + router.replace('/login') + }, [router]) + + return ( +
+

Redirecionando para a página de login...

+
+ ) +} diff --git a/susconecta/app/(auth)/login/page.tsx b/susconecta/app/(auth)/login/page.tsx index 2a55ba9..0fd6d34 100644 --- a/susconecta/app/(auth)/login/page.tsx +++ b/susconecta/app/(auth)/login/page.tsx @@ -12,10 +12,23 @@ import { AuthenticationError } from '@/lib/auth' export default function LoginPage() { const [credentials, setCredentials] = useState({ email: '', password: '' }) const [error, setError] = useState('') - const [loading, setLoading] = useState(false) const router = useRouter() - const { login } = useAuth() + const { login, user } = useAuth() + + // Mapeamento de redirecionamento baseado em role + const getRoleRedirectPath = (userType: string): string => { + switch (userType) { + case 'paciente': + return '/paciente' + case 'profissional': + return '/profissional' + case 'administrador': + return '/dashboard' + default: + return '/' + } + } const handleLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -23,32 +36,73 @@ export default function LoginPage() { setError('') try { - // Tentar fazer login usando o contexto com tipo profissional - const success = await login(credentials.email, credentials.password, 'profissional') + // Tentar fazer login com cada tipo de usuário até conseguir + // Ordem de prioridade: profissional (inclui médico), paciente, administrador + const userTypes: Array<'paciente' | 'profissional' | 'administrador'> = [ + 'profissional', // Tentar profissional PRIMEIRO pois inclui médicos + 'paciente', + 'administrador' + ] + + let lastError: AuthenticationError | Error | null = null + let loginAttempted = false + + for (const userType of userTypes) { + try { + console.log(`[LOGIN] Tentando login como ${userType}...`) + const loginSuccess = await login(credentials.email, credentials.password, userType) + + if (loginSuccess) { + loginAttempted = true + console.log('[LOGIN] Login bem-sucedido como', userType) + console.log('[LOGIN] User state:', user) + + // Aguardar um pouco para o state do usuário ser atualizado + await new Promise(resolve => setTimeout(resolve, 500)) + + // Obter o userType atualizado do localStorage (que foi salvo pela função login) + const storedUser = localStorage.getItem('auth_user') + if (storedUser) { + try { + const userData = JSON.parse(storedUser) + const redirectPath = getRoleRedirectPath(userData.userType) + console.log('[LOGIN] Redirecionando para:', redirectPath) + router.push(redirectPath) + } catch (parseErr) { + console.error('[LOGIN] Erro ao parsear user do localStorage:', parseErr) + router.push('/') + } + } else { + console.warn('[LOGIN] Usuário não encontrado no localStorage') + router.push('/') + } + return + } + } catch (err) { + lastError = err as AuthenticationError | Error + const errorMsg = err instanceof Error ? err.message : String(err) + console.log(`[LOGIN] Falha ao tentar como ${userType}:`, errorMsg) + continue + } + } + + // Se chegou aqui, nenhum tipo funcionou + console.error('[LOGIN] Nenhum tipo de usuário funcionou. Erro final:', lastError) - if (success) { - console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...') - - // Redirecionamento direto - solução que funcionou - window.location.href = '/profissional' + if (lastError instanceof AuthenticationError) { + const errorMsg = lastError.message || lastError.details?.error_code || '' + if (lastError.code === '400' || errorMsg.includes('invalid_credentials') || errorMsg.includes('Email or password')) { + setError('❌ Email ou senha incorretos. Verifique suas credenciais.') + } else { + setError(lastError.message || 'Erro ao fazer login. Tente novamente.') + } + } else if (lastError instanceof Error) { + setError(lastError.message || 'Erro desconhecido ao fazer login.') + } else { + setError('Falha ao fazer login. Credenciais inválidas ou conta não encontrada.') } } catch (err) { - console.error('[LOGIN-PROFISSIONAL] Erro no login:', err) - - if (err instanceof AuthenticationError) { - // Verificar se é erro de credenciais inválidas (pode ser email não confirmado) - if (err.code === '400' || err.details?.error_code === 'invalid_credentials') { - setError( - '⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' + - 'verifique sua caixa de entrada e clique no link de confirmação ' + - 'que foi enviado para ' + credentials.email - ) - } else { - setError(err.message) - } - } else { - setError('Erro inesperado. Tente novamente.') - } + console.error('[LOGIN] Erro no login:', err) } finally { setLoading(false) } @@ -61,7 +115,7 @@ export default function LoginPage() {

- Login Profissional de Saúde + Entrar

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() {
-
-
- {} -
- - -
+
{} 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 - - - -
{} @@ -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 - - - -
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} -

- ) 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/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 From eea59f50633f4cd2831f15a58ef670cb84df60ce Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Sat, 8 Nov 2025 00:23:51 -0300 Subject: [PATCH 2/3] =?UTF-8?q?style(calend=C3=A1rio)=20corrigir=20o=20mod?= =?UTF-8?q?al=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" + /> +
-
- -