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:
316
src/pages/UsersPage.jsx
Normal file
316
src/pages/UsersPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user