backend-01

commit 8f08828e53d9954eca25b6ab14570082287ae705

Author: Pedro Lucas Porcellis <porcellis@eletrotupi.com>

income: add income models, DAOs, and controllers

 app/controllers/IncomesController.php | 73 ++++++++++++++++++++
 app/daos/IncomeDAO.php | 102 +++++++++++++++++++++++++++++
 app/models/Income.php | 93 ++++++++++++++++++++++++++
 app/views/income_create.php | 52 ++++++++++++++
 app/views/income_edit.php | 52 ++++++++++++++
 app/views/incomes.php | 53 +++++++++++++++
 app/views/layout.php | 1 
 config/routes.php | 25 +++++++
 schema.sql | 13 +++


diff --git a/app/controllers/IncomesController.php b/app/controllers/IncomesController.php
new file mode 100644
index 0000000000000000000000000000000000000000..37f44377b798f01accd887d44632f479790cf3c6
--- /dev/null
+++ b/app/controllers/IncomesController.php
@@ -0,0 +1,73 @@
+<?php
+
+require_once __DIR__ . '/../daos/IncomeDAO.php';
+require_once __DIR__ . '/../models/Income.php';
+
+class IncomesController {
+  private $incomeDAO;
+
+  public function __construct() {
+    $this->incomeDAO = new IncomeDAO();
+  }
+
+  public function index() {
+    $incomes = $this->incomeDAO->getIncomesByUser($_SESSION['user_id']);
+
+    return Template::render('incomes', ['incomes' => $incomes]);
+  }
+
+  public function create() {
+    return Template::render('income_create');
+  }
+
+  public function store() {
+    $data = $_POST;
+
+    $income = new Income(
+      null,
+      $data['title'],
+      $data['amount'],
+      $data['income_type'],
+      $data['recurrence_period'],
+      $data['date'],
+      $_SESSION['user_id'],
+    );
+
+    $this->incomeDAO->create($income);
+
+    header('Location: /incomes');
+    exit;
+  }
+
+  public function edit($id) {
+    $income = $this->incomeDAO->getIncomeById($id);
+
+    return Template::render('income_edit', ['income' => $income]);
+  }
+
+  public function update($id) {
+    $data = $_POST;
+
+    $income = new Income(
+      $id,
+      $data['title'],
+      $data['amount'],
+      $data['income_type'],
+      $data['recurrence_period'],
+      $data['date'],
+      $_SESSION['user_id'],
+    );
+
+    $this->incomeDAO->update($income);
+
+    header('Location: /incomes');
+    exit;
+  }
+
+  public function destroy($id) {
+    $this->incomeDAO->destroy($id, $_SESSION['user_id']);
+
+    header('Location: /incomes');
+    exit;
+  }
+}




