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 {
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));

View File

@@ -26,10 +26,17 @@ try {
'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');
}

View File

@@ -18,13 +18,13 @@
<!-- Add Bill Form -->
<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">
<input type="date" name="date" required class="p-2 border rounded">
<select required id="billName" name="billName" class="border rounded p-2">
<option value="" disabled selected>Select Payee</option>
<select required id="payeeId" name="payeeId" class="border rounded p-2">
<!-- Payees will be populated here -->
</select>
<input type="number" step="0.01" name="amount" placeholder="Amount" required class="p-2 border rounded">
@@ -46,11 +46,29 @@
<button type="submit" class="mt-4 bg-blue-500 text-white px-4 py-2 rounded">Add Payee</button>
</form>
<!-- Chart -->
<div class="border p-4 rounded shadow bg-gray-100">
<h2 class="text-2xl font-bold mb-4">Chart</h2>
<!-- Chart & Totals -->
<div class="border p-4 rounded shadow bg-gray-100 grid grid-cols-1 lg:grid-cols-2 gap-4">
<div id="chartSection" class="bg-white p-4 rounded shadow mb-6">
<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>
<!-- Bill List -->
@@ -100,8 +118,8 @@
<!-- Bill Name -->
<div>
<label for="editFormBillName" class="block text-sm font-medium">Payee</label>
<select id="editFormBillName" name="billName" class="border rounded px-3 py-2 w-full">
<label for="editFormPayeeId" class="block text-sm font-medium">Payee</label>
<select id="editFormPayeeId" name="payeeId" class="border rounded px-3 py-2 w-full">
<option value="" disabled>Select Payee</option>
<!-- Options will be populated dynamically -->
</select>

View File

@@ -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', () => {
<p><strong>Amount:</strong> $${bill.amount.toFixed(2)}</p>
<p><strong>Payment ID:</strong> ${bill.paymentId || '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);
@@ -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);
});