316 lines
14 KiB
PHP
316 lines
14 KiB
PHP
<?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 = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'/': '/'
|
|
};
|
|
return chars[tag] || tag;
|
|
});
|
|
}
|
|
|
|
// Initial load
|
|
fetchCategories();
|
|
loadMacroAreas();
|
|
});
|
|
</script>
|