Files
2026-08-21 22:57:44 +00:00

314 lines
15 KiB
PHP

<?php
$titulo_pagina = 'Usuários';
$pagina_ativa = 'usuarios';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_acesso_total();
$ao_usuario_atual = ao_usuario_logado();
// Colunas pelas quais o grid pode ser ordenado — mapa fixo (não vem direto
// do usuário) para evitar que um valor arbitrário de ?ordenar= vire SQL.
const AO_USUARIOS_COLUNAS_ORDENAVEIS = [
'nome' => 'u.nome',
'email' => 'u.email',
'perfil' => 'p.nome',
'status' => 'u.ativo',
'ultimo_login' => 'u.ultimo_login',
];
const AO_USUARIOS_POR_PAGINA = 10;
$erro = null;
$sucesso = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'excluir') {
$id = (int) ($_POST['id'] ?? 0);
if ($id === (int) $ao_usuario_atual['id']) {
$erro = 'Você não pode excluir o próprio usuário enquanto estiver logado com ele.';
} else {
$pdo->prepare('DELETE FROM usuarios WHERE id = ?')->execute([$id]);
header('Location: usuarios.php?excluido=1');
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') {
$id = (int) ($_POST['id'] ?? 0);
$nome = trim($_POST['nome'] ?? '');
$email = trim($_POST['email'] ?? '');
$senha = (string) ($_POST['senha'] ?? '');
$confirmar_senha = (string) ($_POST['confirmar_senha'] ?? '');
$perfil_id = (int) ($_POST['perfil_id'] ?? 0);
$ativo = isset($_POST['ativo']) ? 1 : 0;
if ($nome === '' || $email === '' || $perfil_id === 0) {
$erro = 'Preencha nome, e-mail e perfil.';
} elseif ($id === 0 && $senha === '') {
$erro = 'Informe uma senha para o novo usuário.';
} elseif ($senha !== '' && $senha !== $confirmar_senha) {
$erro = 'A senha e a confirmação de senha não são iguais.';
} else {
try {
if ($id > 0) {
if ($senha !== '') {
$stmt = $pdo->prepare('UPDATE usuarios SET nome=?, email=?, senha_hash=?, perfil_id=?, ativo=? WHERE id=?');
$stmt->execute([$nome, $email, password_hash($senha, PASSWORD_DEFAULT), $perfil_id, $ativo, $id]);
} else {
$stmt = $pdo->prepare('UPDATE usuarios SET nome=?, email=?, perfil_id=?, ativo=? WHERE id=?');
$stmt->execute([$nome, $email, $perfil_id, $ativo, $id]);
}
} else {
$stmt = $pdo->prepare('INSERT INTO usuarios (nome, email, senha_hash, perfil_id, ativo) VALUES (?,?,?,?,?)');
$stmt->execute([$nome, $email, password_hash($senha, PASSWORD_DEFAULT), $perfil_id, $ativo]);
}
header('Location: usuarios.php?salvo=1');
exit;
} catch (PDOException $e) {
$erro = ($e->getCode() === '23000') ? 'Já existe um usuário com este e-mail.' : ('Erro ao salvar: ' . $e->getMessage());
}
}
}
$editando = null;
if (isset($_GET['editar'])) {
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE id = ?');
$stmt->execute([(int) $_GET['editar']]);
$editando = $stmt->fetch() ?: null;
}
// Se o formulário foi enviado e deu erro, mantemos os dados digitados na
// tela (menos as senhas, por segurança) em vez de limpar tudo.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar' && $erro) {
$editando = [
'id' => $id,
'nome' => $nome,
'email' => $email,
'perfil_id' => $perfil_id,
'ativo' => $ativo,
];
}
if (isset($_GET['salvo'])) $sucesso = 'Usuário salvo com sucesso.';
if (isset($_GET['excluido'])) $sucesso = 'Usuário excluído.';
$perfis = $pdo->query('SELECT id, nome FROM perfis WHERE ativo = 1 ORDER BY nome')->fetchAll();
// Ordenação do grid: valida contra a lista fixa acima antes de montar o SQL.
$ao_ordenar = (string) ($_GET['ordenar'] ?? 'nome');
if (!array_key_exists($ao_ordenar, AO_USUARIOS_COLUNAS_ORDENAVEIS)) {
$ao_ordenar = 'nome';
}
$ao_direcao = strtolower((string) ($_GET['direcao'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
$ao_ordenar_sql = AO_USUARIOS_COLUNAS_ORDENAVEIS[$ao_ordenar] . ' ' . strtoupper($ao_direcao);
// Paginação do grid.
$ao_total_usuarios = (int) $pdo->query('SELECT COUNT(*) FROM usuarios')->fetchColumn();
$ao_total_paginas = max(1, (int) ceil($ao_total_usuarios / AO_USUARIOS_POR_PAGINA));
$ao_pagina_atual = (int) ($_GET['pagina'] ?? 1);
if ($ao_pagina_atual < 1) $ao_pagina_atual = 1;
if ($ao_pagina_atual > $ao_total_paginas) $ao_pagina_atual = $ao_total_paginas;
$ao_offset = ($ao_pagina_atual - 1) * AO_USUARIOS_POR_PAGINA;
$usuarios = $pdo->query(
"SELECT u.*, p.nome AS perfil_nome FROM usuarios u INNER JOIN perfis p ON p.id = u.perfil_id
ORDER BY {$ao_ordenar_sql}
LIMIT " . AO_USUARIOS_POR_PAGINA . " OFFSET {$ao_offset}"
)->fetchAll();
/** Monta a URL do grid preservando ordenação/página, trocando só o que for passado em $sobrescrever. */
function ao_usuarios_url(array $sobrescrever, $ordenar, $direcao, $pagina) {
$params = array_merge(['ordenar' => $ordenar, 'direcao' => $direcao, 'pagina' => $pagina], $sobrescrever);
return 'usuarios.php?' . http_build_query($params);
}
require __DIR__ . '/includes/header.php';
?>
<div class="ao-card-header" style="margin-bottom: var(--ao-space-4);">
<h1 class="ao-page-title" style="margin: 0;">Usuários</h1>
<button type="button" class="ao-btn ao-btn-primary" onclick="aoAbrirModalUsuario()">+ Novo usuário</button>
</div>
<?php if ($erro && !$editando): ?>
<div class="ao-badge ao-badge-baixa" style="display:block; margin-bottom: var(--ao-space-4); padding: 10px 14px; font-size: var(--ao-fs-sm);"><?= htmlspecialchars($erro) ?></div>
<?php endif; ?>
<?php if ($sucesso): ?>
<div class="ao-badge ao-badge-alta" style="display:block; margin-bottom: var(--ao-space-4); padding: 10px 14px; font-size: var(--ao-fs-sm);"><?= htmlspecialchars($sucesso) ?></div>
<?php endif; ?>
<div class="ao-modal-backdrop<?= $editando ? ' is-open' : '' ?>" id="ao-modal-usuario">
<div class="ao-modal" role="dialog" aria-modal="true" aria-labelledby="ao-modal-usuario-titulo">
<div class="ao-modal-header">
<strong id="ao-modal-usuario-titulo"><?= ($editando && !empty($editando['id'])) ? 'Editar usuário' : 'Novo usuário' ?></strong>
<button type="button" class="ao-modal-close" onclick="aoFecharModalUsuario()" aria-label="Fechar">&times;</button>
</div>
<?php if ($erro && $editando): ?>
<div class="ao-badge ao-badge-baixa" style="display:block; margin-bottom: var(--ao-space-4); padding: 10px 14px; font-size: var(--ao-fs-sm);"><?= htmlspecialchars($erro) ?></div>
<?php endif; ?>
<form method="post" action="usuarios.php" id="form-usuario" onsubmit="return aoConfirmarSenhas(this);">
<input type="hidden" name="acao" value="salvar">
<input type="hidden" name="id" value="<?= (int) ($editando['id'] ?? 0) ?>">
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="nome">Nome</label>
<input type="text" id="nome" name="nome" required value="<?= htmlspecialchars($editando['nome'] ?? '') ?>">
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="email">E-mail</label>
<input type="email" id="email" name="email" required value="<?= htmlspecialchars($editando['email'] ?? '') ?>">
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="senha">Senha <?= ($editando && !empty($editando['id'])) ? '(deixe em branco para manter a atual)' : '' ?></label>
<input type="password" id="senha" name="senha" autocomplete="new-password" <?= ($editando && !empty($editando['id'])) ? '' : 'required' ?>>
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="confirmar_senha">Confirmar senha</label>
<input type="password" id="confirmar_senha" name="confirmar_senha" autocomplete="new-password" <?= ($editando && !empty($editando['id'])) ? '' : 'required' ?>>
</div>
<div class="ao-row" style="gap: var(--ao-space-4); align-items: flex-end; margin-bottom: var(--ao-space-5);">
<div class="ao-field" style="flex: 1;">
<label for="perfil_id">Perfil</label>
<select id="perfil_id" name="perfil_id" required>
<option value="">Selecione...</option>
<?php foreach ($perfis as $p): ?>
<option value="<?= $p['id'] ?>" <?= (isset($editando) && (int) ($editando['perfil_id'] ?? 0) === (int) $p['id']) ? 'selected' : '' ?>><?= htmlspecialchars($p['nome']) ?></option>
<?php endforeach; ?>
</select>
</div>
<label style="display:flex; align-items:center; gap:6px; font-size: var(--ao-fs-sm); padding-bottom: 10px;">
<input type="checkbox" name="ativo" <?= (!$editando || $editando['ativo']) ? 'checked' : '' ?>> Ativo
</label>
</div>
<button type="submit" class="ao-btn ao-btn-primary"><?= ($editando && !empty($editando['id'])) ? 'Salvar alterações' : 'Cadastrar usuário' ?></button>
<button type="button" class="ao-btn ao-btn-secondary" onclick="aoFecharModalUsuario()">Cancelar</button>
</form>
</div>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Usuários cadastrados</strong></div>
<table class="ao-table">
<thead>
<tr>
<?php
$ao_colunas_grid = [
'nome' => 'Nome',
'email' => 'E-mail',
'perfil' => 'Perfil',
'status' => 'Status',
'ultimo_login' => 'Último login',
];
?>
<?php foreach ($ao_colunas_grid as $ao_col_chave => $ao_col_rotulo): ?>
<th>
<a href="<?= htmlspecialchars(ao_usuarios_url(['ordenar' => $ao_col_chave, 'direcao' => ($ao_ordenar === $ao_col_chave && $ao_direcao === 'asc') ? 'desc' : 'asc', 'pagina' => 1], $ao_ordenar, $ao_direcao, $ao_pagina_atual)) ?>"
class="ao-th-ordenavel">
<?= htmlspecialchars($ao_col_rotulo) ?><?php if ($ao_ordenar === $ao_col_chave): ?><span class="ao-th-seta"><?= $ao_direcao === 'asc' ? ' ▲' : ' ▼' ?></span><?php endif; ?>
</a>
</th>
<?php endforeach; ?>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($usuarios as $u): ?>
<tr>
<td><?= htmlspecialchars($u['nome']) ?></td>
<td class="ao-numeric"><?= htmlspecialchars($u['email']) ?></td>
<td><?= htmlspecialchars($u['perfil_nome']) ?></td>
<td><span class="ao-badge <?= $u['ativo'] ? 'ao-badge-alta' : 'ao-badge-neutro' ?>"><?= $u['ativo'] ? 'Ativo' : 'Inativo' ?></span></td>
<td class="ao-numeric">
<?php if (!empty($u['ultimo_login'])): ?>
<?= htmlspecialchars(date('d/m/Y H:i', strtotime($u['ultimo_login']))) ?>
<?php else: ?>
<span style="color: var(--ao-text-muted);">Nunca</span>
<?php endif; ?>
</td>
<td style="white-space: nowrap; text-align: right;">
<a href="usuarios.php?editar=<?= $u['id'] ?>" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Editar</a>
<?php if ((int) $u['id'] !== (int) $ao_usuario_atual['id']): ?>
<form method="post" action="usuarios.php" style="display:inline;" onsubmit="return confirm('Excluir este usuário?');">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<button type="submit" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Excluir</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ($ao_total_paginas > 1): ?>
<div class="ao-paginacao">
<span class="ao-paginacao-info">
Página <?= $ao_pagina_atual ?> de <?= $ao_total_paginas ?> (<?= $ao_total_usuarios ?> usuários)
</span>
<div class="ao-paginacao-links">
<?php if ($ao_pagina_atual > 1): ?>
<a href="<?= htmlspecialchars(ao_usuarios_url(['pagina' => $ao_pagina_atual - 1], $ao_ordenar, $ao_direcao, $ao_pagina_atual)) ?>" class="ao-btn ao-btn-secondary">&laquo; Anterior</a>
<?php else: ?>
<span class="ao-btn ao-btn-secondary ao-btn-desativado">&laquo; Anterior</span>
<?php endif; ?>
<?php for ($ao_p = 1; $ao_p <= $ao_total_paginas; $ao_p++): ?>
<?php if ($ao_p === $ao_pagina_atual): ?>
<span class="ao-paginacao-numero ao-paginacao-atual"><?= $ao_p ?></span>
<?php else: ?>
<a href="<?= htmlspecialchars(ao_usuarios_url(['pagina' => $ao_p], $ao_ordenar, $ao_direcao, $ao_pagina_atual)) ?>" class="ao-paginacao-numero"><?= $ao_p ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($ao_pagina_atual < $ao_total_paginas): ?>
<a href="<?= htmlspecialchars(ao_usuarios_url(['pagina' => $ao_pagina_atual + 1], $ao_ordenar, $ao_direcao, $ao_pagina_atual)) ?>" class="ao-btn ao-btn-secondary">Próxima &raquo;</a>
<?php else: ?>
<span class="ao-btn ao-btn-secondary ao-btn-desativado">Próxima &raquo;</span>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<style>
.ao-th-ordenavel { display: inline-block; color: inherit; text-decoration: none; }
.ao-th-ordenavel:hover { text-decoration: underline; }
.ao-th-seta { font-size: 10px; }
.ao-paginacao {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
gap: var(--ao-space-3);
padding: var(--ao-space-4);
border-top: 1px solid var(--ao-border);
}
.ao-paginacao-info { font-size: var(--ao-fs-sm); color: var(--ao-text-muted); }
.ao-paginacao-links { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ao-btn-desativado { opacity: .45; cursor: default; pointer-events: none; }
.ao-paginacao-numero {
display: inline-flex; align-items: center; justify-content: center;
min-width: 32px; height: 32px; padding: 0 8px;
border-radius: var(--ao-radius-sm);
font-size: var(--ao-fs-sm); font-weight: 600;
color: var(--ao-text-body); text-decoration: none;
}
.ao-paginacao-numero:hover { background: var(--ao-bg-page); }
.ao-paginacao-atual { background: var(--ao-primary); color: var(--ao-on-primary); }
</style>
<script>
function aoAbrirModalUsuario() {
document.getElementById('ao-modal-usuario').classList.add('is-open');
}
function aoFecharModalUsuario() {
document.getElementById('ao-modal-usuario').classList.remove('is-open');
if (window.location.search) {
window.history.replaceState(null, '', 'usuarios.php');
}
}
document.getElementById('ao-modal-usuario').addEventListener('click', function (evento) {
if (evento.target === this) aoFecharModalUsuario();
});
document.addEventListener('keydown', function (evento) {
if (evento.key === 'Escape') aoFecharModalUsuario();
});
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>