diff --git a/app/daos/IncomeDAO.php b/app/daos/IncomeDAO.php
new file mode 100644
index 0000000000000000000000000000000000000000..f564e36b4781c53a5673abb45ec868278213b3a1
--- /dev/null
+++ b/app/daos/IncomeDAO.php
@@ -0,0 +1,102 @@
+<?php
+
+require_once __DIR__ . '/../../config/database.php';
+require_once __DIR__ . '/../models/Income.php';
+
+class IncomeDAO {
+  private $db;
+
+  public function __construct() {
+    $this->db = getDatabaseConnection();
+  }
+
+  public function create($income) {
+      $sql = 'INSERT INTO incomes (user_id, title, amount, income_type, recurrence_period, date)
+              VALUES (:user_id, :title, :amount, :income_type, :recurrence_period, :date)';
+
+      $stmt = $this->db->prepare($sql);
+      $stmt->bindValue(':user_id', $income->getUserId());
+      $stmt->bindValue(':title', $income->getTitle());
+      $stmt->bindValue(':amount', $income->getAmount());
+      $stmt->bindValue(':income_type', $income->getIncomeType());
+      $stmt->bindValue(':recurrence_period', $income->getRecurrencePeriod());
+      $stmt->bindValue(':date', $income->getDate());
+
+      return $stmt->execute();
+
+      $incomeId = $this->db->lastInsertId();
+      $income->setId($incomeId);
+
+      return $income;
+  }
+
+  public function update($income) {
+      $sql = 'UPDATE incomes SET title = :title, amount = :amount, income_type = :income_type,
+              recurrence_period = :recurrence_period, date = :date WHERE id = :id AND user_id = :user_id';
+      $stmt = $this->db->prepare($sql);
+
+      $stmt->bindValue(':title', $income->getTitle());
+      $stmt->bindValue(':amount', $income->getAmount());
+      $stmt->bindValue(':income_type', $income->getIncomeType());
+      $stmt->bindValue(':recurrence_period', $income->getRecurrencePeriod());
+      $stmt->bindValue(':date', $income->getDate());
+      $stmt->bindValue(':id', $income->getId());
+      $stmt->bindValue(':user_id', $income->getUserId());
+
+      return $stmt->execute();
+  }
+
+  public function getIncomesByUser($user_id) {
+      $sql = 'SELECT * FROM incomes WHERE user_id = :user_id';
+      $stmt = $this->db->prepare($sql);
+
+      $stmt->bindParam(':user_id', $user_id);
+      $stmt->execute();
+
+      $incomes = [];
+
+      while ($incomesData = $stmt->fetch(PDO::FETCH_ASSOC)) {
+          $incomes[] = new Income(
+              $incomesData['id'],
+              $incomesData['title'],
+              $incomesData['amount'],
+              $incomesData['income_type'],
+              $incomesData['recurrence_period'],
+              $incomesData['date'],
+              $incomesData['user_id']
+          );
+      }
+
+      return $incomes;
+  }
+
+  public function getIncomeById($id) {
+      $sql = 'SELECT * FROM incomes WHERE id = :id';
+      $stmt = $this->db->prepare($sql);
+
+      $stmt->bindParam(':id', $id);
+      $stmt->execute();
+
+      $incomeData = $stmt->fetch(PDO::FETCH_ASSOC);
+
+      return new Income(
+          $incomeData['id'],
+          $incomeData['title'],
+          $incomeData['amount'],
+          $incomeData['income_type'],
+          $incomeData['recurrence_period'],
+          $incomeData['date'],
+          $incomeData['user_id']
+      );
+  }
+
+  public function destroy($id, $user_id) {
+      $sql = 'DELETE FROM incomes WHERE id = :id AND user_id = :user_id';
+      $stmt = $this->db->prepare($sql);
+
+      $stmt->bindParam(':id', $id);
+      $stmt->bindParam(':user_id', $user_id);
+
+      return $stmt->execute();
+  }
+}




diff --git a/app/models/Income.php b/app/models/Income.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff5063ffe8e1543361349557b23de5549f6e0ce4
--- /dev/null
+++ b/app/models/Income.php
@@ -0,0 +1,93 @@
+<?php
+
+class Income extends Base {
+  const INCOME_TYPE_ONE_OFF = 'one_off';
+  const INCOME_TYPE_REPEATABLE = 'repeatable';
+
+  const RECURRENCE_PERIOD_MONTHLY = 'monthly';
+  const RECURRENCE_PERIOD_WEEKLY = 'weekly';
+  const RECURRENCE_PERIOD_YEARLY = 'yearly';
+
+  private $title;
+  private $amount;
+  private $incomeType;
+  private $recurrencePeriod;
+  private $date;
+  private $userId;
+
+  public function __construct($id, $title, $amount, $incomeType, $recurrencePeriod, $date, $userId) {
+    $this->id = $id;
+    $this->title = $title;
+    $this->amount = $amount;
+    $this->incomeType = $incomeType;
+    $this->recurrencePeriod = $recurrencePeriod;
+    $this->date = $date;
+    $this->userId = $userId;
+  }
+
+  public function getTitle() {
+    return $this->title;
+  }
+
+  public function getAmount() {
+    return $this->amount;
+  }
+
+  public function getIncomeType() {
+    return $this->incomeType;
+  }
+
+  public function getRecurrencePeriod() {
+    return $this->recurrencePeriod;
+  }
+
+  public function getDate() {
+    return $this->date;
+  }
+
+  public function getUserId() {
+    return $this->userId;
+  }
+
+  public function setTitle($title) {
+    $this->title = $title;
+  }
+
+  public function setAmount($amount) {
+    $this->amount = $amount;
+  }
+
+  public function setIncomeType($incomeType) {
+    $this->incomeType = $incomeType;
+  }
+
+  public function setRecurrencePeriod($recurrencePeriod) {
+    $this->recurrencePeriod = $recurrencePeriod;
+  }
+
+  public function setDate($date) {
+    $this->date = $date;
+  }
+
+  public function setUserId($userId) {
+    $this->userId = $userId;
+  }
+
+  public function getIncomeTypeLabel() {
+    if ($this->incomeType === self::INCOME_TYPE_ONE_OFF) {
+      return 'Único';
+    } else {
+      return 'Recorrente';
+    }
+  }
+
+  public function getRecurrencePeriodLabel() {
+    if ($this->recurrencePeriod === self::RECURRENCE_PERIOD_MONTHLY) {
+      return 'Mensal';
+    } elseif ($this->recurrencePeriod === self::RECURRENCE_PERIOD_WEEKLY) {
+      return 'Semanal';
+    } else {
+      return 'Anual';
+    }
+  }
+}




