Compare commits

...

19 Commits

Author SHA1 Message Date
Flatlogic Bot
1177896daf versao 20 2025-10-29 19:43:33 +00:00
Flatlogic Bot
dbdbcd28a8 Revert to version 20046a4 2025-10-29 19:24:01 +00:00
Flatlogic Bot
6d3c0cd8d3 versao 18 2025-10-29 19:20:02 +00:00
Flatlogic Bot
20046a4844 Versao 17 2025-10-29 17:18:21 +00:00
Flatlogic Bot
e9a7ca1d46 versao 16 2025-10-29 17:01:16 +00:00
Flatlogic Bot
6e8e69c52f versao 15 2025-10-29 16:51:49 +00:00
Flatlogic Bot
9fcffa06dc versao 14 2025-10-29 16:36:48 +00:00
Flatlogic Bot
190ee537e4 versao 13 2025-10-29 16:29:09 +00:00
Flatlogic Bot
024d69e367 versao 12 2025-10-29 16:23:54 +00:00
Flatlogic Bot
e84e983545 versao 11 2025-10-29 16:16:48 +00:00
Flatlogic Bot
5f577e0d7f versao 10 2025-10-29 16:09:14 +00:00
Flatlogic Bot
3b9a4503ac versao 9 2025-10-29 15:55:31 +00:00
Flatlogic Bot
e2359e935c versao 8 2025-10-29 15:53:30 +00:00
Flatlogic Bot
d66f501122 versao 7 2025-10-29 15:49:08 +00:00
Flatlogic Bot
da57997223 versao 6 2025-10-29 15:28:33 +00:00
Flatlogic Bot
19d6fb9cc2 versao 5 2025-10-29 15:25:30 +00:00
Flatlogic Bot
07e06596db versao 4 2025-10-29 15:00:42 +00:00
Flatlogic Bot
a0c63edc92 versao 2 2025-10-29 14:04:14 +00:00
Flatlogic Bot
3be22ccb94 versao 1 2025-10-29 13:38:46 +00:00
29 changed files with 2749 additions and 146 deletions

View File

@ -0,0 +1,159 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/session.php';
require_once __DIR__ . '/../db/config.php';
// Check for user authentication
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Usuário não autenticado.']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
$pdo = db();
switch ($method) {
case 'GET':
handleGet($pdo);
break;
case 'POST':
handlePost($pdo);
break;
case 'PUT':
handlePut($pdo);
break;
case 'PATCH':
handlePatch($pdo);
break;
case 'DELETE':
handleDelete($pdo);
break;
default:
echo json_encode(['success' => false, 'error' => 'Método não suportado.']);
break;
}
function handleGet($pdo) {
try {
if (isset($_GET['action']) && $_GET['action'] == 'getActiveMacroAreas') {
$stmt = $pdo->prepare("SELECT id, name FROM macro_areas WHERE is_active = 1 AND user_id = :user_id ORDER BY name ASC");
$stmt->execute(['user_id' => $_SESSION['user_id']]);
$macro_areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($macro_areas);
} elseif (isset($_GET['id'])) {
$stmt = $pdo->prepare("SELECT * FROM categories WHERE id = :id");
$stmt->execute(['id' => $_GET['id']]);
$category = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode($category);
} else {
$searchTerm = isset($_GET['search']) ? '%' . $_GET['search'] . '%' : '%';
$sql = "SELECT c.id, c.name, c.is_active, c.macro_area_id, ma.name as macro_area_name
FROM categories c
LEFT JOIN macro_areas ma ON c.macro_area_id = ma.id
WHERE c.name LIKE :searchTerm
ORDER BY c.name ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute(['searchTerm' => $searchTerm]);
$categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($categories);
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Erro ao buscar dados: ' . $e->getMessage()]);
}
}
function handlePost($pdo) {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['name']) || empty($data['macro_area_id'])) {
echo json_encode(['success' => false, 'error' => 'Nome e Macro Área são obrigatórios.']);
return;
}
try {
$sql = "INSERT INTO categories (name, macro_area_id, is_active) VALUES (:name, :macro_area_id, :is_active)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':name' => $data['name'],
':macro_area_id' => $data['macro_area_id'],
':is_active' => isset($data['is_active']) ? $data['is_active'] : 1
]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Erro ao criar categoria: ' . $e->getMessage()]);
}
}
function handlePut($pdo) {
$id = $_GET['id'] ?? null;
if (!$id) {
echo json_encode(['success' => false, 'error' => 'ID da categoria não fornecido.']);
return;
}
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['name']) || empty($data['macro_area_id'])) {
echo json_encode(['success' => false, 'error' => 'Nome e Macro Área são obrigatórios.']);
return;
}
try {
$sql = "UPDATE categories SET name = :name, macro_area_id = :macro_area_id, is_active = :is_active WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':id' => $id,
':name' => $data['name'],
':macro_area_id' => $data['macro_area_id'],
':is_active' => isset($data['is_active']) ? $data['is_active'] : 1
]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Erro ao atualizar categoria: ' . $e->getMessage()]);
}
}
function handlePatch($pdo) {
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'] ?? null;
$action = $data['action'] ?? null;
if (!$id || !$action) {
echo json_encode(['success' => false, 'error' => 'Dados inválidos.']);
return;
}
$newStatus = ($action === 'archive') ? 0 : 1;
try {
$sql = "UPDATE categories SET is_active = :is_active WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':id' => $id,
':is_active' => $newStatus
]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Erro ao atualizar status: ' . $e->getMessage()]);
}
}
function handleDelete($pdo) {
$id = $_GET['id'] ?? null;
if (!$id) {
echo json_encode(['success' => false, 'error' => 'ID da categoria não fornecido.']);
return;
}
try {
// Check for dependencies before deleting if necessary
// For example, check if any expenses are using this category
$sql = "DELETE FROM categories WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute(['id' => $id]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Erro ao excluir categoria: ' . $e->getMessage()]);
}
}

View File

