Merge pull request 'backup/user' (#73) from backup/user into develop
Reviewed-on: #73
This commit is contained in:
commit
5047ab75dc
17
susconecta/app/(auth)/login-admin/page-new.tsx
Normal file
17
susconecta/app/(auth)/login-admin/page-new.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,128 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
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() {
|
export default function LoginAdminRedirect() {
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const router = useRouter()
|
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 (
|
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="min-h-screen flex items-center justify-center">
|
||||||
<div className="max-w-md w-full space-y-8">
|
<p>Redirecionando para a página de login...</p>
|
||||||
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1,138 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
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() {
|
export default function LoginPacienteRedirect() {
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { login } = useAuth()
|
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
useEffect(() => {
|
||||||
e.preventDefault()
|
router.replace('/login')
|
||||||
setLoading(true)
|
}, [router])
|
||||||
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)
|
|
||||||
|
|
||||||
return (
|
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="min-h-screen flex items-center justify-center">
|
||||||
<div className="max-w-md w-full space-y-8">
|
<p>Redirecionando...</p>
|
||||||
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
17
susconecta/app/(auth)/login-profissional/page.tsx
Normal file
17
susconecta/app/(auth)/login-profissional/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -12,10 +12,23 @@ import { AuthenticationError } from '@/lib/auth'
|
|||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const router = useRouter()
|
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) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -23,32 +36,73 @@ export default function LoginPage() {
|
|||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Tentar fazer login usando o contexto com tipo profissional
|
// Tentar fazer login com cada tipo de usuário até conseguir
|
||||||
const success = await login(credentials.email, credentials.password, 'profissional')
|
// 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) {
|
let lastError: AuthenticationError | Error | null = null
|
||||||
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
|
let loginAttempted = false
|
||||||
|
|
||||||
// Redirecionamento direto - solução que funcionou
|
for (const userType of userTypes) {
|
||||||
window.location.href = '/profissional'
|
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) {
|
} 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) {
|
// Se chegou aqui, nenhum tipo funcionou
|
||||||
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
|
console.error('[LOGIN] Nenhum tipo de usuário funcionou. Erro final:', lastError)
|
||||||
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
|
|
||||||
setError(
|
if (lastError instanceof AuthenticationError) {
|
||||||
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
|
const errorMsg = lastError.message || lastError.details?.error_code || ''
|
||||||
'verifique sua caixa de entrada e clique no link de confirmação ' +
|
if (lastError.code === '400' || errorMsg.includes('invalid_credentials') || errorMsg.includes('Email or password')) {
|
||||||
'que foi enviado para ' + credentials.email
|
setError('❌ Email ou senha incorretos. Verifique suas credenciais.')
|
||||||
)
|
|
||||||
} else {
|
} 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 {
|
} 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 {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@ -61,7 +115,7 @@ export default function LoginPage() {
|
|||||||
<div className="max-w-md w-full space-y-8">
|
<div className="max-w-md w-full space-y-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
|
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
|
||||||
Login Profissional de Saúde
|
Entrar
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
Entre com suas credenciais para acessar o sistema
|
Entre com suas credenciais para acessar o sistema
|
||||||
@ -70,7 +124,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
|
<CardTitle className="text-center">Login</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleLogin} className="space-y-6">
|
<form onSubmit={handleLogin} className="space-y-6">
|
||||||
@ -121,9 +175,8 @@ export default function LoginPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
<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="/">
|
<Link href="/">
|
||||||
Voltar ao Início
|
Voltar ao Início
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -60,6 +60,12 @@ export default function AgendamentoPage() {
|
|||||||
const patientsById: Record<string, any> = {};
|
const patientsById: Record<string, any> = {};
|
||||||
(patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });
|
(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 || []);
|
setAppointments(arr || []);
|
||||||
|
|
||||||
// --- LÓGICA DE TRANSFORMAÇÃO PARA O NOVO EVENTMANAGER ---
|
// --- 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 === "canceled" || status === "cancelado" || status === "cancelled") color = "red";
|
||||||
else if (status === "requested" || status === "solicitado") color = "blue";
|
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 {
|
return {
|
||||||
id: obj.id || uuidv4(),
|
id: obj.id || uuidv4(),
|
||||||
title,
|
title,
|
||||||
@ -87,6 +100,15 @@ export default function AgendamentoPage() {
|
|||||||
startTime: start,
|
startTime: start,
|
||||||
endTime: end,
|
endTime: end,
|
||||||
color,
|
color,
|
||||||
|
// Campos adicionais para visualização detalhada
|
||||||
|
patientName: patient,
|
||||||
|
professionalName: professional,
|
||||||
|
appointmentType,
|
||||||
|
status: obj.status || null,
|
||||||
|
insuranceProvider: insurance,
|
||||||
|
completedAt,
|
||||||
|
cancelledAt,
|
||||||
|
cancellationReason,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setManagerEvents(newManagerEvents);
|
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
|
// Envia atualização para a API e atualiza UI
|
||||||
const handleEventUpdate = async (id: string, partial: Partial<Event>) => {
|
const handleEventUpdate = async (id: string, partial: Partial<Event>) => {
|
||||||
try {
|
try {
|
||||||
@ -157,59 +301,32 @@ export default function AgendamentoPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-background">
|
<div className="bg-background">
|
||||||
<div className="w-full">
|
<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="w-full max-w-full mx-0 flex flex-col gap-0 p-0 pl-4 sm:pl-6">
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2">
|
<div className="relative flex items-center justify-between gap-0 p-0 py-2 sm:py-0">
|
||||||
{/* Cabeçalho simplificado (sem 3D) */}
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-foreground">Calendário</h1>
|
<h1 className="text-lg font-semibold text-foreground m-0 p-0">Calendário</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground m-0 p-0 text-xs">Navegue através do atalho: Calendário (C).</p>
|
||||||
Navegue através do atalho: Calendário (C).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/* REMOVIDO: botões de abas Calendário/3D */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legenda de status (aplica-se ao EventManager) */}
|
{/* legenda dinâmica: mostra as cores presentes nos agendamentos do dia atual */}
|
||||||
<div className="rounded-md border bg-card/60 p-2 sm:p-3 -mt-2 sm:-mt-4 overflow-x-auto">
|
<div className="sm:absolute sm:top-2 sm:right-2 mt-2 sm:mt-0 z-40">
|
||||||
<div className="flex flex-nowrap items-center gap-4 sm:gap-6 text-xs sm:text-sm whitespace-nowrap">
|
<DynamicLegend events={managerEvents} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Apenas o EventManager */}
|
<div className="w-full m-0 p-0">
|
||||||
<div className="flex w-full">
|
|
||||||
<div className="w-full">
|
|
||||||
{managerLoading ? (
|
{managerLoading ? (
|
||||||
<div className="flex items-center justify-center w-full min-h-[60vh] sm:min-h-[70vh]">
|
<div className="flex items-center justify-center w-full min-h-[70vh] m-0 p-0">
|
||||||
<div className="text-sm text-muted-foreground">Conectando ao calendário — carregando agendamentos...</div>
|
<div className="text-xs text-muted-foreground">Conectando ao calendário — carregando agendamentos...</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full min-h-[60vh] sm:min-h-[70vh]">
|
<div className="w-full min-h-[80vh] m-0 p-0">
|
||||||
<EventManager
|
<EventManager events={managerEvents} className="compact-event-manager" onEventUpdate={handleEventUpdate} />
|
||||||
events={managerEvents}
|
|
||||||
className="compact-event-manager"
|
|
||||||
onEventUpdate={handleEventUpdate}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* REMOVIDO: PatientRegistrationForm (era acionado pelo 3D) */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState, useRef } from "react";
|
||||||
import { parse, parseISO, format } from 'date-fns';
|
import { parse, parseISO, format } from 'date-fns';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@ -167,6 +167,7 @@ export function DoctorRegistrationForm({
|
|||||||
userName: string;
|
userName: string;
|
||||||
userType: 'médico' | 'paciente';
|
userType: 'médico' | 'paciente';
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const savedDoctorRef = useRef<any>(null);
|
||||||
|
|
||||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
|
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
|
// 1. Cria o perfil do médico na tabela doctors
|
||||||
let savedDoctorProfile: any = await criarMedico(medicoPayload);
|
let savedDoctorProfile: any = await criarMedico(medicoPayload);
|
||||||
console.log("✅ Perfil do médico criado:", savedDoctorProfile);
|
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.
|
// 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,
|
// 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; });
|
const patched = await atualizarMedico(String(createdDoctorId), medicoPayload).catch((e) => { console.warn('[DoctorForm] fallback PATCH failed:', e); return null; });
|
||||||
if (patched) {
|
if (patched) {
|
||||||
console.debug('[DoctorForm] fallback PATCH result:', 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) {
|
} catch (e) {
|
||||||
@ -547,6 +555,7 @@ async function handleSubmit(ev: React.FormEvent) {
|
|||||||
// { doctor, doctor_id, email, password, user_id } or similar shapes.
|
// { doctor, doctor_id, email, password, user_id } or similar shapes.
|
||||||
const result = savedDoctorProfile as any;
|
const result = savedDoctorProfile as any;
|
||||||
console.log('✅ Resultado de criarMedico:', result);
|
console.log('✅ Resultado de criarMedico:', result);
|
||||||
|
console.log('🔑 Senha no resultado final:', result?.password);
|
||||||
|
|
||||||
// Determine the doctor id if available
|
// Determine the doctor id if available
|
||||||
let createdDoctorId: string | null = null;
|
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 the function returned credentials, show them in the credentials dialog
|
||||||
if (result && (result.password || result.email || result.user)) {
|
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,
|
email: result.email || form.email,
|
||||||
password: result.password || "",
|
password: result.password || senhaGerada || "",
|
||||||
userName: form.full_name,
|
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);
|
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
|
// 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">
|
<PopoverContent className="w-auto p-0">
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={form.data_nascimento}
|
selected={form.data_nascimento ?? undefined}
|
||||||
onSelect={(date) => setField("data_nascimento", date || null)}
|
onSelect={(date) => setField("data_nascimento", date ?? null)}
|
||||||
initialFocus
|
initialFocus
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@ -1061,28 +1093,37 @@ async function handleSubmit(ev: React.FormEvent) {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-6">{content}</div>
|
<div className="space-y-6">{content}</div>
|
||||||
|
|
||||||
{/* Dialog de credenciais */}
|
|
||||||
{credentials && (
|
|
||||||
<CredentialsDialog
|
<CredentialsDialog
|
||||||
open={showCredentialsDialog}
|
open={showCredentialsDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
console.log('🔄 CredentialsDialog (inline) onOpenChange chamado com:', open);
|
||||||
setShowCredentialsDialog(open);
|
setShowCredentialsDialog(open);
|
||||||
if (!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);
|
setCredentials(null);
|
||||||
if (inline) {
|
|
||||||
onClose?.();
|
// Chamar onSaved se houver médico salvo
|
||||||
} else {
|
if (savedDoctorRef.current) {
|
||||||
onOpenChange?.(false);
|
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}
|
email={credentials?.email || ''}
|
||||||
password={credentials.password}
|
password={credentials?.password || ''}
|
||||||
userName={credentials.userName}
|
userName={credentials?.userName || ''}
|
||||||
userType={credentials.userType}
|
userType={credentials?.userType || 'médico'}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1100,23 +1141,36 @@ async function handleSubmit(ev: React.FormEvent) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Dialog de credenciais */}
|
|
||||||
{credentials && (
|
|
||||||
<CredentialsDialog
|
<CredentialsDialog
|
||||||
open={showCredentialsDialog}
|
open={showCredentialsDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
console.log('🔄 CredentialsDialog (dialog) onOpenChange chamado com:', open);
|
||||||
setShowCredentialsDialog(open);
|
setShowCredentialsDialog(open);
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
// Dialog foi fechado - limpar estados e fechar formulário
|
||||||
|
console.log('✅ Dialog fechado - limpando formulário...');
|
||||||
setCredentials(null);
|
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);
|
onOpenChange?.(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
email={credentials.email}
|
email={credentials?.email || ''}
|
||||||
password={credentials.password}
|
password={credentials?.password || ''}
|
||||||
userName={credentials.userName}
|
userName={credentials?.userName || ''}
|
||||||
userType={credentials.userType}
|
userType={credentials?.userType || 'médico'}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState, useRef } from "react";
|
||||||
import { format, parseISO, parse } from "date-fns";
|
import { format, parseISO, parse } from "date-fns";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@ -139,6 +139,9 @@ export function PatientRegistrationForm({
|
|||||||
userType: 'médico' | 'paciente';
|
userType: 'médico' | 'paciente';
|
||||||
} | null>(null);
|
} | 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]);
|
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
||||||
|
|
||||||
useEffect(() => {
|
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;
|
setErrors((e) => ({ ...e, telefone: 'Telefone é obrigatório quando email é informado (fluxo de criação único).' })); setSubmitting(false); return;
|
||||||
}
|
}
|
||||||
let savedPatientProfile: any = await criarPaciente(patientPayload);
|
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
|
// Fallback: some backend create flows (create-user-with-password) do not
|
||||||
// persist optional patient fields like sex/cep/birth_date. The edit flow
|
// 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; });
|
const patched = await atualizarPaciente(String(pacienteId), patientPayload).catch((e) => { console.warn('[PatientForm] fallback PATCH falhou:', e); return null; });
|
||||||
if (patched) {
|
if (patched) {
|
||||||
console.debug('[PatientForm] fallback PATCH result:', patched);
|
console.debug('[PatientForm] fallback PATCH result:', patched);
|
||||||
savedPatientProfile = patched;
|
// Preserva a senha ao fazer merge do patch
|
||||||
|
savedPatientProfile = { ...patched, password: senhaGerada };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[PatientForm] erro ao tentar fallback PATCH:', e);
|
console.warn('[PatientForm] erro ao tentar fallback PATCH:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybePassword = (savedPatientProfile as any)?.password || (savedPatientProfile as any)?.generated_password;
|
// Usar a senha que foi guardada ANTES do PATCH
|
||||||
if (maybePassword) {
|
const emailToDisplay = savedPatientProfile?.email || form.email;
|
||||||
setCredentials({ email: (savedPatientProfile as any).email || form.email, password: String(maybePassword), userName: form.nome, userType: 'paciente' });
|
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);
|
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) {
|
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.'); }
|
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); }
|
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 }); }
|
} 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); }
|
finally { setSubmitting(false); }
|
||||||
@ -519,8 +563,86 @@ export function PatientRegistrationForm({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (inline) {
|
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'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useState, useCallback, useMemo, useEffect } from "react"
|
import React, { useState, useCallback, useMemo, useEffect } from "react"
|
||||||
|
import { buscarAgendamentoPorId, buscarPacientesPorIds, buscarMedicosPorIds } from "@/lib/api"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@ -29,6 +30,15 @@ export interface Event {
|
|||||||
category?: string
|
category?: string
|
||||||
attendees?: string[]
|
attendees?: string[]
|
||||||
tags?: 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 {
|
export interface EventManagerProps {
|
||||||
@ -230,6 +240,73 @@ export function EventManager({
|
|||||||
} catch {}
|
} 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 (
|
return (
|
||||||
<div className={cn("flex flex-col gap-4", className)}>
|
<div className={cn("flex flex-col gap-4", className)}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -504,7 +581,7 @@ export function EventManager({
|
|||||||
|
|
||||||
{/* Event Dialog */}
|
{/* Event Dialog */}
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<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>
|
<DialogHeader>
|
||||||
<DialogTitle>{isCreating ? "Criar Evento" : "Detalhes do Agendamento"}</DialogTitle>
|
<DialogTitle>{isCreating ? "Criar Evento" : "Detalhes do Agendamento"}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@ -512,17 +589,16 @@ export function EventManager({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Dialog content: form when creating; read-only view when viewing */}
|
||||||
|
{isCreating ? (
|
||||||
|
<>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="title">Título</Label>
|
<Label htmlFor="title">Título</Label>
|
||||||
<Input
|
<Input
|
||||||
id="title"
|
id="title"
|
||||||
value={isCreating ? (newEvent.title ?? "") : (selectedEvent?.title ?? "")}
|
value={newEvent.title ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => setNewEvent((prev) => ({ ...prev, title: e.target.value }))}
|
||||||
isCreating
|
|
||||||
? setNewEvent((prev) => ({ ...prev, title: e.target.value }))
|
|
||||||
: setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null))
|
|
||||||
}
|
|
||||||
placeholder="Título do evento"
|
placeholder="Título do evento"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -531,15 +607,8 @@ export function EventManager({
|
|||||||
<Label htmlFor="description">Descrição</Label>
|
<Label htmlFor="description">Descrição</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={isCreating ? (newEvent.description ?? "") : (selectedEvent?.description ?? "")}
|
value={newEvent.description ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => setNewEvent((prev) => ({ ...prev, description: e.target.value }))}
|
||||||
isCreating
|
|
||||||
? setNewEvent((prev) => ({
|
|
||||||
...prev,
|
|
||||||
description: e.target.value,
|
|
||||||
}))
|
|
||||||
: setSelectedEvent((prev) => (prev ? { ...prev, description: e.target.value } : null))
|
|
||||||
}
|
|
||||||
placeholder="Descrição do evento"
|
placeholder="Descrição do evento"
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
@ -552,26 +621,13 @@ export function EventManager({
|
|||||||
id="startTime"
|
id="startTime"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={
|
value={
|
||||||
isCreating
|
newEvent.startTime
|
||||||
? newEvent.startTime
|
|
||||||
? new Date(newEvent.startTime.getTime() - newEvent.startTime.getTimezoneOffset() * 60000)
|
? new Date(newEvent.startTime.getTime() - newEvent.startTime.getTimezoneOffset() * 60000)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, 16)
|
.slice(0, 16)
|
||||||
: ""
|
: ""
|
||||||
: selectedEvent
|
|
||||||
? new Date(
|
|
||||||
selectedEvent.startTime.getTime() - selectedEvent.startTime.getTimezoneOffset() * 60000,
|
|
||||||
)
|
|
||||||
.toISOString()
|
|
||||||
.slice(0, 16)
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => setNewEvent((prev) => ({ ...prev, startTime: new Date(e.target.value) }))}
|
||||||
const date = new Date(e.target.value)
|
|
||||||
isCreating
|
|
||||||
? setNewEvent((prev) => ({ ...prev, startTime: date }))
|
|
||||||
: setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null))
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -581,39 +637,19 @@ export function EventManager({
|
|||||||
id="endTime"
|
id="endTime"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={
|
value={
|
||||||
isCreating
|
newEvent.endTime
|
||||||
? newEvent.endTime
|
|
||||||
? new Date(newEvent.endTime.getTime() - newEvent.endTime.getTimezoneOffset() * 60000)
|
? new Date(newEvent.endTime.getTime() - newEvent.endTime.getTimezoneOffset() * 60000)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, 16)
|
.slice(0, 16)
|
||||||
: ""
|
: ""
|
||||||
: selectedEvent
|
|
||||||
? new Date(selectedEvent.endTime.getTime() - selectedEvent.endTime.getTimezoneOffset() * 60000)
|
|
||||||
.toISOString()
|
|
||||||
.slice(0, 16)
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => setNewEvent((prev) => ({ ...prev, endTime: new Date(e.target.value) }))}
|
||||||
const date = new Date(e.target.value)
|
|
||||||
isCreating
|
|
||||||
? setNewEvent((prev) => ({ ...prev, endTime: date }))
|
|
||||||
: setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null))
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Campos de Categoria/Cor removidos */}
|
|
||||||
|
|
||||||
{/* Campo de Tags removido */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
{!isCreating && (
|
|
||||||
<Button variant="destructive" onClick={() => selectedEvent && handleDeleteEvent(selectedEvent.id)}>
|
|
||||||
Deletar
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -624,10 +660,108 @@ export function EventManager({
|
|||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={isCreating ? handleCreateEvent : handleUpdateEvent}>
|
<Button onClick={handleCreateEvent}>Criar</Button>
|
||||||
{isCreating ? "Criar" : "Salvar"}
|
</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>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
@ -943,7 +1077,7 @@ function MonthView({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Week View Component
|
// Week View Component (simplified and stable)
|
||||||
function WeekView({
|
function WeekView({
|
||||||
currentDate,
|
currentDate,
|
||||||
events,
|
events,
|
||||||
@ -958,7 +1092,7 @@ function WeekView({
|
|||||||
onEventClick: (event: Event) => void
|
onEventClick: (event: Event) => void
|
||||||
onDragStart: (event: Event) => void
|
onDragStart: (event: Event) => void
|
||||||
onDragEnd: () => void
|
onDragEnd: () => void
|
||||||
onDrop: (date: Date, hour: number) => void
|
onDrop: (date: Date, hour?: number) => void
|
||||||
getColorClasses: (color: string) => { bg: string; text: string }
|
getColorClasses: (color: string) => { bg: string; text: string }
|
||||||
}) {
|
}) {
|
||||||
const startOfWeek = new Date(currentDate)
|
const startOfWeek = new Date(currentDate)
|
||||||
@ -970,103 +1104,55 @@ function WeekView({
|
|||||||
return day
|
return day
|
||||||
})
|
})
|
||||||
|
|
||||||
// NOVO: limita intervalo de horas ao 1º e último evento da semana
|
const getEventsForDay = (date: Date) =>
|
||||||
const [startHour, endHour] = React.useMemo(() => {
|
events.filter((event) => {
|
||||||
let minH = Infinity
|
const d = new Date(event.startTime)
|
||||||
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()
|
|
||||||
return (
|
return (
|
||||||
eventDate.getDate() === date.getDate() &&
|
d.getFullYear() === date.getFullYear() &&
|
||||||
eventDate.getMonth() === date.getMonth() &&
|
d.getMonth() === date.getMonth() &&
|
||||||
eventDate.getFullYear() === date.getFullYear() &&
|
d.getDate() === date.getDate()
|
||||||
eventHour === hour
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-auto">
|
<Card className="overflow-auto">
|
||||||
<div className="grid grid-cols-8 border-b">
|
<div className="grid grid-cols-7 border-b">
|
||||||
<div className="border-r p-2 text-center text-xs font-medium sm:text-sm">Hora</div>
|
|
||||||
{weekDays.map((day) => (
|
{weekDays.map((day) => (
|
||||||
<div
|
<div key={day.toISOString()} className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm">
|
||||||
key={day.toISOString()}
|
<span className="hidden sm:inline">{day.toLocaleDateString(LOCALE, { weekday: "short", timeZone: TIMEZONE })}</span>
|
||||||
className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm"
|
<span className="sm:hidden">{day.toLocaleDateString(LOCALE, { weekday: "narrow", timeZone: TIMEZONE })}</span>
|
||||||
>
|
|
||||||
<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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-8">
|
|
||||||
{hours.map((hour) => (
|
<div className="grid grid-cols-7">
|
||||||
<React.Fragment key={`hour-${hour}`}>
|
{weekDays.map((day, idx) => {
|
||||||
<div
|
const dayEvents = getEventsForDay(day)
|
||||||
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)
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={idx} className="min-h-40 border-r p-2 last:border-r-0">
|
||||||
key={`${day.toISOString()}-${hour}`}
|
<div className="space-y-2">
|
||||||
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"
|
{dayEvents.map((ev) => (
|
||||||
onDragOver={(e) => e.preventDefault()}
|
<div key={ev.id} className="mb-2">
|
||||||
onDrop={() => onDrop(day, hour)}
|
|
||||||
>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{dayEvents.map((event) => (
|
|
||||||
<EventCard
|
<EventCard
|
||||||
key={event.id}
|
event={ev}
|
||||||
event={event}
|
|
||||||
onEventClick={onEventClick}
|
onEventClick={onEventClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
getColorClasses={getColorClasses}
|
getColorClasses={getColorClasses}
|
||||||
variant="default"
|
variant="compact"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Day View Component
|
// Day View Component (simple hourly lanes)
|
||||||
function DayView({
|
function DayView({
|
||||||
currentDate,
|
currentDate,
|
||||||
events,
|
events,
|
||||||
@ -1081,42 +1167,21 @@ function DayView({
|
|||||||
onEventClick: (event: Event) => void
|
onEventClick: (event: Event) => void
|
||||||
onDragStart: (event: Event) => void
|
onDragStart: (event: Event) => void
|
||||||
onDragEnd: () => void
|
onDragEnd: () => void
|
||||||
onDrop: (date: Date, hour: number) => void
|
onDrop: (date: Date, hour?: number) => void
|
||||||
getColorClasses: (color: string) => { bg: string; text: string }
|
getColorClasses: (color: string) => { bg: string; text: string }
|
||||||
}) {
|
}) {
|
||||||
// NOVO: calcula intervalo de horas do 1º ao último evento do dia
|
const hours = Array.from({ length: 24 }, (_, i) => i)
|
||||||
const [startHour, endHour] = React.useMemo(() => {
|
|
||||||
const sameDayEvents = events.filter((ev) => {
|
const getEventsForHour = (hour: number) =>
|
||||||
const d = ev.startTime
|
events.filter((event) => {
|
||||||
|
const d = new Date(event.startTime)
|
||||||
return (
|
return (
|
||||||
d.getDate() === currentDate.getDate() &&
|
d.getFullYear() === currentDate.getFullYear() &&
|
||||||
d.getMonth() === currentDate.getMonth() &&
|
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 (
|
return (
|
||||||
<Card className="overflow-auto">
|
<Card className="overflow-auto">
|
||||||
@ -1124,27 +1189,14 @@ function DayView({
|
|||||||
{hours.map((hour) => {
|
{hours.map((hour) => {
|
||||||
const hourEvents = getEventsForHour(hour)
|
const hourEvents = getEventsForHour(hour)
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={hour} className="flex border-b last:border-b-0" onDragOver={(e) => e.preventDefault()} onDrop={() => onDrop(currentDate, hour)}>
|
||||||
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">
|
<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
|
{hour.toString().padStart(2, "0")}:00
|
||||||
</div>
|
</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="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">
|
<div className="space-y-2">
|
||||||
{hourEvents.map((event) => (
|
{hourEvents.map((event) => (
|
||||||
<EventCard
|
<EventCard key={event.id} event={event} onEventClick={onEventClick} onDragStart={onDragStart} onDragEnd={onDragEnd} getColorClasses={getColorClasses} variant="detailed" />
|
||||||
key={event.id}
|
|
||||||
event={event}
|
|
||||||
onEventClick={onEventClick}
|
|
||||||
onDragStart={onDragStart}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
getColorClasses={getColorClasses}
|
|
||||||
variant="detailed"
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,24 +23,7 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
|
|||||||
@ -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"
|
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
|
asChild
|
||||||
>
|
>
|
||||||
|
<Link href="/login">Entrar</Link>
|
||||||
<Link href="/login-paciente">Sou Paciente</Link>
|
|
||||||
</Button>
|
</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>
|
</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"
|
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
|
asChild
|
||||||
>
|
>
|
||||||
<Link href="/login-paciente">Sou Paciente</Link>
|
<Link href="/login">Entrar</Link>
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -126,17 +126,6 @@ export default function ProtectedRoute({
|
|||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
Você não tem permissão para acessar esta página.
|
Você não tem permissão para acessar esta página.
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -298,8 +298,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
throw new AuthenticationError(
|
throw new AuthenticationError(
|
||||||
'Erro inesperado durante o login',
|
errorMessage || 'Erro inesperado durante o login',
|
||||||
'UNKNOWN_ERROR',
|
'UNKNOWN_ERROR',
|
||||||
error
|
error
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2139,25 +2139,25 @@ export async function criarMedico(input: MedicoInput): Promise<Medico> {
|
|||||||
// If server returned doctor_id, fetch the doctor
|
// If server returned doctor_id, fetch the doctor
|
||||||
if (parsed && parsed.doctor_id) {
|
if (parsed && parsed.doctor_id) {
|
||||||
const doc = await buscarMedicoPorId(String(parsed.doctor_id)).catch(() => null);
|
const doc = await buscarMedicoPorId(String(parsed.doctor_id)).catch(() => null);
|
||||||
if (doc) return Object.assign(doc, { password });
|
if (doc) return { ...doc, password } as any;
|
||||||
if (parsed.doctor) return Object.assign(parsed.doctor, { password });
|
if (parsed.doctor) return { ...parsed.doctor, password } as any;
|
||||||
return Object.assign({ id: parsed.doctor_id, full_name: input.full_name, cpf: cleanCpf, email: input.email } as Medico, { password });
|
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 server returned doctor object directly
|
||||||
if (parsed && (parsed.id || parsed.full_name || parsed.cpf)) {
|
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 server returned an envelope with user, try to locate doctor by email
|
||||||
if (parsed && parsed.user && parsed.user.id) {
|
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(() => []) : []);
|
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 });
|
if (Array.isArray(maybe) && maybe.length) return { ...maybe[0], password } as any;
|
||||||
return Object.assign({ id: parsed.user.id, full_name: input.full_name, email: input.email } as Medico, { password });
|
return { id: parsed.user.id, full_name: input.full_name, email: input.email, password } as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise return parsed with password as best-effort
|
// otherwise return parsed with password as best-effort
|
||||||
return Object.assign(parsed || {}, { password });
|
return { ...(parsed || {}), password } as any;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
lastErr = err;
|
lastErr = err;
|
||||||
const emsg = err && typeof err === 'object' && 'message' in err ? (err as any).message : String(err);
|
const emsg = err && typeof err === 'object' && 'message' in err ? (err as any).message : String(err);
|
||||||
|
|||||||
@ -189,15 +189,7 @@ class HttpClient {
|
|||||||
|
|
||||||
// Redirecionar para login
|
// Redirecionar para login
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const userType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) || 'profissional'
|
window.location.href = '/login'
|
||||||
const loginRoutes = {
|
|
||||||
profissional: '/login',
|
|
||||||
paciente: '/login-paciente',
|
|
||||||
administrador: '/login-admin'
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginRoute = loginRoutes[userType as keyof typeof loginRoutes] || '/login'
|
|
||||||
window.location.href = loginRoute
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,6 @@ export const USER_TYPE_ROUTES: UserTypeRoutes = {
|
|||||||
|
|
||||||
export const LOGIN_ROUTES: LoginRoutes = {
|
export const LOGIN_ROUTES: LoginRoutes = {
|
||||||
profissional: '/login',
|
profissional: '/login',
|
||||||
paciente: '/login-paciente',
|
paciente: '/login',
|
||||||
administrador: '/login-admin',
|
administrador: '/login',
|
||||||
} as const
|
} as const
|
||||||
Loading…
x
Reference in New Issue
Block a user