diff --git a/susconecta/components/ProtectedRoute.tsx b/susconecta/components/ProtectedRoute.tsx index 31e9633..5f7945b 100644 --- a/susconecta/components/ProtectedRoute.tsx +++ b/susconecta/components/ProtectedRoute.tsx @@ -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 (
@@ -130,6 +165,8 @@ export default function ProtectedRoute({ Tipo de acesso necessário: {requiredUserType.join(' ou ')}
Seu tipo de acesso: {user.userType} +
+ Seus papéis: {(user as any).roles ? (user as any).roles.join(', ') : '—'}

{/* Avatar Dropdown Simples */} diff --git a/susconecta/components/forms/doctor-registration-form.tsx b/susconecta/components/forms/doctor-registration-form.tsx index 8e8b072..e2655f1 100644 --- a/susconecta/components/forms/doctor-registration-form.tsx +++ b/susconecta/components/forms/doctor-registration-form.tsx @@ -288,6 +288,14 @@ function setField(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 && ( - - - {errors.submit} - - )} -
setExpanded((s) => ({ ...s, dados: !s.dados }))}> @@ -565,53 +566,11 @@ async function handleSubmit(ev: React.FormEvent) { -
-
- {photoPreview ? ( - Preview - ) : ( - - )} -
-
- - - {errors.photo &&

{errors.photo}

} -

Máximo 5MB

-
-
- -
-
- +
+ setField("full_name", e.target.value)} /> - - - {errors.full_name &&

{errors.full_name}

} -
-
- - setField("nome_social", e.target.value)} /> -
-
-
- - setField("crm", e.target.value)} className={errors.crm ? "border-destructive" : ""} /> - {errors.crm &&

{errors.crm}

} -
-
- - setField("estado_crm", e.target.value)} /> -
-
@@ -693,10 +652,8 @@ async function handleSubmit(ev: React.FormEvent) { { - 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) { />
+ @@ -792,39 +750,29 @@ async function handleSubmit(ev: React.FormEvent) { setField("email", e.target.value)} />
-
-
- - setField("telefone", formatPhone(e.target.value))} - placeholder="(XX) XXXXX-XXXX" - /> -
-
- - setField("celular", formatPhone(e.target.value))} - placeholder="(XX) XXXXX-XXXX" - /> -
-
- +
+ + setField("telefone", formatPhone(e.target.value))} + placeholder="(XX) XXXXX-XXXX" + /> +
+
- setField("celular", formatPhone(e.target.value))} placeholder="(XX) XXXXX-XXXX" />
- setField("contato_emergencia", formatPhone(e.target.value))} placeholder="(XX) XXXXX-XXXX" /> diff --git a/susconecta/components/forms/patient-registration-form.tsx b/susconecta/components/forms/patient-registration-form.tsx index 9cbb0e5..6ca311b 100644 --- a/susconecta/components/forms/patient-registration-form.tsx +++ b/susconecta/components/forms/patient-registration-form.tsx @@ -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({
{photoPreview ? ( - Preview + Preview ) : ( )} @@ -497,7 +526,7 @@ export function PatientRegistrationForm({
- setField("rg", e.target.value)} /> + setField("rg", formatRG(e.target.value))} maxLength={12} />
@@ -520,10 +549,7 @@ export function PatientRegistrationForm({ { - 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({
- setField("telefone", e.target.value)} /> + setField("telefone", formatPhone(e.target.value))} placeholder="(XX) XXXXX-XXXX" />
diff --git a/susconecta/hooks/useAuth.tsx b/susconecta/hooks/useAuth.tsx index 5871f03..488c318 100644 --- a/susconecta/hooks/useAuth.tsx +++ b/susconecta/hooks/useAuth.tsx @@ -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) } diff --git a/susconecta/hooks/useReports.ts b/susconecta/hooks/useReports.ts index e2185bb..8061201 100644 --- a/susconecta/hooks/useReports.ts +++ b/susconecta/hooks/useReports.ts @@ -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(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(); - const doctorsCacheRef = (globalThis as any).__reportsDoctorsCache__ || new Map(); - // store back to globalThis so cache persiste entre hot reloads - (globalThis as any).__reportsPatientsCache__ = patientsCacheRef; - (globalThis as any).__reportsDoctorsCache__ = doctorsCacheRef; + const patientsCacheRef = useRef>((globalThis as any).__reportsPatientsCache__ || new Map()); + const doctorsCacheRef = useRef>((globalThis as any).__reportsDoctorsCache__ || new Map()); + // 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(); @@ -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)); } } diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 6dcc2ba..954589f 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -240,9 +240,10 @@ async function parse(res: Response): Promise { // 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(res: Response): Promise { 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 { 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 { 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 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 diff --git a/susconecta/types/auth.ts b/susconecta/types/auth.ts index e4bcd45..228a423 100644 --- a/susconecta/types/auth.ts +++ b/susconecta/types/auth.ts @@ -17,6 +17,7 @@ export interface UserData { telefone?: string foto_url?: string } + roles?: string[] } export interface LoginRequest {