@ -0,0 +1,578 @@
/* General Body & Layout */
body {
font-family: 'Inter', sans-serif;
background-color: #f8f9fa;
transition: margin-left .3s;
}
.wrapper {
display: flex;
width: 100%;
}
/* Sidebar Styles */
#sidebar {
width: 280px;
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 999;
background: #4C5958;
color: #fff;
transition: all 0.3s;
padding-bottom: 0; /* Allow profile to stick to bottom */
display: flex;
flex-direction: column;
}
#sidebar.mini {
width: 80px;
}
#sidebar.mini .sidebar-header h3,
#sidebar.mini .menu-item-text,
#sidebar.mini .profile-text,
#sidebar.mini .dropdown-toggle::after {
display: none;
}
#sidebar.mini .menu-item .lucide {
margin-right: 0;
}
#sidebar.mini .profile-section .lucide-user-circle {
margin: 0 auto;
}
.sidebar-header {
padding: 20px;
background: #4C5958;
text-align: center;
border-bottom: 1px solid #5a6867;
flex-shrink: 0; /* Prevent header from shrinking */
}
.sidebar-header h3 {
color: white;
font-weight: 700;
margin-bottom: 0;
font-size: 1.5rem;
white-space: nowrap;
}
/* Profile Section */
.profile-section {
padding: 15px 20px;
border-top: 1px solid #5a6867; /* Moved from bottom */
color: white;
flex-shrink: 0; /* Prevent profile from shrinking */
}
.profile-section .dropdown-toggle {
color: white;
text-decoration: none;
display: flex;
align-items: center;
padding: 5px 0;
}
.profile-section .lucide-user-circle {
width: 40px;
height: 40px;
margin-right: 10px;
}
.profile-section .profile-text span {
display: block;
white-space: nowrap;
}
.profile-section .profile-text .username {
font-weight: bold;
}
.profile-section .dropdown-menu {
background-color: #3f4a49;
border: none;
}
.profile-section .dropdown-item {
color: #e0e0e0;
}
.profile-section .dropdown-item:hover {
background-color: #5a6867;
color: white;
}
/* Menu List Styles */
.menu-list {
list-style: none;
padding: 10px 0;
margin: 0;
flex-grow: 1; /* Allow menu to fill space */
overflow-y: auto; /* Add scroll if needed */
}
.menu-list a {
display: flex;
align-items: center;
padding: 12px 20px;
text-decoration: none;
transition: background 0.2s;
font-size: 0.95rem;
white-space: nowrap;
}
.menu-list .lucide {
margin-right: 15px;
width: 20px;
height: 20px;
flex-shrink: 0;
}
/* Top Level Items */
.menu-section > a {
color: #DBF227;
font-weight: 500;
letter-spacing: 0.5px;
}
.menu-section > a:hover {
background: #3f4a49;
}
/* Sub Items (Level 2) */
.sub-menu {
list-style: none;
padding-left: 0;
background: rgba(0,0,0,0.1);
}
.sub-menu a {
/* NOTE: Using white for better contrast against the dark sidebar.
The requested #10403B is too dark for this background. */
color: #FFFFFF;
font-size: 0.9rem;
padding-left: 35px;
}
.sub-menu a:hover {
background: #3f4a49;
}
.sub-menu .lucide {
width: 18px;
height: 18px;
}
/* Sub-Sub Items (Level 3) */
.sub-sub-menu {
list-style: none;
padding-left: 0;
background: rgba(0,0,0,0.15);
}
.sub-sub-menu a {
color: #e0e0e0; /* Slightly dimmer white */
font-size: 0.85rem;
padding-left: 55px;
}
.sub-sub-menu a:hover {
background: #3f4a49;
}
/* Dropdown Arrow for collapsible menus */
.dropdown-toggle::after {
display: inline-block;
margin-left: auto;
vertical-align: .255em;
content: "";
border-top: .3em solid;
border-right: .3em solid transparent;
border-bottom: 0;
border-left: .3em solid transparent;
transition: transform .2s ease-in-out;
}
.dropdown-toggle[aria-expanded="true"]::after {
transform: rotate(-180deg);
}
/* Badge for notifications */
.badge-notification {
background-color: #dc3545;
color: white;
border-radius: 50%;
padding: 0.2em 0.5em;
font-size: 0.7rem;
margin-left: auto;
margin-right: 10px;
}
/* Main Content Area */
#content {
width: 100%;
padding: 20px;
min-height: 100vh;
transition: all 0.3s;
margin-left: 280px;
}
#content.full-width {
margin-left: 80px;
}
.content-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
#sidebar-toggle {
background: none;
border: none;
font-size: 1.5rem;
color: #333;
cursor: pointer;
}
/* Login Form Styles (keep existing) */
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
max-width: 450px;
width: 100%;
padding: 2rem;
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
}
/* Responsive */
@media (max-width: 768px) {
#sidebar {
left: -280px;
}
#sidebar.active {
left: 0;
}
#content {
margin-left: 0;
}
#content.full-width {
margin-left: 0;
}
body.sidebar-open {
overflow: hidden;
}
}
/* Sidebar Logo */
.sidebar-logo {
max-width: 80%;
height: auto;
display: block;
margin: 0 auto 1rem;
}
#sidebar.mini .sidebar-logo {
display: none; /* Oculta o logo quando o menu está minimizado */
}
#sidebar.mini .sidebar-header {
padding: 10px 0;
}
/* Adiciona um ícone para substituir o logo quando minimizado */
#sidebar.mini .sidebar-header::before {
content: '';
display: block;
width: 32px;
height: 32px;
margin: 0 auto;
background-image: url('../pasted-20251029-150345-2b427067.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/*
* Macro Áreas Module Styles
* --------------------------------------------------
*/
body {
background-color: #eeeeee;
}
/* Titles and Text */
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
color: #10403B;
}
/* Primary Buttons */
.btn-primary {
background-color: #005C53;
border-color: #005C53;
color: #FFFFFF;
}
.btn-primary:hover {
background-color: #00443e;
border-color: #00443e;
}
.btn-primary .fas {
color: #FFFFFF;
}
/* Card Styles */
.card {
border: 1px solid #005C53;
}
.card-header {
background-color: #8AA6A3;
color: #FFFFFF;
}
.card-header .m-0.font-weight-bold {
color: #FFFFFF;
}
/* Table Styles */
.table thead th {
background-color: #8AA6A3;
color: #FFFFFF;
border-color: #8AA6A3;
}
/* Badge Styles */
.badge-status-ativo {
background-color: #8AA6A3;
color: #FFFFFF;
}
.badge-status-arquivado {
background-color: #f0f0f0; /* gray-100 */
color: #333; /* gray-800 */
}
/* Action Icons */
.table .btn .fas {
color: #4C5958;
}
/*
* Macro Áreas Modal Form Styles
* --------------------------------------------------
*/
#macroAreaModal .modal-content {
background-color: #eeeeee;
border: 1px solid #10403B;
}
#macroAreaModal .modal-header,
#macroAreaModal .modal-body label {
color: #10403B;
font-weight: 500;
}
#macroAreaModal .modal-header {
border-bottom: 1px solid #dcdcdc; /* Lighter separator */
}
#macroAreaModal .modal-title {
color: #10403B;
}
#macroAreaModal .form-control {
color: #4C5958;
border: 1px solid #b0b6b5;
}
#macroAreaModal .form-control:focus {
border-color: #10403B;
box-shadow: 0 0 0 0.2rem rgba(16, 64, 59, 0.25);
}
#macroAreaModal .form-control::placeholder {
color: #8AA6A3;
opacity: 1;
}
#macroAreaModal .modal-footer {
border-top: 1px solid #dcdcdc; /* Lighter separator */
}
/* Modal Buttons */
#macroAreaModal .modal-footer .btn-secondary {
background-color: #8AA6A3;
border-color: #8AA6A3;
color: #FFFFFF;
}
#macroAreaModal .modal-footer .btn-secondary:hover {
background-color: #799592;
border-color: #799592;
opacity: 0.9;
}
#macroAreaModal .modal-footer .btn-primary {
background-color: #10403B;
border-color: #10403B;
color: #FFFFFF;
}
#macroAreaModal .modal-footer .btn-primary:hover {
background-color: #0a2926;
border-color: #0a2926;
opacity: 0.9;
}
/* Custom Switch for "Ativo" field */
.form-switch {
padding-left: 2.5em;
cursor: pointer;
}
.form-switch .form-check-input {
width: 2em;
height: 1.25em;
margin-left: -2.5em;
background-color: #8AA6A3;
border-color: #8AA6A3;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: left center;
border-radius: 2em;
transition: background-position .15s ease-in-out;
}
.form-switch .form-check-input:checked {
background-position: right center;
background-color: #10403B;
border-color: #10403B;
}
.form-switch .form-check-label {
padding-top: 2px;
}
/*
* Categories Module Styles
* --------------------------------------------------
*/
/* Typography Utilities */
.text-3xl { font-size: 1.875rem; /* 30px */ }
.font-bold { font-weight: 700; }
.text-main { color: #10403B; }
.text-secondary { color: #4C5958; }
/* Page Header */
.page-title {
font-size: 1.875rem; /* 30px */
font-weight: 700;
color: #10403B;
}
.page-subtitle {
color: #10403B;
}
/* Action Icons */
.action-icon {
color: #4C5958;
cursor: pointer;
text-decoration: none;
}
.action-icon:hover {
color: #10403B;
}
/* Table hover */
.table-hover tbody tr:hover {
background-color: #f8f9fa; /* Corresponds to hover:bg-slate-50 */
}
/* Counter Badge */
.badge-counter {
background-color: #8AA6A3;
color: #FFFFFF;
}
/* Status Badges (overriding defaults for consistency) */
.badge-status-ativo {
background-color: #8AA6A3;
color: #FFFFFF;
}
.badge-status-arquivado {
background-color: #e5e7eb; /* bg-gray-100 */
color: #1f2937; /* text-gray-800 */
}
/* Section Icon */
.section-icon {
color: #005C53;
}
/*
* Category Modal Form (#formCategoriaModal)
* --------------------------------------------------
*/
#formCategoriaModal .modal-content {
background-color: #eeeeee;
border: 1px solid #10403B;
}
#formCategoriaModal .modal-header,
#formCategoriaModal .modal-body label {
color: #10403B;
font-weight: 500;
}
#formCategoriaModal .modal-header {
border-bottom: 1px solid #dcdcdc;
}
#formCategoriaModal .modal-title {
color: #10403B;
}
#formCategoriaModal .form-control,
#formCategoriaModal .form-select {
color: #4C5958;
border: 1px solid #b0b6b5;
}
#formCategoriaModal .form-control:focus,
#formCategoriaModal .form-select:focus {
border-color: #10403B;
box-shadow: 0 0 0 0.2rem rgba(16, 64, 59, 0.25);
}
#formCategoriaModal .form-control::placeholder {
color: #4C5958;
opacity: 0.8;
}
#formCategoriaModal .modal-footer {
border-top: 1px solid #dcdcdc;
}
/* Modal Buttons */
#formCategoriaModal .btn-delete-confirm {
background-color: #8AA6A3;
border-color: #8AA6A3;
color: #FFFFFF;
}
#formCategoriaModal .btn-delete-confirm:hover {
background-color: #799592;
border-color: #799592;
}
/* Info Box */
.info-box {
background-color: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 0.375rem;
padding: 1rem;
color: #0c5460;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 KiB

128
Backend/budgets.php Normal file
View File

@ -0,0 +1,128 @@
<?php
require_once __DIR__ . '/db/config.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Proteger a página
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$user_id = $_SESSION['user_id'];
$pdo = db();
$error_message = '';
$success_message = '';
// Definir o mês do orçamento (padrão para o mês atual)
$budget_month_str = $_GET['month'] ?? date('Y-m');
$budget_month_date = date('Y-m-01', strtotime($budget_month_str . '-01'));
// Lógica para salvar/atualizar orçamentos
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$budgets = $_POST['budgets'] ?? [];
$posted_month = $_POST['budget_month'] ?? $budget_month_date;
try {
$sql = "INSERT INTO budgets (user_id, category, amount, budget_month) VALUES (:user_id, :category, :amount, :budget_month)
ON DUPLICATE KEY UPDATE amount = :amount";
$stmt = $pdo->prepare($sql);
foreach ($budgets as $category => $amount) {
if (is_numeric($amount) && $amount >= 0) {
$stmt->execute([
'user_id' => $user_id,
'category' => $category,
'amount' => $amount,
'budget_month' => $posted_month
]);
}
}
$success_message = 'Orçamentos salvos com sucesso!';
// Redirecionar para o mesmo mês para mostrar a atualização
header('Location: budgets.php?month=' . date('Y-m', strtotime($posted_month)));
exit;
} catch (PDOException $e) {
$error_message = 'Erro ao salvar orçamentos: ' . $e->getMessage();
}
}
// Buscar orçamentos existentes para o mês selecionado
$existing_budgets = [];
try {
$stmt = $pdo->prepare("SELECT category, amount FROM budgets WHERE user_id = :user_id AND budget_month = :budget_month");
$stmt->execute(['user_id' => $user_id, 'budget_month' => $budget_month_date]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($results as $row) {
$existing_budgets[$row['category']] = $row['amount'];
}
} catch (PDOException $e) {
$error_message = "Erro ao buscar orçamentos existentes.";
}
// Categorias fixas
$categories = ['Alimentação', 'Transporte', 'Moradia', 'Lazer', 'Saúde', 'Outros'];
include __DIR__ . '/includes/header.php';
?>
<div class="container mt-4">
<h1 class="mb-4">Gerenciar Orçamentos</h1>
<div class="card">
<div class="card-body">
<!-- Seletor de Mês -->
<form method="GET" action="budgets.php" class="mb-4">
<div class="row align-items-end">
<div class="col-md-4">
<label for="month" class="form-label">Selecione o Mês</label>
<input type="month" class="form-control" id="month" name="month" value="<?php echo htmlspecialchars($budget_month_str); ?>">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Carregar</button>
</div>
</div>
</form>
<hr>
<?php if ($error_message): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<?php if (isset($_SESSION['success_message'])) {
echo '<div class="alert alert-success">' . htmlspecialchars($_SESSION['success_message']) . '</div>';
unset($_SESSION['success_message']);
}?>
<!-- Formulário de Orçamentos -->
<form method="POST" action="budgets.php">
<input type="hidden" name="budget_month" value="<?php echo htmlspecialchars($budget_month_date); ?>">
<h4 class="mb-3">Orçamentos para <?php echo date('F \d\e Y', strtotime($budget_month_date)); ?></h4>
<?php foreach ($categories as $category): ?>
<div class="row mb-2 align-items-center">
<div class="col-md-3">
<label for="budget_<?php echo htmlspecialchars($category); ?>" class="form-label"><?php echo htmlspecialchars($category); ?></label>
</div>
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text">R$</span>
<input type="number" step="0.01" class="form-control" id="budget_<?php echo htmlspecialchars($category); ?>"
name="budgets[<?php echo htmlspecialchars($category); ?>]"
value="<?php echo htmlspecialchars($existing_budgets[$category] ?? '0.00'); ?>">
</div>
</div>
</div>
<?php endforeach; ?>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-primary">Salvar Orçamentos</button>
</div>
</form>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

316
Backend/categories.php Normal file
View File

@ -0,0 +1,316 @@
<?php
require_once __DIR__ . '/includes/session.php';
require_once __DIR__ . '/db/config.php';
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid">
<!-- Page Title -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="page-title">Categorias</h1>
<p class="page-subtitle text-secondary">Gerencie as categorias de despesas para organizar suas finanças.</p>
</div>
<button class="btn btn-primary" id="btnNovaCategoria">
<i data-lucide="plus" class="me-2"></i>Nova Categoria
</button>
</div>
<!-- Main Card -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i data-lucide="layers" class="section-icon me-2"></i>
<h5 class="card-title mb-0">Lista de Categorias</h5>
</div>
<div class="input-group" style="width: 300px;">
<span class="input-group-text bg-transparent border-end-0"><i data-lucide="search" style="color: #4C5958;"></i></span>
<input type="text" id="searchInput" class="form-control border-start-0" placeholder="Buscar por nome...">
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Categoria</th>
<th>Macro Área</th>
<th class="text-center">Status</th>
<th class="text-end">Ações</th>
</tr>
</thead>
<tbody id="categoriesTableBody">
<!-- Rows will be inserted by JavaScript -->
</tbody>
</table>
</div>
<div id="noResultsMessage" class="text-center p-4" style="display: none;">
<p class="text-secondary">Nenhuma categoria encontrada.</p>
</div>
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="text-secondary">
Total de categorias: <span id="totalCategoriesBadge" class="badge badge-counter">0</span>
</div>
<!-- Pagination can be added here if needed -->
</div>
</div>
</div>
<!-- Category Modal -->
<div class="modal fade" id="formCategoriaModal" tabindex="-1" aria-labelledby="formCategoriaModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="formCategoriaModalLabel">Nova Categoria</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="formCategoria">
<input type="hidden" id="categoryId" name="id">
<div class="mb-3">
<label for="categoryName" class="form-label">Nome da Categoria</label>
<input type="text" class="form-control" id="categoryName" name="name" required placeholder="Ex: Supermercado, Transporte">
</div>
<div class="mb-3">
<label for="macroAreaSelect" class="form-label">Macro Área</label>
<select class="form-select" id="macroAreaSelect" name="macro_area_id" required>
<option value="">Carregando...</option>
</select>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="categoryStatus" name="is_active" checked>
<label class="form-check-label" for="categoryStatus">Ativo</label>
</div>
</form>
<div id="archiveInfo" class="info-box mt-3" style="display: none;">
<p class="mb-0"><i data-lucide="info" class="me-2"></i>Categorias arquivadas não podem ser usadas em novos lançamentos, mas permanecem nos registros existentes.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" form="formCategoria" class="btn btn-primary">Salvar</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar Exclusão</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Tem certeza de que deseja excluir a categoria "<strong id="deleteCategoryName"></strong>"?</p>
<p class="text-danger"><i data-lucide="alert-triangle" class="me-2"></i>Esta ação não pode ser desfeita.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" id="confirmDeleteBtn" class="btn btn-danger">Excluir</button>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const modalElement = document.getElementById('formCategoriaModal');
const modal = new bootstrap.Modal(modalElement);
const deleteModalElement = document.getElementById('deleteConfirmModal');
const deleteModal = new bootstrap.Modal(deleteModalElement);
const form = document.getElementById('formCategoria');
const categoryIdInput = document.getElementById('categoryId');
const categoryNameInput = document.getElementById('categoryName');
const macroAreaSelect = document.getElementById('macroAreaSelect');
const categoryStatusSwitch = document.getElementById('categoryStatus');
const modalTitle = document.getElementById('formCategoriaModalLabel');
const archiveInfo = document.getElementById('archiveInfo');
const searchInput = document.getElementById('searchInput');
let macroAreas = [];
function loadMacroAreas() {
fetch('/Backend/api/category_handler.php?action=getActiveMacroAreas')
.then(response => response.json())
.then(data => {
macroAreas = data;
const select = document.getElementById('macroAreaSelect');
select.innerHTML = '<option value="">Selecione uma Macro Área</option>'; // Clear existing options
macroAreas.forEach(ma => {
const option = new Option(ma.name, ma.id);
select.add(option);
});
})
.catch(error => {
console.error('Erro ao carregar macro áreas:', error);
const select = document.getElementById('macroAreaSelect');
select.innerHTML = '<option value="">Erro ao carregar</option>';
});
}
function fetchCategories(searchTerm = '') {
fetch(`/Backend/api/category_handler.php?search=${encodeURIComponent(searchTerm)}`)
.then(response => response.json())
.then(data => {
const tableBody = document.getElementById('categoriesTableBody');
const noResultsMessage = document.getElementById('noResultsMessage');
tableBody.innerHTML = '';
if (data.length === 0) {
noResultsMessage.style.display = 'block';
} else {
noResultsMessage.style.display = 'none';
}
data.forEach(category => {
const statusBadge = category.is_active
? `<span class="badge badge-status-ativo">Ativo</span>`
: `<span class="badge badge-status-arquivado">Arquivado</span>`;
const row = `
<tr id="category-row-${category.id}">
<td>${escapeHTML(category.name)}</td>
<td>${escapeHTML(category.macro_area_name || 'N/A')}</td>
<td class="text-center">${statusBadge}</td>
<td class="text-end">
<a href="#" class="action-icon me-2" data-id="${category.id}" onclick="handleEdit(event)"><i data-lucide="edit"></i></a>
<a href="#" class="action-icon me-2" data-id="${category.id}" data-active="${category.is_active}" onclick="handleArchive(event)"><i data-lucide="archive"></i></a>
<a href="#" class="action-icon" data-id="${category.id}" data-name="${escapeHTML(category.name)}" onclick="handleDelete(event)"><i data-lucide="trash-2"></i></a>
</td>
</tr>`;
tableBody.insertAdjacentHTML('beforeend', row);
});
document.getElementById('totalCategoriesBadge').textContent = data.length;
lucide.createIcons();
});
}
function handleFormSubmit(event) {
event.preventDefault();
const id = categoryIdInput.value;
const url = id ? `/Backend/api/category_handler.php?id=${id}` : '/Backend/api/category_handler.php';
const method = id ? 'PUT' : 'POST';
const formData = new FormData(form);
const payload = Object.fromEntries(formData.entries());
payload.is_active = categoryStatusSwitch.checked ? 1 : 0;
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
if (data.success) {
modal.hide();
fetchCategories(searchInput.value);
} else {
alert('Erro ao salvar categoria: ' + data.error);
}
});
}
window.handleEdit = function(event) {
event.preventDefault();
const id = event.currentTarget.dataset.id;
fetch(`/Backend/api/category_handler.php?id=${id}`)
.then(response => response.json())
.then(category => {
modalTitle.textContent = 'Editar Categoria';
categoryIdInput.value = category.id;
categoryNameInput.value = category.name;
macroAreaSelect.value = category.macro_area_id;
categoryStatusSwitch.checked = category.is_active == 1;
archiveInfo.style.display = category.is_active == 1 ? 'none' : 'block';
modal.show();
});
}
window.handleArchive = function(event) {
event.preventDefault();
const id = event.currentTarget.dataset.id;
const isActive = event.currentTarget.dataset.active == 1;
const action = isActive ? 'archive' : 'unarchive';
if (!confirm(`Tem certeza de que deseja ${isActive ? 'arquivar' : 'reativar'} esta categoria?`)) return;
fetch(`/Backend/api/category_handler.php`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id, action: action })
})
.then(response => response.json())
.then(data => {
if (data.success) {
fetchCategories(searchInput.value);
} else {
alert('Erro: ' + data.error);
}
});
}
window.handleDelete = function(event) {
event.preventDefault();
const id = event.currentTarget.dataset.id;
const name = event.currentTarget.dataset.name;
document.getElementById('deleteCategoryName').textContent = name;
deleteModal.show();
document.getElementById('confirmDeleteBtn').onclick = function() {
fetch(`/Backend/api/category_handler.php?id=${id}`, { method: 'DELETE' })
.then(response => response.json())
.then(data => {
if (data.success) {
deleteModal.hide();
fetchCategories(searchInput.value);
} else {
alert('Erro ao excluir: ' + data.error);
}
});
}
}
document.getElementById('btnNovaCategoria').addEventListener('click', function() {
modalTitle.textContent = 'Nova Categoria';
form.reset();
categoryIdInput.value = '';
categoryStatusSwitch.checked = true;
archiveInfo.style.display = 'none';
modal.show();
});
searchInput.addEventListener('input', () => fetchCategories(searchInput.value));
form.addEventListener('submit', handleFormSubmit);
function escapeHTML(str) {
if (typeof str !== 'string') return '';
return str.replace(/[&<>'"/]/g, function (tag) {
var chars = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;'
};
return chars[tag] || tag;
});
}
// Initial load
fetchCategories();
loadMacroAreas();
});
</script>

