versao 20

This commit is contained in:
Flatlogic Bot 2025-10-29 19:43:33 +00:00
parent dbdbcd28a8
commit 1177896daf
5 changed files with 608 additions and 1 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

@ -455,3 +455,124 @@ body {
.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;
}

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>

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

@ -71,7 +71,7 @@ $unclassifiedCount = 5; // Example value
</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="#"><span class="menu-item-text">Categorias</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>