|
|
|
@ -939,6 +939,19 @@ |
|
|
|
justify-content: center; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* Make modal-xl wider to fit multi-column data tables comfortably without squishing text */ |
|
|
|
@media (min-width: 1200px) { |
|
|
|
.modal-xl { |
|
|
|
max-width: 94% !important; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@media (min-width: 1600px) { |
|
|
|
.modal-xl { |
|
|
|
max-width: 1550px !important; |
|
|
|
} |
|
|
|
} |
|
|
|
</style> |
|
|
|
</head> |
|
|
|
|
|
|
|
@ -1191,7 +1204,7 @@ |
|
|
|
<tr id="vendor-breakdown-thead-tr"> |
|
|
|
<th style="text-align: left; background-color: #f8fafc;">Vendor Name</th> |
|
|
|
<th style="background-color: #f8fafc;">Total No of files</th> |
|
|
|
<th style="background-color: #f0fdf4;">Completed</th> |
|
|
|
<th style="background-color: #f0fdf4;">Completed Files</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> |
|
|
|
@ -1353,7 +1366,7 @@ |
|
|
|
<th>Physical File No.</th> |
|
|
|
<th>eFile No./ Receipt No.</th> |
|
|
|
<th>Subject</th> |
|
|
|
<th>Type (SD/ ASD)</th> |
|
|
|
<th>Type of File</th> |
|
|
|
<th>Vendor Name</th> |
|
|
|
<th>Amount</th> |
|
|
|
<th>Age of Bill (Days)</th> |
|
|
|
@ -1716,7 +1729,7 @@ |
|
|
|
<th>Physical File No.</th> |
|
|
|
<th>eFile No./ Receipt No.</th> |
|
|
|
<th>Subject</th> |
|
|
|
<th>Type (SD/ ASD)</th> |
|
|
|
<th>Type of File</th> |
|
|
|
<th>Vendor Name</th> |
|
|
|
<th>Amount</th> |
|
|
|
<th>Age of Bill (Days)</th> |
|
|
|
@ -1769,7 +1782,7 @@ |
|
|
|
<th>Physical File No.</th> |
|
|
|
<th>eFile No./ Receipt No.</th> |
|
|
|
<th>Subject</th> |
|
|
|
<th>Type (SD/ ASD)</th> |
|
|
|
<th>Type of File</th> |
|
|
|
<th>Vendor Name</th> |
|
|
|
<th>Amount</th> |
|
|
|
<th>Age of Bill (Days)</th> |
|
|
|
@ -1804,6 +1817,7 @@ |
|
|
|
|
|
|
|
let activeCategory = 'all'; |
|
|
|
let allRecords = []; |
|
|
|
let currentFilteredRecords = []; |
|
|
|
let recordModalObj = null; |
|
|
|
let historyModalObj = null; |
|
|
|
let deletedRecordsModalObj = null; |
|
|
|
@ -1864,7 +1878,17 @@ |
|
|
|
]); |
|
|
|
activeTypes = await typesRes.json(); |
|
|
|
activeVendors = await vendorsRes.json(); |
|
|
|
activeStatus = await statusRes.json(); |
|
|
|
const fetchedStatus = await statusRes.json(); |
|
|
|
|
|
|
|
// Sort activeStatus to have completed first, then pending, then others |
|
|
|
const completedStatuses = fetchedStatus.filter(s => getStatusKey(s.status_name) === 'completed'); |
|
|
|
const pendingStatuses = fetchedStatus.filter(s => getStatusKey(s.status_name) === 'pending'); |
|
|
|
const otherStatuses = fetchedStatus.filter(s => |
|
|
|
getStatusKey(s.status_name) !== 'completed' && |
|
|
|
getStatusKey(s.status_name) !== 'pending' |
|
|
|
); |
|
|
|
activeStatus = [...completedStatuses, ...pendingStatuses, ...otherStatuses]; |
|
|
|
|
|
|
|
populateDropdowns(); |
|
|
|
} catch (err) { |
|
|
|
console.error("Error loading types, vendors or statuses:", err); |
|
|
|
@ -1880,7 +1904,7 @@ |
|
|
|
if (name.includes('awaiting') || name.includes('details')) { |
|
|
|
return 'awaiting details'; |
|
|
|
} |
|
|
|
if (name.includes('dept') || name.includes('uac') || name.includes('office')) { |
|
|
|
if (name.includes('dept') || name.includes('uac') || name.includes('office') || name.includes('fm unit')) { |
|
|
|
return 'pending with dept'; |
|
|
|
} |
|
|
|
if (name.includes('pending') || name.includes('initiated')) { |
|
|
|
@ -2005,11 +2029,22 @@ |
|
|
|
// Dynamically populate vendor breakdown table headers |
|
|
|
const vendorBreakdownTheadTr = document.getElementById('vendor-breakdown-thead-tr'); |
|
|
|
if (vendorBreakdownTheadTr) { |
|
|
|
const completedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed'); |
|
|
|
const nonCompletedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) !== 'completed'); |
|
|
|
|
|
|
|
let vendorHeadHtml = ` |
|
|
|
<th style="text-align: left; background-color: #f8fafc;">Vendor Name</th> |
|
|
|
<th style="background-color: #f8fafc;">Total No of files</th> |
|
|
|
`; |
|
|
|
activeStatus.forEach(s => { |
|
|
|
completedStatuses.forEach(s => { |
|
|
|
const style = getStatusStyle(s.status_name); |
|
|
|
vendorHeadHtml += `<th style="background-color: ${style.bg}; color: ${style.text};">${escapeHtml(s.status_name)}</th>`; |
|
|
|
}); |
|
|
|
|
|
|
|
// Custom Pending column header |
|
|
|
vendorHeadHtml += `<th style="background-color: #fef2f2; color: #991b1b;">Pending files</th>`; |
|
|
|
|
|
|
|
nonCompletedStatuses.forEach(s => { |
|
|
|
const style = getStatusStyle(s.status_name); |
|
|
|
vendorHeadHtml += `<th style="background-color: ${style.bg}; color: ${style.text};">${escapeHtml(s.status_name)}</th>`; |
|
|
|
}); |
|
|
|
@ -2218,7 +2253,6 @@ |
|
|
|
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; |
|
|
|
@ -2249,6 +2283,8 @@ |
|
|
|
); |
|
|
|
}); |
|
|
|
|
|
|
|
currentFilteredRecords = filtered; |
|
|
|
|
|
|
|
showLoader(false); |
|
|
|
|
|
|
|
const totalRecords = filtered.length; |
|
|
|
@ -3077,8 +3113,11 @@ |
|
|
|
|
|
|
|
const filtered = vendorBreakdownData.filter(v => v.vendorName.toLowerCase().includes(query)); |
|
|
|
|
|
|
|
const completedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed'); |
|
|
|
const nonCompletedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) !== 'completed'); |
|
|
|
|
|
|
|
if (filtered.length === 0) { |
|
|
|
const colSpan = 2 + activeStatus.length + 6; |
|
|
|
const colSpan = 2 + activeStatus.length + 1 + 6; |
|
|
|
tbody.innerHTML = `<tr><td colspan="${colSpan}" class="text-secondary py-3">No matching vendors found</td></tr>`; |
|
|
|
return; |
|
|
|
} |
|
|
|
@ -3116,7 +3155,21 @@ |
|
|
|
<td class="cursor-pointer text-primary fw-bold" onclick="openModalForCount('vendor_total', null, 'Vendor: ' + this.getAttribute('data-vendor'), this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${v.total}</td> |
|
|
|
`; |
|
|
|
|
|
|
|
activeStatus.forEach(s => { |
|
|
|
// 1. Completed statuses |
|
|
|
let totalCompleted = 0; |
|
|
|
completedStatuses.forEach(s => { |
|
|
|
const count = v.statusCounts[s.id] || 0; |
|
|
|
totalCompleted += count; |
|
|
|
const style = getStatusStyle(s.status_name); |
|
|
|
trHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('vendor_status', '${s.id}', 'Vendor: ' + this.getAttribute('data-vendor') + ' (${escapeHtml(s.status_name)})', this.getAttribute('data-vendor'), '${s.id}')" data-vendor="${escapeHtml(v.vendorName)}">${count}</td>`; |
|
|
|
}); |
|
|
|
|
|
|
|
// 2. Custom "Pending" column |
|
|
|
const pendingCount = v.total - totalCompleted; |
|
|
|
trHtml += `<td class="cursor-pointer text-danger fw-bold" onclick="openModalForCount('vendor_pending_custom', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (Pending)', this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${pendingCount}</td>`; |
|
|
|
|
|
|
|
// 3. Non-completed statuses |
|
|
|
nonCompletedStatuses.forEach(s => { |
|
|
|
const count = v.statusCounts[s.id] || 0; |
|
|
|
const style = getStatusStyle(s.status_name); |
|
|
|
trHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('vendor_status', '${s.id}', 'Vendor: ' + this.getAttribute('data-vendor') + ' (${escapeHtml(s.status_name)})', this.getAttribute('data-vendor'), '${s.id}')" data-vendor="${escapeHtml(v.vendorName)}">${count}</td>`; |
|
|
|
@ -3144,10 +3197,25 @@ |
|
|
|
<td class="text-start">TOTAL</td> |
|
|
|
<td class="cursor-pointer text-primary fw-bold" onclick="openModalForCount('total', null, 'Total No of Files')">${sumTotal}</td> |
|
|
|
`; |
|
|
|
activeStatus.forEach(s => { |
|
|
|
|
|
|
|
// 1. Completed statuses |
|
|
|
let sumCompleted = 0; |
|
|
|
completedStatuses.forEach(s => { |
|
|
|
const style = getStatusStyle(s.status_name); |
|
|
|
sumCompleted += sumStatuses[s.id]; |
|
|
|
totalTrHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('status', '${s.id}', 'Status: ' + this.getAttribute('data-status-name'))" data-status-name="${escapeHtml(s.status_name)}">${sumStatuses[s.id]}</td>`; |
|
|
|
}); |
|
|
|
|
|
|
|
// 2. Custom "Pending" column |
|
|
|
const sumPending = sumTotal - sumCompleted; |
|
|
|
totalTrHtml += `<td class="cursor-pointer text-danger fw-bold" onclick="openModalForCount('pending_custom', null, 'Pending Files')">${sumPending}</td>`; |
|
|
|
|
|
|
|
// 3. Non-completed statuses |
|
|
|
nonCompletedStatuses.forEach(s => { |
|
|
|
const style = getStatusStyle(s.status_name); |
|
|
|
totalTrHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('status', '${s.id}', 'Status: ' + this.getAttribute('data-status-name'))" data-status-name="${escapeHtml(s.status_name)}">${sumStatuses[s.id]}</td>`; |
|
|
|
}); |
|
|
|
|
|
|
|
totalTrHtml += ` |
|
|
|
<td class="cursor-pointer" onclick="openModalForCount('age', [1, 15], 'Age: 1-15 Days')">${sumAge1_15}</td> |
|
|
|
<td class="cursor-pointer" onclick="openModalForCount('age', [16, 30], 'Age: 16-30 Days')">${sumAge16_30}</td> |
|
|
|
@ -3335,6 +3403,27 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (clickType === 'vendor_pending_custom') { |
|
|
|
const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim(); |
|
|
|
if (rowVendor !== vendorName) return false; |
|
|
|
|
|
|
|
const rowStatusStr = String(row.status || '').trim(); |
|
|
|
const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s => |
|
|
|
rowStatusStr === String(s.id) || |
|
|
|
rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim() |
|
|
|
); |
|
|
|
if (isCompleted) return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (clickType === 'pending_custom') { |
|
|
|
const rowStatusStr = String(row.status || '').trim(); |
|
|
|
const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s => |
|
|
|
rowStatusStr === String(s.id) || |
|
|
|
rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim() |
|
|
|
); |
|
|
|
if (isCompleted) return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (clickType === 'vendor_total') { |
|
|
|
const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim(); |
|
|
|
if (rowVendor !== vendorName) return false; |
|
|
|
@ -3621,7 +3710,124 @@ |
|
|
|
} |
|
|
|
|
|
|
|
function exportMainTableToExcel() { |
|
|
|
exportTableToExcelHelper('tracking-table', 'Payment_Tracking_Details_Main_Report.xlsx'); |
|
|
|
if (!currentFilteredRecords || currentFilteredRecords.length === 0) { |
|
|
|
alert("No records to export."); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Create a temporary table element in memory |
|
|
|
const tempTable = document.createElement('table'); |
|
|
|
|
|
|
|
// 1. Create head |
|
|
|
const thead = document.createElement('thead'); |
|
|
|
thead.innerHTML = ` |
|
|
|
<tr> |
|
|
|
<th>Sr. No.</th> |
|
|
|
<th>Received Date</th> |
|
|
|
<th>Invoice Date</th> |
|
|
|
<th>Physical File No.</th> |
|
|
|
<th>eFile No./ Receipt No.</th> |
|
|
|
<th>Subject</th> |
|
|
|
<th>Type of File</th> |
|
|
|
<th>Vendor Name</th> |
|
|
|
<th>Amount</th> |
|
|
|
<th>Age of Bill (Days)</th> |
|
|
|
<th>Status (Pending / Completed)</th> |
|
|
|
<th>Remarks</th> |
|
|
|
<th>Last Updated At</th> |
|
|
|
<th>Last Updated By</th> |
|
|
|
</tr> |
|
|
|
`; |
|
|
|
tempTable.appendChild(thead); |
|
|
|
|
|
|
|
// Helpers |
|
|
|
const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val; |
|
|
|
|
|
|
|
const formatDate = dateStr => { |
|
|
|
if (!dateStr) return '-'; |
|
|
|
const parts = dateStr.split('-'); |
|
|
|
if (parts.length === 3) { |
|
|
|
return `${parts[2]}/${parts[1]}/${parts[0]}`; |
|
|
|
} |
|
|
|
return dateStr; |
|
|
|
}; |
|
|
|
|
|
|
|
const formatDateTime = dateTimeStr => { |
|
|
|
if (!dateTimeStr) return '-'; |
|
|
|
try { |
|
|
|
const d = new Date(dateTimeStr); |
|
|
|
if (!isNaN(d.getTime())) { |
|
|
|
const dd = String(d.getDate()).padStart(2, '0'); |
|
|
|
const MM = String(d.getMonth() + 1).padStart(2, '0'); |
|
|
|
const yyyy = d.getFullYear(); |
|
|
|
const HH = String(d.getHours()).padStart(2, '0'); |
|
|
|
const mm = String(d.getMinutes()).padStart(2, '0'); |
|
|
|
const ss = String(d.getSeconds()).padStart(2, '0'); |
|
|
|
return `${dd}/${MM}/${yyyy} ${HH}:${mm}:${ss}`; |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.error(e); |
|
|
|
} |
|
|
|
return dateTimeStr; |
|
|
|
}; |
|
|
|
|
|
|
|
const getTypeLabel = typeVal => { |
|
|
|
const cleanType = String(typeVal || '').trim().toLowerCase(); |
|
|
|
let label = cleanType; |
|
|
|
|
|
|
|
if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) { |
|
|
|
label = 'ASD'; |
|
|
|
} else if (cleanType.includes('agreement')) { |
|
|
|
label = 'Agreement'; |
|
|
|
} else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) { |
|
|
|
label = 'Security Deposit'; |
|
|
|
} else if (cleanType.includes('invoice') || cleanType.includes('bill')) { |
|
|
|
label = 'Tax Invoice'; |
|
|
|
} else if (cleanType === 'emd' || cleanType.includes('earnest')) { |
|
|
|
label = 'EMD'; |
|
|
|
} else if (cleanType.includes('guarantee') || cleanType === 'bg') { |
|
|
|
label = 'Bank Guarantee'; |
|
|
|
} else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') { |
|
|
|
label = 'Delivery Chellan'; |
|
|
|
} else { |
|
|
|
const typeObj = activeTypes.find(t => String(t.id) === cleanType); |
|
|
|
if (typeObj) { |
|
|
|
return typeObj.type_name; |
|
|
|
} |
|
|
|
label = typeVal || '-'; |
|
|
|
} |
|
|
|
return label; |
|
|
|
}; |
|
|
|
|
|
|
|
// 2. Create body |
|
|
|
const tbody = document.createElement('tbody'); |
|
|
|
currentFilteredRecords.forEach((row, index) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim(); |
|
|
|
|
|
|
|
tr.innerHTML = ` |
|
|
|
<td>${index + 1}</td> |
|
|
|
<td>${formatDate(row.received_date)}</td> |
|
|
|
<td>${formatDate(row.invoice_date)}</td> |
|
|
|
<td>${text(row.physical_file_no)}</td> |
|
|
|
<td>${text(row.receiptno)}</td> |
|
|
|
<td>${text(row.subject)}</td> |
|
|
|
<td>${getTypeLabel(row.resolved_type || row.type)}</td> |
|
|
|
<td>${text(row.resolved_vendor || row.vendor_name)}</td> |
|
|
|
<td>${text(row.amount)}</td> |
|
|
|
<td>${calculateAgeOfBill(row.received_date)}</td> |
|
|
|
<td>${text(dispStatus)}</td> |
|
|
|
<td>${text(row.remarks)}</td> |
|
|
|
<td>${formatDateTime(row.updated_date)}</td> |
|
|
|
<td>${text(row.updated_by_name || '-')}</td> |
|
|
|
`; |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
tempTable.appendChild(tbody); |
|
|
|
|
|
|
|
// Convert to Excel workbook and download |
|
|
|
const wb = XLSX.utils.table_to_book(tempTable, { raw: true, sheet: "Sheet1" }); |
|
|
|
XLSX.writeFile(wb, 'Payment_Tracking_Details_Main_Report.xlsx'); |
|
|
|
} |
|
|
|
|
|
|
|
// --- LATEST UPDATES CORNER BADGE LOGIC --- |
|
|
|
|