|
|
<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org" lang="en">
<head> <script th:inline="javascript"> (function () { const currentUser = /*[[${session.tap_usrName}]]*/ null; if (currentUser && sessionStorage.getItem("tap_usrName") !== currentUser) { window.location.replace(/*[[@{/gcctapaldetailsmanagement/login}]]*/ '/gcctapaldetailsmanagement/login'); } })(); </script> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>GCC Tapal 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 & Chart.js & SheetJS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"> <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<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; }
/* Stats Cards styling */ /* Category Grid (3 Cards) */ .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%; }
/* Total 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; }
.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-title { font-size: 0.88rem; font-weight: 600; color: #64748b; margin: 0; text-transform: uppercase; letter-spacing: 0.5px; }
.card-value { font-family: 'Outfit', sans-serif; font-size: 2.1rem; font-weight: 700; color: #09303d; margin: 0; line-height: 1; }
.card-icon { font-size: 2.2rem; opacity: 0.85; }
/* Filter Card styling */ .filter-card { background: #fff; 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: 6px; }
.filter-label { font-size: 0.75rem; font-weight: 600; color: #64748b; text-transform: uppercase; }
.filter-input { border: 1.5px solid #e2e8f0; border-radius: 8px; padding: 8px 12px; font-size: 0.85rem; outline: none; transition: all 0.3s; background-color: #fff; }
.filter-input:focus { border-color: #0f5c78; box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15); }
.btn-filter-search { background-color: #0f5c78; color: #fff; border: none; border-radius: 8px; padding: 9px 24px; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.3s; display: inline-flex; align-items: center; gap: 8px; height: 38px; }
.btn-filter-search:hover { background-color: #09303d; }
.btn-filter-reset { background-color: #f1f5f9; color: #475569; border: 1.5px solid #e2e8f0; border-radius: 8px; padding: 8px 20px; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.3s; display: inline-flex; align-items: center; gap: 6px; height: 38px; }
.btn-filter-reset:hover { background-color: #e2e8f0; }
/* Table card styling */ .table-card { background: #fff; border-radius: 16px; padding: 24px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); margin-bottom: 30px; }
.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.3rem; font-weight: 700; color: #09303d; margin: 0; display: flex; align-items: center; gap: 10px; }
.active-filter-badge { background-color: #e0f2fe; color: #0369a1; font-size: 0.72rem; padding: 4px 10px; border-radius: 12px; font-weight: 600; }
.btn-add-record { background-color: #0f5c78; color: #fff; border: none; border-radius: 8px; padding: 8px 16px; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.3s; display: inline-flex; align-items: center; gap: 6px; }
.btn-add-record:hover { background-color: #09303d; transform: translateY(-1px); }
.table-controls-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; flex-wrap: wrap; gap: 10px; }
.table-controls-left, .table-controls-right { display: flex; align-items: center; gap: 8px; }
.grid-table { border-collapse: separate; border-spacing: 0; width: 100%; }
.grid-table th { background-color: #f8fafc; color: #475569; font-weight: 600; text-transform: uppercase; font-size: 0.75rem; padding: 12px 16px; border-bottom: 2px solid #e2e8f0; letter-spacing: 0.5px; }
.grid-table td { padding: 14px 16px; border-bottom: 1px solid #f1f5f9; font-size: 0.82rem; color: #334155; }
.grid-table tbody tr:hover td { background-color: #f8fafc; }
/* Status badge styles */ .status-badge { font-size: 0.72rem; font-weight: 600; padding: 4px 10px; border-radius: 12px; display: inline-block; text-align: center; }
/* Type badge styles */ .type-badge { font-size: 0.72rem; font-weight: 600; padding: 4px 10px; border-radius: 6px; display: inline-block; }
.type-sd { background-color: #fdf2f8; color: #9d174d; }
.type-tax-invoice { background-color: #ecfdf5; color: #047857; }
.type-emd { background-color: #f0f9ff; color: #0369a1; }
.type-asd { background-color: #fff1f2; color: #be123c; }
.type-agreement { background-color: #faf5ff; color: #6b21a8; }
.type-bank-guarantee { background-color: #fffbeb; color: #b45309; }
.type-delivery-chellan { background-color: #f0fdfa; color: #0f766e; }
/* Action Buttons inside Grid */ .btn-grid-action { width: 32px; height: 32px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; border: none; cursor: pointer; transition: all 0.2s; font-size: 0.85rem; }
.btn-grid-edit { background-color: #e0f2fe; color: #0369a1; }
.btn-grid-edit:hover { background-color: #bae6fd; }
.btn-grid-delete { background-color: #fee2e2; color: #b91c1c; }
.btn-grid-delete:hover { background-color: #fecaca; }
.btn-grid-pdf { background-color: #fef3c7; color: #d97706; }
.btn-grid-pdf:hover { background-color: #fde68a; }
/* Custom Modal headers styling */ .modal-custom-header { background: linear-gradient(135deg, #09303d, #0f5c78); color: #fff; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: none; border-top-left-radius: 12px; border-top-right-radius: 12px; }
.modal-custom-title { font-family: 'Outfit', sans-serif; font-size: 1.15rem; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 10px; }
.modal-content { border-radius: 12px; border: none; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); }
/* Loading overlay spinner */ #loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.8); z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 15px; }
.spinner-inner { width: 50px; height: 50px; border: 4px solid #e2e8f0; border-top-color: #0f5c78; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Custom Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* Responsive Media Queries */ @media (max-width: 991.98px) { .header { flex-direction: column; gap: 15px; text-align: center; padding: 15px; }
.logo-left { flex-direction: column; gap: 8px; align-items: center; }
.logo-text { text-align: center; }
.user-menu { width: 100%; justify-content: center; flex-wrap: wrap; gap: 10px; }
.main-container { max-width: 100%; margin: 15px auto; } } </style></head>
<body>
<!-- Loading Spinner Overlay --> <div id="loading-overlay" style="display: none;"> <div class="spinner-inner"></div> <span class="fw-semibold text-secondary" id="loading-text">Loading data...</span> </div>
<!-- Header Panel --> <header 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>IT CELL MANAGEMENT SERVICES</p> </div> </div> <div class="header-title d-none d-md-block"> <h2>Tapal Tracker Dashboard</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 fa-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>
<div class="user-badge"> <i class="fa-solid fa-user-tie"></i> <span th:text="${usrName}">Admin</span> </div> <a th:href="@{/gcctapaldetailsmanagement/logout}" class="btn-logout"> <i class="fa-solid fa-sign-out-alt"></i> Log Out </a> </div> </header>
<!-- Main Workspace --> <main class="main-container">
<!-- Top statistics metrics grids (3 Cards) --> <div class="category-grid" id="stats-container"> <!-- Total Records --> <div class="category-card card-total" onclick="openModalForCount('total', null, 'Total Tapals')"> <div class="card-left"> <h3 class="card-title">Total Records</h3> <p class="card-value" id="count-total">0</p> </div> <i class="fa-solid fa-folder-open card-icon"></i> </div>
<!-- Completed Tapals --> <div class="category-card card-completed" onclick="openModalForCount('completed_custom', null, 'Completed Tapals')"> <div class="card-left"> <h3 class="card-title">Completed Tapals</h3> <p class="card-value" id="count-completed">0</p> </div> <i class="fa-solid fa-circle-check card-icon"></i> </div>
<!-- Pending Tapals --> <div class="category-card card-pending" onclick="openModalForCount('pending_custom', null, 'Pending Tapals')"> <div class="card-left"> <h3 class="card-title">Pending Tapals</h3> <p class="card-value" id="count-pending">0</p> </div> <i class="fa-solid fa-clock card-icon"></i> </div> </div>
<div class="row g-4 mb-4"> <!-- Summary breakdown Table Card --> <div class="col-lg-8"> <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-line me-2 text-primary"></i>Pending Tapals aging 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 Tapals</th> <th style="background-color: #f0fdf4; color: #166534;">Completed</th> <th style="background-color: #fef2f2; color: #991b1b;">Pending</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 Tapals')">0</td> <td id="summary-completed" class="cursor-pointer text-success" onclick="openModalForCount('completed_custom', null, 'Completed Tapals')">0 </td> <td id="summary-pending" class="cursor-pointer text-danger" style="color: #991b1b;" onclick="openModalForCount('pending_custom', null, 'Pending Tapals')">0</td> <td id="summary-1-15" class="cursor-pointer" onclick="openModalForCount('age', [0, 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 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>Status 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>
<!-- Filter Card --> <div class="filter-card"> <div class="filter-grid"> <!-- Category Filter --> <div class="filter-group"> <label class="filter-label" for="filter-category">Category</label> <select id="filter-category" class="filter-input" onchange="handleCategoryFilterChange(this.value)"> <option value="all">- All Categories -</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="Tapal No, sender, subject..." 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>Tapal Tracking Details</span> <span class="active-filter-badge" id="active-category-badge">Category: All Categories</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 Tapal 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" selected>10</option> <option value="20">20</option> <option value="30">30</option> <option value="45">45</option> <option value="60">60</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"> <thead> <tr> <th class="text-center" style="width: 50px;">Sr. No.</th> <th>Tapal No.</th> <th onclick="sortMainTable('tapal_received_date')" style="cursor: pointer; user-select: none;"> Received Date <span id="main-sort-tapal_received_date-icon"><i class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th> <th>Request From</th> <th>Subject</th> <th class="text-center">Category</th> <th>Status</th> <th>Remarks</th> <th>OC File copy </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; padding: 40px; text-align: center;"> <i class="fa-solid fa-folder-open empty-icon" style="font-size: 3rem; color: #cbd5e1; margin-bottom: 15px;"></i> <h4>No Records Found</h4> <p class="text-secondary">No Tapal tracking records match the active criteria.</p> </div> </div>
<!-- Pagination Controls --> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-3" id="pagination-controls"> <div id="pagination-info" class="text-secondary" style="font-size: 0.82rem; font-weight: 500;"> Showing 0 to 0 of 0 entries </div> <nav aria-label="Table Page Navigation"> <ul class="pagination pagination-sm mb-0 align-items-center gap-1" id="pagination-list"> <!-- Populated dynamically --> </ul> </nav> </div> </div>
</main>
<!-- Save / Edit Record Modal --> <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-circle-plus"></i> Add/Edit Tapal 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"> <!-- Tapal No. --> <div class="col-md-6"> <label for="form-tapal-no" class="form-label fw-semibold text-secondary">Tapal No. <span class="text-danger">*</span></label> <input type="text" class="form-control" id="form-tapal-no" placeholder="Enter Tapal Number" required> </div>
<!-- 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>
<!-- Request From --> <div class="col-md-6"> <label for="form-request-from" class="form-label fw-semibold text-secondary">Request From <span class="text-danger">*</span></label> <input type="text" class="form-control" id="form-request-from" placeholder="Enter Sender / Request From" required> </div>
<!-- Category --> <div class="col-md-6"> <label for="form-category" class="form-label fw-semibold text-secondary">Category <span class="text-danger">*</span></label> <select class="form-select" id="form-category" required> </select> </div>
<!-- Subject --> <div class="col-md-12"> <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>
<!-- 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>
<!-- Tapal File Attachment --> <div class="col-md-6"> <label for="form-tapal-file" class="form-label fw-semibold text-secondary">Tapal File Attachment (Optional)</label> <input type="file" class="form-control" id="form-tapal-file" accept=".pdf,image/*"> <div id="form-existing-tapal-container" class="mt-2" style="display: none;"> <span class="text-secondary fw-semibold">Current Tapal File:</span> <a id="form-existing-tapal-link" href="#" target="_blank" class="ms-1 text-primary fw-bold text-decoration-none"> <i class="fa-solid fa-file-pdf"></i> View File </a> </div> </div>
<!-- OC File Attachment --> <div class="col-md-6"> <label for="form-oc-file" class="form-label fw-semibold text-secondary">OC File Attachment (Optional)</label> <input type="file" class="form-control" id="form-oc-file" accept=".pdf,image/*"> <div id="form-existing-oc-container" class="mt-2" style="display: none;"> <span class="text-secondary fw-semibold">Current OC File:</span> <a id="form-existing-oc-link" href="#" target="_blank" class="ms-1 text-info fw-bold text-decoration-none"> <i class="fa-solid fa-file-pdf"></i> View OC File </a> </div> </div>
<!-- Remarks --> <div class="col-md-12"> <label for="form-remarks" class="form-label fw-semibold text-secondary">Remarks</label> <textarea class="form-control" id="form-remarks" rows="2" placeholder="Additional remarks..."></textarea> </div> </div>
<div class="d-flex justify-content-end gap-2 mt-4"> <button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Cancel</button> <button type="submit" class="btn btn-primary px-4" style="background-color: #0f5c78; border-color: #0f5c78;"> <i class="fa-solid fa-save"></i> Save Record </button> </div> </form> </div> </div> </div> </div>
<!-- History Modal --> <div class="modal fade" id="historyModal" tabindex="-1" aria-labelledby="historyModalLabel" aria-hidden="true"> <div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal-content"> <div class="modal-custom-header" style="background-color: #2c3e50;"> <h5 class="modal-custom-title" id="historyModalLabel"> <i class="fa-solid fa-history"></i> Update History Log </h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body p-4"> <div id="history-details-section" class="mb-4 p-3 bg-light rounded border"> <h6 class="fw-bold mb-2">Tapal Reference Info</h6> <div class="row g-2" style="font-size: 0.85rem;"> <div class="col-md-4"><span class="text-secondary fw-semibold">Tapal No:</span> <span id="hist-ref-no" class="fw-bold">-</span></div> <div class="col-md-4"><span class="text-secondary fw-semibold">Sender:</span> <span id="hist-ref-sender">-</span></div> <div class="col-md-4"><span class="text-secondary fw-semibold">Subject:</span> <span id="hist-ref-subject">-</span></div> </div> </div> <div class="table-responsive" style="max-height: 450px;"> <table class="table table-bordered table-hover align-middle mb-0" style="font-size: 0.8rem;"> <thead class="table-light sticky-top" style="z-index: 1;"> <tr> <th style="width: 50px;" class="text-center">S.No</th> <th>Received Date</th> <th>Subject</th> <th>Category</th> <th>Request From</th> <th>Status</th> <th>Remarks</th> <th>Last Updated At</th> <th>Last Updated By</th> <th class="text-center" style="width: 150px;">Documents</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> <h5>No History Found</h5> <p>No historical updates recorded for this Tapal record.</p> </div> </div> </div> </div> </div> </div>
<!-- Deleted Records Modal --> <div class="modal fade" id="deletedRecordsModal" tabindex="-1" aria-labelledby="deletedRecordsModalLabel" aria-hidden="true"> <div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal-content"> <div class="modal-custom-header" style="background-color: #c0392b;"> <h5 class="modal-custom-title" id="deletedRecordsModalLabel"> <i class="fa-solid fa-trash-can"></i> Deleted Tapal Records </h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body p-4"> <div class="table-responsive" style="max-height: 450px;"> <table class="table table-bordered table-striped align-middle mb-0" style="font-size: 0.8rem;"> <thead class="table-light sticky-top" style="z-index: 1;"> <tr> <th class="text-center" style="width: 50px;">S.No</th> <th>Tapal No.</th> <th>Received Date</th> <th>Request From</th> <th>Subject</th> <th>Category</th> <th>Status</th> <th>Deletion Remarks</th> <th>Deleted Date</th> <th>Deleted By</th> <th class="text-center" style="width: 130px;">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 Tapal tracking records found in the database.</p> </div> </div> </div> </div> </div> </div>
<!-- Count Details List Modal --> <div class="modal fade" id="countDetailsModal" tabindex="-1" aria-labelledby="countDetailsModalLabel" aria-hidden="true"> <div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal-content"> <div class="modal-custom-header" style="background-color: #0f5c78;"> <h5 class="modal-custom-title" id="countDetailsModalLabel"> <i class="fa-solid fa-file-invoice"></i> Tapal Details - <span id="countDetailsModalTypeTitle">Total No of Tapals</span> </h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body p-4"> <div class="table-responsive" style="max-height: 450px;"> <table class="table table-bordered table-hover align-middle mb-0" style="font-size: 0.8rem;"> <thead class="table-light sticky-top" style="z-index: 1;"> <tr> <th class="text-center" style="width: 50px;">Sr. No.</th> <th>Tapal No.</th> <th>Received Date</th> <th>Request From</th> <th>Subject</th> <th>Category</th> <th>Status</th> <th>Remarks</th> <th>Last Updated Date</th> <th>Last Updated By</th> <th class="text-center" style="width: 120px;">Actions</th> </tr> </thead> <tbody id="modal-tracking-tbody"> <!-- Populated dynamically --> </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 Tapal tracking records match the active criteria.</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 countDetailsModalObj = null;
let currentPage = 1; let pageSize = 20;
let activeClickFilter = null; let statusChartInstance = null;
let activeTypes = []; let activeStatus = [];
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')); countDetailsModalObj = new bootstrap.Modal(document.getElementById('countDetailsModal'));
await loadTypesAndVendors(); loadCounts(); loadList(); });
// Helper to map category name to a normalized filter key function getCategoryCode(typeName) { return String(typeName).toLowerCase().trim(); }
async function loadTypesAndVendors() { try { const [typesRes, statusRes] = await Promise.all([ fetch(contextPath + 'gcctapaldetailsmanagement/api/categories?_=' + new Date().getTime()), fetch(contextPath + 'gcctapaldetailsmanagement/api/statuslist?_=' + new Date().getTime()) ]); activeTypes = await typesRes.json(); const allFetched = await statusRes.json(); activeStatus = allFetched.filter(s => { const name = String(s.status_name).toLowerCase().trim(); return !name.includes('duplicate') && !name.includes('no action') && !name.includes('action required'); });
populateDropdowns(); } catch (err) { console.error("Error loading categories or statuses:", err); } }
function getStatusKey(statusName) { if (!statusName) return 'default'; const name = String(statusName).toLowerCase().trim(); if (name.includes('complete') || name.includes('approve') || 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'; } return 'default'; }
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' } };
function getStatusStyle(statusName) { const key = getStatusKey(statusName); return statusColors[key] || { bg: '#f1f5f9', text: '#475569', badgeClass: 'text-secondary' }; }
function populateDropdowns() { // Populate filter-category const filterCatSelect = document.getElementById('filter-category'); if (filterCatSelect) { filterCatSelect.innerHTML = '<option value="all">- All Categories -</option>'; activeTypes.forEach(t => { const catCode = getCategoryCode(t.category_name); const opt = document.createElement('option'); opt.value = catCode; opt.textContent = t.category_name; filterCatSelect.appendChild(opt); }); }
// Populate form-category const formCatSelect = document.getElementById('form-category'); if (formCatSelect) { formCatSelect.innerHTML = '<option value="">-- Select Category --</option>'; activeTypes.forEach(t => { const opt = document.createElement('option'); opt.value = t.id; opt.textContent = t.category_name; formCatSelect.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); }); } }
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; }
function showLoader(show, text = 'Processing...') { const overlay = document.getElementById('loading-overlay'); const overlayText = document.getElementById('loading-text'); if (overlay) { overlayText.textContent = text; overlay.style.display = show ? 'flex' : 'none'; } }
async function loadCounts() { try { const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/counts?_=' + new Date().getTime()); const counts = await response.json();
// Update static cards const countTotalEl = document.getElementById('count-total'); if (countTotalEl) countTotalEl.textContent = counts.total || 0;
const countCompletedEl = document.getElementById('count-completed'); if (countCompletedEl) countCompletedEl.textContent = counts.completed || 0;
const countPendingEl = document.getElementById('count-pending'); if (countPendingEl) countPendingEl.textContent = counts.pending || 0;
// Render aging stats in the summary table document.getElementById('summary-total').textContent = counts.total || 0; document.getElementById('summary-completed').textContent = counts.completed || 0; document.getElementById('summary-pending').textContent = counts.pending || 0; document.getElementById('summary-1-15').textContent = counts.age_1_15 || 0; document.getElementById('summary-16-30').textContent = counts.age_16_30 || 0; document.getElementById('summary-31-60').textContent = counts.age_31_60 || 0; document.getElementById('summary-61-90').textContent = counts.age_61_90 || 0; document.getElementById('summary-91-120').textContent = counts.age_91_120 || 0; document.getElementById('summary-over-120').textContent = counts.age_over_120 || 0;
renderPieChart(counts);
} catch (err) { console.error("Error loading counts:", err); } }
function renderPieChart(counts) { const ctx = document.getElementById('statusPieChart'); if (!ctx) return;
const completed = counts.completed || 0; const pending = counts.pending || 0; const total = counts.total || 0;
if (total === 0) { if (statusChartInstance) statusChartInstance.destroy(); return; }
const data = { labels: ['Completed', 'Pending'], datasets: [{ data: [completed, pending], backgroundColor: ['#22c55e', '#ef4444'], hoverOffset: 6, borderWidth: 2, borderColor: '#ffffff' }] };
if (statusChartInstance) { statusChartInstance.data = data; statusChartInstance.update(); } else { statusChartInstance = new Chart(ctx, { type: 'doughnut', data: data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 10, family: 'Poppins' } } } }, cutout: '70%' } }); } }
async function handleCardClick(categoryKey) { activeCategory = categoryKey;
// Sync filter dropdown selection const filterCatSelect = document.getElementById('filter-category'); if (filterCatSelect) { filterCatSelect.value = categoryKey; }
// Highlight card document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('active')); const selectedCard = document.getElementById('card-' + categoryKey); if (selectedCard) { selectedCard.classList.add('active'); }
// Set Title Badge text const badge = document.getElementById('active-category-badge'); if (badge) { if (categoryKey === 'all') { badge.textContent = 'Category: All Categories'; } else if (categoryKey === 'status_not_updated') { badge.textContent = 'Category: Category Not Updated'; } else { const matched = activeTypes.find(t => getCategoryCode(t.category_name) === categoryKey); badge.textContent = `Category: ${matched ? matched.category_name : categoryKey}`; } }
currentPage = 1; loadList(); }
function handleCategoryFilterChange(categoryKey) { handleCardClick(categoryKey); }
let searchTimeout = null; function handleSearchInput(query) { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { currentPage = 1; loadList(); }, 300); }
function handleTableSearch(query) { handleSearchInput(query); }
// Sorting controls let sortColumn = 'tapal_received_date'; let sortDirection = 'DESC';
function sortMainTable(colName) { if (sortColumn === colName) { sortDirection = (sortDirection === 'ASC') ? 'DESC' : 'ASC'; } else { sortColumn = colName; sortDirection = 'DESC'; }
// Update header sort icons document.querySelectorAll('.grid-table th i').forEach(icon => { icon.className = 'fa-solid fa-sort ms-1'; icon.style.opacity = '0.4'; }); const activeIcon = document.getElementById(`main-sort-${colName}-icon`)?.querySelector('i'); if (activeIcon) { activeIcon.className = (sortDirection === 'ASC') ? 'fa-solid fa-sort-up ms-1' : 'fa-solid fa-sort-down ms-1'; activeIcon.style.opacity = '1'; }
loadList(); }
async function loadList() { showLoader(true, 'Fetching list data...'); const params = new URLSearchParams();
if (activeCategory !== 'all') { params.append('category', activeCategory); }
const statusSelect = document.getElementById('filter-status'); if (statusSelect && statusSelect.value) { params.append('status', statusSelect.value); }
try { const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/list?' + params.toString()); allBackendData = await response.json(); allRecords = [...allBackendData]; updateLatestUpdateBadge();
// Client-side local filtering based on Search Keywords and click badge criteria filterAndRender(); } catch (err) { console.error("Error loading records:", err); showLoader(false); } }
function filterAndRender() { let records = [...allRecords];
// 1. Search Query Local Filter const searchVal = document.getElementById('search-text')?.value.toLowerCase().trim() || ''; const tblSearchVal = document.getElementById('table-search')?.value.toLowerCase().trim() || ''; const activeQuery = searchVal || tblSearchVal;
if (activeQuery) { records = records.filter(row => { return (row.tapal_no && String(row.tapal_no).toLowerCase().includes(activeQuery)) || (row.request_from && String(row.request_from).toLowerCase().includes(activeQuery)) || (row.subject && String(row.subject).toLowerCase().includes(activeQuery)) || (row.remarks && String(row.remarks).toLowerCase().includes(activeQuery)); }); }
// 2. Click statistics badge local filter (Total, Completed, Pending, Age Groups) const clickBadge = document.getElementById('click-filter-badge'); if (activeClickFilter) { clickBadge.style.display = 'inline-block'; clickBadge.innerHTML = `${activeClickFilter.label} <i class="fa-solid fa-circle-xmark ms-1 cursor-pointer" onclick="clearClickFilter()"></i>`;
const type = activeClickFilter.type; const criteria = activeClickFilter.criteria;
if (type === 'completed_custom') { records = records.filter(row => { const rem = row.remarks ? String(row.remarks).trim() : ''; return rem !== '' && rem !== '-'; }); } else if (type === 'pending_custom') { records = records.filter(row => { const rem = row.remarks ? String(row.remarks).trim() : ''; return rem === '' || rem === '-'; }); } else if (type === 'age') { const min = criteria[0]; const max = criteria[1]; records = records.filter(row => { const rem = row.remarks ? String(row.remarks).trim() : ''; const isPending = (rem === '' || rem === '-'); if (!isPending) return false;
const age = calculateAgeDays(row.tapal_received_date); if (max === null) { return age >= min; } return age >= min && age <= max; }); } } else { clickBadge.style.display = 'none'; }
// 3. Client Sort records.sort((a, b) => { let valA = a[sortColumn]; let valB = b[sortColumn];
if (valA === null || valA === undefined) valA = ''; if (valB === null || valB === undefined) valB = '';
// Handle string dates comparison if (sortColumn.includes('date')) { const dateA = new Date(valA).getTime() || 0; const dateB = new Date(valB).getTime() || 0; return (sortDirection === 'ASC') ? dateA - dateB : dateB - dateA; }
if (typeof valA === 'string') { return (sortDirection === 'ASC') ? valA.localeCompare(valB, undefined, { numeric: true, sensitivity: 'base' }) : valB.localeCompare(valA, undefined, { numeric: true, sensitivity: 'base' }); }
return (sortDirection === 'ASC') ? valA - valB : valB - valA; });
currentFilteredRecords = records; renderTablePage(); }
function calculateAgeDays(dateStr) { if (!dateStr) return 0; const diffTime = Math.abs(new Date() - new Date(dateStr)); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - 1; }
function clearClickFilter() { activeClickFilter = null; filterAndRender(); }
function openModalForCount(type, criteria, label) { const params = new URLSearchParams(); if (type) params.append('clickType', type); if (criteria) params.append('clickValue', Array.isArray(criteria) ? criteria.join(',') : criteria); if (label) params.append('title', label); if (activeCategory && activeCategory !== 'all') { params.append('category', activeCategory); } window.location.href = contextPath + 'gcctapaldetailsmanagement/details?' + params.toString(); }
function renderTablePage() { const tbody = document.getElementById('tracking-tbody'); if (!tbody) return;
tbody.innerHTML = '';
const total = currentFilteredRecords.length; if (total === 0) { document.getElementById('no-records-found').style.display = 'block'; document.getElementById('pagination-controls').style.display = 'none'; showLoader(false); return; }
document.getElementById('no-records-found').style.display = 'none'; document.getElementById('pagination-controls').style.display = 'flex';
const totalPages = Math.ceil(total / pageSize); if (currentPage > totalPages) currentPage = totalPages; if (currentPage < 1) currentPage = 1;
const startIdx = (currentPage - 1) * pageSize; const endIdx = Math.min(startIdx + pageSize, total);
const pageRecords = currentFilteredRecords.slice(startIdx, endIdx);
pageRecords.forEach((row, index) => { const tr = document.createElement('tr'); const dispStatus = String(row.resolved_status || getStatusName(row.status_id) || '').trim(); const statusStyle = getStatusStyle(dispStatus);
let tapalDocBtn = '-'; if (row.tapal_file_path) { tapalDocBtn = `<a href="${contextPath + (row.tapal_file_path.startsWith('/') ? row.tapal_file_path.substring(1) : row.tapal_file_path)}" target="_blank" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.75rem;" title="View PDF"><i class="fa-solid fa-file-pdf"></i> View</a>`; }
let ocDocBtn = '-'; if (row.oc_file_path) { ocDocBtn = `<a href="${contextPath + (row.oc_file_path.startsWith('/') ? row.oc_file_path.substring(1) : row.oc_file_path)}" target="_blank" class="btn btn-sm btn-outline-info py-0 px-2" style="font-size: 0.75rem;" title="View OC File"><i class="fa-solid fa-file-image"></i> View</a>`; }
tr.innerHTML = ` <td class="text-center fw-bold">${startIdx + index + 1}</td> <td class="fw-semibold text-dark">${escapeHtml(text(row.tapal_no))}</td> <td>${formatDate(row.tapal_received_date)}</td> <td class="fw-semibold text-secondary">${escapeHtml(text(row.request_from))}</td> <td>${escapeHtml(text(row.subject))}</td> <td><span class="type-badge type-emd">${escapeHtml(text(row.resolved_category || row.cat_type_id))}</span></td> <td><span class="status-badge" style="background-color: ${statusStyle.bg}; color: ${statusStyle.text};">${escapeHtml(dispStatus)}</span></td> <td><small class="text-muted">${escapeHtml(text(row.remarks))}</small></td> <td>${ocDocBtn}</td> <td>${formatDateTime(row.updated_date)}</td> <td class="fw-semibold text-secondary">${escapeHtml(text(row.updated_by_name || '-'))}</td> `;
// Actions cell creation const tdActions = document.createElement('td'); tdActions.className = 'text-center';
if (row.tapal_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 Tapal File'; btnViewPdf.onclick = () => { let pdfUrl = row.tapal_file_path; window.open(contextPath + (pdfUrl.startsWith('/') ? pdfUrl.substring(1) : 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); tdActions.appendChild(btnEdit);
const btnDelete = document.createElement('button'); btnDelete.className = 'btn-grid-action btn-grid-delete me-1'; btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>'; btnDelete.title = 'Delete Record'; btnDelete.onclick = () => confirmDeleteRecord(row.id); tdActions.appendChild(btnDelete); }
// History button const btnHistory = document.createElement('button'); btnHistory.className = 'btn-grid-action btn-outline-secondary me-1'; btnHistory.style.border = '1px solid #64748b'; btnHistory.style.color = '#64748b'; btnHistory.style.backgroundColor = 'transparent'; btnHistory.innerHTML = '<i class="fa-solid fa-history"></i>'; btnHistory.title = 'View History Log'; btnHistory.onclick = () => showHistoryLog(row.tapal_no, row.request_from, row.subject); tdActions.appendChild(btnHistory);
tr.appendChild(tdActions); tbody.appendChild(tr); });
// Pagination info document.getElementById('pagination-info').textContent = `Showing ${startIdx + 1} to ${endIdx} of ${total} entries`; renderPagination(totalPages); showLoader(false); }
function renderPagination(totalPages) { const list = document.getElementById('pagination-list'); list.innerHTML = '';
// Previous page const prevLi = document.createElement('li'); prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`; prevLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="changePage(${currentPage - 1})"><i class="fa-solid fa-chevron-left"></i></a>`; list.appendChild(prevLi);
// Page numbers const maxPageButtons = 5; let startPage = Math.max(1, currentPage - 2); let endPage = Math.min(totalPages, startPage + maxPageButtons - 1); if (endPage - startPage < maxPageButtons - 1) { startPage = Math.max(1, endPage - maxPageButtons + 1); }
for (let i = startPage; i <= endPage; i++) { const li = document.createElement('li'); li.className = `page-item ${currentPage === i ? 'active' : ''}`; li.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="changePage(${i})">${i}</a>`; list.appendChild(li); }
// Next page const nextLi = document.createElement('li'); nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`; nextLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="changePage(${currentPage + 1})"><i class="fa-solid fa-chevron-right"></i></a>`; list.appendChild(nextLi); }
function changePage(page) { currentPage = page; renderTablePage(); }
function changePageSize(size) { pageSize = parseInt(size); currentPage = 1; renderTablePage(); }
// Add modal handling function openAddModal() { document.getElementById('recordForm').reset(); document.getElementById('form-id').value = ''; document.getElementById('form-existing-tapal-container').style.display = 'none'; document.getElementById('form-existing-oc-container').style.display = 'none';
document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-circle-plus"></i> Add Tapal Record';
recordModalObj.show(); }
// Edit modal handling function openEditModal(row) { document.getElementById('recordForm').reset(); document.getElementById('form-id').value = row.id;
document.getElementById('form-tapal-no').value = row.tapal_no || ''; document.getElementById('form-received-date').value = getLocalDateString(row.tapal_received_date) || ''; document.getElementById('form-request-from').value = row.request_from || ''; document.getElementById('form-category').value = row.cat_type_id || ''; document.getElementById('form-subject').value = row.subject || ''; document.getElementById('form-status').value = row.status_id || ''; document.getElementById('form-remarks').value = row.remarks || '';
// Handle Existing Tapal Attachment file links const tapalContainer = document.getElementById('form-existing-tapal-container'); if (row.tapal_file_path) { tapalContainer.style.display = 'block'; document.getElementById('form-existing-tapal-link').href = contextPath + (row.tapal_file_path.startsWith('/') ? row.tapal_file_path.substring(1) : row.tapal_file_path); } else { tapalContainer.style.display = 'none'; }
// Handle Existing OC file link const ocContainer = document.getElementById('form-existing-oc-container'); if (row.oc_file_path) { ocContainer.style.display = 'block'; document.getElementById('form-existing-oc-link').href = contextPath + (row.oc_file_path.startsWith('/') ? row.oc_file_path.substring(1) : row.oc_file_path); } else { ocContainer.style.display = 'none'; }
document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-pen-to-square"></i> Edit Tapal Record';
recordModalObj.show(); }
async function saveRecord(event) { event.preventDefault();
const id = document.getElementById('form-id').value; const tapal_no = document.getElementById('form-tapal-no').value; const tapal_received_date = document.getElementById('form-received-date').value; const request_from = document.getElementById('form-request-from').value; const cat_type_id = document.getElementById('form-category').value; const subject = document.getElementById('form-subject').value; const status_id = document.getElementById('form-status').value; const remarks = document.getElementById('form-remarks').value;
showLoader(true, 'Uploading attachments...');
let tapalFilePath = ''; // If editing, preserve the old value if no new file is uploaded if (id) { const matched = allRecords.find(r => String(r.id) === String(id)); if (matched) tapalFilePath = matched.tapal_file_path || ''; }
let ocFilePath = ''; if (id) { const matched = allRecords.find(r => String(r.id) === String(id)); if (matched) ocFilePath = matched.oc_file_path || ''; }
const csrfHeaders = getCsrfHeaders();
// 1. Upload Tapal File const tapalFileInput = document.getElementById('form-tapal-file'); if (tapalFileInput && tapalFileInput.files.length > 0) { const formData = new FormData(); formData.append('file', tapalFileInput.files[0]); formData.append('type', 'tapal');
try { const uploadResponse = await fetch(contextPath + 'gcctapaldetailsmanagement/api/upload-file', { method: 'POST', headers: csrfHeaders, body: formData }); const uploadResult = await uploadResponse.json(); if (uploadResult.success) { tapalFilePath = uploadResult.filePath; } else { Swal.fire('Error', uploadResult.error || 'Failed to upload Tapal File.', 'error'); showLoader(false); return; } } catch (err) { console.error("Tapal File Upload error:", err); Swal.fire('Error', "Failed to upload Tapal File due to server connection error.", 'error'); showLoader(false); return; } }
// 2. Upload OC File const ocFileInput = document.getElementById('form-oc-file'); if (ocFileInput && ocFileInput.files.length > 0) { const formData = new FormData(); formData.append('file', ocFileInput.files[0]); formData.append('type', 'oc');
try { const uploadResponse = await fetch(contextPath + 'gcctapaldetailsmanagement/api/upload-file', { method: 'POST', headers: csrfHeaders, body: formData }); const uploadResult = await uploadResponse.json(); if (uploadResult.success) { ocFilePath = uploadResult.filePath; } else { Swal.fire('Error', uploadResult.error || 'Failed to upload OC File.', 'error'); showLoader(false); return; } } catch (err) { console.error("OC File Upload error:", err); Swal.fire('Error', "Failed to upload OC File due to connection error.", 'error'); showLoader(false); return; } }
// 3. Save JSON Payload const payload = { id, tapal_no, tapal_received_date, request_from, cat_type_id, subject, status_id, remarks, tapal_file_path: tapalFilePath, oc_file_path: ocFilePath };
try { showLoader(true, 'Saving details...'); const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/save', { method: 'POST', headers: { 'Content-Type': 'application/json', ...csrfHeaders }, body: JSON.stringify(payload) }); const data = await response.json(); if (data.success) { recordModalObj.hide(); Swal.fire('Success', 'Tapal details saved successfully!', 'success'); loadCounts(); loadList(); } else { Swal.fire('Error', data.error || 'Failed to save record details.', 'error'); showLoader(false); } } catch (err) { console.error("Save Error:", err); Swal.fire('Error', "Failed to save record details due to connection error.", 'error'); showLoader(false); } }
// Delete record confirmation function confirmDeleteRecord(id) { Swal.fire({ title: 'Enter Reason for Deletion', input: 'textarea', inputPlaceholder: 'Type deletion remarks here...', inputAttributes: { 'aria-label': 'Type deletion remarks here' }, showCancelButton: true, confirmButtonText: 'Delete Record', confirmButtonColor: '#d33', cancelButtonText: 'Cancel', preConfirm: (remarks) => { if (!remarks || !remarks.trim()) { Swal.showValidationMessage('Deletion remarks are required!'); } return remarks; } }).then((result) => { if (result.isConfirmed) { deleteRecordCall(id, result.value); } }); }
async function deleteRecordCall(id, remarks) { showLoader(true, 'Deleting record...'); const csrfHeaders = getCsrfHeaders(); const params = new URLSearchParams(); params.append('id', id); params.append('remarks', remarks);
try { const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/delete?' + params.toString(), { method: 'POST', headers: csrfHeaders }); const data = await response.json(); if (data.success) { Swal.fire('Deleted!', 'Tapal record has been deleted.', 'success'); loadCounts(); loadList(); } else { Swal.fire('Error', data.error || 'Failed to delete record.', 'error'); showLoader(false); } } catch (err) { console.error("Delete Error:", err); Swal.fire('Error', "Connection failed.", 'error'); showLoader(false); } }
// History Update Log modal async function showHistoryLog(tapalNo, sender, subject) { document.getElementById('hist-ref-no').textContent = tapalNo || '-'; document.getElementById('hist-ref-sender').textContent = sender || '-'; document.getElementById('hist-ref-subject').textContent = subject || '-';
const tbody = document.getElementById('history-tbody'); tbody.innerHTML = ''; document.getElementById('no-history-found').style.display = 'none';
try { showLoader(true, 'Fetching logs...'); const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/history?tapalNo=' + encodeURIComponent(tapalNo)); const historyList = await response.json(); showLoader(false);
if (!historyList || historyList.length === 0) { document.getElementById('no-history-found').style.display = 'block'; } else { historyList.forEach((hist, index) => { const tr = document.createElement('tr'); const dispStatus = hist.resolved_status || getStatusName(hist.status_id) || '';
let docButtons = '-'; if (hist.tapal_file_path || hist.oc_file_path) { docButtons = ''; if (hist.tapal_file_path) { docButtons += `<a href="${contextPath + (hist.tapal_file_path.startsWith('/') ? hist.tapal_file_path.substring(1) : hist.tapal_file_path)}" target="_blank" class="btn btn-sm btn-outline-primary py-0 px-2 me-1" style="font-size: 0.7rem;" title="View File"><i class="fa-solid fa-file-pdf"></i> File</a>`; } if (hist.oc_file_path) { docButtons += `<a href="${contextPath + (hist.oc_file_path.startsWith('/') ? hist.oc_file_path.substring(1) : hist.oc_file_path)}" target="_blank" class="btn btn-sm btn-outline-info py-0 px-2" style="font-size: 0.7rem;" title="View OC File"><i class="fa-solid fa-file-image"></i> OC</a>`; } }
tr.innerHTML = ` <td class="text-center fw-bold">${index + 1}</td> <td>${formatDate(hist.tapal_received_date)}</td> <td>${escapeHtml(text(hist.subject))}</td> <td><span class="type-badge type-emd">${escapeHtml(text(hist.resolved_category || hist.cat_type_id))}</span></td> <td class="fw-semibold text-secondary">${escapeHtml(text(hist.request_from))}</td> <td><span class="status-badge" style="background-color: ${getStatusStyle(dispStatus).bg}; color: ${getStatusStyle(dispStatus).text};">${escapeHtml(dispStatus)}</span></td> <td><small class="text-muted">${escapeHtml(text(hist.remarks))}</small></td> <td>${formatDateTime(hist.updated_date)}</td> <td class="fw-semibold text-secondary">${escapeHtml(text(hist.updated_by_name || hist.updated_by))}</td> <td class="text-center">${docButtons}</td> `; tbody.appendChild(tr); }); } historyModalObj.show(); } catch (err) { console.error("History Log Fetch error:", err); showLoader(false); } }
// Deleted Records modal async function openDeletedRecordsModal() { const tbody = document.getElementById('deleted-tracking-tbody'); tbody.innerHTML = ''; document.getElementById('deleted-no-records-found').style.display = 'none';
try { showLoader(true, 'Fetching deleted records...'); const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/deleted-list?_=' + new Date().getTime()); const deletedList = await response.json(); showLoader(false);
if (!deletedList || deletedList.length === 0) { document.getElementById('deleted-no-records-found').style.display = 'block'; } else { deletedList.forEach((row, index) => { const tr = document.createElement('tr'); const dispStatus = row.resolved_status || getStatusName(row.status_id) || '';
tr.innerHTML = ` <td class="text-center fw-bold">${index + 1}</td> <td class="fw-bold">${escapeHtml(text(row.tapal_no))}</td> <td>${formatDate(row.tapal_received_date)}</td> <td class="fw-semibold text-secondary">${escapeHtml(text(row.request_from))}</td> <td>${escapeHtml(text(row.subject))}</td> <td><span class="type-badge type-emd">${escapeHtml(text(row.resolved_category || row.cat_type_id))}</span></td> <td><span class="status-badge" style="background-color: ${getStatusStyle(dispStatus).bg}; color: ${getStatusStyle(dispStatus).text};">${escapeHtml(dispStatus)}</span></td> <td class="text-danger fw-semibold"><small>${escapeHtml(text(row.remarks))}</small></td> <td>${formatDateTime(row.updated_date)}</td> <td class="fw-semibold text-secondary">${escapeHtml(text(row.updated_by_name || row.updated_by))}</td> `;
const tdActions = document.createElement('td'); tdActions.className = 'text-center';
const btnRestore = document.createElement('button'); btnRestore.className = 'btn btn-xs btn-success fw-bold py-1 px-2'; btnRestore.style.fontSize = '0.72rem'; btnRestore.innerHTML = '<i class="fa-solid fa-trash-arrow-up"></i> Restore'; btnRestore.onclick = () => restoreDeletedRecord(row.id); tdActions.appendChild(btnRestore);
tr.appendChild(tdActions); tbody.appendChild(tr); }); } deletedRecordsModalObj.show(); } catch (err) { console.error("Deleted records list load error:", err); showLoader(false); } }
async function restoreDeletedRecord(id) { showLoader(true, 'Restoring record...'); const csrfHeaders = getCsrfHeaders(); const params = new URLSearchParams(); params.append('id', id);
try { const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/restore?' + params.toString(), { method: 'POST', headers: csrfHeaders }); const data = await response.json(); if (data.success) { deletedRecordsModalObj.hide(); Swal.fire('Restored!', 'The Tapal record has been successfully restored.', 'success'); loadCounts(); loadList(); } else { Swal.fire('Error', data.error || 'Failed to restore record.', 'error'); showLoader(false); } } catch (err) { console.error("Restore Error:", err); Swal.fire('Error', "Failed to restore record due to connection issues.", 'error'); showLoader(false); } }
// Export Main Table records directly to XLSX using SheetJS function exportMainTableToExcel() { if (!currentFilteredRecords || currentFilteredRecords.length === 0) { Swal.fire('Info', 'No records to export.', 'info'); return; }
const tempTable = document.createElement('table'); const thead = document.createElement('thead'); thead.innerHTML = ` <tr> <th>Sr. No.</th> <th>Tapal No.</th> <th>Received Date</th> <th>Request From</th> <th>Subject</th> <th>Category</th> <th>Status</th> <th>Remarks</th> <th>Tapal File Attachment</th> <th>OC File Attachment</th> <th>Last Updated At</th> <th>Last Updated By</th> </tr> `; tempTable.appendChild(thead);
const tbody = document.createElement('tbody'); currentFilteredRecords.forEach((row, index) => { const tr = document.createElement('tr'); const dispStatus = String(row.resolved_status || getStatusName(row.status_id) || '').trim();
tr.innerHTML = ` <td>${index + 1}</td> <td>${text(row.tapal_no)}</td> <td>${formatDate(row.tapal_received_date)}</td> <td>${text(row.request_from)}</td> <td>${text(row.subject)}</td> <td>${text(row.resolved_category || row.cat_type_id)}</td> <td>${text(dispStatus)}</td> <td>${text(row.remarks)}</td> <td>${row.tapal_file_path ? 'Uploaded' : 'Not Uploaded'}</td> <td>${row.oc_file_path ? 'Uploaded' : 'Not Uploaded'}</td> <td>${formatDateTime(row.updated_date)}</td> <td>${text(row.updated_by_name || '-')}</td> `; tbody.appendChild(tr); }); tempTable.appendChild(tbody);
const wb = XLSX.utils.table_to_book(tempTable, { raw: true, sheet: "Sheet1" }); XLSX.writeFile(wb, 'Tapal_Tracking_Details_Report.xlsx'); }
function resetFilters() { document.getElementById('search-text').value = ''; document.getElementById('table-search').value = ''; document.getElementById('filter-status').value = '';
handleCardClick('all'); }
// Helpers and Formatters 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; };
// --- 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) { }
// Filter allRecords to find files updated on the latest date const files = allRecords.filter(r => { return r.updated_date && getLocalDateString(r.updated_date) === latestDate; });
if (files.length === 0) return; // Set modal title const titleEl = document.getElementById('countDetailsModalTypeTitle'); if (titleEl) { titleEl.textContent = `Latest Updates (${dateDisplay})`; }
// Populate the modal table body const tbody = document.getElementById('modal-tracking-tbody'); if (!tbody) return; tbody.innerHTML = '';
files.forEach((row, index) => { const tr = document.createElement('tr'); const dispStatus = String(row.resolved_status || getStatusName(row.status_id) || '').trim(); const statusStyle = getStatusStyle(dispStatus);
tr.innerHTML = ` <td class="text-center fw-bold">${index + 1}</td> <td class="fw-semibold text-dark">${escapeHtml(text(row.tapal_no))}</td> <td>${formatDate(row.tapal_received_date)}</td> <td class="fw-semibold text-secondary">${escapeHtml(text(row.request_from))}</td> <td>${escapeHtml(text(row.subject))}</td> <td><span class="type-badge type-emd">${escapeHtml(text(row.resolved_category || row.cat_type_id))}</span></td> <td><span class="status-badge" style="background-color: ${statusStyle.bg}; color: ${statusStyle.text};">${escapeHtml(dispStatus)}</span></td> <td><small class="text-muted">${escapeHtml(text(row.remarks))}</small></td> <td>${formatDateTime(row.updated_date)}</td> <td class="fw-semibold text-secondary">${escapeHtml(text(row.updated_by_name || '-'))}</td> `;
// Actions cell const tdActions = document.createElement('td'); tdActions.className = 'text-center';
if (row.tapal_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 Tapal File'; btnViewPdf.onclick = () => { let pdfUrl = row.tapal_file_path; window.open(contextPath + (pdfUrl.startsWith('/') ? pdfUrl.substring(1) : 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 = () => { if (countDetailsModalObj) countDetailsModalObj.hide(); openEditModal(row); }; tdActions.appendChild(btnEdit);
const btnDelete = document.createElement('button'); btnDelete.className = 'btn-grid-action btn-grid-delete me-1'; btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>'; btnDelete.title = 'Delete Record'; btnDelete.onclick = () => { if (countDetailsModalObj) countDetailsModalObj.hide(); confirmDeleteRecord(row.id); }; tdActions.appendChild(btnDelete); }
// History button const btnHistory = document.createElement('button'); btnHistory.className = 'btn-grid-action btn-outline-secondary me-1'; btnHistory.style.border = '1px solid #64748b'; btnHistory.style.color = '#64748b'; btnHistory.style.backgroundColor = 'transparent'; btnHistory.innerHTML = '<i class="fa-solid fa-history"></i>'; btnHistory.title = 'View History Log'; btnHistory.onclick = () => { if (countDetailsModalObj) countDetailsModalObj.hide(); showHistoryLog(row.tapal_no, row.request_from, row.subject); }; tdActions.appendChild(btnHistory);
tr.appendChild(tdActions); tbody.appendChild(tr); });
// Show Bootstrap Modal if (!countDetailsModalObj) { countDetailsModalObj = new bootstrap.Modal(document.getElementById('countDetailsModal')); } countDetailsModalObj.show(); }
function getCsrfHeaders() { const tokenMeta = document.querySelector('meta[name="_csrf"]'); const headerMeta = document.querySelector('meta[name="_csrf_header"]'); if (tokenMeta && headerMeta) { return { [headerMeta.getAttribute('content')]: tokenMeta.getAttribute('content') }; } return {}; }
function escapeHtml(string) { const matchHtmlRegExp = /["'&<>]/; const str = '' + string; const match = matchHtmlRegExp.exec(str);
if (!match) { return str; }
let escape; let html = ''; let index = 0; let lastIndex = 0;
for (index = match.index; index < str.length; index++) { switch (str.charCodeAt(index)) { case 34: // " escape = '"'; break; case 38: // & escape = '&'; break; case 39: // ' escape = '''; break; case 60: // < escape = '<'; break; case 62: // > escape = '>'; break; default: continue; }
if (lastIndex !== index) { html += str.substring(lastIndex, index); }
lastIndex = index + 1; html += escape; }
return lastIndex !== index ? html + str.substring(lastIndex, index) : html; } </script></body>
</html>
|