All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
622 lines
26 KiB
TypeScript
622 lines
26 KiB
TypeScript
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
|
import {
|
|
AtSign,
|
|
Check,
|
|
Copy,
|
|
Edit3,
|
|
KeyRound,
|
|
Loader2,
|
|
Mail,
|
|
Plus,
|
|
Search,
|
|
Shield,
|
|
Trash2,
|
|
User as UserIcon,
|
|
X
|
|
} from 'lucide-react';
|
|
import { createUser, deleteUser as deleteManagedUser, fetchUsers, updateUser } from '../dataService';
|
|
import type { ManagedUser } from '../types';
|
|
|
|
type StatusFilter = 'all' | 'active' | 'inactive';
|
|
type PasswordMode = 'auto' | 'manual';
|
|
type Notice = {
|
|
tone: 'success' | 'warning' | 'error';
|
|
text: string;
|
|
temporaryPassword?: string;
|
|
};
|
|
|
|
const statusOptions: Array<{ key: StatusFilter; label: string }> = [
|
|
{ key: 'all', label: 'Todos' },
|
|
{ key: 'active', label: 'Ativos' },
|
|
{ key: 'inactive', label: 'Inativos' }
|
|
];
|
|
|
|
const formatDateTime = (value: string) => {
|
|
const date = new Date(value);
|
|
if (!value || Number.isNaN(date.getTime())) return '-';
|
|
|
|
return new Intl.DateTimeFormat('pt-BR', {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}).format(date);
|
|
};
|
|
|
|
const getInitials = (name: string) => {
|
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
if (!parts.length) return '?';
|
|
return parts.slice(0, 2).map((part) => part[0]).join('').toUpperCase();
|
|
};
|
|
|
|
const AdminUsers = () => {
|
|
const [users, setUsers] = useState<ManagedUser[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [editingUser, setEditingUser] = useState<ManagedUser | null>(null);
|
|
const [userToDelete, setUserToDelete] = useState<ManagedUser | null>(null);
|
|
const [passwordMode, setPasswordMode] = useState<PasswordMode>('auto');
|
|
const [isActive, setIsActive] = useState(true);
|
|
const [name, setName] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [notice, setNotice] = useState<Notice | null>(null);
|
|
const [copyLabel, setCopyLabel] = useState('Copiar');
|
|
|
|
const activeCount = useMemo(() => users.filter((user) => user.isActive).length, [users]);
|
|
const inactiveCount = users.length - activeCount;
|
|
|
|
const filteredUsers = useMemo(() => {
|
|
const query = searchTerm.trim().toLowerCase();
|
|
|
|
return users.filter((user) => {
|
|
const matchesStatus =
|
|
statusFilter === 'all' ||
|
|
(statusFilter === 'active' && user.isActive) ||
|
|
(statusFilter === 'inactive' && !user.isActive);
|
|
const matchesQuery =
|
|
!query ||
|
|
user.name.toLowerCase().includes(query) ||
|
|
user.email.toLowerCase().includes(query);
|
|
|
|
return matchesStatus && matchesQuery;
|
|
});
|
|
}, [searchTerm, statusFilter, users]);
|
|
|
|
const loadUsers = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
setUsers(await fetchUsers());
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
// User management needs an initial API load when the admin page opens.
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
void loadUsers();
|
|
}, []);
|
|
|
|
const resetForm = () => {
|
|
setName('');
|
|
setEmail('');
|
|
setPassword('');
|
|
setPasswordMode('auto');
|
|
setIsActive(true);
|
|
setEditingUser(null);
|
|
setNotice(null);
|
|
setCopyLabel('Copiar');
|
|
};
|
|
|
|
const closeModal = () => {
|
|
if (isSubmitting) return;
|
|
setIsModalOpen(false);
|
|
resetForm();
|
|
};
|
|
|
|
const openCreateModal = () => {
|
|
resetForm();
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const openEditModal = (user: ManagedUser) => {
|
|
setEditingUser(user);
|
|
setName(user.name);
|
|
setEmail(user.email);
|
|
setIsActive(user.isActive);
|
|
setPassword('');
|
|
setPasswordMode('auto');
|
|
setNotice(null);
|
|
setCopyLabel('Copiar');
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleCreateUser = async (event: FormEvent) => {
|
|
event.preventDefault();
|
|
setIsSubmitting(true);
|
|
setNotice(null);
|
|
setCopyLabel('Copiar');
|
|
|
|
try {
|
|
const result = await createUser({
|
|
name,
|
|
email,
|
|
password: passwordMode === 'manual' ? password : undefined
|
|
});
|
|
|
|
setUsers((currentUsers) => [result.user, ...currentUsers]);
|
|
|
|
if (result.temporaryPassword) {
|
|
setNotice({
|
|
tone: 'success',
|
|
text: 'Acesso criado. Copie a senha gerada antes de fechar.',
|
|
temporaryPassword: result.temporaryPassword
|
|
});
|
|
setName('');
|
|
setEmail('');
|
|
setPassword('');
|
|
setPasswordMode('auto');
|
|
} else {
|
|
setNotice({
|
|
tone: 'success',
|
|
text: 'Acesso criado com a senha definida.'
|
|
});
|
|
window.setTimeout(() => {
|
|
setIsModalOpen(false);
|
|
resetForm();
|
|
}, 900);
|
|
}
|
|
} catch (error) {
|
|
setNotice({
|
|
tone: 'error',
|
|
text: error instanceof Error ? error.message : 'Não foi possível criar o acesso.'
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleUpdateUser = async (event: FormEvent) => {
|
|
event.preventDefault();
|
|
if (!editingUser) return;
|
|
|
|
setIsSubmitting(true);
|
|
setNotice(null);
|
|
|
|
try {
|
|
const updatedUser = await updateUser(editingUser.id, {
|
|
name,
|
|
email,
|
|
isActive,
|
|
password: passwordMode === 'manual' ? password : undefined
|
|
});
|
|
|
|
setUsers((currentUsers) => currentUsers.map((user) => user.id === updatedUser.id ? updatedUser : user));
|
|
setNotice({
|
|
tone: 'success',
|
|
text: 'Usuário atualizado.'
|
|
});
|
|
window.setTimeout(() => {
|
|
setIsModalOpen(false);
|
|
resetForm();
|
|
}, 700);
|
|
} catch (error) {
|
|
setNotice({
|
|
tone: 'error',
|
|
text: error instanceof Error ? error.message : 'Não foi possível editar o usuário.'
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteUser = async () => {
|
|
if (!userToDelete) return;
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
await deleteManagedUser(userToDelete.id);
|
|
setUsers((currentUsers) => currentUsers.filter((user) => user.id !== userToDelete.id));
|
|
setUserToDelete(null);
|
|
} catch (error) {
|
|
setNotice({
|
|
tone: 'error',
|
|
text: error instanceof Error ? error.message : 'Não foi possível excluir o usuário.'
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const copyTemporaryPassword = async () => {
|
|
if (!notice?.temporaryPassword) return;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(notice.temporaryPassword);
|
|
setCopyLabel('Copiado');
|
|
window.setTimeout(() => setCopyLabel('Copiar'), 1600);
|
|
} catch {
|
|
setCopyLabel('Falhou');
|
|
}
|
|
};
|
|
|
|
const noticeClass = notice?.tone === 'error'
|
|
? 'border-red-500/30 bg-red-500/10 text-red-300'
|
|
: notice?.tone === 'warning'
|
|
? 'border-amber-500/30 bg-amber-500/10 text-amber-200'
|
|
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300';
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<header className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-brand-primary">
|
|
<Shield className="h-4 w-4" />
|
|
Super admin
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-dark-text">Usuários</h1>
|
|
<p className="mt-1 text-sm text-dark-muted">Controle quem acessa o painel.</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={openCreateModal}
|
|
className="inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Novo usuário
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section className="rounded-lg border border-dark-border bg-dark-card">
|
|
<div className="flex flex-col gap-4 border-b border-dark-border p-4 lg:flex-row lg:items-center lg:justify-between">
|
|
<div className="grid grid-cols-3 divide-x divide-dark-border overflow-hidden rounded-lg border border-dark-border bg-dark-input lg:w-[360px]">
|
|
<div className="px-4 py-3">
|
|
<p className="text-xs font-semibold text-dark-muted">Total</p>
|
|
<p className="mt-1 text-xl font-bold text-dark-text">{users.length}</p>
|
|
</div>
|
|
<div className="px-4 py-3">
|
|
<p className="text-xs font-semibold text-dark-muted">Ativos</p>
|
|
<p className="mt-1 text-xl font-bold text-emerald-300">{activeCount}</p>
|
|
</div>
|
|
<div className="px-4 py-3">
|
|
<p className="text-xs font-semibold text-dark-muted">Inativos</p>
|
|
<p className="mt-1 text-xl font-bold text-red-300">{inactiveCount}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
|
<div className="flex h-10 min-w-0 items-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 md:w-80">
|
|
<Search className="h-4 w-4 shrink-0 text-dark-muted" />
|
|
<input
|
|
type="search"
|
|
value={searchTerm}
|
|
onChange={(event) => setSearchTerm(event.target.value)}
|
|
className="min-w-0 flex-1 bg-transparent text-sm text-dark-text outline-none placeholder:text-dark-muted"
|
|
placeholder="Buscar nome ou e-mail"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 rounded-lg border border-dark-border bg-dark-input p-1">
|
|
{statusOptions.map((option) => (
|
|
<button
|
|
key={option.key}
|
|
type="button"
|
|
onClick={() => setStatusFilter(option.key)}
|
|
className={`h-8 cursor-pointer rounded-md px-3 text-sm font-semibold transition-colors ${
|
|
statusFilter === option.key
|
|
? 'bg-dark-card text-dark-text shadow-sm'
|
|
: 'text-dark-muted hover:text-dark-text'
|
|
}`}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex h-80 items-center justify-center text-brand-primary">
|
|
<Loader2 className="h-7 w-7 animate-spin" />
|
|
</div>
|
|
) : users.length === 0 ? (
|
|
<div className="flex h-80 flex-col items-center justify-center px-6 text-center">
|
|
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-dark-input text-dark-muted">
|
|
<UserIcon className="h-6 w-6" />
|
|
</div>
|
|
<p className="text-base font-bold text-dark-text">Nenhum usuário cadastrado</p>
|
|
<p className="mt-1 max-w-sm text-sm text-dark-muted">Crie o primeiro acesso para liberar o painel a outra pessoa.</p>
|
|
</div>
|
|
) : filteredUsers.length === 0 ? (
|
|
<div className="flex h-80 flex-col items-center justify-center px-6 text-center">
|
|
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-dark-input text-dark-muted">
|
|
<Search className="h-6 w-6" />
|
|
</div>
|
|
<p className="text-base font-bold text-dark-text">Nenhum resultado</p>
|
|
<p className="mt-1 text-sm text-dark-muted">Ajuste a busca ou limpe os filtros.</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full min-w-[760px] text-left">
|
|
<thead className="border-b border-dark-border text-xs uppercase tracking-widest text-dark-muted">
|
|
<tr>
|
|
<th className="px-5 py-3 font-bold">Usuário</th>
|
|
<th className="px-5 py-3 font-bold">E-mail</th>
|
|
<th className="px-5 py-3 font-bold">Status</th>
|
|
<th className="px-5 py-3 text-right font-bold">Criado em</th>
|
|
<th className="px-5 py-3 text-right font-bold">Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-dark-border">
|
|
{filteredUsers.map((user) => (
|
|
<tr key={user.id} className="text-sm transition-colors hover:bg-dark-input/45">
|
|
<td className="px-5 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-brand-primary/10 text-sm font-bold text-brand-primary">
|
|
{getInitials(user.name)}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="truncate font-semibold text-dark-text">{user.name}</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-5 py-4">
|
|
<div className="flex min-w-0 items-center gap-2 text-dark-muted">
|
|
<Mail className="h-4 w-4 shrink-0" />
|
|
<span className="truncate">{user.email}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-5 py-4">
|
|
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold ${
|
|
user.isActive ? 'bg-emerald-500/10 text-emerald-300' : 'bg-red-500/10 text-red-300'
|
|
}`}>
|
|
<span className={`h-1.5 w-1.5 rounded-full ${user.isActive ? 'bg-emerald-300' : 'bg-red-300'}`} />
|
|
{user.isActive ? 'Ativo' : 'Inativo'}
|
|
</span>
|
|
</td>
|
|
<td className="px-5 py-4 text-right text-dark-muted">{formatDateTime(user.createdAt)}</td>
|
|
<td className="px-5 py-4">
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => openEditModal(user)}
|
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-dark-border text-dark-muted transition-colors hover:border-brand-primary/50 hover:text-brand-primary"
|
|
aria-label={`Editar ${user.name}`}
|
|
title="Editar"
|
|
>
|
|
<Edit3 className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setNotice(null);
|
|
setUserToDelete(user);
|
|
}}
|
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-dark-border text-dark-muted transition-colors hover:border-red-400/50 hover:text-red-300"
|
|
aria-label={`Excluir ${user.name}`}
|
|
title="Excluir"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{isModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 py-6">
|
|
<div className="w-full max-w-lg overflow-hidden rounded-lg border border-dark-border bg-dark-card shadow-2xl">
|
|
<div className="flex items-start justify-between border-b border-dark-border px-5 py-4">
|
|
<div>
|
|
<h2 className="text-lg font-bold text-dark-text">{editingUser ? 'Editar usuário' : 'Novo usuário'}</h2>
|
|
<p className="mt-1 text-sm text-dark-muted">
|
|
{editingUser ? 'Atualize dados, status e senha de acesso.' : 'Crie um acesso individual para o painel.'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={closeModal}
|
|
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg text-dark-muted transition-colors hover:bg-dark-input hover:text-dark-text"
|
|
aria-label="Fechar"
|
|
title="Fechar"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={editingUser ? handleUpdateUser : handleCreateUser} className="space-y-4 p-5">
|
|
<label className="block">
|
|
<span className="mb-2 block text-xs font-bold uppercase tracking-widest text-dark-muted">Nome</span>
|
|
<div className="flex h-11 items-center gap-3 rounded-lg border border-dark-border bg-dark-input px-3 transition-colors focus-within:border-brand-primary focus-within:ring-1 focus-within:ring-brand-primary">
|
|
<UserIcon className="h-4 w-4 shrink-0 text-dark-muted" />
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(event) => setName(event.target.value)}
|
|
className="min-w-0 flex-1 bg-transparent text-dark-text outline-none placeholder:text-dark-muted"
|
|
placeholder="Nome completo"
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<span className="mb-2 block text-xs font-bold uppercase tracking-widest text-dark-muted">E-mail</span>
|
|
<div className="flex h-11 items-center gap-3 rounded-lg border border-dark-border bg-dark-input px-3 transition-colors focus-within:border-brand-primary focus-within:ring-1 focus-within:ring-brand-primary">
|
|
<AtSign className="h-4 w-4 shrink-0 text-dark-muted" />
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(event) => setEmail(event.target.value)}
|
|
className="min-w-0 flex-1 bg-transparent text-dark-text outline-none placeholder:text-dark-muted"
|
|
placeholder="usuario@empresa.com"
|
|
required
|
|
/>
|
|
</div>
|
|
</label>
|
|
|
|
<div>
|
|
<span className="mb-2 block text-xs font-bold uppercase tracking-widest text-dark-muted">{editingUser ? 'Senha' : 'Senha inicial'}</span>
|
|
<div className="grid grid-cols-2 rounded-lg border border-dark-border bg-dark-input p-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setPasswordMode('auto')}
|
|
className={`h-9 cursor-pointer rounded-md text-sm font-semibold transition-colors ${passwordMode === 'auto' ? 'bg-dark-card text-dark-text' : 'text-dark-muted hover:text-dark-text'}`}
|
|
>
|
|
{editingUser ? 'Manter' : 'Gerar'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setPasswordMode('manual')}
|
|
className={`h-9 cursor-pointer rounded-md text-sm font-semibold transition-colors ${passwordMode === 'manual' ? 'bg-dark-card text-dark-text' : 'text-dark-muted hover:text-dark-text'}`}
|
|
>
|
|
{editingUser ? 'Alterar' : 'Definir'}
|
|
</button>
|
|
</div>
|
|
|
|
{passwordMode === 'manual' && (
|
|
<div className="mt-3 flex h-11 items-center gap-3 rounded-lg border border-dark-border bg-dark-input px-3 transition-colors focus-within:border-brand-primary focus-within:ring-1 focus-within:ring-brand-primary">
|
|
<KeyRound className="h-4 w-4 shrink-0 text-dark-muted" />
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
className="min-w-0 flex-1 bg-transparent text-dark-text outline-none placeholder:text-dark-muted"
|
|
placeholder="Mínimo de 6 caracteres"
|
|
minLength={6}
|
|
required
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{editingUser && (
|
|
<label className="flex cursor-pointer items-center justify-between rounded-lg border border-dark-border bg-dark-input px-3 py-3">
|
|
<div>
|
|
<span className="block text-sm font-semibold text-dark-text">Usuário ativo</span>
|
|
<span className="mt-0.5 block text-xs text-dark-muted">Usuários inativos não conseguem entrar.</span>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
checked={isActive}
|
|
onChange={(event) => setIsActive(event.target.checked)}
|
|
className="h-5 w-5 cursor-pointer accent-brand-primary"
|
|
/>
|
|
</label>
|
|
)}
|
|
|
|
{notice && (
|
|
<div className={`rounded-lg border px-3 py-3 text-sm ${noticeClass}`}>
|
|
<div className="flex items-start gap-2">
|
|
{notice.tone === 'error' ? <X className="mt-0.5 h-4 w-4 shrink-0" /> : <Check className="mt-0.5 h-4 w-4 shrink-0" />}
|
|
<div className="min-w-0 flex-1">
|
|
<p>{notice.text}</p>
|
|
{notice.temporaryPassword && (
|
|
<div className="mt-3 flex gap-2">
|
|
<code className="min-w-0 flex-1 truncate rounded-md bg-black/25 px-3 py-2 text-amber-100">
|
|
{notice.temporaryPassword}
|
|
</code>
|
|
<button
|
|
type="button"
|
|
onClick={copyTemporaryPassword}
|
|
className="inline-flex h-9 cursor-pointer items-center gap-2 rounded-md border border-amber-400/30 px-3 text-xs font-bold text-amber-100 transition-colors hover:bg-amber-400/10"
|
|
>
|
|
<Copy className="h-3.5 w-3.5" />
|
|
{copyLabel}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
|
|
<button
|
|
type="button"
|
|
onClick={closeModal}
|
|
disabled={isSubmitting}
|
|
className="h-10 cursor-pointer rounded-lg border border-dark-border px-4 text-sm font-semibold text-dark-muted transition-colors hover:text-dark-text disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{isSubmitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
|
{editingUser ? 'Salvar alterações' : 'Criar usuário'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{userToDelete && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 py-6">
|
|
<div className="w-full max-w-md overflow-hidden rounded-lg border border-dark-border bg-dark-card shadow-2xl">
|
|
<div className="border-b border-dark-border px-5 py-4">
|
|
<h2 className="text-lg font-bold text-dark-text">Excluir usuário</h2>
|
|
<p className="mt-1 text-sm text-dark-muted">Esta ação remove o acesso imediatamente.</p>
|
|
</div>
|
|
<div className="space-y-4 p-5">
|
|
<div className="rounded-lg border border-dark-border bg-dark-input p-4">
|
|
<p className="font-semibold text-dark-text">{userToDelete.name}</p>
|
|
<p className="mt-1 text-sm text-dark-muted">{userToDelete.email}</p>
|
|
</div>
|
|
|
|
{notice?.tone === 'error' && (
|
|
<div className={`rounded-lg border px-3 py-3 text-sm ${noticeClass}`}>
|
|
{notice.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (isSubmitting) return;
|
|
setUserToDelete(null);
|
|
setNotice(null);
|
|
}}
|
|
disabled={isSubmitting}
|
|
className="h-10 cursor-pointer rounded-lg border border-dark-border px-4 text-sm font-semibold text-dark-muted transition-colors hover:text-dark-text disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleDeleteUser}
|
|
disabled={isSubmitting}
|
|
className="inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-red-500 px-4 text-sm font-bold text-white transition-colors hover:bg-red-400 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{isSubmitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
|
Excluir
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminUsers;
|