diff --git a/app/views/income_create.php b/app/views/income_create.php
new file mode 100644
index 0000000000000000000000000000000000000000..6725c1096c296525269541489a2a0f4b3182dc2a
--- /dev/null
+++ b/app/views/income_create.php
@@ -0,0 +1,52 @@
+<div class="container mx-auto mt-10">
+  <h1 class="text-2xl font-bold text-gray-700 mb-6">Adicionar Novo Rendimento</h1>
+
+  <form action="/incomes/create" method="POST" class="bg-white p-6 rounded shadow-md">
+    <div class="mb-4">
+      <label for="title" class="block text-sm font-medium text-gray-700">Título</label>
+      <input type="text" id="title" name="title" class="mt-1 block w-full border border-gray-300 rounded p-2" required />
+    </div>
+
+    <div class="mb-4">
+      <label for="amount" class="block text-sm font-medium text-gray-700">Valor</label>
+      <input type="number" id="amount" name="amount" class="mt-1 block w-full border border-gray-300 rounded p-2" required />
+    </div>
+
+    <div class="mb-4">
+      <label for="date" class="block text-sm font-medium text-gray-700">Data</label>
+      <input type="date" id="date" name="date" class="mt-1 block w-full border border-gray-300 rounded p-2" required />
+    </div>
+
+    <label class="mt-5" for="income_type">Tipo de Rendimento</label>
+    <select name="income_type" id="income_type" required>
+      <option value="one_off" <?php echo (isset($income) && $income['income_type'] === 'one_off') ? 'selected' : ''; ?>>Único</option>
+      <option value="repeatable" <?php echo (isset($income) && $income['income_type'] === 'repeatable') ? 'selected' : ''; ?>>Recorrente</option>
+    </select>
+
+    <div class="mt-5" id="recurrence_period_container" style="display: <?php echo (isset($income) && $income['income_type'] === 'repeatable') ? 'block' : 'none'; ?>;">
+      <label for="recurrence_period">Período de Recorrência</label>
+      <select name="recurrence_period" id="recurrence_period">
+        <option value="monthly" <?php echo (isset($income) && $income['recurrence_period'] === 'monthly') ? 'selected' : ''; ?>>Mensal</option>
+        <option value="weekly" <?php echo (isset($income) && $income['recurrence_period'] === 'weekly') ? 'selected' : ''; ?>>Semanal</option>
+        <option value="yearly" <?php echo (isset($income) && $income['recurrence_period'] === 'yearly') ? 'selected' : ''; ?>>Anual</option>
+      </select>
+    </div>
+
+    <div class="mt-5">
+      <button type="submit" class="bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">
+        Salvar Rendimento
+      </button>
+    </div>
+  </form>
+
+  <script>
+    document.getElementById('income_type').addEventListener('change', function() {
+      const recurrencePeriodContainer = document.getElementById('recurrence_period_container');
+      if (this.value === 'repeatable') {
+        recurrencePeriodContainer.style.display = 'block';
+      } else {
+        recurrencePeriodContainer.style.display = 'none';
+      }
+    });
+  </script>
+</div>




