modified: src/App.jsx

modified:   src/components/AppShell.jsx
new file:   src/config/permissions.js
new file:   src/hooks/useAuth.js
new file:   src/pages/UsersPage.jsx
new file:   src/repositories/userRepository.js
This commit is contained in:
2026-05-05 22:49:20 -03:00
parent 06acf8cc61
commit bb5200664a
6 changed files with 717 additions and 22 deletions

View File

@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { authRepository } from './repositories/authRepository.js'
import './App.css'
import { AppShell } from './components/AppShell.jsx'
import { canAccess } from './config/permissions.js'
import { useAuth } from './hooks/useAuth.js'
import { AgendaPage } from './pages/AgendaPage.jsx'
import { AnalyticsPage } from './pages/AnalyticsPage.jsx'
import { ForgotPasswordPage, LoginPage, RegisterPage } from './pages/AuthPages.jsx'
@@ -16,11 +16,13 @@ import { ProfilePage } from './pages/ProfilePage.jsx'
import { ReportsPage } from './pages/ReportsPage.jsx'
import { SettingsPage } from './pages/SettingsPage.jsx'
import { TeamPage } from './pages/TeamPage.jsx'
import { UsersPage } from './pages/UsersPage.jsx'
import { VisitsPage } from './pages/VisitsPage.jsx'
import { patientRepository } from './repositories/patientRepository.js'
function App() {
const [location, setLocation] = useState(() => readLocation())
const { isAuthenticated, role, loading: authLoading } = useAuth()
const navigate = useCallback((to, options = {}) => {
if (options.replace) {
@@ -49,25 +51,47 @@ function App() {
return () => window.removeEventListener('popstate', handlePopState)
}, [])
const route = useMemo(() => resolveRoute(location.pathname, navigate), [location.pathname, navigate])
const isAuthenticated = authRepository.isAuthenticated()
const route = useMemo(
() => resolveRoute(location.pathname, navigate, role),
[location.pathname, navigate, role],
)
// Tela de carregamento enquanto busca o role do usuário
if (authLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-[#0a0a0a]">
<p className="text-sm text-[#a3a3a3]">Carregando...</p>
</div>
)
}
// Rotas públicas (sem shell)
if (!route.withShell) {
return route.element
}
// Usuário não autenticado
if (!isAuthenticated) {
return <LoginPage navigate={navigate} />
}
// Usuário autenticado mas sem permissão para a rota
if (role && !canAccess(role, location.pathname)) {
return (
<AppShell currentPath={location.pathname} navigate={navigate} role={role} routeTitle="Sem acesso">
<UnauthorizedPage navigate={navigate} />
</AppShell>
)
}
return (
<AppShell currentPath={location.pathname} navigate={navigate} routeTitle={route.title}>
<AppShell currentPath={location.pathname} navigate={navigate} role={role} routeTitle={route.title}>
{route.element}
</AppShell>
)
}
function resolveRoute(pathname, navigate) {
function resolveRoute(pathname, navigate, role) {
if (pathname === '/' || pathname === '/login') {
return {
element: <LoginPage navigate={navigate} />,
@@ -102,7 +126,7 @@ function resolveRoute(pathname, navigate) {
if (pathname === '/agenda') {
return {
element: <AgendaPage navigate={navigate} />,
element: <AgendaPage navigate={navigate} role={role} />,
title: 'Agenda',
withShell: true,
}
@@ -110,7 +134,7 @@ function resolveRoute(pathname, navigate) {
if (pathname === '/pacientes') {
return {
element: <PatientsPage navigate={navigate} />,
element: <PatientsPage navigate={navigate} role={role} />,
title: 'Pacientes',
withShell: true,
}
@@ -126,7 +150,6 @@ function resolveRoute(pathname, navigate) {
if (pathname.startsWith('/pacientes/')) {
const patientId = pathname.split('/')[2]
return {
element: <PatientDetailRoute navigate={navigate} patientId={patientId} />,
title: 'Paciente',
@@ -145,7 +168,7 @@ function resolveRoute(pathname, navigate) {
if (pathname === '/laudos') {
return {
element: <ReportsPage navigate={navigate} />,
title: 'Relatorios medicos',
title: 'Relatórios médicos',
withShell: true,
}
}
@@ -174,6 +197,14 @@ function resolveRoute(pathname, navigate) {
}
}
if (pathname === '/usuarios') {
return {
element: <UsersPage role={role} />,
title: 'Usuários',
withShell: true,
}
}
if (pathname === '/perfil') {
return {
element: <ProfilePage navigate={navigate} />,
@@ -192,7 +223,7 @@ function resolveRoute(pathname, navigate) {
return {
element: <NotFoundPage navigate={navigate} />,
title: 'Tela nao encontrada',
title: 'Página não encontrada',
withShell: true,
}
}
@@ -204,7 +235,8 @@ function PatientDetailRoute({ navigate, patientId }) {
useEffect(() => {
let active = true
patientRepository.getById(patientId)
patientRepository
.getById(patientId)
.then((data) => {
if (active) setPatient(data)
})
@@ -221,7 +253,30 @@ function PatientDetailRoute({ navigate, patientId }) {
return <div className="pt-10 text-sm text-[#a3a3a3]">Carregando paciente...</div>
}
return patient ? <PatientDetailPage navigate={navigate} patient={patient} /> : <NotFoundPage navigate={navigate} />
return patient ? (
<PatientDetailPage navigate={navigate} patient={patient} />
) : (
<NotFoundPage navigate={navigate} />
)
}
function UnauthorizedPage({ navigate }) {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<p className="text-5xl">🔒</p>
<h1 className="mt-4 text-2xl font-bold text-[#e5e5e5]">Acesso não permitido</h1>
<p className="mt-2 text-sm text-[#a3a3a3]">
Você não tem permissão para acessar esta página.
</p>
<button
className="mt-6 rounded-lg bg-[#3b82f6] px-5 py-2.5 text-sm font-medium text-white transition hover:bg-[#2563eb]"
onClick={() => navigate('/inicio')}
type="button"
>
Voltar ao painel
</button>
</div>
)
}
function readLocation() {

View File

@@ -1,9 +1,11 @@
import { useEffect, useMemo, useState } from 'react'
import { ROLE_LABELS, ROLE_NAV_ITEMS } from '../config/permissions.js'
import { profileRepository } from '../repositories/profileRepository.js'
import { BrandLogo } from './Brand.jsx'
const navItems = [
// Todos os itens de navegação com seus ícones e metadados
const ALL_NAV_ITEMS = [
{ href: '/inicio', label: 'Painel', icon: 'pulse', activePaths: ['/inicio', '/home', '/dashboard'] },
{ href: '/agenda', label: 'Agenda', icon: 'calendar' },
{ href: '/pacientes', label: 'Pacientes', icon: 'users', exact: true },
@@ -16,6 +18,8 @@ const navItems = [
activePaths: ['/camunicacao', '/comunicacao', '/mensagens'],
},
{ href: '/relatorios', label: 'Relatorios', icon: 'chart' },
{ href: '/profissionais', label: 'Profissionais', icon: 'users' },
{ href: '/usuarios', label: 'Usuarios', icon: 'shield' },
{ href: '/configuracoes', label: 'Configuracoes', icon: 'settings', activePaths: ['/configuracoes', '/config'] },
]
@@ -36,9 +40,10 @@ const titles = {
'/perfil': 'Perfil',
'/configuracoes': 'Configuracoes',
'/config': 'Configuracoes',
'/usuarios': 'Usuarios',
}
export function AppShell({ children, currentPath, navigate, routeTitle }) {
export function AppShell({ children, currentPath, navigate, role, routeTitle }) {
const [menuOpen, setMenuOpen] = useState(false)
const [quickSearch, setQuickSearch] = useState('')
const [viewerProfile, setViewerProfile] = useState({ name: 'Usuario', role: 'Usuario do Sistema' })
@@ -51,24 +56,46 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) {
return routeTitle || titles[currentPath] || 'MediConnect'
}, [currentPath, routeTitle])
// Filtra os itens de navegação com base no role do usuário
const navItems = useMemo(() => {
if (!role) return []
const allowedPaths = ROLE_NAV_ITEMS[role]?.map((item) => item.path) ?? []
return ALL_NAV_ITEMS.filter((item) =>
allowedPaths.some(
(allowed) => item.href === allowed || item.activePaths?.includes(allowed),
),
)
}, [role])
useEffect(() => {
let active = true
profileRepository.getCurrentUserProfile()
profileRepository
.getCurrentUserProfile()
.then((profile) => {
if (!active || !profile) return
setViewerProfile({
name: profile.name || 'Usuario',
role: profile.role || 'Usuario do Sistema',
role: ROLE_LABELS[role] || profile.role || 'Usuario do Sistema',
})
})
.catch(() => {})
.catch(() => {
// Fallback: usa o label do role diretamente
if (active && role) {
setViewerProfile((prev) => ({
...prev,
role: ROLE_LABELS[role] || 'Usuario do Sistema',
}))
}
})
return () => {
active = false
}
}, [])
}, [role])
function goTo(path) {
setMenuOpen(false)
@@ -303,10 +330,10 @@ function AppIcon({ className = 'size-5', name }) {
)
}
if (name === 'dollar') {
if (name === 'shield') {
return (
<svg {...common}>
<path d="M12 2v20M17 6.5C15.8 5.4 14.2 5 12.5 5 9.9 5 8 6.2 8 8s1.6 2.7 4.2 3.3C15 12 17 13 17 15.5S14.8 19 12 19c-2 0-3.8-.6-5-1.8" />
<path d="M12 3 5 6v5c0 4.5 3 8.5 7 10 4-1.5 7-5.5 7-10V6l-7-3Z" />
</svg>
)
}

169
src/config/permissions.js Normal file
View File

@@ -0,0 +1,169 @@
// Roles disponíveis no sistema
export const ROLES = {
ADMIN: 'admin',
GESTOR: 'gestor',
MEDICO: 'medico',
SECRETARIA: 'secretaria',
PACIENTE: 'paciente',
}
// Rotas permitidas por role ('*' = todas)
const ROLE_ROUTES = {
admin: '*',
gestor: [
'/inicio', '/home', '/dashboard',
'/agenda',
'/pacientes',
'/prontuario',
'/laudos',
'/relatorios',
'/comunicacao', '/mensagens', '/camunicacao',
'/profissionais',
'/configuracoes', '/config',
'/consultas',
'/usuarios',
'/perfil',
],
medico: [
'/inicio', '/home', '/dashboard',
'/agenda',
'/prontuario',
'/laudos',
'/comunicacao', '/mensagens', '/camunicacao',
'/relatorios',
'/perfil',
],
secretaria: [
'/inicio', '/home', '/dashboard',
'/agenda',
'/pacientes',
'/comunicacao', '/mensagens', '/camunicacao',
'/perfil',
],
paciente: [
'/inicio', '/home', '/dashboard',
'/perfil',
],
}
// Capacidades especiais por role
export const ROLE_CAPABILITIES = {
admin: {
manageUsers: true,
hardDeletePatients: true,
accessSettings: true,
ownAppointmentsOnly: false,
canEditPatients: true,
canViewReports: true,
canViewMedicalRecords: true,
},
gestor: {
manageUsers: true,
hardDeletePatients: true,
accessSettings: true,
ownAppointmentsOnly: false,
canEditPatients: true,
canViewReports: true,
canViewMedicalRecords: true,
},
medico: {
manageUsers: false,
hardDeletePatients: false,
accessSettings: false,
ownAppointmentsOnly: true,
canEditPatients: false,
canViewReports: true,
canViewMedicalRecords: true,
},
secretaria: {
manageUsers: false,
hardDeletePatients: false,
accessSettings: false,
ownAppointmentsOnly: false,
canEditPatients: true,
canViewReports: false,
canViewMedicalRecords: false,
},
paciente: {
manageUsers: false,
hardDeletePatients: false,
accessSettings: false,
ownAppointmentsOnly: false,
canEditPatients: false,
canViewReports: false,
canViewMedicalRecords: false,
},
}
// Itens do menu por role (para o AppShell)
export const ROLE_NAV_ITEMS = {
admin: [
{ path: '/inicio', label: 'Painel' },
{ path: '/agenda', label: 'Agenda' },
{ path: '/pacientes', label: 'Pacientes' },
{ path: '/prontuario', label: 'Prontuário' },
{ path: '/laudos', label: 'Laudos' },
{ path: '/relatorios', label: 'Relatórios' },
{ path: '/comunicacao', label: 'Comunicação' },
{ path: '/profissionais', label: 'Profissionais' },
{ path: '/usuarios', label: 'Usuários' },
{ path: '/configuracoes', label: 'Configurações' },
],
gestor: [
{ path: '/inicio', label: 'Painel' },
{ path: '/agenda', label: 'Agenda' },
{ path: '/pacientes', label: 'Pacientes' },
{ path: '/prontuario', label: 'Prontuário' },
{ path: '/laudos', label: 'Laudos' },
{ path: '/relatorios', label: 'Relatórios' },
{ path: '/comunicacao', label: 'Comunicação' },
{ path: '/profissionais', label: 'Profissionais' },
{ path: '/usuarios', label: 'Usuários' },
{ path: '/configuracoes', label: 'Configurações' },
],
medico: [
{ path: '/inicio', label: 'Painel' },
{ path: '/agenda', label: 'Agenda' },
{ path: '/prontuario', label: 'Prontuário' },
{ path: '/laudos', label: 'Laudos' },
{ path: '/comunicacao', label: 'Comunicação' },
{ path: '/relatorios', label: 'Relatórios' },
],
secretaria: [
{ path: '/inicio', label: 'Painel' },
{ path: '/agenda', label: 'Agenda' },
{ path: '/pacientes', label: 'Pacientes' },
{ path: '/comunicacao', label: 'Comunicação' },
],
paciente: [
{ path: '/inicio', label: 'Painel' },
],
}
// Verifica se um role pode acessar uma rota
export function canAccess(role, pathname) {
if (!role) return false
const allowed = ROLE_ROUTES[role]
if (allowed === '*') return true
return allowed.some((route) => pathname === route || pathname.startsWith(route + '/'))
}
// Verifica se um role tem uma capacidade específica
export function hasCapability(role, capability) {
return ROLE_CAPABILITIES[role]?.[capability] ?? false
}
// Rótulos amigáveis para cada role
export const ROLE_LABELS = {
admin: 'Administrador',
gestor: 'Gestão / Coordenação',
medico: 'Médico',
secretaria: 'Secretária',
paciente: 'Paciente',
}
// Roles que um gestor pode criar
export const GESTOR_CREATABLE_ROLES = ['medico', 'secretaria', 'paciente']
// Roles que um admin pode criar
export const ADMIN_CREATABLE_ROLES = ['admin', 'gestor', 'medico', 'secretaria', 'paciente']

56
src/hooks/useAuth.js Normal file
View File

@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react'
import { getAuthSession, saveAuthSession } from '../config/api.js'
import { authRepository } from '../repositories/authRepository.js'
export function useAuth() {
const [state, setState] = useState(() => {
const session = getAuthSession()
return {
user: session?.user ?? null,
role: session?.role ?? null,
profile: session?.profile ?? null,
isAuthenticated: !!session?.access_token,
loading: !!session?.access_token && !session?.role,
}
})
useEffect(() => {
// Se não está autenticado ou já tem role salvo, não busca
if (!state.isAuthenticated || state.role) {
setState((s) => ({ ...s, loading: false }))
return
}
let cancelled = false
authRepository
.getUser()
.then((data) => {
if (cancelled || !data) return
// Suporta diferentes formatos de resposta da API
const role =
Array.isArray(data.roles) ? data.roles[0]
: (data.role ?? data.user_metadata?.role ?? data.app_metadata?.role ?? null)
const profile = data.profile ?? null
const user = data.user ?? data ?? null
// Persiste na sessão para evitar nova busca a cada reload
const session = getAuthSession()
saveAuthSession({ ...session, role, profile, user: user || session?.user })
setState((s) => ({ ...s, role, profile, user: user || s.user, loading: false }))
})
.catch(() => {
if (!cancelled) setState((s) => ({ ...s, loading: false }))
})
return () => {
cancelled = true
}
}, [state.isAuthenticated, state.role])
return state
}

316
src/pages/UsersPage.jsx Normal file
View File

@@ -0,0 +1,316 @@
import { useEffect, useState } from 'react'
import { ADMIN_CREATABLE_ROLES, GESTOR_CREATABLE_ROLES, ROLE_LABELS } from '../config/permissions.js'
import { userRepository } from '../repositories/userRepository.js'
const darkInput =
'h-10 w-full rounded-lg border border-[#404040] bg-[#1a1a1a] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]'
const darkLabel = 'mb-1.5 block text-xs font-medium text-[#e5e5e5]'
export function UsersPage({ role: currentRole }) {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [modalOpen, setModalOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [deletingId, setDeletingId] = useState(null)
const [form, setForm] = useState({
email: '',
full_name: '',
role: '',
create_patient_record: false,
cpf: '',
phone_mobile: '',
})
const creatableRoles = currentRole === 'admin' ? ADMIN_CREATABLE_ROLES : GESTOR_CREATABLE_ROLES
useEffect(() => {
loadUsers()
}, [])
async function loadUsers() {
setLoading(true)
setError(null)
try {
const data = await userRepository.getAll()
setUsers(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
function handleFormChange(event) {
const { checked, name, type, value } = event.target
setForm((current) => ({ ...current, [name]: type === 'checkbox' ? checked : value }))
}
async function handleCreate(event) {
event.preventDefault()
if (!form.email || !form.full_name || !form.role) {
window.alert('Preencha email, nome completo e perfil.')
return
}
setSaving(true)
try {
const result = await userRepository.create(form)
console.log('Usuário criado:', result)
window.alert(`Usuário criado! Magic Link enviado para ${form.email}.`)
setModalOpen(false)
setForm({ email: '', full_name: '', role: '', create_patient_record: false, cpf: '', phone_mobile: '' })
loadUsers()
} catch (err) {
console.error('Erro completo:', err)
window.alert(`Erro ao criar usuário: ${err.message}`)
} finally {
setSaving(false)
}
}
async function handleDelete(user) {
const confirmed = window.confirm(
`⚠️ ATENÇÃO: Esta operação é IRREVERSÍVEL!\n\nO usuário "${user.full_name || user.email}" e TODOS os dados relacionados (perfil, agendamentos, registros) serão deletados permanentemente.\n\nDeseja continuar?`
)
if (!confirmed) return
setDeletingId(user.id)
try {
await userRepository.remove(user.id)
setUsers((current) => current.filter((u) => u.id !== user.id))
} catch (err) {
window.alert(`Erro ao deletar usuário: ${err.message}`)
} finally {
setDeletingId(null)
}
}
return (
<div className="mx-auto max-w-7xl space-y-6 text-[#e5e5e5]">
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
<div>
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Usuários do Sistema</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie os usuários e seus perfis de acesso</p>
</div>
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white shadow-sm transition hover:bg-[#2563eb] md:w-auto"
onClick={() => setModalOpen(true)}
type="button"
>
+ Novo usuário
</button>
</div>
{loading ? (
<p className="py-10 text-center text-sm text-[#a3a3a3]">Carregando usuários...</p>
) : error ? (
<p className="py-10 text-center text-sm text-red-400">Erro ao carregar usuários: {error}</p>
) : (
<div className="rounded-2xl border border-[#404040] bg-[#262626] shadow-sm">
<div className="overflow-x-auto">
<table className="w-full whitespace-nowrap text-left text-sm">
<thead className="bg-[#171717] text-xs font-semibold uppercase text-[#a3a3a3]">
<tr>
<th className="px-6 py-4">Nome</th>
<th className="px-6 py-4">Email</th>
<th className="px-6 py-4">Perfil</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4 text-right">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-[#404040]">
{users.length ? (
users.map((user) => {
const userRole = Array.isArray(user.roles) ? user.roles[0] : (user.role ?? '—')
return (
<tr className="transition hover:bg-[#303030]" key={user.id}>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<span className="grid size-8 place-items-center rounded-full bg-[#333333] text-xs font-bold text-[#3b82f6]">
{(user.full_name || user.email || '?').charAt(0).toUpperCase()}
</span>
<span className="font-medium text-[#e5e5e5]">{user.full_name || '—'}</span>
</div>
</td>
<td className="px-6 py-4 text-[#a3a3a3]">{user.email}</td>
<td className="px-6 py-4">
<RoleBadge role={userRole} />
</td>
<td className="px-6 py-4">
<span className={`rounded-full px-2.5 py-1 text-xs font-semibold ${
user.email_confirmed_at
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-amber-500/20 text-amber-400'
}`}>
{user.email_confirmed_at ? 'Ativo' : 'Pendente'}
</span>
</td>
<td className="px-6 py-4 text-right">
<button
className="rounded-lg border border-[#ef4444]/30 bg-[#ef4444]/10 px-3 py-1.5 text-xs font-semibold text-[#ef4444] transition hover:bg-[#ef4444]/20 disabled:opacity-50"
disabled={deletingId === user.id}
onClick={() => handleDelete(user)}
type="button"
>
{deletingId === user.id ? 'Deletando...' : 'Deletar'}
</button>
</td>
</tr>
)
})
) : (
<tr>
<td className="px-6 py-10 text-center text-[#a3a3a3]" colSpan={5}>
Nenhum usuário encontrado.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{modalOpen ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={() => setModalOpen(false)}>
<div
className="w-full max-w-lg rounded-2xl border border-[#404040] bg-[#262626] p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-[#e5e5e5]">Novo Usuário</h2>
<p className="mt-1 text-xs text-[#a3a3a3]">Um Magic Link será enviado para o email cadastrado.</p>
</div>
<button
className="rounded p-1 text-[#a3a3a3] transition hover:bg-[#333333]"
onClick={() => setModalOpen(false)}
type="button"
>
</button>
</div>
<form className="space-y-4" onSubmit={handleCreate}>
<div>
<label className={darkLabel}>Nome completo *</label>
<input
className={darkInput}
name="full_name"
onChange={handleFormChange}
placeholder="Ex: João da Silva"
required
value={form.full_name}
/>
</div>
<div>
<label className={darkLabel}>Email *</label>
<input
className={darkInput}
name="email"
onChange={handleFormChange}
placeholder="email@exemplo.com"
required
type="email"
value={form.email}
/>
</div>
<div>
<label className={darkLabel}>Perfil de acesso *</label>
<select
className={darkInput}
name="role"
onChange={handleFormChange}
required
value={form.role}
>
<option value="">Selecione um perfil</option>
{creatableRoles.map((r) => (
<option key={`role-option-${r}`} value={r}>{ROLE_LABELS[r]}</option>
))}
</select>
</div>
<label className="flex cursor-pointer items-center gap-2 text-sm text-[#e5e5e5]">
<input
checked={form.create_patient_record}
className="size-4 accent-[#3b82f6]"
name="create_patient_record"
onChange={handleFormChange}
type="checkbox"
/>
Criar também um registro de paciente
</label>
{form.create_patient_record ? (
<div className="grid gap-4 rounded-lg border border-[#404040] bg-[#1a1a1a] p-4 sm:grid-cols-2">
<div>
<label className={darkLabel}>CPF *</label>
<input
className={darkInput}
maxLength={14}
name="cpf"
onChange={handleFormChange}
placeholder="000.000.000-00"
required={form.create_patient_record}
value={form.cpf}
/>
</div>
<div>
<label className={darkLabel}>Celular *</label>
<input
className={darkInput}
maxLength={15}
name="phone_mobile"
onChange={handleFormChange}
placeholder="(00) 00000-0000"
required={form.create_patient_record}
value={form.phone_mobile}
/>
</div>
</div>
) : null}
<div className="flex justify-end gap-3 pt-2">
<button
className="rounded-lg border border-[#404040] bg-[#262626] px-4 py-2 text-sm font-medium text-[#e5e5e5] transition hover:bg-[#333333]"
disabled={saving}
onClick={() => setModalOpen(false)}
type="button"
>
Cancelar
</button>
<button
className="rounded-lg bg-[#3b82f6] px-4 py-2 text-sm font-medium text-white transition hover:bg-[#2563eb] disabled:opacity-60"
disabled={saving}
type="submit"
>
{saving ? 'Criando...' : 'Criar e enviar Magic Link'}
</button>
</div>
</form>
</div>
</div>
) : null}
</div>
)
}
function RoleBadge({ role }) {
const styles = {
admin: 'bg-purple-500/20 text-purple-400',
gestor: 'bg-blue-500/20 text-blue-400',
medico: 'bg-emerald-500/20 text-emerald-400',
secretaria: 'bg-amber-500/20 text-amber-400',
paciente: 'bg-[#303030] text-[#a3a3a3]',
}
return (
<span className={`rounded-full px-2.5 py-1 text-xs font-semibold ${styles[role] || styles.paciente}`}>
{ROLE_LABELS[role] || role}
</span>
)
}

View File

@@ -0,0 +1,72 @@
import { apiConfig, getAuthenticatedHeaders } from '../config/api.js'
export const userRepository = {
// Listar todos os usuários (admin/gestor)
async getAll() {
const response = await fetch(`${apiConfig.functionsUrl}/user-info`, {
method: 'POST',
headers: getAuthenticatedHeaders(),
})
if (!response.ok) throw new Error('Erro ao listar usuários')
const data = await response.json()
console.log('Resposta de user-info:', data)
return Array.isArray(data) ? data : (data.users ?? [data])
},
// Buscar usuário por ID (admin/gestor)
async getById(userId) {
const response = await fetch(`${apiConfig.functionsUrl}/user-info-by-id`, {
method: 'POST',
headers: getAuthenticatedHeaders(),
body: JSON.stringify({ user_id: userId }),
})
if (!response.ok) throw new Error('Erro ao buscar usuário')
return response.json()
},
// Criar usuário com magic link (admin/gestor/secretaria)
async create(data) {
const body = {
email: data.email,
full_name: data.full_name,
role: data.role,
}
if (data.create_patient_record) {
body.create_patient_record = true
body.cpf = data.cpf
body.phone_mobile = data.phone_mobile
}
console.log('Enviando para create-user:', body)
const response = await fetch(`${apiConfig.functionsUrl}/create-user`, {
method: 'POST',
headers: getAuthenticatedHeaders(),
body: JSON.stringify(body),
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
console.error('Resposta de erro da API:', error)
throw new Error(error.message || error.error || error.msg || JSON.stringify(error))
}
return response.json()
},
// Deletar usuário permanentemente — IRREVERSÍVEL (admin/gestor)
async remove(userId) {
const response = await fetch(`${apiConfig.functionsUrl}/delete-user`, {
method: 'POST',
headers: getAuthenticatedHeaders(),
body: JSON.stringify({ user_id: userId }),
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.message || 'Erro ao deletar usuário')
}
return true
},
}