diff --git a/Backend/budgets.php b/Backend/budgets.php
index cef7f5f..afce185 100644
--- a/Backend/budgets.php
+++ b/Backend/budgets.php
@@ -11,6 +11,7 @@ if (!isset($_SESSION['user_id'])) {
exit;
}
+$client_id = $_SESSION['client_id'];
$user_id = $_SESSION['user_id'];
$pdo = db();
$error_message = '';
@@ -26,13 +27,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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";
+ $sql = "INSERT INTO budgets (client_id, user_id, category, amount, budget_month) VALUES (:client_id, :user_id, :category, :amount, :budget_month)
+ ON DUPLICATE KEY UPDATE amount = VALUES(amount)";
$stmt = $pdo->prepare($sql);
foreach ($budgets as $category => $amount) {
if (is_numeric($amount) && $amount >= 0) {
$stmt->execute([
+ 'client_id' => $client_id,
'user_id' => $user_id,
'category' => $category,
'amount' => $amount,
@@ -52,8 +54,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 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]);
+ $stmt = $pdo->prepare("SELECT category, amount FROM budgets WHERE client_id = :client_id AND budget_month = :budget_month");
+ $stmt->execute(['client_id' => $client_id, 'budget_month' => $budget_month_date]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($results as $row) {
$existing_budgets[$row['category']] = $row['amount'];
@@ -63,8 +65,10 @@ try {
}
-// Categorias fixas
-$categories = ['Alimentação', 'Transporte', 'Moradia', 'Lazer', 'Saúde', 'Outros'];
+// Obter todas as categorias (macro áreas ativas) para os dropdowns
+$stmt_categories = $pdo->prepare("SELECT nome, slug FROM macro_areas WHERE client_id = :client_id AND ativo = 1 ORDER BY nome ASC");
+$stmt_categories->execute(['client_id' => $client_id]);
+$categories = $stmt_categories->fetchAll(PDO::FETCH_ASSOC);
include __DIR__ . '/includes/header.php';
?>
@@ -101,17 +105,20 @@ include __DIR__ . '/includes/header.php';
Orçamentos para
-
+
diff --git a/Backend/categories.php b/Backend/categories.php
new file mode 100644
index 0000000..4e7a031
--- /dev/null
+++ b/Backend/categories.php
@@ -0,0 +1,307 @@
+prepare("UPDATE categories SET nome = :nome, macro_area_id = :macro_area_id WHERE id = :id AND client_id = :client_id");
+ $stmt->execute(['nome' => $nome, 'macro_area_id' => $macro_area_id, 'id' => $id, 'client_id' => $client_id]);
+ $success = "Categoria atualizada com sucesso!";
+ } else {
+ // Create
+ $stmt = $pdo->prepare("INSERT INTO categories (nome, macro_area_id, client_id) VALUES (:nome, :macro_area_id, :client_id)");
+ $stmt->execute(['nome' => $nome, 'macro_area_id' => $macro_area_id, 'client_id' => $client_id]);
+ $success = "Categoria criada com sucesso!";
+ }
+ }
+ } elseif ($action === 'toggle_archive') {
+ $id = $_POST['id'] ?? null;
+ if ($id) {
+ $stmt = $pdo->prepare("UPDATE categories SET is_archived = 1 - is_archived WHERE id = :id AND client_id = :client_id");
+ $stmt->execute(['id' => $id, 'client_id' => $client_id]);
+ $success = "Status da categoria alterado com sucesso!";
+ }
+ } elseif ($action === 'delete') {
+ $id = $_POST['id'] ?? null;
+ if ($id) {
+ // Check for related expenses
+ $stmt = $pdo->prepare("SELECT COUNT(*) FROM expenses WHERE category_id = :category_id AND client_id = :client_id");
+ $stmt->execute(['category_id' => $id, 'client_id' => $client_id]);
+ if ($stmt->fetchColumn() > 0) {
+ $errors[] = "Não é possível excluir a categoria, pois existem despesas associadas a ela.";
+ } else {
+ $stmt = $pdo->prepare("DELETE FROM categories WHERE id = :id AND client_id = :client_id");
+ $stmt->execute(['id' => $id, 'client_id' => $client_id]);
+ $success = "Categoria excluída com sucesso!";
+ }
+ }
+ }
+ } catch (PDOException $e) {
+ if ($e->getCode() == '23000') { // Integrity constraint violation
+ $errors[] = "Erro: Já existe uma categoria com este nome para a macro área selecionada.";
+ } else {
+ $errors[] = "Ocorreu um erro no banco de dados: " . $e->getMessage();
+ }
+ }
+}
+
+// Fetch data for display
+$macro_areas = $pdo->prepare("SELECT id, nome FROM macro_areas WHERE client_id = :client_id ORDER BY nome");
+$macro_areas->execute(['client_id' => $client_id]);
+$macro_areas_list = $macro_areas->fetchAll();
+
+$stmt = $pdo->prepare("
+ SELECT c.id, c.nome, c.is_archived, c.macro_area_id, m.nome as macro_area_nome
+ FROM categories c
+ JOIN macro_areas m ON c.macro_area_id = m.id
+ WHERE c.client_id = :client_id
+ ORDER BY m.nome, c.nome
+");
+$stmt->execute(['client_id' => $client_id]);
+$categories = $stmt->fetchAll();
+
+$page_title = "Categorias";
+include 'includes/header.php';
+?>
+
+
+
+
+
+
+
+ Categorias
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Nome da Categoria |
+ Macro Área |
+ Status |
+ Ações |
+
+
+
+
+
+ | Nenhuma categoria encontrada. |
+
+
+
+
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Backend/db/fix_schema.php b/Backend/db/fix_schema.php
new file mode 100644
index 0000000..8eb0e63
--- /dev/null
+++ b/Backend/db/fix_schema.php
@@ -0,0 +1,60 @@
+query("SHOW COLUMNS FROM `{$table_name}` LIKE 'client_id'");
+ if ($res->rowCount() == 0) {
+ echo "Adding client_id to {$table_name}...\\n";
+ $pdo->exec("ALTER TABLE `{$table_name}` ADD COLUMN `client_id` INT NOT NULL DEFAULT 1");
+ } else {
+ echo "client_id already exists in {$table_name}. Setting to default...\\n";
+ // This handles cases where the column was added but not populated correctly
+ $pdo->exec("UPDATE `{$table_name}` SET `client_id` = 1 WHERE `client_id` = 0 OR `client_id` IS NULL");
+ }
+
+ // Check and add foreign key
+ $res = $pdo->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$table_name}' AND COLUMN_NAME = 'client_id' AND REFERENCED_TABLE_NAME = 'clients'");
+ if ($res->rowCount() == 0) {
+ echo "Adding foreign key to {$table_name}...\\n";
+ try {
+ $pdo->exec("ALTER TABLE `{$table_name}` ADD CONSTRAINT `{$fk_name}` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE");
+ } catch (PDOException $e) {
+ // If the constraint somehow exists with a different name, this might fail.
+ echo "Could not add foreign key to {$table_name}. It might already exist with a different name. Error: " . $e->getMessage() . "\\n";
+ }
+ } else {
+ echo "Foreign key on {$table_name}.client_id already exists.\\n";
+ }
+}
+
+try {
+ $pdo = db();
+
+ fix_table($pdo, 'users', 'fk_users_client_id');
+ fix_table($pdo, 'macro_areas', 'fk_macro_areas_client_id');
+ fix_table($pdo, 'budgets', 'fk_budgets_client_id');
+ fix_table($pdo, 'expenses', 'fk_expenses_client_id');
+
+ // Mark migrations as executed to prevent them from running again
+ $migrations_to_mark = [
+ '006_add_client_id_to_users.sql',
+ '007_add_client_id_to_macro_areas.sql',
+ '008_add_client_id_to_budgets.sql',
+ '009_add_client_id_to_expenses.sql'
+ ];
+
+ $stmt = $pdo->prepare("INSERT IGNORE INTO migrations (migration_name) VALUES (:migration_name)");
+ foreach ($migrations_to_mark as $migration_name) {
+ echo "Marking migration {$migration_name} as executed...\\n";
+ $stmt->execute(['migration_name' => $migration_name]);
+ }
+
+ echo "\\nSchema fix script finished.\\n";
+
+} catch (PDOException $e) {
+ die("Error fixing schema: " . $e->getMessage());
+}
+
diff --git a/Backend/db/migrations/005_create_clients_table.sql b/Backend/db/migrations/005_create_clients_table.sql
new file mode 100644
index 0000000..06231f5
--- /dev/null
+++ b/Backend/db/migrations/005_create_clients_table.sql
@@ -0,0 +1,5 @@
+CREATE TABLE IF NOT EXISTS `clients` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `name` VARCHAR(255) NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/Backend/db/migrations/005a_insert_default_client.sql b/Backend/db/migrations/005a_insert_default_client.sql
new file mode 100644
index 0000000..3b284e1
--- /dev/null
+++ b/Backend/db/migrations/005a_insert_default_client.sql
@@ -0,0 +1 @@
+INSERT INTO `clients` (`id`, `name`) VALUES (1, 'Default Client') ON DUPLICATE KEY UPDATE `name` = `name`;
\ No newline at end of file
diff --git a/Backend/db/migrations/006_add_client_id_to_users.sql b/Backend/db/migrations/006_add_client_id_to_users.sql
new file mode 100644
index 0000000..83c8c45
--- /dev/null
+++ b/Backend/db/migrations/006_add_client_id_to_users.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `users`
+ADD COLUMN `client_id` INT NOT NULL DEFAULT 1 AFTER `id`,
+ADD CONSTRAINT `fk_users_client_id` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE;
diff --git a/Backend/db/migrations/007_add_client_id_to_macro_areas.sql b/Backend/db/migrations/007_add_client_id_to_macro_areas.sql
new file mode 100644
index 0000000..6537eb6
--- /dev/null
+++ b/Backend/db/migrations/007_add_client_id_to_macro_areas.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `macro_areas`
+ADD COLUMN `client_id` INT NOT NULL DEFAULT 1 AFTER `id`,
+ADD CONSTRAINT `fk_macro_areas_client_id` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE;
diff --git a/Backend/db/migrations/008_add_client_id_to_budgets.sql b/Backend/db/migrations/008_add_client_id_to_budgets.sql
new file mode 100644
index 0000000..007b17b
--- /dev/null
+++ b/Backend/db/migrations/008_add_client_id_to_budgets.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `budgets`
+ADD COLUMN `client_id` INT NOT NULL DEFAULT 1 AFTER `id`,
+ADD CONSTRAINT `fk_budgets_client_id` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE;
diff --git a/Backend/db/migrations/009_add_client_id_to_expenses.sql b/Backend/db/migrations/009_add_client_id_to_expenses.sql
new file mode 100644
index 0000000..2e7cc4e
--- /dev/null
+++ b/Backend/db/migrations/009_add_client_id_to_expenses.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `expenses`
+ADD COLUMN `client_id` INT NOT NULL DEFAULT 1 AFTER `id`,
+ADD CONSTRAINT `fk_expenses_client_id` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE;
diff --git a/Backend/db/migrations/010_fix_budgets_unique_key.sql b/Backend/db/migrations/010_fix_budgets_unique_key.sql
new file mode 100644
index 0000000..fff73f6
--- /dev/null
+++ b/Backend/db/migrations/010_fix_budgets_unique_key.sql
@@ -0,0 +1,10 @@
+-- Drop the foreign key constraint first
+ALTER TABLE `budgets` DROP FOREIGN KEY `budgets_ibfk_1`;
+
+-- Now, drop the old unique key and add the new one
+ALTER TABLE `budgets`
+DROP KEY `user_category_month`,
+ADD UNIQUE KEY `client_month_category` (`client_id`, `budget_month`, `category`);
+
+-- Add the foreign key back
+ALTER TABLE `budgets` ADD CONSTRAINT `fk_budgets_user_id` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE;
\ No newline at end of file
diff --git a/Backend/db/migrations/011_create_categories_table.sql b/Backend/db/migrations/011_create_categories_table.sql
new file mode 100644
index 0000000..ea13a8f
--- /dev/null
+++ b/Backend/db/migrations/011_create_categories_table.sql
@@ -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,
+ `client_id` INT NOT NULL,
+ `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 (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE,
+ UNIQUE KEY `client_macro_area_nome` (`client_id`, `macro_area_id`, `nome`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/Backend/db/migrations/012_add_archived_to_categories.sql b/Backend/db/migrations/012_add_archived_to_categories.sql
new file mode 100644
index 0000000..891f82f
--- /dev/null
+++ b/Backend/db/migrations/012_add_archived_to_categories.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `categories`
+ADD COLUMN `is_archived` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0 = active, 1 = archived' AFTER `client_id`;
\ No newline at end of file
diff --git a/Backend/delete_expense.php b/Backend/delete_expense.php
index 606f266..fb86bad 100644
--- a/Backend/delete_expense.php
+++ b/Backend/delete_expense.php
@@ -12,7 +12,7 @@ if (!isset($_SESSION['user_id'])) {
}
$expense_id = $_GET['id'] ?? null;
-$user_id = $_SESSION['user_id'];
+$client_id = $_SESSION['client_id'];
if (!$expense_id) {
// Se não houver ID, redireciona de volta
@@ -23,11 +23,11 @@ if (!$expense_id) {
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";
+ $sql = "DELETE FROM expenses WHERE id = :id AND client_id = :client_id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'id' => $expense_id,
- 'user_id' => $user_id
+ 'client_id' => $client_id
]);
// Opcional: verificar se alguma linha foi afetada para dar um feedback mais preciso
diff --git a/Backend/delete_macro_area.php b/Backend/delete_macro_area.php
index 7aef25b..65956ac 100644
--- a/Backend/delete_macro_area.php
+++ b/Backend/delete_macro_area.php
@@ -5,11 +5,12 @@ require_once 'db/config.php';
// Check if ID is provided
if (isset($_GET['id'])) {
$id = $_GET['id'];
+ $client_id = $_SESSION['client_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]);
+ $stmt = $pdo->prepare("SELECT slug FROM macro_areas WHERE id = ? AND client_id = ?");
+ $stmt->execute([$id, $client_id]);
$macro_area = $stmt->fetch();
// If the macro area exists, proceed with checks and deletion
@@ -17,8 +18,8 @@ if (isset($_GET['id'])) {
$slug = $macro_area['slug'];
// Check for dependent expenses
- $stmt = $pdo->prepare("SELECT COUNT(*) FROM expenses WHERE category = ?");
- $stmt->execute([$slug]);
+ $stmt = $pdo->prepare("SELECT COUNT(*) FROM expenses WHERE category = ? AND client_id = ?");
+ $stmt->execute([$slug, $client_id]);
$expense_count = $stmt->fetchColumn();
if ($expense_count > 0) {
@@ -26,8 +27,8 @@ if (isset($_GET['id'])) {
$_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]);
+ $stmt = $pdo->prepare("DELETE FROM macro_areas WHERE id = ? AND client_id = ?");
+ $stmt->execute([$id, $client_id]);
$_SESSION['success_message'] = "Macro área excluída com sucesso.";
}
}
diff --git a/Backend/edit_expense.php b/Backend/edit_expense.php
index 8870636..968d4bb 100644
--- a/Backend/edit_expense.php
+++ b/Backend/edit_expense.php
@@ -12,7 +12,7 @@ if (!isset($_SESSION['user_id'])) {
}
$expense_id = $_GET['id'] ?? null;
-$user_id = $_SESSION['user_id'];
+$client_id = $_SESSION['client_id'];
$error_message = '';
$expense = null;
@@ -25,8 +25,8 @@ $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]);
+ $stmt = $pdo->prepare("SELECT * FROM expenses WHERE id = :id AND client_id = :client_id");
+ $stmt->execute(['id' => $expense_id, 'client_id' => $client_id]);
$expense = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$expense) {
@@ -51,7 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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";
+ $sql = "UPDATE expenses SET description = :description, amount = :amount, category = :category, expense_date = :expense_date WHERE id = :id AND client_id = :client_id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'description' => $description,
@@ -59,7 +59,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'category' => $category,
'expense_date' => $expense_date,
'id' => $expense_id,
- 'user_id' => $user_id
+ 'client_id' => $client_id
]);
$_SESSION['success_message'] = 'Despesa atualizada com sucesso!';
@@ -100,13 +100,12 @@ include __DIR__ . '/includes/header.php';
+ $selected = ($expense['category'] === $cat['slug']) ? 'selected' : '';
+ echo "