diff --git a/app/views/income_edit.php b/app/views/income_edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..921512dea8bcba52f8f70d89ba9162d38380f0a6
--- /dev/null
+++ b/app/views/income_edit.php
@@ -0,0 +1,52 @@
+<div class="container mx-auto mt-10">
+  <h1 class="text-2xl font-bold text-gray-700 mb-6">Editar Rendimento</h1>
+
+  <form action="/incomes/edit/<?= $income->getId() ?>" method="POST" class="bg-white p-6 rounded shadow-md">
+    <div class="mb-4">
+      <label for="title" class="block text-sm font-medium text-gray-700">Título</label>
+      <input type="text" id="title" name="title" value="<?= $income->getTitle() ?>" class="mt-1 block w-full border border-gray-300 rounded p-2" required />
+    </div>
+
+    <div class="mb-4">
+      <label for="amount" class="block text-sm font-medium text-gray-700">Valor</label>
+      <input type="number" id="amount" name="amount" value="<?= $income->getAmount() ?>" class="mt-1 block w-full border border-gray-300 rounded p-2" required />
+    </div>
+
+    <div class="mb-4">
+      <label for="date" class="block text-sm font-medium text-gray-700">Data</label>
+      <input type="date" id="date" name="date" value="<?= $income->getDate() ?>" class="mt-1 block w-full border border-gray-300 rounded p-2" required />
+    </div>
+
+    <label class="mt-5" for="income_type">Tipo de Rendimento</label>
+    <select name="income_type" id="income_type" required>
+      <option value="one_off" <?php echo (isset($income) && $income->getIncomeType() === 'one_off') ? 'selected' : ''; ?>>Único</option>
+      <option value="repeatable" <?php echo (isset($income) && $income->getIncomeType() === 'repeatable') ? 'selected' : ''; ?>>Recorrente</option>
+    </select>
+
+    <div class="mt-5" id="recurrence_period_container" style="display: <?php echo (isset($income) && $income->getIncomeType() === 'repeatable') ? 'block' : 'none'; ?>;">
+      <label for="recurrence_period">Período de Recorrência</label>
+      <select name="recurrence_period" id="recurrence_period">
+        <option value="monthly" <?php echo (isset($income) && $income->getRecurrencePeriod() === 'monthly') ? 'selected' : ''; ?>>Mensal</option>
+        <option value="weekly" <?php echo (isset($income) && $income->getRecurrencePeriod() === 'weekly') ? 'selected' : ''; ?>>Semanal</option>
+        <option value="yearly" <?php echo (isset($income) && $income->getRecurrencePeriod() === 'yearly') ? 'selected' : ''; ?>>Anual</option>
+      </select>
+    </div>
+
+    <div class="mt-5">
+      <button type="submit" class="bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">
+        Atualizar Rendimento
+      </button>
+    </div>
+  </form>
+
+  <script>
+    document.getElementById('income_type').addEventListener('change', function() {
+      const recurrencePeriodContainer = document.getElementById('recurrence_period_container');
+      if (this.value === 'repeatable') {
+        recurrencePeriodContainer.style.display = 'block';
+      } else {
+        recurrencePeriodContainer.style.display = 'none';
+      }
+    });
+  </script>
+</div>




diff --git a/app/views/incomes.php b/app/views/incomes.php
new file mode 100644
index 0000000000000000000000000000000000000000..57f78e3024830d8cf9c347e0db0217709a6385b0
--- /dev/null
+++ b/app/views/incomes.php
@@ -0,0 +1,53 @@
+<div class="container mx-auto mt-10">
+  <div class="flex justify-between items-center mb-6">
+    <h1 class="text-2xl font-bold text-gray-700">Rendimentos</h1>
+    <a href="/incomes/create" class="bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">
+      + Adicionar Novo Rendimento
+    </a>
+  </div>
+
+  <?php if (empty($incomes)) : ?>
+    <div class="text-center py-10 bg-white rounded-lg shadow-md">
+      <p class="text-xl text-gray-600">Nenhum rendimento encontrado.</p>
+      <p class="text-gray-500 mt-2">Crie um rendimento para melhor organizar as contas.</p>
+      <a href="/incomes/create" class="mt-4 inline-block bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">
+        + Adicionar Novo Rendimento
+      </a>
+    </div>
+  <?php else : ?>
+    <div class="bg-white shadow-md rounded-lg overflow-hidden">
+      <table class="min-w-full table-auto">
+        <thead>
+          <tr class="bg-gray-200">
+            <th class="py-3 px-6 text-left">Título</th>
+            <th class="py-3 px-6 text-left">Valor</th>
+            <th class="py-3 px-6 text-left">Tipo</th>
+            <th class="py-3 px-6 text-left">Recorrencia</th>
+            <th class="py-3 px-6 text-left">Data</th>
+            <th class="py-3 px-6 text-right">Ações</th>
+          </tr>
+        </thead>
+        <tbody>
+          <?php foreach ($incomes as $income) : ?>
+            <tr class="border-b">
+              <td class="py-3 px-6"><?= htmlspecialchars($income->getTitle()) ?></td>
+              <td class="py-3 px-6">R$ <?= number_format($income->getAmount(), 2, ',', '.') ?></td>
+              <td class="py-3 px-6"><?= htmlspecialchars($income->getIncomeTypeLabel()) ?></td>
+              <td class="py-3 px-6"><?= htmlspecialchars($income->getRecurrencePeriodLabel()) ?></td>
+              <td class="py-3 px-6"><?= date('d/m/Y', strtotime($income->getDate())) ?></td>
+              <td class="py-3 px-6 flex justify-end space-x-2">
+                <a href="/incomes/edit/<?= $income->getId() ?>" class="text-yellow-600 hover:text-yellow-700">
+                  Editar
+                </a>
+
+                <a href="/incomes/delete/<?= $income->getId() ?>" class="text-red-600 hover:text-red-700">
+                  Apagar
+                </a>
+              </td>
+            </tr>
+          <?php endforeach; ?>
+        </tbody>
+      </table>
+    </div>
+  <?php endif; ?>
+</div>




