@ -22,6 +22,8 @@
<!-- Flatpickr Datepicker -->
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" >
< script src = "https://cdn.jsdelivr.net/npm/flatpickr" > < / script >
<!-- Chart.js -->
< script src = "https://cdn.jsdelivr.net/npm/chart.js" > < / script >
<!-- Custom Style Sheet mapping to theme -->
< style >
@ -770,6 +772,133 @@
align-items: center;
gap: 8px;
}
/* Interactive Summary Cell Styles */
.cursor-pointer {
cursor: pointer;
transition: all 0.2s ease;
}
.cursor-pointer:hover {
background-color: #cbd5e1 !important;
transform: scale(1.08);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
z-index: 5;
position: relative;
}
/* Customized premium scrollbar for responsive tables */
.table-responsive::-webkit-scrollbar {
height: 6px;
width: 6px;
}
.table-responsive::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.table-responsive::-webkit-scrollbar-track {
background: #f8fafc;
}
/* Force table cells to wrap cleanly and prevent word clipping */
.grid-table th,
.grid-table td {
white-space: normal;
}
/* Keep straight horizontal scrolling layout only on mobile phone viewports */
@media (max-width: 767.98px) {
.grid-table th,
.grid-table td {
white-space: nowrap;
}
}
/* Media Queries for all devices */
@media (max-width: 991.98px) {
.header {
flex-direction: column;
gap: 15px;
text-align: center;
padding: 20px;
}
.header-title h2 {
font-size: 1.35rem;
}
.user-menu {
width: 100%;
justify-content: center;
}
.main-container {
max-width: 100%;
margin: 15px auto;
}
.category-grid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.card-value {
font-size: 1.5rem;
}
.card-icon {
font-size: 1.85rem;
}
}
@media (max-width: 575.98px) {
.header-title h2 {
font-size: 1.15rem;
}
.category-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.category-card {
padding: 16px;
border-radius: 12px;
}
.card-value {
font-size: 1.25rem;
}
.card-icon {
font-size: 1.5rem;
}
.filter-card,
.table-card {
padding: 16px;
border-radius: 12px;
}
.table-controls-row {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.table-controls-right input {
width: 100% !important;
}
.btn-add-record {
width: 100%;
justify-content: center;
}
}
< / style >
< / head >
@ -878,6 +1007,135 @@
< / div >
<!-- Executive Summary & Charts Section -->
< div class = "row g-4 mb-4" >
<!-- Executive Summary Table Card -->
< div class = "col-lg-8" >
< div class = "card h-100 border-0 shadow-sm" style = "border-radius: 16px; overflow: hidden;" >
< div class = "card-header bg-white py-3 border-0" >
< h4 class = "card-title mb-0 fw-bold"
style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
< i class = "fa-solid fa-chart-line me-2 text-primary" > < / i > Executive Summary (Overall Invoices
Status)
< / h4 >
< / div >
< div class = "card-body p-3" >
< div class = "table-responsive" >
< table class = "table table-bordered text-center align-middle mb-0"
style="font-size: 0.82rem; border-color: #e2e8f0;">
< thead class = "table-light text-secondary fw-bold" style = "font-size: 0.75rem;" >
< tr >
< th style = "background-color: #f8fafc; color: #475569;" > Total Invoices< / th >
< th style = "background-color: #f0fdf4; color: #166534;" > Completed< / th >
< th style = "background-color: #fffbeb; color: #92400e;" > Awaiting Details< / th >
< th style = "background-color: #f0f9ff; color: #075985;" > Pending with Dept< / th >
< th style = "background-color: #fef2f2; color: #991b1b;" > Pending< / th >
< th style = "background-color: #fafaf9; color: #44403c;" > 1 to 15 Days< / th >
< th style = "background-color: #fafaf9; color: #44403c;" > 16 to 30 Days< / th >
< th style = "background-color: #fafaf9; color: #44403c;" > 31 to 60 Days< / th >
< th style = "background-color: #fafaf9; color: #44403c;" > 61 to 90 Days< / th >
< th style = "background-color: #fafaf9; color: #44403c;" > 91 to 120 Days< / th >
< th style = "background-color: #fafaf9; color: #44403c;" > Over 120 Days< / th >
< / tr >
< / thead >
< tbody >
< tr class = "fw-bold" style = "font-size: 1rem; color: #1e293b;" >
< td id = "summary-total" class = "cursor-pointer text-primary"
style="background-color: #f8fafc;" onclick="clearClickFilter()">0< / td >
< td id = "summary-completed" class = "cursor-pointer text-success"
onclick="setClickFilter('status', 'Completed', 'Status: Completed')">0< / td >
< td id = "summary-awaiting" class = "cursor-pointer text-warning"
onclick="setClickFilter('status', 'Awaiting Details', 'Status: Awaiting Details')">
0< / td >
< td id = "summary-dept" class = "cursor-pointer text-info"
onclick="setClickFilter('status', 'Pending with Dept', 'Status: Pending with Dept')">
0< / td >
< td id = "summary-pending" class = "cursor-pointer text-danger"
onclick="setClickFilter('status', 'Pending', 'Status: Pending')">0< / td >
< td id = "summary-1-15" class = "cursor-pointer"
onclick="setClickFilter('age', [1, 15], 'Age: 1 to 15 Days')">0< / td >
< td id = "summary-16-30" class = "cursor-pointer"
onclick="setClickFilter('age', [16, 30], 'Age: 16 to 30 Days')">0< / td >
< td id = "summary-31-60" class = "cursor-pointer"
onclick="setClickFilter('age', [31, 60], 'Age: 31 to 60 Days')">0< / td >
< td id = "summary-61-90" class = "cursor-pointer"
onclick="setClickFilter('age', [61, 90], 'Age: 61 to 90 Days')">0< / td >
< td id = "summary-91-120" class = "cursor-pointer"
onclick="setClickFilter('age', [91, 120], 'Age: 91 to 120 Days')">0< / td >
< td id = "summary-over-120" class = "cursor-pointer text-danger"
onclick="setClickFilter('age', [121, null], 'Age: Over 120 Days')">0< / td >
< / tr >
< / tbody >
< / table >
< / div >
< p class = "text-muted mt-2 mb-0" style = "font-size: 0.72rem; font-style: italic;" >
< i class = "fa-solid fa-circle-info me-1" > < / i > Tip: Click on any count cell to filter the
detailed payment tracker list below.
< / p >
< / div >
< / div >
< / div >
<!-- Pie Chart Card -->
< div class = "col-lg-4" >
< div class = "card h-100 border-0 shadow-sm" style = "border-radius: 16px;" >
< div class = "card-header bg-white py-3 border-0" >
< h4 class = "card-title mb-0 fw-bold"
style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
< i class = "fa-solid fa-chart-pie me-2 text-primary" > < / i > Invoices Status Distribution
< / h4 >
< / div >
< div class = "card-body d-flex align-items-center justify-content-center p-2"
style="position: relative; height: 180px;">
< canvas id = "statusPieChart" style = "max-height: 100%; max-width: 100%;" > < / canvas >
< / div >
< / div >
< / div >
< / div >
<!-- Vendor Breakdown Details Card -->
< div class = "card border-0 shadow-sm mb-4" style = "border-radius: 16px; overflow: hidden;" >
< div
class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center flex-wrap gap-2">
< h4 class = "card-title mb-0 fw-bold"
style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
< i class = "fa-solid fa-users-viewfinder me-2 text-primary" > < / i > Vendor Breakdown Details (Invoices)
< / h4 >
< div style = "font-size: 0.8rem; color: #64748b;" >
< input type = "text" id = "vendor-breakdown-search" class = "form-control form-control-sm"
style="width: 200px; border-radius: 8px;" placeholder="Search vendor breakdown..."
oninput="filterVendorBreakdown(this.value)">
< / div >
< / div >
< div class = "card-body p-3" >
< div class = "table-responsive" style = "max-height: 280px; overflow-y: auto;" >
< table class = "table table-striped table-bordered text-center align-middle mb-0"
style="font-size: 0.8rem; border-color: #e2e8f0;">
< thead class = "table-light sticky-top text-secondary fw-bold"
style="font-size: 0.72rem; z-index: 10;">
< tr >
< th style = "text-align: left; background-color: #f8fafc;" > Vendor Name< / th >
< th style = "background-color: #f8fafc;" > Total Invoices< / th >
< th style = "background-color: #f0fdf4;" > Completed< / th >
< th style = "background-color: #fffbeb;" > Awaiting Details< / th >
< th style = "background-color: #f0f9ff;" > Pending with Dept< / th >
< th style = "background-color: #fef2f2;" > Pending< / th >
< th style = "background-color: #fafaf9;" > 1 to 15 Days< / th >
< th style = "background-color: #fafaf9;" > 16 to 30 Days< / th >
< th style = "background-color: #fafaf9;" > 31 to 60 Days< / th >
< th style = "background-color: #fafaf9;" > 61 to 90 Days< / th >
< th style = "background-color: #fafaf9;" > 91 to 120 Days< / th >
< th style = "background-color: #fafaf9;" > Over 120 Days< / th >
< / tr >
< / thead >
< tbody id = "vendor-breakdown-tbody" >
<!-- Populated dynamically via JS -->
< / tbody >
< / table >
< / div >
< / div >
< / div >
<!-- Filter Card -->
< div class = "filter-card" >
< div class = "filter-grid" >
@ -949,6 +1207,8 @@
< i class = "fa-solid fa-table-list text-primary" > < / i >
< span > Payment Tracking Details< / span >
< span class = "active-filter-badge" id = "active-category-badge" > Type: All Types< / span >
< span class = "active-filter-badge ms-2" id = "click-filter-badge"
style="display: none; background-color: #fee2e2; color: #b91c1c;">< / span >
< / h3 >
< button th:if = "${isAdmin}" class = "btn-add-record" onclick = "openAddModal()" >
< i class = "fa-solid fa-circle-plus" > < / i > Add Payment Tracking Record
@ -1246,6 +1506,11 @@
let currentPage = 1;
let pageSize = 20;
// Custom dashboard filter & chart instances
let activeClickFilter = null;
let statusChartInstance = null;
let vendorBreakdownData = [];
// Dynamic lists for types and vendors
let activeTypes = [];
let activeVendors = [];
@ -1418,6 +1683,9 @@
// Refresh list
loadList();
// Smoothly scroll down to the table card
document.querySelector('.table-card')?.scrollIntoView({ behavior: 'smooth' });
}
// Loading and Rendering list data
@ -1449,6 +1717,8 @@
.then(data => {
allRecords = data;
currentPage = 1;
clearClickFilter(null);
calculateSummaries();
renderTable();
})
.catch(err => {
@ -1466,6 +1736,44 @@
let query = searchVal.toLowerCase().trim();
let filtered = allRecords.filter(row => {
// Apply activeClickFilter if set
if (activeClickFilter) {
const age = parseInt(calculateAgeOfBill(row.received_date), 10);
const status = String(row.status || '').trim();
const vendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
if (activeClickFilter.type === 'status') {
if (activeClickFilter.value === 'Pending') {
if (status === 'Completed' || status === 'Awaiting Details' || status === 'Pending with Dept') return false;
} else {
if (status !== activeClickFilter.value) return false;
}
} else if (activeClickFilter.type === 'age') {
const [min, max] = activeClickFilter.value;
if (max !== null) {
if (age < min | | age > max) return false;
} else {
if (age < min ) return false ;
}
} else if (activeClickFilter.type === 'vendor') {
if (vendor !== activeClickFilter.vendorName) return false;
if (activeClickFilter.col) {
if (activeClickFilter.col === 'completed' & & status !== 'Completed') return false;
if (activeClickFilter.col === 'awaiting' & & status !== 'Awaiting Details') return false;
if (activeClickFilter.col === 'pending_dept' & & status !== 'Pending with Dept') return false;
if (activeClickFilter.col === 'pending' & & (status === 'Completed' || status === 'Awaiting Details' || status === 'Pending with Dept')) return false;
if (activeClickFilter.col === 'age_1_15' & & (age < 1 | | age > 15)) return false;
if (activeClickFilter.col === 'age_16_30' & & (age < 16 | | age > 30)) return false;
if (activeClickFilter.col === 'age_31_60' & & (age < 31 | | age > 60)) return false;
if (activeClickFilter.col === 'age_61_90' & & (age < 61 | | age > 90)) return false;
if (activeClickFilter.col === 'age_91_120' & & (age < 91 | | age > 120)) return false;
if (activeClickFilter.col === 'age_over_120' & & age < = 120) return false;
}
}
}
if (!query) return true;
const vName = String(row.resolved_vendor || row.vendor_name || '').toLowerCase();
const tName = String(row.resolved_type || row.type || '').toLowerCase();
@ -1553,32 +1861,7 @@
return dateTimeStr;
};
// Calculate age of bill (days) dynamically from received date:
const calculateAgeOfBill = receivedDateStr => {
if (!receivedDateStr) return '0';
try {
const recDate = new Date(receivedDateStr);
if (isNaN(recDate.getTime())) return '0';
recDate.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const graceDate = new Date(recDate);
graceDate.setDate(graceDate.getDate() + 7);
if (today < = graceDate) {
return '0';
} else {
const diffTime = today.getTime() - graceDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return String(diffDays);
}
} catch (e) {
console.error("Error calculating age of bill:", e);
return '0';
}
};
const getTypeBadge = typeVal => {
const cleanType = String(typeVal || '').trim().toLowerCase();
@ -2107,6 +2390,284 @@
}
}
// Calculate age of bill (days) dynamically from received date
function calculateAgeOfBill(receivedDateStr) {
if (!receivedDateStr) return '0';
try {
const recDate = new Date(receivedDateStr);
if (isNaN(recDate.getTime())) return '0';
recDate.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const graceDate = new Date(recDate);
graceDate.setDate(graceDate.getDate() + 7);
if (today < = graceDate) {
return '0';
} else {
const diffTime = today.getTime() - graceDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return String(diffDays);
}
} catch (e) {
console.error("Error calculating age of bill:", e);
return '0';
}
}
// --- NEW METRICS & INTERACTIVE CLICK FILTER LOGIC ---
function calculateSummaries() {
let total = allRecords.length;
let completed = 0;
let awaiting = 0;
let dept = 0;
let pending = 0;
let age_1_15 = 0;
let age_16_30 = 0;
let age_31_60 = 0;
let age_61_90 = 0;
let age_91_120 = 0;
let age_over_120 = 0;
const vendorMap = {};
allRecords.forEach(row => {
const status = String(row.status || '').trim();
const age = parseInt(calculateAgeOfBill(row.received_date), 10);
const vendorName = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
if (status === 'Completed') completed++;
else if (status === 'Awaiting Details') awaiting++;
else if (status === 'Pending with Dept') dept++;
else pending++;
if (age >= 1 & & age < = 15) age_1_15++;
else if (age >= 16 & & age < = 30) age_16_30++;
else if (age >= 31 & & age < = 60) age_31_60++;
else if (age >= 61 & & age < = 90) age_61_90++;
else if (age >= 91 & & age < = 120) age_91_120++;
else if (age > 120) age_over_120++;
if (!vendorMap[vendorName]) {
vendorMap[vendorName] = {
vendorName: vendorName,
total: 0,
completed: 0,
awaiting: 0,
dept: 0,
pending: 0,
age_1_15: 0,
age_16_30: 0,
age_31_60: 0,
age_61_90: 0,
age_91_120: 0,
age_over_120: 0
};
}
const vData = vendorMap[vendorName];
vData.total++;
if (status === 'Completed') vData.completed++;
else if (status === 'Awaiting Details') vData.awaiting++;
else if (status === 'Pending with Dept') vData.dept++;
else vData.pending++;
if (age >= 1 & & age < = 15) vData.age_1_15++;
else if (age >= 16 & & age < = 30) vData.age_16_30++;
else if (age >= 31 & & age < = 60) vData.age_31_60++;
else if (age >= 61 & & age < = 90) vData.age_61_90++;
else if (age >= 91 & & age < = 120) vData.age_91_120++;
else if (age > 120) vData.age_over_120++;
});
document.getElementById('summary-total').textContent = total;
document.getElementById('summary-completed').textContent = completed;
document.getElementById('summary-awaiting').textContent = awaiting;
document.getElementById('summary-dept').textContent = dept;
document.getElementById('summary-pending').textContent = pending;
document.getElementById('summary-1-15').textContent = age_1_15;
document.getElementById('summary-16-30').textContent = age_16_30;
document.getElementById('summary-31-60').textContent = age_31_60;
document.getElementById('summary-61-90').textContent = age_61_90;
document.getElementById('summary-91-120').textContent = age_91_120;
document.getElementById('summary-over-120').textContent = age_over_120;
renderStatusChart(completed, awaiting, dept, pending);
vendorBreakdownData = Object.values(vendorMap).sort((a, b) => b.total - a.total);
renderVendorBreakdown();
}
function renderStatusChart(completed, awaiting, dept, pending) {
const ctx = document.getElementById('statusPieChart')?.getContext('2d');
if (!ctx) return;
if (statusChartInstance) {
statusChartInstance.destroy();
}
statusChartInstance = new Chart(ctx, {
type: 'pie',
data: {
labels: ['Completed', 'Awaiting Details', 'Pending with Dept', 'Pending'],
datasets: [{
data: [completed, awaiting, dept, pending],
backgroundColor: [
'#2ecc71',
'#f1c40f',
'#3498db',
'#e74c3c'
],
borderWidth: 1,
borderColor: '#ffffff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
boxWidth: 12,
font: {
size: 10,
family: 'Poppins'
}
}
}
}
}
});
}
function renderVendorBreakdown() {
const tbody = document.getElementById('vendor-breakdown-tbody');
if (!tbody) return;
tbody.innerHTML = '';
const searchVal = document.getElementById('vendor-breakdown-search')?.value || '';
const query = searchVal.toLowerCase().trim();
const filtered = vendorBreakdownData.filter(v => v.vendorName.toLowerCase().includes(query));
if (filtered.length === 0) {
tbody.innerHTML = `< tr > < td colspan = "12" class = "text-secondary py-3" > No matching vendors found< / td > < / tr > `;
return;
}
// Keep track of column sums
let sumTotal = 0;
let sumCompleted = 0;
let sumAwaiting = 0;
let sumDept = 0;
let sumPending = 0;
let sumAge1_15 = 0;
let sumAge16_30 = 0;
let sumAge31_60 = 0;
let sumAge61_90 = 0;
let sumAge91_120 = 0;
let sumAgeOver120 = 0;
filtered.forEach(v => {
sumTotal += v.total;
sumCompleted += v.completed;
sumAwaiting += v.awaiting;
sumDept += v.dept;
sumPending += v.pending;
sumAge1_15 += v.age_1_15;
sumAge16_30 += v.age_16_30;
sumAge31_60 += v.age_31_60;
sumAge61_90 += v.age_61_90;
sumAge91_120 += v.age_91_120;
sumAgeOver120 += v.age_over_120;
const tr = document.createElement('tr');
tr.innerHTML = `
< td class = "fw-semibold text-start text-dark" style = "max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" > ${escapeHtml(v.vendorName)}< / td >
< td class = "cursor-pointer text-primary fw-bold" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)}', '${escapeHtml(v.vendorName)}')" > ${v.total}< / td >
< td class = "cursor-pointer text-success" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (Completed)', '${escapeHtml(v.vendorName)}', 'completed')" > ${v.completed}< / td >
< td class = "cursor-pointer text-warning" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (Awaiting Details)', '${escapeHtml(v.vendorName)}', 'awaiting')" > ${v.awaiting}< / td >
< td class = "cursor-pointer text-info" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (Pending with Dept)', '${escapeHtml(v.vendorName)}', 'pending_dept')" > ${v.dept}< / td >
< td class = "cursor-pointer text-danger" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (Pending)', '${escapeHtml(v.vendorName)}', 'pending')" > ${v.pending}< / td >
< td class = "cursor-pointer" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (1-15 Days)', '${escapeHtml(v.vendorName)}', 'age_1_15')" > ${v.age_1_15}< / td >
< td class = "cursor-pointer" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (16-30 Days)', '${escapeHtml(v.vendorName)}', 'age_16_30')" > ${v.age_16_30}< / td >
< td class = "cursor-pointer" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (31-60 Days)', '${escapeHtml(v.vendorName)}', 'age_31_60')" > ${v.age_31_60}< / td >
< td class = "cursor-pointer" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (61-90 Days)', '${escapeHtml(v.vendorName)}', 'age_61_90')" > ${v.age_61_90}< / td >
< td class = "cursor-pointer" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (91-120 Days)', '${escapeHtml(v.vendorName)}', 'age_91_120')" > ${v.age_91_120}< / td >
< td class = "cursor-pointer text-danger" onclick = "setClickFilter('vendor', null, 'Vendor: ${escapeHtml(v.vendorName)} (Over 120 Days)', '${escapeHtml(v.vendorName)}', 'age_over_120')" > ${v.age_over_120}< / td >
`;
tbody.appendChild(tr);
});
// Append TOTAL row at the bottom
const totalTr = document.createElement('tr');
totalTr.className = 'table-warning fw-bold text-dark';
totalTr.style.borderTop = '2px solid #cbd5e1';
totalTr.innerHTML = `
< td class = "text-start" > TOTAL< / td >
< td > ${sumTotal}< / td >
< td > ${sumCompleted}< / td >
< td > ${sumAwaiting}< / td >
< td > ${sumDept}< / td >
< td > ${sumPending}< / td >
< td > ${sumAge1_15}< / td >
< td > ${sumAge16_30}< / td >
< td > ${sumAge31_60}< / td >
< td > ${sumAge61_90}< / td >
< td > ${sumAge91_120}< / td >
< td class = "text-danger" > ${sumAgeOver120}< / td >
`;
tbody.appendChild(totalTr);
}
function filterVendorBreakdown(val) {
renderVendorBreakdown();
}
function setClickFilter(type, value, displayName, vendorName = null, col = null) {
activeClickFilter = { type, value, displayName, vendorName, col };
const badge = document.getElementById('click-filter-badge');
if (badge) {
badge.innerHTML = `Filter: ${displayName} < i class = "fa-solid fa-circle-xmark ms-1 cursor-pointer" onclick = "clearClickFilter(event)" > < / i > `;
badge.style.display = 'inline-block';
badge.style.cursor = 'pointer';
}
currentPage = 1;
renderTable();
// Smoothly scroll down to the table card
document.querySelector('.table-card')?.scrollIntoView({ behavior: 'smooth' });
}
function clearClickFilter(event) {
if (event) event.stopPropagation();
activeClickFilter = null;
const badge = document.getElementById('click-filter-badge');
if (badge) {
badge.style.display = 'none';
}
currentPage = 1;
renderTable();
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/& /g, "& ")
.replace(/< /g, "< ")
.replace(/>/g, "> ")
.replace(/"/g, "" ")
.replace(/'/g, "' ");
}
// --- END OF NEW METRICS & INTERACTIVE CLICK FILTER LOGIC ---
// Delete record API call
function deleteRecord(id) {
if (!confirm('Are you sure you want to delete this payment tracking record?')) {