diff --git a/includes/api.php b/includes/api.php index 00dbaaf..54d4c4e 100644 --- a/includes/api.php +++ b/includes/api.php @@ -142,9 +142,67 @@ try { break; case 'addPayee': - $stmt = DB::connect()->prepare("INSERT INTO payees (name) VALUES (?)"); - $result = $stmt->execute([$_POST['payeeName']]); - echo json_encode(['success' => $result]); + try { + $name = trim($_POST['payeeName'] ?? ''); + if ($name === '') { + throw new MissingRequiredException('Payee name is required.'); + } + + $stmt = DB::connect()->prepare("INSERT INTO payees (name) VALUES (?)"); + $result = $stmt->execute([$name]); + echo json_encode(['success' => $result]); + } catch (Exception $e) { + http_response_code(400); + echo json_encode(['error' => $e->getMessage()]); + } + break; + + case 'editPayee': + try { + $id = $_POST['id'] ?? null; + $name = trim($_POST['payeeName'] ?? ''); + + if (empty($id) || $name === '') { + throw new MissingRequiredException('Missing payee id or name.'); + } + + $stmt = DB::connect()->prepare("UPDATE payees SET name = ? WHERE id = ?"); + $result = $stmt->execute([$name, $id]); + + echo json_encode(['success' => $result]); + } catch (Exception $e) { + http_response_code(400); + echo json_encode(['error' => $e->getMessage()]); + } + break; + + case 'deletePayee': + try { + $id = $_POST['id'] ?? null; + + if (empty($id)) { + throw new MissingRequiredException('Missing payee id.'); + } + + $db = DB::connect(); + + // Block deletion if payee is still referenced by bills + $usage = $db->prepare("SELECT COUNT(*) FROM bills WHERE payeeId = ?"); + $usage->execute([$id]); + $billCount = (int)$usage->fetchColumn(); + + if ($billCount > 0) { + throw new RuntimeException('Cannot delete payee while bills reference it.'); + } + + $stmt = $db->prepare("DELETE FROM payees WHERE id = ?"); + $result = $stmt->execute([$id]); + + echo json_encode(['success' => $result]); + } catch (Exception $e) { + http_response_code(400); + echo json_encode(['error' => $e->getMessage()]); + } break; case 'getPayees': diff --git a/index.php b/index.php index e64d659..418defa 100644 --- a/index.php +++ b/index.php @@ -17,8 +17,7 @@

Bill Tracker

-
- +
- +
+ + +
@@ -58,6 +60,12 @@ + +
+

Manage Payees

+

No payees yet.

+
    +
    diff --git a/js/app.js b/js/app.js index c21f155..511f65e 100644 --- a/js/app.js +++ b/js/app.js @@ -4,6 +4,8 @@ document.addEventListener('DOMContentLoaded', () => { const billList = document.getElementById('billList'); const form = document.getElementById('billForm'); const currentYear = new Date().getFullYear(); + const payeeList = document.getElementById('payeeList'); + const payeeListEmpty = document.getElementById('payeeListEmpty'); const importModal = document.getElementById('importModal'); const openImportButton = document.getElementById('openImportModal'); const importCancelButton = document.getElementById('importCancel'); @@ -57,6 +59,28 @@ document.addEventListener('DOMContentLoaded', () => { option.textContent = payee.name; // Display payee name payeeSelect.appendChild(option); }); + + if (payeeList) { + payeeList.innerHTML = ''; + + if (payees.length === 0) { + if (payeeListEmpty) payeeListEmpty.classList.remove('hidden'); + } else { + if (payeeListEmpty) payeeListEmpty.classList.add('hidden'); + payees.forEach((payee) => { + const li = document.createElement('li'); + li.className = 'flex items-center justify-between border rounded px-3 py-2 bg-white shadow-sm'; + li.innerHTML = ` + ${payee.name} +
    + + +
    + `; + payeeList.appendChild(li); + }); + } + } } // Fetch and display bills @@ -248,6 +272,13 @@ document.addEventListener('DOMContentLoaded', () => { e.preventDefault(); const formData = new FormData(e.target); + const payeeName = (formData.get('payeeName') || '').toString().trim(); + if (payeeName === '') { + alert('Please enter a payee name.'); + return; + } + formData.set('payeeName', payeeName); + const response = await fetch('/includes/api.php?action=addPayee', { method: 'POST', body: formData @@ -261,6 +292,63 @@ document.addEventListener('DOMContentLoaded', () => { } }); + // Edit/Delete payees + if (payeeList) { + payeeList.addEventListener('click', async (e) => { + const action = e.target.getAttribute('data-action'); + const payeeId = e.target.getAttribute('data-id'); + const currentName = e.target.getAttribute('data-name'); + + if (!action || !payeeId) return; + + if (action === 'edit-payee') { + const newName = prompt('Enter the new payee name:', currentName || ''); + if (!newName || newName.trim() === '') { + return; + } + + const formData = new FormData(); + formData.append('id', payeeId); + formData.append('payeeName', newName.trim()); + + const response = await fetch('/includes/api.php?action=editPayee', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + if (!response.ok || data.error) { + alert(data.error || 'Failed to update payee.'); + return; + } + + await loadPayees('payeeId'); + await loadBills(yearSelect.value, sortSelect.value); // refresh displayed names + } + + if (action === 'delete-payee') { + const confirmed = confirm(`Delete payee "${currentName}"? This cannot be undone.`); + if (!confirmed) return; + + const formData = new FormData(); + formData.append('id', payeeId); + + const response = await fetch('/includes/api.php?action=deletePayee', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + if (!response.ok || data.error) { + alert(data.error || 'Failed to delete payee.'); + return; + } + + await loadPayees('payeeId'); + } + }); + } + // Event listener for edit buttons within bill items billList.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON' && e.target.textContent === 'Edit') {