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
+2
View File
@@ -0,0 +1,2 @@
includes/config.php
includes/ambiente.json
+495
View File
@@ -0,0 +1,495 @@
<?php
$titulo_pagina = 'Ações';
$pagina_ativa = 'acoes';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_permissao('acoes');
// Tudo nesta página é isolado por usuário: cada um só enxerga e só consegue
// alterar as próprias operações. Todo acesso por id abaixo é sempre filtrado
// também por usuario_id, para que ninguém edite/exclua/veja dados de outra
// pessoa forjando o id na URL ou no formulário.
$ao_usuario_id = (int) ao_usuario_logado()['id'];
// Categoria do ativo (Ação/ETF/BDR/FII) — é uma característica do próprio
// ativo, não da operação, então todas as operações de um mesmo código
// (ex: PETR4) precisam usar a mesma categoria (checado na validação do save).
const AO_CATEGORIAS_ATIVO = [
'acao' => 'Ação',
'etf' => 'ETF',
'bdr' => 'BDR',
'fii' => 'FII',
];
/**
* Simula, em ordem cronológica, uma lista de operações (compra/venda) de um
* mesmo ativo e devolve o estado final. Regra do PM (preço médio):
* - compra: recalcula a média ponderada e aumenta a quantidade.
* - venda: só reduz a quantidade; o PM das ações que sobram não muda.
* "ok" vem false se em algum ponto a quantidade ficaria negativa (ou seja,
* uma venda maior do que o que havia disponível até aquela data).
* Cada item de $linhas precisa ter: tipo, quantidade, preco, data_operacao, _ordem.
*/
function ao_simular_transacoes(array $linhas) {
usort($linhas, function ($a, $b) {
$an = $a['data_operacao'] === null;
$bn = $b['data_operacao'] === null;
if ($an !== $bn) {
return $an <=> $bn; // datadas primeiro, sem data por último
}
if (!$an && $a['data_operacao'] !== $b['data_operacao']) {
return strcmp($a['data_operacao'], $b['data_operacao']);
}
return $a['_ordem'] <=> $b['_ordem'];
});
$qtd = 0;
$pm = 0.0;
$ok = true;
foreach ($linhas as $l) {
$q = (int) $l['quantidade'];
$p = (float) $l['preco'];
if ($l['tipo'] === 'compra') {
$novoQtd = $qtd + $q;
$pm = $novoQtd > 0 ? (($qtd * $pm) + ($q * $p)) / $novoQtd : 0.0;
$qtd = $novoQtd;
} else {
$qtd -= $q;
if ($qtd < 0) {
$ok = false;
}
}
}
return ['ok' => $ok, 'qtd' => max(0, $qtd), 'pm' => round($pm, 4)];
}
/** Busca as operações de um ativo (de um usuário), no formato usado por ao_simular_transacoes(). */
function ao_buscar_transacoes_ativo(PDO $pdo, $usuario_id, $ativo, $ignorar_id = null) {
$sql = 'SELECT id, tipo, quantidade, preco, data_operacao FROM acoes WHERE usuario_id = ? AND ativo = ?';
$params = [$usuario_id, $ativo];
if ($ignorar_id !== null) {
$sql .= ' AND id != ?';
$params[] = $ignorar_id;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$linhas = $stmt->fetchAll();
foreach ($linhas as &$l) {
$l['_ordem'] = (int) $l['id'];
}
return $linhas;
}
/**
* Recalcula e salva a posição consolidada (quantidade total + PM) de um
* ativo para um usuário, simulando todo o histórico de compras/vendas dele.
* Se não sobrar quantidade nenhuma, a posição é removida.
*/
function ao_recalcular_posicao_acao(PDO $pdo, $usuario_id, $ativo) {
$linhas = ao_buscar_transacoes_ativo($pdo, $usuario_id, $ativo);
$sim = ao_simular_transacoes($linhas);
if ($sim['qtd'] <= 0) {
$pdo->prepare('DELETE FROM acoes_posicoes WHERE usuario_id = ? AND ativo = ?')->execute([$usuario_id, $ativo]);
return;
}
// A categoria do ativo é a mesma em todas as operações dele (garantido na
// validação do save) — pega de qualquer uma para gravar na posição.
$stmtCategoria = $pdo->prepare('SELECT categoria FROM acoes WHERE usuario_id = ? AND ativo = ? LIMIT 1');
$stmtCategoria->execute([$usuario_id, $ativo]);
$categoria = $stmtCategoria->fetchColumn() ?: 'acao';
$stmt = $pdo->prepare(
'INSERT INTO acoes_posicoes (usuario_id, ativo, categoria, quantidade, preco_medio) VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE categoria = VALUES(categoria), quantidade = VALUES(quantidade), preco_medio = VALUES(preco_medio)'
);
$stmt->execute([$usuario_id, $ativo, $categoria, $sim['qtd'], $sim['pm']]);
}
/** Converte "38,42" ou "38.42" em float. Retorna null se não for um número válido. */
function ao_parse_decimal_br($valor) {
$valor = str_replace(',', '.', trim((string) $valor));
return is_numeric($valor) ? (float) $valor : null;
}
/** Badge colorido com o rótulo (Ação/ETF/BDR/FII) de uma categoria de ativo. */
function ao_badge_categoria($categoria) {
$classes = ['acao' => 'ao-badge-neutro', 'etf' => 'ao-badge-info', 'bdr' => 'ao-badge-alerta', 'fii' => 'ao-badge-fii'];
$classe = $classes[$categoria] ?? 'ao-badge-neutro';
$rotulo = AO_CATEGORIAS_ATIVO[$categoria] ?? $categoria;
return '<span class="ao-badge ' . $classe . '">' . htmlspecialchars($rotulo) . '</span>';
}
$erro = null;
$sucesso = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'excluir') {
$id = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare('SELECT ativo FROM acoes WHERE id = ? AND usuario_id = ?');
$stmt->execute([$id, $ao_usuario_id]);
$linha = $stmt->fetch();
if ($linha) {
// Antes de excluir, confere se remover esta operação deixaria alguma
// venda posterior sem ações suficientes (ex: excluir uma compra cujas
// ações já foram vendidas depois).
$sim = ao_simular_transacoes(ao_buscar_transacoes_ativo($pdo, $ao_usuario_id, $linha['ativo'], $id));
if (!$sim['ok']) {
$erro = 'Não é possível excluir: isso deixaria uma venda sem ações suficientes na carteira.';
} else {
$pdo->prepare('DELETE FROM acoes WHERE id = ? AND usuario_id = ?')->execute([$id, $ao_usuario_id]);
ao_recalcular_posicao_acao($pdo, $ao_usuario_id, $linha['ativo']);
header('Location: acoes.php?excluido=1');
exit;
}
} else {
header('Location: acoes.php?excluido=1');
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') {
$id = (int) ($_POST['id'] ?? 0);
$conta_id = (int) ($_POST['conta_id'] ?? 0);
$ativo = strtoupper(trim($_POST['ativo'] ?? ''));
$categoria = (string) ($_POST['categoria'] ?? '');
$tipo = ($_POST['tipo'] ?? '') === 'venda' ? 'venda' : 'compra';
$quantidade = (int) ($_POST['quantidade'] ?? 0);
$data_operacao = trim($_POST['data_operacao'] ?? '');
$preco_bruto = trim($_POST['preco'] ?? '');
$preco = ao_parse_decimal_br($preco_bruto);
$ativo_antigo = null;
if (!array_key_exists($categoria, AO_CATEGORIAS_ATIVO)) {
$categoria = 'acao';
}
// A conta precisa existir e pertencer ao usuário logado — impede que
// alguém forje o id de uma conta de outra pessoa no formulário.
$stmtConta = $pdo->prepare('SELECT id FROM contas WHERE id = ? AND usuario_id = ?');
$stmtConta->execute([$conta_id, $ao_usuario_id]);
$contaValida = (bool) $stmtConta->fetch();
if (!$contaValida) {
$erro = 'Selecione uma conta válida.';
} elseif ($ativo === '' || $quantidade <= 0 || $preco === null || $preco <= 0) {
$erro = 'Preencha código do ativo, quantidade (maior que zero) e preço (maior que zero) corretamente.';
} elseif ($data_operacao !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $data_operacao)) {
$erro = 'Data da operação inválida.';
} else {
$ignorar_id = $id > 0 ? $id : null;
if ($id > 0) {
$stmtAntigo = $pdo->prepare('SELECT ativo FROM acoes WHERE id = ? AND usuario_id = ?');
$stmtAntigo->execute([$id, $ao_usuario_id]);
$antigo = $stmtAntigo->fetch();
if (!$antigo) {
$erro = 'Operação não encontrada.';
} else {
$ativo_antigo = $antigo['ativo'];
}
}
if (!$erro) {
// A categoria é do ativo, não da operação — todas as operações de
// um mesmo código precisam concordar (evita PETR4 virar "ETF" numa
// operação e "Ação" noutra por engano).
$sqlCategoriaExistente = 'SELECT DISTINCT categoria FROM acoes WHERE usuario_id = ? AND ativo = ?';
$paramsCategoriaExistente = [$ao_usuario_id, $ativo];
if ($ignorar_id !== null) {
$sqlCategoriaExistente .= ' AND id != ?';
$paramsCategoriaExistente[] = $ignorar_id;
}
$stmtCategoriaExistente = $pdo->prepare($sqlCategoriaExistente);
$stmtCategoriaExistente->execute($paramsCategoriaExistente);
$categoriaExistente = $stmtCategoriaExistente->fetchColumn();
if ($categoriaExistente !== false && $categoriaExistente !== $categoria) {
$erro = 'Já existe(m) operação(ões) de ' . htmlspecialchars($ativo) . ' cadastrada(s) como "'
. htmlspecialchars(AO_CATEGORIAS_ATIVO[$categoriaExistente]) . '". Selecione a mesma categoria '
. 'para manter a carteira consistente (ou edite as operações antigas se o código estiver errado).';
}
}
if (!$erro) {
// Valida o ativo de destino incluindo esta operação (nova ou editada).
$linhasDestino = ao_buscar_transacoes_ativo($pdo, $ao_usuario_id, $ativo, $ignorar_id);
$linhasDestino[] = [
'tipo' => $tipo, 'quantidade' => $quantidade, 'preco' => $preco,
'data_operacao' => $data_operacao !== '' ? $data_operacao : null,
'_ordem' => $id > 0 ? $id : PHP_INT_MAX,
];
$simDestino = ao_simular_transacoes($linhasDestino);
if (!$simDestino['ok']) {
$erro = 'Essa venda deixaria a quantidade de ' . htmlspecialchars($ativo) . ' negativa em alguma data — '
. 'você não pode vender mais ações do que possuía até aquele momento.';
} elseif ($ativo_antigo !== null && $ativo_antigo !== $ativo) {
// O código do ativo mudou nesta edição: confere se o ativo antigo
// continua consistente sem esta operação (que está saindo dele).
$simAntigo = ao_simular_transacoes(ao_buscar_transacoes_ativo($pdo, $ao_usuario_id, $ativo_antigo, $id));
if (!$simAntigo['ok']) {
$erro = 'Não é possível mover esta operação para outro ativo: sobraria uma venda de '
. htmlspecialchars($ativo_antigo) . ' sem ações suficientes.';
}
}
}
if (!$erro) {
try {
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE acoes SET conta_id=?, ativo=?, categoria=?, tipo=?, quantidade=?, data_operacao=?, preco=? WHERE id=? AND usuario_id=?');
$stmt->execute([$conta_id, $ativo, $categoria, $tipo, $quantidade, $data_operacao !== '' ? $data_operacao : null, $preco, $id, $ao_usuario_id]);
} else {
$stmt = $pdo->prepare('INSERT INTO acoes (usuario_id, conta_id, ativo, categoria, tipo, quantidade, data_operacao, preco) VALUES (?,?,?,?,?,?,?,?)');
$stmt->execute([$ao_usuario_id, $conta_id, $ativo, $categoria, $tipo, $quantidade, $data_operacao !== '' ? $data_operacao : null, $preco]);
}
ao_recalcular_posicao_acao($pdo, $ao_usuario_id, $ativo);
if ($ativo_antigo !== null && $ativo_antigo !== $ativo) {
ao_recalcular_posicao_acao($pdo, $ao_usuario_id, $ativo_antigo);
}
header('Location: acoes.php?salvo=1');
exit;
} catch (PDOException $e) {
$erro = 'Erro ao salvar: ' . $e->getMessage();
}
}
}
}
$editando = null;
if (isset($_GET['editar'])) {
$stmt = $pdo->prepare('SELECT * FROM acoes WHERE id = ? AND usuario_id = ?');
$stmt->execute([(int) $_GET['editar'], $ao_usuario_id]);
$editando = $stmt->fetch() ?: null;
}
// Se deu erro no envio, mantemos o que foi digitado na tela em vez de limpar tudo.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar' && $erro && $erro !== 'Operação não encontrada.') {
$editando = [
'id' => $id,
'conta_id' => $conta_id,
'ativo' => $ativo,
'categoria' => $categoria,
'tipo' => $tipo,
'quantidade' => $quantidade,
'data_operacao' => $data_operacao,
'preco' => $preco_bruto,
];
}
if (isset($_GET['salvo'])) $sucesso = 'Operação salva com sucesso.';
if (isset($_GET['excluido'])) $sucesso = 'Operação excluída.';
$stmtContas = $pdo->prepare(
'SELECT ct.id, ct.nome AS conta_nome, cr.nome AS corretora_nome
FROM contas ct
INNER JOIN corretoras cr ON cr.id = ct.corretora_id
WHERE ct.usuario_id = ?
ORDER BY cr.nome, ct.nome'
);
$stmtContas->execute([$ao_usuario_id]);
$contas = $stmtContas->fetchAll();
$stmtPosicoes = $pdo->prepare('SELECT * FROM acoes_posicoes WHERE usuario_id = ? ORDER BY ativo');
$stmtPosicoes->execute([$ao_usuario_id]);
$posicoes = $stmtPosicoes->fetchAll();
$stmtOperacoes = $pdo->prepare(
'SELECT a.*, ct.nome AS conta_nome, cr.nome AS corretora_nome
FROM acoes a
LEFT JOIN contas ct ON ct.id = a.conta_id
LEFT JOIN corretoras cr ON cr.id = ct.corretora_id
WHERE a.usuario_id = ?
ORDER BY (a.data_operacao IS NULL), a.data_operacao DESC, a.id DESC'
);
$stmtOperacoes->execute([$ao_usuario_id]);
$operacoes = $stmtOperacoes->fetchAll();
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;">Minha carteira de ações</h1>
<?php if ($contas): ?>
<button type="button" class="ao-btn ao-btn-primary" onclick="aoAbrirModalOperacao()">+ Nova operação</button>
<?php endif; ?>
</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; ?>
<?php if (!$contas): ?>
<div class="ao-card" style="max-width: 560px; margin-bottom: var(--ao-space-6);">
<p style="margin: 0;">Antes de cadastrar uma operação, você precisa ter uma corretora e uma conta cadastradas.
<a href="corretoras.php">Cadastre uma corretora</a> e depois <a href="contas.php">cadastre uma conta</a> nela.</p>
</div>
<?php endif; ?>
<div class="ao-modal-backdrop<?= $editando ? ' is-open' : '' ?>" id="ao-modal-operacao">
<div class="ao-modal" role="dialog" aria-modal="true" aria-labelledby="ao-modal-operacao-titulo">
<div class="ao-modal-header">
<strong id="ao-modal-operacao-titulo"><?= ($editando && !empty($editando['id'])) ? 'Editar operação' : 'Nova operação' ?></strong>
<button type="button" class="ao-modal-close" onclick="aoFecharModalOperacao()" aria-label="Fechar">&times;</button>
</div>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-sm); margin-top: 0;">
Cadastre cada compra ou venda separadamente, mesmo que seja do mesmo ativo em datas diferentes —
o preço médio (PM) é recalculado automaticamente: compras atualizam o PM, vendas só reduzem a
quantidade. Esta carteira é só sua.
</p>
<?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="acoes.php">
<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="conta_id">Conta</label>
<select id="conta_id" name="conta_id" required>
<option value="">Selecione...</option>
<?php foreach ($contas as $ct): ?>
<option value="<?= $ct['id'] ?>" <?= (isset($editando) && (int) ($editando['conta_id'] ?? 0) === (int) $ct['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($ct['corretora_nome']) ?> — <?= htmlspecialchars($ct['conta_nome']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="ativo">Código do ativo</label>
<input type="text" id="ativo" name="ativo" placeholder="Ex: PETR4" required
style="text-transform: uppercase;" value="<?= htmlspecialchars($editando['ativo'] ?? '') ?>">
</div>
<div class="ao-row" style="gap: var(--ao-space-4); margin-bottom: var(--ao-space-4);">
<div class="ao-field" style="flex: 1;">
<label for="categoria">Tipo de ativo</label>
<select id="categoria" name="categoria">
<?php foreach (AO_CATEGORIAS_ATIVO as $ao_cat_chave => $ao_cat_rotulo): ?>
<option value="<?= $ao_cat_chave ?>" <?= (($editando['categoria'] ?? 'acao') === $ao_cat_chave) ? 'selected' : '' ?>><?= htmlspecialchars($ao_cat_rotulo) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="ao-field" style="width: 150px;">
<label for="tipo">Tipo</label>
<select id="tipo" name="tipo">
<option value="compra" <?= (($editando['tipo'] ?? 'compra') === 'compra') ? 'selected' : '' ?>>Compra</option>
<option value="venda" <?= (($editando['tipo'] ?? '') === 'venda') ? 'selected' : '' ?>>Venda</option>
</select>
</div>
</div>
<div class="ao-row" style="gap: var(--ao-space-4); margin-bottom: var(--ao-space-4);">
<div class="ao-field" style="flex: 1;">
<label for="quantidade">Quantidade</label>
<input type="number" id="quantidade" name="quantidade" min="1" step="1" placeholder="100" required
value="<?= htmlspecialchars((string) ($editando['quantidade'] ?? '')) ?>">
</div>
<div class="ao-field" style="flex: 1;">
<label for="data_operacao">Data da operação (se tiver)</label>
<input type="date" id="data_operacao" name="data_operacao" value="<?= htmlspecialchars($editando['data_operacao'] ?? '') ?>">
</div>
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-5);">
<label for="preco">Preço</label>
<input type="text" id="preco" name="preco" placeholder="Ex: 38,42" required
value="<?= htmlspecialchars((string) ($editando['preco'] ?? '')) ?>">
</div>
<button type="submit" class="ao-btn ao-btn-primary"><?= ($editando && !empty($editando['id'])) ? 'Salvar alterações' : 'Cadastrar operação' ?></button>
<button type="button" class="ao-btn ao-btn-secondary" onclick="aoFecharModalOperacao()">Cancelar</button>
</form>
</div>
</div>
<div class="ao-card" style="margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong>Carteira consolidada</strong></div>
<table class="ao-table">
<thead>
<tr><th>Ativo</th><th>Categoria</th><th>Quantidade</th><th>Preço médio (PM)</th><th>Valor investido</th></tr>
</thead>
<tbody>
<?php if (!$posicoes): ?>
<tr><td colspan="5" style="color: var(--ao-text-muted);">Nenhuma ação em carteira no momento.</td></tr>
<?php endif; ?>
<?php foreach ($posicoes as $p): ?>
<tr>
<td class="ao-numeric"><?= htmlspecialchars($p['ativo']) ?></td>
<td><?= ao_badge_categoria($p['categoria']) ?></td>
<td class="ao-numeric"><?= (int) $p['quantidade'] ?></td>
<td class="ao-numeric">R$ <?= number_format((float) $p['preco_medio'], 2, ',', '.') ?></td>
<td class="ao-numeric">R$ <?= number_format((float) $p['preco_medio'] * (int) $p['quantidade'], 2, ',', '.') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Histórico de operações</strong></div>
<table class="ao-table">
<thead>
<tr><th>Data</th><th>Ativo</th><th>Categoria</th><th>Conta</th><th>Tipo</th><th>Quantidade</th><th>Preço</th><th></th></tr>
</thead>
<tbody>
<?php if (!$operacoes): ?>
<tr><td colspan="8" style="color: var(--ao-text-muted);">Nenhuma operação cadastrada ainda.</td></tr>
<?php endif; ?>
<?php foreach ($operacoes as $o): ?>
<tr>
<td class="ao-numeric"><?= $o['data_operacao'] ? htmlspecialchars(date('d/m/Y', strtotime($o['data_operacao']))) : '—' ?></td>
<td class="ao-numeric"><?= htmlspecialchars($o['ativo']) ?></td>
<td><?= ao_badge_categoria($o['categoria']) ?></td>
<td><?= $o['conta_nome'] ? htmlspecialchars($o['corretora_nome'] . ' — ' . $o['conta_nome']) : '—' ?></td>
<td><span class="ao-badge <?= $o['tipo'] === 'compra' ? 'ao-badge-alta' : 'ao-badge-baixa' ?>"><?= $o['tipo'] === 'compra' ? 'Compra' : 'Venda' ?></span></td>
<td class="ao-numeric"><?= (int) $o['quantidade'] ?></td>
<td class="ao-numeric">R$ <?= number_format((float) $o['preco'], 2, ',', '.') ?></td>
<td style="white-space: nowrap; text-align: right;">
<a href="acoes.php?editar=<?= $o['id'] ?>" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Editar</a>
<form method="post" action="acoes.php" style="display:inline;" onsubmit="return confirm('Excluir esta operação? O preço médio do ativo será recalculado.');">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $o['id'] ?>">
<button type="submit" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Excluir</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<style>
/* FII não tem uma cor "pronta" nos badges do sistema (alta/baixa/alerta/info
ficam reservadas para ganho/perda e para os estados de conta/permissão) —
usa a cor de marca (--ao-primary) num tom suave, só para diferenciar. */
.ao-badge-fii { background: var(--ao-primary-soft); color: var(--ao-primary); }
</style>
<script>
function aoAbrirModalOperacao() {
document.getElementById('ao-modal-operacao').classList.add('is-open');
}
function aoFecharModalOperacao() {
document.getElementById('ao-modal-operacao').classList.remove('is-open');
// Se a modal foi aberta por um link de edição (?editar=...), limpa a URL ao fechar.
if (window.location.search) {
window.history.replaceState(null, '', 'acoes.php');
}
}
document.getElementById('ao-modal-operacao').addEventListener('click', function (evento) {
if (evento.target === this) aoFecharModalOperacao();
});
document.addEventListener('keydown', function (evento) {
if (evento.key === 'Escape') aoFecharModalOperacao();
});
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>
+322
View File
@@ -0,0 +1,322 @@
/* ============================================================
AçãoOpção — Identidade Visual (variação Cinza/Grafite)
Arquivo de variáveis e componentes base para o sistema PHP.
Inclua este arquivo após o reset/normalize do projeto:
<link rel="stylesheet" href="/assets/css/identidade-visual.css">
Fontes (Google Fonts) — adicione no <head> antes deste arquivo:
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
============================================================ */
:root {
/* ---------- Paleta cinza/grafite (marca) ---------- */
--ao-neutral-50: #F3F1ED; /* fornecida */
--ao-neutral-100: #E6E3DB;
--ao-neutral-200: #D7D1C6; /* fornecida */
--ao-neutral-300: #AAA8A5;
--ao-neutral-500: #6B6F77; /* fornecida — cor primária */
--ao-neutral-700: #3C3F45; /* fornecida */
--ao-neutral-800: #33353A;
--ao-neutral-900: #2B2D31; /* fornecida */
/* ---------- Neutros de apoio (mesma escala — paleta já é neutra) ---------- */
--ao-gray-50: var(--ao-neutral-50);
--ao-gray-100: var(--ao-neutral-100);
--ao-gray-200: var(--ao-neutral-200);
--ao-gray-300: var(--ao-neutral-300);
--ao-gray-400: #8B8D8F;
--ao-gray-600: #55575C;
--ao-gray-800: var(--ao-neutral-800);
--ao-gray-900: var(--ao-neutral-900);
--ao-white: #FFFFFF;
/* ---------- Cores semânticas (mercado) — universais, não mudam com a marca ---------- */
--ao-alta: #3E8C4B; /* alta / ganho / positivo — usada em preenchimentos sólidos */
--ao-alta-bg: #E4F1E6;
--ao-alta-text: var(--ao-alta); /* usada em texto/badge — troca de tom no modo escuro */
--ao-baixa: #C1443B; /* baixa / perda / negativo — usada em preenchimentos sólidos */
--ao-baixa-bg: #FBEAE8;
--ao-baixa-text: var(--ao-baixa);
--ao-alerta: #C98A2E; /* atenção / pendente */
--ao-alerta-bg: #FBF1E1;
--ao-alerta-text: var(--ao-alerta);
--ao-info: #3B7A9E; /* informativo / neutro-azulado */
--ao-info-bg: #E7F1F5;
--ao-info-text: var(--ao-info);
/* ---------- Papéis funcionais ---------- */
--ao-primary: var(--ao-neutral-700);
--ao-primary-hover: var(--ao-neutral-900);
--ao-primary-soft: var(--ao-neutral-100);
--ao-on-primary: var(--ao-white); /* cor do texto sobre botões/elementos "primary" */
--ao-accent: var(--ao-neutral-500);
--ao-bg-page: var(--ao-neutral-50);
--ao-bg-surface: var(--ao-white);
--ao-border: var(--ao-neutral-200);
--ao-text-strong: var(--ao-neutral-900);
--ao-text-body: var(--ao-neutral-800);
--ao-text-muted: var(--ao-gray-600);
--ao-text-on-dark: var(--ao-white);
--ao-focus-ring: rgba(107, 111, 119, 0.45);
/* ---------- Tipografia ---------- */
--ao-font-heading: 'Poppins', 'Segoe UI', Arial, sans-serif;
--ao-font-body: 'Inter', 'Segoe UI', Arial, sans-serif;
--ao-font-mono: 'IBM Plex Mono', 'Roboto Mono', Consolas, monospace; /* preços, tickers, %s */
--ao-fs-xs: 0.75rem;
--ao-fs-sm: 0.875rem;
--ao-fs-base: 1rem;
--ao-fs-lg: 1.125rem;
--ao-fs-xl: 1.375rem;
--ao-fs-2xl: 1.75rem;
--ao-fs-3xl: 2.25rem;
/* ---------- Espaçamento / raio / sombra ---------- */
--ao-space-1: 4px;
--ao-space-2: 8px;
--ao-space-3: 12px;
--ao-space-4: 16px;
--ao-space-5: 24px;
--ao-space-6: 32px;
--ao-space-8: 48px;
--ao-radius-sm: 6px;
--ao-radius-md: 10px;
--ao-radius-lg: 16px;
--ao-radius-pill: 999px;
--ao-shadow-sm: 0 1px 2px rgba(43, 45, 49, 0.07);
--ao-shadow-md: 0 4px 12px rgba(43, 45, 49, 0.09);
--ao-shadow-lg: 0 12px 32px rgba(43, 45, 49, 0.16);
}
/* ============================================================
Base
============================================================ */
body {
background: var(--ao-bg-page);
color: var(--ao-text-body);
font-family: var(--ao-font-body);
font-size: var(--ao-fs-base);
line-height: 1.55;
}
h1, h2, h3, h4, .ao-heading {
font-family: var(--ao-font-heading);
color: var(--ao-text-strong);
font-weight: 600;
line-height: 1.25;
}
a { color: var(--ao-primary); }
a:hover { color: var(--ao-primary-hover); }
.ao-numeric, .ao-price, .ao-ticker {
font-family: var(--ao-font-mono);
font-variant-numeric: tabular-nums;
}
/* ============================================================
Botões
============================================================ */
.ao-btn {
display: inline-flex;
align-items: center;
gap: var(--ao-space-2);
font-family: var(--ao-font-body);
font-weight: 600;
font-size: var(--ao-fs-sm);
padding: 10px 18px;
border-radius: var(--ao-radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.ao-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--ao-focus-ring);
}
.ao-btn-primary {
background: var(--ao-primary);
color: var(--ao-on-primary);
}
.ao-btn-primary:hover { background: var(--ao-primary-hover); }
.ao-btn-secondary {
background: var(--ao-neutral-100);
color: var(--ao-neutral-800);
}
.ao-btn-secondary:hover { background: var(--ao-neutral-200); }
.ao-btn-outline {
background: transparent;
border-color: var(--ao-primary);
color: var(--ao-primary);
}
.ao-btn-outline:hover { background: var(--ao-bg-page); }
.ao-btn-danger {
background: var(--ao-baixa);
color: var(--ao-white);
}
/* ============================================================
Cards / superfícies
============================================================ */
.ao-card {
background: var(--ao-bg-surface);
border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-lg);
box-shadow: var(--ao-shadow-sm);
padding: var(--ao-space-5);
}
.ao-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--ao-space-4);
}
/* ============================================================
Badges / indicadores de mercado
============================================================ */
.ao-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--ao-fs-xs);
font-weight: 700;
letter-spacing: .02em;
padding: 3px 10px;
border-radius: var(--ao-radius-pill);
}
.ao-badge-alta { background: var(--ao-alta-bg); color: var(--ao-alta-text); }
.ao-badge-baixa { background: var(--ao-baixa-bg); color: var(--ao-baixa-text); }
.ao-badge-alerta { background: var(--ao-alerta-bg); color: var(--ao-alerta-text); }
.ao-badge-info { background: var(--ao-info-bg); color: var(--ao-info-text); }
.ao-badge-neutro { background: var(--ao-gray-100); color: var(--ao-gray-600); }
/* ============================================================
Tabelas (cotações, posições, histórico de operações)
============================================================ */
.ao-table {
width: 100%;
border-collapse: collapse;
background: var(--ao-bg-surface);
border-radius: var(--ao-radius-md);
overflow: hidden;
}
.ao-table th {
text-align: left;
font-family: var(--ao-font-body);
font-size: var(--ao-fs-xs);
text-transform: uppercase;
letter-spacing: .04em;
color: var(--ao-text-muted);
background: var(--ao-bg-page);
padding: var(--ao-space-3) var(--ao-space-4);
border-bottom: 1px solid var(--ao-border);
}
.ao-table td {
padding: var(--ao-space-3) var(--ao-space-4);
border-bottom: 1px solid var(--ao-border);
font-size: var(--ao-fs-sm);
}
.ao-table tr:hover td { background: var(--ao-bg-page); }
.ao-table .ao-up { color: var(--ao-alta-text); }
.ao-table .ao-down { color: var(--ao-baixa-text); }
/* ============================================================
Formulários
============================================================ */
.ao-field label {
display: block;
font-size: var(--ao-fs-sm);
font-weight: 600;
color: var(--ao-text-strong);
margin-bottom: var(--ao-space-1);
}
.ao-field input,
.ao-field select,
.ao-field textarea {
width: 100%;
font-family: var(--ao-font-body);
font-size: var(--ao-fs-sm);
padding: 10px 12px;
border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-sm);
background: var(--ao-bg-surface);
color: var(--ao-text-body);
}
.ao-field input:focus,
.ao-field select:focus,
.ao-field textarea:focus {
outline: none;
border-color: var(--ao-primary);
box-shadow: 0 0 0 3px var(--ao-focus-ring);
}
/* ============================================================
Topbar / navegação
============================================================ */
.ao-topbar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--ao-neutral-900);
color: var(--ao-white);
padding: var(--ao-space-3) var(--ao-space-5);
}
.ao-topbar a { color: var(--ao-neutral-200); }
.ao-topbar .ao-brand {
display: flex;
align-items: center;
gap: var(--ao-space-2);
font-family: var(--ao-font-heading);
font-weight: 700;
color: var(--ao-white);
}
/* ============================================================
Modo escuro
Ative adicionando data-ao-mode="escuro" na tag <html>.
Como todos os componentes acima já leem cores por variável,
o modo escuro é só uma troca de tokens — nenhuma regra de
componente precisa ser duplicada.
============================================================ */
html[data-ao-mode="escuro"] {
--ao-bg-page: #151619;
--ao-bg-surface: #212326;
--ao-border: #323439;
--ao-text-strong: #F4F5F5;
--ao-text-body: #DBDDE1;
--ao-text-muted: #89909F;
--ao-primary: var(--ao-neutral-300);
--ao-primary-hover: var(--ao-neutral-200);
--ao-primary-soft: #323439;
--ao-on-primary: var(--ao-neutral-900);
--ao-focus-ring: rgba(170, 168, 165, 0.5);
--ao-shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
--ao-shadow-md: 0 6px 16px rgba(0, 0, 0, .45);
--ao-shadow-lg: 0 18px 40px rgba(0, 0, 0, .55);
/* as cores "sólidas" (--ao-alta/--ao-baixa/...) continuam iguais —
são usadas em botões de preenchimento cheio e já têm contraste
garantido. Só as versões usadas como TEXTO sobre fundo escuro
(badges, tabela) ficam mais claras para manter legibilidade. */
--ao-alta-text: #7ED17A;
--ao-alta-bg: rgba(126, 209, 122, .16);
--ao-baixa-text: #FF8A7D;
--ao-baixa-bg: rgba(255, 138, 125, .16);
--ao-alerta-text: #F0B35C;
--ao-alerta-bg: rgba(240, 179, 92, .16);
--ao-info-text: #6FC1EE;
--ao-info-bg: rgba(111, 193, 238, .16);
}
html[data-ao-mode="escuro"] .ao-btn-secondary { background: var(--ao-border); color: var(--ao-text-body); }
html[data-ao-mode="escuro"] .ao-badge-neutro { background: rgba(255, 255, 255, .08); color: var(--ao-text-muted); }
+322
View File
@@ -0,0 +1,322 @@
/* ============================================================
AçãoOpção — Identidade Visual (variação Marrom)
Arquivo de variáveis e componentes base para o sistema PHP.
Inclua este arquivo após o reset/normalize do projeto:
<link rel="stylesheet" href="/assets/css/identidade-visual.css">
Fontes (Google Fonts) — adicione no <head> antes deste arquivo:
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
============================================================ */
:root {
/* ---------- Paleta marrom (marca) ---------- */
--ao-brown-50: #F3E9DC; /* fornecida */
--ao-brown-100: #E6D6BF;
--ao-brown-200: #D8C2A4; /* fornecida */
--ao-brown-300: #C4A17A;
--ao-brown-500: #A6754C; /* fornecida — cor primária */
--ao-brown-700: #6B3F2A; /* fornecida */
--ao-brown-800: #533221;
--ao-brown-900: #3B2418; /* fornecida */
/* ---------- Neutros (cinza com sub-tom marrom) ---------- */
--ao-gray-50: #F8F6F3;
--ao-gray-100: #F0EBE4;
--ao-gray-200: #DED4C8;
--ao-gray-300: #C2B4A3;
--ao-gray-400: #A0917F;
--ao-gray-600: #6B5D4E;
--ao-gray-800: #392F26;
--ao-gray-900: #201A15;
--ao-white: #FFFFFF;
/* ---------- Cores semânticas (mercado) — universais, não mudam com a marca ---------- */
--ao-alta: #4C8C3F; /* alta / ganho / positivo — usada em preenchimentos sólidos */
--ao-alta-bg: #E5F1DE;
--ao-alta-text: var(--ao-alta); /* usada em texto/badge — troca de tom no modo escuro */
--ao-baixa: #C1443B; /* baixa / perda / negativo — usada em preenchimentos sólidos */
--ao-baixa-bg: #FBEAE8;
--ao-baixa-text: var(--ao-baixa);
--ao-alerta: #C98A2E; /* atenção / pendente */
--ao-alerta-bg: #FBF1E1;
--ao-alerta-text: var(--ao-alerta);
--ao-info: #3B7A9E; /* informativo / neutro-azulado */
--ao-info-bg: #E7F1F5;
--ao-info-text: var(--ao-info);
/* ---------- Papéis funcionais ---------- */
--ao-primary: var(--ao-brown-700);
--ao-primary-hover: var(--ao-brown-800);
--ao-primary-soft: var(--ao-brown-100);
--ao-on-primary: var(--ao-white); /* cor do texto sobre botões/elementos "primary" */
--ao-accent: var(--ao-brown-500);
--ao-bg-page: var(--ao-brown-50);
--ao-bg-surface: var(--ao-white);
--ao-border: var(--ao-gray-200);
--ao-text-strong: var(--ao-gray-900);
--ao-text-body: var(--ao-gray-800);
--ao-text-muted: var(--ao-gray-600);
--ao-text-on-dark: var(--ao-white);
--ao-focus-ring: rgba(166, 117, 76, 0.45);
/* ---------- Tipografia ---------- */
--ao-font-heading: 'Poppins', 'Segoe UI', Arial, sans-serif;
--ao-font-body: 'Inter', 'Segoe UI', Arial, sans-serif;
--ao-font-mono: 'IBM Plex Mono', 'Roboto Mono', Consolas, monospace; /* preços, tickers, %s */
--ao-fs-xs: 0.75rem;
--ao-fs-sm: 0.875rem;
--ao-fs-base: 1rem;
--ao-fs-lg: 1.125rem;
--ao-fs-xl: 1.375rem;
--ao-fs-2xl: 1.75rem;
--ao-fs-3xl: 2.25rem;
/* ---------- Espaçamento / raio / sombra ---------- */
--ao-space-1: 4px;
--ao-space-2: 8px;
--ao-space-3: 12px;
--ao-space-4: 16px;
--ao-space-5: 24px;
--ao-space-6: 32px;
--ao-space-8: 48px;
--ao-radius-sm: 6px;
--ao-radius-md: 10px;
--ao-radius-lg: 16px;
--ao-radius-pill: 999px;
--ao-shadow-sm: 0 1px 2px rgba(32, 26, 21, 0.07);
--ao-shadow-md: 0 4px 12px rgba(32, 26, 21, 0.09);
--ao-shadow-lg: 0 12px 32px rgba(32, 26, 21, 0.16);
}
/* ============================================================
Base
============================================================ */
body {
background: var(--ao-bg-page);
color: var(--ao-text-body);
font-family: var(--ao-font-body);
font-size: var(--ao-fs-base);
line-height: 1.55;
}
h1, h2, h3, h4, .ao-heading {
font-family: var(--ao-font-heading);
color: var(--ao-text-strong);
font-weight: 600;
line-height: 1.25;
}
a { color: var(--ao-primary); }
a:hover { color: var(--ao-primary-hover); }
.ao-numeric, .ao-price, .ao-ticker {
font-family: var(--ao-font-mono);
font-variant-numeric: tabular-nums;
}
/* ============================================================
Botões
============================================================ */
.ao-btn {
display: inline-flex;
align-items: center;
gap: var(--ao-space-2);
font-family: var(--ao-font-body);
font-weight: 600;
font-size: var(--ao-fs-sm);
padding: 10px 18px;
border-radius: var(--ao-radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.ao-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--ao-focus-ring);
}
.ao-btn-primary {
background: var(--ao-primary);
color: var(--ao-on-primary);
}
.ao-btn-primary:hover { background: var(--ao-primary-hover); }
.ao-btn-secondary {
background: var(--ao-brown-100);
color: var(--ao-brown-800);
}
.ao-btn-secondary:hover { background: var(--ao-brown-200); }
.ao-btn-outline {
background: transparent;
border-color: var(--ao-primary);
color: var(--ao-primary);
}
.ao-btn-outline:hover { background: var(--ao-bg-page); }
.ao-btn-danger {
background: var(--ao-baixa);
color: var(--ao-white);
}
/* ============================================================
Cards / superfícies
============================================================ */
.ao-card {
background: var(--ao-bg-surface);
border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-lg);
box-shadow: var(--ao-shadow-sm);
padding: var(--ao-space-5);
}
.ao-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--ao-space-4);
}
/* ============================================================
Badges / indicadores de mercado
============================================================ */
.ao-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--ao-fs-xs);
font-weight: 700;
letter-spacing: .02em;
padding: 3px 10px;
border-radius: var(--ao-radius-pill);
}
.ao-badge-alta { background: var(--ao-alta-bg); color: var(--ao-alta-text); }
.ao-badge-baixa { background: var(--ao-baixa-bg); color: var(--ao-baixa-text); }
.ao-badge-alerta { background: var(--ao-alerta-bg); color: var(--ao-alerta-text); }
.ao-badge-info { background: var(--ao-info-bg); color: var(--ao-info-text); }
.ao-badge-neutro { background: var(--ao-gray-100); color: var(--ao-gray-600); }
/* ============================================================
Tabelas (cotações, posições, histórico de operações)
============================================================ */
.ao-table {
width: 100%;
border-collapse: collapse;
background: var(--ao-bg-surface);
border-radius: var(--ao-radius-md);
overflow: hidden;
}
.ao-table th {
text-align: left;
font-family: var(--ao-font-body);
font-size: var(--ao-fs-xs);
text-transform: uppercase;
letter-spacing: .04em;
color: var(--ao-text-muted);
background: var(--ao-bg-page);
padding: var(--ao-space-3) var(--ao-space-4);
border-bottom: 1px solid var(--ao-border);
}
.ao-table td {
padding: var(--ao-space-3) var(--ao-space-4);
border-bottom: 1px solid var(--ao-border);
font-size: var(--ao-fs-sm);
}
.ao-table tr:hover td { background: var(--ao-bg-page); }
.ao-table .ao-up { color: var(--ao-alta-text); }
.ao-table .ao-down { color: var(--ao-baixa-text); }
/* ============================================================
Formulários
============================================================ */
.ao-field label {
display: block;
font-size: var(--ao-fs-sm);
font-weight: 600;
color: var(--ao-text-strong);
margin-bottom: var(--ao-space-1);
}
.ao-field input,
.ao-field select,
.ao-field textarea {
width: 100%;
font-family: var(--ao-font-body);
font-size: var(--ao-fs-sm);
padding: 10px 12px;
border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-sm);
background: var(--ao-bg-surface);
color: var(--ao-text-body);
}
.ao-field input:focus,
.ao-field select:focus,
.ao-field textarea:focus {
outline: none;
border-color: var(--ao-primary);
box-shadow: 0 0 0 3px var(--ao-focus-ring);
}
/* ============================================================
Topbar / navegação
============================================================ */
.ao-topbar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--ao-brown-900);
color: var(--ao-white);
padding: var(--ao-space-3) var(--ao-space-5);
}
.ao-topbar a { color: var(--ao-brown-100); }
.ao-topbar .ao-brand {
display: flex;
align-items: center;
gap: var(--ao-space-2);
font-family: var(--ao-font-heading);
font-weight: 700;
color: var(--ao-white);
}
/* ============================================================
Modo escuro
Ative adicionando data-ao-mode="escuro" na tag <html>.
Como todos os componentes acima já leem cores por variável,
o modo escuro é só uma troca de tokens — nenhuma regra de
componente precisa ser duplicada.
============================================================ */
html[data-ao-mode="escuro"] {
--ao-bg-page: #21140D;
--ao-bg-surface: #331F15;
--ao-border: #483023;
--ao-text-strong: #F6F5F4;
--ao-text-body: #E3DCD9;
--ao-text-muted: #AA8D7D;
--ao-primary: var(--ao-brown-300);
--ao-primary-hover: var(--ao-brown-200);
--ao-primary-soft: #483023;
--ao-on-primary: var(--ao-brown-900);
--ao-focus-ring: rgba(196, 161, 122, 0.5);
--ao-shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
--ao-shadow-md: 0 6px 16px rgba(0, 0, 0, .45);
--ao-shadow-lg: 0 18px 40px rgba(0, 0, 0, .55);
/* as cores "sólidas" (--ao-alta/--ao-baixa/...) continuam iguais —
são usadas em botões de preenchimento cheio e já têm contraste
garantido. Só as versões usadas como TEXTO sobre fundo escuro
(badges, tabela) ficam mais claras para manter legibilidade. */
--ao-alta-text: #7ED17A;
--ao-alta-bg: rgba(126, 209, 122, .16);
--ao-baixa-text: #FF8A7D;
--ao-baixa-bg: rgba(255, 138, 125, .16);
--ao-alerta-text: #F0B35C;
--ao-alerta-bg: rgba(240, 179, 92, .16);
--ao-info-text: #6FC1EE;
--ao-info-bg: rgba(111, 193, 238, .16);
}
html[data-ao-mode="escuro"] .ao-btn-secondary { background: var(--ao-border); color: var(--ao-text-body); }
html[data-ao-mode="escuro"] .ao-badge-neutro { background: rgba(255, 255, 255, .08); color: var(--ao-text-muted); }
+322
View File
@@ -0,0 +1,322 @@
/* ============================================================
AçãoOpção — Identidade Visual
Arquivo de variáveis e componentes base para o sistema PHP.
Inclua este arquivo após o reset/normalize do projeto:
<link rel="stylesheet" href="/assets/css/identidade-visual.css">
Fontes (Google Fonts) — adicione no <head> antes deste arquivo:
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
============================================================ */
:root {
/* ---------- Paleta verde (marca) ---------- */
--ao-green-50: #F7F9F4;
--ao-green-100: #EBEFE6;
--ao-green-200: #CFE0BC; /* fornecida */
--ao-green-300: #99CD85; /* fornecida */
--ao-green-500: #7FA653; /* fornecida — cor primária */
--ao-green-700: #63783D; /* fornecida */
--ao-green-800: #3D4A26;
--ao-green-900: #212915;
/* ---------- Neutros (cinza com sub-tom verde) ---------- */
--ao-gray-50: #F7F8F6;
--ao-gray-100: #EEF0EC;
--ao-gray-200: #DDE1D8;
--ao-gray-300: #C3C9BE;
--ao-gray-400: #9CA69A;
--ao-gray-600: #5D665A;
--ao-gray-800: #33392F;
--ao-gray-900: #1B1F17;
--ao-white: #FFFFFF;
/* ---------- Cores semânticas (mercado) ---------- */
--ao-alta: #4C8C3F; /* alta / ganho / positivo — usada em preenchimentos sólidos */
--ao-alta-bg: #E5F1DE;
--ao-alta-text: var(--ao-alta); /* usada em texto/badge — troca de tom no modo escuro */
--ao-baixa: #C1443B; /* baixa / perda / negativo — usada em preenchimentos sólidos */
--ao-baixa-bg: #FBEAE8;
--ao-baixa-text: var(--ao-baixa);
--ao-alerta: #C98A2E; /* atenção / pendente */
--ao-alerta-bg: #FBF1E1;
--ao-alerta-text: var(--ao-alerta);
--ao-info: #3B7A9E; /* informativo / neutro-azulado */
--ao-info-bg: #E7F1F5;
--ao-info-text: var(--ao-info);
/* ---------- Papéis funcionais ---------- */
--ao-primary: var(--ao-green-700);
--ao-primary-hover: var(--ao-green-800);
--ao-primary-soft: var(--ao-green-100);
--ao-on-primary: var(--ao-white); /* cor do texto sobre botões/elementos "primary" */
--ao-accent: var(--ao-green-500);
--ao-bg-page: var(--ao-green-50);
--ao-bg-surface: var(--ao-white);
--ao-border: var(--ao-gray-200);
--ao-text-strong: var(--ao-gray-900);
--ao-text-body: var(--ao-gray-800);
--ao-text-muted: var(--ao-gray-600);
--ao-text-on-dark: var(--ao-white);
--ao-focus-ring: rgba(127, 166, 83, 0.45);
/* ---------- Tipografia ---------- */
--ao-font-heading: 'Poppins', 'Segoe UI', Arial, sans-serif;
--ao-font-body: 'Inter', 'Segoe UI', Arial, sans-serif;
--ao-font-mono: 'IBM Plex Mono', 'Roboto Mono', Consolas, monospace; /* preços, tickers, %s */
--ao-fs-xs: 0.75rem;
--ao-fs-sm: 0.875rem;
--ao-fs-base: 1rem;
--ao-fs-lg: 1.125rem;
--ao-fs-xl: 1.375rem;
--ao-fs-2xl: 1.75rem;
--ao-fs-3xl: 2.25rem;
/* ---------- Espaçamento / raio / sombra ---------- */
--ao-space-1: 4px;
--ao-space-2: 8px;
--ao-space-3: 12px;
--ao-space-4: 16px;
--ao-space-5: 24px;
--ao-space-6: 32px;
--ao-space-8: 48px;
--ao-radius-sm: 6px;
--ao-radius-md: 10px;
--ao-radius-lg: 16px;
--ao-radius-pill: 999px;
--ao-shadow-sm: 0 1px 2px rgba(33, 41, 21, 0.06);
--ao-shadow-md: 0 4px 12px rgba(33, 41, 21, 0.08);
--ao-shadow-lg: 0 12px 32px rgba(33, 41, 21, 0.14);
}
/* ============================================================
Base
============================================================ */
body {
background: var(--ao-bg-page);
color: var(--ao-text-body);
font-family: var(--ao-font-body);
font-size: var(--ao-fs-base);
line-height: 1.55;
}
h1, h2, h3, h4, .ao-heading {
font-family: var(--ao-font-heading);
color: var(--ao-text-strong);
font-weight: 600;
line-height: 1.25;
}
a { color: var(--ao-primary); }
a:hover { color: var(--ao-primary-hover); }
.ao-numeric, .ao-price, .ao-ticker {
font-family: var(--ao-font-mono);
font-variant-numeric: tabular-nums;
}
/* ============================================================
Botões
============================================================ */
.ao-btn {
display: inline-flex;
align-items: center;
gap: var(--ao-space-2);
font-family: var(--ao-font-body);
font-weight: 600;
font-size: var(--ao-fs-sm);
padding: 10px 18px;
border-radius: var(--ao-radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.ao-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--ao-focus-ring);
}
.ao-btn-primary {
background: var(--ao-primary);
color: var(--ao-on-primary);
}
.ao-btn-primary:hover { background: var(--ao-primary-hover); }
.ao-btn-secondary {
background: var(--ao-green-100);
color: var(--ao-green-800);
}
.ao-btn-secondary:hover { background: var(--ao-green-200); }
.ao-btn-outline {
background: transparent;
border-color: var(--ao-primary);
color: var(--ao-primary);
}
.ao-btn-outline:hover { background: var(--ao-bg-page); }
.ao-btn-danger {
background: var(--ao-baixa);
color: var(--ao-white);
}
/* ============================================================
Cards / superfícies
============================================================ */
.ao-card {
background: var(--ao-bg-surface);
border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-lg);
box-shadow: var(--ao-shadow-sm);
padding: var(--ao-space-5);
}
.ao-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--ao-space-4);
}
/* ============================================================
Badges / indicadores de mercado
============================================================ */
.ao-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--ao-fs-xs);
font-weight: 700;
letter-spacing: .02em;
padding: 3px 10px;
border-radius: var(--ao-radius-pill);
}
.ao-badge-alta { background: var(--ao-alta-bg); color: var(--ao-alta-text); }
.ao-badge-baixa { background: var(--ao-baixa-bg); color: var(--ao-baixa-text); }
.ao-badge-alerta { background: var(--ao-alerta-bg); color: var(--ao-alerta-text); }
.ao-badge-info { background: var(--ao-info-bg); color: var(--ao-info-text); }
.ao-badge-neutro { background: var(--ao-gray-100); color: var(--ao-gray-600); }
/* ============================================================
Tabelas (cotações, posições, histórico de operações)
============================================================ */
.ao-table {
width: 100%;
border-collapse: collapse;
background: var(--ao-bg-surface);
border-radius: var(--ao-radius-md);
overflow: hidden;
}
.ao-table th {
text-align: left;
font-family: var(--ao-font-body);
font-size: var(--ao-fs-xs);
text-transform: uppercase;
letter-spacing: .04em;
color: var(--ao-text-muted);
background: var(--ao-bg-page);
padding: var(--ao-space-3) var(--ao-space-4);
border-bottom: 1px solid var(--ao-border);
}
.ao-table td {
padding: var(--ao-space-3) var(--ao-space-4);
border-bottom: 1px solid var(--ao-border);
font-size: var(--ao-fs-sm);
}
.ao-table tr:hover td { background: var(--ao-bg-page); }
.ao-table .ao-up { color: var(--ao-alta-text); }
.ao-table .ao-down { color: var(--ao-baixa-text); }
/* ============================================================
Formulários
============================================================ */
.ao-field label {
display: block;
font-size: var(--ao-fs-sm);
font-weight: 600;
color: var(--ao-text-strong);
margin-bottom: var(--ao-space-1);
}
.ao-field input,
.ao-field select,
.ao-field textarea {
width: 100%;
font-family: var(--ao-font-body);
font-size: var(--ao-fs-sm);
padding: 10px 12px;
border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-sm);
background: var(--ao-bg-surface);
color: var(--ao-text-body);
}
.ao-field input:focus,
.ao-field select:focus,
.ao-field textarea:focus {
outline: none;
border-color: var(--ao-primary);
box-shadow: 0 0 0 3px var(--ao-focus-ring);
}
/* ============================================================
Topbar / navegação
============================================================ */
.ao-topbar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--ao-green-900);
color: var(--ao-white);
padding: var(--ao-space-3) var(--ao-space-5);
}
.ao-topbar a { color: var(--ao-green-100); }
.ao-topbar .ao-brand {
display: flex;
align-items: center;
gap: var(--ao-space-2);
font-family: var(--ao-font-heading);
font-weight: 700;
color: var(--ao-white);
}
/* ============================================================
Modo escuro
Ative adicionando data-ao-mode="escuro" na tag <html>.
Como todos os componentes acima já leem cores por variável,
o modo escuro é só uma troca de tokens — nenhuma regra de
componente precisa ser duplicada.
============================================================ */
html[data-ao-mode="escuro"] {
--ao-bg-page: #191F0F;
--ao-bg-surface: #272F18;
--ao-border: #3B4724;
--ao-text-strong: #F5F5F4;
--ao-text-body: #DFE2DA;
--ao-text-muted: #99A682;
--ao-primary: var(--ao-green-300);
--ao-primary-hover: var(--ao-green-200);
--ao-primary-soft: #3B4724;
--ao-on-primary: var(--ao-green-900);
--ao-focus-ring: rgba(153, 205, 133, 0.5);
--ao-shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
--ao-shadow-md: 0 6px 16px rgba(0, 0, 0, .45);
--ao-shadow-lg: 0 18px 40px rgba(0, 0, 0, .55);
/* as cores "sólidas" (--ao-alta/--ao-baixa/...) continuam iguais —
são usadas em botões de preenchimento cheio e já têm contraste
garantido. Só as versões usadas como TEXTO sobre fundo escuro
(badges, tabela) ficam mais claras para manter legibilidade. */
--ao-alta-text: #7ED17A;
--ao-alta-bg: rgba(126, 209, 122, .16);
--ao-baixa-text: #FF8A7D;
--ao-baixa-bg: rgba(255, 138, 125, .16);
--ao-alerta-text: #F0B35C;
--ao-alerta-bg: rgba(240, 179, 92, .16);
--ao-info-text: #6FC1EE;
--ao-info-bg: rgba(111, 193, 238, .16);
}
html[data-ao-mode="escuro"] .ao-btn-secondary { background: var(--ao-border); color: var(--ao-text-body); }
html[data-ao-mode="escuro"] .ao-badge-neutro { background: rgba(255, 255, 255, .08); color: var(--ao-text-muted); }
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="512" height="512">
<defs><linearGradient id="aoBadgeGradCinza" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#AAA8A5"/><stop offset="100%" stop-color="#2B2D31"/></linearGradient></defs>
<rect x="0" y="0" width="100" height="100" rx="24" fill="url(#aoBadgeGradCinza)"/>
<line x1="25.5" y1="62" x2="25.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<line x1="40.5" y1="52" x2="40.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<line x1="55.5" y1="40" x2="55.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<rect x="21" y="66" width="9" height="14" rx="2.2" fill="#FFFFFF"/>
<rect x="36" y="56" width="9" height="24" rx="2.2" fill="#FFFFFF"/>
<rect x="51" y="44" width="9" height="36" rx="2.2" fill="#FFFFFF"/>
<path d="M 55.5 44 L 55.5 32 L 46 21 M 55.5 32 L 66 19" fill="none" stroke="#FFFFFF" stroke-width="4.4" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="46" cy="21" r="3.4" fill="#FFFFFF"/>
<circle cx="66" cy="19" r="3.4" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="512" height="512">
<defs><linearGradient id="aoBadgeGradMarrom" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#C4A17A"/><stop offset="100%" stop-color="#6B3F2A"/></linearGradient></defs>
<rect x="0" y="0" width="100" height="100" rx="24" fill="url(#aoBadgeGradMarrom)"/>
<line x1="25.5" y1="62" x2="25.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<line x1="40.5" y1="52" x2="40.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<line x1="55.5" y1="40" x2="55.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<rect x="21" y="66" width="9" height="14" rx="2.2" fill="#FFFFFF"/>
<rect x="36" y="56" width="9" height="24" rx="2.2" fill="#FFFFFF"/>
<rect x="51" y="44" width="9" height="36" rx="2.2" fill="#FFFFFF"/>
<path d="M 55.5 44 L 55.5 32 L 46 21 M 55.5 32 L 66 19" fill="none" stroke="#FFFFFF" stroke-width="4.4" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="46" cy="21" r="3.4" fill="#FFFFFF"/>
<circle cx="66" cy="19" r="3.4" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="512" height="512">
<defs><linearGradient id="aoBadgeGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#99CD85"/><stop offset="100%" stop-color="#63783D"/></linearGradient></defs>
<rect x="0" y="0" width="100" height="100" rx="24" fill="url(#aoBadgeGrad)"/>
<line x1="25.5" y1="62" x2="25.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<line x1="40.5" y1="52" x2="40.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<line x1="55.5" y1="40" x2="55.5" y2="84" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
<rect x="21" y="66" width="9" height="14" rx="2.2" fill="#FFFFFF"/>
<rect x="36" y="56" width="9" height="24" rx="2.2" fill="#FFFFFF"/>
<rect x="51" y="44" width="9" height="36" rx="2.2" fill="#FFFFFF"/>
<path d="M 55.5 44 L 55.5 32 L 46 21 M 55.5 32 L 66 19" fill="none" stroke="#FFFFFF" stroke-width="4.4" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="46" cy="21" r="3.4" fill="#FFFFFF"/>
<circle cx="66" cy="19" r="3.4" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+87
View File
@@ -0,0 +1,87 @@
<?php
$titulo_pagina = 'Configurações';
$pagina_ativa = 'configuracoes';
// Precisamos de config.php/auth.php já carregados antes do form processar o
// POST (header.php também os carrega, mas fazemos isso explicitamente aqui em
// cima para: 1) checar a permissão antes de mexer no ambiente; 2) usar
// ao_salvar_ambiente()/AO_URLS antes de qualquer saída).
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_permissao('configuracoes');
$ao_ambiente_nomes = [
'desenvolvimento' => 'Desenvolvimento',
'producao' => 'Produção',
];
$ao_mensagem = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ambiente'])) {
if (ao_salvar_ambiente($_POST['ambiente'])) {
// Redireciona para evitar reenvio do form ao atualizar a página.
header('Location: configuracoes.php?salvo=1');
exit;
} else {
$ao_mensagem = ['tipo' => 'erro', 'texto' => 'Ambiente inválido — nada foi alterado.'];
}
}
if (isset($_GET['salvo'])) {
$ao_mensagem = ['tipo' => 'sucesso', 'texto' => 'Ambiente atualizado com sucesso.'];
}
require __DIR__ . '/includes/header.php';
?>
<h1 class="ao-page-title">Configurações do sistema</h1>
<?php if ($ao_mensagem): ?>
<div class="ao-badge <?= $ao_mensagem['tipo'] === 'sucesso' ? 'ao-badge-alta' : 'ao-badge-baixa' ?>" style="display:inline-block; margin-bottom: var(--ao-space-5); padding: 8px 14px; font-size: var(--ao-fs-sm);">
<?= htmlspecialchars($ao_mensagem['texto']) ?>
</div>
<?php endif; ?>
<div class="ao-card" style="max-width: 560px; margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong>Ambiente</strong></div>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-sm); margin-top: 0;">
Define a URL base usada pelo sistema. Em desenvolvimento, use o XAMPP local;
em produção, use o domínio publicado.
</p>
<form method="post" action="configuracoes.php">
<?php foreach (AO_URLS as $chave => $url): ?>
<label style="display:flex; align-items:flex-start; gap: 10px; padding: var(--ao-space-3) 0; border-bottom: 1px solid var(--ao-border); cursor: pointer;">
<input type="radio" name="ambiente" value="<?= htmlspecialchars($chave) ?>" <?= AO_AMBIENTE === $chave ? 'checked' : '' ?> style="margin-top: 3px;">
<span>
<strong style="display:block;"><?= htmlspecialchars($ao_ambiente_nomes[$chave] ?? $chave) ?></strong>
<span class="ao-numeric" style="color: var(--ao-text-muted); font-size: var(--ao-fs-xs);"><?= htmlspecialchars($url) ?></span>
</span>
</label>
<?php endforeach; ?>
<button type="submit" class="ao-btn ao-btn-primary" style="margin-top: var(--ao-space-5);">Salvar ambiente</button>
</form>
</div>
<div class="ao-card" style="max-width: 560px;">
<div class="ao-card-header"><strong>Ambiente ativo</strong></div>
<table class="ao-table">
<tbody>
<tr><td>Ambiente</td><td class="ao-numeric"><?= htmlspecialchars($ao_ambiente_nomes[AO_AMBIENTE] ?? AO_AMBIENTE) ?></td></tr>
<tr><td>URL base</td><td class="ao-numeric"><?= htmlspecialchars(AO_BASE_URL) ?></td></tr>
<tr><td>HTTPS obrigatório</td><td class="ao-numeric"><?= AO_IS_PRODUCAO ? 'Sim' : 'Não' ?></td></tr>
<tr><td>Banco de dados (host)</td><td class="ao-numeric"><?= htmlspecialchars(AO_DB_HOST) ?></td></tr>
<tr><td>Banco de dados (nome)</td><td class="ao-numeric"><?= htmlspecialchars(AO_DB_NOME) ?></td></tr>
<tr><td>Usuário do banco</td><td class="ao-numeric"><?= htmlspecialchars(AO_DB_USUARIO) ?></td></tr>
</tbody>
</table>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-xs); margin-bottom: 0;">
A senha do banco não é exibida aqui por segurança. Para alterar usuário/senha de cada
ambiente, edite <code>includes/config.php</code> (bloco <code>AO_BANCOS</code>) diretamente no servidor.
</p>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
+177
View File
@@ -0,0 +1,177 @@
<?php
$titulo_pagina = 'Contas';
$pagina_ativa = 'contas';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_permissao('contas');
// Cada usuário só vê e só mexe nas próprias contas — toda consulta sobre a
// tabela "contas" abaixo é sempre filtrada também por usuario_id. A
// corretora em si é um catálogo global (não é filtrada por usuário).
$ao_usuario_id = (int) ao_usuario_logado()['id'];
const AO_MAX_CONTAS_POR_CORRETORA = 3;
$erro = null;
$sucesso = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'excluir') {
$id = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare('SELECT COUNT(*) FROM acoes WHERE conta_id = ? AND usuario_id = ?');
$stmt->execute([$id, $ao_usuario_id]);
if ((int) $stmt->fetchColumn() > 0) {
$erro = 'Não é possível excluir: esta conta já tem ações registradas nela.';
} else {
$pdo->prepare('DELETE FROM contas WHERE id = ? AND usuario_id = ?')->execute([$id, $ao_usuario_id]);
header('Location: contas.php?excluido=1');
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') {
$id = (int) ($_POST['id'] ?? 0);
$corretora_id = (int) ($_POST['corretora_id'] ?? 0);
$nome = trim($_POST['nome'] ?? '');
// A corretora é um catálogo global — só precisa existir, não precisa
// pertencer a este usuário.
$stmtCorretora = $pdo->prepare('SELECT id FROM corretoras WHERE id = ?');
$stmtCorretora->execute([$corretora_id]);
$corretoraValida = (bool) $stmtCorretora->fetch();
if ($nome === '' || !$corretoraValida) {
$erro = 'Selecione uma corretora válida e informe o nome da conta.';
} else {
// O limite de até 3 contas é por usuário dentro de cada corretora —
// não pelo total de contas de todo mundo naquela corretora.
$stmtCount = $pdo->prepare('SELECT COUNT(*) FROM contas WHERE corretora_id = ? AND usuario_id = ? AND id != ?');
$stmtCount->execute([$corretora_id, $ao_usuario_id, $id ?: 0]);
if ((int) $stmtCount->fetchColumn() >= AO_MAX_CONTAS_POR_CORRETORA) {
$erro = 'Esta corretora já tem o máximo de ' . AO_MAX_CONTAS_POR_CORRETORA . ' contas.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE contas SET corretora_id = ?, nome = ? WHERE id = ? AND usuario_id = ?');
$stmt->execute([$corretora_id, $nome, $id, $ao_usuario_id]);
} else {
$stmt = $pdo->prepare('INSERT INTO contas (corretora_id, usuario_id, nome) VALUES (?, ?, ?)');
$stmt->execute([$corretora_id, $ao_usuario_id, $nome]);
}
header('Location: contas.php?salvo=1');
exit;
} catch (PDOException $e) {
$erro = ($e->getCode() === '23000') ? 'Essa corretora já tem uma conta com esse nome.' : ('Erro ao salvar: ' . $e->getMessage());
}
}
}
}
$editando = null;
if (isset($_GET['editar'])) {
$stmt = $pdo->prepare('SELECT * FROM contas WHERE id = ? AND usuario_id = ?');
$stmt->execute([(int) $_GET['editar'], $ao_usuario_id]);
$editando = $stmt->fetch() ?: null;
} elseif (isset($_GET['corretora'])) {
// Vindo de "Ver contas" em Corretoras: só pré-seleciona a corretora no formulário.
$editando = ['id' => 0, 'corretora_id' => (int) $_GET['corretora'], 'nome' => ''];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar' && $erro) {
$editando = ['id' => $id, 'corretora_id' => $corretora_id, 'nome' => $nome];
}
if (isset($_GET['salvo'])) $sucesso = 'Conta salva com sucesso.';
if (isset($_GET['excluido'])) $sucesso = 'Conta excluída.';
// Corretora é global: a lista de opções é a mesma para todos os usuários.
$stmtCorretoras = $pdo->query('SELECT id, nome FROM corretoras ORDER BY nome');
$corretoras = $stmtCorretoras->fetchAll();
$stmt = $pdo->prepare(
'SELECT ct.*, cr.nome AS corretora_nome, (SELECT COUNT(*) FROM acoes a WHERE a.conta_id = ct.id) AS total_acoes
FROM contas ct
INNER JOIN corretoras cr ON cr.id = ct.corretora_id
WHERE ct.usuario_id = ?
ORDER BY cr.nome, ct.nome'
);
$stmt->execute([$ao_usuario_id]);
$contas = $stmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<h1 class="ao-page-title">Contas</h1>
<?php if ($erro): ?>
<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; ?>
<?php if (!$corretoras): ?>
<div class="ao-card" style="max-width: 480px; margin-bottom: var(--ao-space-6);">
<p style="margin: 0;">Nenhuma corretora cadastrada ainda no sistema. <a href="corretoras.php">Cadastre uma corretora</a> antes de criar contas.</p>
</div>
<?php else: ?>
<div class="ao-card" style="max-width: 480px; margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong><?= $editando && !empty($editando['id']) ? 'Editar conta' : 'Nova conta' ?></strong></div>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-sm); margin-top: 0;">Você pode ter até 3 contas suas em cada corretora.</p>
<form method="post" action="contas.php">
<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="corretora_id">Corretora</label>
<select id="corretora_id" name="corretora_id" required>
<option value="">Selecione...</option>
<?php foreach ($corretoras as $cr): ?>
<option value="<?= $cr['id'] ?>" <?= (isset($editando) && (int) ($editando['corretora_id'] ?? 0) === (int) $cr['id']) ? 'selected' : '' ?>><?= htmlspecialchars($cr['nome']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-5);">
<label for="nome">Nome/apelido da conta</label>
<input type="text" id="nome" name="nome" placeholder="Ex: Conta principal" required
value="<?= htmlspecialchars($editando['nome'] ?? '') ?>">
</div>
<button type="submit" class="ao-btn ao-btn-primary"><?= $editando && !empty($editando['id']) ? 'Salvar alterações' : 'Cadastrar conta' ?></button>
<?php if ($editando): ?>
<a href="contas.php" class="ao-btn ao-btn-secondary">Cancelar</a>
<?php endif; ?>
</form>
</div>
<?php endif; ?>
<div class="ao-card">
<div class="ao-card-header"><strong>Contas cadastradas</strong></div>
<table class="ao-table">
<thead>
<tr><th>Corretora</th><th>Conta</th><th>Ações vinculadas</th><th></th></tr>
</thead>
<tbody>
<?php if (!$contas): ?>
<tr><td colspan="4" style="color: var(--ao-text-muted);">Nenhuma conta cadastrada ainda.</td></tr>
<?php endif; ?>
<?php foreach ($contas as $c): ?>
<tr>
<td><?= htmlspecialchars($c['corretora_nome']) ?></td>
<td><?= htmlspecialchars($c['nome']) ?></td>
<td class="ao-numeric"><?= (int) $c['total_acoes'] ?></td>
<td style="white-space: nowrap; text-align: right;">
<a href="contas.php?editar=<?= $c['id'] ?>" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Editar</a>
<form method="post" action="contas.php" style="display:inline;" onsubmit="return confirm('Excluir esta conta?');">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $c['id'] ?>">
<button type="submit" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Excluir</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
+143
View File
@@ -0,0 +1,143 @@
<?php
$titulo_pagina = 'Corretoras';
$pagina_ativa = 'corretoras';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_permissao('corretoras');
// Corretora é um catálogo global, compartilhado por todos os usuários — não
// pertence a ninguém em especial. Usamos $ao_usuario_id só para contar, na
// listagem, quantas contas O USUÁRIO ATUAL tem em cada corretora (o limite
// de até 3 contas é por usuário, não pela corretora como um todo).
$ao_usuario_id = (int) ao_usuario_logado()['id'];
$erro = null;
$sucesso = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'excluir') {
$id = (int) ($_POST['id'] ?? 0);
// Verifica em TODOS os usuários, não só no atual: como a corretora é
// compartilhada, excluir uma que outra pessoa ainda usa apagaria as
// contas dela também (ON DELETE CASCADE) — por isso o bloqueio é global.
$stmt = $pdo->prepare('SELECT COUNT(*) FROM contas WHERE corretora_id = ?');
$stmt->execute([$id]);
if ((int) $stmt->fetchColumn() > 0) {
$erro = 'Não é possível excluir: esta corretora ainda tem contas cadastradas (suas ou de outro usuário). Exclua as contas primeiro.';
} else {
$pdo->prepare('DELETE FROM corretoras WHERE id = ?')->execute([$id]);
header('Location: corretoras.php?excluido=1');
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') {
$id = (int) ($_POST['id'] ?? 0);
$nome = trim($_POST['nome'] ?? '');
if ($nome === '') {
$erro = 'Informe o nome da corretora.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE corretoras SET nome = ? WHERE id = ?');
$stmt->execute([$nome, $id]);
} else {
$stmt = $pdo->prepare('INSERT INTO corretoras (nome) VALUES (?)');
$stmt->execute([$nome]);
}
header('Location: corretoras.php?salvo=1');
exit;
} catch (PDOException $e) {
$erro = ($e->getCode() === '23000') ? 'Já existe uma corretora com esse nome.' : ('Erro ao salvar: ' . $e->getMessage());
}
}
}
$editando = null;
if (isset($_GET['editar'])) {
$stmt = $pdo->prepare('SELECT * FROM corretoras WHERE id = ?');
$stmt->execute([(int) $_GET['editar']]);
$editando = $stmt->fetch() ?: null;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar' && $erro) {
$editando = ['id' => $id, 'nome' => $nome];
}
if (isset($_GET['salvo'])) $sucesso = 'Corretora salva com sucesso.';
if (isset($_GET['excluido'])) $sucesso = 'Corretora excluída.';
$stmt = $pdo->prepare(
'SELECT c.*, (SELECT COUNT(*) FROM contas ct WHERE ct.corretora_id = c.id AND ct.usuario_id = ?) AS total_contas
FROM corretoras c ORDER BY c.nome'
);
$stmt->execute([$ao_usuario_id]);
$corretoras = $stmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<h1 class="ao-page-title">Corretoras</h1>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-sm); max-width: 640px; margin-top: -8px;">
Lista compartilhada por todos os usuários — cadastre aqui as instituições financeiras
(ex: XP Investimentos, Rico, Clear). Cada uma pode ter até 3 contas suas — cadastre as
contas em <a href="contas.php">Contas</a>.
</p>
<?php if ($erro): ?>
<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-card" style="max-width: 480px; margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong><?= $editando ? 'Editar corretora' : 'Nova corretora' ?></strong></div>
<form method="post" action="corretoras.php">
<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-5);">
<label for="nome">Nome da corretora</label>
<input type="text" id="nome" name="nome" placeholder="Ex: XP Investimentos" required
value="<?= htmlspecialchars($editando['nome'] ?? '') ?>">
</div>
<button type="submit" class="ao-btn ao-btn-primary"><?= $editando ? 'Salvar alterações' : 'Cadastrar corretora' ?></button>
<?php if ($editando): ?>
<a href="corretoras.php" class="ao-btn ao-btn-secondary">Cancelar</a>
<?php endif; ?>
</form>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Corretoras cadastradas</strong></div>
<table class="ao-table">
<thead>
<tr><th>Nome</th><th>Minhas contas</th><th></th></tr>
</thead>
<tbody>
<?php if (!$corretoras): ?>
<tr><td colspan="3" style="color: var(--ao-text-muted);">Nenhuma corretora cadastrada ainda.</td></tr>
<?php endif; ?>
<?php foreach ($corretoras as $c): ?>
<tr>
<td><?= htmlspecialchars($c['nome']) ?></td>
<td class="ao-numeric"><?= (int) $c['total_contas'] ?> / 3</td>
<td style="white-space: nowrap; text-align: right;">
<a href="contas.php?corretora=<?= $c['id'] ?>" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Ver contas</a>
<a href="corretoras.php?editar=<?= $c['id'] ?>" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Editar</a>
<form method="post" action="corretoras.php" style="display:inline;" onsubmit="return confirm('Excluir esta corretora? Ela é compartilhada — só será excluída se nenhum usuário tiver contas nela.');">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $c['id'] ?>">
<button type="submit" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Excluir</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
+237
View File
@@ -0,0 +1,237 @@
<?php
$titulo_pagina = 'Cotações';
$pagina_ativa = 'cotacoes';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_permissao('cotacoes');
$ao_usuario_id = (int) ao_usuario_logado()['id'];
/** Converte "38,42" ou "38.42" em float. Retorna null se não for um número válido. */
function ao_cot_parse_decimal_br($valor) {
$valor = str_replace(',', '.', trim((string) $valor));
return is_numeric($valor) ? (float) $valor : null;
}
$erro = null;
$sucesso = null;
// Passa a acompanhar um novo ativo. A cotação em si mora numa tabela única e
// global ("cotacoes"): se o ativo ainda não existe ali, criamos com preço
// zerado (até alguém informar o primeiro valor); se já existe, é só ligar
// este usuário a ele.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'acompanhar') {
$ativo = strtoupper(trim($_POST['ativo'] ?? ''));
if ($ativo === '') {
$erro = 'Informe o código do ativo que você quer acompanhar.';
} else {
try {
$pdo->prepare('INSERT IGNORE INTO cotacoes (ativo) VALUES (?)')->execute([$ativo]);
$pdo->prepare('INSERT INTO cotacoes_acompanhamentos (usuario_id, ativo) VALUES (?, ?)')
->execute([$ao_usuario_id, $ativo]);
header('Location: cotacoes.php?acompanhado=1');
exit;
} catch (PDOException $e) {
$erro = ($e->getCode() === '23000') ? ('Você já acompanha ' . $ativo . '.') : ('Erro ao salvar: ' . $e->getMessage());
}
}
}
// Para de acompanhar (não mexe na cotação compartilhada, só na lista pessoal).
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'parar') {
$id = (int) ($_POST['id'] ?? 0);
$pdo->prepare('DELETE FROM cotacoes_acompanhamentos WHERE id = ? AND usuario_id = ?')->execute([$id, $ao_usuario_id]);
header('Location: cotacoes.php?parado=1');
exit;
}
// Atualiza o preço de um ativo na tabela única/global — vale para todo mundo
// que acompanha esse ativo, não só para quem atualizou. Só deixamos atualizar
// o preço de um ativo que o próprio usuário acompanha (evita alguém ficar
// alterando cotação de ativo que nem apareceu pra ele na tela).
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'atualizar_preco') {
$ativo = strtoupper(trim($_POST['ativo'] ?? ''));
$preco_bruto = trim($_POST['preco'] ?? '');
$preco = ao_cot_parse_decimal_br($preco_bruto);
$stmtAcompanha = $pdo->prepare('SELECT 1 FROM cotacoes_acompanhamentos WHERE usuario_id = ? AND ativo = ?');
$stmtAcompanha->execute([$ao_usuario_id, $ativo]);
if (!$stmtAcompanha->fetch()) {
$erro = 'Você só pode atualizar o preço de ativos que você acompanha.';
} elseif ($preco === null || $preco <= 0) {
$erro = 'Informe um preço válido (maior que zero).';
} else {
$stmtAtual = $pdo->prepare('SELECT preco_atual FROM cotacoes WHERE ativo = ?');
$stmtAtual->execute([$ativo]);
$preco_atual_antigo = (float) $stmtAtual->fetchColumn();
// Se é a primeira vez que alguém informa um preço para este ativo, não
// dá pra calcular variação a partir de zero — começa "zerada" (0%) em
// vez de mostrar um salto absurdo de +infinito.
$preco_anterior = $preco_atual_antigo > 0 ? $preco_atual_antigo : $preco;
$stmt = $pdo->prepare('UPDATE cotacoes SET preco_anterior = ?, preco_atual = ?, atualizado_por = ? WHERE ativo = ?');
$stmt->execute([$preco_anterior, $preco, $ao_usuario_id, $ativo]);
header('Location: cotacoes.php?atualizado=1');
exit;
}
}
if (isset($_GET['acompanhado'])) $sucesso = 'Ativo adicionado à sua lista de acompanhamento.';
if (isset($_GET['parado'])) $sucesso = 'Você parou de acompanhar esse ativo.';
if (isset($_GET['atualizado'])) $sucesso = 'Cotação atualizada — todo mundo que acompanha esse ativo já vê o novo preço.';
// Mantém o que foi digitado no formulário de "acompanhar" se deu erro.
$ativo_digitado = ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'acompanhar')
? (string) ($_POST['ativo'] ?? '')
: '';
// A lista de preços vem sempre da tabela "cotacoes" (única/global) — aqui só
// filtramos para os ativos que ESTE usuário decidiu acompanhar.
$stmt = $pdo->prepare(
'SELECT ca.id AS acompanhamento_id, c.ativo, c.preco_atual, c.preco_anterior, c.atualizado_em, c.atualizado_por,
u.nome AS atualizado_por_nome
FROM cotacoes_acompanhamentos ca
INNER JOIN cotacoes c ON c.ativo = ca.ativo
LEFT JOIN usuarios u ON u.id = c.atualizado_por
WHERE ca.usuario_id = ?
ORDER BY c.ativo'
);
$stmt->execute([$ao_usuario_id]);
$acompanhados = $stmt->fetchAll();
// Calcula a variação percentual de cada um e separa em altas (>= 0%) e
// baixas (< 0%) para as duas grids pedidas.
foreach ($acompanhados as &$a) {
$anterior = (float) $a['preco_anterior'];
$atual = (float) $a['preco_atual'];
$a['tem_cotacao'] = $a['atualizado_por'] !== null;
$a['variacao'] = $anterior > 0 ? (($atual - $anterior) / $anterior) * 100 : 0.0;
}
unset($a);
$altas = array_values(array_filter($acompanhados, fn($a) => $a['variacao'] >= 0));
$baixas = array_values(array_filter($acompanhados, fn($a) => $a['variacao'] < 0));
usort($altas, fn($a, $b) => $b['variacao'] <=> $a['variacao']);
usort($baixas, fn($a, $b) => $a['variacao'] <=> $b['variacao']);
require __DIR__ . '/includes/header.php';
/** Uma linha da grid de cotações (compartilhada entre a grid de altas e a de baixas). */
function ao_cot_linha(array $a) {
$classe = $a['variacao'] >= 0 ? 'ao-badge-alta' : 'ao-badge-baixa';
$seta = $a['variacao'] >= 0 ? '▲' : '▼';
?>
<tr>
<td class="ao-numeric"><?= htmlspecialchars($a['ativo']) ?></td>
<td class="ao-numeric">
<?php if ($a['tem_cotacao']): ?>
R$ <?= number_format((float) $a['preco_atual'], 2, ',', '.') ?>
<?php else: ?>
<span style="color: var(--ao-text-muted);">sem cotação ainda</span>
<?php endif; ?>
</td>
<td class="ao-numeric">
<?php if ($a['tem_cotacao']): ?>
<span class="ao-badge <?= $classe ?>"><?= $seta ?> <?= number_format(abs($a['variacao']), 2, ',', '.') ?>%</span>
<?php else: ?>
<span style="color: var(--ao-text-muted);">—</span>
<?php endif; ?>
</td>
<td style="color: var(--ao-text-muted); font-size: var(--ao-fs-xs);">
<?php if ($a['tem_cotacao']): ?>
<?= htmlspecialchars($a['atualizado_por_nome'] ?? '—') ?> em <?= htmlspecialchars(date('d/m/Y H:i', strtotime($a['atualizado_em']))) ?>
<?php else: ?>
<?php endif; ?>
</td>
<td style="white-space: nowrap; text-align: right;">
<form method="post" action="cotacoes.php" style="display:inline-flex; gap: 4px; align-items: center;">
<input type="hidden" name="acao" value="atualizar_preco">
<input type="hidden" name="ativo" value="<?= htmlspecialchars($a['ativo']) ?>">
<input type="text" name="preco" placeholder="Novo preço" required
style="width: 100px; padding: 4px 8px; font-size: var(--ao-fs-xs);">
<button type="submit" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Atualizar</button>
</form>
<form method="post" action="cotacoes.php" style="display:inline;" onsubmit="return confirm('Parar de acompanhar <?= htmlspecialchars($a['ativo']) ?>?');">
<input type="hidden" name="acao" value="parar">
<input type="hidden" name="id" value="<?= (int) $a['acompanhamento_id'] ?>">
<button type="submit" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Parar</button>
</form>
</td>
</tr>
<?php
}
?>
<h1 class="ao-page-title">Cotações do dia</h1>
<?php if ($erro): ?>
<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-card" style="max-width: 480px; margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong>Acompanhar novo ativo</strong></div>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-sm); margin-top: 0;">
A cotação é compartilhada: qualquer pessoa que também acompanhe o ativo pode
atualizar o preço, e todo mundo que o acompanha vê o valor mais recente.
</p>
<form method="post" action="cotacoes.php" class="ao-row" style="gap: var(--ao-space-3); align-items: flex-end;">
<input type="hidden" name="acao" value="acompanhar">
<div class="ao-field" style="flex: 1; margin-bottom: 0;">
<label for="ativo">Código do ativo</label>
<input type="text" id="ativo" name="ativo" placeholder="Ex: PETR4" required
style="text-transform: uppercase;" value="<?= htmlspecialchars($ativo_digitado) ?>">
</div>
<button type="submit" class="ao-btn ao-btn-primary">Acompanhar</button>
</form>
</div>
<?php if (!$acompanhados): ?>
<div class="ao-card">
<p style="margin: 0; color: var(--ao-text-muted);">Você ainda não está acompanhando nenhum ativo. Use o formulário acima para adicionar o primeiro.</p>
</div>
<?php else: ?>
<div class="ao-card" style="margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong>Altas do dia</strong></div>
<table class="ao-table">
<thead>
<tr><th>Ativo</th><th>Último</th><th>Variação</th><th>Última atualização</th><th></th></tr>
</thead>
<tbody>
<?php if (!$altas): ?>
<tr><td colspan="5" style="color: var(--ao-text-muted);">Nenhum ativo em alta no momento.</td></tr>
<?php endif; ?>
<?php foreach ($altas as $a) ao_cot_linha($a); ?>
</tbody>
</table>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Baixas do dia</strong></div>
<table class="ao-table">
<thead>
<tr><th>Ativo</th><th>Último</th><th>Variação</th><th>Última atualização</th><th></th></tr>
</thead>
<tbody>
<?php if (!$baixas): ?>
<tr><td colspan="5" style="color: var(--ao-text-muted);">Nenhum ativo em baixa no momento.</td></tr>
<?php endif; ?>
<?php foreach ($baixas as $a) ao_cot_linha($a); ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php require __DIR__ . '/includes/footer.php'; ?>
+105
View File
@@ -0,0 +1,105 @@
<?php
/**
* AçãoOpção — autenticação e controle de permissões por página.
* Inclua (require_once) depois de config.php e db.php.
*/
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php';
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
// Páginas de administração do próprio sistema de acesso. Ficam sempre
// restritas a perfis com acesso_total, independente da tabela `paginas`
// (que serve para as páginas de negócio, essas sim configuráveis por perfil).
const AO_PAGINAS_ADMIN = [
'usuarios' => ['arquivo' => 'usuarios.php', 'nome' => 'Usuários'],
'perfis' => ['arquivo' => 'perfis.php', 'nome' => 'Perfis'],
'paginas_admin' => ['arquivo' => 'paginas_admin.php', 'nome' => 'Páginas'],
];
function ao_usuario_logado() {
return $_SESSION['ao_usuario'] ?? null;
}
function ao_exigir_login() {
if (!ao_usuario_logado()) {
$_SESSION['ao_pos_login'] = $_SERVER['REQUEST_URI'] ?? 'index.php';
header('Location: login.php');
exit;
}
}
/** true se o usuário logado pode acessar a página de negócio $chave (tabela `paginas`). */
function ao_tem_permissao($chave) {
$usuario = ao_usuario_logado();
if (!$usuario) {
return false;
}
if (!empty($usuario['acesso_total'])) {
return true;
}
return in_array($chave, $usuario['paginas'] ?? [], true);
}
/** Carrega os dados de sessão do usuário a partir do banco (login e refresh de permissões). */
function ao_carregar_sessao_usuario(PDO $pdo, array $usuarioDb, array $perfilDb) {
$paginas = [];
if (empty($perfilDb['acesso_total'])) {
$stmt = $pdo->prepare(
'SELECT p.chave FROM paginas p
INNER JOIN perfil_paginas pp ON pp.pagina_id = p.id
WHERE pp.perfil_id = ? AND p.ativo = 1'
);
$stmt->execute([$perfilDb['id']]);
$paginas = array_column($stmt->fetchAll(), 'chave');
}
$_SESSION['ao_usuario'] = [
'id' => (int) $usuarioDb['id'],
'nome' => $usuarioDb['nome'],
'email' => $usuarioDb['email'],
'perfil_id' => (int) $perfilDb['id'],
'perfil_nome' => $perfilDb['nome'],
'acesso_total' => (bool) $perfilDb['acesso_total'],
'paginas' => $paginas,
];
}
function ao_pagina_acesso_negado($ao_tema_atual = null) {
http_response_code(403);
$cor = $ao_tema_atual['cor'] ?? '#7FA653';
echo '<!doctype html><html lang="pt-BR"><head><meta charset="UTF-8">'
. '<title>Acesso negado — AçãoOpção</title>'
. '<meta name="viewport" content="width=device-width, initial-scale=1.0"></head>'
. '<body style="font-family: system-ui, sans-serif; display:flex; align-items:center; justify-content:center; '
. 'min-height:100vh; margin:0; background:#F4F5F5;">'
. '<div style="max-width:420px; text-align:center; padding:32px;">'
. '<div style="font-size:42px; margin-bottom:8px;">🔒</div>'
. '<h1 style="font-size:1.25rem; margin-bottom:8px;">Acesso negado</h1>'
. '<p style="color:#666; margin-bottom:24px;">Seu perfil não tem permissão para acessar esta página. '
. 'Fale com um administrador se precisar desse acesso.</p>'
. '<a href="index.php" style="display:inline-block; padding:10px 20px; border-radius:8px; '
. 'background:' . htmlspecialchars($cor) . '; color:#fff; text-decoration:none; font-weight:600;">Voltar</a>'
. '</div></body></html>';
exit;
}
/** Exige login e, se $chave for informada, permissão para aquela página de negócio. */
function ao_exigir_permissao($chave = null, $ao_tema_atual = null) {
ao_exigir_login();
if ($chave !== null && !ao_tem_permissao($chave)) {
ao_pagina_acesso_negado($ao_tema_atual);
}
}
/** Exige login e acesso_total (para as páginas de administração do próprio sistema). */
function ao_exigir_acesso_total($ao_tema_atual = null) {
ao_exigir_login();
$usuario = ao_usuario_logado();
if (empty($usuario['acesso_total'])) {
ao_pagina_acesso_negado($ao_tema_atual);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* AçãoOpção — configuração de ambiente (desenvolvimento / produção).
* Inclua este arquivo antes de includes/theme.php.
*/
define('AO_AMBIENTE_ARQUIVO', __DIR__ . '/ambiente.json');
const AO_URLS = [
'desenvolvimento' => 'http://localhost:8080/claude/AcaoOpcao/',
'producao' => 'https://acaoeopcaoderivativos.systems/',
];
function ao_ler_ambiente() {
$padrao = 'desenvolvimento';
if (!file_exists(AO_AMBIENTE_ARQUIVO)) {
return $padrao;
}
$conteudo = json_decode((string) file_get_contents(AO_AMBIENTE_ARQUIVO), true);
$ambiente = is_array($conteudo) ? ($conteudo['ambiente'] ?? $padrao) : $padrao;
return array_key_exists($ambiente, AO_URLS) ? $ambiente : $padrao;
}
function ao_salvar_ambiente($ambiente) {
if (!array_key_exists($ambiente, AO_URLS)) {
return false;
}
$ok = file_put_contents(
AO_AMBIENTE_ARQUIVO,
json_encode(['ambiente' => $ambiente], JSON_PRETTY_PRINT)
);
return $ok !== false;
}
// Constantes disponíveis para o resto do sistema:
// AO_AMBIENTE 'desenvolvimento' | 'producao'
// AO_BASE_URL URL completa do ambiente ativo
// AO_IS_PRODUCAO true quando o ambiente ativo é produção (https)
define('AO_AMBIENTE', ao_ler_ambiente());
define('AO_BASE_URL', AO_URLS[AO_AMBIENTE]);
define('AO_IS_PRODUCAO', AO_AMBIENTE === 'producao');
// Credenciais do banco de dados MySQL por ambiente. Troque os valores de
// "producao" diretamente aqui, editando o arquivo no servidor — por segurança,
// evite colar senha de produção em e-mail, chat ou qualquer lugar fora deste
// arquivo. O ambiente ativo é escolhido na tela Configurações do sistema.
const AO_BANCOS = [
'desenvolvimento' => [
'host' => 'localhost',
'nome' => 'acaoopcao',
'usuario' => 'josima70_josimar',
'senha' => 'Josim@r230976',
],
'producao' => [
'host' => 'database', // ajuste se o banco de produção estiver em outro host
'nome' => 'acaoopcao',
'usuario' => 'root',
'senha' => 'Josim@r230976',
],
];
define('AO_DB_HOST', AO_BANCOS[AO_AMBIENTE]['host']);
define('AO_DB_NOME', AO_BANCOS[AO_AMBIENTE]['nome']);
define('AO_DB_USUARIO', AO_BANCOS[AO_AMBIENTE]['usuario']);
define('AO_DB_SENHA', AO_BANCOS[AO_AMBIENTE]['senha']);
/**
* Monta uma URL absoluta a partir de um caminho relativo ao projeto,
* usando a URL base do ambiente ativo. Útil para links que precisam
* ser absolutos (ex.: compartilhamento, tag canonical, e-mails).
* Os links internos do próprio sistema continuam relativos e não
* precisam desta função para funcionar nos dois ambientes.
*/
function ao_url($caminho = '') {
return rtrim(AO_BASE_URL, '/') . '/' . ltrim($caminho, '/');
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* AçãoOpção — conexão com o banco de dados MySQL.
* Inclua (require_once) antes de qualquer uso de $pdo.
* As credenciais usadas aqui vêm de includes/config.php (AO_BANCOS),
* de acordo com o ambiente ativo (desenvolvimento/produção).
*/
require_once __DIR__ . '/config.php';
try {
$pdo = new PDO(
"mysql:host=" . AO_DB_HOST . ";dbname=" . AO_DB_NOME . ";charset=utf8mb4",
AO_DB_USUARIO,
AO_DB_SENHA,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
http_response_code(500);
die(
'<div style="font-family: sans-serif; max-width: 640px; margin: 60px auto; padding: 24px; ' .
'border: 1px solid #e2b3ab; background: #fdf1ef; color: #7a2c22; border-radius: 8px;">' .
'<h2 style="margin-top:0;">Não foi possível conectar ao banco de dados</h2>' .
'<p>Ambiente ativo: <strong>' . htmlspecialchars(AO_AMBIENTE) . '</strong>. Verifique se o MySQL está ' .
'ativo, se o banco <strong>' . htmlspecialchars(AO_DB_NOME) . '</strong> foi criado a partir de ' .
'<code>includes/esquema.sql</code>, e se o usuário/senha em <code>includes/config.php</code> ' .
'(bloco <code>AO_BANCOS</code>) estão corretos para este ambiente.</p>' .
'<p style="font-size: 0.85rem; opacity: .8;">Detalhe técnico: ' . htmlspecialchars($e->getMessage()) . '</p>' .
'</div>'
);
}
+6
View File
@@ -0,0 +1,6 @@
</main>
<footer style="text-align:center; padding: var(--ao-space-6) var(--ao-space-4); color: var(--ao-text-muted); font-size: var(--ao-fs-xs);">
AçãoOpção protótipo de identidade visual (tema atual: <?= htmlspecialchars($ao_tema_atual['nome']) ?>)
</footer>
</body>
</html>
+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">
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* AçãoOpção — controle de identidade visual (tema) do sistema.
* Inclua este arquivo no topo de header.php, antes de qualquer saída HTML,
* sempre depois de includes/config.php (usamos AO_IS_PRODUCAO aqui).
*/
require_once __DIR__ . '/config.php';
$ao_temas_info = [
'verde' => [
'nome' => 'Verde',
'css' => 'assets/css/identidade-visual-verde.css',
'favicon' => 'assets/img/logos/verde/favicon.ico',
'icone' => 'assets/img/logos/verde/svg/icone-cor.svg',
'cor' => '#7FA653',
],
'marrom' => [
'nome' => 'Marrom',
'css' => 'assets/css/identidade-visual-marrom.css',
'favicon' => 'assets/img/logos/marrom/favicon.ico',
'icone' => 'assets/img/logos/marrom/svg/icone-cor.svg',
'cor' => '#A6754C',
],
'cinza' => [
'nome' => 'Cinza',
'css' => 'assets/css/identidade-visual-cinza.css',
'favicon' => 'assets/img/logos/cinza/favicon.ico',
'icone' => 'assets/img/logos/cinza/svg/icone-cor.svg',
'cor' => '#6B6F77',
],
];
$ao_modos_info = [
'claro' => ['nome' => 'Claro', 'icone' => '☀️'],
'escuro' => ['nome' => 'Escuro', 'icone' => '🌙'],
];
// Duração do cookie de identidade: 1 ano. Guardamos a escolha do usuário
// em cookie (não em sessão) justamente para que ela sobreviva ao fechar
// o navegador — é isso que "lembra" o tema/modo na próxima visita.
const AO_COOKIE_DURACAO = 60 * 60 * 24 * 365;
function ao_definir_cookie($nome, $valor) {
setcookie($nome, $valor, [
'expires' => time() + AO_COOKIE_DURACAO,
'path' => '/',
'secure' => AO_IS_PRODUCAO,
'httponly' => true,
'samesite' => 'Lax',
]);
}
// Troca de tema/modo via link "?tema=marrom" e/ou "?modo=escuro" — grava
// em cookie e redireciona para a mesma URL sem o parâmetro, para manter
// o endereço limpo. Os dois podem vir juntos na mesma requisição.
$ao_precisa_redirecionar = false;
if (isset($_GET['tema']) && array_key_exists($_GET['tema'], $ao_temas_info)) {
ao_definir_cookie('ao_tema', $_GET['tema']);
$_COOKIE['ao_tema'] = $_GET['tema'];
$ao_precisa_redirecionar = true;
}
if (isset($_GET['modo']) && array_key_exists($_GET['modo'], $ao_modos_info)) {
ao_definir_cookie('ao_modo', $_GET['modo']);
$_COOKIE['ao_modo'] = $_GET['modo'];
$ao_precisa_redirecionar = true;
}
if ($ao_precisa_redirecionar) {
$destino = strtok($_SERVER['REQUEST_URI'], '?');
header('Location: ' . $destino);
exit;
}
$ao_tema = $_COOKIE['ao_tema'] ?? 'verde';
if (!array_key_exists($ao_tema, $ao_temas_info)) {
$ao_tema = 'verde';
}
$ao_modo = $_COOKIE['ao_modo'] ?? 'claro';
if (!array_key_exists($ao_modo, $ao_modos_info)) {
$ao_modo = 'claro';
}
$ao_tema_atual = $ao_temas_info[$ao_tema];
$ao_modo_atual = $ao_modos_info[$ao_modo];
$ao_proximo_modo = $ao_modo === 'claro' ? 'escuro' : 'claro';
+89
View File
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ação e Opção Derivativos</title>
<style>
:root {
--bg: #0f172a;
--card: #111827;
--accent: #38bdf8;
--text: #e5e7eb;
--muted: #94a3b8;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at top, #1e293b, #0f172a 60%);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: var(--text);
padding: 24px;
}
.card {
max-width: 560px;
width: 100%;
background: var(--card);
border: 1px solid rgba(148, 163, 184, 0.15);
border-radius: 16px;
padding: 48px 40px;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
}
.badge {
display: inline-block;
padding: 6px 14px;
border-radius: 999px;
background: rgba(56, 189, 248, 0.12);
color: var(--accent);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
margin-bottom: 20px;
}
h1 {
margin: 0 0 12px;
font-size: 28px;
font-weight: 700;
color: #fff;
}
p {
margin: 0 0 8px;
color: var(--muted);
line-height: 1.6;
font-size: 15px;
}
.divider {
height: 1px;
background: rgba(148, 163, 184, 0.15);
margin: 28px 0;
}
.contact {
font-size: 14px;
color: var(--muted);
}
.contact a {
color: var(--accent);
text-decoration: none;
}
.contact a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="card">
<span class="badge">Em breve</span>
<h1>Ação e Opção Derivativos</h1>
<p>Nosso site está em fase final de preparação.</p>
<p>Volte em breve para conferir as novidades.</p>
<div class="divider"></div>
<p class="contact">Dúvidas ou contato: <a href="mailto:contato@acaoeopcaoderivativos.systems">contato@acaoeopcaoderivativos.systems</a></p>
</div>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
<?php
$titulo_pagina = 'Carteira';
$pagina_ativa = 'carteira';
require __DIR__ . '/includes/header.php';
$posicoes = [
['ativo' => 'PETR4', 'qtd' => 300, 'preco_medio' => 36.10, 'atual' => 38.42, 'variacao' => 6.42],
['ativo' => 'VALE3', 'qtd' => 150, 'preco_medio' => 63.80, 'atual' => 61.07, 'variacao' => -4.28],
['ativo' => 'PETR4C40', 'qtd' => 1000, 'preco_medio' => 1.30, 'atual' => 1.12, 'variacao' => -13.85],
];
?>
<h1 class="ao-page-title">Minha carteira</h1>
<div class="ao-row" style="margin-bottom: var(--ao-space-5); flex-wrap: wrap; gap: 16px;">
<div class="ao-card" style="flex: 1; min-width: 220px;">
<div class="ao-card-header">
<strong>Saldo consolidado</strong>
<span class="ao-badge ao-badge-alta">+3,2% no mês</span>
</div>
<p class="ao-numeric" style="font-size: 1.5rem; margin: 0;">R$ 48.920,17</p>
</div>
<div class="ao-card" style="flex: 1; min-width: 220px;">
<div class="ao-card-header"><strong>Em ações</strong></div>
<p class="ao-numeric" style="font-size: 1.5rem; margin: 0;">R$ 36.150,00</p>
</div>
<div class="ao-card" style="flex: 1; min-width: 220px;">
<div class="ao-card-header"><strong>Em opções</strong></div>
<p class="ao-numeric" style="font-size: 1.5rem; margin: 0;">R$ 12.770,17</p>
</div>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Posições abertas</strong></div>
<table class="ao-table">
<thead>
<tr><th>Ativo</th><th>Qtd.</th><th>Preço médio</th><th>Atual</th><th>Resultado</th></tr>
</thead>
<tbody>
<?php foreach ($posicoes as $p): ?>
<?php $classe = $p['variacao'] >= 0 ? 'ao-up' : 'ao-down'; ?>
<tr>
<td class="ao-numeric"><?= htmlspecialchars($p['ativo']) ?></td>
<td class="ao-numeric"><?= $p['qtd'] ?></td>
<td class="ao-numeric">R$ <?= number_format($p['preco_medio'], 2, ',', '.') ?></td>
<td class="ao-numeric">R$ <?= number_format($p['atual'], 2, ',', '.') ?></td>
<td class="ao-numeric <?= $classe ?>"><?= $p['variacao'] >= 0 ? '+' : '' ?><?= number_format($p['variacao'], 2, ',', '.') ?>%</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
+107
View File
@@ -0,0 +1,107 @@
<?php
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/theme.php';
if (ao_usuario_logado()) {
header('Location: index.php');
exit;
}
$erro = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$senha = (string) ($_POST['senha'] ?? '');
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = ? AND ativo = 1');
$stmt->execute([$email]);
$usuarioDb = $stmt->fetch();
if ($usuarioDb && password_verify($senha, $usuarioDb['senha_hash'])) {
$stmtPerfil = $pdo->prepare('SELECT * FROM perfis WHERE id = ? AND ativo = 1');
$stmtPerfil->execute([$usuarioDb['perfil_id']]);
$perfilDb = $stmtPerfil->fetch();
if (!$perfilDb) {
$erro = 'Seu perfil de acesso está inativo. Fale com um administrador.';
} else {
ao_carregar_sessao_usuario($pdo, $usuarioDb, $perfilDb);
session_regenerate_id(true);
$pdo->prepare('UPDATE usuarios SET ultimo_login = NOW() WHERE id = ?')->execute([$usuarioDb['id']]);
$destino = $_SESSION['ao_pos_login'] ?? 'index.php';
unset($_SESSION['ao_pos_login']);
header('Location: ' . $destino);
exit;
}
} else {
$erro = 'E-mail ou senha inválidos.';
}
}
?>
<!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>Entrar — AçãoOpção</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">
<link rel="stylesheet" href="<?= htmlspecialchars($ao_tema_atual['css']) ?>">
<style>
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: var(--ao-bg-page); color: var(--ao-text-body);
}
.ao-login-card {
width: 100%; max-width: 380px; margin: 24px; padding: var(--ao-space-6);
background: var(--ao-bg-surface); border: 1px solid var(--ao-border);
border-radius: var(--ao-radius-lg, 16px); box-shadow: var(--ao-shadow-md);
}
.ao-login-brand { display: flex; align-items: center; gap: 10px; margin-bottom: var(--ao-space-5); }
.ao-login-brand img { width: 32px; height: 32px; border-radius: 9px; }
.ao-login-brand strong { font-family: 'Poppins', sans-serif; font-size: 1.1rem; color: var(--ao-text-strong); }
.ao-login-card h1 { font-size: 1.15rem; margin: 0 0 var(--ao-space-4); color: var(--ao-text-strong); }
.ao-field { margin-bottom: var(--ao-space-4); }
.ao-field label { display: block; font-size: var(--ao-fs-xs); font-weight: 600; margin-bottom: 6px; color: var(--ao-text-muted); }
.ao-erro {
background: var(--ao-baixa-bg, #fdf1ef); color: var(--ao-baixa-text, #a4372b);
padding: 10px 14px; border-radius: 8px; font-size: var(--ao-fs-sm); margin-bottom: var(--ao-space-4);
}
.ao-login-rodape { margin-top: var(--ao-space-5); font-size: var(--ao-fs-xs); color: var(--ao-text-muted); text-align: center; }
</style>
</head>
<body>
<div class="ao-login-card">
<div class="ao-login-brand">
<img src="<?= htmlspecialchars($ao_tema_atual['icone']) ?>" alt="">
<strong>AçãoOpção</strong>
</div>
<h1>Entrar na sua conta</h1>
<?php if ($erro): ?>
<div class="ao-erro"><?= htmlspecialchars($erro) ?></div>
<?php endif; ?>
<form method="post" action="login.php">
<div class="ao-field">
<label for="email">E-mail</label>
<input type="email" id="email" name="email" required autofocus value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">
</div>
<div class="ao-field">
<label for="senha">Senha</label>
<input type="password" id="senha" name="senha" required>
</div>
<button type="submit" class="ao-btn ao-btn-primary" style="width:100%;">Entrar</button>
</form>
<p class="ao-login-rodape">AçãoOpção — acesso restrito a usuários cadastrados.</p>
</div>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
<?php
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy();
header('Location: login.php');
exit;
+125
View File
@@ -0,0 +1,125 @@
<?php
$titulo_pagina = 'Minha conta';
// Sem $pagina_ativa de propósito: esta página não passa pelo sistema de
// permissões configuráveis (tabela `paginas`) nem pelo bloqueio de
// administrador — qualquer usuário logado pode mudar o próprio nome e senha,
// independente do que o perfil dele permite ver.
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_login();
$ao_usuario_atual = ao_usuario_logado();
$ao_usuario_id = (int) $ao_usuario_atual['id'];
$erro = null;
$sucesso = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') {
$nome = trim($_POST['nome'] ?? '');
$senha_atual = (string) ($_POST['senha_atual'] ?? '');
$nova_senha = (string) ($_POST['nova_senha'] ?? '');
$confirmar_nova_senha = (string) ($_POST['confirmar_nova_senha'] ?? '');
$quer_trocar_senha = $nova_senha !== '' || $confirmar_nova_senha !== '';
if ($nome === '') {
$erro = 'Informe seu nome.';
} elseif ($quer_trocar_senha && $nova_senha !== $confirmar_nova_senha) {
$erro = 'A nova senha e a confirmação não são iguais.';
} elseif ($quer_trocar_senha && $senha_atual === '') {
$erro = 'Informe sua senha atual para definir uma nova senha.';
} else {
if ($quer_trocar_senha) {
$stmt = $pdo->prepare('SELECT senha_hash FROM usuarios WHERE id = ?');
$stmt->execute([$ao_usuario_id]);
$linha = $stmt->fetch();
if (!$linha || !password_verify($senha_atual, $linha['senha_hash'])) {
$erro = 'Senha atual incorreta.';
}
}
if (!$erro) {
try {
if ($quer_trocar_senha) {
$stmt = $pdo->prepare('UPDATE usuarios SET nome = ?, senha_hash = ? WHERE id = ?');
$stmt->execute([$nome, password_hash($nova_senha, PASSWORD_DEFAULT), $ao_usuario_id]);
} else {
$stmt = $pdo->prepare('UPDATE usuarios SET nome = ? WHERE id = ?');
$stmt->execute([$nome, $ao_usuario_id]);
}
// Atualiza a sessão para o nome novo aparecer no cabeçalho na hora.
$_SESSION['ao_usuario']['nome'] = $nome;
header('Location: minha-conta.php?salvo=1');
exit;
} catch (PDOException $e) {
$erro = 'Erro ao salvar: ' . $e->getMessage();
}
}
}
}
if (isset($_GET['salvo'])) {
$sucesso = 'Dados salvos com sucesso.';
}
// Nome exibido no formulário: o que acabou de ser digitado (se deu erro) ou o
// nome atual da sessão.
$ao_nome_form = $_SERVER['REQUEST_METHOD'] === 'POST' ? ($nome ?? $ao_usuario_atual['nome']) : $ao_usuario_atual['nome'];
require __DIR__ . '/includes/header.php';
?>
<h1 class="ao-page-title">Minha conta</h1>
<?php if ($erro): ?>
<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-card" style="max-width: 480px;">
<div class="ao-card-header"><strong>Meus dados</strong></div>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-sm); margin-top: 0;">
E-mail: <strong><?= htmlspecialchars($ao_usuario_atual['email']) ?></strong>
</p>
<form method="post" action="minha-conta.php" id="form-minha-conta" onsubmit="return aoConfirmarNovaSenha(this);">
<input type="hidden" name="acao" value="salvar">
<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($ao_nome_form) ?>">
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="senha_atual">Senha atual <span style="font-weight: 400; color: var(--ao-text-muted);">(só para trocar a senha)</span></label>
<input type="password" id="senha_atual" name="senha_atual" autocomplete="current-password">
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="nova_senha">Nova senha <span style="font-weight: 400; color: var(--ao-text-muted);">(deixe em branco para manter a atual)</span></label>
<input type="password" id="nova_senha" name="nova_senha" autocomplete="new-password">
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-5);">
<label for="confirmar_nova_senha">Confirmar nova senha</label>
<input type="password" id="confirmar_nova_senha" name="confirmar_nova_senha" autocomplete="new-password">
</div>
<button type="submit" class="ao-btn ao-btn-primary">Salvar alterações</button>
</form>
</div>
<script>
// Confere no navegador se a nova senha e a confirmação batem, antes mesmo
// de enviar o formulário — a checagem que vale de verdade é feita no servidor.
function aoConfirmarNovaSenha(form) {
var nova = form.querySelector('#nova_senha').value;
var confirmar = form.querySelector('#confirmar_nova_senha').value;
if (nova !== confirmar) {
alert('A nova senha e a confirmação não são iguais.');
return false;
}
return true;
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>
+62
View File
@@ -0,0 +1,62 @@
<?php
$titulo_pagina = 'Operações';
$pagina_ativa = 'operacoes';
require __DIR__ . '/includes/header.php';
$historico = [
['data' => '20/08/2026', 'ativo' => 'PETR4', 'operacao' => 'Compra', 'qtd' => 100, 'preco' => 37.90],
['data' => '18/08/2026', 'ativo' => 'PETR4C40', 'operacao' => 'Venda', 'qtd' => 500, 'preco' => 1.24],
['data' => '12/08/2026', 'ativo' => 'VALE3', 'operacao' => 'Compra', 'qtd' => 150, 'preco' => 63.80],
];
?>
<h1 class="ao-page-title">Registrar operação</h1>
<div class="ao-card" style="max-width: 480px; margin-bottom: var(--ao-space-6);">
<form method="post" action="#">
<div class="ao-field" style="margin-bottom: var(--ao-space-4);">
<label for="ativo">Código do ativo</label>
<input type="text" id="ativo" name="ativo" placeholder="Ex: PETR4">
</div>
<div class="ao-row" style="gap: var(--ao-space-4); margin-bottom: var(--ao-space-4); align-items: flex-start;">
<div class="ao-field" style="flex: 1;">
<label for="quantidade">Quantidade</label>
<input type="number" id="quantidade" name="quantidade" placeholder="100">
</div>
<div class="ao-field" style="flex: 1;">
<label for="preco">Preço</label>
<input type="text" id="preco" name="preco" placeholder="38,42">
</div>
</div>
<div class="ao-field" style="margin-bottom: var(--ao-space-5);">
<label for="operacao">Operação</label>
<select id="operacao" name="operacao">
<option>Compra</option>
<option>Venda</option>
</select>
</div>
<button type="submit" class="ao-btn ao-btn-primary">Registrar</button>
</form>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Histórico recente</strong></div>
<table class="ao-table">
<thead>
<tr><th>Data</th><th>Ativo</th><th>Operação</th><th>Qtd.</th><th>Preço</th></tr>
</thead>
<tbody>
<?php foreach ($historico as $h): ?>
<tr>
<td class="ao-numeric"><?= htmlspecialchars($h['data']) ?></td>
<td class="ao-numeric"><?= htmlspecialchars($h['ativo']) ?></td>
<td><span class="ao-badge <?= $h['operacao'] === 'Compra' ? 'ao-badge-alta' : 'ao-badge-baixa' ?>"><?= htmlspecialchars($h['operacao']) ?></span></td>
<td class="ao-numeric"><?= $h['qtd'] ?></td>
<td class="ao-numeric">R$ <?= number_format($h['preco'], 2, ',', '.') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
+144
View File
@@ -0,0 +1,144 @@
<?php
$titulo_pagina = 'Páginas';
$pagina_ativa = 'paginas_admin';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_acesso_total();
$erro = null;
$sucesso = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'excluir') {
$id = (int) ($_POST['id'] ?? 0);
$pdo->prepare('DELETE FROM paginas WHERE id = ?')->execute([$id]);
header('Location: paginas_admin.php?excluido=1');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') {
$id = (int) ($_POST['id'] ?? 0);
$chave = strtolower(trim($_POST['chave'] ?? ''));
$nome = trim($_POST['nome'] ?? '');
$arquivo = trim($_POST['arquivo'] ?? '');
$ordem = (int) ($_POST['ordem'] ?? 0);
$ativo = isset($_POST['ativo']) ? 1 : 0;
if ($chave === '' || $nome === '' || $arquivo === '') {
$erro = 'Preencha chave, nome e arquivo.';
} elseif (!preg_match('/^[a-z0-9_]+$/', $chave)) {
$erro = 'A chave só pode ter letras minúsculas, números e sublinhado (ex: relatorios_mensais).';
} elseif (array_key_exists($chave, AO_PAGINAS_ADMIN)) {
$erro = 'Essa chave é reservada para as páginas de administração do próprio sistema.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE paginas SET chave=?, nome=?, arquivo=?, ordem=?, ativo=? WHERE id=?');
$stmt->execute([$chave, $nome, $arquivo, $ordem, $ativo, $id]);
} else {
$stmt = $pdo->prepare('INSERT INTO paginas (chave, nome, arquivo, ordem, ativo) VALUES (?,?,?,?,?)');
$stmt->execute([$chave, $nome, $arquivo, $ordem, $ativo]);
}
header('Location: paginas_admin.php?salvo=1');
exit;
} catch (PDOException $e) {
$erro = ($e->getCode() === '23000') ? 'Já existe uma página com esta chave.' : ('Erro ao salvar: ' . $e->getMessage());
}
}
}
$editando = null;
if (isset($_GET['editar'])) {
$stmt = $pdo->prepare('SELECT * FROM paginas WHERE id = ?');
$stmt->execute([(int) $_GET['editar']]);
$editando = $stmt->fetch() ?: null;
}
if (isset($_GET['salvo'])) $sucesso = 'Página salva com sucesso.';
if (isset($_GET['excluido'])) $sucesso = 'Página excluída.';
$paginas = $pdo->query('SELECT * FROM paginas ORDER BY ordem, nome')->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<h1 class="ao-page-title">Páginas do sistema</h1>
<p style="color: var(--ao-text-muted); font-size: var(--ao-fs-sm); max-width: 640px; margin-top: -8px;">
Esta é a lista de páginas de negócio que podem ser liberadas por perfil, em <a href="perfis.php">Perfis</a>.
Ao cadastrar uma página nova aqui, crie também o arquivo <code>.php</code> correspondente no projeto,
usando a mesma chave na variável <code>$pagina_ativa</code>.
</p>
<?php if ($erro): ?>
<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-card" style="max-width: 600px; margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong><?= $editando ? 'Editar página' : 'Nova página' ?></strong></div>
<form method="post" action="paginas_admin.php">
<input type="hidden" name="acao" value="salvar">
<input type="hidden" name="id" value="<?= (int) ($editando['id'] ?? 0) ?>">
<div class="ao-row" style="gap: var(--ao-space-4); margin-bottom: var(--ao-space-4);">
<div class="ao-field" style="flex: 1;">
<label for="chave">Chave (interna)</label>
<input type="text" id="chave" name="chave" placeholder="ex: relatorios" required value="<?= htmlspecialchars($editando['chave'] ?? '') ?>">
</div>
<div class="ao-field" style="flex: 1;">
<label for="nome">Nome no menu</label>
<input type="text" id="nome" name="nome" placeholder="ex: Relatórios" required value="<?= htmlspecialchars($editando['nome'] ?? '') ?>">
</div>
</div>
<div class="ao-row" style="gap: var(--ao-space-4); margin-bottom: var(--ao-space-5); align-items: flex-end;">
<div class="ao-field" style="flex: 1;">
<label for="arquivo">Arquivo .php</label>
<input type="text" id="arquivo" name="arquivo" placeholder="ex: relatorios.php" required value="<?= htmlspecialchars($editando['arquivo'] ?? '') ?>">
</div>
<div class="ao-field" style="width: 100px;">
<label for="ordem">Ordem</label>
<input type="number" id="ordem" name="ordem" value="<?= (int) ($editando['ordem'] ?? 0) ?>">
</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' : '' ?>> Ativa
</label>
</div>
<button type="submit" class="ao-btn ao-btn-primary"><?= $editando ? 'Salvar alterações' : 'Cadastrar página' ?></button>
<?php if ($editando): ?>
<a href="paginas_admin.php" class="ao-btn ao-btn-secondary">Cancelar</a>
<?php endif; ?>
</form>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Páginas cadastradas</strong></div>
<table class="ao-table">
<thead>
<tr><th>Ordem</th><th>Nome</th><th>Chave</th><th>Arquivo</th><th>Status</th><th></th></tr>
</thead>
<tbody>
<?php foreach ($paginas as $p): ?>
<tr>
<td class="ao-numeric"><?= (int) $p['ordem'] ?></td>
<td><?= htmlspecialchars($p['nome']) ?></td>
<td class="ao-numeric"><?= htmlspecialchars($p['chave']) ?></td>
<td class="ao-numeric"><?= htmlspecialchars($p['arquivo']) ?></td>
<td><span class="ao-badge <?= $p['ativo'] ? 'ao-badge-alta' : 'ao-badge-neutro' ?>"><?= $p['ativo'] ? 'Ativa' : 'Inativa' ?></span></td>
<td style="white-space: nowrap; text-align: right;">
<a href="paginas_admin.php?editar=<?= $p['id'] ?>" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Editar</a>
<form method="post" action="paginas_admin.php" style="display:inline;" onsubmit="return confirm('Excluir esta página? Os perfis vinculados a ela perdem essa permissão.');">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $p['id'] ?>">
<button type="submit" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Excluir</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
+167
View File
@@ -0,0 +1,167 @@
<?php
$titulo_pagina = 'Perfis';
$pagina_ativa = 'perfis';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
ao_exigir_acesso_total();
$erro = null;
$sucesso = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'excluir') {
$id = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare('SELECT COUNT(*) FROM usuarios WHERE perfil_id = ?');
$stmt->execute([$id]);
if ((int) $stmt->fetchColumn() > 0) {
$erro = 'Não é possível excluir: existem usuários com este perfil.';
} else {
$pdo->prepare('DELETE FROM perfis WHERE id = ?')->execute([$id]);
header('Location: perfis.php?excluido=1');
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') {
$id = (int) ($_POST['id'] ?? 0);
$nome = trim($_POST['nome'] ?? '');
$acesso_total = isset($_POST['acesso_total']) ? 1 : 0;
$ativo = isset($_POST['ativo']) ? 1 : 0;
$paginas_sel = array_map('intval', $_POST['paginas'] ?? []);
if ($nome === '') {
$erro = 'Informe o nome do perfil.';
} else {
try {
$pdo->beginTransaction();
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE perfis SET nome=?, acesso_total=?, ativo=? WHERE id=?');
$stmt->execute([$nome, $acesso_total, $ativo, $id]);
} else {
$stmt = $pdo->prepare('INSERT INTO perfis (nome, acesso_total, ativo) VALUES (?,?,?)');
$stmt->execute([$nome, $acesso_total, $ativo]);
$id = (int) $pdo->lastInsertId();
}
$pdo->prepare('DELETE FROM perfil_paginas WHERE perfil_id = ?')->execute([$id]);
if (!$acesso_total && $paginas_sel) {
$stmtIns = $pdo->prepare('INSERT INTO perfil_paginas (perfil_id, pagina_id) VALUES (?, ?)');
foreach ($paginas_sel as $pid) {
$stmtIns->execute([$id, $pid]);
}
}
$pdo->commit();
header('Location: perfis.php?salvo=1');
exit;
} catch (PDOException $e) {
$pdo->rollBack();
$erro = ($e->getCode() === '23000') ? 'Já existe um perfil com este nome.' : ('Erro ao salvar: ' . $e->getMessage());
}
}
}
$editando = null;
$paginas_do_perfil = [];
if (isset($_GET['editar'])) {
$stmt = $pdo->prepare('SELECT * FROM perfis WHERE id = ?');
$stmt->execute([(int) $_GET['editar']]);
$editando = $stmt->fetch() ?: null;
if ($editando) {
$stmt2 = $pdo->prepare('SELECT pagina_id FROM perfil_paginas WHERE perfil_id = ?');
$stmt2->execute([$editando['id']]);
$paginas_do_perfil = array_map('intval', array_column($stmt2->fetchAll(), 'pagina_id'));
}
}
if (isset($_GET['salvo'])) $sucesso = 'Perfil salvo com sucesso.';
if (isset($_GET['excluido'])) $sucesso = 'Perfil excluído.';
$todas_paginas = $pdo->query('SELECT id, chave, nome FROM paginas WHERE ativo = 1 ORDER BY ordem, nome')->fetchAll();
$perfis = $pdo->query(
'SELECT pf.*, (SELECT COUNT(*) FROM usuarios u WHERE u.perfil_id = pf.id) AS total_usuarios FROM perfis pf ORDER BY pf.nome'
)->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<h1 class="ao-page-title">Perfis de acesso</h1>
<?php if ($erro): ?>
<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-card" style="max-width: 560px; margin-bottom: var(--ao-space-6);">
<div class="ao-card-header"><strong><?= $editando ? 'Editar perfil' : 'Novo perfil' ?></strong></div>
<form method="post" action="perfis.php">
<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 do perfil</label>
<input type="text" id="nome" name="nome" required value="<?= htmlspecialchars($editando['nome'] ?? '') ?>">
</div>
<label style="display:flex; align-items:center; gap:6px; font-size: var(--ao-fs-sm); margin-bottom: var(--ao-space-4);">
<input type="checkbox" id="acesso_total" name="acesso_total" <?= (!empty($editando['acesso_total'])) ? 'checked' : '' ?>
onchange="document.getElementById('bloco-paginas').style.display = this.checked ? 'none' : 'block';">
Acesso total (administrador — libera todas as páginas automaticamente)
</label>
<div id="bloco-paginas" style="display: <?= (!empty($editando['acesso_total'])) ? 'none' : 'block' ?>; margin-bottom: var(--ao-space-4); padding: var(--ao-space-3); background: var(--ao-bg-page); border-radius: 8px;">
<label style="display:block; font-size: var(--ao-fs-xs); font-weight:600; color: var(--ao-text-muted); margin-bottom: 8px;">Páginas liberadas para este perfil</label>
<?php if (!$todas_paginas): ?>
<p style="font-size: var(--ao-fs-sm); color: var(--ao-text-muted); margin:0;">Nenhuma página cadastrada ainda — cadastre em <a href="paginas_admin.php">Páginas</a>.</p>
<?php endif; ?>
<?php foreach ($todas_paginas as $p): ?>
<label style="display:flex; align-items:center; gap:6px; font-size: var(--ao-fs-sm); padding: 4px 0;">
<input type="checkbox" name="paginas[]" value="<?= $p['id'] ?>" <?= in_array((int) $p['id'], $paginas_do_perfil, true) ? 'checked' : '' ?>>
<?= htmlspecialchars($p['nome']) ?>
</label>
<?php endforeach; ?>
</div>
<label style="display:flex; align-items:center; gap:6px; font-size: var(--ao-fs-sm); margin-bottom: var(--ao-space-5);">
<input type="checkbox" name="ativo" <?= (!$editando || $editando['ativo']) ? 'checked' : '' ?>> Ativo
</label>
<button type="submit" class="ao-btn ao-btn-primary"><?= $editando ? 'Salvar alterações' : 'Cadastrar perfil' ?></button>
<?php if ($editando): ?>
<a href="perfis.php" class="ao-btn ao-btn-secondary">Cancelar</a>
<?php endif; ?>
</form>
</div>
<div class="ao-card">
<div class="ao-card-header"><strong>Perfis cadastrados</strong></div>
<table class="ao-table">
<thead>
<tr><th>Nome</th><th>Acesso</th><th>Usuários</th><th>Status</th><th></th></tr>
</thead>
<tbody>
<?php foreach ($perfis as $p): ?>
<tr>
<td><?= htmlspecialchars($p['nome']) ?></td>
<td><?= $p['acesso_total'] ? '<span class="ao-badge ao-badge-alta">Total</span>' : '<span class="ao-badge ao-badge-neutro">Configurado</span>' ?></td>
<td class="ao-numeric"><?= (int) $p['total_usuarios'] ?></td>
<td><span class="ao-badge <?= $p['ativo'] ? 'ao-badge-alta' : 'ao-badge-neutro' ?>"><?= $p['ativo'] ? 'Ativo' : 'Inativo' ?></span></td>
<td style="white-space: nowrap; text-align: right;">
<a href="perfis.php?editar=<?= $p['id'] ?>" class="ao-btn ao-btn-secondary" style="padding: 4px 10px; font-size: var(--ao-fs-xs);">Editar</a>
<?php if ((int) $p['total_usuarios'] === 0): ?>
<form method="post" action="perfis.php" style="display:inline;" onsubmit="return confirm('Excluir este perfil?');">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $p['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>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
+2
View File
@@ -0,0 +1,2 @@
<?php
phpinfo();
+81
View File
@@ -0,0 +1,81 @@
<?php
// ATENÇÃO: este arquivo é só para diagnóstico.
// Ele expõe a senha do banco. Apague-o (junto com phpinfo.php) assim que terminar o teste.
$host = 'database';
$user = 'root';
$pass = 'Josim@r230976';
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Diagnóstico do servidor</title>
<style>
body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0f172a;color:#e5e7eb;padding:32px;}
h1{color:#38bdf8;font-size:20px;border-bottom:1px solid #334155;padding-bottom:8px;}
.ok{color:#4ade80;font-weight:bold;}
.fail{color:#f87171;font-weight:bold;}
table{border-collapse:collapse;width:100%;margin-top:10px;}
td,th{border:1px solid #334155;padding:6px 10px;text-align:left;font-size:14px;color:#e5e7eb;}
.card{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:20px;margin-bottom:24px;}
a.btn{display:inline-block;background:#38bdf8;color:#0f172a;padding:8px 16px;border-radius:6px;text-decoration:none;font-weight:bold;}
</style>
</head>
<body>
<div class="card">
<h1>1. Teste de conexão com o banco de dados</h1>
<?php
try {
$pdo = new PDO("mysql:host=$host;charset=utf8mb4", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "<p class='ok'>&#10004; Conexão bem-sucedida com o host '$host'.</p>";
$version = $pdo->query("SELECT VERSION() AS v")->fetch(PDO::FETCH_ASSOC);
echo "<p><strong>Versão do servidor de banco:</strong> " . htmlspecialchars($version['v']) . "</p>";
$dbs = $pdo->query("SHOW DATABASES")->fetchAll(PDO::FETCH_COLUMN);
echo "<p><strong>Bancos de dados disponíveis:</strong></p><ul>";
foreach ($dbs as $db) {
echo "<li>" . htmlspecialchars($db) . "</li>";
}
echo "</ul>";
} catch (PDOException $e) {
echo "<p class='fail'>&#10008; Falha na conexão: " . htmlspecialchars($e->getMessage()) . "</p>";
}
?>
</div>
<div class="card">
<h1>2. Informações resumidas do PHP</h1>
<table>
<?php
$rows = [
'Versão do PHP' => phpversion(),
'Servidor (SAPI)' => php_sapi_name(),
'Sistema operacional' => php_uname(),
'Document root' => $_SERVER['DOCUMENT_ROOT'] ?? '-',
'Extensão pdo_mysql ativa' => extension_loaded('pdo_mysql') ? 'sim' : 'não',
'Extensão mysqli ativa' => extension_loaded('mysqli') ? 'sim' : 'não',
'Limite de memória' => ini_get('memory_limit'),
'Upload máximo' => ini_get('upload_max_filesize'),
'Post máximo' => ini_get('post_max_size'),
'Timezone' => date_default_timezone_get(),
];
foreach ($rows as $k => $v) {
echo "<tr><td>" . htmlspecialchars($k) . "</td><td>" . htmlspecialchars((string)$v) . "</td></tr>";
}
?>
</table>
</div>
<div class="card">
<h1>3. Configurações completas do PHP</h1>
<p>O <code>phpinfo()</code> tem estilo próprio que bagunça o layout desta página, então ele fica isolado em outra página:</p>
<a class="btn" href="phpinfo.php" target="_blank" rel="noopener">Abrir phpinfo() completo</a>
</div>
</body>
</html>
+313
View File
@@ -0,0 +1,313 @@
<?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'; ?>