|
|
|
@ -1212,7 +1212,7 @@ |
|
|
|
<label class="filter-label" for="filter-from-date">From Date</label> |
|
|
|
<input type="text" id="filter-from-date" class="filter-input" placeholder="dd/mm/yyyy" readonly> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="filter-group"> |
|
|
|
<label class="filter-label" for="filter-to-date">To Date</label> |
|
|
|
<input type="text" id="filter-to-date" class="filter-input" placeholder="dd/mm/yyyy" readonly> |
|
|
|
@ -1278,6 +1278,12 @@ |
|
|
|
onclick="exportMainTableToExcel()"> |
|
|
|
<i class="fa-solid fa-file-excel"></i> Export to Excel |
|
|
|
</button> |
|
|
|
<button th:if="${isAdmin}" |
|
|
|
class="btn btn-sm btn-danger fw-bold px-3 d-flex align-items-center gap-2" |
|
|
|
style="border-radius: 8px; height: 36px; font-size: 0.85rem; margin: 0;" |
|
|
|
onclick="openDeletedRecordsModal()"> |
|
|
|
<i class="fa-solid fa-trash-can"></i> Deleted Records |
|
|
|
</button> |
|
|
|
<button th:if="${isAdmin}" class="btn-add-record" onclick="openAddModal()" |
|
|
|
style="margin: 0; height: 36px;"> |
|
|
|
<i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record |
|
|
|
@ -1672,6 +1678,14 @@ |
|
|
|
<option value="all">- All Vendors -</option> |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="col-md-3"> |
|
|
|
<label class="form-label fw-bold text-secondary text-uppercase" |
|
|
|
style="font-size: 0.75rem; letter-spacing: 0.5px;">Search:</label> |
|
|
|
<input type="text" id="modal-table-search" class="form-control form-control-sm" |
|
|
|
placeholder="Search in this table..." oninput="applyModalFilters()"> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="col-md-3"> |
|
|
|
<button |
|
|
|
class="btn btn-sm btn-success fw-bold px-3 d-inline-flex align-items-center gap-2 w-100 justify-content-center" |
|
|
|
@ -1721,7 +1735,58 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Bootstrap & FontAwesome bundles --> |
|
|
|
<!-- Deleted Records Modal --> |
|
|
|
<div class="modal fade" id="deletedRecordsModal" tabindex="-1" aria-labelledby="deletedRecordsModalLabel" |
|
|
|
aria-hidden="true"> |
|
|
|
<div class="modal-dialog modal-xl modal-dialog-centered"> |
|
|
|
<div class="modal-content"> |
|
|
|
<div class="modal-custom-header" style="background-color: #dc3545;"> |
|
|
|
<h5 class="modal-custom-title" id="deletedRecordsModalLabel"> |
|
|
|
<i class="fa-solid fa-trash-can"></i> Deleted Payment Records |
|
|
|
</h5> |
|
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" |
|
|
|
aria-label="Close"></button> |
|
|
|
</div> |
|
|
|
<div class="modal-body" style="padding: 1.5rem;"> |
|
|
|
<!-- Responsive Table Inside Modal --> |
|
|
|
<div class="table-responsive" |
|
|
|
style="max-height: 450px; overflow-y: auto; border: 1px solid #eef2f6;"> |
|
|
|
<table class="table grid-table table-striped table-hover align-middle mb-0" |
|
|
|
id="deleted-tracking-table"> |
|
|
|
<thead class="table-light"> |
|
|
|
<tr> |
|
|
|
<th class="text-center" style="width: 50px;">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 (SD/ ASD)</th> |
|
|
|
<th>Vendor Name</th> |
|
|
|
<th>Amount</th> |
|
|
|
<th>Age of Bill (Days)</th> |
|
|
|
<th>Status</th> |
|
|
|
<th>Deletion Remarks</th> |
|
|
|
<th>Deleted Date</th> |
|
|
|
<th>Deleted By</th> |
|
|
|
<th class="text-center" style="width: 100px;">Actions</th> |
|
|
|
</tr> |
|
|
|
</thead> |
|
|
|
<tbody id="deleted-tracking-tbody"> |
|
|
|
<!-- Populated dynamically via JS --> |
|
|
|
</tbody> |
|
|
|
</table> |
|
|
|
<div id="deleted-no-records-found" class="empty-state text-center py-5" style="display: none;"> |
|
|
|
<i class="fa-solid fa-trash-arrow-up empty-icon" |
|
|
|
style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i> |
|
|
|
<h5>No Deleted Records</h5> |
|
|
|
<p>No deleted payment tracking records found in the database.</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> |
|
|
|
|
|
|
|
<!-- Custom Page Scripting --> |
|
|
|
@ -1733,6 +1798,7 @@ |
|
|
|
let allRecords = []; |
|
|
|
let recordModalObj = null; |
|
|
|
let historyModalObj = null; |
|
|
|
let deletedRecordsModalObj = null; |
|
|
|
let currentPage = 1; |
|
|
|
let pageSize = 20; |
|
|
|
|
|
|
|
@ -1752,6 +1818,7 @@ |
|
|
|
document.addEventListener('DOMContentLoaded', async () => { |
|
|
|
recordModalObj = new bootstrap.Modal(document.getElementById('recordModal')); |
|
|
|
historyModalObj = new bootstrap.Modal(document.getElementById('historyModal')); |
|
|
|
deletedRecordsModalObj = new bootstrap.Modal(document.getElementById('deletedRecordsModal')); |
|
|
|
// initDatePickers(); |
|
|
|
await loadTypesAndVendors(); |
|
|
|
loadCounts(); |
|
|
|
@ -3139,6 +3206,8 @@ |
|
|
|
modalTypeSelect.value = 'all'; |
|
|
|
modalStatusSelect.value = 'all'; |
|
|
|
modalVendorSelect.value = 'all'; |
|
|
|
const modalSearchInput = document.getElementById('modal-table-search'); |
|
|
|
if (modalSearchInput) modalSearchInput.value = ''; |
|
|
|
|
|
|
|
// If click was category/type-based |
|
|
|
if (clickType === 'category' && clickValue) { |
|
|
|
@ -3220,6 +3289,8 @@ |
|
|
|
const selectedType = document.getElementById('modal-filter-type').value; |
|
|
|
const selectedStatus = document.getElementById('modal-filter-status').value; |
|
|
|
const selectedVendor = document.getElementById('modal-filter-vendor').value; |
|
|
|
const searchVal = document.getElementById('modal-table-search')?.value || ''; |
|
|
|
const query = searchVal.toLowerCase().trim(); |
|
|
|
|
|
|
|
const { clickType, clickValue, vendorName, col } = modalClickFilter; |
|
|
|
|
|
|
|
@ -3316,6 +3387,26 @@ |
|
|
|
if (rowVendor.toLowerCase() !== selectedVendor.toLowerCase()) return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 3. Apply the modal search keyword |
|
|
|
if (query) { |
|
|
|
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(); |
|
|
|
const matchesSearch = ( |
|
|
|
vName.includes(query) || |
|
|
|
tName.includes(query) || |
|
|
|
(row.subject && String(row.subject).toLowerCase().includes(query)) || |
|
|
|
(row.amount && String(row.amount).toLowerCase().includes(query)) || |
|
|
|
(row.invoice_no && String(row.invoice_no).toLowerCase().includes(query)) || |
|
|
|
(row.receiptno && String(row.receiptno).toLowerCase().includes(query)) || |
|
|
|
(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)) || |
|
|
|
statusName.includes(query) |
|
|
|
); |
|
|
|
if (!matchesSearch) return false; |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
}); |
|
|
|
|
|
|
|
@ -3796,6 +3887,191 @@ |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// --- DELETED RECORDS MODAL & RESTORE LOGIC --- |
|
|
|
function openDeletedRecordsModal() { |
|
|
|
if (!deletedRecordsModalObj) { |
|
|
|
deletedRecordsModalObj = new bootstrap.Modal(document.getElementById('deletedRecordsModal')); |
|
|
|
} |
|
|
|
deletedRecordsModalObj.show(); |
|
|
|
showLoader(true, 'Fetching deleted records...'); |
|
|
|
|
|
|
|
fetch(contextPath + 'gccservicepaymenttracker/api/deleted-list?_=' + new Date().getTime()) |
|
|
|
.then(response => response.json()) |
|
|
|
.then(data => { |
|
|
|
renderDeletedRecordsTable(data); |
|
|
|
showLoader(false); |
|
|
|
}) |
|
|
|
.catch(err => { |
|
|
|
console.error("Fetch Deleted Records Error:", err); |
|
|
|
alert("Failed to load deleted records."); |
|
|
|
showLoader(false); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function renderDeletedRecordsTable(data) { |
|
|
|
const tbody = document.getElementById('deleted-tracking-tbody'); |
|
|
|
const noRecords = document.getElementById('deleted-no-records-found'); |
|
|
|
|
|
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
|
|
if (!data || data.length === 0) { |
|
|
|
noRecords.style.display = 'block'; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
noRecords.style.display = 'none'; |
|
|
|
|
|
|
|
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())) return dateTimeStr; |
|
|
|
const pad = n => String(n).padStart(2, '0'); |
|
|
|
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`; |
|
|
|
} catch (e) { |
|
|
|
return dateTimeStr; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const getTypeBadge = typeVal => { |
|
|
|
const cleanType = String(typeVal || '').trim().toLowerCase(); |
|
|
|
let label = cleanType; |
|
|
|
let badgeClass = 'type-sd'; |
|
|
|
|
|
|
|
if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) { |
|
|
|
label = 'ASD'; |
|
|
|
badgeClass = 'type-asd'; |
|
|
|
} else if (cleanType.includes('agreement')) { |
|
|
|
label = 'Agreement'; |
|
|
|
badgeClass = 'type-agreement'; |
|
|
|
} else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) { |
|
|
|
label = 'Security Deposit'; |
|
|
|
badgeClass = 'type-sd'; |
|
|
|
} else if (cleanType.includes('invoice') || cleanType.includes('bill')) { |
|
|
|
label = 'Tax Invoice'; |
|
|
|
badgeClass = 'type-tax-invoice'; |
|
|
|
} else if (cleanType === 'emd' || cleanType.includes('earnest')) { |
|
|
|
label = 'EMD'; |
|
|
|
badgeClass = 'type-emd'; |
|
|
|
} else if (cleanType.includes('guarantee') || cleanType === 'bg') { |
|
|
|
label = 'Bank Guarantee'; |
|
|
|
badgeClass = 'type-bank-guarantee'; |
|
|
|
} else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') { |
|
|
|
label = 'Delivery Chellan'; |
|
|
|
badgeClass = 'type-delivery-chellan'; |
|
|
|
} else { |
|
|
|
const typeObj = activeTypes.find(t => String(t.id) === cleanType); |
|
|
|
if (typeObj) { |
|
|
|
return getTypeBadge(typeObj.type_name); |
|
|
|
} |
|
|
|
label = typeVal || '-'; |
|
|
|
badgeClass = 'status-default'; |
|
|
|
} |
|
|
|
return `<span class="type-badge ${badgeClass}">${label}</span>`; |
|
|
|
}; |
|
|
|
|
|
|
|
data.forEach((row, index) => { |
|
|
|
const tr = document.createElement('tr'); |
|
|
|
|
|
|
|
const tdIndex = document.createElement('td'); |
|
|
|
tdIndex.className = 'text-center fw-semibold text-secondary'; |
|
|
|
tdIndex.textContent = index + 1; |
|
|
|
tr.appendChild(tdIndex); |
|
|
|
|
|
|
|
const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim(); |
|
|
|
|
|
|
|
tr.innerHTML += ` |
|
|
|
<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>${getTypeBadge(row.resolved_type || row.type)}</td> |
|
|
|
<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-${dispStatus.toLowerCase().replace(/\s+/g, '-')}">${text(dispStatus)}</span></td> |
|
|
|
<td><small class="text-danger fw-semibold">${text(row.remarks)}</small></td> |
|
|
|
<td>${formatDateTime(row.updated_date)}</td> |
|
|
|
<td class="fw-semibold text-secondary">${text(row.updated_by_name)}</td> |
|
|
|
`; |
|
|
|
|
|
|
|
// Actions cell |
|
|
|
const tdActions = document.createElement('td'); |
|
|
|
tdActions.className = 'text-center'; |
|
|
|
tdActions.style.whiteSpace = 'nowrap'; |
|
|
|
|
|
|
|
// Restore Button |
|
|
|
const btnRestore = document.createElement('button'); |
|
|
|
btnRestore.className = 'btn btn-sm btn-outline-warning d-inline-flex align-items-center gap-1'; |
|
|
|
btnRestore.innerHTML = '<i class="fa-solid fa-trash-arrow-up"></i> Restore'; |
|
|
|
btnRestore.style.fontSize = '0.72rem'; |
|
|
|
btnRestore.style.fontWeight = '600'; |
|
|
|
btnRestore.title = 'Restore Payment Record'; |
|
|
|
btnRestore.onclick = () => { |
|
|
|
restoreDeletedRecord(row.id); |
|
|
|
}; |
|
|
|
tdActions.appendChild(btnRestore); |
|
|
|
|
|
|
|
tr.appendChild(tdActions); |
|
|
|
tbody.appendChild(tr); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function restoreDeletedRecord(id) { |
|
|
|
if (!confirm("Are you sure you want to restore this payment record?")) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (deletedRecordsModalObj) { |
|
|
|
deletedRecordsModalObj.hide(); |
|
|
|
} |
|
|
|
|
|
|
|
showLoader(true, 'Restoring record...'); |
|
|
|
|
|
|
|
const csrfHeaders = getCsrfHeaders(); |
|
|
|
const headers = { |
|
|
|
'Content-Type': 'application/x-www-form-urlencoded', |
|
|
|
...csrfHeaders |
|
|
|
}; |
|
|
|
|
|
|
|
const body = new URLSearchParams(); |
|
|
|
body.append('id', id); |
|
|
|
|
|
|
|
fetch(contextPath + 'gccservicepaymenttracker/api/restore', { |
|
|
|
method: 'POST', |
|
|
|
headers: headers, |
|
|
|
body: body |
|
|
|
}) |
|
|
|
.then(response => response.json()) |
|
|
|
.then(data => { |
|
|
|
if (data.success) { |
|
|
|
alert('Record successfully restored!'); |
|
|
|
loadCounts(); |
|
|
|
loadList(); |
|
|
|
} else { |
|
|
|
alert(data.error || 'Failed to restore record.'); |
|
|
|
showLoader(false); |
|
|
|
} |
|
|
|
}) |
|
|
|
.catch(err => { |
|
|
|
console.error("Restore Error:", err); |
|
|
|
alert("Failed to restore record due to connection error."); |
|
|
|
showLoader(false); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Dynamic stacked modals manager to handle overlays and backdrops dynamically |
|
|
|
document.addEventListener('show.bs.modal', function (event) { |
|
|
|
// Find currently shown modals |
|
|
|
|