Merge pull request 'backup/user' (#73) from backup/user into develop

Reviewed-on: RiseUP/riseup-squad20#73
This commit is contained in:
M-Gabrielly 2025-11-08 03:40:50 +00:00
commit 5047ab75dc
16 changed files with 832 additions and 690 deletions

View File

@ -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 (
<div className="min-h-screen flex items-center justify-center">
<p>Redirecionando para a página de login...</p>
</div>
)
}

View File

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
Login Administrador de Clínica
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Entre com suas credenciais para acessar o sistema administrativo
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Acesso Administrativo</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email
</label>
<Input
id="email"
type="email"
placeholder="Digite seu email"
value={credentials.email}
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Senha
</label>
<Input
id="password"
type="password"
placeholder="Digite sua senha"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
type="submit"
className="w-full cursor-pointer"
disabled={loading}
>
{loading ? 'Entrando...' : 'Entrar no Sistema Administrativo'}
</Button>
</form>
<div className="mt-4 text-center">
<Button variant="outline" asChild className="w-full hover:bg-primary! hover:text-white! hover:border-primary! transition-all duration-200">
<Link href="/">
Voltar ao Início
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
<div className="min-h-screen flex items-center justify-center">
<p>Redirecionando para a página de login...</p>
</div>
)
}

View File

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
Sou Paciente
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Acesse sua área pessoal e gerencie suas consultas
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email
</label>
<Input
id="email"
type="email"
placeholder="Digite seu email"
value={credentials.email}
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Senha
</label>
<Input
id="password"
type="password"
placeholder="Digite sua senha"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
type="submit"
className="w-full cursor-pointer"
disabled={loading}
>
{loading ? 'Entrando...' : 'Entrar na Minha Área'}
</Button>
</form>
<div className="mt-4 text-center">
<Button variant="outline" asChild className="w-full hover:bg-primary! hover:text-white! hover:border-primary! transition-all duration-200">
<Link href="/">
Voltar ao Início
</Link>
</Button>
</div>
{/* Auto-cadastro UI removed */}
</CardContent>
</Card>
</div>
<div className="min-h-screen flex items-center justify-center">
<p>Redirecionando...</p>
</div>
)
}

View File

@ -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 (
<div className="min-h-screen flex items-center justify-center">
<p>Redirecionando para a página de login...</p>
</div>
)
}

View File

@ -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'
]
if (success) {
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
let lastError: AuthenticationError | Error | null = null
let loginAttempted = false
// Redirecionamento direto - solução que funcionou
window.location.href = '/profissional'
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) {
console.error('[LOGIN-PROFISSIONAL] Erro no login:', 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
}
}
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
)
// Se chegou aqui, nenhum tipo funcionou
console.error('[LOGIN] Nenhum tipo de usuário funcionou. Erro final:', lastError)
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(err.message)
setError(lastError.message || 'Erro ao fazer login. Tente novamente.')
}
} else if (lastError instanceof Error) {
setError(lastError.message || 'Erro desconhecido ao fazer login.')
} else {
setError('Erro inesperado. Tente novamente.')
setError('Falha ao fazer login. Credenciais inválidas ou conta não encontrada.')
}
} catch (err) {
console.error('[LOGIN] Erro no login:', err)
} finally {
setLoading(false)
}
@ -61,7 +115,7 @@ export default function LoginPage() {
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
Login Profissional de Saúde
Entrar
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Entre com suas credenciais para acessar o sistema
@ -70,7 +124,7 @@ export default function LoginPage() {
<Card>
<CardHeader>
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
<CardTitle className="text-center">Login</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin} className="space-y-6">
@ -121,9 +175,8 @@ export default function LoginPage() {
</Button>
</form>
<div className="mt-4 text-center">
<Button variant="outline" asChild className="w-full hover:bg-primary! hover:text-white! hover:border-primary! transition-all duration-200">
<Button variant="ghost" asChild className="w-full">
<Link href="/">
Voltar ao Início
</Link>

View File

@ -60,6 +60,12 @@ export default function AgendamentoPage() {
const patientsById: Record<string, any> = {};
(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<string, any> = {};
(doctors || []).forEach((d: any) => { if (d && d.id) doctorsById[String(d.id)] = d; });
setAppointments(arr || []);
// --- LÓGICA DE TRANSFORMAÇÃO PARA O NOVO EVENTMANAGER ---
@ -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<string, string> = {
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<string, Set<string>>()
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 (
<div className="max-w-full sm:max-w-[520px] rounded-lg border border-slate-700 bg-gradient-to-b from-card/70 to-card/50 px-3 py-2 shadow-md flex items-center gap-4 text-sm overflow-x-auto whitespace-nowrap">
{/* Bloco grande com os três status principais sempre visíveis e responsivos */}
<div className="flex items-center gap-4 shrink-0">
<div className="flex items-center gap-2">
<span aria-hidden className="h-2 w-2 sm:h-3 sm:w-3 rounded-full bg-blue-500 ring-1 ring-white/6" />
<span className="text-foreground text-xs sm:text-sm font-medium">Solicitado</span>
</div>
<div className="flex items-center gap-2">
<span aria-hidden className="h-2 w-2 sm:h-3 sm:w-3 rounded-full bg-green-500 ring-1 ring-white/6" />
<span className="text-foreground text-xs sm:text-sm font-medium">Confirmado</span>
</div>
<div className="flex items-center gap-2">
<span aria-hidden className="h-2 w-2 sm:h-3 sm:w-3 rounded-full bg-red-500 ring-1 ring-white/6" />
<span className="text-foreground text-xs sm:text-sm font-medium">Cancelado</span>
</div>
</div>
{/* Itens extras detectados dinamicamente (menores) */}
{extras.length > 0 && (
<div className="flex items-center gap-3 ml-3 flex-wrap">
{extras.map(({ col, statuses }) => {
const statusList = statuses.map(statusDisplay).filter(Boolean).join(', ')
const cls = colorClassMap[col.toLowerCase()]
return (
<div key={col} className="flex items-center gap-2">
{cls ? (
<span aria-hidden className={`h-2 w-2 rounded-full ${cls} ring-1`} />
) : (
<span aria-hidden className="h-2 w-2 rounded-full ring-1" style={{ backgroundColor: hashToColor(col) }} />
)}
<span className="text-foreground text-xs">{statusList || col}</span>
</div>
)
})}
</div>
)}
</div>
)
}
// Envia atualização para a API e atualiza UI
const handleEventUpdate = async (id: string, partial: Partial<Event>) => {
try {
@ -157,59 +301,32 @@ export default function AgendamentoPage() {
return (
<div className="bg-background">
<div className="w-full">
<div className="w-full max-w-7xl mx-auto flex flex-col gap-6 sm:gap-10 p-4 sm:p-6">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2">
{/* Cabeçalho simplificado (sem 3D) */}
<div className="w-full max-w-full mx-0 flex flex-col gap-0 p-0 pl-4 sm:pl-6">
<div className="relative flex items-center justify-between gap-0 p-0 py-2 sm:py-0">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-foreground">Calendário</h1>
<p className="text-muted-foreground">
Navegue através do atalho: Calendário (C).
</p>
</div>
{/* REMOVIDO: botões de abas Calendário/3D */}
<h1 className="text-lg font-semibold text-foreground m-0 p-0">Calendário</h1>
<p className="text-muted-foreground m-0 p-0 text-xs">Navegue através do atalho: Calendário (C).</p>
</div>
{/* Legenda de status (aplica-se ao EventManager) */}
<div className="rounded-md border bg-card/60 p-2 sm:p-3 -mt-2 sm:-mt-4 overflow-x-auto">
<div className="flex flex-nowrap items-center gap-4 sm:gap-6 text-xs sm:text-sm whitespace-nowrap">
<div className="flex items-center gap-2">
<span aria-hidden className="h-3 w-3 rounded-full bg-blue-500 ring-2 ring-blue-500/30" />
<span className="text-foreground">Solicitado</span>
</div>
<div className="flex items-center gap-2">
<span aria-hidden className="h-3 w-3 rounded-full bg-green-500 ring-2 ring-green-500/30" />
<span className="text-foreground">Confirmado</span>
</div>
{/* Novo: Cancelado (vermelho) */}
<div className="flex items-center gap-2">
<span aria-hidden className="h-3 w-3 rounded-full bg-red-500 ring-2 ring-red-500/30" />
<span className="text-foreground">Cancelado</span>
</div>
{/* legenda dinâmica: mostra as cores presentes nos agendamentos do dia atual */}
<div className="sm:absolute sm:top-2 sm:right-2 mt-2 sm:mt-0 z-40">
<DynamicLegend events={managerEvents} />
</div>
</div>
{/* Apenas o EventManager */}
<div className="flex w-full">
<div className="w-full">
<div className="w-full m-0 p-0">
{managerLoading ? (
<div className="flex items-center justify-center w-full min-h-[60vh] sm:min-h-[70vh]">
<div className="text-sm text-muted-foreground">Conectando ao calendário carregando agendamentos...</div>
<div className="flex items-center justify-center w-full min-h-[70vh] m-0 p-0">
<div className="text-xs text-muted-foreground">Conectando ao calendário carregando agendamentos...</div>
</div>
) : (
<div className="w-full min-h-[60vh] sm:min-h-[70vh]">
<EventManager
events={managerEvents}
className="compact-event-manager"
onEventUpdate={handleEventUpdate}
/>
<div className="w-full min-h-[80vh] m-0 p-0">
<EventManager events={managerEvents} className="compact-event-manager" onEventUpdate={handleEventUpdate} />
</div>
)}
</div>
</div>
</div>
{/* REMOVIDO: PatientRegistrationForm (era acionado pelo 3D) */}
</div>
</div>
);
}

View File

@ -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<any>(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) {
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={form.data_nascimento}
onSelect={(date) => setField("data_nascimento", date || null)}
selected={form.data_nascimento ?? undefined}
onSelect={(date) => setField("data_nascimento", date ?? null)}
initialFocus
/>
</PopoverContent>
@ -1061,28 +1093,37 @@ async function handleSubmit(ev: React.FormEvent) {
<>
<div className="space-y-6">{content}</div>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentialsDialog}
onOpenChange={(open) => {
console.log('🔄 CredentialsDialog (inline) onOpenChange chamado com:', open);
setShowCredentialsDialog(open);
if (!open) {
// Quando o dialog de credenciais fecha, fecha o formulário também
// Dialog foi fechado - limpar estados e fechar formulário
console.log('✅ Dialog fechado - limpando formulário...');
setCredentials(null);
if (inline) {
onClose?.();
} else {
onOpenChange?.(false);
// Chamar onSaved se houver médico salvo
if (savedDoctorRef.current) {
onSaved?.(savedDoctorRef.current);
savedDoctorRef.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}
email={credentials?.email || ''}
password={credentials?.password || ''}
userName={credentials?.userName || ''}
userType={credentials?.userType || 'médico'}
/>
)}
</>
);
}
@ -1100,23 +1141,36 @@ async function handleSubmit(ev: React.FormEvent) {
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentialsDialog}
onOpenChange={(open) => {
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;
}
// 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}
email={credentials?.email || ''}
password={credentials?.password || ''}
userName={credentials?.userName || ''}
userType={credentials?.userType || 'médico'}
/>
)}
</>
);
}

View File

@ -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";
@ -139,6 +139,9 @@ export function PatientRegistrationForm({
userType: 'médico' | 'paciente';
} | null>(null);
// Ref para guardar o paciente salvo para chamar onSaved quando o dialog fechar
const savedPatientRef = useRef<any>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
useEffect(() => {
@ -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 (<><div className="space-y-6">{content}</div>{credentials && (<CredentialsDialog open={showCredentialsDialog} onOpenChange={(open) => { 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 (
<>
<div className="space-y-6">{content}</div>
<CredentialsDialog
open={showCredentialsDialog}
onOpenChange={(open) => {
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;
}
return (<><Dialog open={open} onOpenChange={onOpenChange}><DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto"><DialogHeader><DialogTitle className="flex items-center gap-2"><User className="h-5 w-5" /> {title}</DialogTitle></DialogHeader>{content}</DialogContent></Dialog>{credentials && (<CredentialsDialog open={showCredentialsDialog} onOpenChange={(open) => { setShowCredentialsDialog(open); if (!open) { setCredentials(null); onOpenChange?.(false); } }} 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 || 'paciente'}
/>
</>
);
}
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
<CredentialsDialog
open={showCredentialsDialog}
onOpenChange={(open) => {
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'}
/>
</>
);
}

View File

@ -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 (
<div className={cn("flex flex-col gap-4", className)}>
{/* Header */}
@ -504,7 +581,7 @@ export function EventManager({
{/* Event Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogContent className="w-full max-w-full sm:max-w-2xl md:max-w-3xl max-h-[90vh] overflow-y-auto p-4 sm:p-6">
<DialogHeader>
<DialogTitle>{isCreating ? "Criar Evento" : "Detalhes do Agendamento"}</DialogTitle>
<DialogDescription>
@ -512,17 +589,16 @@ export function EventManager({
</DialogDescription>
</DialogHeader>
{/* Dialog content: form when creating; read-only view when viewing */}
{isCreating ? (
<>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Título</Label>
<Input
id="title"
value={isCreating ? (newEvent.title ?? "") : (selectedEvent?.title ?? "")}
onChange={(e) =>
isCreating
? setNewEvent((prev) => ({ ...prev, title: e.target.value }))
: setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null))
}
value={newEvent.title ?? ""}
onChange={(e) => setNewEvent((prev) => ({ ...prev, title: e.target.value }))}
placeholder="Título do evento"
/>
</div>
@ -531,15 +607,8 @@ export function EventManager({
<Label htmlFor="description">Descrição</Label>
<Textarea
id="description"
value={isCreating ? (newEvent.description ?? "") : (selectedEvent?.description ?? "")}
onChange={(e) =>
isCreating
? setNewEvent((prev) => ({
...prev,
description: e.target.value,
}))
: setSelectedEvent((prev) => (prev ? { ...prev, description: e.target.value } : null))
}
value={newEvent.description ?? ""}
onChange={(e) => setNewEvent((prev) => ({ ...prev, description: e.target.value }))}
placeholder="Descrição do evento"
rows={3}
/>
@ -552,26 +621,13 @@ export function EventManager({
id="startTime"
type="datetime-local"
value={
isCreating
? newEvent.startTime
newEvent.startTime
? new Date(newEvent.startTime.getTime() - newEvent.startTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16)
: ""
: selectedEvent
? new Date(
selectedEvent.startTime.getTime() - selectedEvent.startTime.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 16)
: ""
}
onChange={(e) => {
const date = new Date(e.target.value)
isCreating
? setNewEvent((prev) => ({ ...prev, startTime: date }))
: setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null))
}}
onChange={(e) => setNewEvent((prev) => ({ ...prev, startTime: new Date(e.target.value) }))}
/>
</div>
@ -581,39 +637,19 @@ export function EventManager({
id="endTime"
type="datetime-local"
value={
isCreating
? newEvent.endTime
newEvent.endTime
? new Date(newEvent.endTime.getTime() - newEvent.endTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16)
: ""
: selectedEvent
? new Date(selectedEvent.endTime.getTime() - selectedEvent.endTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16)
: ""
}
onChange={(e) => {
const date = new Date(e.target.value)
isCreating
? setNewEvent((prev) => ({ ...prev, endTime: date }))
: setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null))
}}
onChange={(e) => setNewEvent((prev) => ({ ...prev, endTime: new Date(e.target.value) }))}
/>
</div>
</div>
{/* Campos de Categoria/Cor removidos */}
{/* Campo de Tags removido */}
</div>
<DialogFooter>
{!isCreating && (
<Button variant="destructive" onClick={() => selectedEvent && handleDeleteEvent(selectedEvent.id)}>
Deletar
</Button>
)}
<Button
variant="outline"
onClick={() => {
@ -624,10 +660,108 @@ export function EventManager({
>
Cancelar
</Button>
<Button onClick={isCreating ? handleCreateEvent : handleUpdateEvent}>
{isCreating ? "Criar" : "Salvar"}
<Button onClick={handleCreateEvent}>Criar</Button>
</DialogFooter>
</>
) : (
<>
{/* Read-only compact view: title + stacked details + descrição abaixo */}
<div className="space-y-4">
<div>
<h3 className="text-lg sm:text-xl font-semibold">{selectedEvent?.title || "—"}</h3>
</div>
<div className="p-3 sm:p-4 rounded-md border bg-card/5 text-sm text-muted-foreground">
<div className="grid grid-cols-1 gap-3">
<div>
<div className="text-[12px] text-muted-foreground">Profissional</div>
<div className="mt-1 text-sm font-medium break-words">{selectedEvent?.professionalName || "—"}</div>
</div>
<div>
<div className="text-[12px] text-muted-foreground">Paciente</div>
<div className="mt-1 text-sm font-medium break-words">{selectedEvent?.patientName || "—"}</div>
</div>
<div>
<div className="text-[12px] text-muted-foreground">Tipo</div>
<div className="mt-1 text-sm font-medium break-words">{selectedEvent?.appointmentType || "—"}</div>
</div>
<div>
<div className="text-[12px] text-muted-foreground">Status</div>
<div className="mt-1 text-sm font-medium break-words">{selectedEvent?.status || "—"}</div>
</div>
<div>
<div className="text-[12px] text-muted-foreground">Data</div>
<div className="mt-1 text-sm font-medium break-words">{(() => {
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)
})()}</div>
</div>
{selectedEvent?.completedAt && (
<div>
<div className="text-[12px] text-muted-foreground">Concluído em</div>
<div className="mt-1 text-sm font-medium break-words">{(() => {
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 "—" }
})()}</div>
</div>
)}
{selectedEvent?.cancelledAt && (
<div>
<div className="text-[12px] text-muted-foreground">Cancelado em</div>
<div className="mt-1 text-sm font-medium break-words">{(() => {
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 "—" }
})()}</div>
<div className="text-[12px] text-muted-foreground mt-2">Motivo do cancelamento</div>
<div className="mt-1 text-sm font-medium break-words">{selectedEvent?.cancellationReason || "—"}</div>
</div>
)}
</div>
</div>
<div>
<Label>Observações</Label>
<div className="min-h-[80px] sm:min-h-[120px] p-3 rounded-md border bg-muted/5 text-sm text-muted-foreground whitespace-pre-wrap">
{sanitizeDescription(selectedEvent?.description) ?? "—"}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsDialogOpen(false)
setIsCreating(false)
setSelectedEvent(null)
}}
>
Fechar
</Button>
</DialogFooter>
</div>
</>
)}
</DialogContent>
</Dialog>
</div>
@ -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 (
<Card className="overflow-auto">
<div className="grid grid-cols-8 border-b">
<div className="border-r p-2 text-center text-xs font-medium sm:text-sm">Hora</div>
<div className="grid grid-cols-7 border-b">
{weekDays.map((day) => (
<div
key={day.toISOString()}
className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm"
>
<div className="hidden sm:block">{day.toLocaleDateString(LOCALE, { weekday: "short", timeZone: TIMEZONE })}</div>
<div className="sm:hidden">{day.toLocaleDateString(LOCALE, { weekday: "narrow", timeZone: TIMEZONE })}</div>
<div className="text-[10px] text-muted-foreground sm:text-xs">
{day.toLocaleDateString(LOCALE, { month: "short", day: "numeric", timeZone: TIMEZONE })}
</div>
<div key={day.toISOString()} className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm">
<span className="hidden sm:inline">{day.toLocaleDateString(LOCALE, { weekday: "short", timeZone: TIMEZONE })}</span>
<span className="sm:hidden">{day.toLocaleDateString(LOCALE, { weekday: "narrow", timeZone: TIMEZONE })}</span>
</div>
))}
</div>
<div className="grid grid-cols-8">
{hours.map((hour) => (
<React.Fragment key={`hour-${hour}`}>
<div
key={`time-${hour}`}
className="border-b border-r p-1 text-[10px] text-muted-foreground sm:p-2 sm:text-xs"
>
{hour.toString().padStart(2, "0")}:00
</div>
{weekDays.map((day) => {
const dayEvents = getEventsForDayAndHour(day, hour)
<div className="grid grid-cols-7">
{weekDays.map((day, idx) => {
const dayEvents = getEventsForDay(day)
return (
<div
key={`${day.toISOString()}-${hour}`}
className="min-h-12 border-b border-r p-0.5 transition-colors hover:bg-accent/50 last:border-r-0 sm:min-h-16 sm:p-1"
onDragOver={(e) => e.preventDefault()}
onDrop={() => onDrop(day, hour)}
>
<div className="space-y-1">
{dayEvents.map((event) => (
<div key={idx} className="min-h-40 border-r p-2 last:border-r-0">
<div className="space-y-2">
{dayEvents.map((ev) => (
<div key={ev.id} className="mb-2">
<EventCard
key={event.id}
event={event}
event={ev}
onEventClick={onEventClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
getColorClasses={getColorClasses}
variant="default"
variant="compact"
/>
</div>
))}
</div>
</div>
)
})}
</React.Fragment>
))}
</div>
</Card>
)
}
// 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 (
<Card className="overflow-auto">
@ -1124,27 +1189,14 @@ function DayView({
{hours.map((hour) => {
const hourEvents = getEventsForHour(hour)
return (
<div
key={hour}
className="flex border-b last:border-b-0"
onDragOver={(e) => e.preventDefault()}
onDrop={() => onDrop(currentDate, hour)}
>
<div key={hour} className="flex border-b last:border-b-0" onDragOver={(e) => e.preventDefault()} onDrop={() => onDrop(currentDate, hour)}>
<div className="w-14 flex-shrink-0 border-r p-2 text-xs text-muted-foreground sm:w-20 sm:p-3 sm:text-sm">
{hour.toString().padStart(2, "0")}:00
</div>
<div className="min-h-16 flex-1 p-1 transition-colors hover:bg-accent/50 sm:min-h-20 sm:p-2">
<div className="space-y-2">
{hourEvents.map((event) => (
<EventCard
key={event.id}
event={event}
onEventClick={onEventClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
getColorClasses={getColorClasses}
variant="detailed"
/>
<EventCard key={event.id} event={event} onEventClick={onEventClick} onDragStart={onDragStart} onDragEnd={onDragEnd} getColorClasses={getColorClasses} variant="detailed" />
))}
</div>
</div>

View File

@ -23,24 +23,7 @@ export function HeroSection() {
</div>
</div>
{}
<div className="flex flex-col sm:flex-row gap-4">
<Button
size="lg"
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent"
asChild
>
<Link href="/login-paciente">Portal do Paciente</Link>
</Button>
<Button
size="lg"
variant="outline"
className="text-primary border-primary bg-transparent cursor-pointer 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
>
<Link href="/login">Sou Profissional de Saúde</Link>
</Button>
</div>
</div>
{}

View File

@ -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
>
<Link href="/login-paciente">Sou Paciente</Link>
<Link href="/login">Entrar</Link>
</Button>
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
<Link href="/login">Sou Profissional de Saúde</Link>
</Button>
<Link href="/login-admin">
<Button
variant="outline"
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 cursor-pointer"
>
Sou Administrador de uma Clínica
</Button>
</Link>
</div>
{}
@ -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
>
<Link href="/login-paciente">Sou Paciente</Link>
<Link href="/login">Entrar</Link>
</Button>
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
<Link href="/login">Sou Profissional de Saúde</Link>
</Button>
<Link href="/login-admin">
<Button
variant="outline"
className="text-primary border-primary bg-transparent w-full 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 cursor-pointer"
>
Sou Administrador de uma Clínica
</Button>
</Link>
</div>
</nav>
</div>

View File

@ -126,17 +126,6 @@ export default function ProtectedRoute({
<p className="text-gray-600 mb-4">
Você não tem permissão para acessar esta página.
</p>
<p className="text-sm text-gray-500 mb-6">
Tipo de acesso necessário: {requiredUserType.join(' ou ')}
<br />
Seu tipo de acesso: {user.userType}
</p>
<button
onClick={() => 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
</button>
</div>
</div>
)

View File

@ -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
)

View File

@ -2139,25 +2139,25 @@ export async function criarMedico(input: MedicoInput): Promise<Medico> {
// 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);

View File

@ -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'
}
}

View File

@ -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