|
|
|
@ -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,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 = '<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,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 `<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">₹ ${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; |
|
|
|
// 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; |
|
|
|
} |
|
|
|
|
|
|
|
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 = `<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); |
|
|
|
} |
|
|
|
|
|
|
|
|