From 1c7f43d7b1d25b4ac9e35d50ab7150f891babd0d Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 9 Feb 2025 09:44:05 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feature:=20Basic=20features=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- includes/api.php | 105 +++++++++++++++++++++-- index.php | 137 ++++++++++++++++++++++-------- js/app.js | 214 +++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 381 insertions(+), 75 deletions(-) diff --git a/includes/api.php b/includes/api.php index d6a8fbc..26fc76a 100644 --- a/includes/api.php +++ b/includes/api.php @@ -1,10 +1,16 @@ $_POST['date'], - 'billName' => $_POST['billName'], - 'amount' => (float)$_POST['amount'], + 'date' => (new DateTime($_POST['date']))->format('Y-m-d'), // Normalize to YYYY-MM-DD + 'billName' => $_POST['billName'], + 'amount' => (float)$_POST['amount'], 'paymentId' => $_POST['paymentId'], - 'year' => (int)explode('-', $_POST['date'])[0] + 'year' => (int)explode('-', $_POST['date'])[0], + 'comment' => $_POST['comment'] ]; - echo json_encode(['success' => Bill::add($data)]); + $result = Bill::add($data); + echo json_encode(['success' => $result]); + break; + + case 'edit': + file_put_contents('debug.log', print_r($_POST, true), FILE_APPEND); + + try { + $data = [ + 'id' => $_POST['id'], + 'date' => (new DateTime($_POST['date']))->format('Y-m-d'), + 'billName' => $_POST['billName'], + '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) { + throw new MissingRequiredException('Missing required fields.'); + } + + // Prepare and execute the update statement + $stmt = DB::connect()->prepare(" + UPDATE bills + SET billDate = ?, billName = ?, amount = ?, paymentId = ?, comment = ? + WHERE id = ? + "); + + $result = $stmt->execute([ + $data['date'], + $data['billName'], + $data['amount'], + $data['paymentId'], + $data['comment'], + $data['id'] + ]); + + if (!$result) { + throw new UpdateException('Failed to update the database.'); + } + + echo json_encode(['success' => true]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } + break; + + case 'getPayees': + $stmt = DB::connect()->query("SELECT DISTINCT billName FROM bills ORDER BY billName ASC"); + echo json_encode($stmt->fetchAll(PDO::FETCH_COLUMN)); + break; + + case 'addPayee': + $stmt = DB::connect()->prepare("INSERT INTO bills (billName) VALUES (?)"); + $result = $stmt->execute([$_POST['billName']]); + echo json_encode(['success' => $result]); break; case 'getAll': echo json_encode(Bill::getAll()->fetchAll(PDO::FETCH_ASSOC)); break; - case 'getTotals': + case 'getById': + $id = $_GET['id']; + $stmt = DB::connect()->prepare("SELECT * FROM bills WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode($stmt->fetch(PDO::FETCH_ASSOC)); + break; + + case 'getTotals': echo json_encode(Bill::getYearlyTotals()->fetchAll(PDO::FETCH_ASSOC)); break; + case 'getByYear': + $year = $_GET['year'] ?? date('Y'); + $sort = $_GET['sort'] ?? 'date_asc'; // Default sorting + + $sortOptions = [ + 'date_asc' => 'billDate ASC', + 'date_desc' => 'billDate DESC', + 'payee' => 'billName ASC, billDate ASC', + ]; + + $orderBy = $sortOptions[$sort] ?? $sortOptions['date_asc']; + + $stmt = DB::connect()->prepare("SELECT * FROM bills WHERE year = ? ORDER BY $orderBy"); + $stmt->execute([$year]); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); + break; + + case 'getYears': + $stmt = DB::connect()->query("SELECT DISTINCT year FROM bills ORDER BY year DESC"); + echo json_encode($stmt->fetchAll(PDO::FETCH_COLUMN)); + break; + default: throw new InvalidActionException('Invalid action'); } diff --git a/index.php b/index.php index c249fe4..3edbfcc 100644 --- a/index.php +++ b/index.php @@ -9,61 +9,128 @@ - +
-

Bill Payments

+

Bill Tracker

-
+

Bill Payments

+ +
- + + +
- -
- +
+ +
+

Add New Payee

+ + + + +
+ + +
+

Chart

+ +
- -
- - - - - - - - - + +
+
+

Payments