66
Backend/db/migrate.php Normal file
View File

@ -0,0 +1,66 @@
<?php
// Script de migração aprimorado para rodar múltiplos arquivos SQL de forma idempotente.
require_once __DIR__ . '/config.php';
try {
$pdo = db();
// 1. Garantir que a tabela de controle de migrações exista
$pdo->exec("CREATE TABLE IF NOT EXISTS `migrations` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`migration_name` VARCHAR(255) NOT NULL UNIQUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
// 2. Obter migrações já executadas
$executed_migrations_stmt = $pdo->query("SELECT migration_name FROM migrations");
$executed_migrations = $executed_migrations_stmt->fetchAll(PDO::FETCH_COLUMN);
// 3. Encontrar todos os arquivos de migração
$migration_files = glob(__DIR__ . '/migrations/*.sql');
sort($migration_files);
$new_migrations_run = false;
// 4. Iterar e executar novas migrações
foreach ($migration_files as $file) {
$migration_name = basename($file);
if (!in_array($migration_name, $executed_migrations)) {
echo "Executando migração: {$migration_name}...\n";
$sql = file_get_contents($file);
$pdo->exec($sql);
// Registrar a migração como executada
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (:migration_name)");
$stmt->execute(['migration_name' => $migration_name]);
echo "Migração '{$migration_name}' executada com sucesso!\n";
$new_migrations_run = true;
}
}
if (!$new_migrations_run) {
echo "Nenhuma nova migração para executar. O banco de dados já está atualizado.\n";
} else {
echo "\nMigrações concluídas.\n";
}
// Lógica de atualização de senha do usuário de teste (executada sempre)
// Garante que a senha seja corrigida mesmo que a migração inicial tenha falhado.
$password = 'password123';
$hash = password_hash($password, PASSWORD_DEFAULT);
// Força a atualização da senha para o usuário de teste, independentemente do valor atual.
$update_hash_sql = "UPDATE users SET password_hash = :hash WHERE email = 'chefe@familia.com'";
$stmt_user = $pdo->prepare($update_hash_sql);
$stmt_user->execute(['hash' => $hash]);
if ($stmt_user->rowCount() > 0) {
echo "Hash do usuário de teste foi corrigido para o valor padrão.\n";
}
} catch (PDOException $e) {
die("Erro na migração: " . $e->getMessage());
}

View File

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS `users` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL,
`email` VARCHAR(100) NOT NULL UNIQUE,
`password_hash` VARCHAR(255) NOT NULL,
`role` ENUM('admin', 'family_head', 'family_member', 'guest') NOT NULL DEFAULT 'family_head',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Inserir um usuário de teste para que possamos fazer login
-- A senha será definida pelo script de migração
INSERT INTO users (name, email, password_hash, role)
SELECT 'Chefe da Família Teste', 'chefe@familia.com', 'placeholder', 'family_head'
WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'chefe@familia.com');

