All GCC vendors and employee details for IT department management
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.
 
 
 
 

2151 lines
83 KiB

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GCC Payment Tracker - Dashboard</title>
<!-- CSRF Tokens -->
<meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" />
<meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" />
<!-- Google Fonts -->
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap"
rel="stylesheet">
<!-- Bootstrap & FontAwesome -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" type="text/css">
<!-- Flatpickr Datepicker -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<!-- Custom Style Sheet mapping to theme -->
<style>
body {
background-color: #f4f7f6;
font-family: 'Poppins', sans-serif;
color: #333;
}
.header {
background: linear-gradient(135deg, #09303d, #0f5c78);
color: #fff;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.logo-left {
display: flex;
align-items: center;
gap: 15px;
}
.logo-left img {
width: 60px;
height: 60px;
}
.logo-text h2 {
font-family: 'Outfit', sans-serif;
font-size: 1.1rem;
font-weight: 700;
margin: 0;
line-height: 1.2;
letter-spacing: 0.5px;
}
.logo-text p {
font-size: 0.7rem;
margin: 0;
color: #aed6f1;
}
.header-title h2 {
font-family: 'Outfit', sans-serif;
font-size: 1.5rem;
font-weight: 700;
margin: 0;
}
.user-menu {
display: flex;
align-items: center;
gap: 15px;
}
.user-badge {
background: rgba(255, 255, 255, 0.15);
padding: 8px 16px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.btn-logout {
background: #e74c3c;
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-logout:hover {
background: #c0392b;
color: #fff;
transform: translateY(-1.5px);
}
.main-container {
max-width: 95%;
margin: 35px auto;
padding: 0 15px;
}
/* 4 Category Cards Dashboard */
.category-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 35px;
}
.category-card {
background: #ffffff;
border: 1px solid #eef2f6;
border-radius: 16px;
padding: 24px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.02);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
.category-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 5px;
height: 100%;
}
/* SD Card Accent */
.category-card.card-sd::before {
background-color: #0f5c78;
}
.category-card.card-sd.active {
border-color: #0f5c78;
box-shadow: 0 10px 25px rgba(15, 92, 120, 0.15);
background-color: #f0f7f9;
}
/* Invoice Card Accent */
.category-card.card-invoice::before {
background-color: #27ae60;
}
.category-card.card-invoice.active {
border-color: #27ae60;
box-shadow: 0 10px 25px rgba(39, 174, 96, 0.15);
background-color: #f1fbf4;
}
/* EMD Card Accent */
.category-card.card-emd::before {
background-color: #8e44ad;
}
.category-card.card-emd.active {
border-color: #8e44ad;
box-shadow: 0 10px 25px rgba(142, 68, 173, 0.15);
background-color: #f9f2fc;
}
/* BG Card Accent */
.category-card.card-bg::before {
background-color: #1e40af;
}
.category-card.card-bg.active {
border-color: #1e40af;
box-shadow: 0 10px 25px rgba(30, 64, 175, 0.15);
background-color: #eff6ff;
}
/* DC Card Accent */
.category-card.card-dc::before {
background-color: #b91c1c;
}
.category-card.card-dc.active {
border-color: #b91c1c;
box-shadow: 0 10px 25px rgba(185, 28, 28, 0.15);
background-color: #fef2f2;
}
/* ASD Card Accent */
.category-card.card-asd::before {
background-color: #d35400;
}
.category-card.card-asd.active {
border-color: #d35400;
box-shadow: 0 10px 25px rgba(211, 84, 0, 0.15);
background-color: #fdf5e6;
}
/* Agreement Card Accent */
.category-card.card-agreement::before {
background-color: #db2777;
}
.category-card.card-agreement.active {
border-color: #db2777;
box-shadow: 0 10px 25px rgba(219, 39, 119, 0.15);
background-color: #fdf2f8;
}
.category-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.06);
}
.card-left {
display: flex;
flex-direction: column;
gap: 5px;
}
.card-icon {
font-size: 2.2rem;
opacity: 0.85;
transition: all 0.3s;
}
.card-sd .card-icon {
color: #0f5c78;
}
.card-invoice .card-icon {
color: #27ae60;
}
.card-emd .card-icon {
color: #8e44ad;
}
.card-bg .card-icon {
color: #1e40af;
}
.card-dc .card-icon {
color: #b91c1c;
}
.card-asd .card-icon {
color: #d35400;
}
.card-agreement .card-icon {
color: #db2777;
}
.category-card:hover .card-icon {
transform: scale(1.1);
}
.card-title {
font-family: 'Outfit', sans-serif;
font-size: 0.95rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0;
}
.card-value {
font-family: 'Outfit', sans-serif;
font-size: 1.85rem;
font-weight: 800;
color: #1e293b;
margin: 0;
}
/* Filter Section */
.filter-card {
background: #ffffff;
border-radius: 16px;
border: 1px solid #eef2f6;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
padding: 24px;
margin-bottom: 30px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
align-items: end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-label {
font-size: 0.8rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-input {
height: 44px;
border-radius: 10px;
border: 1.5px solid #cbd5e1;
padding: 10px 16px;
font-size: 0.9rem;
font-weight: 500;
color: #334155;
background-color: #f8fafc;
transition: all 0.3s ease;
}
.filter-input:focus {
outline: none;
border-color: #0f5c78;
background-color: #ffffff;
box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
}
.btn-filter-search {
height: 44px;
border-radius: 10px;
border: none;
background: linear-gradient(135deg, #0f5c78, #1781a6);
color: #ffffff;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
box-shadow: 0 4px 10px rgba(15, 92, 120, 0.25);
}
.btn-filter-search:hover {
background: linear-gradient(135deg, #1781a6, #0f5c78);
transform: translateY(-1.5px);
}
.btn-filter-reset {
height: 44px;
border-radius: 10px;
border: 1.5px solid #cbd5e1;
background-color: #ffffff;
color: #475569;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
}
.btn-filter-reset:hover {
background-color: #f1f5f9;
color: #0f5c78;
border-color: #0f5c78;
}
/* Table Card Section */
.table-card {
background: #ffffff;
border-radius: 16px;
border: 1px solid #eef2f6;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
padding: 24px;
}
.table-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.table-card-title {
font-family: 'Outfit', sans-serif;
font-size: 1.25rem;
font-weight: 700;
color: #1e293b;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.active-filter-badge {
background-color: #e2e8f0;
color: #475569;
font-size: 0.78rem;
font-weight: 700;
padding: 5px 12px;
border-radius: 20px;
text-transform: uppercase;
}
.btn-add-record {
background: linear-gradient(135deg, #0f5c78, #1781a6);
color: #ffffff;
border: none;
padding: 10px 20px;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 10px rgba(15, 92, 120, 0.2);
text-decoration: none;
}
.btn-add-record:hover {
background: linear-gradient(135deg, #1781a6, #0f5c78);
transform: translateY(-2px);
color: #ffffff;
}
/* Custom Data Grid Table */
.grid-table {
font-size: 0.85rem;
}
.grid-table th {
background-color: #f1f5f9 !important;
color: #475569 !important;
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.5px;
padding: 14px 10px;
border-bottom: 2px solid #cbd5e1 !important;
vertical-align: middle;
}
.grid-table td {
padding: 12px 10px;
vertical-align: middle;
border-bottom: 1px solid #e2e8f0;
}
.grid-table tbody tr:hover {
background-color: #f8fafc;
}
/* Badges for status */
.status-badge {
font-size: 0.72rem;
font-weight: 700;
padding: 4px 10px;
border-radius: 12px;
text-transform: uppercase;
display: inline-block;
}
.status-pending {
background-color: #fef9c3;
color: #854d0e;
}
.status-awaiting-details {
background-color: #ffedd5;
color: #c2410c;
}
.status-pending-with-dept {
background-color: #e0f2fe;
color: #0369a1;
}
.status-completed {
background-color: #d1fae5;
color: #065f46;
}
.status-no-action {
background-color: #f1f5f9;
color: #475569;
}
.status-processed {
background-color: #dbeafe;
color: #1e40af;
}
.status-approved {
background-color: #d1fae5;
color: #065f46;
}
.status-paid {
background-color: #dcfce7;
color: #166534;
}
.status-rejected {
background-color: #fee2e2;
color: #991b1b;
}
.status-default {
background-color: #f1f5f9;
color: #475569;
}
/* Type Badges */
.type-badge {
font-size: 0.72rem;
font-weight: 700;
padding: 4px 10px;
border-radius: 12px;
text-transform: uppercase;
display: inline-block;
}
.type-sd {
background-color: #f0f7f9;
color: #0f5c78;
border: 1px solid rgba(15, 92, 120, 0.25);
}
.type-tax-invoice {
background-color: #f1fbf4;
color: #27ae60;
border: 1px solid rgba(39, 174, 96, 0.25);
}
.type-emd {
background-color: #f9f2fc;
color: #8e44ad;
border: 1px solid rgba(142, 68, 173, 0.25);
}
.type-bank-guarantee {
background-color: #eff6ff;
color: #1e40af;
border: 1px solid rgba(30, 64, 175, 0.25);
}
.type-delivery-chellan {
background-color: #fef2f2;
color: #b91c1c;
border: 1px solid rgba(185, 28, 28, 0.25);
}
.type-asd {
background-color: #fdf5e6;
color: #d35400;
border: 1px solid rgba(211, 84, 0, 0.25);
}
.type-agreement {
background-color: #fdf2f8;
color: #db2777;
border: 1px solid rgba(219, 39, 119, 0.25);
}
.btn-grid-action {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-grid-edit {
background-color: #e0f2fe;
color: #0369a1;
}
.btn-grid-edit:hover {
background-color: #0369a1;
color: #ffffff;
transform: scale(1.08);
}
.btn-grid-delete {
background-color: #fee2e2;
color: #b91c1c;
}
.btn-grid-delete:hover {
background-color: #b91c1c;
color: #ffffff;
transform: scale(1.08);
}
.btn-grid-pdf {
background-color: #fef2f2;
color: #b91c1c;
border: 1px solid rgba(185, 28, 28, 0.15) !important;
}
.btn-grid-pdf:hover {
background-color: #b91c1c;
color: #ffffff;
transform: scale(1.08);
}
/* Loading Overlay styling */
#loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 99999;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
#loading-overlay.active {
opacity: 1;
pointer-events: all;
display: flex;
}
.loader-spinner-container {
background: white;
padding: 2.5rem 3.5rem;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(15, 92, 120, 0.1);
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
}
.loader-spinner {
font-size: 3rem;
color: #0f5c78;
}
.loader-text {
font-family: 'Outfit', 'Inter', sans-serif;
font-size: 1.1rem;
font-weight: 600;
color: #0f5c78;
letter-spacing: 0.5px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #64748b;
}
.empty-icon {
font-size: 3.5rem;
color: #cbd5e1;
margin-bottom: 15px;
}
/* Custom Modal Header Styling */
.modal-custom-header {
background-color: #0f5c78;
color: white;
border-top-left-radius: 14px;
border-top-right-radius: 14px;
padding: 18px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-custom-title {
font-weight: 700;
font-size: 1.25rem;
display: flex;
align-items: center;
gap: 10px;
margin: 0;
font-family: 'Outfit', sans-serif;
}
.modal-content {
border-radius: 14px;
border: none;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.25);
}
/* Pagination and Table Controls Styling */
.pagination .page-item .page-link {
color: #0f5c78;
border-color: #cbd5e1;
font-weight: 500;
padding: 6px 12px;
transition: all 0.2s;
}
.pagination .page-item.active .page-link {
background-color: #0f5c78;
border-color: #0f5c78;
color: #ffffff;
}
.pagination .page-item .page-link:hover {
background-color: #e2e8f0;
border-color: #0f5c78;
color: #0f5c78;
}
.pagination .page-item.disabled .page-link {
color: #94a3b8;
border-color: #cbd5e1;
background-color: #f1f5f9;
}
.table-controls-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.table-controls-left,
.table-controls-right {
display: flex;
align-items: center;
gap: 8px;
}
</style>
</head>
<body>
<!-- Fullscreen Loading Overlay -->
<div id="loading-overlay">
<div class="loader-spinner-container">
<i class="fa-solid fa-spinner fa-spin loader-spinner"></i>
<span class="loader-text" id="loader-overlay-text">Processing...</span>
</div>
</div>
<!-- Header Bar -->
<div class="header">
<div class="logo-left">
<img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation Logo">
<div class="logo-text">
<h2>GREATER CHENNAI CORPORATION</h2>
<p>பெருநகர சென்னை மாநகராட்சி</p>
</div>
</div>
<div class="header-title">
<h2>Payment Processing Tracker</h2>
</div>
<div class="user-menu">
<span class="user-badge">
<i class="fa-solid fa-user-shield"></i>
<span th:text="${usrName}">Guest</span>
</span>
<a th:href="@{/gccservicepaymenttracker/logout}" class="btn-logout">
<i class="fa-solid fa-right-from-bracket"></i> Logout
</a>
</div>
</div>
<div class="main-container">
<!-- Category Grid (5 Cards) -->
<div class="category-grid">
<!-- Security Deposit -->
<div class="category-card card-sd" onclick="selectCategory('sd', this)">
<div class="card-left">
<h3 class="card-title">Security Deposit</h3>
<p class="card-value" id="count-sd">0</p>
</div>
<i class="fa-solid fa-wallet card-icon"></i>
</div>
<!-- Tax Invoice -->
<div class="category-card card-invoice" onclick="selectCategory('tax invoice', this)">
<div class="card-left">
<h3 class="card-title">Tax Invoice</h3>
<p class="card-value" id="count-invoice">0</p>
</div>
<i class="fa-solid fa-file-invoice-dollar card-icon"></i>
</div>
<!-- EMD -->
<div class="category-card card-emd" onclick="selectCategory('emd', this)">
<div class="card-left">
<h3 class="card-title">EMD</h3>
<p class="card-value" id="count-emd">0</p>
</div>
<i class="fa-solid fa-file-signature card-icon"></i>
</div>
<!-- Bank Guarantee commented out
<div class="category-card card-bg" onclick="selectCategory('bank guarantee', this)">
<div class="card-left">
<h3 class="card-title">Bank Guarantee</h3>
<p class="card-value" id="count-bg">0</p>
</div>
<i class="fa-solid fa-shield-halved card-icon"></i>
</div>
-->
<!-- Delivery Chellan commented out
<div class="category-card card-dc" onclick="selectCategory('delivery chellan', this)">
<div class="card-left">
<h3 class="card-title">Delivery Chellan</h3>
<p class="card-value" id="count-dc">0</p>
</div>
<i class="fa-solid fa-truck-ramp-box card-icon"></i>
</div>
-->
<!-- ASD -->
<div class="category-card card-asd" onclick="selectCategory('asd', this)">
<div class="card-left">
<h3 class="card-title">ASD</h3>
<p class="card-value" id="count-asd">0</p>
</div>
<i class="fa-solid fa-vault card-icon"></i>
</div>
<!-- Agreement -->
<div class="category-card card-agreement" onclick="selectCategory('agreement', this)">
<div class="card-left">
<h3 class="card-title">Agreement</h3>
<p class="card-value" id="count-agreement">0</p>
</div>
<i class="fa-solid fa-file-contract card-icon"></i>
</div>
</div>
<!-- Filter Card -->
<div class="filter-card">
<div class="filter-grid">
<!-- Date range picker
<div class="filter-group">
<label class="filter-label" for="filter-from-date">From Date</label>
<input type="text" id="filter-from-date" class="filter-input" placeholder="dd/mm/yyyy" readonly>
</div>
<div class="filter-group">
<label class="filter-label" for="filter-to-date">To Date</label>
<input type="text" id="filter-to-date" class="filter-input" placeholder="dd/mm/yyyy" readonly>
</div>
-->
<!-- Type Filter -->
<div class="filter-group">
<label class="filter-label" for="filter-type">Type</label>
<select id="filter-type" class="filter-input" onchange="handleTypeFilterChange(this.value)">
<option value="all">- All Types -</option>
<!-- Commented out static options to load from database
<option value="sd">Security Deposit</option>
<option value="tax invoice">Tax Invoice</option>
<option value="emd">EMD</option>
<option value="asd">ASD</option>
<option value="agreement">Agreement</option>
-->
</select>
</div>
<!-- Status Filter -->
<div class="filter-group">
<label class="filter-label" for="filter-status">Status</label>
<select id="filter-status" class="filter-input" onchange="loadList()">
<option value="">- All Statuses -</option>
<option value="Awaiting Details">Awaiting Details</option>
<option value="Pending with Dept">Pending with Dept</option>
<option value="Completed">Completed</option>
<option value="No Action">No Action</option>
<option value="Pending">Pending</option>
</select>
</div>
<!-- Text Search -->
<div class="filter-group">
<label class="filter-label" for="search-text">Search Keyword</label>
<input type="text" id="search-text" class="filter-input" placeholder="Vendor, subject, invoice #..."
oninput="handleSearchInput(this.value)">
</div>
<!-- Action buttons -->
<button class="btn-filter-search" onclick="loadList()">
<i class="fa-solid fa-magnifying-glass"></i> Filter
</button>
<button class="btn-filter-reset" onclick="resetFilters()">
<i class="fa-solid fa-rotate-right"></i> Reset
</button>
</div>
</div>
<!-- Table Card Section -->
<div class="table-card">
<div class="table-card-header">
<h3 class="table-card-title">
<i class="fa-solid fa-table-list text-primary"></i>
<span>Payment Tracking Details</span>
<span class="active-filter-badge" id="active-category-badge">Type: All Types</span>
</h3>
<button th:if="${isAdmin}" class="btn-add-record" onclick="openAddModal()">
<i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record
</button>
</div>
<!-- Table Controls: Page size & Real-time Search -->
<div class="table-controls-row">
<div class="table-controls-left">
<span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Show</span>
<select id="table-page-size" class="form-select form-select-sm" style="width: 80px;"
onchange="changePageSize(this.value)">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="30">30</option>
<option value="40">40</option>
<option value="50">50</option>
</select>
<span class="text-secondary fw-semibold" style="font-size: 0.85rem;">entries</span>
</div>
<div class="table-controls-right">
<span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Search:</span>
<input type="text" id="table-search" class="form-control form-control-sm" style="width: 250px;"
placeholder="Search in this table..." oninput="handleTableSearch(this.value)">
</div>
</div>
<!-- Responsive Table Grid -->
<div class="table-responsive">
<table class="table grid-table table-striped table-hover align-middle" id="tracking-table">
<!-- Commented out old thead code
<thead>
<tr>
<th class="text-center" style="width: 50px;">S.No</th>
<th>Received Date</th>
<th>Type</th>
<th>Vendor Name</th>
<th>Subject</th>
<th>Amount</th>
<th>Invoice No</th>
<th>Receipt No</th>
<th>Invoice Date</th>
<th>Physical File No</th>
<th>Remarks</th>
<th>TAT</th>
<th>Status</th>
<th>Followup</th>
<th>My Followup</th>
<th class="text-center" style="width: 100px;">Actions</th>
</tr>
</thead>
-->
<thead>
<tr>
<th class="text-center" style="width: 50px;">Sr. No.</th>
<th>Received Date</th>
<th>Invoice Date</th>
<th>Physical File No.</th>
<th>eFile No./ Receipt No.</th>
<th>Subject</th>
<th>Type (SD/ ASD)</th>
<th>Vendor Name</th>
<th>Amount</th>
<th>Age of Bill (Days)</th>
<th>Status (Pending / Completed)</th>
<th>Remarks</th>
<th>Updated on</th>
<th class="text-center" style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody id="tracking-tbody">
<!-- Populated dynamically via JS -->
</tbody>
</table>
<div id="no-records-found" class="empty-state" style="display: none;">
<i class="fa-solid fa-folder-open empty-icon"></i>
<h4>No Records Found</h4>
<p>No payment tracking records match the active criteria.</p>
</div>
</div>
<!-- Pagination Controls -->
<div class="d-flex justify-content-between align-items-center mt-3 flex-wrap gap-2">
<div class="text-secondary fw-semibold" style="font-size: 0.85rem;" id="pagination-info">
Showing 0 to 0 of 0 entries
</div>
<nav aria-label="Table navigation">
<ul class="pagination pagination-sm mb-0" id="pagination-controls">
<!-- Populated dynamically via JS -->
</ul>
</nav>
</div>
</div>
</div>
<!-- Add/Edit Modal Form -->
<div class="modal fade" id="recordModal" tabindex="-1" aria-labelledby="recordModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-custom-header">
<h5 class="modal-custom-title" id="recordModalLabel">
<i class="fa-solid fa-file-invoice-dollar"></i> Add/Edit Record
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="recordForm" onsubmit="saveRecord(event)">
<input type="hidden" id="form-id" name="id">
<div class="row g-3">
<!-- Received Date -->
<div class="col-md-6">
<label for="form-received-date" class="form-label fw-semibold text-secondary">Received
Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" id="form-received-date" required
onclick="this.showPicker()">
</div>
<!-- Type -->
<div class="col-md-6">
<label for="form-type" class="form-label fw-semibold text-secondary">Type <span
class="text-danger">*</span></label>
<select class="form-select" id="form-type" required>
<!-- Commented out static options to load from database
<option value="sd">Security Deposit</option>
<option value="tax invoice">Tax Invoice</option>
<option value="emd">EMD</option>
<option value="asd">ASD</option>
<option value="agreement">Agreement</option>
-->
</select>
</div>
<!-- Vendor Name (Dynamic dropdown + Write-in) -->
<div class="col-md-6">
<label for="form-vendor-select" class="form-label fw-semibold text-secondary">Vendor
Name <span class="text-danger">*</span></label>
<select class="form-select mb-2" id="form-vendor-select"
onchange="toggleCustomVendor()">
<option value="">-- Select Vendor --</option>
<!-- Commented out model-populated options
<option th:each="vendor : ${vendorDetails}" th:value="${vendor.vendor_name}"
th:text="${vendor.vendor_name}"></option>
-->
<option value="__custom__">-- Write-in custom vendor --</option>
</select>
<input type="text" class="form-control" id="form-vendor-custom"
placeholder="Enter Custom Vendor Name" style="display: none;">
</div>
<!-- Subject -->
<div class="col-md-6">
<label for="form-subject" class="form-label fw-semibold text-secondary">Subject <span
class="text-danger">*</span></label>
<input type="text" class="form-control" id="form-subject"
placeholder="Enter subject/matter" required>
</div>
<!-- Amount -->
<div class="col-md-6">
<label for="form-amount" class="form-label fw-semibold text-secondary">Amount <span
class="text-danger">*</span></label>
<input type="text" class="form-control" id="form-amount"
placeholder="Enter amount (e.g. 50,000)" required>
</div>
<!-- Invoice No -->
<div class="col-md-6">
<label for="form-invoice-no" class="form-label fw-semibold text-secondary">Invoice
No</label>
<input type="text" class="form-control" id="form-invoice-no"
placeholder="Enter invoice number">
</div>
<!-- Receipt No -->
<div class="col-md-6">
<label for="form-receiptno" class="form-label fw-semibold text-secondary">Receipt
No</label>
<input type="text" class="form-control" id="form-receiptno"
placeholder="Enter Receipt No">
</div>
<!-- Invoice Date -->
<div class="col-md-6">
<label for="form-invoice-date" class="form-label fw-semibold text-secondary">Invoice
Date</label>
<input type="date" class="form-control" id="form-invoice-date"
onclick="this.showPicker()">
</div>
<!-- Physical File No -->
<div class="col-md-6">
<label for="form-physical-file-no"
class="form-label fw-semibold text-secondary">Physical File No</label>
<input type="text" class="form-control" id="form-physical-file-no"
placeholder="Enter physical file reference number">
</div>
<!-- TAT -->
<!-- <div class="col-md-6">
<label for="form-tat" class="form-label fw-semibold text-secondary">Turnaround Time
(TAT)</label>
<input type="text" class="form-control" id="form-tat" placeholder="e.g. 5 days">
</div> -->
<!-- Status -->
<div class="col-md-6">
<label for="form-status" class="form-label fw-semibold text-secondary">Status <span
class="text-danger">*</span></label>
<select class="form-select" id="form-status" required>
<option value="Awaiting Details">Awaiting Details</option>
<option value="Pending with Dept">Pending with Dept</option>
<option value="Completed">Completed</option>
<option value="No Action">No Action</option>
<option value="Pending">Pending</option>
</select>
</div>
<!-- Followup commented out
<div class="col-md-6">
<label for="form-followup"
class="form-label fw-semibold text-secondary">Follow-up</label>
<input type="text" class="form-control" id="form-followup" placeholder="Follow-up note">
</div>
-->
<!-- My Followup commented out
<div class="col-md-6">
<label for="form-myfollowup" class="form-label fw-semibold text-secondary">My
Follow-up</label>
<input type="text" class="form-control" id="form-myfollowup"
placeholder="Internal follow-up note">
</div>
-->
<!-- Order By commented out
<div class="col-md-6">
<label for="form-orderby" class="form-label fw-semibold text-secondary">Order By</label>
<input type="text" class="form-control" id="form-orderby"
placeholder="Reference order description">
</div>
-->
<!-- PDF Attachment -->
<div class="col-md-12">
<label for="form-pdf" class="form-label fw-semibold text-secondary">PDF Attachment
(Optional)</label>
<input type="file" class="form-control" id="form-pdf" accept="application/pdf">
<div id="form-existing-pdf-container" class="mt-2" style="display: none;">
<span class="text-secondary fw-semibold">Current Document:</span>
<a id="form-existing-pdf-link" href="#" target="_blank"
class="ms-1 text-primary fw-bold text-decoration-none">
<i class="fa-solid fa-file-pdf"></i> View PDF
</a>
</div>
</div>
<!-- Remarks -->
<div class="col-12">
<label for="form-remarks" class="form-label fw-semibold text-secondary">Remarks</label>
<textarea class="form-control" id="form-remarks" rows="2"
placeholder="Additional remarks..."></textarea>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4"
style="background-color: #0f5c78; border-color: #0f5c78;">
<i class="fa-solid fa-save"></i> Save Record
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Bootstrap & FontAwesome bundles -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom Page Scripting -->
<script th:inline="javascript">
const contextPath = /*[[@{/}]]*/ '';
const isAdmin = /*[[${isAdmin}]]*/ false;
let activeCategory = 'all';
let allRecords = [];
let recordModalObj = null;
let currentPage = 1;
let pageSize = 20;
// Dynamic lists for types and vendors
let activeTypes = [];
let activeVendors = [];
// Date Pickers
// let fromDatePicker, toDatePicker;
document.addEventListener('DOMContentLoaded', async () => {
recordModalObj = new bootstrap.Modal(document.getElementById('recordModal'));
// initDatePickers();
await loadTypesAndVendors();
loadCounts();
loadList();
});
// Helper to map type_name to a category code
function getCategoryCode(typeName) {
const type = String(typeName).toLowerCase().trim();
if (type === 'asd' || type.includes('asd') || type.includes('additional security')) {
return 'asd';
} else if (type.includes('agreement')) {
return 'agreement';
} else if (type === 'sd' || (type.includes('security') && !type.includes('additional') && !type.includes('asd'))) {
return 'sd';
} else if (type.includes('invoice') || type.includes('bill')) {
return 'tax invoice';
} else if (type === 'emd' || type.includes('earnest')) {
return 'emd';
} else if (type.includes('guarantee') || type === 'bg') {
return 'bank guarantee';
} else if (type.includes('chellan') || type.includes('challan') || type === 'dc') {
return 'delivery chellan';
}
return type;
}
// Fetch types and vendors from DB endpoints
async function loadTypesAndVendors() {
try {
const [typesRes, vendorsRes] = await Promise.all([
fetch(contextPath + 'gccservicepaymenttracker/api/types?_=' + new Date().getTime()),
fetch(contextPath + 'gccservicepaymenttracker/api/vendors?_=' + new Date().getTime())
]);
activeTypes = await typesRes.json();
activeVendors = await vendorsRes.json();
populateDropdowns();
} catch (err) {
console.error("Error loading types or vendors:", err);
}
}
// Populate selects with dynamic options
function populateDropdowns() {
// Populate filter-type
const filterTypeSelect = document.getElementById('filter-type');
if (filterTypeSelect) {
filterTypeSelect.innerHTML = '<option value="all">- All Types -</option>';
activeTypes.forEach(t => {
const categoryCode = getCategoryCode(t.type_name);
const opt = document.createElement('option');
opt.value = categoryCode;
opt.textContent = t.type_name;
filterTypeSelect.appendChild(opt);
});
}
// Populate form-type
const formTypeSelect = document.getElementById('form-type');
if (formTypeSelect) {
formTypeSelect.innerHTML = '<option value="">-- Select Type --</option>';
activeTypes.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.type_name;
formTypeSelect.appendChild(opt);
});
}
// Populate form-vendor-select
const formVendorSelect = document.getElementById('form-vendor-select');
if (formVendorSelect) {
formVendorSelect.innerHTML = '<option value="">-- Select Vendor --</option>';
activeVendors.forEach(v => {
const opt = document.createElement('option');
opt.value = v.id;
opt.textContent = v.vendor_name;
formVendorSelect.appendChild(opt);
});
const customOpt = document.createElement('option');
customOpt.value = '__custom__';
customOpt.textContent = '-- Write-in custom vendor --';
formVendorSelect.appendChild(customOpt);
}
}
// Show/hide loader overlay
function showLoader(show, text = 'Processing...') {
const overlay = document.getElementById('loading-overlay');
const overlayText = document.getElementById('loader-overlay-text');
overlayText.textContent = text;
if (show) {
overlay.classList.add('active');
} else {
overlay.classList.remove('active');
}
}
// Get CSRF details
function getCsrfHeaders() {
const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
const headers = {};
if (csrfToken && csrfHeader) {
headers[csrfHeader] = csrfToken;
}
return headers;
}
// Category count loaders
function loadCounts() {
fetch(contextPath + 'gccservicepaymenttracker/api/counts?_=' + new Date().getTime())
.then(response => response.json())
.then(data => {
document.getElementById('count-sd').textContent = data.sd || 0;
document.getElementById('count-invoice').textContent = data['tax invoice'] || 0;
document.getElementById('count-emd').textContent = data.emd || 0;
const bgEl = document.getElementById('count-bg');
if (bgEl) bgEl.textContent = data['bank guarantee'] || 0;
const dcEl = document.getElementById('count-dc');
if (dcEl) dcEl.textContent = data['delivery chellan'] || 0;
document.getElementById('count-asd').textContent = data.asd || 0;
document.getElementById('count-agreement').textContent = data.agreement || 0;
})
.catch(err => console.error("Error loading category counts:", err));
}
// Handles select change of card
function selectCategory(category, cardElement) {
const isActive = cardElement.classList.contains('active');
// Remove active class from all
document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
const filterTypeSelect = document.getElementById('filter-type');
if (isActive) {
activeCategory = 'all';
document.getElementById('active-category-badge').textContent = 'Type: All Types';
if (filterTypeSelect) {
filterTypeSelect.value = 'all';
}
} else {
cardElement.classList.add('active');
activeCategory = category;
if (filterTypeSelect) {
filterTypeSelect.value = category;
}
// Update badge text
const labels = {
'sd': 'Security Deposit',
'tax invoice': 'Tax Invoice',
'emd': 'EMD',
'asd': 'ASD',
'agreement': 'Agreement'
};
document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
}
// Refresh list
loadList();
}
// Loading and Rendering list data
function loadList() {
showLoader(true, 'Loading payment tracking details...');
const statusVal = document.getElementById('filter-status').value;
const searchVal = document.getElementById('search-text').value;
// Sync search-text to table-search when hitting main filter search
const tblSearch = document.getElementById('table-search');
if (tblSearch) {
tblSearch.value = searchVal;
}
// Build query params
const params = new URLSearchParams();
if (activeCategory && activeCategory !== 'all') {
params.append('category', activeCategory);
}
if (statusVal) params.append('status', statusVal);
// Add cache buster
params.append('_', new Date().getTime());
fetch(contextPath + 'gccservicepaymenttracker/api/list?' + params.toString())
.then(response => response.json())
.then(data => {
allRecords = data;
currentPage = 1;
renderTable();
})
.catch(err => {
console.error("Error loading details:", err);
showLoader(false);
});
}
function renderTable() {
const tbody = document.getElementById('tracking-tbody');
const noRecords = document.getElementById('no-records-found');
tbody.innerHTML = '';
const searchVal = document.getElementById('table-search')?.value || '';
let query = searchVal.toLowerCase().trim();
let filtered = allRecords.filter(row => {
if (!query) return true;
const vName = String(row.resolved_vendor || row.vendor_name || '').toLowerCase();
const tName = String(row.resolved_type || row.type || '').toLowerCase();
return (
vName.includes(query) ||
tName.includes(query) ||
(row.subject && String(row.subject).toLowerCase().includes(query)) ||
(row.amount && String(row.amount).toLowerCase().includes(query)) ||
(row.invoice_no && String(row.invoice_no).toLowerCase().includes(query)) ||
(row.receiptno && String(row.receiptno).toLowerCase().includes(query)) ||
(row.physical_file_no && String(row.physical_file_no).toLowerCase().includes(query)) ||
(row.remarks && String(row.remarks).toLowerCase().includes(query)) ||
(row.tat && String(row.tat).toLowerCase().includes(query)) ||
(row.status && String(row.status).toLowerCase().includes(query))
);
});
showLoader(false);
const totalRecords = filtered.length;
if (totalRecords === 0) {
noRecords.style.display = 'block';
document.getElementById('pagination-info').textContent = 'Showing 0 to 0 of 0 entries';
document.getElementById('pagination-controls').innerHTML = '';
return;
}
noRecords.style.display = 'none';
// Paginate
const totalPages = Math.ceil(totalRecords / pageSize);
if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentPage < 1) {
currentPage = 1;
}
const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, totalRecords);
const paginatedData = filtered.slice(startIndex, endIndex);
// Update Pagination Info
document.getElementById('pagination-info').textContent =
`Showing ${startIndex + 1} to ${endIndex} of ${totalRecords} entries`;
paginatedData.forEach((row, pageIndex) => {
const globalIndex = startIndex + pageIndex;
const tr = document.createElement('tr');
// S.No
const tdIndex = document.createElement('td');
tdIndex.className = 'text-center fw-semibold text-secondary';
tdIndex.textContent = globalIndex + 1;
tr.appendChild(tdIndex);
// Helper for blank values
const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
// Format Dates (yyyy-MM-dd to dd/MM/yyyy)
const formatDate = dateStr => {
if (!dateStr) return '-';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`;
}
return dateStr;
};
// Format Date-Times (yyyy-MM-ddTHH:mm:ss... to dd/MM/yyyy HH:mm:ss)
const formatDateTime = dateTimeStr => {
if (!dateTimeStr) return '-';
try {
const tParts = dateTimeStr.split('T');
const datePart = tParts[0];
const timePart = tParts.length > 1 ? tParts[1].split('.')[0] : '';
const dateParts = datePart.split('-');
if (dateParts.length === 3) {
const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`;
return timePart ? `${formattedDate} ${timePart}` : formattedDate;
}
} catch (e) {
console.error(e);
}
return dateTimeStr;
};
// Calculate age of bill (days) dynamically from received date:
const calculateAgeOfBill = receivedDateStr => {
if (!receivedDateStr) return '0';
try {
const recDate = new Date(receivedDateStr);
if (isNaN(recDate.getTime())) return '0';
recDate.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const graceDate = new Date(recDate);
graceDate.setDate(graceDate.getDate() + 7);
if (today <= graceDate) {
return '0';
} else {
const diffTime = today.getTime() - graceDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return String(diffDays);
}
} catch (e) {
console.error("Error calculating age of bill:", e);
return '0';
}
};
const getTypeBadge = typeVal => {
const cleanType = String(typeVal || '').trim().toLowerCase();
let label = cleanType;
let badgeClass = 'type-sd';
if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) {
label = 'ASD';
badgeClass = 'type-asd';
} else if (cleanType.includes('agreement')) {
label = 'Agreement';
badgeClass = 'type-agreement';
} else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) {
label = 'Security Deposit';
badgeClass = 'type-sd';
} else if (cleanType.includes('invoice') || cleanType.includes('bill')) {
label = 'Tax Invoice';
badgeClass = 'type-tax-invoice';
} else if (cleanType === 'emd' || cleanType.includes('earnest')) {
label = 'EMD';
badgeClass = 'type-emd';
} else if (cleanType.includes('guarantee') || cleanType === 'bg') {
label = 'Bank Guarantee';
badgeClass = 'type-bank-guarantee';
} else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') {
label = 'Delivery Chellan';
badgeClass = 'type-delivery-chellan';
} else {
// Fallback: check if cleanType is a numeric ID from activeTypes
const typeObj = activeTypes.find(t => String(t.id) === cleanType);
if (typeObj) {
return getTypeBadge(typeObj.type_name);
}
label = typeVal || '-';
badgeClass = 'status-default';
}
return `<span class="type-badge ${badgeClass}">${label}</span>`;
};
tr.innerHTML += `
<td>${formatDate(row.received_date)}</td>
<td>${formatDate(row.invoice_date)}</td>
<td>${text(row.physical_file_no)}</td>
<td>${text(row.receiptno)}</td>
<td>${text(row.subject)}</td>
<td>${getTypeBadge(row.resolved_type || row.type)}</td>
<td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
<td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
<td>${calculateAgeOfBill(row.received_date)}</td>
<td><span class="status-badge status-${String(row.status || '').toLowerCase().trim().replace(/\s+/g, '-')}">${text(row.status)}</span></td>
<td><small class="text-muted">${text(row.remarks)}</small></td>
<td>${formatDateTime(row.updated_date)}</td>
`;
// Actions cell
const tdActions = document.createElement('td');
tdActions.className = 'text-center';
if (row.file_path) {
const btnViewPdf = document.createElement('button');
btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
btnViewPdf.title = 'View PDF Document';
btnViewPdf.onclick = () => {
let pdfUrl = row.file_path;
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
pdfUrl = contextPath + pdfUrl.substring(1);
} else {
pdfUrl = contextPath + pdfUrl;
}
window.open(pdfUrl, '_blank');
};
tdActions.appendChild(btnViewPdf);
}
if (isAdmin) {
const btnEdit = document.createElement('button');
btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
btnEdit.title = 'Edit Record';
btnEdit.onclick = () => openEditModal(row);
const btnDelete = document.createElement('button');
btnDelete.className = 'btn-grid-action btn-grid-delete';
btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
btnDelete.title = 'Delete Record';
btnDelete.onclick = () => deleteRecord(row.id);
tdActions.appendChild(btnEdit);
tdActions.appendChild(btnDelete);
}
tr.appendChild(tdActions);
tbody.appendChild(tr);
});
// Rebuild Pagination Controls
buildPaginationControls(totalPages);
}
function buildPaginationControls(totalPages) {
const container = document.getElementById('pagination-controls');
container.innerHTML = '';
if (totalPages <= 1) {
return;
}
// Previous Button
const prevLi = document.createElement('li');
prevLi.className = 'page-item' + (currentPage === 1 ? ' disabled' : '');
prevLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage - 1})">Previous</a>`;
container.appendChild(prevLi);
// Page numbers
const maxVisible = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
if (endPage - startPage + 1 < maxVisible) {
startPage = Math.max(1, endPage - maxVisible + 1);
}
if (startPage > 1) {
const firstLi = document.createElement('li');
firstLi.className = 'page-item';
firstLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(1)">1</a>`;
container.appendChild(firstLi);
if (startPage > 2) {
const dotsLi = document.createElement('li');
dotsLi.className = 'page-item disabled';
dotsLi.innerHTML = `<span class="page-link">...</span>`;
container.appendChild(dotsLi);
}
}
for (let i = startPage; i <= endPage; i++) {
const pageLi = document.createElement('li');
pageLi.className = 'page-item' + (i === currentPage ? ' active' : '');
pageLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${i})">${i}</a>`;
container.appendChild(pageLi);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const dotsLi = document.createElement('li');
dotsLi.className = 'page-item disabled';
dotsLi.innerHTML = `<span class="page-link">...</span>`;
container.appendChild(dotsLi);
}
const lastLi = document.createElement('li');
lastLi.className = 'page-item';
lastLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${totalPages})">${totalPages}</a>`;
container.appendChild(lastLi);
}
// Next Button
const nextLi = document.createElement('li');
nextLi.className = 'page-item' + (currentPage === totalPages ? ' disabled' : '');
nextLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage + 1})">Next</a>`;
container.appendChild(nextLi);
}
function goToPage(page) {
currentPage = page;
renderTable();
}
function changePageSize(size) {
pageSize = parseInt(size, 10);
currentPage = 1;
renderTable();
}
// Handles search in local table search input
function handleTableSearch(keyword) {
const filterSearch = document.getElementById('search-text');
if (filterSearch) {
filterSearch.value = keyword;
}
currentPage = 1;
renderTable();
}
function handleSearchInput(keyword) {
const tblSearch = document.getElementById('table-search');
if (tblSearch) {
tblSearch.value = keyword;
}
currentPage = 1;
renderTable();
}
function handleTypeFilterChange(category) {
activeCategory = category;
// Remove active class from all top cards
document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
if (category !== 'all') {
const classMap = {
'sd': 'card-sd',
'tax invoice': 'card-invoice',
'emd': 'card-emd',
'asd': 'card-asd',
'agreement': 'card-agreement'
};
const cardClass = classMap[category];
if (cardClass) {
const cardElement = document.querySelector('.' + cardClass);
if (cardElement) {
cardElement.classList.add('active');
}
}
const labels = {
'sd': 'Security Deposit',
'tax invoice': 'Tax Invoice',
'emd': 'EMD',
'asd': 'ASD',
'agreement': 'Agreement'
};
document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
} else {
document.getElementById('active-category-badge').textContent = 'Type: All Types';
}
loadList();
}
function resetFilters() {
document.getElementById('filter-status').value = '';
document.getElementById('search-text').value = '';
const filterType = document.getElementById('filter-type');
if (filterType) {
filterType.value = 'all';
}
activeCategory = 'all';
document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
document.getElementById('active-category-badge').textContent = 'Type: All Types';
const tblSearch = document.getElementById('table-search');
if (tblSearch) {
tblSearch.value = '';
}
currentPage = 1;
loadList();
}
// Toggle text field for dynamic vendor select dropdown
function toggleCustomVendor() {
const select = document.getElementById('form-vendor-select');
const customInput = document.getElementById('form-vendor-custom');
if (select.value === '__custom__') {
customInput.style.display = 'block';
customInput.required = true;
} else {
customInput.style.display = 'none';
customInput.required = false;
customInput.value = '';
}
}
// Edit/Add Modals
function openAddModal() {
document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record';
document.getElementById('recordForm').reset();
document.getElementById('form-id').value = '';
// Set type to active category by default
const formTypeSelect = document.getElementById('form-type');
formTypeSelect.value = '';
if (activeCategory && activeCategory !== 'all') {
for (let option of formTypeSelect.options) {
if (option.value) {
const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
if (typeObj && getCategoryCode(typeObj.type_name) === activeCategory) {
formTypeSelect.value = option.value;
break;
}
}
}
}
document.getElementById('form-status').value = 'Pending';
document.getElementById('form-vendor-custom').style.display = 'none';
document.getElementById('form-vendor-custom').required = false;
// Reset PDF upload elements
document.getElementById('form-pdf').value = '';
document.getElementById('form-existing-pdf-container').style.display = 'none';
recordModalObj.show();
}
function openEditModal(row) {
document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-file-signature"></i> Edit Payment Tracking Record';
document.getElementById('recordForm').reset();
document.getElementById('form-id').value = row.id;
document.getElementById('form-received-date').value = row.received_date || '';
// Set type select
const typeSelect = document.getElementById('form-type');
const rowType = String(row.type || '').trim().toLowerCase();
typeSelect.value = '';
let found = false;
for (let option of typeSelect.options) {
if (option.value && String(option.value) === String(row.type)) {
typeSelect.value = option.value;
found = true;
break;
}
}
if (!found && rowType) {
const legacyCategory = getCategoryCode(rowType);
for (let option of typeSelect.options) {
if (option.value) {
const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
if (typeObj) {
const optCategory = getCategoryCode(typeObj.type_name);
if (optCategory === legacyCategory) {
typeSelect.value = option.value;
found = true;
break;
}
}
}
}
}
document.getElementById('form-subject').value = row.subject || '';
document.getElementById('form-amount').value = row.amount || '';
document.getElementById('form-invoice-no').value = row.invoice_no || '';
document.getElementById('form-receiptno').value = row.receiptno || '';
document.getElementById('form-invoice-date').value = row.invoice_date || '';
document.getElementById('form-physical-file-no').value = row.physical_file_no || '';
const tatEl = document.getElementById('form-tat');
if (tatEl) tatEl.value = row.tat || '';
const statusSelect = document.getElementById('form-status');
const rowStatus = String(row.status || '').trim();
statusSelect.value = 'Pending';
for (let option of statusSelect.options) {
if (option.value.toLowerCase() === rowStatus.toLowerCase()) {
statusSelect.value = option.value;
break;
}
}
const followupEl = document.getElementById('form-followup');
if (followupEl) followupEl.value = row.followup || '';
const myfollowupEl = document.getElementById('form-myfollowup');
if (myfollowupEl) myfollowupEl.value = row.myfollowup || '';
const orderbyEl = document.getElementById('form-orderby');
if (orderbyEl) orderbyEl.value = row.order_by || '';
document.getElementById('form-remarks').value = row.remarks || '';
// Set vendor select (using ID or legacy fallback/custom)
const select = document.getElementById('form-vendor-select');
const vendorVal = String(row.vendor_name || '').trim();
select.value = '';
document.getElementById('form-vendor-custom').style.display = 'none';
document.getElementById('form-vendor-custom').required = false;
document.getElementById('form-vendor-custom').value = '';
if (vendorVal) {
let optionFound = false;
for (let option of select.options) {
if (option.value && option.value !== '__custom__' && String(option.value) === vendorVal) {
select.value = option.value;
optionFound = true;
break;
}
}
if (!optionFound) {
const matchedVendor = activeVendors.find(v => String(v.vendor_name).toLowerCase().trim() === vendorVal.toLowerCase());
if (matchedVendor) {
select.value = String(matchedVendor.id);
optionFound = true;
}
}
if (!optionFound) {
select.value = '__custom__';
const customInput = document.getElementById('form-vendor-custom');
customInput.style.display = 'block';
customInput.required = true;
customInput.value = row.vendor_name;
}
}
// Populate/Reset PDF elements
document.getElementById('form-pdf').value = '';
const existingPdfContainer = document.getElementById('form-existing-pdf-container');
const existingPdfLink = document.getElementById('form-existing-pdf-link');
if (row.file_path) {
existingPdfContainer.style.display = 'block';
let pdfUrl = row.file_path;
if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
pdfUrl = contextPath + pdfUrl.substring(1);
} else {
pdfUrl = contextPath + pdfUrl;
}
existingPdfLink.href = pdfUrl;
} else {
existingPdfContainer.style.display = 'none';
}
recordModalObj.show();
}
// SAVE Record API call
async function saveRecord(event) {
event.preventDefault();
showLoader(true, 'Saving record details...');
const id = document.getElementById('form-id').value;
const received_date = document.getElementById('form-received-date').value;
const type = document.getElementById('form-type').value;
const subject = document.getElementById('form-subject').value;
const amount = document.getElementById('form-amount').value;
const invoice_no = document.getElementById('form-invoice-no').value;
const receiptno = document.getElementById('form-receiptno').value;
const invoice_date = document.getElementById('form-invoice-date').value;
const physical_file_no = document.getElementById('form-physical-file-no').value;
const tatEl = document.getElementById('form-tat');
const tat = tatEl ? tatEl.value : '';
const status = document.getElementById('form-status').value;
// Preserve original values if they were loaded in edit mode
let originalRecord = null;
if (id) {
originalRecord = allRecords.find(r => String(r.id) === String(id));
}
const followupEl = document.getElementById('form-followup');
const followup = followupEl ? followupEl.value : (originalRecord ? originalRecord.followup : '');
const myfollowupEl = document.getElementById('form-myfollowup');
const myfollowup = myfollowupEl ? myfollowupEl.value : (originalRecord ? originalRecord.myfollowup : '');
const orderbyEl = document.getElementById('form-orderby');
const order_by = orderbyEl ? orderbyEl.value : (originalRecord ? originalRecord.order_by : '');
const remarks = document.getElementById('form-remarks').value;
// Resolve vendor
const vendorSelect = document.getElementById('form-vendor-select').value;
const vendorName = vendorSelect === '__custom__' ? document.getElementById('form-vendor-custom').value : vendorSelect;
if (!vendorName) {
alert('Please select or specify a vendor.');
showLoader(false);
return;
}
// Upload PDF if selected
let filePath = originalRecord ? originalRecord.file_path : null;
const pdfInput = document.getElementById('form-pdf');
if (pdfInput && pdfInput.files && pdfInput.files.length > 0) {
showLoader(true, 'Uploading PDF document...');
const formData = new FormData();
formData.append('file', pdfInput.files[0]);
const csrfHeaders = getCsrfHeaders();
try {
const uploadResponse = await fetch(contextPath + 'gccservicepaymenttracker/api/upload-pdf', {
method: 'POST',
headers: {
...csrfHeaders
},
body: formData
});
const uploadResult = await uploadResponse.json();
if (uploadResult.success) {
filePath = uploadResult.filePath;
} else {
alert(uploadResult.error || 'Failed to upload PDF file.');
showLoader(false);
return;
}
} catch (err) {
console.error("Upload error:", err);
alert("Failed to upload PDF due to connection error.");
showLoader(false);
return;
}
}
const payload = {
id, received_date, type, vendor_name: vendorName, subject, amount, invoice_no,
receiptno, invoice_date, physical_file_no, tat, status, followup,
myfollowup, order_by, remarks, file_path: filePath
};
const csrfHeaders = getCsrfHeaders();
const headers = {
'Content-Type': 'application/json',
...csrfHeaders
};
try {
showLoader(true, 'Saving record details...');
const response = await fetch(contextPath + 'gccservicepaymenttracker/api/save', {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.success) {
recordModalObj.hide();
loadCounts();
loadList();
} else {
alert(data.error || 'Failed to save record.');
showLoader(false);
}
} catch (err) {
console.error("Save Error:", err);
alert("Failed to save record due to connection error.");
showLoader(false);
}
}
// Delete record API call
function deleteRecord(id) {
if (!confirm('Are you sure you want to delete this payment tracking record?')) {
return;
}
showLoader(true, 'Deleting record...');
const csrfHeaders = getCsrfHeaders();
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
...csrfHeaders
};
const body = new URLSearchParams();
body.append('id', id);
fetch(contextPath + 'gccservicepaymenttracker/api/delete', {
method: 'POST',
headers: headers,
body: body
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadCounts();
loadList();
} else {
alert(data.error || 'Failed to delete record.');
showLoader(false);
}
})
.catch(err => {
console.error("Delete Error:", err);
alert("Failed to delete record due to connection error.");
showLoader(false);
});
}
</script>
</body>
</html>