forked from RiseUP/riseup_squad_03
modified: src/App.jsx
modified: src/components/AppShell.jsx modified: src/config/api.js modified: src/config/permissions.js modified: src/data/mockData.js modified: src/hooks/useAgenda.js modified: src/hooks/useAuth.js modified: src/mappers/appointmentMapper.js modified: src/pages/AgendaPage.jsx modified: src/pages/AuthPages.jsx modified: src/pages/HomePage.jsx modified: src/pages/MedicalRecordsPage.jsx modified: src/pages/MessagesPage.jsx modified: src/pages/NotFoundPage.jsx modified: src/pages/PatientsPage.jsx modified: src/pages/ReportsPage.jsx modified: src/pages/TeamPage.jsx modified: src/pages/UsersPage.jsx modified: src/pages/VisitsPage.jsx modified: src/repositories/authRepository.js new file: src/repositories/availabilityRepository.js modified: src/repositories/communicationRepository.js modified: src/repositories/patientRepository.js modified: src/repositories/professionalRepository.js modified: src/repositories/profileRepository.js modified: src/repositories/reportRepository.js modified: src/repositories/repositoryUtils.js modified: src/repositories/settingsRepository.js modified: src/repositories/userRepository.js modified: src/repositories/visitRepository.js
This commit is contained in:
@@ -1,11 +1,34 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { ADMIN_CREATABLE_ROLES, GESTOR_CREATABLE_ROLES, ROLE_LABELS } from '../config/permissions.js'
|
||||
import { ADMIN_CREATABLE_ROLES, GESTOR_CREATABLE_ROLES, hasCapability, normalizeRole, 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]'
|
||||
const authMethodOptions = [
|
||||
{
|
||||
value: 'magic_link',
|
||||
label: 'Magic Link',
|
||||
description: 'Enviar link de acesso por email',
|
||||
},
|
||||
{
|
||||
value: 'password',
|
||||
label: 'Email e senha',
|
||||
description: 'Definir senha inicial agora',
|
||||
},
|
||||
]
|
||||
const initialUserForm = {
|
||||
email: '',
|
||||
full_name: '',
|
||||
phone: '',
|
||||
cpf: '',
|
||||
role: '',
|
||||
auth_method: 'magic_link',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
create_patient_record: false,
|
||||
}
|
||||
|
||||
export function UsersPage({ role: currentRole }) {
|
||||
const [users, setUsers] = useState([])
|
||||
@@ -14,16 +37,12 @@ export function UsersPage({ role: currentRole }) {
|
||||
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 [form, setForm] = useState(initialUserForm)
|
||||
|
||||
const creatableRoles = currentRole === 'admin' ? ADMIN_CREATABLE_ROLES : GESTOR_CREATABLE_ROLES
|
||||
const normalizedRole = normalizeRole(currentRole)
|
||||
const canManageUsers = hasCapability(normalizedRole, 'manageUsers')
|
||||
const creatableRoles = normalizedRole === 'admin' ? ADMIN_CREATABLE_ROLES : GESTOR_CREATABLE_ROLES
|
||||
const isPasswordCreation = form.auth_method === 'password'
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers()
|
||||
@@ -47,29 +66,60 @@ export function UsersPage({ role: currentRole }) {
|
||||
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
|
||||
async function handleCreate(event) {
|
||||
event.preventDefault()
|
||||
if (!canManageUsers) {
|
||||
window.alert('Você não tem permissão para criar usuários.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.email || !form.full_name || !form.phone || !form.cpf || !form.role) {
|
||||
window.alert('Preencha email, nome completo, celular, CPF e perfil.')
|
||||
return
|
||||
}
|
||||
|
||||
if (isPasswordCreation) {
|
||||
if (!form.password || !form.confirm_password) {
|
||||
window.alert('Preencha a senha e a confirmação de senha.')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.password.length < 8) {
|
||||
window.alert('A senha deve ter pelo menos 8 caracteres.')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.password !== form.confirm_password) {
|
||||
window.alert('A confirmação de senha não confere.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
if (isPasswordCreation) {
|
||||
await userRepository.createWithPassword(form)
|
||||
window.alert(`Usuário criado com email e senha para ${form.email}.`)
|
||||
} else {
|
||||
await userRepository.create(form)
|
||||
window.alert(`Usuário criado! Magic Link enviado para ${form.email}.`)
|
||||
}
|
||||
setModalOpen(false)
|
||||
setForm(initialUserForm)
|
||||
loadUsers()
|
||||
} catch (err) {
|
||||
window.alert(`Erro ao criar usuário: ${err.message}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (!canManageUsers) {
|
||||
window.alert('Você não tem permissão para deletar usuários.')
|
||||
return
|
||||
}
|
||||
|
||||
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?`
|
||||
)
|
||||
@@ -86,6 +136,15 @@ async function handleCreate(event) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!canManageUsers) {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl rounded-2xl border border-[#404040] bg-[#262626] p-8 text-center text-[#e5e5e5]">
|
||||
<h1 className="text-xl font-bold">Acesso não permitido</h1>
|
||||
<p className="mt-2 text-sm text-[#a3a3a3]">Somente Administrador e Gestão/Coordenação podem gerenciar usuários.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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">
|
||||
@@ -175,13 +234,15 @@ async function handleCreate(event) {
|
||||
{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"
|
||||
className="max-h-[90vh] w-full max-w-lg overflow-y-auto 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>
|
||||
<p className="mt-1 text-xs text-[#a3a3a3]">
|
||||
{isPasswordCreation ? 'Crie o acesso inicial com email e senha.' : 'Um Magic Link sera enviado para o email cadastrado.'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="rounded p-1 text-[#a3a3a3] transition hover:bg-[#333333]"
|
||||
@@ -193,6 +254,40 @@ async function handleCreate(event) {
|
||||
</div>
|
||||
|
||||
<form className="space-y-4" onSubmit={handleCreate}>
|
||||
<div>
|
||||
<span className={darkLabel}>Criar usuário usando *</span>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{authMethodOptions.map((option) => {
|
||||
const selected = form.auth_method === option.value
|
||||
return (
|
||||
<label
|
||||
className={`cursor-pointer rounded-lg border p-3 transition ${
|
||||
selected
|
||||
? 'border-[#3b82f6] bg-[#3b82f6]/15 text-[#e5e5e5]'
|
||||
: 'border-[#404040] bg-[#1a1a1a] text-[#a3a3a3] hover:border-[#525252] hover:text-[#e5e5e5]'
|
||||
}`}
|
||||
key={option.value}
|
||||
>
|
||||
<span className="flex items-start gap-3">
|
||||
<input
|
||||
checked={selected}
|
||||
className="mt-1 size-4 accent-[#3b82f6]"
|
||||
name="auth_method"
|
||||
onChange={handleFormChange}
|
||||
type="radio"
|
||||
value={option.value}
|
||||
/>
|
||||
<span>
|
||||
<span className="block text-sm font-semibold">{option.label}</span>
|
||||
<span className="mt-1 block text-xs text-[#a3a3a3]">{option.description}</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={darkLabel}>Nome completo *</label>
|
||||
<input
|
||||
@@ -218,6 +313,65 @@ async function handleCreate(event) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={darkLabel}>Celular *</label>
|
||||
<input
|
||||
className={darkInput}
|
||||
maxLength={15}
|
||||
name="phone"
|
||||
onChange={handleFormChange}
|
||||
placeholder="(00) 00000-0000"
|
||||
required
|
||||
value={form.phone}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={darkLabel}>CPF *</label>
|
||||
<input
|
||||
className={darkInput}
|
||||
maxLength={14}
|
||||
name="cpf"
|
||||
onChange={handleFormChange}
|
||||
placeholder="000.000.000-00"
|
||||
required
|
||||
value={form.cpf}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isPasswordCreation ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className={darkLabel}>Senha *</label>
|
||||
<input
|
||||
autoComplete="new-password"
|
||||
className={darkInput}
|
||||
minLength={8}
|
||||
name="password"
|
||||
onChange={handleFormChange}
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
required={isPasswordCreation}
|
||||
type="password"
|
||||
value={form.password}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={darkLabel}>Confirmar senha *</label>
|
||||
<input
|
||||
autoComplete="new-password"
|
||||
className={darkInput}
|
||||
minLength={8}
|
||||
name="confirm_password"
|
||||
onChange={handleFormChange}
|
||||
placeholder="Repita a senha"
|
||||
required={isPasswordCreation}
|
||||
type="password"
|
||||
value={form.confirm_password}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<label className={darkLabel}>Perfil de acesso *</label>
|
||||
<select
|
||||
@@ -245,35 +399,6 @@ async function handleCreate(event) {
|
||||
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]"
|
||||
@@ -288,7 +413,7 @@ async function handleCreate(event) {
|
||||
disabled={saving}
|
||||
type="submit"
|
||||
>
|
||||
{saving ? 'Criando...' : 'Criar e enviar Magic Link'}
|
||||
{saving ? 'Criando...' : isPasswordCreation ? 'Criar com senha' : 'Criar e enviar Magic Link'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user