|
|
<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org" lang="en">
<head> <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>
<!-- Custom Style Sheet mapping to theme --> <style> 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; }
.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; } </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"> <span class="user-badge"> <i class="fa-solid fa-user-shield"></i> <span th:text="${usrName}">Guest</span> </span> <a th:href="@{/gccservicepaymenttracker/logout}" class="btn-logout"> <i class="fa-solid fa-right-from-bracket"></i> Logout </a> </div> </div>
<div class="main-container">
<!-- Category Grid (5 Cards) --> <div class="category-grid">
<!-- Security Deposit --> <div class="category-card card-sd" onclick="selectCategory('sd', this)"> <div class="card-left"> <h3 class="card-title">Security Deposit</h3> <p class="card-value" id="count-sd">0</p> </div> <i class="fa-solid fa-wallet card-icon"></i> </div>
<!-- Tax Invoice --> <div class="category-card card-invoice" onclick="selectCategory('tax invoice', this)"> <div class="card-left"> <h3 class="card-title">Tax Invoice</h3> <p class="card-value" id="count-invoice">0</p> </div> <i class="fa-solid fa-file-invoice-dollar card-icon"></i> </div>
<!-- EMD --> <div class="category-card card-emd" onclick="selectCategory('emd', this)"> <div class="card-left"> <h3 class="card-title">EMD</h3> <p class="card-value" id="count-emd">0</p> </div> <i class="fa-solid fa-file-signature card-icon"></i> </div>
<!-- Bank Guarantee commented out
<div class="category-card card-bg" onclick="selectCategory('bank guarantee', this)"> <div class="card-left"> <h3 class="card-title">Bank Guarantee</h3> <p class="card-value" id="count-bg">0</p> </div> <i class="fa-solid fa-shield-halved card-icon"></i> </div> -->
<!-- Delivery Chellan commented out
<div class="category-card card-dc" onclick="selectCategory('delivery chellan', this)"> <div class="card-left"> <h3 class="card-title">Delivery Chellan</h3> <p class="card-value" id="count-dc">0</p> </div> <i class="fa-solid fa-truck-ramp-box card-icon"></i> </div> -->
<!-- ASD --> <div class="category-card card-asd" onclick="selectCategory('asd', this)"> <div class="card-left"> <h3 class="card-title">ASD</h3> <p class="card-value" id="count-asd">0</p> </div> <i class="fa-solid fa-vault card-icon"></i> </div>
<!-- Agreement --> <div class="category-card card-agreement" onclick="selectCategory('agreement', this)"> <div class="card-left"> <h3 class="card-title">Agreement</h3> <p class="card-value" id="count-agreement">0</p> </div> <i class="fa-solid fa-file-contract card-icon"></i> </div>
</div>
<!-- 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> <option value="Awaiting Details">Awaiting Details</option> <option value="Pending with Dept">Pending with Dept</option> <option value="Completed">Completed</option> <option value="No Action">No Action</option> <option value="Pending">Pending</option> </select> </div>
<!-- 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> </h3> <button th:if="${isAdmin}" class="btn-add-record" onclick="openAddModal()"> <i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record </button> </div>
<!-- Table Controls: Page size & Real-time Search --> <div class="table-controls-row"> <div class="table-controls-left"> <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Show</span> <select id="table-page-size" class="form-select form-select-sm" style="width: 80px;" onchange="changePageSize(this.value)"> <option value="10">10</option> <option value="20" selected>20</option> <option value="30">30</option> <option value="40">40</option> <option value="50">50</option> </select> <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">entries</span> </div> <div class="table-controls-right"> <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Search:</span> <input type="text" id="table-search" class="form-control form-control-sm" style="width: 250px;" placeholder="Search in this table..." oninput="handleTableSearch(this.value)"> </div> </div>
<!-- Responsive Table Grid --> <div class="table-responsive"> <table class="table grid-table table-striped table-hover align-middle" id="tracking-table"> <!-- Commented out old thead code
<thead> <tr> <th class="text-center" style="width: 50px;">S.No</th> <th>Received Date</th> <th>Type</th> <th>Vendor Name</th> <th>Subject</th> <th>Amount</th> <th>Invoice No</th> <th>Receipt No</th> <th>Invoice Date</th> <th>Physical File No</th> <th>Remarks</th> <th>TAT</th> <th>Status</th> <th>Followup</th> <th>My Followup</th> <th class="text-center" style="width: 100px;">Actions</th> </tr> </thead> --> <thead> <tr> <th class="text-center" style="width: 50px;">Sr. No.</th> <th>Received Date</th> <th>Invoice Date</th> <th>Physical File No.</th> <th>eFile No./ Receipt No.</th> <th>Subject</th> <th>Type (SD/ ASD)</th> <th>Vendor Name</th> <th>Amount</th> <th>Age of Bill (Days)</th> <th>Status (Pending / Completed)</th> <th>Remarks</th> <th>Updated on</th> <th class="text-center" style="width: 100px;">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> <option value="Awaiting Details">Awaiting Details</option> <option value="Pending with Dept">Pending with Dept</option> <option value="Completed">Completed</option> <option value="No Action">No Action</option> <option value="Pending">Pending</option> </select> </div>
<!-- Followup commented out
<div class="col-md-6"> <label for="form-followup" class="form-label fw-semibold text-secondary">Follow-up</label> <input type="text" class="form-control" id="form-followup" placeholder="Follow-up note"> </div> -->
<!-- My Followup commented out
<div class="col-md-6"> <label for="form-myfollowup" class="form-label fw-semibold text-secondary">My Follow-up</label> <input type="text" class="form-control" id="form-myfollowup" placeholder="Internal follow-up note"> </div> -->
<!-- Order By commented out
<div class="col-md-6"> <label for="form-orderby" class="form-label fw-semibold text-secondary">Order By</label> <input type="text" class="form-control" id="form-orderby" placeholder="Reference order description"> </div> -->
<!-- PDF Attachment --> <div class="col-md-12"> <label for="form-pdf" class="form-label fw-semibold text-secondary">PDF Attachment (Optional)</label> <input type="file" class="form-control" id="form-pdf" accept="application/pdf"> <div id="form-existing-pdf-container" class="mt-2" style="display: none;"> <span class="text-secondary fw-semibold">Current Document:</span> <a id="form-existing-pdf-link" href="#" target="_blank" class="ms-1 text-primary fw-bold text-decoration-none"> <i class="fa-solid fa-file-pdf"></i> View PDF </a> </div> </div>
<!-- Remarks --> <div class="col-12"> <label for="form-remarks" class="form-label fw-semibold text-secondary">Remarks</label> <textarea class="form-control" id="form-remarks" rows="2" placeholder="Additional remarks..."></textarea> </div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4"> <button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Cancel</button> <button type="submit" class="btn btn-primary px-4" style="background-color: #0f5c78; border-color: #0f5c78;"> <i class="fa-solid fa-save"></i> Save Record </button> </div> </form> </div> </div> </div> </div>
<!-- Bootstrap & FontAwesome bundles --> <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 recordModalObj = null; let currentPage = 1; let pageSize = 20;
// Dynamic lists for types and vendors let activeTypes = []; let activeVendors = [];
// Date Pickers // let fromDatePicker, toDatePicker;
document.addEventListener('DOMContentLoaded', async () => { recordModalObj = new bootstrap.Modal(document.getElementById('recordModal')); // 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 and vendors from DB endpoints async function loadTypesAndVendors() { try { const [typesRes, vendorsRes] = await Promise.all([ fetch(contextPath + 'gccservicepaymenttracker/api/types?_=' + new Date().getTime()), fetch(contextPath + 'gccservicepaymenttracker/api/vendors?_=' + new Date().getTime()) ]); activeTypes = await typesRes.json(); activeVendors = await vendorsRes.json(); populateDropdowns(); } catch (err) { console.error("Error loading types or vendors:", err); } }
// 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 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); } }
// Show/hide loader overlay function showLoader(show, text = 'Processing...') { const overlay = document.getElementById('loading-overlay'); const overlayText = document.getElementById('loader-overlay-text'); overlayText.textContent = text; if (show) { overlay.classList.add('active'); } else { overlay.classList.remove('active'); } }
// Get CSRF details function getCsrfHeaders() { const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content"); const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content"); const headers = {}; if (csrfToken && csrfHeader) { headers[csrfHeader] = csrfToken; } return headers; }
// Category count loaders function loadCounts() { fetch(contextPath + 'gccservicepaymenttracker/api/counts?_=' + new Date().getTime()) .then(response => response.json()) .then(data => { document.getElementById('count-sd').textContent = data.sd || 0; document.getElementById('count-invoice').textContent = data['tax invoice'] || 0; document.getElementById('count-emd').textContent = data.emd || 0; const bgEl = document.getElementById('count-bg'); if (bgEl) bgEl.textContent = data['bank guarantee'] || 0; const dcEl = document.getElementById('count-dc'); if (dcEl) dcEl.textContent = data['delivery chellan'] || 0; document.getElementById('count-asd').textContent = data.asd || 0; document.getElementById('count-agreement').textContent = data.agreement || 0; }) .catch(err => console.error("Error loading category counts:", err)); }
// Handles select change of card function selectCategory(category, cardElement) { const isActive = cardElement.classList.contains('active');
// Remove active class from all document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
const filterTypeSelect = document.getElementById('filter-type');
if (isActive) { activeCategory = 'all'; document.getElementById('active-category-badge').textContent = 'Type: All Types'; if (filterTypeSelect) { filterTypeSelect.value = 'all'; } } else { cardElement.classList.add('active'); activeCategory = category;
if (filterTypeSelect) { filterTypeSelect.value = category; }
// Update badge text const labels = { 'sd': 'Security Deposit', 'tax invoice': 'Tax Invoice', 'emd': 'EMD', 'asd': 'ASD', 'agreement': 'Agreement' }; document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category); }
// Refresh list loadList(); }
// Loading and Rendering list data function loadList() { showLoader(true, 'Loading payment tracking details...');
const statusVal = document.getElementById('filter-status').value; const searchVal = document.getElementById('search-text').value;
// Sync search-text to table-search when hitting main filter search const tblSearch = document.getElementById('table-search'); if (tblSearch) { tblSearch.value = searchVal; }
// Build query params const params = new URLSearchParams(); if (activeCategory && activeCategory !== 'all') { params.append('category', activeCategory); }
if (statusVal) params.append('status', statusVal);
// Add cache buster params.append('_', new Date().getTime());
fetch(contextPath + 'gccservicepaymenttracker/api/list?' + params.toString()) .then(response => response.json()) .then(data => { allRecords = data; currentPage = 1; renderTable(); }) .catch(err => { console.error("Error loading details:", err); showLoader(false); }); }
function renderTable() { const tbody = document.getElementById('tracking-tbody'); const noRecords = document.getElementById('no-records-found'); tbody.innerHTML = '';
const searchVal = document.getElementById('table-search')?.value || ''; let query = searchVal.toLowerCase().trim();
let filtered = allRecords.filter(row => { if (!query) return true; const vName = String(row.resolved_vendor || row.vendor_name || '').toLowerCase(); const tName = String(row.resolved_type || row.type || '').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)) || (row.status && String(row.status).toLowerCase().includes(query)) ); });
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 tParts = dateTimeStr.split('T'); const datePart = tParts[0]; const timePart = tParts.length > 1 ? tParts[1].split('.')[0] : ''; const dateParts = datePart.split('-'); if (dateParts.length === 3) { const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`; return timePart ? `${formattedDate} ${timePart}` : formattedDate; } } catch (e) { console.error(e); } return dateTimeStr; };
// Calculate age of bill (days) dynamically from received date: const 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() + 7);
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'; } };
const getTypeBadge = typeVal => { const cleanType = String(typeVal || '').trim().toLowerCase(); let label = cleanType; let badgeClass = 'type-sd';
if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) { label = 'ASD'; badgeClass = 'type-asd'; } else if (cleanType.includes('agreement')) { label = 'Agreement'; badgeClass = 'type-agreement'; } else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) { label = 'Security Deposit'; badgeClass = 'type-sd'; } else if (cleanType.includes('invoice') || cleanType.includes('bill')) { label = 'Tax Invoice'; badgeClass = 'type-tax-invoice'; } else if (cleanType === 'emd' || cleanType.includes('earnest')) { label = 'EMD'; badgeClass = 'type-emd'; } else if (cleanType.includes('guarantee') || cleanType === 'bg') { label = 'Bank Guarantee'; badgeClass = 'type-bank-guarantee'; } else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') { label = 'Delivery Chellan'; badgeClass = 'type-delivery-chellan'; } else { // 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); } label = typeVal || '-'; badgeClass = 'status-default'; } return `<span class="type-badge ${badgeClass}">${label}</span>`; };
tr.innerHTML += ` <td>${formatDate(row.received_date)}</td> <td>${formatDate(row.invoice_date)}</td> <td>${text(row.physical_file_no)}</td> <td>${text(row.receiptno)}</td> <td>${text(row.subject)}</td> <td>${getTypeBadge(row.resolved_type || row.type)}</td> <td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td> <td class="fw-semibold text-dark">₹ ${text(row.amount)}</td> <td>${calculateAgeOfBill(row.received_date)}</td> <td><span class="status-badge status-${String(row.status || '').toLowerCase().trim().replace(/\s+/g, '-')}">${text(row.status)}</span></td> <td><small class="text-muted">${text(row.remarks)}</small></td> <td>${formatDateTime(row.updated_date)}</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 (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; } } } } document.getElementById('form-status').value = 'Pending';
document.getElementById('form-vendor-custom').style.display = 'none'; document.getElementById('form-vendor-custom').required = false;
// Reset PDF upload elements document.getElementById('form-pdf').value = ''; document.getElementById('form-existing-pdf-container').style.display = 'none';
recordModalObj.show(); }
function openEditModal(row) { document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-file-signature"></i> Edit Payment Tracking Record'; document.getElementById('recordForm').reset();
document.getElementById('form-id').value = row.id; document.getElementById('form-received-date').value = row.received_date || ''; // Set type select const typeSelect = document.getElementById('form-type'); const rowType = String(row.type || '').trim().toLowerCase(); typeSelect.value = ''; let found = false; for (let option of typeSelect.options) { if (option.value && String(option.value) === String(row.type)) { typeSelect.value = option.value; found = true; break; } } if (!found && rowType) { const legacyCategory = getCategoryCode(rowType); for (let option of typeSelect.options) { if (option.value) { const typeObj = activeTypes.find(t => String(t.id) === String(option.value)); if (typeObj) { const optCategory = getCategoryCode(typeObj.type_name); if (optCategory === legacyCategory) { typeSelect.value = option.value; found = true; break; } } } } }
document.getElementById('form-subject').value = row.subject || ''; document.getElementById('form-amount').value = row.amount || ''; document.getElementById('form-invoice-no').value = row.invoice_no || '';
document.getElementById('form-receiptno').value = row.receiptno || '';
document.getElementById('form-invoice-date').value = row.invoice_date || ''; document.getElementById('form-physical-file-no').value = row.physical_file_no || ''; const tatEl = document.getElementById('form-tat'); if (tatEl) tatEl.value = row.tat || ''; const statusSelect = document.getElementById('form-status'); const rowStatus = String(row.status || '').trim(); statusSelect.value = 'Pending'; for (let option of statusSelect.options) { if (option.value.toLowerCase() === rowStatus.toLowerCase()) { statusSelect.value = option.value; break; } } const followupEl = document.getElementById('form-followup'); if (followupEl) followupEl.value = row.followup || ''; const myfollowupEl = document.getElementById('form-myfollowup'); if (myfollowupEl) myfollowupEl.value = row.myfollowup || ''; const orderbyEl = document.getElementById('form-orderby'); if (orderbyEl) orderbyEl.value = row.order_by || ''; document.getElementById('form-remarks').value = row.remarks || '';
// Set vendor select (using ID or legacy fallback/custom) const select = document.getElementById('form-vendor-select'); const vendorVal = String(row.vendor_name || '').trim(); select.value = ''; document.getElementById('form-vendor-custom').style.display = 'none'; document.getElementById('form-vendor-custom').required = false; document.getElementById('form-vendor-custom').value = '';
if (vendorVal) { let optionFound = false; for (let option of select.options) { if (option.value && option.value !== '__custom__' && String(option.value) === vendorVal) { select.value = option.value; optionFound = true; break; } }
if (!optionFound) { const matchedVendor = activeVendors.find(v => String(v.vendor_name).toLowerCase().trim() === vendorVal.toLowerCase()); if (matchedVendor) { select.value = String(matchedVendor.id); optionFound = true; } }
if (!optionFound) { select.value = '__custom__'; const customInput = document.getElementById('form-vendor-custom'); customInput.style.display = 'block'; customInput.required = true; customInput.value = row.vendor_name; } }
// Populate/Reset PDF elements document.getElementById('form-pdf').value = ''; const existingPdfContainer = document.getElementById('form-existing-pdf-container'); const existingPdfLink = document.getElementById('form-existing-pdf-link'); if (row.file_path) { existingPdfContainer.style.display = 'block'; let pdfUrl = row.file_path; if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) { pdfUrl = contextPath + pdfUrl.substring(1); } else { pdfUrl = contextPath + pdfUrl; } existingPdfLink.href = pdfUrl; } else { existingPdfContainer.style.display = 'none'; }
recordModalObj.show(); }
// SAVE Record API call async function saveRecord(event) { event.preventDefault(); showLoader(true, 'Saving record details...');
const id = document.getElementById('form-id').value; const received_date = document.getElementById('form-received-date').value; const type = document.getElementById('form-type').value; const subject = document.getElementById('form-subject').value; const amount = document.getElementById('form-amount').value; const invoice_no = document.getElementById('form-invoice-no').value; const receiptno = document.getElementById('form-receiptno').value; const invoice_date = document.getElementById('form-invoice-date').value; const physical_file_no = document.getElementById('form-physical-file-no').value; const tatEl = document.getElementById('form-tat'); const tat = tatEl ? tatEl.value : ''; const status = document.getElementById('form-status').value; // Preserve original values if they were loaded in edit mode let originalRecord = null; if (id) { originalRecord = allRecords.find(r => String(r.id) === String(id)); } const followupEl = document.getElementById('form-followup'); const followup = followupEl ? followupEl.value : (originalRecord ? originalRecord.followup : ''); const myfollowupEl = document.getElementById('form-myfollowup'); const myfollowup = myfollowupEl ? myfollowupEl.value : (originalRecord ? originalRecord.myfollowup : ''); const orderbyEl = document.getElementById('form-orderby'); const order_by = orderbyEl ? orderbyEl.value : (originalRecord ? originalRecord.order_by : ''); const remarks = document.getElementById('form-remarks').value;
// Resolve vendor const vendorSelect = document.getElementById('form-vendor-select').value; const vendorName = vendorSelect === '__custom__' ? document.getElementById('form-vendor-custom').value : vendorSelect;
if (!vendorName) { alert('Please select or specify a vendor.'); showLoader(false); return; }
// Upload PDF if selected let filePath = originalRecord ? originalRecord.file_path : null; const pdfInput = document.getElementById('form-pdf'); if (pdfInput && pdfInput.files && pdfInput.files.length > 0) { showLoader(true, 'Uploading PDF document...'); const formData = new FormData(); formData.append('file', pdfInput.files[0]);
const csrfHeaders = getCsrfHeaders(); try { const uploadResponse = await fetch(contextPath + 'gccservicepaymenttracker/api/upload-pdf', { method: 'POST', headers: { ...csrfHeaders }, body: formData }); const uploadResult = await uploadResponse.json(); if (uploadResult.success) { filePath = uploadResult.filePath; } else { alert(uploadResult.error || 'Failed to upload PDF file.'); showLoader(false); return; } } catch (err) { console.error("Upload error:", err); alert("Failed to upload PDF due to connection error."); showLoader(false); return; } }
const payload = { id, received_date, type, vendor_name: vendorName, subject, amount, invoice_no, receiptno, invoice_date, physical_file_no, tat, status, followup, myfollowup, order_by, remarks, file_path: filePath };
const csrfHeaders = getCsrfHeaders(); const headers = { 'Content-Type': 'application/json', ...csrfHeaders };
try { showLoader(true, 'Saving record details...'); const response = await fetch(contextPath + 'gccservicepaymenttracker/api/save', { method: 'POST', headers: headers, body: JSON.stringify(payload) }); const data = await response.json(); if (data.success) { recordModalObj.hide(); loadCounts(); loadList(); } else { alert(data.error || 'Failed to save record.'); showLoader(false); } } catch (err) { console.error("Save Error:", err); alert("Failed to save record due to connection error."); showLoader(false); } }
// Delete record API call function deleteRecord(id) { if (!confirm('Are you sure you want to delete this payment tracking record?')) { return; }
showLoader(true, 'Deleting 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/delete', { method: 'POST', headers: headers, body: body }) .then(response => response.json()) .then(data => { if (data.success) { 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); }); } </script></body>
</html>
|