From e627949e339346ff09fe9d9041358ef7a1799a4a Mon Sep 17 00:00:00 2001 From: yoga-agoy Date: Fri, 3 Jul 2026 16:37:48 +0530 Subject: [PATCH] changes in dashboard page dynamically load status --- .../GccServicePaymentTrackerController.java | 6 + .../GccServicePaymentTrackerService.java | 27 +- .../gccservicepaymenttracker/dashboard.html | 391 +++++++++++++----- 3 files changed, 319 insertions(+), 105 deletions(-) diff --git a/src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java b/src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java index b1e75c7..671ddef 100644 --- a/src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java +++ b/src/main/java/com/gcc_manpower/gccservicepaymenttracker/controller/GccServicePaymentTrackerController.java @@ -255,4 +255,10 @@ public class GccServicePaymentTrackerController { public List> getActiveVendors() { return paymentTrackerService.getActiveVendors(); } + + @GetMapping("/api/statuslist") + @ResponseBody + public List> getStatusList() { + return paymentTrackerService.getStatusList(); + } } diff --git a/src/main/java/com/gcc_manpower/gccservicepaymenttracker/service/GccServicePaymentTrackerService.java b/src/main/java/com/gcc_manpower/gccservicepaymenttracker/service/GccServicePaymentTrackerService.java index 217a383..b6d6646 100644 --- a/src/main/java/com/gcc_manpower/gccservicepaymenttracker/service/GccServicePaymentTrackerService.java +++ b/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 params = new ArrayList<>(); @@ -123,8 +125,15 @@ public class GccServicePaymentTrackerService { } if (status != null && !status.trim().isEmpty()) { - sql.append(" AND LOWER(p.status) = ?"); - params.add(status.trim().toLowerCase()); + 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> 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<>(); + } + } } diff --git a/src/main/resources/templates/gccservicepaymenttracker/dashboard.html b/src/main/resources/templates/gccservicepaymenttracker/dashboard.html index aae7ea4..30690c7 100644 --- a/src/main/resources/templates/gccservicepaymenttracker/dashboard.html +++ b/src/main/resources/templates/gccservicepaymenttracker/dashboard.html @@ -1024,7 +1024,7 @@ - + @@ -1039,7 +1039,7 @@ - + - + @@ -1172,11 +1172,6 @@ @@ -1423,11 +1418,6 @@ @@ -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,21 +1538,61 @@ 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 function populateDropdowns() { // Populate filter-type @@ -1589,6 +1620,30 @@ }); } + // Populate filter-status + const filterStatusSelect = document.getElementById('filter-status'); + if (filterStatusSelect) { + filterStatusSelect.innerHTML = ''; + 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 = ''; + 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 = ``; + activeStatus.forEach(s => { + const style = getStatusStyle(s.status_name); + summaryHeadHtml += ``; + }); + summaryHeadHtml += ` + + + + + + + `; + summaryTheadTr.innerHTML = summaryHeadHtml; + } + + // Dynamically populate vendor breakdown table headers + const vendorBreakdownTheadTr = document.getElementById('vendor-breakdown-thead-tr'); + if (vendorBreakdownTheadTr) { + let vendorHeadHtml = ` + + + `; + activeStatus.forEach(s => { + const style = getStatusStyle(s.status_name); + vendorHeadHtml += ``; + }); + vendorHeadHtml += ` + + + + + + + `; + 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,17 +1873,27 @@ 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 (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; + 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; + } } } } @@ -1777,6 +1901,7 @@ 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 `${label}`; }; + const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim(); + tr.innerHTML += ` @@ -1911,7 +2038,7 @@ - + `; @@ -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; + // Dynamically populate body values for dynamic statuses + let summaryBodyHtml = ``; + activeStatus.forEach(s => { + const count = statusCounts[s.id] || 0; + const style = getStatusStyle(s.status_name); + summaryBodyHtml += ``; + }); + summaryBodyHtml += ` + + + + + + + `; + const summaryBbody = document.getElementById('summary-tbody-tr'); + if (summaryBbody) { + summaryBbody.innerHTML = summaryBodyHtml; + } - renderStatusChart(completed, awaiting, dept, pending); + // 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 = ``; + const colSpan = 2 + activeStatus.length + 6; + tbody.innerHTML = ``; 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 = ` - - - - - - - - - - - + `; + + activeStatus.forEach(s => { + const count = v.statusCounts[s.id] || 0; + const style = getStatusStyle(s.status_name); + trHtml += ``; + }); + + trHtml += ` + + + + + + + `; + + 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 = ` - - - - + `; + activeStatus.forEach(s => { + totalTrHtml += ``; + }); + totalTrHtml += ` @@ -2622,6 +2808,7 @@ `; + totalTr.innerHTML = totalTrHtml; tbody.appendChild(totalTr); }
Total Invoices Completed Awaiting Details
0
Vendor Name Total Invoices Completed Total Invoices${escapeHtml(s.status_name)}1 to 15 Days16 to 30 Days31 to 60 Days61 to 90 Days91 to 120 DaysOver 120 DaysVendor NameTotal Invoices${escapeHtml(s.status_name)}1 to 15 Days16 to 30 Days31 to 60 Days61 to 90 Days91 to 120 DaysOver 120 Days${formatDate(row.received_date)} ${formatDate(row.invoice_date)}${text(row.resolved_vendor || row.vendor_name)} ₹ ${text(row.amount)} ${calculateAgeOfBill(row.received_date)}${text(row.status)}${text(dispStatus)} ${text(row.remarks)} ${formatDateTime(row.updated_date)}${total}${count}${age_1_15}${age_16_30}${age_31_60}${age_61_90}${age_91_120}${age_over_120}
No matching vendors found
No matching vendors found
${escapeHtml(v.vendorName)}${v.total}${v.completed}${v.awaiting}${v.dept}${v.pending}${v.age_1_15}${v.age_16_30}${v.age_31_60}${v.age_61_90}${v.age_91_120}${v.age_over_120}${v.total}${count}${v.age_1_15}${v.age_16_30}${v.age_31_60}${v.age_61_90}${v.age_91_120}${v.age_over_120}TOTAL ${sumTotal}${sumCompleted}${sumAwaiting}${sumDept}${sumPending}${sumStatuses[s.id]}${sumAge1_15} ${sumAge16_30} ${sumAge31_60}${sumAge91_120} ${sumAgeOver120}