From f72930006b1010731c46edac691f1b8f7b88a104 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 9 Feb 2025 13:15:44 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feature:=20Add=20YTD=20summary=20and?= =?UTF-8?q?=20update=20adding=20and=20editing=20for=20new=20database=20sch?= =?UTF-8?q?ema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/style.css | 44 ++++++++++++++++++++++++ includes/api.php | 69 ++++++++++++++++++++++++++++++++------ index.php | 36 +++++++++++++++----- js/app.js | 87 ++++++++++++++++++++++++++++++++++-------------- 4 files changed, 191 insertions(+), 45 deletions(-) diff --git a/css/style.css b/css/style.css index ff711d4..6fc0530 100644 --- a/css/style.css +++ b/css/style.css @@ -581,6 +581,9 @@ .mr-auto { margin-right: auto; } + .mb-0 { + margin-bottom: calc(var(--spacing) * 0); + } .mb-2 { margin-bottom: calc(var(--spacing) * 2); } @@ -608,6 +611,9 @@ .inline { display: inline; } + .inline-block { + display: inline-block; + } .inline-flex { display: inline-flex; } @@ -650,6 +656,12 @@ .resize { resize: both; } + .list-disc { + list-style-type: disc; + } + .list-none { + list-style-type: none; + } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -718,6 +730,9 @@ .bg-gray-300 { background-color: var(--color-gray-300); } + .bg-green-500 { + background-color: var(--color-green-500); + } .bg-red-300 { background-color: var(--color-red-300); } @@ -757,6 +772,15 @@ .py-2 { padding-block: calc(var(--spacing) * 2); } + .pb-1 { + padding-bottom: calc(var(--spacing) * 1); + } + .pl-0 { + padding-left: calc(var(--spacing) * 0); + } + .pl-5 { + padding-left: calc(var(--spacing) * 5); + } .text-2xl { font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); @@ -765,6 +789,10 @@ font-size: var(--text-4xl); line-height: var(--tw-leading, var(--text-4xl--line-height)); } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); @@ -781,6 +809,10 @@ --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } .text-gray-500 { color: var(--color-gray-500); } @@ -820,6 +852,13 @@ } } } + .hover\:bg-green-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-green-600); + } + } + } .hover\:bg-red-400 { &:hover { @media (hover: hover) { @@ -849,6 +888,11 @@ grid-template-columns: repeat(5, minmax(0, 1fr)); } } + .lg\:grid-cols-2 { + @media (width >= 64rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } .lg\:grid-cols-4 { @media (width >= 64rem) { grid-template-columns: repeat(4, minmax(0, 1fr)); diff --git a/includes/api.php b/includes/api.php index 1edbec2..c715b84 100644 --- a/includes/api.php +++ b/includes/api.php @@ -18,18 +18,25 @@ try { switch($action) { case 'add': $data = [ - 'date' => (new DateTime($_POST['date']))->format('Y-m-d'), - 'payeeId' => $_POST['payeeId'], // Payee ID instead of name - 'amount' => (float)$_POST['amount'], + 'date' => (new DateTime($_POST['date']))->format('Y-m-d'), + 'payeeId' => $_POST['payeeId'], // Payee ID instead of name + 'amount' => (float)$_POST['amount'], 'paymentId' => $_POST['paymentId'], - 'comment' => $_POST['comment'] ?? '', - 'year' => (int)explode('-', $_POST['date'])[0] + 'comment' => $_POST['comment'] ?? '', + 'year' => (int)explode('-', $_POST['date'])[0] ]; + // Validate required fields + if (empty($data['date']) || empty($data['payeeId']) || $data['amount'] <= 0) { + echo json_encode(['error' => 'Missing required fields.']); + exit; + } + $stmt = DB::connect()->prepare(" INSERT INTO bills (billDate, payeeId, amount, paymentId, comment, year) VALUES (?, ?, ?, ?, ?, ?) "); + $result = $stmt->execute([ $data['date'], $data['payeeId'], @@ -49,27 +56,27 @@ try { $data = [ 'id' => $_POST['id'], 'date' => (new DateTime($_POST['date']))->format('Y-m-d'), - 'billName' => $_POST['billName'], + 'payeeId' => $_POST['payeeId'], 'amount' => (float)$_POST['amount'], 'paymentId' => $_POST['paymentId'], 'comment' => $_POST['comment'] ?? '' ]; // Validate required fields - if (empty($data['id']) || empty($data['date']) || empty($data['billName']) || $data['amount'] <= 0) { + if (empty($data['id']) || empty($data['date']) || empty($data['payeeId']) || $data['amount'] <= 0) { throw new MissingRequiredException('Missing required fields.'); } // Prepare and execute the update statement $stmt = DB::connect()->prepare(" UPDATE bills - SET billDate = ?, billName = ?, amount = ?, paymentId = ?, comment = ? + SET billDate = ?, payeeId = ?, amount = ?, paymentId = ?, comment = ? WHERE id = ? "); $result = $stmt->execute([ $data['date'], - $data['billName'], + $data['payeeId'], $data['amount'], $data['paymentId'], $data['comment'], @@ -93,9 +100,18 @@ try { case 'getById': $id = $_GET['id']; - $stmt = DB::connect()->prepare("SELECT * FROM bills WHERE id = ?"); + + $stmt = DB::connect()->prepare(" + SELECT bills.*, payees.name AS payeeName, payees.id AS payeeId + FROM bills + JOIN payees ON bills.payeeId = payees.id + WHERE bills.id = ? + "); $stmt->execute([$id]); - echo json_encode($stmt->fetch(PDO::FETCH_ASSOC)); + + $bill = $stmt->fetch(PDO::FETCH_ASSOC); + + echo json_encode($bill); break; case 'getTotals': @@ -150,6 +166,37 @@ try { } break; + case 'getYtdAmounts': + $year = $_GET['year'] ?? date('Y'); + + // Fetch YTD amounts for each payee + $stmt = DB::connect()->prepare(" + SELECT payees.name AS payeeName, SUM(bills.amount) AS totalAmount + FROM bills + JOIN payees ON bills.payeeId = payees.id + WHERE bills.year = ? + GROUP BY payees.name + ORDER BY payees.name ASC + "); + $stmt->execute([$year]); + $payeeAmounts = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Fetch overall YTD amount + $stmt = DB::connect()->prepare(" + SELECT SUM(amount) AS overallAmount + FROM bills + WHERE year = ? + "); + $stmt->execute([$year]); + + $overallAmount = $stmt->fetch(PDO::FETCH_ASSOC)['overallAmount'] ?? 0; + + echo json_encode([ + 'payeeAmounts' => $payeeAmounts, + 'overallAmount' => $overallAmount + ]); + break; + default: throw new InvalidActionException('Invalid action'); } diff --git a/index.php b/index.php index 5733fe0..9e66b91 100644 --- a/index.php +++ b/index.php @@ -18,13 +18,13 @@
-

Bill Payments

+

Add New Payment

- + @@ -46,10 +46,28 @@ - -
-

Chart

- + +
+
+

Chart

+ +
+ +
+

Year-to-Date Summary

+ +
+

Overall YTD Amount:

$0.00 +
+ +
+

YTD by Payee:

+ +
    + +
+
+
@@ -100,8 +118,8 @@
- - diff --git a/js/app.js b/js/app.js index a764428..29c3463 100644 --- a/js/app.js +++ b/js/app.js @@ -3,6 +3,7 @@ document.addEventListener('DOMContentLoaded', () => { const sortSelect = document.getElementById('sortSelect'); const billList = document.getElementById('billList'); const form = document.getElementById('billForm'); + const currentYear = new Date().getFullYear(); // Populate the year dropdown async function loadYears() { @@ -27,13 +28,21 @@ document.addEventListener('DOMContentLoaded', () => { } // Fetch and display payees - async function loadPayees() { + async function loadPayees(selectId) { const response = await fetch('/includes/api.php?action=getPayees'); const payees = await response.json(); - const payeeSelect = document.getElementById('billName'); + const payeeSelect = document.getElementById(selectId || 'payeeId'); + payeeSelect.innerHTML = ''; // Clear existing options + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = 'Select Payee'; + defaultOption.disabled = true; + defaultOption.selected = true; + payeeSelect.appendChild(defaultOption); + payees.forEach(payee => { const option = document.createElement('option'); option.value = payee.id; // Use payee ID as the value @@ -73,7 +82,7 @@ document.addEventListener('DOMContentLoaded', () => {

Amount: $${bill.amount.toFixed(2)}

Payment ID: ${bill.paymentId || 'N/A'}

Comment: ${bill.comment || 'N/A'}

- + `; billList.appendChild(billItem); @@ -99,17 +108,42 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('editFormComment').value = bill.comment || ''; // Populate the payee dropdown and select the correct option - await loadPayees(); // Ensure the payee dropdown is populated - document.getElementById('editFormBillName').value = bill.billName; + await loadPayees('editFormPayeeId'); // Ensure the payee dropdown is populated + + const payeeDropdown = document.getElementById('editFormPayeeId'); + payeeDropdown.value = bill.payeeId; // Use payeeId instead of billName // Open the modal document.getElementById('editModal').classList.add('flex'); document.getElementById('editModal').classList.remove('hidden'); } + async function loadYtdAmounts(year) { + const response = await fetch(`/includes/api.php?action=getYtdAmounts&year=${year}`); + const data = await response.json(); + + // Display overall YTD amount + const ytdOverallAmount = document.getElementById('ytdOverallAmount'); + ytdOverallAmount.textContent = `$${(data.overallAmount || 0).toFixed(2)}`; + + // Display YTD amounts by payee + const ytdPayeeList = document.getElementById('ytdPayeeList'); + ytdPayeeList.innerHTML = ''; // Clear existing items + + data.payeeAmounts.forEach(payee => { + const listItem = document.createElement('li'); + const strongElement = document.createElement('strong'); + strongElement.textContent = `${payee.payeeName}:`; + listItem.appendChild(strongElement); + listItem.appendChild(document.createTextNode(` $${payee.totalAmount.toFixed(2)}`)); + ytdPayeeList.appendChild(listItem); + }); + } + // Add event listener for year selection yearSelect.addEventListener('change', (e) => { const selectedYear = e.target.value; + loadYtdAmounts(selectedYear); loadBills(selectedYear, sortSelect.value); // Use the selected sort order }); @@ -119,6 +153,26 @@ document.addEventListener('DOMContentLoaded', () => { loadBills(yearSelect.value, selectedSort); // Use the selected year }); + // Add new bill + form.addEventListener('submit', async (e) => { + e.preventDefault(); + + const formData = new FormData(form); + + const response = await fetch('/includes/api.php?action=add', { + method: 'POST', + body: formData + }); + + if (response.ok) { + form.reset(); + loadYears(); // Reload years in case a new year was added + loadYtdAmounts(yearSelect.value); // Reload YTD amounts + } else { + alert('Failed to add bill. Please try again.'); + } + }); + // Add new payee document.getElementById('addPayeeForm').addEventListener('submit', async (e) => { e.preventDefault(); @@ -130,31 +184,13 @@ document.addEventListener('DOMContentLoaded', () => { }); if (response.ok) { - loadPayees(); // Reload payee dropdown + loadPayees('payeeId'); // Reload payee dropdown e.target.reset(); // Clear the form } else { alert('Failed to add payee. Please try again.'); } }); - // Add new bill - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const formData = new FormData(form); - const response = await fetch('/includes/api.php?action=add', { - method: 'POST', - body: formData - }); - - if (response.ok) { - form.reset(); - loadYears(); // Reload years in case a new year was added - } else { - alert('Failed to add bill. Please try again.'); - } - }); - // Event listener for edit buttons within bill items billList.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON' && e.target.textContent === 'Edit') { @@ -193,6 +229,7 @@ document.addEventListener('DOMContentLoaded', () => { }); // On page load + loadPayees('payeeId'); loadYears(); - loadPayees(); + loadYtdAmounts(currentYear); });