feat(auth): persist roles and support multi-role in ProtectedRoute
- Persist `roles` array in localStorage on login and session restore. - Reconcile `userType` from roles returned by the `user-info` function. - `ProtectedRoute` now accepts `requiredUserType?: UserType[]` and `requiredRoles?: string[]` and evaluates multi-role permission (OR semantics). - Minor adjustments in `useAuth` and debug logs to ensure consistent `profile` and `roles` restoration. - Main files changed: `hooks/useAuth.tsx`, `components/ProtectedRoute.tsx`, `types/auth.ts.
This commit is contained in:
parent
ca84c563b6
commit
05123e6c8f
@ -8,16 +8,54 @@ import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
requiredUserType?: UserType[]
|
||||
requiredRoles?: string[]
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({
|
||||
children,
|
||||
requiredUserType
|
||||
requiredUserType,
|
||||
requiredRoles
|
||||
}: ProtectedRouteProps) {
|
||||
const { authStatus, user } = useAuth()
|
||||
const router = useRouter()
|
||||
const isRedirecting = useRef(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [accessDenied, setAccessDenied] = useState(false)
|
||||
|
||||
// Computa permissões de forma síncrona a partir do user e dos roles/userType
|
||||
const computeHasPermission = () => {
|
||||
// sem requisitos, permite
|
||||
if ((!requiredUserType || requiredUserType.length === 0) && (!requiredRoles || requiredRoles.length === 0)) return true
|
||||
if (!user) return false
|
||||
|
||||
const userRoles = (user as any).roles || []
|
||||
|
||||
// checa requiredRoles (strings arbitrárias de papéis)
|
||||
const rolesOk = requiredRoles && requiredRoles.length > 0
|
||||
? requiredRoles.some((req: string) => {
|
||||
if (user.userType === req) return true
|
||||
if (req === 'profissional' && (userRoles.includes('medico') || userRoles.includes('enfermeiro'))) return true
|
||||
if (req === 'administrador' && (userRoles.includes('admin') || userRoles.includes('gestor') || userRoles.includes('secretaria'))) return true
|
||||
if (req === 'paciente' && userRoles.includes('paciente')) return true
|
||||
return userRoles.includes(req)
|
||||
})
|
||||
: false
|
||||
|
||||
// checa requiredUserType (compatibilidade com tipos altos do sistema)
|
||||
const userTypeOk = requiredUserType && requiredUserType.length > 0
|
||||
? requiredUserType.some((req: UserType) => {
|
||||
if (user.userType === req) return true
|
||||
if (req === 'profissional' && (userRoles.includes('medico') || userRoles.includes('enfermeiro'))) return true
|
||||
if (req === 'administrador' && (userRoles.includes('admin') || userRoles.includes('gestor') || userRoles.includes('secretaria'))) return true
|
||||
if (req === 'paciente' && userRoles.includes('paciente')) return true
|
||||
return userRoles.includes(req)
|
||||
})
|
||||
: false
|
||||
|
||||
return rolesOk || userTypeOk
|
||||
}
|
||||
|
||||
const hasPermission = computeHasPermission()
|
||||
|
||||
useEffect(() => {
|
||||
// marca que o componente já montou no cliente
|
||||
@ -61,18 +99,15 @@ export default function ProtectedRoute({
|
||||
}
|
||||
|
||||
// Se autenticado mas não tem permissão para esta página
|
||||
if (authStatus === 'authenticated' && user && requiredUserType && !requiredUserType.includes(user.userType)) {
|
||||
isRedirecting.current = true
|
||||
|
||||
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página', {
|
||||
if (authStatus === 'authenticated' && user && requiredUserType && !hasPermission) {
|
||||
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página (accessDenied)', {
|
||||
userType: user.userType,
|
||||
userRoles: (user as any).roles || [],
|
||||
requiredTypes: requiredUserType
|
||||
})
|
||||
|
||||
const correctRoute = USER_TYPE_ROUTES[user.userType]
|
||||
console.log('[PROTECTED-ROUTE] Redirecionando para área correta:', correctRoute)
|
||||
|
||||
router.push(correctRoute)
|
||||
|
||||
// Marcar acesso negado para renderizar fallback sem redirecionamentos/speinners infinitos
|
||||
setAccessDenied(true)
|
||||
return
|
||||
}
|
||||
|
||||
@ -85,7 +120,7 @@ export default function ProtectedRoute({
|
||||
})
|
||||
isRedirecting.current = false
|
||||
}
|
||||
}, [authStatus, user, requiredUserType, router])
|
||||
}, [authStatus, user, requiredUserType, router, hasPermission])
|
||||
|
||||
// Durante loading, mostrar spinner
|
||||
if (authStatus === 'loading') {
|
||||
@ -102,8 +137,8 @@ export default function ProtectedRoute({
|
||||
)
|
||||
}
|
||||
|
||||
// Se não autenticado ou redirecionando, mostrar spinner
|
||||
if (authStatus === 'unauthenticated' || isRedirecting.current) {
|
||||
// Se não autenticado ou redirecionando para login, mostrar spinner
|
||||
if (authStatus === 'unauthenticated' || (isRedirecting.current && !accessDenied)) {
|
||||
// evitar render no servidor para não causar mismatch de hidratação
|
||||
if (!mounted) return null
|
||||
|
||||
@ -117,8 +152,8 @@ export default function ProtectedRoute({
|
||||
)
|
||||
}
|
||||
|
||||
// Se usuário não tem permissão, mostrar fallback (não deveria chegar aqui devido ao useEffect)
|
||||
if (requiredUserType && user && !requiredUserType.includes(user.userType)) {
|
||||
// Se usuário não tem permissão, mostrar fallback (baseado em hasPermission)
|
||||
if (requiredUserType && user && !hasPermission) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@ -130,6 +165,8 @@ export default function ProtectedRoute({
|
||||
Tipo de acesso necessário: {requiredUserType.join(' ou ')}
|
||||
<br />
|
||||
Seu tipo de acesso: {user.userType}
|
||||
<br />
|
||||
Seus papéis: {(user as any).roles ? (user as any).roles.join(', ') : '—'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push(USER_TYPE_ROUTES[user.userType])}
|
||||
|
||||
@ -48,7 +48,7 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
<SimpleThemeToggle />
|
||||
<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"
|
||||
className="text-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
|
||||
></Button>
|
||||
{/* Avatar Dropdown Simples */}
|
||||
|
||||
@ -288,6 +288,14 @@ function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Auto formata input de data para dd/mm/aaaa enquanto digita
|
||||
function formatDateInput(v: string) {
|
||||
const nums = v.replace(/\D/g, "").slice(0, 8);
|
||||
if (nums.length <= 2) return nums;
|
||||
if (nums.length <= 4) return `${nums.slice(0, 2)}/${nums.slice(2)}`;
|
||||
return `${nums.slice(0, 2)}/${nums.slice(2, 4)}/${nums.slice(4, 8)}`;
|
||||
}
|
||||
|
||||
function formatRG(v: string) {
|
||||
v = v.replace(/\D/g, "").slice(0, 9);
|
||||
v = v.replace(/(\d{2})(\d)/, "$1.$2");
|
||||
@ -542,13 +550,6 @@ async function handleSubmit(ev: React.FormEvent) {
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{errors.submit && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{errors.submit}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
|
||||
<Card>
|
||||
@ -565,53 +566,11 @@ async function handleSubmit(ev: React.FormEvent) {
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-4 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
|
||||
{photoPreview ? (
|
||||
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="photo" className="cursor-pointer rounded-md transition-colors">
|
||||
<Button type="button" variant="ghost" asChild className="bg-primary text-primary-foreground border-transparent hover:bg-primary">
|
||||
<span>
|
||||
<Upload className="mr-2 h-4 w-4 text-primary-foreground" /> Carregar Foto
|
||||
</span>
|
||||
</Button>
|
||||
</Label>
|
||||
<Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhoto} />
|
||||
{errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>}
|
||||
<p className="text-xs text-muted-foreground">Máximo 5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Nome *</Label>
|
||||
<div className="space-y-2">
|
||||
<Label>Nome completo</Label>
|
||||
<Input value={form.full_name} onChange={(e) => setField("full_name", e.target.value)} />
|
||||
|
||||
|
||||
{errors.full_name && <p className="text-sm text-destructive">{errors.full_name}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Nome Social</Label>
|
||||
<Input value={form.nome_social} onChange={(e) => setField("nome_social", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>CRM *</Label>
|
||||
<Input value={form.crm} onChange={(e) => setField("crm", e.target.value)} className={errors.crm ? "border-destructive" : ""} />
|
||||
{errors.crm && <p className="text-sm text-destructive">{errors.crm}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Estado do CRM</Label>
|
||||
<Input value={form.estado_crm} onChange={(e) => setField("estado_crm", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
@ -693,10 +652,8 @@ async function handleSubmit(ev: React.FormEvent) {
|
||||
<Input
|
||||
placeholder="dd/mm/aaaa"
|
||||
value={form.data_nascimento}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10);
|
||||
setField("data_nascimento", v);
|
||||
}}
|
||||
onChange={(e) => setField("data_nascimento", formatDateInput(e.target.value))}
|
||||
maxLength={10}
|
||||
onBlur={() => {
|
||||
const raw = form.data_nascimento;
|
||||
const parts = raw.split(/\D+/).filter(Boolean);
|
||||
@ -708,6 +665,7 @@ async function handleSubmit(ev: React.FormEvent) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
@ -792,39 +750,29 @@ async function handleSubmit(ev: React.FormEvent) {
|
||||
<Label>E-mail</Label>
|
||||
<Input value={form.email} onChange={(e) => setField("email", e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Telefone</Label>
|
||||
<Input
|
||||
value={form.telefone}
|
||||
onChange={(e) => setField("telefone", formatPhone(e.target.value))}
|
||||
placeholder="(XX) XXXXX-XXXX"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Celular</Label>
|
||||
<Input
|
||||
value={form.celular}
|
||||
onChange={(e) => setField("celular", formatPhone(e.target.value))}
|
||||
placeholder="(XX) XXXXX-XXXX"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Telefone</Label>
|
||||
<Input
|
||||
value={form.telefone}
|
||||
onChange={(e) => setField("telefone", formatPhone(e.target.value))}
|
||||
placeholder="(XX) XXXXX-XXXX"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Celular</Label>
|
||||
<Input
|
||||
value={form.celular}
|
||||
<Input
|
||||
value={form.celular}
|
||||
onChange={(e) => setField("celular", formatPhone(e.target.value))}
|
||||
placeholder="(XX) XXXXX-XXXX"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Contato de Emergência</Label>
|
||||
<Input
|
||||
value={form.contato_emergencia}
|
||||
<Input
|
||||
value={form.contato_emergencia}
|
||||
onChange={(e) => setField("contato_emergencia", formatPhone(e.target.value))}
|
||||
placeholder="(XX) XXXXX-XXXX"
|
||||
/>
|
||||
|
||||
@ -12,6 +12,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react";
|
||||
import Image from 'next/image';
|
||||
|
||||
import {
|
||||
Paciente,
|
||||
@ -165,6 +166,14 @@ export function PatientRegistrationForm({
|
||||
setField("cpf", formatCPF(v));
|
||||
}
|
||||
|
||||
function formatRG(v: string) {
|
||||
let s = String(v || "").replace(/\D/g, "").slice(0, 9);
|
||||
s = s.replace(/(\d{2})(\d)/, "$1.$2");
|
||||
s = s.replace(/(\d{3})(\d)/, "$1.$2");
|
||||
s = s.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
|
||||
return s;
|
||||
}
|
||||
|
||||
function formatCEP(v: string) {
|
||||
const n = v.replace(/\D/g, "").slice(0, 8);
|
||||
return n.replace(/(\d{5})(\d{0,3})/, (_, a, b) => `${a}${b ? "-" + b : ""}`);
|
||||
@ -188,6 +197,26 @@ export function PatientRegistrationForm({
|
||||
} finally {
|
||||
setSearchingCEP(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Formata telefone enquanto digita: (XX) XXXXX-XXXX
|
||||
function formatPhone(v: string) {
|
||||
const n = v.replace(/\D/g, "").slice(0, 11);
|
||||
if (n.length > 6) {
|
||||
return n.replace(/(\d{2})(\d{5})(\d{0,4})/, "($1) $2-$3");
|
||||
} else if (n.length > 2) {
|
||||
return n.replace(/(\d{2})(\d{0,5})/, "($1) $2");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Auto formata input de data para dd/mm/aaaa enquanto digita
|
||||
function formatDateInput(v: string) {
|
||||
const nums = v.replace(/\D/g, "").slice(0, 8);
|
||||
if (nums.length <= 2) return nums;
|
||||
if (nums.length <= 4) return `${nums.slice(0,2)}/${nums.slice(2)}`;
|
||||
return `${nums.slice(0,2)}/${nums.slice(2,4)}/${nums.slice(4,8)}`;
|
||||
}
|
||||
|
||||
function validateLocal(): boolean {
|
||||
@ -447,7 +476,7 @@ export function PatientRegistrationForm({
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
|
||||
{photoPreview ? (
|
||||
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
||||
<Image src={photoPreview} alt="Preview" className="w-full h-full object-cover" width={400} height={300} />
|
||||
) : (
|
||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||
)}
|
||||
@ -497,7 +526,7 @@ export function PatientRegistrationForm({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>RG</Label>
|
||||
<Input value={form.rg} onChange={(e) => setField("rg", e.target.value)} />
|
||||
<Input value={form.rg} onChange={(e) => setField("rg", formatRG(e.target.value))} maxLength={12} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -520,10 +549,7 @@ export function PatientRegistrationForm({
|
||||
<Input
|
||||
placeholder="dd/mm/aaaa"
|
||||
value={form.birth_date}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10);
|
||||
setField("birth_date", v);
|
||||
}}
|
||||
onChange={(e) => setField("birth_date", formatDateInput(e.target.value))}
|
||||
onBlur={() => {
|
||||
const raw = form.birth_date;
|
||||
const parts = raw.split(/\D+/).filter(Boolean);
|
||||
@ -560,7 +586,7 @@ export function PatientRegistrationForm({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Telefone</Label>
|
||||
<Input value={form.telefone} onChange={(e) => setField("telefone", e.target.value)} />
|
||||
<Input value={form.telefone} onChange={(e) => setField("telefone", formatPhone(e.target.value))} placeholder="(XX) XXXXX-XXXX" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@ -46,8 +46,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Persistir dados de forma atômica
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken)
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType)
|
||||
// Garantir que roles também sejam persistidos se presentes
|
||||
const toStore = { ...userData } as any
|
||||
if (userData && (userData as any).roles) {
|
||||
toStore.roles = (userData as any).roles
|
||||
}
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(toStore))
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, toStore.userType)
|
||||
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
|
||||
@ -55,7 +60,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
setToken(accessToken)
|
||||
setUser(userData)
|
||||
// Garantir que o estado mantenha roles também
|
||||
setUser(userData as any)
|
||||
setAuthStatus('authenticated')
|
||||
|
||||
console.log('[AUTH] LOGIN realizado - Dados salvos!', {
|
||||
@ -151,6 +157,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
// que 'auth_user.profile' fique vazio após um reload completo
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Persistir também roles se disponíveis
|
||||
if (info?.roles && Array.isArray(info.roles)) {
|
||||
userData.roles = info.roles
|
||||
}
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
|
||||
}
|
||||
} catch (e) {
|
||||
@ -218,6 +228,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
response.user.userType = derived
|
||||
console.log('[AUTH] userType reconciled from roles ->', derived)
|
||||
}
|
||||
// Persistir roles no objeto user para suportar multi-role
|
||||
if (response.user && roles.length) {
|
||||
response.user.roles = roles
|
||||
}
|
||||
} else {
|
||||
console.warn('[AUTH] Falha ao obter user-info para reconciliar roles:', infoRes.status)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// hooks/useReports.ts
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Report,
|
||||
CreateReportData,
|
||||
@ -43,11 +43,11 @@ export function useReports(): UseReportsReturn {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Caches em memória para evitar múltiplas buscas pelo mesmo ID durante a sessão
|
||||
const patientsCacheRef = (globalThis as any).__reportsPatientsCache__ || new Map<string, any>();
|
||||
const doctorsCacheRef = (globalThis as any).__reportsDoctorsCache__ || new Map<string, any>();
|
||||
// store back to globalThis so cache persiste entre hot reloads
|
||||
(globalThis as any).__reportsPatientsCache__ = patientsCacheRef;
|
||||
(globalThis as any).__reportsDoctorsCache__ = doctorsCacheRef;
|
||||
const patientsCacheRef = useRef<Map<string, any>>((globalThis as any).__reportsPatientsCache__ || new Map<string, any>());
|
||||
const doctorsCacheRef = useRef<Map<string, any>>((globalThis as any).__reportsDoctorsCache__ || new Map<string, any>());
|
||||
// store back to globalThis so cache persists between hot reloads
|
||||
(globalThis as any).__reportsPatientsCache__ = patientsCacheRef.current;
|
||||
(globalThis as any).__reportsDoctorsCache__ = doctorsCacheRef.current;
|
||||
|
||||
// Função para tratar erros
|
||||
const handleError = useCallback((error: any) => {
|
||||
@ -80,10 +80,10 @@ export function useReports(): UseReportsReturn {
|
||||
|
||||
for (const r of arr) {
|
||||
const pid = r.patient_id ?? r.patient ?? r.paciente;
|
||||
if (pid && typeof pid === 'string' && !patientIds.includes(pid) && !patientsCacheRef.has(String(pid))) patientIds.push(pid);
|
||||
if (pid && typeof pid === 'string' && !patientIds.includes(pid) && !patientsCacheRef.current.has(String(pid))) patientIds.push(pid);
|
||||
|
||||
const did = r.requested_by ?? r.created_by ?? r.executante;
|
||||
if (did && typeof did === 'string' && !doctorIds.includes(did) && !doctorsCacheRef.has(String(did))) doctorIds.push(did);
|
||||
if (did && typeof did === 'string' && !doctorIds.includes(did) && !doctorsCacheRef.current.has(String(did))) doctorIds.push(did);
|
||||
}
|
||||
|
||||
const patientsById = new Map<string, any>();
|
||||
@ -92,7 +92,7 @@ export function useReports(): UseReportsReturn {
|
||||
const patients = await buscarPacientesPorIds(patientIds);
|
||||
for (const p of patients) {
|
||||
patientsById.set(String(p.id), p);
|
||||
patientsCacheRef.set(String(p.id), p);
|
||||
patientsCacheRef.current.set(String(p.id), p);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore batch failure
|
||||
@ -100,14 +100,14 @@ export function useReports(): UseReportsReturn {
|
||||
}
|
||||
|
||||
// fallback individual para quaisquer IDs que não foram resolvidos no batch
|
||||
const unresolvedPatientIds = patientIds.filter(id => !patientsById.has(String(id)) && !patientsCacheRef.has(String(id)));
|
||||
const unresolvedPatientIds = patientIds.filter(id => !patientsById.has(String(id)) && !patientsCacheRef.current.has(String(id)));
|
||||
if (unresolvedPatientIds.length) {
|
||||
await Promise.all(unresolvedPatientIds.map(async (id) => {
|
||||
try {
|
||||
const p = await buscarPacientePorId(id);
|
||||
if (p) {
|
||||
patientsById.set(String(id), p);
|
||||
patientsCacheRef.set(String(id), p);
|
||||
patientsCacheRef.current.set(String(id), p);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore individual failure
|
||||
@ -121,21 +121,21 @@ export function useReports(): UseReportsReturn {
|
||||
const doctors = await buscarMedicosPorIds(doctorIds);
|
||||
for (const d of doctors) {
|
||||
doctorsById.set(String(d.id), d);
|
||||
doctorsCacheRef.set(String(d.id), d);
|
||||
doctorsCacheRef.current.set(String(d.id), d);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const unresolvedDoctorIds = doctorIds.filter(id => !doctorsById.has(String(id)) && !doctorsCacheRef.has(String(id)));
|
||||
const unresolvedDoctorIds = doctorIds.filter(id => !doctorsById.has(String(id)) && !doctorsCacheRef.current.has(String(id)));
|
||||
if (unresolvedDoctorIds.length) {
|
||||
await Promise.all(unresolvedDoctorIds.map(async (id) => {
|
||||
try {
|
||||
const d = await buscarMedicoPorId(id);
|
||||
if (d) {
|
||||
doctorsById.set(String(id), d);
|
||||
doctorsCacheRef.set(String(id), d);
|
||||
doctorsCacheRef.current.set(String(id), d);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
@ -156,7 +156,7 @@ export function useReports(): UseReportsReturn {
|
||||
const pid = r.patient_id ?? r.patient ?? r.paciente;
|
||||
if (!copy.paciente && pid) {
|
||||
if (patientsById.has(String(pid))) copy.paciente = patientsById.get(String(pid));
|
||||
else if (patientsCacheRef.has(String(pid))) copy.paciente = patientsCacheRef.get(String(pid));
|
||||
else if (patientsCacheRef.current.has(String(pid))) copy.paciente = patientsCacheRef.current.get(String(pid));
|
||||
}
|
||||
|
||||
// Executante: prefira campos de nome já fornecidos
|
||||
@ -167,7 +167,7 @@ export function useReports(): UseReportsReturn {
|
||||
const did = r.requested_by ?? r.created_by ?? r.executante;
|
||||
if (did) {
|
||||
if (doctorsById.has(String(did))) copy.executante = doctorsById.get(String(did))?.full_name ?? doctorsById.get(String(did))?.nome ?? copy.executante;
|
||||
else if (doctorsCacheRef.has(String(did))) copy.executante = doctorsCacheRef.get(String(did))?.full_name ?? doctorsCacheRef.get(String(did))?.nome ?? copy.executante;
|
||||
else if (doctorsCacheRef.current.has(String(did))) copy.executante = doctorsCacheRef.current.get(String(did))?.full_name ?? doctorsCacheRef.current.get(String(did))?.nome ?? copy.executante;
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,7 +183,7 @@ export function useReports(): UseReportsReturn {
|
||||
if (pid && !r.paciente) unresolvedPatients.push(String(pid));
|
||||
const did = r.requested_by ?? r.created_by ?? r.executante;
|
||||
// note: if executante was resolved to a name, r.executante will be string name; if still ID, it may be ID
|
||||
if (did && (typeof r.executante === 'undefined' || (typeof r.executante === 'string' && r.executante.length > 30 && r.executante.includes('-')))) {
|
||||
if (did && (r.executante === undefined || (typeof r.executante === 'string' && r.executante.length > 30 && r.executante.includes('-')))) {
|
||||
unresolvedDoctors.push(String(did));
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,9 +240,10 @@ async function parse<T>(res: Response): Promise<T> {
|
||||
|
||||
// Mensagens amigáveis para erros comuns
|
||||
let friendlyMessage = msg;
|
||||
|
||||
// Erros de criação de usuário
|
||||
|
||||
// Erros de criação de usuário ou códigos conhecidos
|
||||
if (res.url?.includes('create-user')) {
|
||||
// organizar casos por checagens em ordem específica
|
||||
if (msg?.includes('Failed to assign user role')) {
|
||||
friendlyMessage = 'O usuário foi criado mas houve falha ao atribuir permissões. Entre em contato com o administrador do sistema para verificar as configurações da Edge Function.';
|
||||
} else if (msg?.includes('already registered')) {
|
||||
@ -251,41 +252,42 @@ async function parse<T>(res: Response): Promise<T> {
|
||||
friendlyMessage = 'Tipo de acesso inválido.';
|
||||
} else if (msg?.includes('Missing required fields')) {
|
||||
friendlyMessage = 'Campos obrigatórios não preenchidos.';
|
||||
} else if (res.status === 401) {
|
||||
friendlyMessage = 'Você não está autenticado. Faça login novamente.';
|
||||
} else if (res.status === 403) {
|
||||
friendlyMessage = 'Você não tem permissão para criar usuários.';
|
||||
} else if (res.status === 500) {
|
||||
friendlyMessage = 'Erro no servidor ao criar usuário. Entre em contato com o suporte.';
|
||||
}
|
||||
}
|
||||
// Erro de CPF duplicado
|
||||
else if (code === '23505' && msg.includes('patients_cpf_key')) {
|
||||
friendlyMessage = 'Já existe um paciente cadastrado com este CPF. Por favor, verifique se o paciente já está registrado no sistema ou use um CPF diferente.';
|
||||
}
|
||||
// Erro de email duplicado (paciente)
|
||||
else if (code === '23505' && msg.includes('patients_email_key')) {
|
||||
friendlyMessage = 'Já existe um paciente cadastrado com este email. Por favor, use um email diferente.';
|
||||
}
|
||||
// Erro de CRM duplicado (médico)
|
||||
else if (code === '23505' && msg.includes('doctors_crm')) {
|
||||
friendlyMessage = 'Já existe um médico cadastrado com este CRM. Por favor, verifique se o médico já está registrado no sistema.';
|
||||
}
|
||||
// Erro de email duplicado (médico)
|
||||
else if (code === '23505' && msg.includes('doctors_email_key')) {
|
||||
friendlyMessage = 'Já existe um médico cadastrado com este email. Por favor, use um email diferente.';
|
||||
}
|
||||
// Outros erros de constraint unique
|
||||
else if (code === '23505') {
|
||||
friendlyMessage = 'Registro duplicado: já existe um cadastro com essas informações no sistema.';
|
||||
}
|
||||
// Erro de foreign key (registro referenciado em outra tabela)
|
||||
else if (code === '23503') {
|
||||
// Mensagem específica para pacientes com relatórios vinculados
|
||||
if (msg && msg.toString().toLowerCase().includes('reports')) {
|
||||
friendlyMessage = 'Não é possível excluir este paciente porque existem relatórios vinculados a ele. Exclua ou desvincule os relatórios antes de remover o paciente.';
|
||||
} else {
|
||||
friendlyMessage = 'Registro referenciado em outra tabela. Remova referências dependentes antes de tentar novamente.';
|
||||
switch (res.status) {
|
||||
case 401:
|
||||
friendlyMessage = 'Você não está autenticado. Faça login novamente.';
|
||||
break;
|
||||
case 403:
|
||||
friendlyMessage = 'Você não tem permissão para criar usuários.';
|
||||
break;
|
||||
case 500:
|
||||
friendlyMessage = 'Erro no servidor ao criar usuário. Entre em contato com o suporte.';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// outros códigos comuns retornados pelo banco/edge
|
||||
if (code === '23505') {
|
||||
if (msg.includes('patients_cpf_key')) {
|
||||
friendlyMessage = 'Já existe um paciente cadastrado com este CPF. Por favor, verifique se o paciente já está registrado no sistema ou use um CPF diferente.';
|
||||
} else if (msg.includes('patients_email_key')) {
|
||||
friendlyMessage = 'Já existe um paciente cadastrado com este email. Por favor, use um email diferente.';
|
||||
} else if (msg.includes('doctors_crm')) {
|
||||
friendlyMessage = 'Já existe um médico cadastrado com este CRM. Por favor, verifique se o médico já está registrado no sistema.';
|
||||
} else if (msg.includes('doctors_email_key')) {
|
||||
friendlyMessage = 'Já existe um médico cadastrado com este email. Por favor, use um email diferente.';
|
||||
} else {
|
||||
friendlyMessage = 'Registro duplicado: já existe um cadastro com essas informações no sistema.';
|
||||
}
|
||||
} else if (code === '23503') {
|
||||
// Mensagem específica para pacientes com relatórios vinculados
|
||||
if (msg && msg.toString().toLowerCase().includes('reports')) {
|
||||
friendlyMessage = 'Não é possível excluir este paciente porque existem relatórios vinculados a ele. Exclua ou desvincule os relatórios antes de remover o paciente.';
|
||||
} else {
|
||||
friendlyMessage = 'Registro referenciado em outra tabela. Remova referências dependentes antes de tentar novamente.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -374,7 +376,7 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
||||
try {
|
||||
const [key, val] = String(query).split('=');
|
||||
const params = new URLSearchParams();
|
||||
if (key && typeof val !== 'undefined') params.set(key, val);
|
||||
if (key && val !== undefined) params.set(key, val);
|
||||
params.set('limit', '10');
|
||||
const url = `${REST}/patients?${params.toString()}`;
|
||||
const headers = baseHeaders();
|
||||
@ -713,9 +715,9 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
||||
try {
|
||||
// Build the URL safely using URLSearchParams so special characters (like @) are encoded correctly
|
||||
// query is like 'nome_social=ilike.*something*' -> split into key/value
|
||||
const [key, val] = String(query).split('=');
|
||||
const params = new URLSearchParams();
|
||||
if (key && typeof val !== 'undefined') params.set(key, val);
|
||||
const [key, val] = String(query).split('=');
|
||||
const params = new URLSearchParams();
|
||||
if (key && val !== undefined) params.set(key, val);
|
||||
params.set('limit', '10');
|
||||
const url = `${REST}/doctors?${params.toString()}`;
|
||||
const headers = baseHeaders();
|
||||
@ -749,7 +751,8 @@ export async function buscarMedicoPorId(id: string | number): Promise<Medico | n
|
||||
const sId = String(id);
|
||||
|
||||
// Helper para escape de aspas
|
||||
const escapeQuotes = (v: string) => v.replace(/"/g, '\\"');
|
||||
// escape double quotes for safe inclusion in quoted queries
|
||||
const escapeQuotes = (v: string) => String(v).replace(/"/g, String.raw`"`);
|
||||
|
||||
try {
|
||||
// 1) Se parece UUID, busca por id direto
|
||||
|
||||
@ -17,6 +17,7 @@ export interface UserData {
|
||||
telefone?: string
|
||||
foto_url?: string
|
||||
}
|
||||
roles?: string[]
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user