View File

@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS `expenses` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`description` VARCHAR(255) NOT NULL,
`amount` DECIMAL(10, 2) NOT NULL,
`category` VARCHAR(100) NOT NULL,
`expense_date` DATE NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS `budgets` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`category` VARCHAR(100) NOT NULL,
`amount` DECIMAL(10, 2) NOT NULL,
`budget_month` DATE NOT NULL, -- Stores the first day of the month, e.g., 2025-10-01
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
UNIQUE KEY `user_category_month` (`user_id`, `category`, `budget_month`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS `macro_areas` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nome` VARCHAR(255) NOT NULL,
`slug` VARCHAR(255) NOT NULL UNIQUE,
`descricao` TEXT,
`ativo` BOOLEAN NOT NULL DEFAULT TRUE,
`user_id` INT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS `categories` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nome` VARCHAR(255) NOT NULL,
`macro_area_id` INT NOT NULL,
`ativo` BOOLEAN NOT NULL DEFAULT TRUE,
`user_id` INT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`macro_area_id`) REFERENCES `macro_areas`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/db/config.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Proteger a página: redirecionar para o login se não estiver logado
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$expense_id = $_GET['id'] ?? null;
$user_id = $_SESSION['user_id'];
if (!$expense_id) {
// Se não houver ID, redireciona de volta
header('Location: expenses.php');
exit;
}
try {
$pdo = db();
// A cláusula WHERE garante que um usuário só pode deletar suas próprias despesas
$sql = "DELETE FROM expenses WHERE id = :id AND user_id = :user_id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $expense_id,
'user_id' => $user_id
]);
// Opcional: verificar se alguma linha foi afetada para dar um feedback mais preciso
if ($stmt->rowCount() > 0) {
$_SESSION['success_message'] = 'Despesa excluída com sucesso!';
} else {
$_SESSION['error_message'] = 'Não foi possível excluir a despesa. Ela não foi encontrada ou não pertence a você.';
}
} catch (PDOException $e) {
$_SESSION['error_message'] = 'Erro ao excluir a despesa: ' . $e->getMessage();
}
// Redirecionar de volta para a página de despesas
header('Location: expenses.php');
exit;

View File

@ -0,0 +1,44 @@
<?php
require_once 'includes/session.php';
require_once 'db/config.php';
// Check if ID is provided
if (isset($_GET['id'])) {
$id = $_GET['id'];
$pdo = db();
// First, find the macro area to get its slug
$stmt = $pdo->prepare("SELECT slug FROM macro_areas WHERE id = ?");
$stmt->execute([$id]);
$macro_area = $stmt->fetch();
// If the macro area exists, proceed with checks and deletion
if ($macro_area) {
$slug = $macro_area['slug'];
// Check for dependent expenses
$stmt = $pdo->prepare("SELECT COUNT(*) FROM expenses WHERE category = ?");
$stmt->execute([$slug]);
$expense_count = $stmt->fetchColumn();
if ($expense_count > 0) {
// Dependencies found, set error message and redirect
$_SESSION['error_message'] = "Não é possível excluir a macro área. Existem {$expense_count} despesas associadas.";
} else {
// No dependencies, proceed with deletion
$stmt = $pdo->prepare("DELETE FROM macro_areas WHERE id = ?");
$stmt->execute([$id]);
$_SESSION['success_message'] = "Macro área excluída com sucesso.";
}
}
// If the macro area was not found, we just redirect without any message.
} else {
// ID not specified in URL
$_SESSION['error_message'] = "ID da macro área não especificado.";
}
// Redirect back to the list in all cases
header('Location: /Backend/macro_areas.php');
exit;
?>

126
Backend/edit_expense.php Normal file
View File

