Versão inicial em produção

This commit is contained in:
root
2026-08-21 22:57:44 +00:00
commit fb27cd60c0
32 changed files with 3848 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
<?php
require_once __DIR__ . '/theme.php';
require_once __DIR__ . '/auth.php';
ao_exigir_login();
$ao_usuario = ao_usuario_logado();
// Página de administração do próprio sistema (usuários/perfis/páginas)?
// Essas sempre exigem acesso_total, independente da tabela `paginas`.
if (isset($pagina_ativa) && array_key_exists($pagina_ativa, AO_PAGINAS_ADMIN)) {
ao_exigir_acesso_total($ao_tema_atual);
} elseif (isset($pagina_ativa)) {
ao_exigir_permissao($pagina_ativa, $ao_tema_atual);
}
// Páginas de negócio configuráveis, na ordem cadastrada em `paginas`.
$ao_nav_paginas = $pdo->query('SELECT chave, nome, arquivo FROM paginas WHERE ativo = 1 ORDER BY ordem, nome')->fetchAll();
// "Contas" não fica mais no menu principal — mora no submenu do usuário.
// Buscamos os dados dela aqui (nome/arquivo, caso alguém renomeie em
// Páginas) para montar o link lá embaixo, já checando a permissão normal.
$ao_pagina_contas = null;
foreach ($ao_nav_paginas as $p) {
if ($p['chave'] === 'contas') {
$ao_pagina_contas = $p;
break;
}
}
?>
<!DOCTYPE html>
<html lang="pt-BR" data-ao-mode="<?= htmlspecialchars($ao_modo) ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AçãoOpção<?= isset($titulo_pagina) ? ' — ' . htmlspecialchars($titulo_pagina) : '' ?></title>
<link rel="icon" href="<?= htmlspecialchars($ao_tema_atual['favicon']) ?>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@500;600&display=swap" rel="stylesheet">
<!-- A identidade visual inteira do sistema muda ao trocar apenas este arquivo CSS -->
<link rel="stylesheet" href="<?= htmlspecialchars($ao_tema_atual['css']) ?>">
<style>
* { box-sizing: border-box; }
body { margin: 0; }
.ao-shell { max-width: 1180px; margin: 0 auto; padding: var(--ao-space-6) var(--ao-space-5); }
.ao-topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--ao-space-4); }
.ao-topbar nav { display: flex; gap: var(--ao-space-4); flex-wrap: wrap; }
.ao-topbar nav a { font-size: var(--ao-fs-sm); font-weight: 600; padding: 6px 2px; border-bottom: 2px solid transparent; text-decoration: none; white-space: nowrap; }
.ao-topbar nav a.ativo { border-bottom-color: currentColor; }
.ao-nav-separador { width: 1px; align-self: stretch; background: rgba(255,255,255,.25); margin: 0 2px; }
.ao-brand-icon { width: 28px; height: 28px; border-radius: 8px; display: block; }
.ao-topbar-direita { display: flex; align-items: center; gap: var(--ao-space-4); flex-wrap: wrap; }
/* Menu do usuário (nome no cabeçalho -> submenu) */
.ao-user-menu { position: relative; }
.ao-user-menu-trigger {
display: flex; align-items: center; gap: 8px;
background: none; border: none; cursor: pointer; padding: 4px 2px;
font-family: var(--ao-font-body); color: #fff;
}
.ao-user-menu-trigger strong { font-size: var(--ao-fs-sm); color: #fff; }
.ao-user-menu-caret { font-size: 10px; color: rgba(255,255,255,.75); transition: transform .15s ease; }
.ao-user-menu.aberto .ao-user-menu-caret { transform: rotate(180deg); }
.ao-user-menu-dropdown {
display: none;
position: absolute; top: calc(100% + 12px); right: 0;
min-width: 230px;
background: var(--ao-bg-surface);
border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-md);
box-shadow: var(--ao-shadow-lg);
padding: var(--ao-space-2);
z-index: 1000;
}
.ao-user-menu.aberto .ao-user-menu-dropdown { display: block; }
.ao-user-menu-dropdown a {
display: block; padding: 8px 10px; border-radius: var(--ao-radius-sm);
font-size: var(--ao-fs-sm); font-weight: 600;
color: var(--ao-text-body); text-decoration: none;
}
.ao-user-menu-dropdown a:hover, .ao-user-menu-dropdown a.ativo { background: var(--ao-bg-page); }
.ao-user-menu-separador { height: 1px; background: var(--ao-border); margin: var(--ao-space-2) 6px; }
.ao-user-menu-secao-titulo {
font-size: var(--ao-fs-xs); text-transform: uppercase; letter-spacing: .04em;
color: var(--ao-text-muted); padding: 6px 10px 4px;
}
.ao-theme-switcher { display: flex; align-items: center; gap: 8px; padding: 2px 10px 10px; }
.ao-theme-dot {
width: 20px; height: 20px; border-radius: 50%;
border: 2px solid var(--ao-border);
display: inline-block; transition: transform .15s ease;
}
.ao-theme-dot:hover { transform: scale(1.15); }
.ao-theme-dot.ativo { border-color: var(--ao-text-strong); box-shadow: 0 0 0 2px var(--ao-focus-ring); }
.ao-mode-toggle {
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 50%;
background: var(--ao-bg-page);
border: 1px solid var(--ao-border);
font-size: 14px; line-height: 1; text-decoration: none;
transition: background-color .15s ease, transform .15s ease;
}
.ao-mode-toggle:hover { background: var(--ao-primary-soft); transform: scale(1.08); }
.ao-page-title { margin-bottom: var(--ao-space-5); }
/* Utilitário de layout usado em vários formulários (acoes.php, usuarios.php,
operacoes.php, paginas_admin.php, index.php) para colocar campos lado a
lado. Cada uso já define seu próprio "gap" inline. */
.ao-row { display: flex; flex-wrap: wrap; }
/* Popup de cadastro/edição — compartilhado por qualquer página que use um
<div class="ao-modal-backdrop" id="...">, com a classe "is-open" para
controlar se aparece (o PHP já pode renderizar aberto, ex: ao editar). */
.ao-modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
align-items: center;
justify-content: center;
padding: var(--ao-space-4);
z-index: 1000;
}
.ao-modal-backdrop.is-open { display: flex; }
.ao-modal {
background: var(--ao-bg-surface);
border-radius: var(--ao-radius-lg);
box-shadow: var(--ao-shadow-lg);
padding: var(--ao-space-5);
width: 100%;
max-width: 560px;
max-height: 90vh;
overflow-y: auto;
}
.ao-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--ao-space-2);
}
.ao-modal-close {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--ao-text-muted);
padding: 0 var(--ao-space-1);
}
.ao-modal-close:hover { color: var(--ao-text-strong); }
body, .ao-card, .ao-table, .ao-field input, .ao-field select, .ao-field textarea, .ao-btn {
transition: background-color .2s ease, color .2s ease, border-color .2s ease;
}
@media (max-width: 900px) {
.ao-topbar { flex-wrap: wrap; gap: 12px; padding: var(--ao-space-3); }
}
</style>
</head>
<body>
<header class="ao-topbar">
<div class="ao-brand">
<img src="<?= htmlspecialchars($ao_tema_atual['icone']) ?>" alt="" class="ao-brand-icon">
<span>AçãoOpção</span>
</div>
<nav>
<?php foreach ($ao_nav_paginas as $p): ?>
<?php if ($p['chave'] === 'contas') continue; // fica no submenu do usuário ?>
<?php if (!empty($ao_usuario['acesso_total']) || in_array($p['chave'], $ao_usuario['paginas'] ?? [], true)): ?>
<a href="<?= htmlspecialchars($p['arquivo']) ?>" class="<?= ($pagina_ativa ?? '') === $p['chave'] ? 'ativo' : '' ?>"><?= htmlspecialchars($p['nome']) ?></a>
<?php endif; ?>
<?php endforeach; ?>
<?php if (!empty($ao_usuario['acesso_total'])): ?>
<span class="ao-nav-separador"></span>
<?php foreach (AO_PAGINAS_ADMIN as $chave => $info): ?>
<a href="<?= htmlspecialchars($info['arquivo']) ?>" class="<?= ($pagina_ativa ?? '') === $chave ? 'ativo' : '' ?>"><?= htmlspecialchars($info['nome']) ?></a>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<div class="ao-topbar-direita">
<div class="ao-user-menu" id="ao-user-menu">
<button type="button" class="ao-user-menu-trigger" onclick="aoAlternarMenuUsuario()">
<strong><?= htmlspecialchars($ao_usuario['nome']) ?></strong>
<span class="ao-user-menu-caret">▾</span>
</button>
<div class="ao-user-menu-dropdown">
<?php if ($ao_pagina_contas && (!empty($ao_usuario['acesso_total']) || in_array('contas', $ao_usuario['paginas'] ?? [], true))): ?>
<a href="<?= htmlspecialchars($ao_pagina_contas['arquivo']) ?>" class="<?= ($pagina_ativa ?? '') === 'contas' ? 'ativo' : '' ?>"><?= htmlspecialchars($ao_pagina_contas['nome']) ?></a>
<?php endif; ?>
<a href="minha-conta.php">Minha conta</a>
<div class="ao-user-menu-separador"></div>
<div class="ao-user-menu-secao-titulo">Identidade visual</div>
<div class="ao-theme-switcher">
<?php foreach ($ao_temas_info as $chave => $info): ?>
<a href="?tema=<?= urlencode($chave) ?>"
class="ao-theme-dot <?= $ao_tema === $chave ? 'ativo' : '' ?>"
style="background: <?= htmlspecialchars($info['cor']) ?>;"
title="Trocar para <?= htmlspecialchars($info['nome']) ?>"></a>
<?php endforeach; ?>
<a href="?modo=<?= urlencode($ao_proximo_modo) ?>"
class="ao-mode-toggle"
title="Trocar para modo <?= htmlspecialchars($ao_modos_info[$ao_proximo_modo]['nome']) ?>">
<?= $ao_modo_atual['icone'] ?>
</a>
</div>
<div class="ao-user-menu-separador"></div>
<a href="logout.php">Sair</a>
</div>
</div>
</div>
</header>
<script>
function aoAlternarMenuUsuario(forcarAberto) {
var menu = document.getElementById('ao-user-menu');
if (typeof forcarAberto === 'boolean') {
menu.classList.toggle('aberto', forcarAberto);
} else {
menu.classList.toggle('aberto');
}
}
document.addEventListener('click', function (evento) {
var menu = document.getElementById('ao-user-menu');
if (menu && !menu.contains(evento.target)) {
aoAlternarMenuUsuario(false);
}
});
document.addEventListener('keydown', function (evento) {
if (evento.key === 'Escape') aoAlternarMenuUsuario(false);
});
</script>
<main class="ao-shell">