-
- - - - - - - - - -
DateBillAmountPayment ID
$
+
+
+ + +
+ +
+ + +
+
+
+ +
+
+
+ + + diff --git a/js/app.js b/js/app.js index 8135cfb..56561e4 100644 --- a/js/app.js +++ b/js/app.js @@ -1,6 +1,148 @@ document.addEventListener('DOMContentLoaded', () => { - const form = document.getElementById('billForm'); + const yearSelect = document.getElementById('yearSelect'); + const sortSelect = document.getElementById('sortSelect'); const billList = document.getElementById('billList'); + const form = document.getElementById('billForm'); + + // Populate the year dropdown + async function loadYears() { + const response = await fetch('/includes/api.php?action=getYears'); + const years = await response.json(); + + yearSelect.innerHTML = ''; // Clear existing options + years.forEach(year => { + const option = document.createElement('option'); + option.value = year; + option.textContent = year; + yearSelect.appendChild(option); + }); + + // Set default year to the most recent one + if (years.length > 0) { + yearSelect.value = years[0]; + loadBills(years[0], sortSelect.value); // Load bills for the most recent year with the default sort order + } else { + billList.innerHTML = '

No data available.

'; + } + } + + // Fetch and display payees + async function loadPayees() { + const response = await fetch('/includes/api.php?action=getPayees'); + const payees = await response.json(); + + const payeeSelect = document.getElementById('editFormBillName'); + payeeSelect.innerHTML = ''; // Clear existing options + + // Add default "Select Payee" option + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = 'Select Payee'; + defaultOption.disabled = true; + payeeSelect.appendChild(defaultOption); + + // Populate payees + payees.forEach(payee => { + const option = document.createElement('option'); + option.value = payee; + option.textContent = payee; + payeeSelect.appendChild(option); + }); + } + + // Fetch and display bills + async function loadBills(year, sort) { + const response = await fetch(`/includes/api.php?action=getByYear&year=${year}&sort=${sort}`); + const data = await response.json(); + + billList.innerHTML = ''; // Clear current bills + if (data.length === 0) { + billList.innerHTML = '

No bills found for this year.

'; + return; + } + + data.forEach(bill => { + const billItem = document.createElement('div'); + billItem.className = 'mb-2 p-4 border rounded bg-gray-100 shadow-sm relative'; + + // Parse the date string as a local date + const dateParts = bill.billDate.split('-'); // Split "YYYY-MM-DD" + const localDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); // Month is 0-indexed + + const formattedDate = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }).format(localDate); + + billItem.innerHTML = ` +

Date: ${formattedDate}

+

Bill Name: ${bill.billName}

+

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

+

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

+

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

+ + `; + + billList.appendChild(billItem); + }); + } + + // Edit a bill + async function editBill(id) { + // Find the specific bill data by ID + const response = await fetch(`/includes/api.php?action=getById&id=${id}`); + const bill = await response.json(); + + if (!bill) { + alert("Error: Could not retrieve the bill data."); + return; + } + + // Populate modal fields with the bill data + document.getElementById('editFormId').value = bill.id; + document.getElementById('editFormDate').value = bill.billDate; + document.getElementById('editFormAmount').value = bill.amount; + document.getElementById('editFormPaymentId').value = bill.paymentId; + 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; + + // Open the modal + document.getElementById('editModal').classList.remove('hidden'); + } + + // Add event listener for year selection + yearSelect.addEventListener('change', (e) => { + const selectedYear = e.target.value; + loadBills(selectedYear, sortSelect.value); // Use the selected sort order + }); + + // Event listener for sort selection + sortSelect.addEventListener('change', (e) => { + const selectedSort = e.target.value; + loadBills(yearSelect.value, selectedSort); // Use the selected year + }); + + // Add new payee + document.getElementById('addPayeeForm').addEventListener('submit', async (e) => { + e.preventDefault(); + + const formData = new FormData(e.target); + const response = await fetch('/includes/api.php?action=addPayee', { + method: 'POST', + body: formData + }); + + if (response.ok) { + loadPayees(); // 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) => { @@ -14,45 +156,49 @@ document.addEventListener('DOMContentLoaded', () => { if (response.ok) { form.reset(); - loadBills(); - updateChart(); + loadYears(); // Reload years in case a new year was added + } else { + alert('Failed to add bill. Please try again.'); } }); - // Load bills via AJAX - async function loadBills() { - const response = await fetch('/includes/api.php?action=getAll'); - const bills = await response.json(); + // Event listener for edit buttons within bill items + billList.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON' && e.target.textContent === 'Edit') { + const billId = e.target.getAttribute('data-id'); + editBill(billId); + } + }); - billList.innerHTML = bills.map(bill => ` - - ${new Date(bill.billDate).toISOString().split('T')[0]} - ${bill.billName} - $${parseFloat(bill.amount).toFixed(2)} - ${bill.paymentId} - - `).join(''); - } + // Save edited bill + document.getElementById('editForm').addEventListener('submit', async (e) => { + e.preventDefault(); - // Initialize chart - let chart; - async function updateChart() { - const response = await fetch('/includes/api.php?action=getTotals'); - const data = await response.json(); + const formData = new FormData(e.target); - if (chart) chart.destroy(); - - chart = new Chart(document.getElementById('chart'), { - type: 'bar', - data: { - labels: [...new Set(data.map(item => item.year))], - datasets: Object.groupBy(data, ({ billName }) => billName).map(([name, values]) => ({ - label: name, - data: values.map(v => v.total) - })) - } + const response = await fetch('/includes/api.php?action=edit', { + method: 'POST', + body: formData }); - } - updateChart(); + if (response.ok) { + loadBills(yearSelect.value, sortSelect.value); // Reload bills after editing + document.getElementById('editModal').classList.add('hidden'); // Close the modal + } else { + alert('Failed to edit bill. Please try again.'); + } + }); + + // Event listener for close button in the modal + document.getElementById('editFormCancel').addEventListener('click', () => { + // Close the modal + document.getElementById('editModal').classList.add('hidden'); + + // Clear modal fields + document.getElementById('editForm').reset(); + }); + + // On page load + loadYears(); + loadPayees(); });