@ -0,0 +1,126 @@
<?php
require_once __DIR__ . '/db/config.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Proteger a página
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$expense_id = $_GET['id'] ?? null;
$user_id = $_SESSION['user_id'];
$error_message = '';
$expense = null;
if (!$expense_id) {
header('Location: expenses.php');
exit;
}
$pdo = db();
// Buscar a despesa para garantir que ela pertence ao usuário
try {
$stmt = $pdo->prepare("SELECT * FROM expenses WHERE id = :id AND user_id = :user_id");
$stmt->execute(['id' => $expense_id, 'user_id' => $user_id]);
$expense = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$expense) {
$_SESSION['error_message'] = 'Despesa não encontrada.';
header('Location: expenses.php');
exit;
}
} catch (PDOException $e) {
$_SESSION['error_message'] = 'Erro ao buscar despesa.';
header('Location: expenses.php');
exit;
}
// Lógica para atualizar a despesa
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$description = $_POST['description'] ?? '';
$amount = $_POST['amount'] ?? '';
$category = $_POST['category'] ?? '';
$expense_date = $_POST['expense_date'] ?? '';
if (empty($description) || empty($amount) || empty($category) || empty($expense_date)) {
$error_message = 'Todos os campos são obrigatórios.';
} else {
try {
$sql = "UPDATE expenses SET description = :description, amount = :amount, category = :category, expense_date = :expense_date WHERE id = :id AND user_id = :user_id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'description' => $description,
'amount' => $amount,
'category' => $category,
'expense_date' => $expense_date,
'id' => $expense_id,
'user_id' => $user_id
]);
$_SESSION['success_message'] = 'Despesa atualizada com sucesso!';
header('Location: expenses.php');
exit;
} catch (PDOException $e) {
$error_message = 'Erro ao atualizar a despesa: ' . $e->getMessage();
}
}
}
include __DIR__ . '/includes/header.php';
?>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h4 class="card-title">Editar Despesa</h4>
<?php if ($error_message): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<form method="POST" action="edit_expense.php?id=<?php echo htmlspecialchars($expense_id); ?>">
<div class="mb-3">
<label for="description" class="form-label">Descrição</label>
<input type="text" class="form-control" id="description" name="description" value="<?php echo htmlspecialchars($expense['description']); ?>" required>
</div>
<div class="mb-3">
<label for="amount" class="form-label">Valor (R$)</label>
<input type="number" step="0.01" class="form-control" id="amount" name="amount" value="<?php echo htmlspecialchars($expense['amount']); ?>" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">Categoria</label>
<select class="form-select" id="category" name="category" required>
<option value="">Selecione...</option>
<?php
$categories = ['Alimentação', 'Transporte', 'Moradia', 'Lazer', 'Saúde', 'Outros'];
foreach ($categories as $cat) {
$selected = ($expense['category'] === $cat) ? 'selected' : '';
echo "<option value=\"".htmlspecialchars($cat)."\" $selected>".htmlspecialchars($cat)."</option>";
}
?>
</select>
</div>
<div class="mb-3">
<label for="expense_date" class="form-label">Data da Despesa</label>
<input type="date" class="form-control" id="expense_date" name="expense_date" value="<?php echo htmlspecialchars($expense['expense_date']); ?>" required>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="expenses.php" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">Salvar Alterações</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

247
Backend/expenses.php Normal file
View File

@ -0,0 +1,247 @@
<?php
require_once __DIR__ . '/db/config.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Proteger a página
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$user_id = $_SESSION['user_id'];
$pdo = db();
$error_message = '';
$success_message = '';
// Lógica para ADICIONAR nova despesa (POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$description = $_POST['description'] ?? '';
$amount = $_POST['amount'] ?? '';
$category = $_POST['category'] ?? '';
$expense_date = $_POST['expense_date'] ?? '';
if (empty($description) || empty($amount) || empty($category) || empty($expense_date)) {
$error_message = 'Todos os campos são obrigatórios para adicionar uma despesa.';
} else {
try {
$sql = "INSERT INTO expenses (user_id, description, amount, category, expense_date) VALUES (:user_id, :description, :amount, :category, :expense_date)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'user_id' => $user_id,
'description' => $description,
'amount' => $amount,
'category' => $category,
'expense_date' => $expense_date
]);
$_SESSION['success_message'] = 'Despesa registrada com sucesso!';
header('Location: expenses.php'); // Redirecionar para limpar o POST
exit;
} catch (PDOException $e) {
$error_message = 'Erro ao registrar a despesa: ' . $e->getMessage();
}
}
}
// Lógica para FILTRAR e BUSCAR despesas (GET)
$filter_start_date = $_GET['start_date'] ?? '';
$filter_end_date = $_GET['end_date'] ?? '';
$filter_category = $_GET['category'] ?? '';
$sql = "SELECT * FROM expenses WHERE user_id = :user_id";
$params = ['user_id' => $user_id];
if ($filter_start_date) {
$sql .= " AND expense_date >= :start_date";
$params['start_date'] = $filter_start_date;
}
if ($filter_end_date) {
$sql .= " AND expense_date <= :end_date";
$params['end_date'] = $filter_end_date;
}
if ($filter_category) {
$sql .= " AND category = :category";
$params['category'] = $filter_category;
}
$sql .= " ORDER BY expense_date DESC";
$expenses = [];
$total_filtered_amount = 0;
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Calcular total dos itens filtrados
foreach ($expenses as $expense) {
$total_filtered_amount += $expense['amount'];
}
} catch (PDOException $e) {
$error_message = 'Erro ao buscar despesas: ' . $e->getMessage();
}
// Obter todas as categorias para o dropdown do filtro
$categories = ['Alimentação', 'Transporte', 'Moradia', 'Lazer', 'Saúde', 'Outros'];
include __DIR__ . '/includes/header.php';
?>
<div class="container mt-4">
<div class="row">
<!-- Coluna para Adicionar Despesa -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body">
<h4 class="card-title">Registrar Nova Despesa</h4>
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && $error_message): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<form method="POST" action="expenses.php">
<!-- Campos do formulário de adição -->
<div class="mb-3">
<label for="description" class="form-label">Descrição</label>
<input type="text" class="form-control" id="description" name="description" required>
</div>
<div class="mb-3">
<label for="amount" class="form-label">Valor (R$)</label>
<input type="number" step="0.01" class="form-control" id="amount" name="amount" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">Categoria</label>
<select class="form-select" id="category" name="category" required>
<option value="">Selecione...</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo htmlspecialchars($cat); ?>"><?php echo htmlspecialchars($cat); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="expense_date" class="form-label">Data da Despesa</label>
<input type="date" class="form-control" id="expense_date" name="expense_date" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Adicionar Despesa</button>
</div>
</form>
</div>
</div>
</div>
<!-- Coluna para Listar e Filtrar Despesas -->
<div class="col-md-8">
<div class="card">
<div class="card-body">
<h4 class="card-title">Minhas Despesas</h4>
<!-- Mensagens de feedback -->
<?php
if (isset($_SESSION['success_message'])) {
echo '<div class="alert alert-success">' . htmlspecialchars($_SESSION['success_message']) . '</div>';
unset($_SESSION['success_message']);
}
if (isset($_SESSION['error_message'])) {
echo '<div class="alert alert-danger">' . htmlspecialchars($_SESSION['error_message']) . '</div>';
unset($_SESSION['error_message']);
}
?>
<!-- Formulário de Filtro -->
<form method="GET" action="expenses.php" class="mb-4 p-3 bg-light rounded">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label for="start_date" class="form-label">De</label>
<input type="date" class="form-control" id="start_date" name="start_date" value="<?php echo htmlspecialchars($filter_start_date); ?>">
</div>
<div class="col-md-4">
<label for="end_date" class="form-label">Até</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="<?php echo htmlspecialchars($filter_end_date); ?>">
</div>
<div class="col-md-4">
<label for="filter_category" class="form-label">Categoria</label>
<select class="form-select" id="filter_category" name="category">
<option value="">Todas</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo htmlspecialchars($cat); ?>" <?php echo ($filter_category === $cat) ? 'selected' : ''; ?>><?php echo htmlspecialchars($cat); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12 d-flex justify-content-end mt-3">
<a href="expenses.php" class="btn btn-secondary me-2">Limpar</a>
<button type="submit" class="btn btn-primary me-2">Filtrar</button>
<a href="#" id="export-csv" class="btn btn-success"><i class="bi bi-download me-2"></i>Exportar CSV</a>
</div>
</div>
</form>
<!-- Resumo dos Filtros -->
<div class="alert alert-info">
<strong>Total Filtrado:</strong> R$ <?php echo number_format($total_filtered_amount, 2, ',', '.'); ?>
</div>
<!-- Tabela de Despesas -->
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Descrição</th>
<th>Valor</th>
<th>Categoria</th>
<th>Data</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
<?php if (empty($expenses)): ?>
<tr>
<td colspan="5" class="text-center">Nenhuma despesa encontrada para os filtros aplicados.</td>
</tr>
<?php else: ?>
<?php foreach ($expenses as $expense): ?>
<tr>
<td><?php echo htmlspecialchars($expense['description']); ?></td>
<td>R$ <?php echo number_format($expense['amount'], 2, ',', '.'); ?></td>
<td><?php echo htmlspecialchars($expense['category']); ?></td>
<td><?php echo date('d/m/Y', strtotime($expense['expense_date'])); ?></td>
<td>
<a href="edit_expense.php?id=<?php echo $expense['id']; ?>" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil-sm"></i></a>
<a href="delete_expense.php?id=<?php echo $expense['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Tem certeza que deseja excluir esta despesa?');"><i class="bi bi-trash-sm"></i></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const exportBtn = document.getElementById('export-csv');
if (exportBtn) {
exportBtn.addEventListener('click', function(e) {
e.preventDefault();
const startDate = document.getElementById('start_date').value;
const endDate = document.getElementById('end_date').value;
const category = document.getElementById('filter_category').value;
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (category) params.append('category', category);
const exportUrl = 'export.php?' + params.toString();
window.location.href = exportUrl;
});
}
});
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>