diff --git a/app/views/layout.php b/app/views/layout.php
index 6db2a2ea2eeaef7a6fec31ab366b7a9109ca8f27..1788340999037f2ef0f5e5a5672ac804c4c14291 100644
--- a/app/views/layout.php
+++ b/app/views/layout.php
@@ -17,6 +17,7 @@     
<? if (isset($_SESSION['user_id'])): ?> <nav class="max-w-4xl mx-auto px-4 py-2"> + <a href="/incomes" class="text-blue-700 mr-5">Rendimentos</a> <a href="/tags" class="text-blue-700 mr-5">Tags</a> <a href="/bills" class="text-blue-700 mr-5">Gastos</a> <a href="/logout" class="text-blue-700">Sair</a> diff --git a/config/routes.php b/config/routes.php index 03883b1ff0f723d5c92fbea1978fb7a07e2d2bba..0e3e4f7b8931d3558865ce43ed97b9c3e6e6753d 100644 --- a/config/routes.php +++ b/config/routes.php @@ -6,6 +6,7 @@ require_once '../app/controllers/AuthController.php'; require_once '../app/controllers/DashboardController.php'; require_once '../app/controllers/BillsController.php'; require_once '../app/controllers/TagsController.php'; +require_once '../app/controllers/IncomesController.php'; return function() { $uri = explode('?', trim($_SERVER['REQUEST_URI'], '/'))[0]; @@ -82,6 +83,30 @@ // GET /tags/delete/{id} } elseif (preg_match('/^tags\/delete\/(\d+)$/', $uri, $matches) && $method === 'GET') { return (new TagsController())->destroy($matches[1]); + + // GET /incomes + } elseif ($uri === 'incomes' && $method === 'GET') { + return (new IncomesController())->index(); + + // GET /incomes/create + } elseif ($uri === 'incomes/create' && $method === 'GET') { + return (new IncomesController())->create(); + + // POST /incomes/create + } elseif ($uri === 'incomes/create' && $method === 'POST') { + return (new IncomesController())->store(); + + // GET /incomes/edit/{id} + } elseif (preg_match('/^incomes\/edit\/(\d+)$/', $uri, $matches) && $method === 'GET') { + return (new IncomesController())->edit($matches[1]); + + // POST /incomes/edit/{id} + } elseif (preg_match('/^incomes\/edit\/(\d+)$/', $uri, $matches) && $method === 'POST') { + return (new IncomesController())->update($matches[1]); + + // GET /incomes/delete/{id} + } elseif (preg_match('/^incomes\/delete\/(\d+)$/', $uri, $matches) && $method === 'GET') { + return (new IncomesController())->destroy($matches[1]); // GET /logout } elseif ($uri === 'logout') { diff --git a/schema.sql b/schema.sql index ae34c454504978a8bda71fc6807cd48d221da06d..842306e9b0622eeba9a1006dfe0fb13ab912d663 100644 --- a/schema.sql +++ b/schema.sql @@ -34,3 +34,16 @@ tag_id INTEGER NOT NULL, FOREIGN KEY (bill_id) REFERENCES bills(id), FOREIGN KEY (tag_id) REFERENCES tags(id) ); + +CREATE TABLE IF NOT EXISTS incomes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title VARCHAR(255) NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + income_type TEXT NOT NULL DEFAULT 'one_off', + recurrence_period TEXT NOT NULL DEFAULT 'monthly', + date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +);