Browse Source

changes in dashboard page dynamically load status

master
yoga-agoy 3 months ago
parent
commit
e627949e33
  1. 6
      src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java
  2. 25
      src/main/java/com/gcc_manpower/gccservicepaymenttracker/service/GccServicePaymentTrackerService.java
  3. 379
      src/main/resources/templates/gccservicepaymenttracker/dashboard.html

6
src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java

@ -255,4 +255,10 @@ public class GccServicePaymentTrackerController {
public List<Map<String, Object>> getActiveVendors() {
return paymentTrackerService.getActiveVendors();
}
@GetMapping("/api/statuslist")
@ResponseBody
public List<Map<String, Object>> getStatusList() {
return paymentTrackerService.getStatusList();
}
}

25
src/main/java/com/gcc_manpower/gccservicepaymenttracker/service/GccServicePaymentTrackerService.java

@ -73,10 +73,12 @@ public class GccServicePaymentTrackerService {
StringBuilder sql = new StringBuilder(
"SELECT p.*, "
+ "t.type_name as resolved_type, "
+ "v.vendor_name as resolved_vendor "
+ "v.vendor_name as resolved_vendor, "
+ "s.status_name as resolved_status "
+ "FROM payment_tracking_details p "
+ "LEFT JOIN type_list t ON p.type REGEXP '^[0-9]+$' AND t.id = p.type "
+ "LEFT JOIN vendors_list v ON p.vendor_name REGEXP '^[0-9]+$' AND v.id = p.vendor_name "
+ "LEFT JOIN status_list s ON p.status REGEXP '^[0-9]+$' AND s.id = p.status "
+ "WHERE p.is_active = 1 AND p.is_delete = 0");
List<Object> params = new ArrayList<>();
@ -123,9 +125,16 @@ public class GccServicePaymentTrackerService {
}
if (status != null && !status.trim().isEmpty()) {
sql.append(" AND LOWER(p.status) = ?");
if (status.trim().matches("^[0-9]+$")) {
sql.append(" AND (p.status = ? OR (NOT p.status REGEXP '^[0-9]+$' AND p.status = (SELECT status_name FROM status_list WHERE id = ? LIMIT 1)))");
params.add(status.trim());
params.add(status.trim());
} else {
sql.append(" AND (LOWER(p.status) = ? OR (p.status REGEXP '^[0-9]+$' AND LOWER(s.status_name) = ?))");
params.add(status.trim().toLowerCase());
params.add(status.trim().toLowerCase());
}
}
sql.append(" ORDER BY p.order_by ASC, p.received_date DESC, p.id DESC");
@ -323,4 +332,16 @@ public class GccServicePaymentTrackerService {
return new ArrayList<>();
}
}
public List<Map<String, Object>> getStatusList() {
String sql = "SELECT id, status_name FROM status_list "
+ "WHERE is_active = 1 AND is_delete = 0 "
+ "ORDER BY id ASC, status_name ASC";
try {
return jdbcTemplate.queryForList(sql);
} catch (Exception e) {
System.err.println("Error fetching active vendors: " + e.getMessage());
return new ArrayList<>();
}
}
}

379
src/main/resources/templates/gccservicepaymenttracker/dashboard.html

@ -1024,7 +1024,7 @@
<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>
<tr id="summary-thead-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>
@ -1039,7 +1039,7 @@
</tr>
</thead>
<tbody>
<tr class="fw-bold" style="font-size: 1rem; color: #1e293b;">
<tr class="fw-bold" style="font-size: 1rem; color: #1e293b;" id="summary-tbody-tr">
<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"
@ -1113,7 +1113,7 @@
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>
<tr id="vendor-breakdown-thead-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>
@ -1172,11 +1172,6 @@
<label class="filter-label" for="filter-status">Status</label>
<select id="filter-status" class="filter-input" onchange="loadList()">
<option value="">- All Statuses -</option>
<option value="Awaiting Details">Awaiting Details</option>
<option value="Pending with Dept">Pending with Dept</option>
<option value="Completed">Completed</option>
<option value="No Action">No Action</option>
<option value="Pending">Pending</option>
</select>
</div>
@ -1423,11 +1418,6 @@
<label for="form-status" class="form-label fw-semibold text-secondary">Status <span
class="text-danger">*</span></label>
<select class="form-select" id="form-status" required>
<option value="Awaiting Details">Awaiting Details</option>
<option value="Pending with Dept">Pending with Dept</option>
<option value="Completed">Completed</option>
<option value="No Action">No Action</option>
<option value="Pending">Pending</option>
</select>
</div>
@ -1511,9 +1501,10 @@
let statusChartInstance = null;
let vendorBreakdownData = [];
// Dynamic lists for types and vendors
// Dynamic lists for types, vendors, and statuses
let activeTypes = [];
let activeVendors = [];
let activeStatus = [];
// Date Pickers
// let fromDatePicker, toDatePicker;
@ -1547,19 +1538,59 @@
return type;
}
// Fetch types and vendors from DB endpoints
// Fetch types, vendors, and statuses from DB endpoints
async function loadTypesAndVendors() {
try {
const [typesRes, vendorsRes] = await Promise.all([
const [typesRes, vendorsRes, statusRes] = await Promise.all([
fetch(contextPath + 'gccservicepaymenttracker/api/types?_=' + new Date().getTime()),
fetch(contextPath + 'gccservicepaymenttracker/api/vendors?_=' + new Date().getTime())
fetch(contextPath + 'gccservicepaymenttracker/api/vendors?_=' + new Date().getTime()),
fetch(contextPath + 'gccservicepaymenttracker/api/statuslist?_=' + new Date().getTime())
]);
activeTypes = await typesRes.json();
activeVendors = await vendorsRes.json();
activeStatus = await statusRes.json();
populateDropdowns();
} catch (err) {
console.error("Error loading types or vendors:", err);
console.error("Error loading types, vendors or statuses:", err);
}
}
function getStatusKey(statusName) {
if (!statusName) return 'default';
const name = String(statusName).toLowerCase().trim();
if (name.includes('completed') || name.includes('approved') || name.includes('paid')) {
return 'completed';
}
if (name.includes('awaiting') || name.includes('details')) {
return 'awaiting details';
}
if (name.includes('dept') || name.includes('uac') || name.includes('office')) {
return 'pending with dept';
}
if (name.includes('pending') || name.includes('initiated')) {
return 'pending';
}
if (name.includes('no action') || name.includes('action required') || name.includes('duplicate')) {
return 'no action';
}
return 'default';
}
const statusColors = {
'completed': { bg: '#f0fdf4', text: '#166534', badgeClass: 'text-success' },
'awaiting details': { bg: '#fffbeb', text: '#92400e', badgeClass: 'text-warning' },
'pending with dept': { bg: '#f0f9ff', text: '#075985', badgeClass: 'text-info' },
'pending': { bg: '#fef2f2', text: '#991b1b', badgeClass: 'text-danger' },
'no action': { bg: '#fafaf9', text: '#44403c', badgeClass: 'text-secondary' },
'processed': { bg: '#dbeafe', text: '#1e40af', badgeClass: 'text-primary' },
'approved': { bg: '#d1fae5', text: '#065f46', badgeClass: 'text-success' },
'paid': { bg: '#dcfce7', text: '#166534', badgeClass: 'text-success' },
'rejected': { bg: '#fee2e2', text: '#991b1b', badgeClass: 'text-danger' }
};
function getStatusStyle(statusName) {
const key = getStatusKey(statusName);
return statusColors[key] || { bg: '#f1f5f9', text: '#475569', badgeClass: 'text-secondary' };
}
// Populate selects with dynamic options
@ -1589,6 +1620,30 @@
});
}
// Populate filter-status
const filterStatusSelect = document.getElementById('filter-status');
if (filterStatusSelect) {
filterStatusSelect.innerHTML = '<option value="">- All Statuses -</option>';
activeStatus.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.status_name;
filterStatusSelect.appendChild(opt);
});
}
// Populate form-status
const formStatusSelect = document.getElementById('form-status');
if (formStatusSelect) {
formStatusSelect.innerHTML = '<option value="">-- Select Status --</option>';
activeStatus.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.status_name;
formStatusSelect.appendChild(opt);
});
}
// Populate form-vendor-select
const formVendorSelect = document.getElementById('form-vendor-select');
if (formVendorSelect) {
@ -1604,6 +1659,60 @@
customOpt.textContent = '-- Write-in custom vendor --';
formVendorSelect.appendChild(customOpt);
}
// Dynamically populate summary table headers
const summaryTheadTr = document.getElementById('summary-thead-tr');
if (summaryTheadTr) {
let summaryHeadHtml = `<th style="background-color: #f8fafc; color: #475569;">Total Invoices</th>`;
activeStatus.forEach(s => {
const style = getStatusStyle(s.status_name);
summaryHeadHtml += `<th style="background-color: ${style.bg}; color: ${style.text};">${escapeHtml(s.status_name)}</th>`;
});
summaryHeadHtml += `
<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>
`;
summaryTheadTr.innerHTML = summaryHeadHtml;
}
// Dynamically populate vendor breakdown table headers
const vendorBreakdownTheadTr = document.getElementById('vendor-breakdown-thead-tr');
if (vendorBreakdownTheadTr) {
let vendorHeadHtml = `
<th style="text-align: left; background-color: #f8fafc;">Vendor Name</th>
<th style="background-color: #f8fafc;">Total Invoices</th>
`;
activeStatus.forEach(s => {
const style = getStatusStyle(s.status_name);
vendorHeadHtml += `<th style="background-color: ${style.bg}; color: ${style.text};">${escapeHtml(s.status_name)}</th>`;
});
vendorHeadHtml += `
<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>
`;
vendorBreakdownTheadTr.innerHTML = vendorHeadHtml;
}
}
// Helper to get status name by ID or fallback to status value
function getStatusName(statusVal) {
if (statusVal === null || statusVal === undefined || String(statusVal).trim() === '') {
return '-';
}
const cleanVal = String(statusVal).trim();
const statusObj = activeStatus.find(s => String(s.id) === cleanVal);
if (statusObj) {
return statusObj.status_name;
}
return statusVal;
}
// Show/hide loader overlay
@ -1739,14 +1848,19 @@
// Apply activeClickFilter if set
if (activeClickFilter) {
const age = parseInt(calculateAgeOfBill(row.received_date), 10);
const status = String(row.status || '').trim();
const status = String(row.resolved_status || getStatusName(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;
const filterVal = String(activeClickFilter.value);
const matched = activeStatus.find(s => String(s.id) === filterVal);
if (matched) {
const rowStatusStr = String(row.status || '').trim();
const matches = rowStatusStr === String(matched.id) ||
rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
if (!matches) return false;
} else {
if (status !== activeClickFilter.value) return false;
if (status.toLowerCase() !== filterVal.toLowerCase()) return false;
}
} else if (activeClickFilter.type === 'age') {
const [min, max] = activeClickFilter.value;
@ -1759,10 +1873,19 @@
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.match(/^[0-9]+$/)) {
const matched = activeStatus.find(s => String(s.id) === activeClickFilter.col);
if (matched) {
const rowStatusStr = String(row.status || '').trim();
const rowMatches = rowStatusStr === String(matched.id) ||
rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
if (!rowMatches) return false;
}
} else {
if (activeClickFilter.col === 'completed' && status.toLowerCase() !== 'completed') return false;
if (activeClickFilter.col === 'awaiting' && status.toLowerCase() !== 'awaiting details') return false;
if (activeClickFilter.col === 'pending_dept' && status.toLowerCase() !== 'pending with dept') return false;
if (activeClickFilter.col === 'pending' && (status.toLowerCase() === 'completed' || status.toLowerCase() === 'awaiting details' || status.toLowerCase() === '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;
@ -1773,10 +1896,12 @@
}
}
}
}
if (!query) return true;
const vName = String(row.resolved_vendor || row.vendor_name || '').toLowerCase();
const tName = String(row.resolved_type || row.type || '').toLowerCase();
const statusName = String(row.resolved_status || getStatusName(row.status) || '').toLowerCase();
return (
vName.includes(query) ||
tName.includes(query) ||
@ -1787,7 +1912,7 @@
(row.physical_file_no && String(row.physical_file_no).toLowerCase().includes(query)) ||
(row.remarks && String(row.remarks).toLowerCase().includes(query)) ||
(row.tat && String(row.tat).toLowerCase().includes(query)) ||
(row.status && String(row.status).toLowerCase().includes(query))
statusName.includes(query)
);
});
@ -1901,6 +2026,8 @@
return `<span class="type-badge ${badgeClass}">${label}</span>`;
};
const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim();
tr.innerHTML += `
<td>${formatDate(row.received_date)}</td>
<td>${formatDate(row.invoice_date)}</td>
@ -1911,7 +2038,7 @@
<td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
<td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
<td>${calculateAgeOfBill(row.received_date)}</td>
<td><span class="status-badge status-${String(row.status || '').toLowerCase().trim().replace(/\s+/g, '-')}">${text(row.status)}</span></td>
<td><span class="status-badge status-${dispStatus.toLowerCase().replace(/\s+/g, '-')}">${text(dispStatus)}</span></td>
<td><small class="text-muted">${text(row.remarks)}</small></td>
<td>${formatDateTime(row.updated_date)}</td>
`;
@ -2149,7 +2276,8 @@
}
}
}
document.getElementById('form-status').value = 'Pending';
const pendingObj = activeStatus.find(s => s.status_name.toLowerCase().trim() === 'pending');
document.getElementById('form-status').value = pendingObj ? String(pendingObj.id) : '';
document.getElementById('form-vendor-custom').style.display = 'none';
document.getElementById('form-vendor-custom').required = false;
@ -2212,13 +2340,34 @@
const statusSelect = document.getElementById('form-status');
const rowStatus = String(row.status || '').trim();
statusSelect.value = 'Pending';
statusSelect.value = '';
let statusFound = false;
// 1. Try matching by option value (which can be ID or exact string match)
for (let option of statusSelect.options) {
if (option.value.toLowerCase() === rowStatus.toLowerCase()) {
if (option.value && String(option.value).toLowerCase() === rowStatus.toLowerCase()) {
statusSelect.value = option.value;
statusFound = true;
break;
}
}
// 2. Try matching by option text content (which is the status name)
if (!statusFound && rowStatus) {
for (let option of statusSelect.options) {
if (option.text && option.text.toLowerCase().trim() === rowStatus.toLowerCase().trim()) {
statusSelect.value = option.value;
statusFound = true;
break;
}
}
}
// Fallback default
if (!statusFound) {
const pendingObj = activeStatus.find(s => s.status_name.toLowerCase().trim() === 'pending');
statusSelect.value = pendingObj ? String(pendingObj.id) : '';
}
const followupEl = document.getElementById('form-followup');
if (followupEl) followupEl.value = row.followup || '';
const myfollowupEl = document.getElementById('form-myfollowup');
@ -2402,7 +2551,7 @@
today.setHours(0, 0, 0, 0);
const graceDate = new Date(recDate);
graceDate.setDate(graceDate.getDate() + 7);
graceDate.setDate(graceDate.getDate());
if (today <= graceDate) {
return '0';
@ -2420,10 +2569,12 @@
// --- NEW METRICS & INTERACTIVE CLICK FILTER LOGIC ---
function calculateSummaries() {
let total = allRecords.length;
let completed = 0;
let awaiting = 0;
let dept = 0;
let pending = 0;
// Dynamic Status Counts Map
const statusCounts = {};
activeStatus.forEach(s => {
statusCounts[s.id] = 0;
});
let age_1_15 = 0;
let age_16_30 = 0;
@ -2435,14 +2586,22 @@
const vendorMap = {};
allRecords.forEach(row => {
const status = String(row.status || '').trim();
const statusName = String(row.resolved_status || getStatusName(row.status) || '').trim();
const statusVal = String(row.status || '').trim();
// Find matching status in activeStatus (either by ID or legacy name)
const matchedStatus = activeStatus.find(s =>
String(s.id) === statusVal ||
s.status_name.toLowerCase().trim() === statusName.toLowerCase().trim()
);
const statusId = matchedStatus ? matchedStatus.id : null;
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 (statusId !== null) {
statusCounts[statusId] = (statusCounts[statusId] || 0) + 1;
}
if (age >= 1 && age <= 15) age_1_15++;
else if (age >= 16 && age <= 30) age_16_30++;
@ -2455,10 +2614,7 @@
vendorMap[vendorName] = {
vendorName: vendorName,
total: 0,
completed: 0,
awaiting: 0,
dept: 0,
pending: 0,
statusCounts: {}, // Map status ID to count
age_1_15: 0,
age_16_30: 0,
age_31_60: 0,
@ -2466,14 +2622,16 @@
age_91_120: 0,
age_over_120: 0
};
activeStatus.forEach(s => {
vendorMap[vendorName].statusCounts[s.id] = 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 (statusId !== null) {
vData.statusCounts[statusId] = (vData.statusCounts[statusId] || 0) + 1;
}
if (age >= 1 && age <= 15) vData.age_1_15++;
else if (age >= 16 && age <= 30) vData.age_16_30++;
@ -2483,26 +2641,48 @@
else if (age > 120) vData.age_over_120++;
});
// Populate total
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);
// Dynamically populate body values for dynamic statuses
let summaryBodyHtml = `<td id="summary-total" class="cursor-pointer text-primary" style="background-color: #f8fafc;" onclick="clearClickFilter()">${total}</td>`;
activeStatus.forEach(s => {
const count = statusCounts[s.id] || 0;
const style = getStatusStyle(s.status_name);
summaryBodyHtml += `<td id="summary-status-${s.id}" class="cursor-pointer ${style.badgeClass}" onclick="setClickFilter('status', '${s.id}', 'Status: ' + this.getAttribute('data-status-name'))" data-status-name="${escapeHtml(s.status_name)}">${count}</td>`;
});
summaryBodyHtml += `
<td id="summary-1-15" class="cursor-pointer" onclick="setClickFilter('age', [1, 15], 'Age: 1 to 15 Days')">${age_1_15}</td>
<td id="summary-16-30" class="cursor-pointer" onclick="setClickFilter('age', [16, 30], 'Age: 16 to 30 Days')">${age_16_30}</td>
<td id="summary-31-60" class="cursor-pointer" onclick="setClickFilter('age', [31, 60], 'Age: 31 to 60 Days')">${age_31_60}</td>
<td id="summary-61-90" class="cursor-pointer" onclick="setClickFilter('age', [61, 90], 'Age: 61 to 90 Days')">${age_61_90}</td>
<td id="summary-91-120" class="cursor-pointer" onclick="setClickFilter('age', [91, 120], 'Age: 91 to 120 Days')">${age_91_120}</td>
<td id="summary-over-120" class="cursor-pointer text-danger" onclick="setClickFilter('age', [121, null], 'Age: Over 120 Days')">${age_over_120}</td>
`;
const summaryBbody = document.getElementById('summary-tbody-tr');
if (summaryBbody) {
summaryBbody.innerHTML = summaryBodyHtml;
}
// Render status pie chart dynamically
const chartLabels = activeStatus.map(s => s.status_name);
const chartData = activeStatus.map(s => statusCounts[s.id] || 0);
const chartColors = activeStatus.map(s => {
const key = getStatusKey(s.status_name);
if (key === 'completed') return '#2ecc71';
if (key === 'awaiting details') return '#f1c40f';
if (key === 'pending with dept') return '#3498db';
if (key === 'pending') return '#e74c3c';
if (key === 'no action') return '#95a5a6';
return '#7f8c8d'; // Fallback
});
renderStatusChart(chartLabels, chartData, chartColors);
vendorBreakdownData = Object.values(vendorMap).sort((a, b) => b.total - a.total);
renderVendorBreakdown();
}
function renderStatusChart(completed, awaiting, dept, pending) {
function renderStatusChart(labels, data, colors) {
const ctx = document.getElementById('statusPieChart')?.getContext('2d');
if (!ctx) return;
@ -2513,15 +2693,10 @@
statusChartInstance = new Chart(ctx, {
type: 'pie',
data: {
labels: ['Completed', 'Awaiting Details', 'Pending with Dept', 'Pending'],
labels: labels,
datasets: [{
data: [completed, awaiting, dept, pending],
backgroundColor: [
'#2ecc71',
'#f1c40f',
'#3498db',
'#e74c3c'
],
data: data,
backgroundColor: colors,
borderWidth: 1,
borderColor: '#ffffff'
}]
@ -2556,16 +2731,17 @@
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>`;
const colSpan = 2 + activeStatus.length + 6;
tbody.innerHTML = `<tr><td colspan="${colSpan}" 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;
const sumStatuses = {};
activeStatus.forEach(s => {
sumStatuses[s.id] = 0;
});
let sumAge1_15 = 0;
let sumAge16_30 = 0;
let sumAge31_60 = 0;
@ -2575,10 +2751,9 @@
filtered.forEach(v => {
sumTotal += v.total;
sumCompleted += v.completed;
sumAwaiting += v.awaiting;
sumDept += v.dept;
sumPending += v.pending;
activeStatus.forEach(s => {
sumStatuses[s.id] += (v.statusCounts[s.id] || 0);
});
sumAge1_15 += v.age_1_15;
sumAge16_30 += v.age_16_30;
sumAge31_60 += v.age_31_60;
@ -2587,20 +2762,29 @@
sumAgeOver120 += v.age_over_120;
const tr = document.createElement('tr');
tr.innerHTML = `
// Construct HTML row dynamically
let trHtml = `
<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>
<td class="cursor-pointer text-primary fw-bold" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor'), this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${v.total}</td>
`;
activeStatus.forEach(s => {
const count = v.statusCounts[s.id] || 0;
const style = getStatusStyle(s.status_name);
trHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (${escapeHtml(s.status_name)})', this.getAttribute('data-vendor'), '${s.id}')" data-vendor="${escapeHtml(v.vendorName)}">${count}</td>`;
});
trHtml += `
<td class="cursor-pointer" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (1-15 Days)', this.getAttribute('data-vendor'), 'age_1_15')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_1_15}</td>
<td class="cursor-pointer" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (16-30 Days)', this.getAttribute('data-vendor'), 'age_16_30')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_16_30}</td>
<td class="cursor-pointer" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (31-60 Days)', this.getAttribute('data-vendor'), 'age_31_60')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_31_60}</td>
<td class="cursor-pointer" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (61-90 Days)', this.getAttribute('data-vendor'), 'age_61_90')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_61_90}</td>
<td class="cursor-pointer" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (91-120 Days)', this.getAttribute('data-vendor'), 'age_91_120')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_91_120}</td>
<td class="cursor-pointer text-danger" onclick="setClickFilter('vendor', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (Over 120 Days)', this.getAttribute('data-vendor'), 'age_over_120')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_over_120}</td>
`;
tr.innerHTML = trHtml;
tbody.appendChild(tr);
});
@ -2608,13 +2792,15 @@
const totalTr = document.createElement('tr');
totalTr.className = 'table-warning fw-bold text-dark';
totalTr.style.borderTop = '2px solid #cbd5e1';
totalTr.innerHTML = `
let totalTrHtml = `
<td class="text-start">TOTAL</td>
<td>${sumTotal}</td>
<td>${sumCompleted}</td>
<td>${sumAwaiting}</td>
<td>${sumDept}</td>
<td>${sumPending}</td>
`;
activeStatus.forEach(s => {
totalTrHtml += `<td>${sumStatuses[s.id]}</td>`;
});
totalTrHtml += `
<td>${sumAge1_15}</td>
<td>${sumAge16_30}</td>
<td>${sumAge31_60}</td>
@ -2622,6 +2808,7 @@
<td>${sumAge91_120}</td>
<td class="text-danger">${sumAgeOver120}</td>
`;
totalTr.innerHTML = totalTrHtml;
tbody.appendChild(totalTr);
}

Loading…
Cancel
Save