You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2517 lines
106 KiB
2517 lines
106 KiB
<!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>
|