60
Backend/export.php Normal file
View File

@ -0,0 +1,60 @@
<?php
session_start();
require_once 'db/config.php';
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit();
}
$user_id = $_SESSION['user_id'];
$pdo = db();
// Build query with filters
$sql = "SELECT * FROM expenses WHERE user_id = :user_id";
$params = ['user_id' => $user_id];
if (!empty($_GET['start_date'])) {
$sql .= " AND expense_date >= :start_date";
$params['start_date'] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$sql .= " AND expense_date <= :end_date";
$params['end_date'] = $_GET['end_date'];
}
if (!empty($_GET['category'])) {
$sql .= " AND category = :category";
$params['category'] = $_GET['category'];
}
$sql .= " ORDER BY expense_date DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Set headers for CSV download
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="despesas.csv"');
// Open output stream
$output = fopen('php://output', 'w');
// Write CSV header
fputcsv($output, ['Data', 'Descricao', 'Valor', 'Categoria']);
// Write data
if ($expenses) {
foreach ($expenses as $expense) {
fputcsv($output, [
$expense['expense_date'],
$expense['description'],
$expense['amount'],
$expense['category']
]);
}
}
fclose($output);
exit();

View File

@ -0,0 +1,23 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
lucide.createIcons();
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebar = document.getElementById('sidebar');
const content = document.getElementById('content');
if (sidebarToggle && sidebar && content) {
sidebarToggle.addEventListener('click', function() {
// This handles both mobile (active) and desktop (mini) states
// The CSS media queries will apply the correct styles.
sidebar.classList.toggle('active');
sidebar.classList.toggle('mini');
content.classList.toggle('full-width');
});
}
});
</script>
</body>
</html>

192
Backend/includes/header.php Normal file
View File

@ -0,0 +1,192 @@
<?php
// Buscar o nome do usuário se estiver logado
$user_name = '';
$first_name = '';
if (isset($_SESSION['user_id'])) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT name FROM users WHERE id = :id");
$stmt->execute(['id' => $_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
// Pega o primeiro nome para uma saudação mais curta
$first_name = explode(' ', $user['name'])[0];
$user_name = $user['name'];
}
} catch (PDOException $e) {
// Em caso de erro, o nome fica em branco
}
}
// Placeholder for unclassified expenses count
$unclassifiedCount = 5; // Example value
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Concilia Fácil</title>
<meta name="description" content="Software de controle financeiro familiar para transformar despesas em investimentos.">
<meta name="keywords" content="controle financeiro, finanças pessoais, orçamento familiar, investimentos, economizar dinheiro, gestão de despesas, app de finanças, Built with Flatlogic Generator">
<meta property="og:title" content="Concilia Fácil">
<meta property="og:description" content="Software de controle financeiro familiar para transformar despesas em investimentos.">
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<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=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/Backend/assets/css/custom.css?v=<?php echo time(); ?>">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body>
<div class="wrapper">
<!-- Sidebar -->
<?php if (isset($_SESSION['user_id'])): ?>
<nav id="sidebar">
<div class="sidebar-header">
<img src="/Backend/assets/pasted-20251029-150345-2b427067.png" alt="Concilia Fácil Logo" class="sidebar-logo">
</div>
<ul class="list-unstyled menu-list">
<!-- Cadastros Básicos -->
<li class="menu-section">
<a href="#cadastrosSubmenu" data-bs-toggle="collapse" aria-expanded="true" class="dropdown-toggle">
<i data-lucide="folder-tree"></i>
<span class="menu-item-text">Cadastros Básicos</span>
</a>
<ul class="collapse show list-unstyled sub-menu" id="cadastrosSubmenu">
<!-- Plano de Contas -->
<li>
<a href="#planoContasSubmenu" data-bs-toggle="collapse" aria-expanded="true" class="dropdown-toggle">
<i data-lucide="book-text"></i>
<span class="menu-item-text">Plano de Contas</span>
</a>
<ul class="collapse show list-unstyled sub-sub-menu" id="planoContasSubmenu">
<li><a href="/Backend/macro_areas.php"><span class="menu-item-text">Macro Áreas</span></a></li>
<li><a href="/Backend/categories.php"><span class="menu-item-text">Categorias</span></a></li>
<li><a href="#"><span class="menu-item-text">Centro de Custo</span></a></li>
<li><a href="#"><span class="menu-item-text">Alocação do Plano</span></a></li>
</ul>
</li>
<!-- Bancos -->
<li>
<a href="#bancosSubmenu" data-bs-toggle="collapse" aria-expanded="true" class="dropdown-toggle">
<i data-lucide="building-2"></i>
<span class="menu-item-text">Bancos</span>
</a>
<ul class="collapse show list-unstyled sub-sub-menu" id="bancosSubmenu">
<li><a href="#"><span class="menu-item-text">Bancos</span></a></li>
<li><a href="#"><span class="menu-item-text">Tipo de Conta</span></a></li>
<li><a href="#"><span class="menu-item-text">Contas Bancárias</span></a></li>
<li><a href="#"><span class="menu-item-text">Status de Pagamento</span></a></li>
<li><a href="#"><span class="menu-item-text">Portabilidade</span></a></li>
</ul>
</li>
<!-- Famílias -->
<li>
<a href="#familiasSubmenu" data-bs-toggle="collapse" aria-expanded="true" class="dropdown-toggle">
<i data-lucide="users"></i>
<span class="menu-item-text">Famílias</span>
</a>
<ul class="collapse show list-unstyled sub-sub-menu" id="familiasSubmenu">
<li><a href="#"><span class="menu-item-text">Relações Familiares</span></a></li>
<li><a href="#"><span class="menu-item-text">Grupos Familiares</span></a></li>
<li><a href="#"><span class="menu-item-text">Composição Familiar</span></a></li>
</ul>
</li>
<!-- Cartões de Crédito -->
<li>
<a href="#cartoesSubmenu" data-bs-toggle="collapse" aria-expanded="true" class="dropdown-toggle">
<i data-lucide="credit-card"></i>
<span class="menu-item-text">Cartões de Crédito</span>
</a>
<ul class="collapse show list-unstyled sub-sub-menu" id="cartoesSubmenu">
<li><a href="#"><span class="menu-item-text">Bandeiras</span></a></li>
<li><a href="#"><span class="menu-item-text">Cartões de Crédito</span></a></li>
</ul>
</li>
</ul>
</li>
<!-- Despesas -->
<li class="menu-section">
<a href="#despesasSubmenu" data-bs-toggle="collapse" aria-expanded="true" class="dropdown-toggle">
<i data-lucide="wallet"></i>
<span class="menu-item-text">Despesas</span>
</a>
<ul class="collapse show list-unstyled sub-menu" id="despesasSubmenu">
<li><a href="#"><span class="menu-item-text">Vencimento</span></a></li>
<li>
<a href="#">
<span class="menu-item-text">Identificação de Despesas</span>
<?php if ($unclassifiedCount > 0): ?>
<span class="badge-notification"><?php echo $unclassifiedCount; ?></span>
<?php endif; ?>
</a>
</li>
<li><a href="/Backend/expenses.php"><span class="menu-item-text">Movimentação Bancária</span></a></li>
<li><a href="#"><span class="menu-item-text">Despesas no Cartão</span></a></li>
<li><a href="#"><span class="menu-item-text">Previsão de Despesas</span></a></li>
</ul>
</li>
<!-- Análise de Dados -->
<li class="menu-section">
<a href="#analiseSubmenu" data-bs-toggle="collapse" aria-expanded="true" class="dropdown-toggle">
<i data-lucide="bar-chart-2"></i>
<span class="menu-item-text">Análise de Dados</span>
</a>
<ul class="collapse show list-unstyled sub-menu" id="analiseSubmenu">
<li><a href="#"><span class="menu-item-text">Gráfico de Despesas por Áreas</span></a></li>
</ul>
</li>
</ul>
<div class="profile-section dropdown">
<a href="#" class="dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
<i data-lucide="user-circle"></i>
<div class="profile-text">
<span class="username"><?php echo htmlspecialchars($first_name); ?></span>
<span>Meu Perfil</span>
</div>
</a>
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="dropdownUser1">
<li><a class="dropdown-item" href="/Backend/profile.php">Ver Perfil</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/Backend/logout.php">Sair</a></li>
</ul>
</div>
</nav>
<?php endif; ?>
<!-- Page Content -->
<div id="content" class="<?php echo !isset($_SESSION['user_id']) ? 'w-100' : '' ?>">
<?php if (isset($_SESSION['user_id'])): ?>
<div class="content-header">
<button type="button" id="sidebar-toggle" class="btn">
<i data-lucide="menu"></i>
</button>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success" role="alert">
<?php echo $_SESSION['success_message']; ?>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger" role="alert">
<?php echo $_SESSION['error_message']; ?>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>

