You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4487 lines
198 KiB
4487 lines
198 KiB
<!DOCTYPE html>
|
|
<html xmlns:th="http://www.thymeleaf.org" lang="en">
|
|
|
|
<head>
|
|
<script th:inline="javascript">
|
|
(function () {
|
|
const currentUser = /*[[${session.pay_usrName}]]*/ null;
|
|
if (currentUser && sessionStorage.getItem("pay_usrName") !== currentUser) {
|
|
window.location.replace(/*[[@{/gccservicepaymenttracker/login}]]*/ '/gccservicepaymenttracker/login');
|
|
}
|
|
})();
|
|
</script>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GCC Payment Tracker - Dashboard</title>
|
|
|
|
<!-- CSRF Tokens -->
|
|
<meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" />
|
|
<meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" />
|
|
|
|
<!-- Google Fonts -->
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
rel="stylesheet">
|
|
|
|
<!-- Bootstrap & FontAwesome -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
|
<link rel="stylesheet" th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" type="text/css">
|
|
|
|
<!-- Flatpickr Datepicker -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
<!-- Chart.js -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<!-- SheetJS for Export to Excel -->
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
|
|
|
<!-- Custom Style Sheet mapping to theme -->
|
|
<style>
|
|
.text-info-dark {
|
|
color: #075985 !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.text-warning-dark {
|
|
color: #92400e !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
body {
|
|
background-color: #f4f7f6;
|
|
font-family: 'Poppins', sans-serif;
|
|
color: #333;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, #09303d, #0f5c78);
|
|
color: #fff;
|
|
padding: 15px 30px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.logo-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.logo-left img {
|
|
width: 60px;
|
|
height: 60px;
|
|
}
|
|
|
|
.logo-text h2 {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
margin: 0;
|
|
line-height: 1.2;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.logo-text p {
|
|
font-size: 0.7rem;
|
|
margin: 0;
|
|
color: #aed6f1;
|
|
}
|
|
|
|
.header-title h2 {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin: 0;
|
|
}
|
|
|
|
.user-menu {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.user-badge {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-logout {
|
|
background: #e74c3c;
|
|
color: #fff;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-logout:hover {
|
|
background: #c0392b;
|
|
color: #fff;
|
|
transform: translateY(-1.5px);
|
|
}
|
|
|
|
.main-container {
|
|
max-width: 95%;
|
|
margin: 35px auto;
|
|
padding: 0 15px;
|
|
}
|
|
|
|
/* 4 Category Cards Dashboard */
|
|
.category-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 35px;
|
|
}
|
|
|
|
.category-card {
|
|
background: #ffffff;
|
|
border: 1px solid #eef2f6;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.02);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.category-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 5px;
|
|
height: 100%;
|
|
}
|
|
|
|
/* SD Card Accent */
|
|
.category-card.card-sd::before {
|
|
background-color: #0f5c78;
|
|
}
|
|
|
|
.category-card.card-sd.active {
|
|
border-color: #0f5c78;
|
|
box-shadow: 0 10px 25px rgba(15, 92, 120, 0.15);
|
|
background-color: #f0f7f9;
|
|
}
|
|
|
|
/* Invoice Card Accent */
|
|
.category-card.card-invoice::before {
|
|
background-color: #27ae60;
|
|
}
|
|
|
|
.category-card.card-invoice.active {
|
|
border-color: #27ae60;
|
|
box-shadow: 0 10px 25px rgba(39, 174, 96, 0.15);
|
|
background-color: #f1fbf4;
|
|
}
|
|
|
|
/* EMD Card Accent */
|
|
.category-card.card-emd::before {
|
|
background-color: #8e44ad;
|
|
}
|
|
|
|
.category-card.card-emd.active {
|
|
border-color: #8e44ad;
|
|
box-shadow: 0 10px 25px rgba(142, 68, 173, 0.15);
|
|
background-color: #f9f2fc;
|
|
}
|
|
|
|
/* BG Card Accent */
|
|
.category-card.card-bg::before {
|
|
background-color: #1e40af;
|
|
}
|
|
|
|
.category-card.card-bg.active {
|
|
border-color: #1e40af;
|
|
box-shadow: 0 10px 25px rgba(30, 64, 175, 0.15);
|
|
background-color: #eff6ff;
|
|
}
|
|
|
|
/* DC Card Accent */
|
|
.category-card.card-dc::before {
|
|
background-color: #b91c1c;
|
|
}
|
|
|
|
.category-card.card-dc.active {
|
|
border-color: #b91c1c;
|
|
box-shadow: 0 10px 25px rgba(185, 28, 28, 0.15);
|
|
background-color: #fef2f2;
|
|
}
|
|
|
|
/* ASD Card Accent */
|
|
.category-card.card-asd::before {
|
|
background-color: #d35400;
|
|
}
|
|
|
|
.category-card.card-asd.active {
|
|
border-color: #d35400;
|
|
box-shadow: 0 10px 25px rgba(211, 84, 0, 0.15);
|
|
background-color: #fdf5e6;
|
|
}
|
|
|
|
/* Agreement Card Accent */
|
|
.category-card.card-agreement::before {
|
|
background-color: #db2777;
|
|
}
|
|
|
|
.category-card.card-agreement.active {
|
|
border-color: #db2777;
|
|
box-shadow: 0 10px 25px rgba(219, 39, 119, 0.15);
|
|
background-color: #fdf2f8;
|
|
}
|
|
|
|
/* Total No of Files Card Accent */
|
|
.category-card.card-total::before {
|
|
background-color: #2563eb;
|
|
}
|
|
|
|
.category-card.card-total.active {
|
|
border-color: #2563eb;
|
|
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.15);
|
|
background-color: #eff6ff;
|
|
}
|
|
|
|
.card-total .card-icon {
|
|
color: #2563eb;
|
|
}
|
|
|
|
/* Type Not Updated Files Card Accent */
|
|
.category-card.card-status-not-updated::before {
|
|
background-color: #475569;
|
|
}
|
|
|
|
.category-card.card-status-not-updated.active {
|
|
border-color: #475569;
|
|
box-shadow: 0 10px 25px rgba(71, 85, 105, 0.15);
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
.card-status-not-updated .card-icon {
|
|
color: #475569;
|
|
}
|
|
|
|
.category-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.card-left {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.card-icon {
|
|
font-size: 2.2rem;
|
|
opacity: 0.85;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.card-sd .card-icon {
|
|
color: #0f5c78;
|
|
}
|
|
|
|
.card-invoice .card-icon {
|
|
color: #27ae60;
|
|
}
|
|
|
|
.card-emd .card-icon {
|
|
color: #8e44ad;
|
|
}
|
|
|
|
.card-bg .card-icon {
|
|
color: #1e40af;
|
|
}
|
|
|
|
.card-dc .card-icon {
|
|
color: #b91c1c;
|
|
}
|
|
|
|
.card-asd .card-icon {
|
|
color: #d35400;
|
|
}
|
|
|
|
.card-agreement .card-icon {
|
|
color: #db2777;
|
|
}
|
|
|
|
.category-card:hover .card-icon {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.card-title {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 0.95rem;
|
|
font-weight: 700;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin: 0;
|
|
}
|
|
|
|
.card-value {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 1.85rem;
|
|
font-weight: 800;
|
|
color: #1e293b;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Filter Section */
|
|
.filter-card {
|
|
background: #ffffff;
|
|
border-radius: 16px;
|
|
border: 1px solid #eef2f6;
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
|
|
padding: 24px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.filter-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
align-items: end;
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.filter-label {
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.filter-input {
|
|
height: 44px;
|
|
border-radius: 10px;
|
|
border: 1.5px solid #cbd5e1;
|
|
padding: 10px 16px;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
color: #334155;
|
|
background-color: #f8fafc;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.filter-input:focus {
|
|
outline: none;
|
|
border-color: #0f5c78;
|
|
background-color: #ffffff;
|
|
box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
|
|
}
|
|
|
|
.btn-filter-search {
|
|
height: 44px;
|
|
border-radius: 10px;
|
|
border: none;
|
|
background: linear-gradient(135deg, #0f5c78, #1781a6);
|
|
color: #ffffff;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 10px rgba(15, 92, 120, 0.25);
|
|
}
|
|
|
|
.btn-filter-search:hover {
|
|
background: linear-gradient(135deg, #1781a6, #0f5c78);
|
|
transform: translateY(-1.5px);
|
|
}
|
|
|
|
.btn-filter-reset {
|
|
height: 44px;
|
|
border-radius: 10px;
|
|
border: 1.5px solid #cbd5e1;
|
|
background-color: #ffffff;
|
|
color: #475569;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-filter-reset:hover {
|
|
background-color: #f1f5f9;
|
|
color: #0f5c78;
|
|
border-color: #0f5c78;
|
|
}
|
|
|
|
/* Table Card Section */
|
|
.table-card {
|
|
background: #ffffff;
|
|
border-radius: 16px;
|
|
border: 1px solid #eef2f6;
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
|
|
padding: 24px;
|
|
}
|
|
|
|
.table-card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
}
|
|
|
|
.table-card-title {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.active-filter-badge {
|
|
background-color: #e2e8f0;
|
|
color: #475569;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
padding: 5px 12px;
|
|
border-radius: 20px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.btn-add-record {
|
|
background: linear-gradient(135deg, #0f5c78, #1781a6);
|
|
color: #ffffff;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 10px;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
box-shadow: 0 4px 10px rgba(15, 92, 120, 0.2);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-add-record:hover {
|
|
background: linear-gradient(135deg, #1781a6, #0f5c78);
|
|
transform: translateY(-2px);
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* Custom Data Grid Table */
|
|
.grid-table {
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.grid-table th {
|
|
background-color: #f1f5f9 !important;
|
|
color: #475569 !important;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.5px;
|
|
padding: 14px 10px;
|
|
border-bottom: 2px solid #cbd5e1 !important;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.grid-table td {
|
|
padding: 12px 10px;
|
|
vertical-align: middle;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.grid-table tbody tr:hover {
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
/* Badges for status */
|
|
.status-badge {
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
text-transform: uppercase;
|
|
display: inline-block;
|
|
}
|
|
|
|
.status-pending {
|
|
background-color: #fef9c3;
|
|
color: #854d0e;
|
|
}
|
|
|
|
.status-awaiting-details {
|
|
background-color: #ffedd5;
|
|
color: #c2410c;
|
|
}
|
|
|
|
.status-pending-with-dept {
|
|
background-color: #e0f2fe;
|
|
color: #0369a1;
|
|
}
|
|
|
|
.status-completed {
|
|
background-color: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.status-no-action {
|
|
background-color: #f1f5f9;
|
|
color: #475569;
|
|
}
|
|
|
|
.status-processed {
|
|
background-color: #dbeafe;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.status-approved {
|
|
background-color: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.status-paid {
|
|
background-color: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.status-rejected {
|
|
background-color: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.status-default {
|
|
background-color: #f1f5f9;
|
|
color: #475569;
|
|
}
|
|
|
|
/* Type Badges */
|
|
.type-badge {
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
text-transform: uppercase;
|
|
display: inline-block;
|
|
}
|
|
|
|
.type-sd {
|
|
background-color: #f0f7f9;
|
|
color: #0f5c78;
|
|
border: 1px solid rgba(15, 92, 120, 0.25);
|
|
}
|
|
|
|
.type-tax-invoice {
|
|
background-color: #f1fbf4;
|
|
color: #27ae60;
|
|
border: 1px solid rgba(39, 174, 96, 0.25);
|
|
}
|
|
|
|
.type-emd {
|
|
background-color: #f9f2fc;
|
|
color: #8e44ad;
|
|
border: 1px solid rgba(142, 68, 173, 0.25);
|
|
}
|
|
|
|
.type-bank-guarantee {
|
|
background-color: #eff6ff;
|
|
color: #1e40af;
|
|
border: 1px solid rgba(30, 64, 175, 0.25);
|
|
}
|
|
|
|
.type-delivery-chellan {
|
|
background-color: #fef2f2;
|
|
color: #b91c1c;
|
|
border: 1px solid rgba(185, 28, 28, 0.25);
|
|
}
|
|
|
|
.type-asd {
|
|
background-color: #fdf5e6;
|
|
color: #d35400;
|
|
border: 1px solid rgba(211, 84, 0, 0.25);
|
|
}
|
|
|
|
.type-agreement {
|
|
background-color: #fdf2f8;
|
|
color: #db2777;
|
|
border: 1px solid rgba(219, 39, 119, 0.25);
|
|
}
|
|
|
|
.btn-grid-action {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-grid-edit {
|
|
background-color: #e0f2fe;
|
|
color: #0369a1;
|
|
}
|
|
|
|
.btn-grid-edit:hover {
|
|
background-color: #0369a1;
|
|
color: #ffffff;
|
|
transform: scale(1.08);
|
|
}
|
|
|
|
.btn-grid-delete {
|
|
background-color: #fee2e2;
|
|
color: #b91c1c;
|
|
}
|
|
|
|
.btn-grid-delete:hover {
|
|
background-color: #b91c1c;
|
|
color: #ffffff;
|
|
transform: scale(1.08);
|
|
}
|
|
|
|
.btn-grid-pdf {
|
|
background-color: #fef2f2;
|
|
color: #b91c1c;
|
|
border: 1px solid rgba(185, 28, 28, 0.15) !important;
|
|
}
|
|
|
|
.btn-grid-pdf:hover {
|
|
background-color: #b91c1c;
|
|
color: #ffffff;
|
|
transform: scale(1.08);
|
|
}
|
|
|
|
/* Loading Overlay styling */
|
|
#loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: rgba(255, 255, 255, 0.75);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
z-index: 99999;
|
|
display: none;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
#loading-overlay.active {
|
|
opacity: 1;
|
|
pointer-events: all;
|
|
display: flex;
|
|
}
|
|
|
|
.loader-spinner-container {
|
|
background: white;
|
|
padding: 2.5rem 3.5rem;
|
|
border-radius: 16px;
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
|
|
border: 1px solid rgba(15, 92, 120, 0.1);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.loader-spinner {
|
|
font-size: 3rem;
|
|
color: #0f5c78;
|
|
}
|
|
|
|
.loader-text {
|
|
font-family: 'Outfit', 'Inter', sans-serif;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: #0f5c78;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.empty-icon {
|
|
font-size: 3.5rem;
|
|
color: #cbd5e1;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
/* Custom Modal Header Styling */
|
|
.modal-custom-header {
|
|
background-color: #0f5c78;
|
|
color: white;
|
|
border-top-left-radius: 14px;
|
|
border-top-right-radius: 14px;
|
|
padding: 18px 24px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-custom-title {
|
|
font-weight: 700;
|
|
font-size: 1.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin: 0;
|
|
font-family: 'Outfit', sans-serif;
|
|
}
|
|
|
|
.modal-content {
|
|
border-radius: 14px;
|
|
border: none;
|
|
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.25);
|
|
}
|
|
|
|
/* Pagination and Table Controls Styling */
|
|
.pagination .page-item .page-link {
|
|
color: #0f5c78;
|
|
border-color: #cbd5e1;
|
|
font-weight: 500;
|
|
padding: 6px 12px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.pagination .page-item.active .page-link {
|
|
background-color: #0f5c78;
|
|
border-color: #0f5c78;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.pagination .page-item .page-link:hover {
|
|
background-color: #e2e8f0;
|
|
border-color: #0f5c78;
|
|
color: #0f5c78;
|
|
}
|
|
|
|
.pagination .page-item.disabled .page-link {
|
|
color: #94a3b8;
|
|
border-color: #cbd5e1;
|
|
background-color: #f1f5f9;
|
|
}
|
|
|
|
.table-controls-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
}
|
|
|
|
.table-controls-left,
|
|
.table-controls-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* Interactive Summary Cell Styles */
|
|
.cursor-pointer {
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.cursor-pointer:hover {
|
|
background-color: #cbd5e1 !important;
|
|
transform: scale(1.08);
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
|
|
z-index: 5;
|
|
position: relative;
|
|
}
|
|
|
|
/* Customized premium scrollbar for responsive tables */
|
|
.table-responsive::-webkit-scrollbar {
|
|
height: 6px;
|
|
width: 6px;
|
|
}
|
|
|
|
.table-responsive::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.table-responsive::-webkit-scrollbar-track {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
/* Force table cells to wrap cleanly and prevent word clipping */
|
|
.grid-table th,
|
|
.grid-table td {
|
|
white-space: normal;
|
|
}
|
|
|
|
/* Keep straight horizontal scrolling layout only on mobile phone viewports */
|
|
@media (max-width: 767.98px) {
|
|
|
|
.grid-table th,
|
|
.grid-table td {
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
/* Media Queries for all devices */
|
|
@media (max-width: 991.98px) {
|
|
.header {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header-title h2 {
|
|
font-size: 1.35rem;
|
|
}
|
|
|
|
.user-menu {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
|
|
.main-container {
|
|
max-width: 100%;
|
|
margin: 15px auto;
|
|
}
|
|
|
|
.category-grid {
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 15px;
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.card-value {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.card-icon {
|
|
font-size: 1.85rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 575.98px) {
|
|
.header-title h2 {
|
|
font-size: 1.15rem;
|
|
}
|
|
|
|
.category-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 10px;
|
|
}
|
|
|
|
.category-card {
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.card-value {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.card-icon {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.filter-card,
|
|
.table-card {
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.table-controls-row {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
}
|
|
|
|
.table-controls-right input {
|
|
width: 100% !important;
|
|
}
|
|
|
|
.btn-add-record {
|
|
width: 100%;
|
|
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>
|
|
|
|
<body>
|
|
|
|
<!-- Fullscreen Loading Overlay -->
|
|
<div id="loading-overlay">
|
|
<div class="loader-spinner-container">
|
|
<i class="fa-solid fa-spinner fa-spin loader-spinner"></i>
|
|
<span class="loader-text" id="loader-overlay-text">Processing...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Header Bar -->
|
|
<div class="header">
|
|
<div class="logo-left">
|
|
<img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation Logo">
|
|
<div class="logo-text">
|
|
<h2>GREATER CHENNAI CORPORATION</h2>
|
|
<p>பெருநகர சென்னை மாநகராட்சி</p>
|
|
</div>
|
|
</div>
|
|
<div class="header-title">
|
|
<h2>Payment Processing Tracker</h2>
|
|
</div>
|
|
<div class="user-menu" style="display: flex; align-items: center; gap: 15px;">
|
|
<!-- Latest Update Badge -->
|
|
<div id="latest-update-badge" class="d-flex align-items-center gap-2 p-2 px-3 rounded text-white"
|
|
style="display: none !important; background-color: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); font-size: 0.8rem; font-family: 'Outfit', sans-serif; cursor: pointer; transition: all 0.3s;"
|
|
onclick="openLatestUpdatesModal()">
|
|
<i class="fa-solid fa-bell text-warning animate-bounce"></i>
|
|
<div>
|
|
Latest Update: <span class="fw-bold text-warning" id="latest-update-count">0</span> File(s) on
|
|
<span class="fw-semibold" id="latest-update-time">-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<span class="user-badge">
|
|
<i class="fa-solid fa-user-shield"></i>
|
|
<span th:text="${usrName}">Guest</span>
|
|
</span>
|
|
<a th:href="@{/gccservicepaymenttracker/logout}" class="btn-logout">
|
|
<i class="fa-solid fa-right-from-bracket"></i> Logout
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-container">
|
|
|
|
<!-- Category Grid (7 Cards) -->
|
|
<div class="category-grid">
|
|
|
|
<!-- Total No of Files -->
|
|
<div class="category-card card-total" onclick="openModalForCount('total', null, 'Total No of Files')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">Total No of Files</h3>
|
|
<p class="card-value" id="count-total">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-folder-open card-icon"></i>
|
|
</div>
|
|
|
|
<!-- Type Not Updated Files Files -->
|
|
<div class="category-card card-status-not-updated"
|
|
onclick="openModalForCount('category', 'status_not_updated', 'Type Not Updated Files Files')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">Type Not Updated Files</h3>
|
|
<p class="card-value" id="count-status-not-updated">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-circle-question card-icon"></i>
|
|
</div>
|
|
|
|
<!-- Security Deposit -->
|
|
<div class="category-card card-sd" onclick="openModalForCount('category', 'sd', 'Security Deposit')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">Security Deposit</h3>
|
|
<p class="card-value" id="count-sd">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-wallet card-icon"></i>
|
|
</div>
|
|
|
|
<!-- Tax Invoice -->
|
|
<div class="category-card card-invoice"
|
|
onclick="openModalForCount('category', 'tax invoice', 'Tax Invoice')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">Tax Invoice</h3>
|
|
<p class="card-value" id="count-invoice">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-file-invoice-dollar card-icon"></i>
|
|
</div>
|
|
|
|
<!-- EMD -->
|
|
<div class="category-card card-emd" onclick="openModalForCount('category', 'emd', 'EMD')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">EMD</h3>
|
|
<p class="card-value" id="count-emd">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-file-signature card-icon"></i>
|
|
</div>
|
|
|
|
<!-- Bank Guarantee commented out
|
|
<div class="category-card card-bg" onclick="openModalForCount('category', 'bank guarantee', 'Bank Guarantee')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">Bank Guarantee</h3>
|
|
<p class="card-value" id="count-bg">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-shield-halved card-icon"></i>
|
|
</div>
|
|
-->
|
|
|
|
<!-- Delivery Chellan commented out
|
|
<div class="category-card card-dc" onclick="openModalForCount('category', 'delivery chellan', 'Delivery Chellan')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">Delivery Chellan</h3>
|
|
<p class="card-value" id="count-dc">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-truck-ramp-box card-icon"></i>
|
|
</div>
|
|
-->
|
|
|
|
<!-- ASD -->
|
|
<div class="category-card card-asd" onclick="openModalForCount('category', 'asd', 'ASD')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">ASD</h3>
|
|
<p class="card-value" id="count-asd">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-vault card-icon"></i>
|
|
</div>
|
|
|
|
<!-- Agreement -->
|
|
<div class="category-card card-agreement" onclick="openModalForCount('category', 'agreement', 'Agreement')">
|
|
<div class="card-left">
|
|
<h3 class="card-title">Agreement</h3>
|
|
<p class="card-value" id="count-agreement">0</p>
|
|
</div>
|
|
<i class="fa-solid fa-file-contract card-icon"></i>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Executive Summary & Charts Section -->
|
|
<div class="row g-4 mb-4">
|
|
<!-- Executive Summary Table Card -->
|
|
<div class="col-lg-8">
|
|
<div class="card h-100 border-0 shadow-sm" style="border-radius: 16px; overflow: hidden;">
|
|
<div class="card-header bg-white py-3 border-0">
|
|
<h4 class="card-title mb-0 fw-bold"
|
|
style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
|
|
<i class="fa-solid fa-chart-line me-2 text-primary"></i>Total Files in No of days wise count
|
|
</h4>
|
|
</div>
|
|
<div class="card-body p-3">
|
|
<div class="table-responsive">
|
|
<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 id="summary-thead-tr">
|
|
<th style="background-color: #f8fafc; color: #475569;">Total No of files</th>
|
|
<th style="background-color: #f0fdf4; color: #166534;">Completed Files</th>
|
|
<th style="background-color: #fef2f2; color: #991b1b;">Pending Files</th>
|
|
<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>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<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="openModalForCount('total', null, 'Total No of files')">0</td>
|
|
<td id="summary-completed" class="cursor-pointer text-success"
|
|
onclick="openModalForCount('completed_custom', null, 'Completed Files')">0
|
|
</td>
|
|
<td id="summary-pending" class="cursor-pointer text-danger"
|
|
style="color: #991b1b; font-weight: bold;"
|
|
onclick="openModalForCount('pending_custom', null, 'Pending Files')">0</td>
|
|
<td id="summary-1-15" class="cursor-pointer"
|
|
onclick="openModalForCount('age', [1, 15], 'Age: 1 to 15 Days')">0</td>
|
|
<td id="summary-16-30" class="cursor-pointer"
|
|
onclick="openModalForCount('age', [16, 30], 'Age: 16 to 30 Days')">0</td>
|
|
<td id="summary-31-60" class="cursor-pointer"
|
|
onclick="openModalForCount('age', [31, 60], 'Age: 31 to 60 Days')">0</td>
|
|
<td id="summary-61-90" class="cursor-pointer"
|
|
onclick="openModalForCount('age', [61, 90], 'Age: 61 to 90 Days')">0</td>
|
|
<td id="summary-91-120" class="cursor-pointer"
|
|
onclick="openModalForCount('age', [91, 120], 'Age: 91 to 120 Days')">0</td>
|
|
<td id="summary-over-120" class="cursor-pointer text-danger"
|
|
onclick="openModalForCount('age', [121, null], 'Age: Over 120 Days')">0</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p class="text-muted mt-2 mb-0" style="font-size: 0.72rem; font-style: italic;">
|
|
<i class="fa-solid fa-circle-info me-1"></i>Tip: Click on any count cell to filter the
|
|
detailed payment tracker list below.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pie Chart Card -->
|
|
<div class="col-lg-4">
|
|
<div class="card h-100 border-0 shadow-sm" style="border-radius: 16px;">
|
|
<div class="card-header bg-white py-3 border-0">
|
|
<h4 class="card-title mb-0 fw-bold"
|
|
style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
|
|
<i class="fa-solid fa-chart-pie me-2 text-primary"></i>Invoices Status Distribution
|
|
</h4>
|
|
</div>
|
|
<div class="card-body d-flex align-items-center justify-content-center p-2"
|
|
style="position: relative; height: 180px;">
|
|
<canvas id="statusPieChart" style="max-height: 100%; max-width: 100%;"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vendor Breakdown Details Card -->
|
|
<div class="card border-0 shadow-sm mb-4" style="border-radius: 16px; overflow: hidden;">
|
|
<div
|
|
class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
<h4 class="card-title mb-0 fw-bold"
|
|
style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
|
|
<i class="fa-solid fa-users-viewfinder me-2 text-primary"></i>Vendor Breakdown Details (Invoices)
|
|
</h4>
|
|
<div style="font-size: 0.8rem; color: #64748b;">
|
|
<input type="text" id="vendor-breakdown-search" class="form-control form-control-sm"
|
|
style="width: 200px; border-radius: 8px;" placeholder="Search vendor breakdown..."
|
|
oninput="filterVendorBreakdown(this.value)">
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-3">
|
|
<div class="table-responsive" style="max-height: 280px; overflow-y: auto;">
|
|
<table class="table table-striped table-bordered text-center align-middle mb-0"
|
|
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 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 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>
|
|
<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>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="vendor-breakdown-tbody">
|
|
<!-- Populated dynamically via JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Card -->
|
|
<div class="filter-card">
|
|
<div class="filter-grid">
|
|
|
|
<!-- Date range picker
|
|
<div class="filter-group">
|
|
<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>
|
|
</div>
|
|
-->
|
|
|
|
<!-- Type Filter -->
|
|
<div class="filter-group">
|
|
<label class="filter-label" for="filter-type">Type</label>
|
|
<select id="filter-type" class="filter-input" onchange="handleTypeFilterChange(this.value)">
|
|
<option value="all">- All Types -</option>
|
|
<!-- Commented out static options to load from database
|
|
<option value="sd">Security Deposit</option>
|
|
<option value="tax invoice">Tax Invoice</option>
|
|
<option value="emd">EMD</option>
|
|
<option value="asd">ASD</option>
|
|
<option value="agreement">Agreement</option>
|
|
-->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Status Filter -->
|
|
<div class="filter-group">
|
|
<label class="filter-label" for="filter-status">Status</label>
|
|
<select id="filter-status" class="filter-input" onchange="loadList()">
|
|
<option value="">- All Statuses -</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Text Search -->
|
|
<div class="filter-group">
|
|
<label class="filter-label" for="search-text">Search Keyword</label>
|
|
<input type="text" id="search-text" class="filter-input" placeholder="Vendor, subject, invoice #..."
|
|
oninput="handleSearchInput(this.value)">
|
|
</div>
|
|
|
|
<!-- Action buttons -->
|
|
<button class="btn-filter-search" onclick="loadList()">
|
|
<i class="fa-solid fa-magnifying-glass"></i> Filter
|
|
</button>
|
|
|
|
<button class="btn-filter-reset" onclick="resetFilters()">
|
|
<i class="fa-solid fa-rotate-right"></i> Reset
|
|
</button>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table Card Section -->
|
|
<div class="table-card">
|
|
|
|
<div class="table-card-header">
|
|
<h3 class="table-card-title">
|
|
<i class="fa-solid fa-table-list text-primary"></i>
|
|
<span>Payment Tracking Details</span>
|
|
<span class="active-filter-badge" id="active-category-badge">Type: All Types</span>
|
|
<span class="active-filter-badge ms-2" id="click-filter-badge"
|
|
style="display: none; background-color: #fee2e2; color: #b91c1c;"></span>
|
|
</h3>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<button class="btn btn-sm btn-success fw-bold px-3 d-flex align-items-center gap-2"
|
|
style="border-radius: 8px; height: 36px; font-size: 0.85rem;"
|
|
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
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table Controls: Page size & Real-time Search -->
|
|
<div class="table-controls-row">
|
|
<div class="table-controls-left">
|
|
<span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Show</span>
|
|
<select id="table-page-size" class="form-select form-select-sm" style="width: 80px;"
|
|
onchange="changePageSize(this.value)">
|
|
<option value="10">10</option>
|
|
<option value="20" selected>20</option>
|
|
<option value="30">30</option>
|
|
<option value="40">40</option>
|
|
<option value="50">50</option>
|
|
</select>
|
|
<span class="text-secondary fw-semibold" style="font-size: 0.85rem;">entries</span>
|
|
</div>
|
|
<div class="table-controls-right">
|
|
<span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Search:</span>
|
|
<input type="text" id="table-search" class="form-control form-control-sm" style="width: 250px;"
|
|
placeholder="Search in this table..." oninput="handleTableSearch(this.value)">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Responsive Table Grid -->
|
|
<div class="table-responsive">
|
|
<table class="table grid-table table-striped table-hover align-middle" id="tracking-table">
|
|
<!-- Commented out old thead code
|
|
<thead>
|
|
<tr>
|
|
<th class="text-center" style="width: 50px;">S.No</th>
|
|
<th>Received Date</th>
|
|
<th>Type</th>
|
|
<th>Vendor Name</th>
|
|
<th>Subject</th>
|
|
<th>Amount</th>
|
|
<th>Invoice No</th>
|
|
<th>Receipt No</th>
|
|
<th>Invoice Date</th>
|
|
<th>Physical File No</th>
|
|
<th>Remarks</th>
|
|
<th>TAT</th>
|
|
<th>Status</th>
|
|
<th>Followup</th>
|
|
<th>My Followup</th>
|
|
<th class="text-center" style="width: 100px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
-->
|
|
<thead>
|
|
<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 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>
|
|
<th class="text-center" style="width: 120px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tracking-tbody">
|
|
<!-- Populated dynamically via JS -->
|
|
</tbody>
|
|
</table>
|
|
<div id="no-records-found" class="empty-state" style="display: none;">
|
|
<i class="fa-solid fa-folder-open empty-icon"></i>
|
|
<h4>No Records Found</h4>
|
|
<p>No payment tracking records match the active criteria.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination Controls -->
|
|
<div class="d-flex justify-content-between align-items-center mt-3 flex-wrap gap-2">
|
|
<div class="text-secondary fw-semibold" style="font-size: 0.85rem;" id="pagination-info">
|
|
Showing 0 to 0 of 0 entries
|
|
</div>
|
|
<nav aria-label="Table navigation">
|
|
<ul class="pagination pagination-sm mb-0" id="pagination-controls">
|
|
<!-- Populated dynamically via JS -->
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Add/Edit Modal Form -->
|
|
<div class="modal fade" id="recordModal" tabindex="-1" aria-labelledby="recordModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-custom-header">
|
|
<h5 class="modal-custom-title" id="recordModalLabel">
|
|
<i class="fa-solid fa-file-invoice-dollar"></i> Add/Edit Record
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="recordForm" onsubmit="saveRecord(event)">
|
|
<input type="hidden" id="form-id" name="id">
|
|
|
|
<div class="row g-3">
|
|
|
|
<!-- Received Date -->
|
|
<div class="col-md-6">
|
|
<label for="form-received-date" class="form-label fw-semibold text-secondary">Received
|
|
Date <span class="text-danger">*</span></label>
|
|
<input type="date" class="form-control" id="form-received-date" required
|
|
onclick="this.showPicker()">
|
|
</div>
|
|
|
|
<!-- Type -->
|
|
<div class="col-md-6">
|
|
<label for="form-type" class="form-label fw-semibold text-secondary">Type <span
|
|
class="text-danger">*</span></label>
|
|
<select class="form-select" id="form-type" required>
|
|
<!-- Commented out static options to load from database
|
|
<option value="sd">Security Deposit</option>
|
|
<option value="tax invoice">Tax Invoice</option>
|
|
<option value="emd">EMD</option>
|
|
<option value="asd">ASD</option>
|
|
<option value="agreement">Agreement</option>
|
|
-->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Vendor Name (Dynamic dropdown + Write-in) -->
|
|
<div class="col-md-6">
|
|
<label for="form-vendor-select" class="form-label fw-semibold text-secondary">Vendor
|
|
Name <span class="text-danger">*</span></label>
|
|
<select class="form-select mb-2" id="form-vendor-select"
|
|
onchange="toggleCustomVendor()">
|
|
<option value="">-- Select Vendor --</option>
|
|
<!-- Commented out model-populated options
|
|
<option th:each="vendor : ${vendorDetails}" th:value="${vendor.vendor_name}"
|
|
th:text="${vendor.vendor_name}"></option>
|
|
-->
|
|
<option value="__custom__">-- Write-in custom vendor --</option>
|
|
</select>
|
|
<input type="text" class="form-control" id="form-vendor-custom"
|
|
placeholder="Enter Custom Vendor Name" style="display: none;">
|
|
</div>
|
|
|
|
<!-- Subject -->
|
|
<div class="col-md-6">
|
|
<label for="form-subject" class="form-label fw-semibold text-secondary">Subject <span
|
|
class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="form-subject"
|
|
placeholder="Enter subject/matter" required>
|
|
</div>
|
|
|
|
<!-- Amount -->
|
|
<div class="col-md-6">
|
|
<label for="form-amount" class="form-label fw-semibold text-secondary">Amount <span
|
|
class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="form-amount"
|
|
placeholder="Enter amount (e.g. 50,000)" required>
|
|
</div>
|
|
|
|
<!-- Invoice No -->
|
|
<div class="col-md-6">
|
|
<label for="form-invoice-no" class="form-label fw-semibold text-secondary">Invoice
|
|
No</label>
|
|
<input type="text" class="form-control" id="form-invoice-no"
|
|
placeholder="Enter invoice number">
|
|
</div>
|
|
|
|
<!-- Receipt No -->
|
|
<div class="col-md-6">
|
|
<label for="form-receiptno" class="form-label fw-semibold text-secondary">Receipt
|
|
No</label>
|
|
<input type="text" class="form-control" id="form-receiptno"
|
|
placeholder="Enter Receipt No">
|
|
</div>
|
|
|
|
<!-- Invoice Date -->
|
|
<div class="col-md-6">
|
|
<label for="form-invoice-date" class="form-label fw-semibold text-secondary">Invoice
|
|
Date</label>
|
|
<input type="date" class="form-control" id="form-invoice-date"
|
|
onclick="this.showPicker()">
|
|
</div>
|
|
|
|
<!-- Physical File No -->
|
|
<div class="col-md-6">
|
|
<label for="form-physical-file-no"
|
|
class="form-label fw-semibold text-secondary">Physical File No</label>
|
|
<input type="text" class="form-control" id="form-physical-file-no"
|
|
placeholder="Enter physical file reference number">
|
|
</div>
|
|
|
|
<!-- TAT -->
|
|
<!-- <div class="col-md-6">
|
|
<label for="form-tat" class="form-label fw-semibold text-secondary">Turnaround Time
|
|
(TAT)</label>
|
|
<input type="text" class="form-control" id="form-tat" placeholder="e.g. 5 days">
|
|
</div> -->
|
|
|
|
<!-- Status -->
|
|
<div class="col-md-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>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Followup commented out
|
|
<div class="col-md-6">
|
|
<label for="form-followup"
|
|
class="form-label fw-semibold text-secondary">Follow-up</label>
|
|
<input type="text" class="form-control" id="form-followup" placeholder="Follow-up note">
|
|
</div>
|
|
-->
|
|
|
|
<!-- My Followup commented out
|
|
<div class="col-md-6">
|
|
<label for="form-myfollowup" class="form-label fw-semibold text-secondary">My
|
|
Follow-up</label>
|
|
<input type="text" class="form-control" id="form-myfollowup"
|
|
placeholder="Internal follow-up note">
|
|
</div>
|
|
-->
|
|
|
|
<!-- Order By commented out
|
|
<div class="col-md-6">
|
|
<label for="form-orderby" class="form-label fw-semibold text-secondary">Order By</label>
|
|
<input type="text" class="form-control" id="form-orderby"
|
|
placeholder="Reference order description">
|
|
</div>
|
|
-->
|
|
|
|
<!-- PDF Attachment -->
|
|
<div class="col-md-12">
|
|
<label for="form-pdf" class="form-label fw-semibold text-secondary">PDF Attachment
|
|
(Optional)</label>
|
|
<input type="file" class="form-control" id="form-pdf" accept="application/pdf">
|
|
<div id="form-existing-pdf-container" class="mt-2" style="display: none;">
|
|
<span class="text-secondary fw-semibold">Current Document:</span>
|
|
<a id="form-existing-pdf-link" href="#" target="_blank"
|
|
class="ms-1 text-primary fw-bold text-decoration-none">
|
|
<i class="fa-solid fa-file-pdf"></i> View PDF
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remarks -->
|
|
<div class="col-12">
|
|
<label for="form-remarks" class="form-label fw-semibold text-secondary">Remarks</label>
|
|
<textarea class="form-control" id="form-remarks" rows="2"
|
|
placeholder="Additional remarks..."></textarea>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-end gap-2 mt-4">
|
|
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary px-4"
|
|
style="background-color: #0f5c78; border-color: #0f5c78;">
|
|
<i class="fa-solid fa-save"></i> Save Record
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History Modal -->
|
|
<div class="modal fade" id="historyModal" tabindex="-1" aria-labelledby="historyModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-custom-header" style="background-color: #2c3e50;">
|
|
<h5 class="modal-custom-title" id="historyModalLabel">
|
|
<i class="fa-solid fa-clock-rotate-left"></i> Payment Record Update History
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="table-responsive" style="max-height: 400px; border: 1px solid #eef2f6;">
|
|
<table class="table table-hover modal-table grid-table" id="history-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 50px;" class="text-center">S.No</th>
|
|
<th>Received Date</th>
|
|
<th>Invoice Date</th>
|
|
<th>Subject</th>
|
|
<th>Type</th>
|
|
<th>Vendor Name</th>
|
|
<th>Amount</th>
|
|
<th>Status</th>
|
|
<th>Remarks</th>
|
|
<th>Last Updated At</th>
|
|
<th>Last Updated By</th>
|
|
<th class="text-center" style="width: 80px;">Document</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="history-tbody">
|
|
<!-- Populated dynamically -->
|
|
</tbody>
|
|
</table>
|
|
<div id="no-history-found" class="text-center text-muted py-5" style="display: none;">
|
|
<i class="fa-solid fa-history fs-1 mb-3 text-secondary" style="opacity: 0.5;"></i>
|
|
<p class="fw-bold mb-0">No update history found for this record.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal with Remarks -->
|
|
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel"
|
|
aria-hidden="true" style="z-index: 1070;">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-custom-header" style="background-color: #dc3545; color: white;">
|
|
<h5 class="modal-custom-title" id="deleteConfirmModalLabel">
|
|
<i class="fa-solid fa-triangle-exclamation"></i> Confirm Delete
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-dark fw-semibold">Are you sure you want to delete this payment tracking record?</p>
|
|
<div class="mb-3">
|
|
<label for="delete-remarks" class="form-label fw-bold text-secondary text-uppercase"
|
|
style="font-size: 0.75rem;">Remarks / Reason for Deletion <span
|
|
class="text-danger">*</span></label>
|
|
<textarea id="delete-remarks" class="form-control" rows="3"
|
|
placeholder="Enter remarks for deleting this record..." required></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-light p-2">
|
|
<button type="button" class="btn btn-sm btn-secondary px-3" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-sm btn-danger px-3" onclick="submitDeleteRecord()">Delete
|
|
Record</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Count Details Modal -->
|
|
<div class="modal fade" id="countDetailsModal" tabindex="-1" aria-labelledby="countDetailsModalLabel"
|
|
aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-custom-header" style="background-color: #0f5c78;">
|
|
<h5 class="modal-custom-title" id="countDetailsModalLabel">
|
|
<i class="fa-solid fa-list-check"></i> Count Details
|
|
</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;">
|
|
<!-- Filters Row Inside Modal -->
|
|
<div class="row g-3 mb-4 p-3 bg-light rounded border align-items-end">
|
|
<div class="col-md-3">
|
|
<label for="modal-filter-type" class="form-label fw-bold text-secondary text-uppercase"
|
|
style="font-size: 0.75rem; letter-spacing: 0.5px;">Type Filter</label>
|
|
<select id="modal-filter-type" class="form-select form-select-sm"
|
|
onchange="applyModalFilters()">
|
|
<option value="all">- All Types -</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="modal-filter-status" class="form-label fw-bold text-secondary text-uppercase"
|
|
style="font-size: 0.75rem; letter-spacing: 0.5px;">Status Filter</label>
|
|
<select id="modal-filter-status" class="form-select form-select-sm"
|
|
onchange="applyModalFilters()">
|
|
<option value="all">- All Statuses -</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="modal-filter-vendor" class="form-label fw-bold text-secondary text-uppercase"
|
|
style="font-size: 0.75rem; letter-spacing: 0.5px;">Vendor Filter</label>
|
|
<select id="modal-filter-vendor" class="form-select form-select-sm"
|
|
onchange="applyModalFilters()">
|
|
<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"
|
|
style="border-radius: 8px; height: 31px;" onclick="exportModalTableToExcel()">
|
|
<i class="fa-solid fa-file-excel"></i> Export to Excel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Responsive Table Inside Modal -->
|
|
<div class="table-responsive"
|
|
style="max-height: 400px; overflow-y: auto; border: 1px solid #eef2f6;">
|
|
<table class="table grid-table table-striped table-hover align-middle mb-0"
|
|
id="modal-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 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 Date</th>
|
|
<th>Last Updated By</th>
|
|
<th class="text-center" style="width: 120px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modal-tracking-tbody">
|
|
<!-- Populated dynamically via JS -->
|
|
</tbody>
|
|
</table>
|
|
<div id="modal-no-records-found" class="empty-state text-center py-5" style="display: none;">
|
|
<i class="fa-solid fa-folder-open empty-icon"
|
|
style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i>
|
|
<h5>No Records Found</h5>
|
|
<p>No payment tracking records match the active criteria.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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 of File</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 -->
|
|
<script th:inline="javascript">
|
|
const contextPath = /*[[@{/}]]*/ '';
|
|
const isAdmin = /*[[${isAdmin}]]*/ false;
|
|
|
|
let activeCategory = 'all';
|
|
let allRecords = [];
|
|
let currentFilteredRecords = [];
|
|
let recordModalObj = null;
|
|
let historyModalObj = null;
|
|
let deletedRecordsModalObj = null;
|
|
let currentPage = 1;
|
|
let pageSize = 20;
|
|
|
|
// Custom dashboard filter & chart instances
|
|
let activeClickFilter = null;
|
|
let statusChartInstance = null;
|
|
let vendorBreakdownData = [];
|
|
|
|
// Dynamic lists for types, vendors, and statuses
|
|
let activeTypes = [];
|
|
let activeVendors = [];
|
|
let activeStatus = [];
|
|
|
|
// Date Pickers
|
|
// let fromDatePicker, toDatePicker;
|
|
|
|
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();
|
|
loadList();
|
|
});
|
|
|
|
// Helper to map type_name to a category code
|
|
function getCategoryCode(typeName) {
|
|
const type = String(typeName).toLowerCase().trim();
|
|
if (type === 'asd' || type.includes('asd') || type.includes('additional security')) {
|
|
return 'asd';
|
|
} else if (type.includes('agreement')) {
|
|
return 'agreement';
|
|
} else if (type === 'sd' || (type.includes('security') && !type.includes('additional') && !type.includes('asd'))) {
|
|
return 'sd';
|
|
} else if (type.includes('invoice') || type.includes('bill')) {
|
|
return 'tax invoice';
|
|
} else if (type === 'emd' || type.includes('earnest')) {
|
|
return 'emd';
|
|
} else if (type.includes('guarantee') || type === 'bg') {
|
|
return 'bank guarantee';
|
|
} else if (type.includes('chellan') || type.includes('challan') || type === 'dc') {
|
|
return 'delivery chellan';
|
|
}
|
|
return type;
|
|
}
|
|
|
|
// Fetch types, vendors, and statuses from DB endpoints
|
|
async function loadTypesAndVendors() {
|
|
try {
|
|
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/statuslist?_=' + new Date().getTime())
|
|
]);
|
|
activeTypes = await typesRes.json();
|
|
activeVendors = await vendorsRes.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);
|
|
}
|
|
}
|
|
|
|
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') || name.includes('fm unit')) {
|
|
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-dark' },
|
|
'pending with dept': { bg: '#f0f9ff', text: '#075985', badgeClass: 'text-info-dark' },
|
|
'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
|
|
const filterTypeSelect = document.getElementById('filter-type');
|
|
if (filterTypeSelect) {
|
|
filterTypeSelect.innerHTML = '<option value="all">- All Types -</option>';
|
|
const optNotUpdated = document.createElement('option');
|
|
optNotUpdated.value = 'status_not_updated';
|
|
optNotUpdated.textContent = 'Status Not Updated';
|
|
filterTypeSelect.appendChild(optNotUpdated);
|
|
|
|
activeTypes.forEach(t => {
|
|
const categoryCode = getCategoryCode(t.type_name);
|
|
const opt = document.createElement('option');
|
|
opt.value = categoryCode;
|
|
opt.textContent = t.type_name;
|
|
filterTypeSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
// Populate form-type
|
|
const formTypeSelect = document.getElementById('form-type');
|
|
if (formTypeSelect) {
|
|
formTypeSelect.innerHTML = '<option value="">-- Select Type --</option>';
|
|
activeTypes.forEach(t => {
|
|
const opt = document.createElement('option');
|
|
opt.value = t.id;
|
|
opt.textContent = t.type_name;
|
|
formTypeSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
// 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) {
|
|
formVendorSelect.innerHTML = '<option value="">-- Select Vendor --</option>';
|
|
activeVendors.forEach(v => {
|
|
const opt = document.createElement('option');
|
|
opt.value = v.id;
|
|
opt.textContent = v.vendor_name;
|
|
formVendorSelect.appendChild(opt);
|
|
});
|
|
const customOpt = document.createElement('option');
|
|
customOpt.value = '__custom__';
|
|
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 No of files</th>
|
|
<th style="background-color: #f0fdf4; color: #166534;">Completed Files</th>
|
|
<th style="background-color: #fef2f2; color: #991b1b;">Pending Files</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) {
|
|
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>
|
|
`;
|
|
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>`;
|
|
});
|
|
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
|
|
function showLoader(show, text = 'Processing...') {
|
|
const overlay = document.getElementById('loading-overlay');
|
|
const overlayText = document.getElementById('loader-overlay-text');
|
|
overlayText.textContent = text;
|
|
if (show) {
|
|
overlay.classList.add('active');
|
|
} else {
|
|
overlay.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
// Get CSRF details
|
|
function getCsrfHeaders() {
|
|
const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
|
|
const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
|
|
const headers = {};
|
|
if (csrfToken && csrfHeader) {
|
|
headers[csrfHeader] = csrfToken;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
// Category count loaders
|
|
function loadCounts() {
|
|
fetch(contextPath + 'gccservicepaymenttracker/api/counts?_=' + new Date().getTime())
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
document.getElementById('count-sd').textContent = data.sd || 0;
|
|
document.getElementById('count-invoice').textContent = data['tax invoice'] || 0;
|
|
document.getElementById('count-emd').textContent = data.emd || 0;
|
|
const bgEl = document.getElementById('count-bg');
|
|
if (bgEl) bgEl.textContent = data['bank guarantee'] || 0;
|
|
const dcEl = document.getElementById('count-dc');
|
|
if (dcEl) dcEl.textContent = data['delivery chellan'] || 0;
|
|
document.getElementById('count-asd').textContent = data.asd || 0;
|
|
document.getElementById('count-agreement').textContent = data.agreement || 0;
|
|
|
|
// Set total and Type Not Updated Files counts
|
|
const totalEl = document.getElementById('count-total');
|
|
if (totalEl) totalEl.textContent = data.total || 0;
|
|
const missingTypeEl = document.getElementById('count-status-not-updated');
|
|
if (missingTypeEl) missingTypeEl.textContent = data.status_not_updated || 0;
|
|
})
|
|
.catch(err => console.error("Error loading category counts:", err));
|
|
}
|
|
|
|
// Handles select change of card
|
|
function selectCategory(category, cardElement) {
|
|
const isActive = cardElement.classList.contains('active');
|
|
|
|
// Remove active class from all
|
|
document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
|
|
|
|
const filterTypeSelect = document.getElementById('filter-type');
|
|
|
|
if (isActive) {
|
|
activeCategory = 'all';
|
|
document.getElementById('active-category-badge').textContent = 'Type: All Types';
|
|
if (filterTypeSelect) {
|
|
filterTypeSelect.value = 'all';
|
|
}
|
|
} else {
|
|
cardElement.classList.add('active');
|
|
activeCategory = category;
|
|
|
|
if (filterTypeSelect) {
|
|
filterTypeSelect.value = category;
|
|
}
|
|
|
|
// Update badge text
|
|
const labels = {
|
|
'sd': 'Security Deposit',
|
|
'tax invoice': 'Tax Invoice',
|
|
'emd': 'EMD',
|
|
'asd': 'ASD',
|
|
'agreement': 'Agreement'
|
|
};
|
|
document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
|
|
}
|
|
|
|
// Refresh list
|
|
loadList();
|
|
|
|
// Smoothly scroll down to the table card
|
|
document.querySelector('.table-card')?.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
// Loading and Rendering list data
|
|
function loadList() {
|
|
showLoader(true, 'Loading payment tracking details...');
|
|
|
|
const statusVal = document.getElementById('filter-status').value;
|
|
const searchVal = document.getElementById('search-text').value;
|
|
|
|
// Sync search-text to table-search when hitting main filter search
|
|
const tblSearch = document.getElementById('table-search');
|
|
if (tblSearch) {
|
|
tblSearch.value = searchVal;
|
|
}
|
|
|
|
// Build query params
|
|
const params = new URLSearchParams();
|
|
if (activeCategory && activeCategory !== 'all') {
|
|
params.append('category', activeCategory);
|
|
}
|
|
|
|
if (statusVal) params.append('status', statusVal);
|
|
|
|
// Add cache buster
|
|
params.append('_', new Date().getTime());
|
|
|
|
fetch(contextPath + 'gccservicepaymenttracker/api/list?' + params.toString())
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
allRecords = data;
|
|
currentPage = 1;
|
|
clearClickFilter(null);
|
|
calculateSummaries();
|
|
renderTable();
|
|
updateLatestUpdateBadge();
|
|
if (document.getElementById('countDetailsModal')?.classList.contains('show')) {
|
|
applyModalFilters();
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error("Error loading details:", err);
|
|
showLoader(false);
|
|
});
|
|
}
|
|
|
|
function renderTable() {
|
|
const tbody = document.getElementById('tracking-tbody');
|
|
const noRecords = document.getElementById('no-records-found');
|
|
tbody.innerHTML = '';
|
|
|
|
const searchVal = document.getElementById('table-search')?.value || '';
|
|
let query = searchVal.toLowerCase().trim();
|
|
|
|
let filtered = allRecords.filter(row => {
|
|
// Apply activeClickFilter if set
|
|
if (activeClickFilter) {
|
|
const age = parseInt(calculateAgeOfBill(row.received_date), 10);
|
|
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') {
|
|
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.toLowerCase() !== filterVal.toLowerCase()) return false;
|
|
}
|
|
} else if (activeClickFilter.type === 'age') {
|
|
const [min, max] = activeClickFilter.value;
|
|
if (max !== null) {
|
|
if (age < min || age > max) return false;
|
|
} else {
|
|
if (age < min) return false;
|
|
}
|
|
} else if (activeClickFilter.type === 'vendor') {
|
|
if (vendor !== activeClickFilter.vendorName) return false;
|
|
|
|
if (activeClickFilter.col) {
|
|
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 === '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 (!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) ||
|
|
(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)
|
|
);
|
|
});
|
|
|
|
currentFilteredRecords = filtered;
|
|
|
|
showLoader(false);
|
|
|
|
const totalRecords = filtered.length;
|
|
if (totalRecords === 0) {
|
|
noRecords.style.display = 'block';
|
|
document.getElementById('pagination-info').textContent = 'Showing 0 to 0 of 0 entries';
|
|
document.getElementById('pagination-controls').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
noRecords.style.display = 'none';
|
|
|
|
// Paginate
|
|
const totalPages = Math.ceil(totalRecords / pageSize);
|
|
if (currentPage > totalPages) {
|
|
currentPage = totalPages;
|
|
}
|
|
if (currentPage < 1) {
|
|
currentPage = 1;
|
|
}
|
|
|
|
const startIndex = (currentPage - 1) * pageSize;
|
|
const endIndex = Math.min(startIndex + pageSize, totalRecords);
|
|
const paginatedData = filtered.slice(startIndex, endIndex);
|
|
|
|
// Update Pagination Info
|
|
document.getElementById('pagination-info').textContent =
|
|
`Showing ${startIndex + 1} to ${endIndex} of ${totalRecords} entries`;
|
|
|
|
paginatedData.forEach((row, pageIndex) => {
|
|
const globalIndex = startIndex + pageIndex;
|
|
const tr = document.createElement('tr');
|
|
|
|
// S.No
|
|
const tdIndex = document.createElement('td');
|
|
tdIndex.className = 'text-center fw-semibold text-secondary';
|
|
tdIndex.textContent = globalIndex + 1;
|
|
tr.appendChild(tdIndex);
|
|
|
|
// Helper for blank values
|
|
const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
|
|
|
|
// Format Dates (yyyy-MM-dd to dd/MM/yyyy)
|
|
const formatDate = dateStr => {
|
|
if (!dateStr) return '-';
|
|
const parts = dateStr.split('-');
|
|
if (parts.length === 3) {
|
|
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
|
}
|
|
return dateStr;
|
|
};
|
|
|
|
// Format Date-Times (yyyy-MM-ddTHH:mm:ss... to dd/MM/yyyy HH:mm:ss)
|
|
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 getTypeBadge = (typeVal, filePath) => {
|
|
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 {
|
|
// Fallback: check if cleanType is a numeric ID from activeTypes
|
|
const typeObj = activeTypes.find(t => String(t.id) === cleanType);
|
|
if (typeObj) {
|
|
return getTypeBadge(typeObj.type_name, filePath);
|
|
}
|
|
label = typeVal || '-';
|
|
badgeClass = 'status-default';
|
|
}
|
|
|
|
let onClickAttr = '';
|
|
if (filePath) {
|
|
let pdfUrl = filePath;
|
|
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
|
|
pdfUrl = contextPath + pdfUrl.substring(1);
|
|
} else {
|
|
pdfUrl = contextPath + pdfUrl;
|
|
}
|
|
onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
|
|
} else {
|
|
onClickAttr = `onclick="alert('pdf file not uploaded')"`;
|
|
}
|
|
|
|
return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${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>
|
|
<td>${text(row.physical_file_no)}</td>
|
|
<td>${text(row.receiptno)}</td>
|
|
<td>${text(row.subject)}</td>
|
|
<td>${getTypeBadge(row.resolved_type || row.type, row.file_path)}</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-muted">${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';
|
|
|
|
if (row.file_path) {
|
|
const btnViewPdf = document.createElement('button');
|
|
btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
|
|
btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
|
|
btnViewPdf.title = 'View PDF Document';
|
|
btnViewPdf.onclick = () => {
|
|
let pdfUrl = row.file_path;
|
|
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
|
|
pdfUrl = contextPath + pdfUrl.substring(1);
|
|
} else {
|
|
pdfUrl = contextPath + pdfUrl;
|
|
}
|
|
window.open(pdfUrl, '_blank');
|
|
};
|
|
tdActions.appendChild(btnViewPdf);
|
|
}
|
|
|
|
// History Button (Available to all logged in users)
|
|
const btnHistory = document.createElement('button');
|
|
btnHistory.className = 'btn-grid-action btn-outline-secondary me-1';
|
|
btnHistory.innerHTML = '<i class="fa-solid fa-clock-rotate-left"></i>';
|
|
btnHistory.style.border = '1px solid #cbd5e1';
|
|
btnHistory.title = 'View Update History';
|
|
btnHistory.onclick = () => showHistoryModal(row.id);
|
|
tdActions.appendChild(btnHistory);
|
|
|
|
if (isAdmin) {
|
|
const btnEdit = document.createElement('button');
|
|
btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
|
|
btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
|
|
btnEdit.title = 'Edit Record';
|
|
btnEdit.onclick = () => openEditModal(row);
|
|
|
|
const btnDelete = document.createElement('button');
|
|
btnDelete.className = 'btn-grid-action btn-grid-delete';
|
|
btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
|
|
btnDelete.title = 'Delete Record';
|
|
btnDelete.onclick = () => deleteRecord(row.id);
|
|
|
|
tdActions.appendChild(btnEdit);
|
|
tdActions.appendChild(btnDelete);
|
|
}
|
|
tr.appendChild(tdActions);
|
|
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
// Rebuild Pagination Controls
|
|
buildPaginationControls(totalPages);
|
|
}
|
|
|
|
function buildPaginationControls(totalPages) {
|
|
const container = document.getElementById('pagination-controls');
|
|
container.innerHTML = '';
|
|
|
|
if (totalPages <= 1) {
|
|
return;
|
|
}
|
|
|
|
// Previous Button
|
|
const prevLi = document.createElement('li');
|
|
prevLi.className = 'page-item' + (currentPage === 1 ? ' disabled' : '');
|
|
prevLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage - 1})">Previous</a>`;
|
|
container.appendChild(prevLi);
|
|
|
|
// Page numbers
|
|
const maxVisible = 5;
|
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
|
|
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
|
|
|
|
if (endPage - startPage + 1 < maxVisible) {
|
|
startPage = Math.max(1, endPage - maxVisible + 1);
|
|
}
|
|
|
|
if (startPage > 1) {
|
|
const firstLi = document.createElement('li');
|
|
firstLi.className = 'page-item';
|
|
firstLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(1)">1</a>`;
|
|
container.appendChild(firstLi);
|
|
|
|
if (startPage > 2) {
|
|
const dotsLi = document.createElement('li');
|
|
dotsLi.className = 'page-item disabled';
|
|
dotsLi.innerHTML = `<span class="page-link">...</span>`;
|
|
container.appendChild(dotsLi);
|
|
}
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
const pageLi = document.createElement('li');
|
|
pageLi.className = 'page-item' + (i === currentPage ? ' active' : '');
|
|
pageLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${i})">${i}</a>`;
|
|
container.appendChild(pageLi);
|
|
}
|
|
|
|
if (endPage < totalPages) {
|
|
if (endPage < totalPages - 1) {
|
|
const dotsLi = document.createElement('li');
|
|
dotsLi.className = 'page-item disabled';
|
|
dotsLi.innerHTML = `<span class="page-link">...</span>`;
|
|
container.appendChild(dotsLi);
|
|
}
|
|
|
|
const lastLi = document.createElement('li');
|
|
lastLi.className = 'page-item';
|
|
lastLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${totalPages})">${totalPages}</a>`;
|
|
container.appendChild(lastLi);
|
|
}
|
|
|
|
// Next Button
|
|
const nextLi = document.createElement('li');
|
|
nextLi.className = 'page-item' + (currentPage === totalPages ? ' disabled' : '');
|
|
nextLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage + 1})">Next</a>`;
|
|
container.appendChild(nextLi);
|
|
}
|
|
|
|
function goToPage(page) {
|
|
currentPage = page;
|
|
renderTable();
|
|
}
|
|
|
|
function changePageSize(size) {
|
|
pageSize = parseInt(size, 10);
|
|
currentPage = 1;
|
|
renderTable();
|
|
}
|
|
|
|
// Handles search in local table search input
|
|
function handleTableSearch(keyword) {
|
|
const filterSearch = document.getElementById('search-text');
|
|
if (filterSearch) {
|
|
filterSearch.value = keyword;
|
|
}
|
|
currentPage = 1;
|
|
renderTable();
|
|
}
|
|
|
|
function handleSearchInput(keyword) {
|
|
const tblSearch = document.getElementById('table-search');
|
|
if (tblSearch) {
|
|
tblSearch.value = keyword;
|
|
}
|
|
currentPage = 1;
|
|
renderTable();
|
|
}
|
|
|
|
function handleTypeFilterChange(category) {
|
|
activeCategory = category;
|
|
|
|
// Remove active class from all top cards
|
|
document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
|
|
|
|
if (category !== 'all') {
|
|
const classMap = {
|
|
'sd': 'card-sd',
|
|
'tax invoice': 'card-invoice',
|
|
'emd': 'card-emd',
|
|
'asd': 'card-asd',
|
|
'agreement': 'card-agreement'
|
|
};
|
|
const cardClass = classMap[category];
|
|
if (cardClass) {
|
|
const cardElement = document.querySelector('.' + cardClass);
|
|
if (cardElement) {
|
|
cardElement.classList.add('active');
|
|
}
|
|
}
|
|
|
|
const labels = {
|
|
'sd': 'Security Deposit',
|
|
'tax invoice': 'Tax Invoice',
|
|
'emd': 'EMD',
|
|
'asd': 'ASD',
|
|
'agreement': 'Agreement'
|
|
};
|
|
document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
|
|
} else {
|
|
document.getElementById('active-category-badge').textContent = 'Type: All Types';
|
|
}
|
|
|
|
loadList();
|
|
}
|
|
|
|
function resetFilters() {
|
|
document.getElementById('filter-status').value = '';
|
|
document.getElementById('search-text').value = '';
|
|
|
|
const filterType = document.getElementById('filter-type');
|
|
if (filterType) {
|
|
filterType.value = 'all';
|
|
}
|
|
|
|
activeCategory = 'all';
|
|
document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
|
|
document.getElementById('active-category-badge').textContent = 'Type: All Types';
|
|
|
|
const tblSearch = document.getElementById('table-search');
|
|
if (tblSearch) {
|
|
tblSearch.value = '';
|
|
}
|
|
currentPage = 1;
|
|
loadList();
|
|
}
|
|
|
|
// Toggle text field for dynamic vendor select dropdown
|
|
function toggleCustomVendor() {
|
|
const select = document.getElementById('form-vendor-select');
|
|
const customInput = document.getElementById('form-vendor-custom');
|
|
if (select.value === '__custom__') {
|
|
customInput.style.display = 'block';
|
|
customInput.required = true;
|
|
} else {
|
|
customInput.style.display = 'none';
|
|
customInput.required = false;
|
|
customInput.value = '';
|
|
}
|
|
}
|
|
|
|
// Edit/Add Modals
|
|
function openAddModal() {
|
|
document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record';
|
|
document.getElementById('recordForm').reset();
|
|
document.getElementById('form-id').value = '';
|
|
|
|
// Set type to active category by default
|
|
const formTypeSelect = document.getElementById('form-type');
|
|
formTypeSelect.value = '';
|
|
if (activeCategory && activeCategory !== 'all') {
|
|
for (let option of formTypeSelect.options) {
|
|
if (option.value) {
|
|
const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
|
|
if (typeObj && getCategoryCode(typeObj.type_name) === activeCategory) {
|
|
formTypeSelect.value = option.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
|
|
// Reset PDF upload elements
|
|
document.getElementById('form-pdf').value = '';
|
|
document.getElementById('form-existing-pdf-container').style.display = 'none';
|
|
|
|
recordModalObj.show();
|
|
}
|
|
|
|
function openEditModal(row) {
|
|
document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-file-signature"></i> Edit Payment Tracking Record';
|
|
document.getElementById('recordForm').reset();
|
|
|
|
document.getElementById('form-id').value = row.id;
|
|
document.getElementById('form-received-date').value = row.received_date || '';
|
|
|
|
// Set type select
|
|
const typeSelect = document.getElementById('form-type');
|
|
const rowType = String(row.type || '').trim().toLowerCase();
|
|
typeSelect.value = '';
|
|
|
|
let found = false;
|
|
for (let option of typeSelect.options) {
|
|
if (option.value && String(option.value) === String(row.type)) {
|
|
typeSelect.value = option.value;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found && rowType) {
|
|
const legacyCategory = getCategoryCode(rowType);
|
|
for (let option of typeSelect.options) {
|
|
if (option.value) {
|
|
const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
|
|
if (typeObj) {
|
|
const optCategory = getCategoryCode(typeObj.type_name);
|
|
if (optCategory === legacyCategory) {
|
|
typeSelect.value = option.value;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
document.getElementById('form-subject').value = row.subject || '';
|
|
document.getElementById('form-amount').value = row.amount || '';
|
|
document.getElementById('form-invoice-no').value = row.invoice_no || '';
|
|
|
|
document.getElementById('form-receiptno').value = row.receiptno || '';
|
|
|
|
document.getElementById('form-invoice-date').value = row.invoice_date || '';
|
|
document.getElementById('form-physical-file-no').value = row.physical_file_no || '';
|
|
const tatEl = document.getElementById('form-tat');
|
|
if (tatEl) tatEl.value = row.tat || '';
|
|
|
|
const statusSelect = document.getElementById('form-status');
|
|
const rowStatus = String(row.status || '').trim();
|
|
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 && 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');
|
|
if (myfollowupEl) myfollowupEl.value = row.myfollowup || '';
|
|
const orderbyEl = document.getElementById('form-orderby');
|
|
if (orderbyEl) orderbyEl.value = row.order_by || '';
|
|
document.getElementById('form-remarks').value = row.remarks || '';
|
|
|
|
// Set vendor select (using ID or legacy fallback/custom)
|
|
const select = document.getElementById('form-vendor-select');
|
|
const vendorVal = String(row.vendor_name || '').trim();
|
|
select.value = '';
|
|
document.getElementById('form-vendor-custom').style.display = 'none';
|
|
document.getElementById('form-vendor-custom').required = false;
|
|
document.getElementById('form-vendor-custom').value = '';
|
|
|
|
if (vendorVal) {
|
|
let optionFound = false;
|
|
for (let option of select.options) {
|
|
if (option.value && option.value !== '__custom__' && String(option.value) === vendorVal) {
|
|
select.value = option.value;
|
|
optionFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!optionFound) {
|
|
const matchedVendor = activeVendors.find(v => String(v.vendor_name).toLowerCase().trim() === vendorVal.toLowerCase());
|
|
if (matchedVendor) {
|
|
select.value = String(matchedVendor.id);
|
|
optionFound = true;
|
|
}
|
|
}
|
|
|
|
if (!optionFound) {
|
|
select.value = '__custom__';
|
|
const customInput = document.getElementById('form-vendor-custom');
|
|
customInput.style.display = 'block';
|
|
customInput.required = true;
|
|
customInput.value = row.vendor_name;
|
|
}
|
|
}
|
|
|
|
// Populate/Reset PDF elements
|
|
document.getElementById('form-pdf').value = '';
|
|
const existingPdfContainer = document.getElementById('form-existing-pdf-container');
|
|
const existingPdfLink = document.getElementById('form-existing-pdf-link');
|
|
if (row.file_path) {
|
|
existingPdfContainer.style.display = 'block';
|
|
let pdfUrl = row.file_path;
|
|
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
|
|
pdfUrl = contextPath + pdfUrl.substring(1);
|
|
} else {
|
|
pdfUrl = contextPath + pdfUrl;
|
|
}
|
|
existingPdfLink.href = pdfUrl;
|
|
} else {
|
|
existingPdfContainer.style.display = 'none';
|
|
}
|
|
|
|
recordModalObj.show();
|
|
}
|
|
|
|
// SAVE Record API call
|
|
async function saveRecord(event) {
|
|
event.preventDefault();
|
|
showLoader(true, 'Saving record details...');
|
|
|
|
const id = document.getElementById('form-id').value;
|
|
const received_date = document.getElementById('form-received-date').value;
|
|
const type = document.getElementById('form-type').value;
|
|
const subject = document.getElementById('form-subject').value;
|
|
const amount = document.getElementById('form-amount').value;
|
|
const invoice_no = document.getElementById('form-invoice-no').value;
|
|
const receiptno = document.getElementById('form-receiptno').value;
|
|
const invoice_date = document.getElementById('form-invoice-date').value;
|
|
const physical_file_no = document.getElementById('form-physical-file-no').value;
|
|
const tatEl = document.getElementById('form-tat');
|
|
const tat = tatEl ? tatEl.value : '';
|
|
const status = document.getElementById('form-status').value;
|
|
// Preserve original values if they were loaded in edit mode
|
|
let originalRecord = null;
|
|
if (id) {
|
|
originalRecord = allRecords.find(r => String(r.id) === String(id));
|
|
}
|
|
const followupEl = document.getElementById('form-followup');
|
|
const followup = followupEl ? followupEl.value : (originalRecord ? originalRecord.followup : '');
|
|
const myfollowupEl = document.getElementById('form-myfollowup');
|
|
const myfollowup = myfollowupEl ? myfollowupEl.value : (originalRecord ? originalRecord.myfollowup : '');
|
|
const orderbyEl = document.getElementById('form-orderby');
|
|
const order_by = orderbyEl ? orderbyEl.value : (originalRecord ? originalRecord.order_by : '');
|
|
const remarks = document.getElementById('form-remarks').value;
|
|
|
|
// Resolve vendor
|
|
const vendorSelect = document.getElementById('form-vendor-select').value;
|
|
const vendorName = vendorSelect === '__custom__' ? document.getElementById('form-vendor-custom').value : vendorSelect;
|
|
|
|
if (!vendorName) {
|
|
alert('Please select or specify a vendor.');
|
|
showLoader(false);
|
|
return;
|
|
}
|
|
|
|
// Upload PDF if selected
|
|
let filePath = originalRecord ? originalRecord.file_path : null;
|
|
const pdfInput = document.getElementById('form-pdf');
|
|
if (pdfInput && pdfInput.files && pdfInput.files.length > 0) {
|
|
showLoader(true, 'Uploading PDF document...');
|
|
const formData = new FormData();
|
|
formData.append('file', pdfInput.files[0]);
|
|
|
|
const csrfHeaders = getCsrfHeaders();
|
|
try {
|
|
const uploadResponse = await fetch(contextPath + 'gccservicepaymenttracker/api/upload-pdf', {
|
|
method: 'POST',
|
|
headers: {
|
|
...csrfHeaders
|
|
},
|
|
body: formData
|
|
});
|
|
const uploadResult = await uploadResponse.json();
|
|
if (uploadResult.success) {
|
|
filePath = uploadResult.filePath;
|
|
} else {
|
|
alert(uploadResult.error || 'Failed to upload PDF file.');
|
|
showLoader(false);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.error("Upload error:", err);
|
|
alert("Failed to upload PDF due to connection error.");
|
|
showLoader(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const payload = {
|
|
id, received_date, type, vendor_name: vendorName, subject, amount, invoice_no,
|
|
receiptno, invoice_date, physical_file_no, tat, status, followup,
|
|
myfollowup, order_by, remarks, file_path: filePath
|
|
};
|
|
|
|
const csrfHeaders = getCsrfHeaders();
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...csrfHeaders
|
|
};
|
|
|
|
try {
|
|
showLoader(true, 'Saving record details...');
|
|
const response = await fetch(contextPath + 'gccservicepaymenttracker/api/save', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
recordModalObj.hide();
|
|
loadCounts();
|
|
loadList();
|
|
} else {
|
|
alert(data.error || 'Failed to save record.');
|
|
showLoader(false);
|
|
}
|
|
} catch (err) {
|
|
console.error("Save Error:", err);
|
|
alert("Failed to save record due to connection error.");
|
|
showLoader(false);
|
|
}
|
|
}
|
|
|
|
// Calculate age of bill (days) dynamically from received date
|
|
function calculateAgeOfBill(receivedDateStr) {
|
|
if (!receivedDateStr) return '0';
|
|
try {
|
|
const recDate = new Date(receivedDateStr);
|
|
if (isNaN(recDate.getTime())) return '0';
|
|
recDate.setHours(0, 0, 0, 0);
|
|
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
const graceDate = new Date(recDate);
|
|
graceDate.setDate(graceDate.getDate());
|
|
|
|
if (today <= graceDate) {
|
|
return '0';
|
|
} else {
|
|
const diffTime = today.getTime() - graceDate.getTime();
|
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
return String(diffDays);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error calculating age of bill:", e);
|
|
return '0';
|
|
}
|
|
}
|
|
|
|
// --- NEW METRICS & INTERACTIVE CLICK FILTER LOGIC ---
|
|
function calculateSummaries() {
|
|
let total = allRecords.length;
|
|
|
|
// Dynamic Status Counts Map
|
|
const statusCounts = {};
|
|
activeStatus.forEach(s => {
|
|
statusCounts[s.id] = 0;
|
|
});
|
|
|
|
let age_1_15 = 0;
|
|
let age_16_30 = 0;
|
|
let age_31_60 = 0;
|
|
let age_61_90 = 0;
|
|
let age_91_120 = 0;
|
|
let age_over_120 = 0;
|
|
|
|
const vendorMap = {};
|
|
|
|
allRecords.forEach(row => {
|
|
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 isCompleted = matchedStatus && getStatusKey(matchedStatus.status_name) === 'completed';
|
|
|
|
const age = parseInt(calculateAgeOfBill(row.received_date), 10);
|
|
const vendorName = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
|
|
|
|
if (statusId !== null) {
|
|
statusCounts[statusId] = (statusCounts[statusId] || 0) + 1;
|
|
}
|
|
|
|
if (!isCompleted) {
|
|
if (age >= 1 && age <= 15) age_1_15++;
|
|
else if (age >= 16 && age <= 30) age_16_30++;
|
|
else if (age >= 31 && age <= 60) age_31_60++;
|
|
else if (age >= 61 && age <= 90) age_61_90++;
|
|
else if (age >= 91 && age <= 120) age_91_120++;
|
|
else if (age > 120) age_over_120++;
|
|
}
|
|
|
|
if (!vendorMap[vendorName]) {
|
|
vendorMap[vendorName] = {
|
|
vendorName: vendorName,
|
|
total: 0,
|
|
statusCounts: {}, // Map status ID to count
|
|
age_1_15: 0,
|
|
age_16_30: 0,
|
|
age_31_60: 0,
|
|
age_61_90: 0,
|
|
age_91_120: 0,
|
|
age_over_120: 0,
|
|
comp_age_1_15: 0,
|
|
comp_age_16_30: 0,
|
|
comp_age_31_60: 0,
|
|
comp_age_61_90: 0,
|
|
comp_age_91_120: 0,
|
|
comp_age_over_120: 0
|
|
};
|
|
activeStatus.forEach(s => {
|
|
vendorMap[vendorName].statusCounts[s.id] = 0;
|
|
});
|
|
}
|
|
|
|
const vData = vendorMap[vendorName];
|
|
vData.total++;
|
|
if (statusId !== null) {
|
|
vData.statusCounts[statusId] = (vData.statusCounts[statusId] || 0) + 1;
|
|
}
|
|
|
|
if (isCompleted) {
|
|
if (age >= 1 && age <= 15) vData.comp_age_1_15++;
|
|
else if (age >= 16 && age <= 30) vData.comp_age_16_30++;
|
|
else if (age >= 31 && age <= 60) vData.comp_age_31_60++;
|
|
else if (age >= 61 && age <= 90) vData.comp_age_61_90++;
|
|
else if (age >= 91 && age <= 120) vData.comp_age_91_120++;
|
|
else if (age > 120) vData.comp_age_over_120++;
|
|
} else {
|
|
if (age >= 1 && age <= 15) vData.age_1_15++;
|
|
else if (age >= 16 && age <= 30) vData.age_16_30++;
|
|
else if (age >= 31 && age <= 60) vData.age_31_60++;
|
|
else if (age >= 61 && age <= 90) vData.age_61_90++;
|
|
else if (age >= 91 && age <= 120) vData.age_91_120++;
|
|
else if (age > 120) vData.age_over_120++;
|
|
}
|
|
});
|
|
let completedTotal = 0;
|
|
const completedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed');
|
|
completedStatuses.forEach(s => {
|
|
completedTotal += (statusCounts[s.id] || 0);
|
|
});
|
|
|
|
const pendingTotal = total - completedTotal;
|
|
|
|
let summaryBodyHtml = `
|
|
<td id="summary-total" class="cursor-pointer text-primary" style="background-color: #f8fafc;" onclick="openModalForCount('total', null, 'Total No of files')">${total}</td>
|
|
<td id="summary-completed" class="cursor-pointer text-success" onclick="openModalForCount('completed_custom', null, 'Completed Files')">${completedTotal}</td>
|
|
<td id="summary-pending" class="cursor-pointer text-danger" style="color: #991b1b; font-weight: bold;" onclick="openModalForCount('pending_custom', null, 'Pending Files')">${pendingTotal}</td>
|
|
`;
|
|
summaryBodyHtml += `
|
|
<td id="summary-1-15" class="cursor-pointer" onclick="openModalForCount('age', [1, 15], 'Age: 1 to 15 Days')">${age_1_15}</td>
|
|
<td id="summary-16-30" class="cursor-pointer" onclick="openModalForCount('age', [16, 30], 'Age: 16 to 30 Days')">${age_16_30}</td>
|
|
<td id="summary-31-60" class="cursor-pointer" onclick="openModalForCount('age', [31, 60], 'Age: 31 to 60 Days')">${age_31_60}</td>
|
|
<td id="summary-61-90" class="cursor-pointer" onclick="openModalForCount('age', [61, 90], 'Age: 61 to 90 Days')">${age_61_90}</td>
|
|
<td id="summary-91-120" class="cursor-pointer" onclick="openModalForCount('age', [91, 120], 'Age: 91 to 120 Days')">${age_91_120}</td>
|
|
<td id="summary-over-120" class="cursor-pointer text-danger" onclick="openModalForCount('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(labels, data, colors) {
|
|
const ctx = document.getElementById('statusPieChart')?.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
if (statusChartInstance) {
|
|
statusChartInstance.destroy();
|
|
}
|
|
|
|
statusChartInstance = new Chart(ctx, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
data: data,
|
|
backgroundColor: colors,
|
|
borderWidth: 1,
|
|
borderColor: '#ffffff'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
boxWidth: 12,
|
|
font: {
|
|
size: 10,
|
|
family: 'Poppins'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderVendorBreakdown() {
|
|
const tbody = document.getElementById('vendor-breakdown-tbody');
|
|
if (!tbody) return;
|
|
tbody.innerHTML = '';
|
|
|
|
const searchVal = document.getElementById('vendor-breakdown-search')?.value || '';
|
|
const query = searchVal.toLowerCase().trim();
|
|
|
|
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 + 1 + 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;
|
|
const sumStatuses = {};
|
|
activeStatus.forEach(s => {
|
|
sumStatuses[s.id] = 0;
|
|
});
|
|
let sumAge1_15 = 0;
|
|
let sumAge16_30 = 0;
|
|
let sumAge31_60 = 0;
|
|
let sumAge61_90 = 0;
|
|
let sumAge91_120 = 0;
|
|
let sumAgeOver120 = 0;
|
|
|
|
filtered.forEach(v => {
|
|
sumTotal += v.total;
|
|
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;
|
|
sumAge61_90 += v.age_61_90;
|
|
sumAge91_120 += v.age_91_120;
|
|
sumAgeOver120 += v.age_over_120;
|
|
|
|
const tr = document.createElement('tr');
|
|
|
|
// 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="openModalForCount('vendor_total', null, 'Vendor: ' + this.getAttribute('data-vendor'), this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${v.total}</td>
|
|
`;
|
|
|
|
// 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>`;
|
|
});
|
|
|
|
trHtml += `
|
|
<td class="cursor-pointer" onclick="openModalForCount('vendor_age', [1, 15], '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="openModalForCount('vendor_age', [16, 30], '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="openModalForCount('vendor_age', [31, 60], '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="openModalForCount('vendor_age', [61, 90], '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="openModalForCount('vendor_age', [91, 120], '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="openModalForCount('vendor_age', [121, 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);
|
|
});
|
|
|
|
// Append TOTAL row at the bottom
|
|
const totalTr = document.createElement('tr');
|
|
totalTr.className = 'table-warning fw-bold text-dark';
|
|
totalTr.style.borderTop = '2px solid #cbd5e1';
|
|
|
|
let totalTrHtml = `
|
|
<td class="text-start">TOTAL</td>
|
|
<td class="cursor-pointer text-primary fw-bold" onclick="openModalForCount('total', null, 'Total No of Files')">${sumTotal}</td>
|
|
`;
|
|
|
|
// 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>
|
|
<td class="cursor-pointer" onclick="openModalForCount('age', [31, 60], 'Age: 31-60 Days')">${sumAge31_60}</td>
|
|
<td class="cursor-pointer" onclick="openModalForCount('age', [61, 90], 'Age: 61-90 Days')">${sumAge61_90}</td>
|
|
<td class="cursor-pointer" onclick="openModalForCount('age', [91, 120], 'Age: 91-120 Days')">${sumAge91_120}</td>
|
|
<td class="cursor-pointer text-danger" onclick="openModalForCount('age', [121, null], 'Age: Over 120 Days')">${sumAgeOver120}</td>
|
|
`;
|
|
totalTr.innerHTML = totalTrHtml;
|
|
tbody.appendChild(totalTr);
|
|
}
|
|
|
|
function filterVendorBreakdown(val) {
|
|
renderVendorBreakdown();
|
|
}
|
|
|
|
function setClickFilter(type, value, displayName, vendorName = null, col = null) {
|
|
activeClickFilter = { type, value, displayName, vendorName, col };
|
|
|
|
const badge = document.getElementById('click-filter-badge');
|
|
if (badge) {
|
|
badge.innerHTML = `Filter: ${displayName} <i class="fa-solid fa-circle-xmark ms-1 cursor-pointer" onclick="clearClickFilter(event)"></i>`;
|
|
badge.style.display = 'inline-block';
|
|
badge.style.cursor = 'pointer';
|
|
}
|
|
|
|
currentPage = 1;
|
|
renderTable();
|
|
|
|
// Smoothly scroll down to the table card
|
|
document.querySelector('.table-card')?.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
function clearClickFilter(event) {
|
|
if (event) event.stopPropagation();
|
|
activeClickFilter = null;
|
|
const badge = document.getElementById('click-filter-badge');
|
|
if (badge) {
|
|
badge.style.display = 'none';
|
|
}
|
|
currentPage = 1;
|
|
renderTable();
|
|
}
|
|
|
|
// --- COUNT DETAILS MODAL LOGIC ---
|
|
let modalClickFilter = null; // Stores { clickType, clickValue, displayName, vendorName, col }
|
|
let countDetailsModalObj = null;
|
|
|
|
function openModalForCount(clickType, clickValue, displayName, vendorName = null, col = null) {
|
|
modalClickFilter = { clickType, clickValue, displayName, vendorName, col };
|
|
|
|
// Set modal title
|
|
document.getElementById('countDetailsModalLabel').innerHTML = `<i class="fa-solid fa-list-check me-2"></i>Count Details: ${escapeHtml(displayName)}`;
|
|
|
|
// Initialize modal dropdown options
|
|
populateModalDropdowns();
|
|
|
|
// Set default selected values in modal dropdowns based on click filters
|
|
const modalTypeSelect = document.getElementById('modal-filter-type');
|
|
const modalStatusSelect = document.getElementById('modal-filter-status');
|
|
const modalVendorSelect = document.getElementById('modal-filter-vendor');
|
|
|
|
// Reset to default
|
|
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) {
|
|
modalTypeSelect.value = clickValue;
|
|
}
|
|
|
|
// If click was status-based
|
|
if (clickType === 'status' && clickValue) {
|
|
modalStatusSelect.value = clickValue;
|
|
} else if (clickType === 'vendor_status' && col) {
|
|
modalStatusSelect.value = col;
|
|
}
|
|
|
|
// If click was vendor-based
|
|
if (vendorName) {
|
|
// Find matching option or set value
|
|
for (let opt of modalVendorSelect.options) {
|
|
if (opt.textContent.trim().toLowerCase() === vendorName.trim().toLowerCase() || opt.value === vendorName) {
|
|
modalVendorSelect.value = opt.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render the modal table
|
|
applyModalFilters();
|
|
|
|
// Show modal
|
|
if (!countDetailsModalObj) {
|
|
countDetailsModalObj = new bootstrap.Modal(document.getElementById('countDetailsModal'));
|
|
}
|
|
countDetailsModalObj.show();
|
|
}
|
|
|
|
function populateModalDropdowns() {
|
|
const modalTypeSelect = document.getElementById('modal-filter-type');
|
|
const modalStatusSelect = document.getElementById('modal-filter-status');
|
|
const modalVendorSelect = document.getElementById('modal-filter-vendor');
|
|
|
|
if (modalTypeSelect) {
|
|
modalTypeSelect.innerHTML = '<option value="all">- All Types -</option>';
|
|
const optNotUpdated = document.createElement('option');
|
|
optNotUpdated.value = 'status_not_updated';
|
|
optNotUpdated.textContent = 'Status Not Updated';
|
|
modalTypeSelect.appendChild(optNotUpdated);
|
|
|
|
activeTypes.forEach(t => {
|
|
const opt = document.createElement('option');
|
|
opt.value = getCategoryCode(t.type_name);
|
|
opt.textContent = t.type_name;
|
|
modalTypeSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
if (modalStatusSelect) {
|
|
modalStatusSelect.innerHTML = '<option value="all">- All Statuses -</option>';
|
|
activeStatus.forEach(s => {
|
|
const opt = document.createElement('option');
|
|
opt.value = s.id;
|
|
opt.textContent = s.status_name;
|
|
modalStatusSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
if (modalVendorSelect) {
|
|
modalVendorSelect.innerHTML = '<option value="all">- All Vendors -</option>';
|
|
activeVendors.forEach(v => {
|
|
const opt = document.createElement('option');
|
|
opt.value = String(v.vendor_name).trim(); // Match by vendor_name text
|
|
opt.textContent = v.vendor_name;
|
|
modalVendorSelect.appendChild(opt);
|
|
});
|
|
}
|
|
}
|
|
|
|
function applyModalFilters() {
|
|
if (!modalClickFilter) return;
|
|
|
|
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;
|
|
|
|
let filtered = allRecords.filter(row => {
|
|
// 1. Apply the original click filters from the cell / card
|
|
let age = parseInt(calculateAgeOfBill(row.received_date), 10);
|
|
|
|
if (clickType === 'category') {
|
|
if (clickValue === 'status_not_updated') {
|
|
const rowType = String(row.type || '').trim();
|
|
if (rowType !== '') return false;
|
|
} else {
|
|
const rowCategory = getCategoryCode(row.resolved_type || row.type);
|
|
if (rowCategory !== clickValue) return false;
|
|
}
|
|
}
|
|
|
|
if (clickType === 'age') {
|
|
// Exclude completed files for pending age column clicks
|
|
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;
|
|
|
|
const [min, max] = clickValue;
|
|
if (max !== null) {
|
|
if (age < min || age > max) return false;
|
|
} else {
|
|
if (age < min) return false;
|
|
}
|
|
}
|
|
|
|
if (clickType === 'completed_custom') {
|
|
// Filter for completed status
|
|
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 === 'completed_age') {
|
|
// Filter for completed status
|
|
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;
|
|
|
|
const [min, max] = clickValue;
|
|
if (max !== null) {
|
|
if (age < min || age > max) return false;
|
|
} else {
|
|
if (age < min) return false;
|
|
}
|
|
}
|
|
|
|
if (clickType === 'status') {
|
|
const rowStatusStr = String(row.status || '').trim();
|
|
const matched = activeStatus.find(s => String(s.id) === clickValue);
|
|
if (matched) {
|
|
const matches = rowStatusStr === String(matched.id) ||
|
|
rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
|
|
if (!matches) return false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (clickType === 'vendor_status') {
|
|
const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
|
|
if (rowVendor !== vendorName) return false;
|
|
|
|
const rowStatusStr = String(row.status || '').trim();
|
|
const matched = activeStatus.find(s => String(s.id) === col);
|
|
if (matched) {
|
|
const matches = rowStatusStr === String(matched.id) ||
|
|
rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
|
|
if (!matches) return false;
|
|
}
|
|
}
|
|
|
|
if (clickType === 'vendor_age') {
|
|
const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
|
|
if (rowVendor !== vendorName) return false;
|
|
|
|
// Exclude completed files for pending vendor age clicks
|
|
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;
|
|
|
|
const [min, max] = clickValue;
|
|
if (max !== null) {
|
|
if (age < min || age > max) return false;
|
|
} else {
|
|
if (age < min) return false;
|
|
}
|
|
}
|
|
|
|
if (clickType === 'vendor_completed_age') {
|
|
const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
|
|
if (rowVendor !== vendorName) return false;
|
|
|
|
// Filter for completed status
|
|
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;
|
|
|
|
const [min, max] = clickValue;
|
|
if (max !== null) {
|
|
if (age < min || age > max) return false;
|
|
} else {
|
|
if (age < min) return false;
|
|
}
|
|
}
|
|
|
|
if (clickType === 'latest_updates') {
|
|
if (row.updated_date !== clickValue) return false;
|
|
}
|
|
|
|
// 2. Apply the modal dropdown filters
|
|
if (selectedType !== 'all') {
|
|
if (selectedType === 'status_not_updated') {
|
|
const rowType = String(row.type || '').trim();
|
|
if (rowType !== '') return false;
|
|
} else {
|
|
const rowCategory = getCategoryCode(row.resolved_type || row.type);
|
|
if (rowCategory !== selectedType) return false;
|
|
}
|
|
}
|
|
|
|
if (selectedStatus !== 'all') {
|
|
const rowStatusStr = String(row.status || '').trim();
|
|
const matched = activeStatus.find(s => String(s.id) === selectedStatus);
|
|
if (matched) {
|
|
const matches = rowStatusStr === String(matched.id) ||
|
|
rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
|
|
if (!matches) return false;
|
|
}
|
|
}
|
|
|
|
if (selectedVendor !== 'all') {
|
|
const rowVendor = String(row.resolved_vendor || row.vendor_name || '').trim();
|
|
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;
|
|
});
|
|
|
|
renderModalTable(filtered);
|
|
}
|
|
|
|
function renderModalTable(data) {
|
|
const tbody = document.getElementById('modal-tracking-tbody');
|
|
const noRecords = document.getElementById('modal-no-records-found');
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
if (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, filePath) => {
|
|
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, filePath);
|
|
}
|
|
label = typeVal || '-';
|
|
badgeClass = 'status-default';
|
|
}
|
|
|
|
let onClickAttr = '';
|
|
if (filePath) {
|
|
let pdfUrl = filePath;
|
|
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
|
|
pdfUrl = contextPath + pdfUrl.substring(1);
|
|
} else {
|
|
pdfUrl = contextPath + pdfUrl;
|
|
}
|
|
onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
|
|
} else {
|
|
onClickAttr = `onclick="alert('pdf file not uploaded')"`;
|
|
}
|
|
|
|
return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${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, row.file_path)}</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-muted">${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'; // avoid wrapping action buttons
|
|
|
|
if (row.file_path) {
|
|
const btnViewPdf = document.createElement('button');
|
|
btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
|
|
btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
|
|
btnViewPdf.title = 'View PDF Document';
|
|
btnViewPdf.onclick = () => {
|
|
let pdfUrl = row.file_path;
|
|
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
|
|
pdfUrl = contextPath + pdfUrl.substring(1);
|
|
} else {
|
|
pdfUrl = contextPath + pdfUrl;
|
|
}
|
|
window.open(pdfUrl, '_blank');
|
|
};
|
|
tdActions.appendChild(btnViewPdf);
|
|
}
|
|
|
|
// History Button
|
|
const btnHistory = document.createElement('button');
|
|
btnHistory.className = 'btn-grid-action btn-outline-secondary me-1';
|
|
btnHistory.innerHTML = '<i class="fa-solid fa-clock-rotate-left"></i>';
|
|
btnHistory.style.border = '1px solid #cbd5e1';
|
|
btnHistory.title = 'View Update History';
|
|
btnHistory.onclick = () => {
|
|
showHistoryModal(row.id);
|
|
};
|
|
tdActions.appendChild(btnHistory);
|
|
|
|
if (isAdmin) {
|
|
const btnEdit = document.createElement('button');
|
|
btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
|
|
btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
|
|
btnEdit.title = 'Edit Record';
|
|
btnEdit.onclick = () => {
|
|
openEditModal(row);
|
|
};
|
|
|
|
const btnDelete = document.createElement('button');
|
|
btnDelete.className = 'btn-grid-action btn-grid-delete';
|
|
btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
|
|
btnDelete.title = 'Delete Record';
|
|
btnDelete.onclick = () => {
|
|
deleteRecord(row.id);
|
|
};
|
|
|
|
tdActions.appendChild(btnEdit);
|
|
tdActions.appendChild(btnDelete);
|
|
}
|
|
tr.appendChild(tdActions);
|
|
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
// --- EXPORT TO EXCEL LOGIC ---
|
|
function exportTableToExcelHelper(tableId, filename) {
|
|
const originalTable = document.getElementById(tableId);
|
|
if (!originalTable) {
|
|
alert("Table element not found.");
|
|
return;
|
|
}
|
|
|
|
// Clone table to modify it and strip actions column
|
|
const clonedTable = originalTable.cloneNode(true);
|
|
|
|
// Find Actions column index
|
|
const headers = clonedTable.querySelectorAll('thead th');
|
|
let actionsColIndex = -1;
|
|
headers.forEach((th, index) => {
|
|
const text = th.textContent.trim().toLowerCase();
|
|
if (text === 'actions' || text === 'action') {
|
|
actionsColIndex = index;
|
|
th.remove();
|
|
}
|
|
});
|
|
|
|
if (actionsColIndex !== -1) {
|
|
const rows = clonedTable.querySelectorAll('tbody tr');
|
|
rows.forEach(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
if (cells[actionsColIndex]) {
|
|
cells[actionsColIndex].remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Find Type of File column index to append PDF Status
|
|
let typeColIndex = -1;
|
|
clonedTable.querySelectorAll('thead th').forEach((th, index) => {
|
|
if (th.textContent.trim().toLowerCase() === 'type of file') {
|
|
typeColIndex = index;
|
|
}
|
|
});
|
|
|
|
if (typeColIndex !== -1) {
|
|
// Insert header "PDF Status" right after "Type of File"
|
|
const th = document.createElement('th');
|
|
th.textContent = 'PDF Status';
|
|
th.style.backgroundColor = '#f0fdf4';
|
|
th.style.color = '#166534';
|
|
|
|
const theadTr = clonedTable.querySelector('thead tr');
|
|
const nextTh = theadTr.children[typeColIndex + 1];
|
|
if (nextTh) {
|
|
theadTr.insertBefore(th, nextTh);
|
|
} else {
|
|
theadTr.appendChild(th);
|
|
}
|
|
|
|
// Insert td for each row
|
|
clonedTable.querySelectorAll('tbody tr').forEach(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
const typeCell = cells[typeColIndex];
|
|
let pdfStatusText = 'pdf file not uploaded';
|
|
if (typeCell) {
|
|
const span = typeCell.querySelector('span');
|
|
if (span && span.getAttribute('title') && span.getAttribute('title').toLowerCase().includes('click')) {
|
|
pdfStatusText = 'PDF Uploaded';
|
|
}
|
|
}
|
|
|
|
const td = document.createElement('td');
|
|
td.textContent = pdfStatusText;
|
|
|
|
const nextTd = row.children[typeColIndex + 1];
|
|
if (nextTd) {
|
|
row.insertBefore(td, nextTd);
|
|
} else {
|
|
row.appendChild(td);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Convert to Excel workbook and download
|
|
const wb = XLSX.utils.table_to_book(clonedTable, { raw: true, sheet: "Sheet1" });
|
|
XLSX.writeFile(wb, filename);
|
|
}
|
|
|
|
function exportModalTableToExcel() {
|
|
exportTableToExcelHelper('modal-tracking-table', 'Payment_Tracking_Details_Modal_Report.xlsx');
|
|
}
|
|
|
|
function exportMainTableToExcel() {
|
|
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>PDF Status</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>${row.file_path ? 'PDF Uploaded' : 'pdf file not uploaded'}</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 ---
|
|
function updateLatestUpdateBadge() {
|
|
const badge = document.getElementById('latest-update-badge');
|
|
if (!badge) return;
|
|
|
|
if (!allRecords || allRecords.length === 0) {
|
|
badge.style.setProperty('display', 'none', 'important');
|
|
return;
|
|
}
|
|
|
|
// Find the maximum updated_date
|
|
let maxTime = 0;
|
|
let maxDateStr = null;
|
|
|
|
allRecords.forEach(r => {
|
|
if (r.updated_date) {
|
|
const time = new Date(r.updated_date).getTime();
|
|
if (!isNaN(time) && time > maxTime) {
|
|
maxTime = time;
|
|
maxDateStr = r.updated_date;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (maxTime === 0 || !maxDateStr) {
|
|
badge.style.setProperty('display', 'none', 'important');
|
|
return;
|
|
}
|
|
|
|
// Count how many files share this exact latest update date/time stamp
|
|
let count = 0;
|
|
allRecords.forEach(r => {
|
|
if (r.updated_date === maxDateStr) {
|
|
count++;
|
|
}
|
|
});
|
|
|
|
// Formatted date string helper
|
|
const formatDateTimeHelper = 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 countEl = document.getElementById('latest-update-count');
|
|
const timeEl = document.getElementById('latest-update-time');
|
|
if (countEl) countEl.textContent = count;
|
|
if (timeEl) timeEl.textContent = formatDateTimeHelper(maxDateStr);
|
|
|
|
badge.style.setProperty('display', 'flex', 'important');
|
|
badge.setAttribute('data-latest-date', maxDateStr);
|
|
}
|
|
|
|
function openLatestUpdatesModal() {
|
|
const badge = document.getElementById('latest-update-badge');
|
|
if (!badge) return;
|
|
const latestDate = badge.getAttribute('data-latest-date');
|
|
if (!latestDate) return;
|
|
|
|
openModalForCount('latest_updates', latestDate, 'Latest Updates');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
// --- END OF NEW METRICS & INTERACTIVE CLICK FILTER LOGIC ---
|
|
|
|
// Delete record API call with remarks confirmation modal
|
|
let recordIdToDelete = null;
|
|
let deleteConfirmModalObj = null;
|
|
|
|
function deleteRecord(id) {
|
|
recordIdToDelete = id;
|
|
document.getElementById('delete-remarks').value = '';
|
|
|
|
if (!deleteConfirmModalObj) {
|
|
deleteConfirmModalObj = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
|
|
}
|
|
deleteConfirmModalObj.show();
|
|
}
|
|
|
|
function submitDeleteRecord() {
|
|
if (!recordIdToDelete) return;
|
|
|
|
const remarks = document.getElementById('delete-remarks').value.trim();
|
|
if (!remarks) {
|
|
alert('Please enter remarks/reason for deletion.');
|
|
return;
|
|
}
|
|
|
|
if (deleteConfirmModalObj) {
|
|
deleteConfirmModalObj.hide();
|
|
}
|
|
|
|
showLoader(true, 'Deleting record...');
|
|
|
|
const csrfHeaders = getCsrfHeaders();
|
|
const headers = {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
...csrfHeaders
|
|
};
|
|
|
|
const body = new URLSearchParams();
|
|
body.append('id', recordIdToDelete);
|
|
body.append('remarks', remarks);
|
|
|
|
fetch(contextPath + 'gccservicepaymenttracker/api/delete', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: body
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
recordIdToDelete = null;
|
|
loadCounts();
|
|
loadList();
|
|
} else {
|
|
alert(data.error || 'Failed to delete record.');
|
|
showLoader(false);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error("Delete Error:", err);
|
|
alert("Failed to delete record due to connection error.");
|
|
showLoader(false);
|
|
});
|
|
}
|
|
|
|
// Show update history for a payment tracking record
|
|
function showHistoryModal(id) {
|
|
const tbody = document.getElementById('history-tbody');
|
|
const noHistoryFound = document.getElementById('no-history-found');
|
|
|
|
tbody.innerHTML = '';
|
|
noHistoryFound.style.display = 'none';
|
|
|
|
historyModalObj.show();
|
|
|
|
// Local formatting helpers for history records
|
|
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;
|
|
};
|
|
|
|
fetch(contextPath + 'gccservicepaymenttracker/api/history?id=' + id)
|
|
.then(response => response.json())
|
|
.then(historyList => {
|
|
if (!historyList || historyList.length === 0) {
|
|
noHistoryFound.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
historyList.forEach((item, index) => {
|
|
const tr = document.createElement('tr');
|
|
|
|
const dispStatus = String(item.resolved_status || getStatusName(item.status) || '').trim();
|
|
|
|
// Document button if file path is available
|
|
let docBtnHtml = '-';
|
|
if (item.file_path) {
|
|
let pdfUrl = item.file_path;
|
|
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
|
|
pdfUrl = contextPath + pdfUrl.substring(1);
|
|
} else {
|
|
pdfUrl = contextPath + pdfUrl;
|
|
}
|
|
docBtnHtml = `<a href="${pdfUrl}" target="_blank" class="btn-grid-action btn-grid-pdf"><i class="fa-solid fa-file-pdf"></i></a>`;
|
|
}
|
|
|
|
// Resolve type badge
|
|
let typeHtml = '';
|
|
if (item.type) {
|
|
const cleanType = String(item.type).toLowerCase().trim();
|
|
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) {
|
|
label = typeObj.type_name;
|
|
}
|
|
badgeClass = 'status-default';
|
|
}
|
|
typeHtml = `<span class="type-badge ${badgeClass}">${label}</span>`;
|
|
} else {
|
|
typeHtml = '-';
|
|
}
|
|
|
|
// Resolve vendor name
|
|
let resolvedVendor = item.resolved_vendor || item.vendor_name || '-';
|
|
|
|
tr.innerHTML = `
|
|
<td class="text-center fw-semibold text-secondary">${index + 1}</td>
|
|
<td>${formatDate(item.received_date)}</td>
|
|
<td>${formatDate(item.invoice_date)}</td>
|
|
<td>${text(item.subject)}</td>
|
|
<td>${typeHtml}</td>
|
|
<td class="fw-semibold text-secondary">${text(resolvedVendor)}</td>
|
|
<td class="fw-semibold text-dark">₹ ${text(item.amount)}</td>
|
|
<td><span class="status-badge status-${dispStatus.toLowerCase().replace(/\s+/g, '-')}">${text(dispStatus)}</span></td>
|
|
<td><small class="text-muted">${text(item.remarks)}</small></td>
|
|
<td>${formatDateTime(item.updated_at)}</td>
|
|
<td class="fw-semibold text-secondary">${text(item.updated_by_name || '-')}</td>
|
|
<td class="text-center">${docBtnHtml}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
})
|
|
.catch(err => {
|
|
console.error("Fetch History Error:", err);
|
|
tbody.innerHTML = '<tr><td colspan="12" class="text-center text-danger py-3"><i class="fa-solid fa-triangle-exclamation"></i> Failed to load history records.</td></tr>';
|
|
});
|
|
}
|
|
|
|
// --- 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, filePath) => {
|
|
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, filePath);
|
|
}
|
|
label = typeVal || '-';
|
|
badgeClass = 'status-default';
|
|
}
|
|
|
|
let onClickAttr = '';
|
|
if (filePath) {
|
|
let pdfUrl = filePath;
|
|
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
|
|
pdfUrl = contextPath + pdfUrl.substring(1);
|
|
} else {
|
|
pdfUrl = contextPath + pdfUrl;
|
|
}
|
|
onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
|
|
} else {
|
|
onClickAttr = `onclick="alert('pdf file not uploaded')"`;
|
|
}
|
|
|
|
return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${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, row.file_path)}</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
|
|
const openModals = document.querySelectorAll('.modal.show');
|
|
const count = openModals.length + 1;
|
|
const zIndex = 1050 + (10 * count);
|
|
event.target.style.setProperty('z-index', zIndex, 'important');
|
|
|
|
// Adjust the backdrop z-index on next tick
|
|
setTimeout(() => {
|
|
const backdrops = document.querySelectorAll('.modal-backdrop');
|
|
if (backdrops.length > 0) {
|
|
backdrops[backdrops.length - 1].style.setProperty('z-index', zIndex - 5, 'important');
|
|
}
|
|
}, 0);
|
|
});
|
|
|
|
document.addEventListener('hidden.bs.modal', function (event) {
|
|
event.target.style.removeProperty('z-index');
|
|
// If there are still open modals, make sure the body still has the modal-open class
|
|
const openModals = document.querySelectorAll('.modal.show');
|
|
if (openModals.length > 0) {
|
|
document.body.classList.add('modal-open');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|