versao 20
This commit is contained in:
parent
dbdbcd28a8
commit
1177896daf
159
Backend/api/category_handler.php
Normal file
159
Backend/api/category_handler.php
Normal 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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -455,3 +455,124 @@ body {
|
|||||||
.form-switch .form-check-label {
|
.form-switch .form-check-label {
|
||||||
padding-top: 2px;
|
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
316
Backend/categories.php
Normal 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 = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'/': '/'
|
||||||
|
};
|
||||||
|
return chars[tag] || tag;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetchCategories();
|
||||||
|
loadMacroAreas();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
11
Backend/db/migrations/005_create_categories_table.sql
Normal file
11
Backend/db/migrations/005_create_categories_table.sql
Normal 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;
|
||||||
@ -71,7 +71,7 @@ $unclassifiedCount = 5; // Example value
|
|||||||
</a>
|
</a>
|
||||||
<ul class="collapse show list-unstyled sub-sub-menu" id="planoContasSubmenu">
|
<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/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">Centro de Custo</span></a></li>
|
||||||
<li><a href="#"><span class="menu-item-text">Alocação do Plano</span></a></li>
|
<li><a href="#"><span class="menu-item-text">Alocação do Plano</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user