View File

@ -0,0 +1,37 @@
<?php
// Define um nome de sessão específico para a aplicação
$session_name = 'flatlogic_app_session';
session_name($session_name);
// Determina se a conexão é segura, considerando o proxy reverso (Cloudflare)
$is_secure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
// Configurações de segurança para o cookie da sessão
if ($is_secure) {
// Configuração para produção sob HTTPS (essencial para proxies)
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'None' // 'None' requer 'secure' => true
]);
} else {
// Configuração para desenvolvimento local (HTTP)
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
}
// Inicia a sessão
if (session_status() == PHP_SESSION_NONE) {
session_start();
}

82
Backend/login.php Normal file
View File

@ -0,0 +1,82 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/session.php';
// Se já estiver logado, redireciona para o dashboard
if (isset($_SESSION['user_id'])) {
header('Location: /index.php');
exit;
}
$error_message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password)) {
$error_message = 'Por favor, preencha o e-mail e a senha.';
} else {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE email = :email");
$stmt->execute(['email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password_hash'])) {
// Login bem-sucedido: Redireciona com flag de depuração
// Senha correta, inicie a sessão
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_email'] = $user['email'];
header("Location: /index.php");
exit();
} else {
// Credenciais inválidas
$error_message = 'E-mail ou senha inválidos.';
}
} catch (PDOException $e) {
// Idealmente, logar o erro em vez de exibir
$error_message = 'Erro no banco de dados. Tente novamente mais tarde.';
}
}
}
include __DIR__ . '/includes/header.php';
?>
<div class="container login-container">
<div class="card login-card">
<div class="card-body">
<h3 class="card-title text-center mb-4">
<i class="bi bi-safe me-2"></i>
<span class="navbar-brand-logo">Galilei Finance</span>
</h3>
<?php if ($error_message): ?>
<div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($error_message); ?>
</div>
<?php endif; ?>
<form method="POST" action="/Backend/login.php">
<div class="mb-3">
<label for="email" class="form-label">E-mail</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Senha</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Entrar</button>
</div>
<div class="text-center mt-3">
<a href="#">Esqueceu a senha?</a>
</div>
</form>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

8
Backend/logout.php Normal file
View File

@ -0,0 +1,8 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
session_unset();
session_destroy();
header('Location: /Backend/login.php');
exit;

114
Backend/macro_area_form.php Normal file
View File

@ -0,0 +1,114 @@
<?php
require_once 'includes/session.php';
require_once 'db/config.php';
// Helper function to generate a slug from a string
function generateSlug($string) {
$string = iconv('UTF-8', 'ASCII//TRANSLIT', $string);
$string = strtolower($string);
$string = preg_replace('/[^a-z0-9_\-]+/', '-', $string);
$string = preg_replace('/-+/', '-', $string);
$string = trim($string, '-');
return $string;
}
$pdo = db();
$error = null;
$macro_area = [
'id' => '',
'nome' => '',
'descricao' => '',
'ativo' => 1
];
$page_title = 'Nova Macro Área';
// Handle form submission (Create/Update)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? null;
$nome = trim($_POST['nome'] ?? '');
$descricao = trim($_POST['descricao'] ?? '');
$ativo = isset($_POST['ativo']) ? 1 : 0;
$slug = generateSlug($nome);
if (empty($nome)) {
$error = "O campo Nome é obrigatório.";
// Repopulate form data on error
$macro_area = $_POST;
} else {
$stmt = $pdo->prepare('SELECT id FROM macro_areas WHERE (nome = ? OR slug = ?) AND id <> ?');
$stmt->execute([$nome, $slug, $id ?: 0]);
if ($stmt->fetch()) {
$error = "Já existe uma Macro Área com este nome.";
// Repopulate form data on error
$macro_area = $_POST;
} else {
if ($id) {
// Update
$stmt = $pdo->prepare('UPDATE macro_areas SET nome = ?, slug = ?, descricao = ?, ativo = ? WHERE id = ?');
$stmt->execute([$nome, $slug, $descricao, $ativo, $id]);
$redirect_id = $id;
} else {
// Create
$stmt = $pdo->prepare('INSERT INTO macro_areas (nome, slug, descricao, ativo, user_id) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([$nome, $slug, $descricao, $ativo, $_SESSION['user_id'] ?? 1]);
$redirect_id = $pdo->lastInsertId();
}
header("Location: /Backend/macro_area_form.php");
exit;
}
}
} elseif (isset($_GET['id'])) {
// Handle edit mode (fetch data)
$id = $_GET['id'];
$stmt = $pdo->prepare('SELECT * FROM macro_areas WHERE id = ?');
$stmt->execute([$id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
$macro_area = $data;
$page_title = 'Editar Macro Área';
}
}
include_once 'includes/header.php';
?>
<div class="container-fluid">
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0"><?php echo $page_title; ?></h1>
</div>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $error; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold" style="color: #005C53;">Detalhes da Macro Área</h6>
</div>
<div class="card-body">
<form id="macroAreaForm" method="POST" action="/Backend/macro_area_form.php<?php echo !empty($macro_area['id']) ? '?id='.$macro_area['id'] : ''; ?>">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($macro_area['id']); ?>">
<div class="mb-3">
<label for="macroAreaNome" class="form-label">Nome *</label>
<input type="text" class="form-control" id="macroAreaNome" name="nome" placeholder="Ex: Moradia, Alimentação, Saúde" value="<?php echo htmlspecialchars($macro_area['nome']); ?>" required>
</div>
<div class="mb-3">
<label for="macroAreaDescricao" class="form-label">Descrição</label>
<textarea class="form-control" id="macroAreaDescricao" name="descricao" rows="3" placeholder="Descrição opcional da macro área"><?php echo htmlspecialchars($macro_area['descricao']); ?></textarea>
</div>
<div class="mb-3 form-check form-switch">
<input type="checkbox" class="form-check-input" id="macroAreaAtivo" name="ativo" value="1" <?php echo !empty($macro_area['ativo']) && $macro_area['ativo'] ? 'checked' : ''; ?>>
<label class="form-check-label" for="macroAreaAtivo">Ativo</label>
</div>
<div class="mt-4">
<a href="/Backend/macro_areas.php" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
</div>
</div>
<?php include_once 'includes/footer.php'; ?>

191
Backend/macro_areas.php Normal file
View File

@ -0,0 +1,191 @@
<?php
require_once 'includes/session.php';
require_once 'db/config.php';
include_once 'includes/header.php';
$pdo = db();
$stmt = $pdo->query('SELECT * FROM macro_areas ORDER BY nome ASC');
$macro_areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0">Macro Áreas</h1>
<div>
<button id="printButton" class="btn btn-secondary btn-icon-split">
<span class="icon text-white-50"><i data-lucide="file-text" style="color: #FFFFFF;"></i></span>
<span class="text">Imprimir Lista</span>
</button>
<a href="/Backend/macro_area_form.php" class="btn btn-primary btn-icon-split">
<span class="icon text-white-50"><i data-lucide="plus" style="color: #FFFFFF;"></i></span>
<span class="text">Nova Macro Área</span>
</a>
</div>
</div>
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php echo $_SESSION['success_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $_SESSION['error_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold" style="color: #005C53;">
<i data-lucide="layers" class="me-2"></i>Lista de Macro Áreas
</h6>
<div class="input-group" style="width: 250px;">
<span class="input-group-text text-slate-400"><i data-lucide="search"></i></span>
<input type="text" class="form-control" id="searchInput" placeholder="Buscar...">
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-sm" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Nome</th>
<th>Descrição</th>
<th>Status</th>
<th style="width: 85px;">Ações</th>
</tr>
</thead>
<tbody id="tableBody">
<?php foreach ($macro_areas as $area):
$badgeClass = $area['ativo'] ? 'badge-status-ativo' : 'badge-status-arquivado';
$statusText = $area['ativo'] ? 'Ativo' : 'Arquivado';
?>
<tr>
<td><?php echo htmlspecialchars($area['nome']); ?></td>
<td><?php echo htmlspecialchars($area['descricao']); ?></td>
<td>
<span class="badge <?php echo $badgeClass; ?>"><?php echo $statusText; ?></span>
</td>
<td>
<div class="d-flex">
<a href="/Backend/macro_area_form.php?id=<?php echo $area['id']; ?>" class="btn btn-sm">
<i data-lucide="edit" style="color: #4C5958; width: 18px; height: 18px;"></i>
</a>
<a href="/Backend/delete_macro_area.php?id=<?php echo $area['id']; ?>" class="btn btn-sm">
<i data-lucide="trash-2" style="color: #4C5958; width: 18px; height: 18px;"></i>
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($macro_areas)): ?>
<tr>
<td colspan="4" class="text-center">Nenhuma macro área encontrada.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php include_once 'includes/footer.php'; ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Search functionality
const searchInput = document.getElementById('searchInput');
const tableBody = document.getElementById('tableBody');
const tableRows = tableBody.getElementsByTagName('tr');
searchInput.addEventListener('keyup', function() {
const searchTerm = searchInput.value.toLowerCase();
for (let i = 0; i < tableRows.length; i++) {
const row = tableRows[i];
const cells = row.getElementsByTagName('td');
let match = false;
// Start search from the first cell, and not the last one (actions)
for (let j = 0; j < cells.length - 1; j++) {
if (cells[j]) {
if (cells[j].textContent.toLowerCase().indexOf(searchTerm) > -1) {
match = true;
break;
}
}
}
if (match) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
});
// Print functionality
const printButton = document.getElementById('printButton');
printButton.addEventListener('click', function() {
fetch('/Backend/print_macro_areas.php')
.then(response => response.text())
.then(html => {
const printWindow = window.open('', '_blank');
printWindow.document.write(html);
printWindow.document.close();
printWindow.focus(); // Required for some browsers
// Use a small timeout to ensure content is loaded before printing
setTimeout(() => {
printWindow.print();
}, 250);
printWindow.addEventListener('afterprint', () => {
printWindow.close();
});
})
.catch(error => console.error('Error fetching print content:', error));
});
});
</script>
<style>
#printButton {
background-color: #005C53;
border-color: #005C53;
}
#printButton:hover {
background-color: #004c43;
border-color: #004c43;
}
.input-group-text {
background-color: #fff;
border-right: 0;
}
#searchInput {
border-left: 0;
}
.btn-sm i[data-lucide] {
width: 18px;
height: 18px;
}
/* Custom styles for status badges */
.badge-status-ativo {
background-color: #d1e7dd; /* Light green */
color: #0f5132; /* Dark green */
border: 1px solid #badbcc;
}
.badge-status-arquivado {
background-color: #f8d7da; /* Light red */
color: #842029; /* Dark red */
border: 1px solid #f5c2c7;
}
</style>

