mirror of
https://github.com/Solo-Web-Works/BillTrak.git
synced 2026-01-29 06:20:34 +00:00
✨feature: Add YTD summary and update adding and editing for new database schema
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
36
index.php
36
index.php
@@ -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,10 +46,28 @@
|
||||
<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>
|
||||
<canvas id="chart"></canvas>
|
||||
<!-- 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
87
js/app.js
87
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', () => {
|
||||
<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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user