|
|
<!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; }
.text-success-dark { color: #166534 !important; font-weight: bold; }
.text-danger-dark { color: #991b1b !important; font-weight: bold; }
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 Bills 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; }
/* Completed Card Accent */ .category-card.card-completed::before { background-color: #166534; }
.category-card.card-completed.active { border-color: #166534; box-shadow: 0 10px 25px rgba(22, 101, 52, 0.15); background-color: #f0fdf4; }
.card-completed .card-icon { color: #166534; }
/* Pending Card Accent */ .category-card.card-pending::before { background-color: #991b1b; }
.category-card.card-pending.active { border-color: #991b1b; box-shadow: 0 10px 25px rgba(153, 27, 27, 0.15); background-color: #fef2f2; }
.card-pending .card-icon { color: #991b1b; }
.pending-split-box { transition: all 0.2s ease-in-out; }
.pending-split-box:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08); filter: brightness(0.97); }
.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">
<!-- As On Date & Completed View Mode Display Bar (Above Total No of Bills card) --> <div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2"> <div class="as-on-date-badge p-2 px-3 rounded-pill bg-white shadow-sm d-inline-flex align-items-center gap-2" style="border: 1px solid #e2e8f0; font-size: 0.88rem; color: #475569; font-weight: 600;"> <i class="fa-regular fa-calendar-check text-primary fs-6"></i> <span>As on Date: <strong id="as-on-date-val" style="color: #09303d;"></strong></span> </div>
<div class="completed-view-badge p-2 px-3 rounded-pill bg-white shadow-sm d-inline-flex align-items-center gap-2" style="border: 1px solid #e2e8f0; font-size: 0.88rem; color: #475569; font-weight: 600;"> <i class="fa-solid fa-filter text-success fs-6"></i> <span>Completed View Mode:</span> <select id="completed-view-mode" class="form-select form-select-sm border-0 py-0 ps-1 pe-4 fw-bold" style="color: #09303d; width: auto; font-size: 0.88rem; cursor: pointer; box-shadow: none;" onchange="onCompletedViewModeChange()"> <option value="current_month">Current Month Completed</option> <option value="last_month_completed">Last Month Completed List</option> <option value="all_history">All Completed (History)</option> </select> </div> </div>
<!-- Category Grid (3 Cards) --> <div class="category-grid">
<!-- Total No of Bills --> <div class="category-card card-total" onclick="openModalForCount('total', null, 'Total No of Bills')"> <div class="card-left"> <h3 class="card-title">Total No of Bills</h3> <p class="card-value" id="count-total">0</p> </div> <i class="fa-solid fa-folder-open card-icon"></i> </div>
<!-- Completed Bills --> <div class="category-card card-completed" onclick="openModalForCount('completed_custom', null, 'Completed Bills')"> <div class="card-left"> <h3 class="card-title">Completed Bills</h3> <p class="card-value" id="count-completed">0</p> </div> <i class="fa-solid fa-circle-check card-icon"></i> </div>
<!-- Pending Bills --> <div class="category-card card-pending" onclick="openModalForCount('pending_custom', null, 'Pending Bills')"> <div class="card-left flex-grow-1"> <div class="d-flex justify-content-between align-items-center"> <div> <h3 class="card-title">Pending Bills</h3> <p class="card-value" id="count-pending">0</p> </div> </div> <!-- Split: Pending with IT Cell & Pending with Others --> <div class="d-flex gap-2 mt-2 pt-2 border-top" style="border-color: #fee2e2 !important;"> <div class="pending-split-box flex-fill text-center py-1 px-2 rounded-2" style="background-color: #e0f2fe; border: 1px solid #bae6fd; cursor: pointer;" onclick="event.stopPropagation(); openModalForCount('it_cell_custom', null, 'Pending with IT Cell');" title="Click to view Pending with IT Cell"> <span class="d-block fw-bold" style="font-size: 0.68rem; color: #0369a1; text-transform: uppercase; letter-spacing: 0.3px;">IT Cell</span> <span class="fw-bold font-monospace" id="count-pending-itcell" style="font-size: 0.95rem; color: #0284c7;">0</span> </div> <div class="pending-split-box flex-fill text-center py-1 px-2 rounded-2" style="background-color: #fff7ed; border: 1px solid #ffedd5; cursor: pointer;" onclick="event.stopPropagation(); openModalForCount('others_custom', null, 'Pending with Others');" title="Click to view Pending with Others"> <span class="d-block fw-bold" style="font-size: 0.68rem; color: #c2410c; text-transform: uppercase; letter-spacing: 0.3px;">Others</span> <span class="fw-bold font-monospace" id="count-pending-others" style="font-size: 0.95rem; color: #ea580c;">0</span> </div> </div> </div> <i class="fa-solid fa-clock card-icon ms-3"></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 No of Pending Bills 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 Bills</th> <th style="background-color: #f0fdf4; color: #166534;">Completed Bills</th> <th style="background-color: #fef2f2; color: #991b1b;">Pending Bills</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 Bills')">0</td> <td id="summary-completed" class="cursor-pointer text-success" onclick="openModalForCount('completed_custom', null, 'Completed Bills')">0 </td> <td id="summary-pending" class="cursor-pointer text-danger" style="color: #991b1b; font-weight: bold;" onclick="openModalForCount('pending_custom', null, 'Pending Bills')">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>Total Bills in Percentage </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 </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 Bills</th> <th style="background-color: #f0fdf4;">Completed Bills</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 class="age-col d-none" style="background-color: #fafaf9;">1 to 15 Days</th> <th class="age-col d-none" style="background-color: #fafaf9;">16 to 30 Days</th> <th class="age-col d-none" style="background-color: #fafaf9;">31 to 60 Days</th> <th class="age-col d-none" style="background-color: #fafaf9;">61 to 90 Days</th> <th class="age-col d-none" style="background-color: #fafaf9;">91 to 120 Days</th> <th class="age-col d-none" 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 onclick="sortMainTable('received_date')" style="cursor: pointer; user-select: none;"> Received Date <span id="main-sort-received_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortMainTable('invoice_date')" style="cursor: pointer; user-select: none;"> Invoice Date <span id="main-sort-invoice_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></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>Remarks</th> <th>Mail Screenshot</th> <th>Payment Review</th> <th onclick="sortMainTable('updated_date')" style="cursor: pointer; user-select: none;">Last Updated At <span id="main-sort-updated_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></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>
<!-- Screenshot Attachment --> <div class="col-md-12 mt-3"> <label for="form-screenshot" class="form-label fw-semibold text-secondary">Screenshot Attachment (Optional)</label> <input type="file" class="form-control" id="form-screenshot" accept="image/*"> <div id="form-existing-screenshot-container" class="mt-2" style="display: none;"> <span class="text-secondary fw-semibold">Current Screenshot:</span> <a id="form-existing-screenshot-link" href="#" target="_blank" class="ms-1 text-primary fw-bold text-decoration-none"> <i class="fa-solid fa-image"></i> View Screenshot </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>
<!-- Payment Review --> <div class="col-12 mt-3"> <label for="form-payment-review" class="form-label fw-semibold text-secondary">Payment Review</label> <textarea class="form-control" id="form-payment-review" rows="2" placeholder="Enter payment review details..."></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>Payment Review</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> <!-- OLD IN-PAGE MODAL (COMMENTED OUT FOR REFERENCE - ALL COUNT METRICS NOW OPEN IN A NEW TAB)
<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;"> <div class="mb-3"> <div class="d-flex align-items-center mb-2"> <span class="text-uppercase fw-bold text-muted me-2" style="font-size: 0.75rem; letter-spacing: 0.5px;">Type Filter</span> </div> <div id="modal-type-badges" class="d-inline-flex flex-wrap gap-2 align-middle"> </div> </div> <div class="mb-4"> <div class="d-flex align-items-center mb-2"> <span class="text-uppercase fw-bold text-muted me-2" style="font-size: 0.75rem; letter-spacing: 0.5px;">Status Filter</span> </div> <div id="modal-status-badges" class="d-inline-flex flex-wrap gap-2 align-middle"> </div> </div> <div class="row g-3 align-items-end mb-4 p-3 rounded-3" style="background-color: #f8fafc; border: 1px solid #e2e8f0;"> <div class="col-md-3"> <label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Type of Bill</label> <select id="modal-filter-type" class="filter-input" onchange="applyModalFilters()"> <option value="all">- All Types -</option> </select> </div> <div class="col-md-3"> <label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Status</label> <select id="modal-filter-status" class="filter-input" onchange="applyModalFilters()"> <option value="all">- All Statuses -</option> </select> </div> <div class="col-md-3"> <label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Vendor</label> <select id="modal-filter-vendor" class="filter-input" onchange="applyModalFilters()"> <option value="all">- All Vendors -</option> </select> </div> <div class="col-md-3"> <label class="form-label fw-bold text-secondary" style="font-size: 0.82rem;">Search Table</label> <input type="text" id="modal-table-search" class="filter-input" placeholder="Search bill, vendor, dept..." oninput="applyModalFilters()"> </div> </div> <div class="d-flex justify-content-between align-items-center mb-3 px-1"> <div class="d-flex align-items-center gap-2"> <span class="badge bg-secondary px-3 py-2" id="modal-records-count" style="font-size: 0.85rem; font-weight: 600;">0 Records</span> </div> <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-1" style="border-radius: 6px; font-size: 0.82rem;" onclick="exportModalTableToExcel()"> <i class="fa-solid fa-file-excel"></i> Export Excel </button> </div> </div> <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="modal-tracking-table"> <thead class="table-light"> <tr> <th class="text-center" style="width: 50px;">Sr. No.</th> <th onclick="sortModalTable('received_date')" style="cursor: pointer; user-select: none;">Received Date <span id="modal-sort-received_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('bill_date')" style="cursor: pointer; user-select: none;">Invoice Date <span id="modal-sort-bill_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('department_name')" style="cursor: pointer; user-select: none;">Department <span id="modal-sort-department_name-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('file_no')" style="cursor: pointer; user-select: none;">Physical File No. <span id="modal-sort-file_no-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('bill_no')" style="cursor: pointer; user-select: none;">eFile No./ Receipt No. <span id="modal-sort-bill_no-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('subject')" style="cursor: pointer; user-select: none;">Subject <span id="modal-sort-subject-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('resolved_type')" style="cursor: pointer; user-select: none;">Type of File <span id="modal-sort-resolved_type-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('vendor_name')" style="cursor: pointer; user-select: none;">Vendor Name <span id="modal-sort-vendor_name-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('amount')" style="cursor: pointer; user-select: none;">Amount <span id="modal-sort-amount-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('age')" style="cursor: pointer; user-select: none;">Age of Bill (Days) <span id="modal-sort-age-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th onclick="sortModalTable('resolved_status')" style="cursor: pointer; user-select: none;">Status <span id="modal-sort-resolved_status-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th>Remarks</th> <th onclick="sortModalTable('updated_date')" style="cursor: pointer; user-select: none;">Last Updated Date <span id="modal-sort-updated_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th>Last Updated By</th> <th class="text-center" style="width: 120px;">Actions</th> </tr> </thead> <tbody id="modal-tracking-tbody"> </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> --> </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>Payment Review</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 allBackendData = [];
function getLocalDateString(dateTimeStr) { if (!dateTimeStr) return null; try { const d = new Date(dateTimeStr); if (isNaN(d.getTime())) return null; const dd = String(d.getDate()).padStart(2, '0'); const MM = String(d.getMonth() + 1).padStart(2, '0'); const yyyy = d.getFullYear(); return `${yyyy}-${MM}-${dd}`; } catch (e) { return null; } } 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 = []; let showVendorAgeBreakdown = false;
// 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 allFetched = await statusRes.json(); const fetchedStatus = allFetched.filter(s => { const name = String(s.status_name).toLowerCase().trim(); return !name.includes('duplicate') && !name.includes('no action') && !name.includes('action required'); });
// Sort activeStatus to have completed first, then non-completed in a custom logical order const completedStatuses = fetchedStatus.filter(s => getStatusKey(s.status_name) === 'completed'); const nonCompletedStatuses = fetchedStatus.filter(s => getStatusKey(s.status_name) !== 'completed');
function getNonCompletedOrderScore(statusName) { const name = String(statusName).toLowerCase(); if (name.includes('awaiting') || name.includes('details')) return 0; if (name.includes('initiated') || name.includes('to be')) return 1; if (name.includes('eoffice') || name.includes('e-office')) return 2; if (name.includes('uac')) return 3; if (name.includes('fm unit') || name.includes('fmunit') || name.includes('fm_unit')) return 4; return 99; }
nonCompletedStatuses.sort((a, b) => { const scoreA = getNonCompletedOrderScore(a.status_name); const scoreB = getNonCompletedOrderScore(b.status_name); if (scoreA !== scoreB) { return scoreA - scoreB; } return a.status_name.localeCompare(b.status_name); });
activeStatus = [...completedStatuses, ...nonCompletedStatuses];
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('fm unit')) { return 'pending with dept'; } if (name.includes('pending') || name.includes('initiated') || name.includes('eoffice') || name.includes('e-office')) { return 'pending'; } if (name.includes('no action') || name.includes('action required') || name.includes('duplicate')) { return 'no action'; } return 'default'; }
function getStatusGroup(statusName) { if (!statusName) return 'others'; const name = String(statusName).toLowerCase().trim(); if (name.includes('completed') || name.includes('approved') || name.includes('paid')) { return 'completed'; } if (name.includes('initiated') || name.includes('eoffice') || name.includes('e-office')) { return 'it_cell'; } return 'others'; }
const statusColors = { 'completed': { bg: '#f0fdf4', text: '#166534', badgeClass: 'text-success-dark' }, '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-dark' }, '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-dark' }, 'paid': { bg: '#dcfce7', text: '#166534', badgeClass: 'text-success-dark' }, 'rejected': { bg: '#fee2e2', text: '#991b1b', badgeClass: 'text-danger-dark' } };
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>'; 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 Bills</th> <th style="background-color: #f0fdf4; color: #166534;">Completed Bills</th> <th style="background-color: #fef2f2; color: #991b1b;">Pending Bills</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 Bills</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>`; });
// Pending Bills column header with click toggle icon const toggleIcon = showPendingSubBreakdown ? 'fa-angles-left' : 'fa-angles-right'; const titleText = showPendingSubBreakdown ? 'Click to hide status breakdown' : 'Click to show Pending with IT Cell & Pending with Others breakdown'; const subDNone = showPendingSubBreakdown ? '' : 'd-none';
vendorHeadHtml += ` <th class="cursor-pointer" style="background-color: #fef2f2; color: #991b1b; cursor: pointer;" onclick="togglePendingSubBreakdown()" title="${titleText}"> Pending Bills <i class="fa-solid ${toggleIcon} ms-1 text-muted" style="transition: transform 0.2s;"></i> </th> `;
// Sub-breakdown headers under Pending (Shown when showPendingSubBreakdown is true) vendorHeadHtml += ` <th class="pending-sub-col ${subDNone}" style="background-color: #e0f2fe; color: #0369a1;">Pending with IT Cell</th> <th class="pending-sub-col ${subDNone}" style="background-color: #fff7ed; color: #c2410c;">Pending with Others</th> `;
// Days-wise count headers - ALWAYS VISIBLE BY DEFAULT vendorHeadHtml += ` <th class="age-col" style="background-color: #fafaf9;">1 to 15 Days</th> <th class="age-col" style="background-color: #fafaf9;">16 to 30 Days</th> <th class="age-col" style="background-color: #fafaf9;">31 to 60 Days</th> <th class="age-col" style="background-color: #fafaf9;">61 to 90 Days</th> <th class="age-col" style="background-color: #fafaf9;">91 to 120 Days</th> <th class="age-col" style="background-color: #fafaf9;">Over 120 Days</th> `; vendorBreakdownTheadTr.innerHTML = vendorHeadHtml; } }
let showPendingSubBreakdown = false;
function togglePendingSubBreakdown() { showPendingSubBreakdown = !showPendingSubBreakdown; populateDropdowns(); renderVendorBreakdown(); }
// 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; }
function loadCounts() { // No-op: counts are dynamically calculated and set by calculateSummaries() }
// Sorting States and Logic let mainSortCol = ''; let mainSortAsc = true; let modalSortCol = ''; let modalSortAsc = true;
function sortMainTable(colName) { if (mainSortCol === colName) { mainSortAsc = !mainSortAsc; } else { mainSortCol = colName; mainSortAsc = true; } updateSortHeaders('main'); renderTable(); }
function sortModalTable(colName) { if (modalSortCol === colName) { modalSortAsc = !modalSortAsc; } else { modalSortCol = colName; modalSortAsc = true; } updateSortHeaders('modal'); applyModalFilters(); }
function updateSortHeaders(type) { const cols = ['received_date', 'invoice_date', 'updated_date']; cols.forEach(col => { const elementId = `${type}-sort-${col}-icon`; const el = document.getElementById(elementId); if (!el) return;
const activeCol = (type === 'main') ? mainSortCol : modalSortCol; const activeAsc = (type === 'main') ? mainSortAsc : modalSortAsc;
if (activeCol === col) { el.innerHTML = activeAsc ? `<i class="fa-solid fa-sort-up ms-1 text-primary"></i>` : `<i class="fa-solid fa-sort-down ms-1 text-primary"></i>`; } else { el.innerHTML = `<i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i>`; } }); }
// 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 => { allBackendData = data; applyCompletedViewFilter(); currentPage = 1; clearClickFilter(null); updateAsOnDateDisplay(); calculateSummaries(); renderTable(); updateLatestUpdateBadge(); if (document.getElementById('countDetailsModal')?.classList.contains('show')) { applyModalFilters(); } }) .catch(err => { console.error("Error loading details:", err); showLoader(false); }); }
function applyCompletedViewFilter() { const mode = document.getElementById('completed-view-mode')?.value || 'current_month';
const now = new Date(); const firstDayOfCurrentMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0); const firstDayOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0, 0);
allRecords = allBackendData.filter(row => { const statusName = String(row.resolved_status || getStatusName(row.status) || '').trim(); const statusVal = String(row.status || '').trim(); const matchedStatus = activeStatus.find(s => String(s.id) === statusVal || s.status_name.toLowerCase().trim() === statusName.toLowerCase().trim() ); const isCompleted = matchedStatus && getStatusKey(matchedStatus.status_name) === 'completed'; if (isCompleted) { const compDateStr = row.updated_date || row.received_date || row.cdate; let compDate = compDateStr ? new Date(compDateStr) : null; if (compDate && !isNaN(compDate.getTime())) { if (mode === 'current_month') { if (compDate < firstDayOfCurrentMonth) { return false; // Exclude completed bills of previous months } } else if (mode === 'last_month_completed') { if (compDate < firstDayOfLastMonth || compDate >= firstDayOfCurrentMonth) { return false; // Include ONLY completed bills of the previous month } } // For 'all_history', include all completed bills } } return true; }); }
function onCompletedViewModeChange() { applyCompletedViewFilter(); calculateSummaries(); renderTable(); }
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) ); });
// Sort main table dataset if (mainSortCol) { filtered.sort((a, b) => { let valA = a[mainSortCol] || ''; let valB = b[mainSortCol] || '';
let timeA = valA ? new Date(valA).getTime() : 0; let timeB = valB ? new Date(valB).getTime() : 0; if (isNaN(timeA)) timeA = 0; if (isNaN(timeB)) timeB = 0;
if (timeA !== timeB) { return mainSortAsc ? timeA - timeB : timeB - timeA; } return mainSortAsc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA)); }); }
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();
let screenshotHtml = '-'; if (row.mail_screenshot_path) { let imgUrl = row.mail_screenshot_path; if (contextPath && contextPath.endsWith('/') && imgUrl.startsWith('/')) { imgUrl = contextPath + imgUrl.substring(1); } else { imgUrl = contextPath + imgUrl; } screenshotHtml = `<a href="${imgUrl}" target="_blank" class="btn btn-sm btn-outline-info py-0 px-2" style="font-size: 0.75rem; border-color: #06b6d4; color: #06b6d4; display: inline-flex; align-items: center; gap: 4px;" title="View Screenshot"><i class="fa-solid fa-image"></i> View</a>`; }
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>${screenshotHtml}</td> <td><small class="text-muted">${text(row.payment_review)}</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); }
if (row.mail_screenshot_path) { const btnViewScreenshot = document.createElement('button'); btnViewScreenshot.className = 'btn-grid-action btn-outline-info me-1'; btnViewScreenshot.style.border = '1px solid #06b6d4'; btnViewScreenshot.style.color = '#06b6d4'; btnViewScreenshot.innerHTML = '<i class="fa-solid fa-image"></i>'; btnViewScreenshot.title = 'View Screenshot'; btnViewScreenshot.onclick = () => { let imgUrl = row.mail_screenshot_path; if (contextPath && contextPath.endsWith('/') && imgUrl.startsWith('/')) { imgUrl = contextPath + imgUrl.substring(1); } else { imgUrl = contextPath + imgUrl; } window.open(imgUrl, '_blank'); }; tdActions.appendChild(btnViewScreenshot); }
// 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';
// Reset Screenshot upload elements document.getElementById('form-screenshot').value = ''; document.getElementById('form-existing-screenshot-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 || ''; document.getElementById('form-payment-review').value = row.payment_review || '';
// 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'; }
// Populate/Reset Screenshot elements document.getElementById('form-screenshot').value = ''; const existingScreenshotContainer = document.getElementById('form-existing-screenshot-container'); const existingScreenshotLink = document.getElementById('form-existing-screenshot-link'); if (row.mail_screenshot_path) { existingScreenshotContainer.style.display = 'block'; let imgUrl = row.mail_screenshot_path; if (contextPath && contextPath.endsWith('/') && imgUrl.startsWith('/')) { imgUrl = contextPath + imgUrl.substring(1); } else { imgUrl = contextPath + imgUrl; } existingScreenshotLink.href = imgUrl; } else { existingScreenshotContainer.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; const payment_review = document.getElementById('form-payment-review').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; } }
// Upload Screenshot if selected let screenshotPath = originalRecord ? originalRecord.mail_screenshot_path : null; const screenshotInput = document.getElementById('form-screenshot'); if (screenshotInput && screenshotInput.files && screenshotInput.files.length > 0) { showLoader(true, 'Uploading Screenshot...'); const formData = new FormData(); formData.append('file', screenshotInput.files[0]);
const csrfHeaders = getCsrfHeaders(); try { const uploadResponse = await fetch(contextPath + 'gccservicepaymenttracker/api/upload-screenshot', { method: 'POST', headers: { ...csrfHeaders }, body: formData }); const uploadResult = await uploadResponse.json(); if (uploadResult.success) { screenshotPath = uploadResult.filePath; } else { alert(uploadResult.error || 'Failed to upload Screenshot.'); showLoader(false); return; } } catch (err) { console.error("Upload error:", err); alert("Failed to upload Screenshot 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, payment_review, file_path: filePath, mail_screenshot_path: screenshotPath };
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'; } }
function updateAsOnDateDisplay() { const now = new Date(); const dd = String(now.getDate()).padStart(2, '0'); const mm = String(now.getMonth() + 1).padStart(2, '0'); const yyyy = now.getFullYear(); const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; const monthName = monthNames[now.getMonth()];
const formatted = `${dd}/${mm}/${yyyy} (${monthName} ${yyyy})`; const el = document.getElementById('as-on-date-val'); if (el) el.textContent = formatted; }
// --- NEW METRICS & INTERACTIVE CLICK FILTER LOGIC --- function calculateSummaries() { let rawTotal = 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 = {}; let allCompletedCount = 0; let totalItCell = 0; let totalOthers = 0;
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 (isCompleted) { allCompletedCount++; if (statusId !== null) { statusCounts[statusId] = (statusCounts[statusId] || 0) + 1; } } else { if (statusId !== null) { statusCounts[statusId] = (statusCounts[statusId] || 0) + 1; }
if (age >= 1 && age <= 15) age_1_15++; else if (age >= 16 && age <= 30) age_16_30++; 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 itCellCount: 0, othersCount: 0, 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 { const group = getStatusGroup(statusName); if (group === 'it_cell') { totalItCell++; vData.itCellCount++; } else { totalOthers++; vData.othersCount++; }
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 total = rawTotal; let completedTotal = allCompletedCount; let pendingTotal = total - completedTotal;
// Set top dashboard cards counts dynamically const cardTotalEl = document.getElementById('count-total'); if (cardTotalEl) cardTotalEl.textContent = total; const cardCompletedEl = document.getElementById('count-completed'); if (cardCompletedEl) cardCompletedEl.textContent = completedTotal; const cardPendingEl = document.getElementById('count-pending'); if (cardPendingEl) cardPendingEl.textContent = pendingTotal; const cardPendingItCellEl = document.getElementById('count-pending-itcell'); if (cardPendingItCellEl) cardPendingItCellEl.textContent = totalItCell; const cardPendingOthersEl = document.getElementById('count-pending-others'); if (cardPendingOthersEl) cardPendingOthersEl.textContent = totalOthers;
let summaryBodyHtml = ` <td id="summary-total" class="cursor-pointer text-primary" style="background-color: #f8fafc;" onclick="openModalForCount('total', null, 'Total No of Bills')">${total}</td> <td id="summary-completed" class="cursor-pointer text-success-dark" onclick="openModalForCount('completed_custom', null, 'Completed Bills')">${completedTotal}</td> <td id="summary-pending" class="cursor-pointer text-danger-dark" onclick="openModalForCount('pending_custom', null, 'Pending Bills')">${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(); }
const totalSum = data.reduce((a, b) => a + b, 0);
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' }, generateLabels: function (chart) { const data = chart.data; if (data.labels.length && data.datasets.length) { return data.labels.map((label, i) => { const value = data.datasets[0].data[i]; const percentage = totalSum > 0 ? ((value / totalSum) * 100).toFixed(1) : 0; const color = data.datasets[0].backgroundColor[i]; return { text: `${label}: ${value} (${percentage}%)`, fillStyle: color, strokeStyle: color, lineWidth: 0, hidden: isNaN(data.datasets[0].data[i]) || chart.getDatasetMeta(0).data[i].hidden, index: i }; }); } return []; } } }, tooltip: { callbacks: { label: function (context) { const label = context.label || ''; const value = context.raw || 0; const percentage = totalSum > 0 ? ((value / totalSum) * 100).toFixed(1) : 0; return ` ${label}: ${value} (${percentage}%)`; } } } }, onClick: (event, activeElements) => { if (activeElements && activeElements.length > 0) { const index = activeElements[0].index; const statusName = labels[index]; const statusObj = activeStatus.find(s => s.status_name === statusName); if (statusObj) { openModalForCount('status', statusObj.id, 'Status: ' + statusObj.status_name); } } } } }); }
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'); const subDNone = showPendingSubBreakdown ? '' : 'd-none';
if (filtered.length === 0) { const colSpan = 2 + completedStatuses.length + 1 + (showPendingSubBreakdown ? (2 + nonCompletedStatuses.length) : 0) + 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 sumItCell = 0; let sumOthers = 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); }); sumItCell += (v.itCellCount || 0); sumOthers += (v.othersCount || 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-dark 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. Sub-breakdown: Pending with IT Cell & Pending with Others trHtml += ` <td class="cursor-pointer pending-sub-col ${subDNone} text-primary fw-semibold" onclick="openModalForCount('vendor_it_cell', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (Pending with IT Cell)', this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${v.itCellCount || 0}</td> <td class="cursor-pointer pending-sub-col ${subDNone} text-warning-dark fw-semibold" onclick="openModalForCount('vendor_others', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (Pending with Others)', this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${v.othersCount || 0}</td> `;
// 4. Days-wise columns (ALWAYS VISIBLE) trHtml += ` <td class="cursor-pointer age-col" 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 age-col" 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 age-col" 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 age-col" 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 age-col" 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 age-col" 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 Bills')">${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-dark fw-bold" onclick="openModalForCount('pending_custom', null, 'Pending Bills')">${sumPending}</td>`;
// 3. Sub-breakdown: Pending with IT Cell & Pending with Others totalTrHtml += ` <td class="cursor-pointer pending-sub-col ${subDNone} text-primary fw-bold" onclick="openModalForCount('it_cell_custom', null, 'Pending with IT Cell')">${sumItCell}</td> <td class="cursor-pointer pending-sub-col ${subDNone} text-warning-dark fw-bold" onclick="openModalForCount('others_custom', null, 'Pending with Others')">${sumOthers}</td> `;
// 4. Days-wise columns (ALWAYS VISIBLE) totalTrHtml += ` <td class="cursor-pointer age-col" onclick="openModalForCount('age', [1, 15], 'Age: 1-15 Days')">${sumAge1_15}</td> <td class="cursor-pointer age-col" onclick="openModalForCount('age', [16, 30], 'Age: 16-30 Days')">${sumAge16_30}</td> <td class="cursor-pointer age-col" onclick="openModalForCount('age', [31, 60], 'Age: 31-60 Days')">${sumAge31_60}</td> <td class="cursor-pointer age-col" onclick="openModalForCount('age', [61, 90], 'Age: 61-90 Days')">${sumAge61_90}</td> <td class="cursor-pointer age-col" onclick="openModalForCount('age', [91, 120], 'Age: 91-120 Days')">${sumAge91_120}</td> <td class="cursor-pointer text-danger age-col" 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; let activeModalQuickType = 'all'; let activeModalQuickStatus = 'all';
function openModalForCount(clickType, clickValue, displayName, vendorName = null, col = null) { /* // ================= OLD IN-PAGE MODAL CODE (COMMENTED OUT FOR REFERENCE) ================= modalClickFilter = { clickType, clickValue, displayName, vendorName, col };
if (clickType === 'category' && clickValue) { activeModalQuickType = clickValue; } else { activeModalQuickType = 'all'; }
if (clickType === 'status' && clickValue) { activeModalQuickStatus = String(clickValue); } else if (clickType === 'vendor_status' && col) { activeModalQuickStatus = String(col); } else { activeModalQuickStatus = 'all'; }
// 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) { 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(); // ======================================================================================= */
// NEW: Open Details View in New Browser Tab const params = new URLSearchParams(); if (clickType) params.append('clickType', clickType); if (clickValue) params.append('clickValue', Array.isArray(clickValue) ? clickValue.join(',') : clickValue); if (displayName) params.append('title', displayName); if (vendorName) params.append('vendor', vendorName); if (col) params.append('col', col);
const cvModeSelect = document.getElementById('completed-view-mode'); if (cvModeSelect) { params.append('completedViewMode', cvModeSelect.value); }
window.location.href = contextPath + 'gccservicepaymenttracker/details?' + params.toString(); }
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>'; 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 updateModalQuickFilters(clickFilteredRecords) { const container = document.getElementById('modal-type-badges'); if (!container) return;
const colorMapping = { 'all': { color: '#0f5c78', bgLight: '#f0f9ff', textLight: '#e0f2fe' }, 'tax invoice': { color: '#166534', bgLight: '#f0fdf4', textLight: '#dcfce7' }, 'sd': { color: '#1e40af', bgLight: '#eff6ff', textLight: '#dbeafe' }, 'emd': { color: '#6b21a8', bgLight: '#faf5ff', textLight: '#f3e8ff' }, 'asd': { color: '#9a3412', bgLight: '#fff7ed', textLight: '#ffedd5' }, 'agreement': { color: '#9d174d', bgLight: '#fdf2f8', textLight: '#fce7f3' }, 'bank guarantee': { color: '#0369a1', bgLight: '#f0f9ff', textLight: '#e0f2fe' }, 'delivery chellan': { color: '#4f46e5', bgLight: '#e0e7ff', textLight: '#c7d2fe' } };
const getStylesForType = (key) => { if (colorMapping[key]) return colorMapping[key]; let hash = 0; for (let i = 0; i < key.length; i++) { hash = key.charCodeAt(i) + ((hash << 5) - hash); } const h = Math.abs(hash) % 360; return { color: `hsl(${h}, 70%, 25%)`, bgLight: `hsl(${h}, 70%, 97%)`, textLight: `hsl(${h}, 70%, 90%)` }; };
const quickTypes = [ { key: 'all', label: 'All Bills', color: '#0f5c78', bgLight: '#f0f9ff', textLight: '#e0f2fe' } ];
activeTypes.forEach(t => { const key = getCategoryCode(t.type_name); if (!quickTypes.some(qt => qt.key === key)) { const styles = getStylesForType(key); quickTypes.push({ key: key, label: t.type_name, color: styles.color, bgLight: styles.bgLight, textLight: styles.textLight }); } });
const counts = { all: clickFilteredRecords.length }; quickTypes.forEach(t => { if (t.key !== 'all') counts[t.key] = 0; });
clickFilteredRecords.forEach(row => { const cat = getCategoryCode(row.resolved_type || row.type); if (cat in counts) { counts[cat]++; } else { const matchedType = activeTypes.find(t => String(t.id) === cat); if (matchedType) { const resolvedCat = getCategoryCode(matchedType.type_name); if (resolvedCat in counts) counts[resolvedCat]++; } } });
let html = ''; quickTypes.forEach(t => { const count = counts[t.key] || 0; const isActive = activeModalQuickType === t.key;
let badgeStyle, countStyle; if (isActive) { badgeStyle = `background-color: ${t.color}; color: white; border-color: ${t.color}; font-weight: 600; font-size: 0.95rem; padding: 8px 18px; box-shadow: 0 3px 10px rgba(0,0,0,0.1);`; countStyle = `background-color: ${t.textLight}; color: ${t.color}; font-size: 0.85rem; font-weight: 800; padding: 3px 9px; border-radius: 12px;`; } else { badgeStyle = `background-color: white; color: #334155; border-color: #cbd5e1; font-weight: 500; font-size: 0.95rem; padding: 8px 18px;`; countStyle = `background-color: ${t.bgLight}; color: ${t.color}; font-size: 0.85rem; font-weight: 800; padding: 3px 9px; border-radius: 12px;`; }
html += ` <span class="badge cursor-pointer border rounded-pill d-inline-flex align-items-center gap-2" style="${badgeStyle} cursor: pointer; transition: all 0.2s;" onclick="setModalQuickType('${t.key}')"> ${escapeHtml(t.label)} <span style="${countStyle}">${count}</span> </span> `; });
container.innerHTML = html; }
function updateModalStatusBadges(typeFiltered) { const container = document.getElementById('modal-status-badges'); if (!container) return;
// Calculate count for each status in typeFiltered const counts = { all: typeFiltered.length }; typeFiltered.forEach(row => { const statusVal = String(row.status || '').trim(); const statusName = String(row.resolved_status || getStatusName(row.status) || '').trim();
const matched = activeStatus.find(s => String(s.id) === statusVal || s.status_name.toLowerCase().trim() === statusName.toLowerCase().trim() ); if (matched) { counts[matched.id] = (counts[matched.id] || 0) + 1; } });
// Render status filter badges let html = '';
// 'All Statuses' badge const isAllActive = activeModalQuickStatus === 'all'; const allStyle = isAllActive ? `background-color: #0f5c78; color: white; border-color: #0f5c78; font-weight: 600; font-size: 0.95rem; padding: 8px 18px; box-shadow: 0 3px 10px rgba(0,0,0,0.1);` : `background-color: white; color: #334155; border-color: #cbd5e1; font-weight: 500; font-size: 0.95rem; padding: 8px 18px;`; const allCountStyle = isAllActive ? `background-color: #e0f2fe; color: #0f5c78; font-size: 0.85rem; font-weight: 800; padding: 3px 9px; border-radius: 12px;` : `background-color: #f0f9ff; color: #0f5c78; font-size: 0.85rem; font-weight: 800; padding: 3px 9px; border-radius: 12px;`;
html += ` <span class="badge cursor-pointer border rounded-pill d-inline-flex align-items-center gap-2" style="${allStyle} cursor: pointer; transition: all 0.2s;" onclick="setModalQuickStatus('all')"> All Statuses <span style="${allCountStyle}">${counts.all}</span> </span> `;
activeStatus.forEach(s => { const count = counts[s.id] || 0; const isActive = activeModalQuickStatus === String(s.id);
const colorInfo = getStatusStyle(s.status_name); const primaryColor = colorInfo.text || '#475569'; const bgLight = colorInfo.bg || '#f1f5f9';
let badgeStyle, countStyle; if (isActive) { badgeStyle = `background-color: ${primaryColor}; color: white; border-color: ${primaryColor}; font-weight: 600; font-size: 0.95rem; padding: 8px 18px; box-shadow: 0 3px 10px rgba(0,0,0,0.1);`; countStyle = `background-color: white; color: ${primaryColor}; font-size: 0.85rem; font-weight: 800; padding: 3px 9px; border-radius: 12px;`; } else { badgeStyle = `background-color: white; color: #334155; border-color: #cbd5e1; font-weight: 500; font-size: 0.95rem; padding: 8px 18px;`; countStyle = `background-color: ${bgLight}; color: ${primaryColor}; font-size: 0.85rem; font-weight: 800; padding: 3px 9px; border-radius: 12px;`; }
html += ` <span class="badge cursor-pointer border rounded-pill d-inline-flex align-items-center gap-2" style="${badgeStyle} cursor: pointer; transition: all 0.2s;" onclick="setModalQuickStatus('${s.id}')"> ${escapeHtml(s.status_name)} <span style="${countStyle}">${count}</span> </span> `; });
container.innerHTML = html; }
function setModalQuickStatus(statusId) { activeModalQuickStatus = statusId; const modalStatusSelect = document.getElementById('modal-filter-status'); if (modalStatusSelect) modalStatusSelect.value = statusId;
if (statusId !== 'all') { activeModalQuickType = 'all'; const modalTypeSelect = document.getElementById('modal-filter-type'); if (modalTypeSelect) modalTypeSelect.value = 'all'; } applyModalFilters(); }
function setModalQuickType(key) { activeModalQuickType = key; const modalTypeSelect = document.getElementById('modal-filter-type'); if (modalTypeSelect) modalTypeSelect.value = key;
if (key !== 'all') { activeModalQuickStatus = 'all'; const modalStatusSelect = document.getElementById('modal-filter-status'); if (modalStatusSelect) modalStatusSelect.value = 'all'; } applyModalFilters(); }
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;
// Step 1: Filter by click context let clickFiltered = allRecords.filter(row => { 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 Bills 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 Bills 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 (getLocalDateString(row.updated_date) !== clickValue) return false; }
return true; });
// Step 2: Render type quick filter badges using clickFiltered dataset updateModalQuickFilters(clickFiltered);
// Step 3: Apply activeModalQuickType filter let filtered = clickFiltered; if (activeModalQuickType !== 'all') { filtered = filtered.filter(row => { const cat = getCategoryCode(row.resolved_type || row.type); if (activeModalQuickType === 'status_not_updated') { return cat === 'status_not_updated'; } return cat === activeModalQuickType; }); }
// Step 3.5: Dynamically calculate and update status counts and render status filter badges updateModalStatusBadges(filtered);
// Step 3.6: Apply activeModalQuickStatus filter if (activeModalQuickStatus !== 'all') { filtered = filtered.filter(row => { const statusVal = String(row.status || '').trim(); const statusName = String(row.resolved_status || getStatusName(row.status) || '').trim();
const matched = activeStatus.find(s => String(s.id) === activeModalQuickStatus); if (matched) { return statusVal === String(matched.id) || statusName.toLowerCase().trim() === matched.status_name.toLowerCase().trim(); } return true; }); }
// Step 4: Apply other modal dropdowns and search query if (selectedType !== 'all') { filtered = filtered.filter(row => { if (selectedType === 'status_not_updated') { const rowType = String(row.type || '').trim(); return rowType === ''; } else { const rowCategory = getCategoryCode(row.resolved_type || row.type); return rowCategory === selectedType; } }); }
if (selectedStatus !== 'all') { filtered = filtered.filter(row => { const rowStatusStr = String(row.status || '').trim(); const matched = activeStatus.find(s => String(s.id) === selectedStatus); if (matched) { return rowStatusStr === String(matched.id) || rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim(); } return true; }); }
if (selectedVendor !== 'all') { filtered = filtered.filter(row => { const rowVendor = String(row.resolved_vendor || row.vendor_name || '').trim(); return rowVendor.toLowerCase() === selectedVendor.toLowerCase(); }); }
if (query) { filtered = filtered.filter(row => { 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) ); }); }
// Sort modal dataset if (modalSortCol) { filtered.sort((a, b) => { let valA = a[modalSortCol] || ''; let valB = b[modalSortCol] || '';
let timeA = valA ? new Date(valA).getTime() : 0; let timeB = valB ? new Date(valB).getTime() : 0; if (isNaN(timeA)) timeA = 0; if (isNaN(timeB)) timeB = 0;
if (timeA !== timeB) { return modalSortAsc ? timeA - timeB : timeB - timeA; } return modalSortAsc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA)); }); }
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><small class="text-muted">${text(row.payment_review)}</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); }
if (row.mail_screenshot_path) { const btnViewScreenshot = document.createElement('button'); btnViewScreenshot.className = 'btn-grid-action btn-outline-info me-1'; btnViewScreenshot.style.border = '1px solid #06b6d4'; btnViewScreenshot.style.color = '#06b6d4'; btnViewScreenshot.innerHTML = '<i class="fa-solid fa-image"></i>'; btnViewScreenshot.title = 'View Screenshot'; btnViewScreenshot.onclick = () => { let imgUrl = row.mail_screenshot_path; if (contextPath && contextPath.endsWith('/') && imgUrl.startsWith('/')) { imgUrl = contextPath + imgUrl.substring(1); } else { imgUrl = contextPath + imgUrl; } window.open(imgUrl, '_blank'); }; tdActions.appendChild(btnViewScreenshot); }
// 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</th> <th>Remarks</th> <th>Screenshot Status</th> <th>Payment Review</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>${row.mail_screenshot_path ? 'Screenshot Uploaded' : 'Screenshot not uploaded'}</td> <td>${text(row.payment_review)}</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; }
const latestDateOnly = getLocalDateString(maxDateStr);
// Count how many files share this exact latest update date stamp let count = 0; allRecords.forEach(r => { if (r.updated_date && getLocalDateString(r.updated_date) === latestDateOnly) { 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', latestDateOnly); }
function openLatestUpdatesModal() { const badge = document.getElementById('latest-update-badge'); if (!badge) return; const latestDate = badge.getAttribute('data-latest-date'); if (!latestDate) return;
let dateDisplay = '-'; try { const d = new Date(latestDate); if (!isNaN(d.getTime())) { const pad = n => String(n).padStart(2, '0'); dateDisplay = `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`; } } catch (e) { }
openModalForCount('latest_updates', latestDate, `Latest Updates (${dateDisplay})`); }
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();
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 me-1" title="View PDF"><i class="fa-solid fa-file-pdf"></i></a>`; } if (item.mail_screenshot_path) { let imgUrl = item.mail_screenshot_path; if (contextPath && contextPath.endsWith('/') && imgUrl.startsWith('/')) { imgUrl = contextPath + imgUrl.substring(1); } else { imgUrl = contextPath + imgUrl; } docBtnHtml += `<a href="${imgUrl}" target="_blank" class="btn-grid-action btn-outline-info" style="border: 1px solid #06b6d4; color: #06b6d4;" title="View Screenshot"><i class="fa-solid fa-image"></i></a>`; } if (!docBtnHtml) { docBtnHtml = '-'; }
// 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><small class="text-muted">${text(item.payment_review)}</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><small class="text-muted">${text(row.payment_review)}</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>
|