feature: Add YTD summary and update adding and editing for new database schema

This commit is contained in:
Keith Solomon
2025-02-09 13:15:44 -06:00
parent 62071c6645
commit f72930006b
4 changed files with 191 additions and 45 deletions

View File

@@ -581,6 +581,9 @@
.mr-auto { .mr-auto {
margin-right: auto; margin-right: auto;
} }
.mb-0 {
margin-bottom: calc(var(--spacing) * 0);
}
.mb-2 { .mb-2 {
margin-bottom: calc(var(--spacing) * 2); margin-bottom: calc(var(--spacing) * 2);
} }
@@ -608,6 +611,9 @@
.inline { .inline {
display: inline; display: inline;
} }
.inline-block {
display: inline-block;
}
.inline-flex { .inline-flex {
display: inline-flex; display: inline-flex;
} }
@@ -650,6 +656,12 @@
.resize { .resize {
resize: both; resize: both;
} }
.list-disc {
list-style-type: disc;
}
.list-none {
list-style-type: none;
}
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
} }
@@ -718,6 +730,9 @@
.bg-gray-300 { .bg-gray-300 {
background-color: var(--color-gray-300); background-color: var(--color-gray-300);
} }
.bg-green-500 {
background-color: var(--color-green-500);
}
.bg-red-300 { .bg-red-300 {
background-color: var(--color-red-300); background-color: var(--color-red-300);
} }
@@ -757,6 +772,15 @@
.py-2 { .py-2 {
padding-block: calc(var(--spacing) * 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 { .text-2xl {
font-size: var(--text-2xl); font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height)); line-height: var(--tw-leading, var(--text-2xl--line-height));
@@ -765,6 +789,10 @@
font-size: var(--text-4xl); font-size: var(--text-4xl);
line-height: var(--tw-leading, var(--text-4xl--line-height)); 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 { .text-sm {
font-size: var(--text-sm); font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height)); line-height: var(--tw-leading, var(--text-sm--line-height));
@@ -781,6 +809,10 @@
--tw-font-weight: var(--font-weight-medium); --tw-font-weight: var(--font-weight-medium);
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 { .text-gray-500 {
color: var(--color-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\:bg-red-400 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -849,6 +888,11 @@
grid-template-columns: repeat(5, minmax(0, 1fr)); 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 { .lg\:grid-cols-4 {
@media (width >= 64rem) { @media (width >= 64rem) {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));

View File

@@ -18,18 +18,25 @@ try {
switch($action) { switch($action) {
case 'add': case 'add':
$data = [ $data = [
'date' => (new DateTime($_POST['date']))->format('Y-m-d'), 'date' => (new DateTime($_POST['date']))->format('Y-m-d'),
'payeeId' => $_POST['payeeId'], // Payee ID instead of name 'payeeId' => $_POST['payeeId'], // Payee ID instead of name
'amount' => (float)$_POST['amount'], 'amount' => (float)$_POST['amount'],
'paymentId' => $_POST['paymentId'], 'paymentId' => $_POST['paymentId'],
'comment' => $_POST['comment'] ?? '', 'comment' => $_POST['comment'] ?? '',
'year' => (int)explode('-', $_POST['date'])[0] '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(" $stmt = DB::connect()->prepare("
INSERT INTO bills (billDate, payeeId, amount, paymentId, comment, year) INSERT INTO bills (billDate, payeeId, amount, paymentId, comment, year)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
"); ");
$result = $stmt->execute([ $result = $stmt->execute([
$data['date'], $data['date'],
$data['payeeId'], $data['payeeId'],
@@ -49,27 +56,27 @@ try {
$data = [ $data = [
'id' => $_POST['id'], 'id' => $_POST['id'],
'date' => (new DateTime($_POST['date']))->format('Y-m-d'), 'date' => (new DateTime($_POST['date']))->format('Y-m-d'),
'billName' => $_POST['billName'], 'payeeId' => $_POST['payeeId'],
'amount' => (float)$_POST['amount'], 'amount' => (float)$_POST['amount'],
'paymentId' => $_POST['paymentId'], 'paymentId' => $_POST['paymentId'],
'comment' => $_POST['comment'] ?? '' 'comment' => $_POST['comment'] ?? ''
]; ];
// Validate required fields // 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.'); throw new MissingRequiredException('Missing required fields.');
} }
// Prepare and execute the update statement // Prepare and execute the update statement
$stmt = DB::connect()->prepare(" $stmt = DB::connect()->prepare("
UPDATE bills UPDATE bills
SET billDate = ?, billName = ?, amount = ?, paymentId = ?, comment = ? SET billDate = ?, payeeId = ?, amount = ?, paymentId = ?, comment = ?
WHERE id = ? WHERE id = ?
"); ");
$result = $stmt->execute([ $result = $stmt->execute([
$data['date'], $data['date'],
$data['billName'], $data['payeeId'],
$data['amount'], $data['amount'],
$data['paymentId'], $data['paymentId'],
$data['comment'], $data['comment'],
@@ -93,9 +100,18 @@ try {
case 'getById': case 'getById':
$id = $_GET['id']; $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]); $stmt->execute([$id]);
echo json_encode($stmt->fetch(PDO::FETCH_ASSOC));
$bill = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode($bill);
break; break;
case 'getTotals': case 'getTotals':
@@ -150,6 +166,37 @@ try {
} }
break; 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: default:
throw new InvalidActionException('Invalid action'); throw new InvalidActionException('Invalid action');
} }

View File

@@ -18,13 +18,13 @@
<!-- Add Bill Form --> <!-- Add Bill Form -->
<form id="billForm" class="bg-white p-4 mb-6 rounded shadow"> <form id="billForm" class="bg-white p-4 mb-6 rounded shadow">
<h2 class="text-2xl font-bold mb-4">Bill Payments</h2> <h2 class="text-2xl font-bold mb-4">Add New Payment</h2>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4"> <div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<input type="date" name="date" required class="p-2 border rounded"> <input type="date" name="date" required class="p-2 border rounded">
<select required id="billName" name="billName" class="border rounded p-2"> <select required id="payeeId" name="payeeId" class="border rounded p-2">
<option value="" disabled selected>Select Payee</option> <!-- Payees will be populated here -->
</select> </select>
<input type="number" step="0.01" name="amount" placeholder="Amount" required class="p-2 border rounded"> <input type="number" step="0.01" name="amount" placeholder="Amount" required class="p-2 border rounded">
@@ -46,10 +46,28 @@
<button type="submit" class="mt-4 bg-blue-500 text-white px-4 py-2 rounded">Add Payee</button> <button type="submit" class="mt-4 bg-blue-500 text-white px-4 py-2 rounded">Add Payee</button>
</form> </form>
<!-- Chart --> <!-- Chart & Totals -->
<div class="border p-4 rounded shadow bg-gray-100"> <div class="border p-4 rounded shadow bg-gray-100 grid grid-cols-1 lg:grid-cols-2 gap-4">
<h2 class="text-2xl font-bold mb-4">Chart</h2> <div id="chartSection" class="bg-white p-4 rounded shadow mb-6">
<canvas id="chart"></canvas> <h3 class="text-xl font-bold mb-4">Chart</h3>
<canvas id="chart"></canvas>
</div>
<div id="ytdSection" class="bg-white p-4 rounded shadow mb-6">
<h3 class="text-xl font-bold mb-4">Year-to-Date Summary</h3>
<div id="ytdOverall" class="mb-4">
<h3 class="text-lg font-semibold mb-0 inline-block">Overall YTD Amount:</h3> <span id="ytdOverallAmount" class="text-lg font-semibold">$0.00</span>
</div>
<div id="ytdPayees">
<h3 class="text-lg font-semibold mb-0 inline-block">YTD by Payee:</h3>
<ul id="ytdPayeeList" class="list-none pl-0">
<!-- Payee amounts will be populated here -->
</ul>
</div>
</div>
</div> </div>
</div> </div>
@@ -100,8 +118,8 @@
<!-- Bill Name --> <!-- Bill Name -->
<div> <div>
<label for="editFormBillName" class="block text-sm font-medium">Payee</label> <label for="editFormPayeeId" class="block text-sm font-medium">Payee</label>
<select id="editFormBillName" name="billName" class="border rounded px-3 py-2 w-full"> <select id="editFormPayeeId" name="payeeId" class="border rounded px-3 py-2 w-full">
<option value="" disabled>Select Payee</option> <option value="" disabled>Select Payee</option>
<!-- Options will be populated dynamically --> <!-- Options will be populated dynamically -->
</select> </select>

View File

@@ -3,6 +3,7 @@ document.addEventListener('DOMContentLoaded', () => {
const sortSelect = document.getElementById('sortSelect'); const sortSelect = document.getElementById('sortSelect');
const billList = document.getElementById('billList'); const billList = document.getElementById('billList');
const form = document.getElementById('billForm'); const form = document.getElementById('billForm');
const currentYear = new Date().getFullYear();
// Populate the year dropdown // Populate the year dropdown
async function loadYears() { async function loadYears() {
@@ -27,13 +28,21 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// Fetch and display payees // Fetch and display payees
async function loadPayees() { async function loadPayees(selectId) {
const response = await fetch('/includes/api.php?action=getPayees'); const response = await fetch('/includes/api.php?action=getPayees');
const payees = await response.json(); const payees = await response.json();
const payeeSelect = document.getElementById('billName'); const payeeSelect = document.getElementById(selectId || 'payeeId');
payeeSelect.innerHTML = ''; // Clear existing options 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 => { payees.forEach(payee => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = payee.id; // Use payee ID as the value option.value = payee.id; // Use payee ID as the value
@@ -73,7 +82,7 @@ document.addEventListener('DOMContentLoaded', () => {
<p><strong>Amount:</strong> $${bill.amount.toFixed(2)}</p> <p><strong>Amount:</strong> $${bill.amount.toFixed(2)}</p>
<p><strong>Payment ID:</strong> ${bill.paymentId || 'N/A'}</p> <p><strong>Payment ID:</strong> ${bill.paymentId || 'N/A'}</p>
<p><strong>Comment:</strong> ${bill.comment || 'N/A'}</p> <p><strong>Comment:</strong> ${bill.comment || 'N/A'}</p>
<button class="block absolute top-2 right-2 border text-gray-500 hover:text-gray-700 px-2 py-0 rounded" data-id="${bill.id}">Edit</button> <button class="block absolute top-2 right-2 px-2 py-0 pb-1 rounded bg-green-500 text-white hover:bg-green-600" data-id="${bill.id}">Edit</button>
`; `;
billList.appendChild(billItem); billList.appendChild(billItem);
@@ -99,17 +108,42 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('editFormComment').value = bill.comment || ''; document.getElementById('editFormComment').value = bill.comment || '';
// Populate the payee dropdown and select the correct option // Populate the payee dropdown and select the correct option
await loadPayees(); // Ensure the payee dropdown is populated await loadPayees('editFormPayeeId'); // Ensure the payee dropdown is populated
document.getElementById('editFormBillName').value = bill.billName;
const payeeDropdown = document.getElementById('editFormPayeeId');
payeeDropdown.value = bill.payeeId; // Use payeeId instead of billName
// Open the modal // Open the modal
document.getElementById('editModal').classList.add('flex'); document.getElementById('editModal').classList.add('flex');
document.getElementById('editModal').classList.remove('hidden'); 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 // Add event listener for year selection
yearSelect.addEventListener('change', (e) => { yearSelect.addEventListener('change', (e) => {
const selectedYear = e.target.value; const selectedYear = e.target.value;
loadYtdAmounts(selectedYear);
loadBills(selectedYear, sortSelect.value); // Use the selected sort order loadBills(selectedYear, sortSelect.value); // Use the selected sort order
}); });
@@ -119,6 +153,26 @@ document.addEventListener('DOMContentLoaded', () => {
loadBills(yearSelect.value, selectedSort); // Use the selected year 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 // Add new payee
document.getElementById('addPayeeForm').addEventListener('submit', async (e) => { document.getElementById('addPayeeForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
@@ -130,31 +184,13 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
if (response.ok) { if (response.ok) {
loadPayees(); // Reload payee dropdown loadPayees('payeeId'); // Reload payee dropdown
e.target.reset(); // Clear the form e.target.reset(); // Clear the form
} else { } else {
alert('Failed to add payee. Please try again.'); 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 // Event listener for edit buttons within bill items
billList.addEventListener('click', (e) => { billList.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON' && e.target.textContent === 'Edit') { if (e.target.tagName === 'BUTTON' && e.target.textContent === 'Edit') {
@@ -193,6 +229,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
// On page load // On page load
loadPayees('payeeId');
loadYears(); loadYears();
loadPayees(); loadYtdAmounts(currentYear);
}); });