View File

@ -0,0 +1,113 @@
<?php
require_once 'includes/session.php';
require_once 'db/config.php';
$pdo = db();
$stmt = $pdo->query('SELECT * FROM macro_areas ORDER BY nome ASC');
$macro_areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
$total = count($macro_areas);
$active = 0;
$archived = 0;
foreach ($macro_areas as $area) {
if ($area['ativo']) {
$active++;
} else {
$archived++;
}
}
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Lista de Macro Áreas</title>
<style>
body {
font-family: sans-serif;
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
h1 {
font-size: 24px;
}
.header {
margin-bottom: 20px;
font-size: 12px;
color: #666;
}
.summary {
margin-top: 20px;
font-size: 14px;
}
@page {
margin: 10mm;
}
@media print {
body {
margin: 0;
}
.no-print {
display: none;
}
}
</style>
</head>
<body>
<div style="text-align: center; margin-bottom: 20px;">
<img src="assets/pasted-20251029-150345-2b427067.png" alt="Logotipo Galilei" style="max-width: 150px; height: auto;">
</div>
<h1>Lista de Macro Áreas</h1>
<div class="header">
Gerado em: <?php echo date('d/m/Y H:i:s'); ?>
</div>
<table>
<thead>
<tr>
<th>#</th>
<th>Nome</th>
<th>Descrição</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php $count = 1; foreach ($macro_areas as $area): ?>
<tr>
<td><?php echo $count++; ?></td>
<td><strong><?php echo htmlspecialchars($area['nome']); ?></strong></td>
<td><?php echo htmlspecialchars($area['descricao']); ?></td>
<td><?php echo $area['ativo'] ? 'Ativo' : 'Arquivado'; ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($macro_areas)): ?>
<tr>
<td colspan="4" style="text-align: center;">Nenhuma macro área encontrada.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<div class="summary">
<p>
<strong>Total de Macro Áreas:</strong> <?php echo $total; ?> |
<strong>Ativas:</strong> <?php echo $active; ?> |
<strong>Arquivadas:</strong> <?php echo $archived; ?>
</p>
</div>
</body>
</html>

136
Backend/profile.php Normal file
View File

@ -0,0 +1,136 @@
<?php
require_once __DIR__ . '/db/config.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$user_id = $_SESSION['user_id'];
$pdo = db();
$update_name_error = '';
$update_name_success = '';
$update_password_error = '';
$update_password_success = '';
// Obter dados do usuário
$stmt = $pdo->prepare("SELECT name, email FROM users WHERE id = :id");
$stmt->execute(['id' => $user_id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Lógica para lidar com o POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Atualizar Nome
if (isset($_POST['update_name'])) {
$name = $_POST['name'] ?? '';
if (empty($name)) {
$update_name_error = 'O nome não pode estar em branco.';
} else {
try {
$stmt = $pdo->prepare("UPDATE users SET name = :name WHERE id = :id");
$stmt->execute(['name' => $name, 'id' => $user_id]);
$user['name'] = $name; // Atualizar o nome na página
$update_name_success = 'Nome atualizado com sucesso!';
} catch (PDOException $e) {
$update_name_error = 'Erro ao atualizar o nome.';
}
}
}
// Atualizar Senha
if (isset($_POST['update_password'])) {
$current_password = $_POST['current_password'] ?? '';
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (empty($current_password) || empty($new_password) || empty($confirm_password)) {
$update_password_error = 'Todos os campos de senha são obrigatórios.';
} elseif ($new_password !== $confirm_password) {
$update_password_error = 'A nova senha e a confirmação não correspondem.';
} else {
try {
// Verificar senha atual
$stmt = $pdo->prepare("SELECT password_hash FROM users WHERE id = :id");
$stmt->execute(['id' => $user_id]);
$hash = $stmt->fetchColumn();
if (password_verify($current_password, $hash)) {
// Se a senha atual estiver correta, criar novo hash e atualizar
$new_hash = password_hash($new_password, PASSWORD_DEFAULT);
$stmt_update = $pdo->prepare("UPDATE users SET password_hash = :hash WHERE id = :id");
$stmt_update->execute(['hash' => $new_hash, 'id' => $user_id]);
$update_password_success = 'Senha atualizada com sucesso!';
} else {
$update_password_error = 'A senha atual está incorreta.';
}
} catch (PDOException $e) {
$update_password_error = 'Erro ao atualizar a senha.';
}
}
}
}
include __DIR__ . '/includes/header.php';
?>
<div class="container mt-4">
<h1 class="mb-4">Meu Perfil</h1>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">Atualizar Informações</div>
<div class="card-body">
<?php if ($update_name_success): ?><div class="alert alert-success"><?php echo $update_name_success; ?></div><?php endif; ?>
<?php if ($update_name_error): ?><div class="alert alert-danger"><?php echo $update_name_error; ?></div><?php endif; ?>
<form method="POST" action="profile.php">
<input type="hidden" name="update_name" value="1">
<div class="mb-3">
<label for="name" class="form-label">Nome</label>
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($user['name']); ?>" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">E-mail</label>
<input type="email" class="form-control" id="email" value="<?php echo htmlspecialchars($user['email']); ?>" disabled>
<div class="form-text">O e-mail não pode ser alterado.</div>
</div>
<button type="submit" class="btn btn-primary">Salvar Nome</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Alterar Senha</div>
<div class="card-body">
<?php if ($update_password_success): ?><div class="alert alert-success"><?php echo $update_password_success; ?></div><?php endif; ?>
<?php if ($update_password_error): ?><div class="alert alert-danger"><?php echo $update_password_error; ?></div><?php endif; ?>
<form method="POST" action="profile.php">
<input type="hidden" name="update_password" value="1">
<div class="mb-3">
<label for="current_password" class="form-label">Senha Atual</label>
<input type="password" class="form-control" id="current_password" name="current_password" required>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">Nova Senha</label>
<input type="password" class="form-control" id="new_password" name="new_password" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirmar Nova Senha</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary">Alterar Senha</button>
</form>
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

172
index.php
View File

@ -1,150 +1,30 @@
<?php <?php
declare(strict_types=1); require_once 'Backend/includes/session.php';
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; // Verifica se o usuário está logado, redireciona para o login se não estiver
$now = date('Y-m-d H:i:s'); if (!isset($_SESSION['user_id'])) {
header("Location: Backend/login.php");
exit();
}
require_once 'Backend/db/config.php';
require_once 'Backend/includes/header.php';
?> ?>
<!doctype html>
<html lang="en"> <div class="container-fluid">
<head> <div class="row">
<meta charset="utf-8" /> <div class="col-md-12">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <div class="card">
<title>New Style</title> <div class="card-header">
<?php <h4 class="card-title">Dashboard</h4>
// Read project preview data from environment </div>
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <div class="card-body">
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; <p>Bem-vindo ao seu painel de controle de despesas!</p>
?> <p>Use os links na barra de navegação para gerenciar suas despesas e orçamentos.</p>
<?php if ($projectDescription): ?> </div>
<!-- Meta description --> </div>
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> </div>
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<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=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> </div>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <?php require_once 'Backend/includes/footer.php'; ?>
</footer>
</body>
</html>