Versão inicial em produção
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
includes/config.php
|
||||
includes/ambiente.json
|
||||
@@ -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">×</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'; ?>
|
||||
@@ -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); }
|
||||
@@ -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); }
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
@@ -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
@@ -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
@@ -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'; ?>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, '/');
|
||||
}
|
||||
@@ -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>'
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
@@ -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
@@ -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>
|
||||
@@ -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'; ?>
|
||||
@@ -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
@@ -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
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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
@@ -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'; ?>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
phpinfo();
|
||||
+81
@@ -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'>✔ 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'>✘ 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
@@ -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">×</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">« Anterior</a>
|
||||
<?php else: ?>
|
||||
<span class="ao-btn ao-btn-secondary ao-btn-desativado">« 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 »</a>
|
||||
<?php else: ?>
|
||||
<span class="ao-btn ao-btn-secondary ao-btn-desativado">Próxima »</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'; ?>
|
||||
Reference in New Issue
Block a user