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.
1012 lines
29 KiB
1012 lines
29 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 IT Cell Outsourced Manpower Details</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}" />
|
|
|
|
<link rel="stylesheet" type="text/css" th:href="@{/assets/css/body-form-styles.css}">
|
|
<link rel="stylesheet" type="text/css" th:href="@{/assets/css/home-styles.css}">
|
|
|
|
<th:block th:replace="~{fragments/header :: header}"></th:block>
|
|
|
|
<style>
|
|
.spreadsheet-container {
|
|
background: #ffffff;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
padding: 25px;
|
|
margin: 20px auto;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.sheet-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
border-bottom: 2px solid #eef2f5;
|
|
padding-bottom: 15px;
|
|
}
|
|
|
|
.sheet-title {
|
|
color: #0f5c78;
|
|
font-weight: 800;
|
|
font-size: 1.8rem;
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.sheet-date-badge {
|
|
background: #eef7fa;
|
|
color: #0f5c78;
|
|
font-weight: 700;
|
|
padding: 6px 14px;
|
|
border-radius: 20px;
|
|
font-size: 0.9rem;
|
|
border: 1px solid #d4e7ee;
|
|
}
|
|
|
|
.table-responsive {
|
|
overflow-x: auto;
|
|
border-radius: 8px;
|
|
border: 1px solid #dcdfe3;
|
|
}
|
|
|
|
.sheet-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.92rem;
|
|
text-align: left;
|
|
background: #fff;
|
|
}
|
|
|
|
.sheet-table th {
|
|
background-color: #0f5c78;
|
|
color: #ffffff;
|
|
font-weight: 700;
|
|
padding: 12px 10px;
|
|
border: 1px solid #0b4960;
|
|
text-align: center;
|
|
vertical-align: middle;
|
|
text-transform: uppercase;
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.sheet-table td {
|
|
padding: 8px 10px;
|
|
border: 1px solid #e1e4e8;
|
|
vertical-align: middle;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.sheet-table tbody tr:nth-child(even) {
|
|
background-color: #f9fbfb;
|
|
}
|
|
|
|
.sheet-table tbody tr:hover {
|
|
background-color: #f1f6f8;
|
|
}
|
|
|
|
.col-center {
|
|
text-align: center;
|
|
}
|
|
|
|
.col-right {
|
|
text-align: right;
|
|
}
|
|
|
|
/* Inputs */
|
|
.sheet-input-number {
|
|
width: 75px;
|
|
padding: 5px 8px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
color: #0f5c78;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.sheet-input-number:focus {
|
|
border-color: #1e82a6;
|
|
box-shadow: 0 0 0 3px rgba(30, 130, 166, 0.15);
|
|
outline: none;
|
|
}
|
|
|
|
.sheet-input-text {
|
|
width: 90%;
|
|
min-width: 150px;
|
|
padding: 5px 10px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.sheet-input-text:focus {
|
|
border-color: #1e82a6;
|
|
box-shadow: 0 0 0 3px rgba(30, 130, 166, 0.15);
|
|
outline: none;
|
|
}
|
|
|
|
/* Totals Row */
|
|
.totals-row {
|
|
background-color: #e8eff2 !important;
|
|
font-weight: 800;
|
|
border-top: 2px solid #0f5c78;
|
|
}
|
|
|
|
.totals-row td {
|
|
color: #0f5c78 !important;
|
|
font-size: 0.98rem;
|
|
border-top: 2px solid #0f5c78;
|
|
}
|
|
|
|
/* Action Buttons */
|
|
.action-bar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 20px;
|
|
gap: 15px;
|
|
}
|
|
|
|
.save-btn {
|
|
background-color: #10b981;
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 28px;
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
box-shadow: 0 4px 6px rgba(16, 185, 129, 0.2);
|
|
transition: background-color 0.2s, transform 0.1s;
|
|
}
|
|
|
|
.save-btn:hover {
|
|
background-color: #059669;
|
|
}
|
|
|
|
.save-btn:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
/* Remarks styling */
|
|
.vacant-highlight {
|
|
color: #e74c3c;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.no-vacant {
|
|
color: #27ae60;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Custom Dashboard Tabs */
|
|
.dashboard-tabs {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin-bottom: 25px;
|
|
border-bottom: 2px solid #eef2f5;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.dashboard-tab {
|
|
padding: 10px 22px;
|
|
font-size: 1.05rem;
|
|
font-weight: 700;
|
|
color: #7f8c8d;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 8px 8px 0 0;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.dashboard-tab:hover {
|
|
color: #0f5c78;
|
|
background-color: rgba(15, 92, 120, 0.05);
|
|
}
|
|
|
|
.dashboard-tab.active {
|
|
color: #0f5c78;
|
|
}
|
|
|
|
.dashboard-tab.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -12px;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 4px;
|
|
background-color: #0f5c78;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Vendor Directory Styles */
|
|
.vendor-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.vendor-card {
|
|
background: #ffffff;
|
|
border: 1px solid #eef2f6;
|
|
border-radius: 12px;
|
|
padding: 22px;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.02);
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.vendor-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 4px;
|
|
height: 100%;
|
|
background-color: #0f5c78;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.vendor-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 25px rgba(15, 92, 120, 0.08);
|
|
border-color: #d4e7ee;
|
|
}
|
|
|
|
.vendor-card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: start;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.vendor-card-title {
|
|
font-size: 1.15rem;
|
|
font-weight: 800;
|
|
color: #2c3e50;
|
|
margin: 0;
|
|
text-transform: capitalize;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.vendor-card-desc {
|
|
font-size: 0.9rem;
|
|
color: #7f8c8d;
|
|
line-height: 1.4;
|
|
margin-bottom: 20px;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.vendor-card-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-top: 15px;
|
|
border-top: 1px dashed #eef2f6;
|
|
}
|
|
|
|
.vendor-badge {
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
background-color: #eef7fa;
|
|
color: #0f5c78;
|
|
}
|
|
|
|
/* Employee Count Interactive Badge */
|
|
.emp-count-badge {
|
|
background: linear-gradient(135deg, #0f5c78, #1e82a6);
|
|
color: #ffffff;
|
|
font-weight: 800;
|
|
font-size: 1.05rem;
|
|
padding: 8px 18px;
|
|
border-radius: 30px;
|
|
cursor: pointer;
|
|
border: none;
|
|
box-shadow: 0 4px 10px rgba(30, 130, 166, 0.25);
|
|
transition: all 0.2s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.emp-count-badge:hover {
|
|
transform: scale(1.08);
|
|
box-shadow: 0 6px 15px rgba(30, 130, 166, 0.4);
|
|
background: linear-gradient(135deg, #1e82a6, #249cc8);
|
|
color: #ffffff;
|
|
}
|
|
|
|
.emp-count-badge:active {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
/* Modal styling overrides */
|
|
.modal-custom-header {
|
|
background-color: #0f5c78;
|
|
color: white;
|
|
border-top-left-radius: 14px;
|
|
border-top-right-radius: 14px;
|
|
padding: 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-custom-title {
|
|
font-weight: 800;
|
|
font-size: 1.35rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin: 0;
|
|
}
|
|
|
|
.modal-search-box {
|
|
position: relative;
|
|
margin-bottom: 20px;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.modal-search-box input {
|
|
padding: 10px 15px 10px 40px;
|
|
border: 1px solid #dcdfe3;
|
|
border-radius: 8px;
|
|
width: 100%;
|
|
font-size: 0.95rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.modal-search-box input:focus {
|
|
border-color: #0f5c78;
|
|
box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
|
|
outline: none;
|
|
}
|
|
|
|
.modal-search-box i {
|
|
position: absolute;
|
|
left: 15px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #7f8c8d;
|
|
}
|
|
|
|
.modal-table {
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.modal-table th {
|
|
background-color: #eef7fa !important;
|
|
color: #0f5c78 !important;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
font-size: 0.8rem;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 2px solid #0f5c78 !important;
|
|
}
|
|
|
|
.modal-table td {
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 25px;
|
|
}
|
|
|
|
.export-btn {
|
|
background-color: #27ae60;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
font-size: 0.9rem;
|
|
font-weight: 700;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.export-btn:hover {
|
|
background-color: #219653;
|
|
color: white;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<th:block th:replace="~{fragments/navbar :: navbar(curPage=${curPage}, isAdmin=${isAdmin})}"></th:block>
|
|
|
|
<div class="container" style="max-width: 95%;">
|
|
<div class="spreadsheet-container">
|
|
|
|
<!-- Dashboard Tabs -->
|
|
<div class="dashboard-tabs">
|
|
<button class="dashboard-tab active" id="btn-vendor-tab" onclick="switchTab('vendor-tab')">
|
|
<i class="fa-solid fa-users-gear"></i> Vendor Directory & Counts
|
|
</button>
|
|
<button class="dashboard-tab" id="btn-manpower-tab" onclick="switchTab('manpower-tab')">
|
|
<i class="fa-solid fa-file-spreadsheet"></i> Manpower Statistics
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab 1: Vendor Directory & Counts -->
|
|
<div id="vendor-tab-content" class="tab-content">
|
|
<div class="sheet-header">
|
|
<h2 class="sheet-title">
|
|
<i class="fa-solid fa-cubes text-primary"></i> Vendor Directory & Employees
|
|
</h2>
|
|
<span class="sheet-date-badge">Overall Count Wise Summary</span>
|
|
</div>
|
|
|
|
<div class="vendor-grid">
|
|
<div class="vendor-card" th:each="vendor : ${vendorDetails}">
|
|
<div class="vendor-card-header">
|
|
<h3 class="vendor-card-title" th:text="${vendor.vendor_name}">Vendor Name</h3>
|
|
<span class="vendor-badge" th:text="'Order: ' + ${vendor.orderby}">Order</span>
|
|
</div>
|
|
<p class="vendor-card-desc"
|
|
th:text="${vendor.description != null ? vendor.description : 'No description provided.'}">
|
|
Description</p>
|
|
<div class="vendor-card-footer">
|
|
<span style="font-weight: 600; color: #7f8c8d; font-size: 0.9rem;">
|
|
Employees Count:
|
|
</span>
|
|
<button class="emp-count-badge"
|
|
th:attr="onclick=|fetchAndShowEmployees('${vendor.vendor_name}')|">
|
|
<i class="fa-solid fa-users"></i>
|
|
<span th:text="${vendor.actual_employee_count}">0</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 2: Manpower Statistics (Existing Spreadsheet) -->
|
|
<div id="manpower-tab-content" class="tab-content" style="display: none;">
|
|
<div class="sheet-header">
|
|
<h2 class="sheet-title">
|
|
<i class="fa-solid fa-file-spreadsheet"></i> Manpower Statistics Report
|
|
</h2>
|
|
<span class="sheet-date-badge" id="current-date-badge">As on 21.05.2026</span>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="sheet-table" id="manpower-sheet-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 50px;">S.No</th>
|
|
<th>Description (Designation)</th>
|
|
<th>Vendor Name</th>
|
|
<th style="width: 120px;">Manpower Count<br>as per Tender</th>
|
|
<th style="width: 120px;">Available<br>Manpower Count</th>
|
|
<th>Working Location</th>
|
|
<th style="width: 110px;">Registered<br>in App</th>
|
|
<th>Vacant / Remarks</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr th:each="entry, stat : ${manpowerEntries}">
|
|
<td class="col-center" th:text="${stat.count}">1</td>
|
|
<td th:text="${entry.description}" style="font-weight: 600;">Description</td>
|
|
<td th:text="${entry.vendor_name}">Vendor Name</td>
|
|
|
|
<!-- Read-only Tender Count -->
|
|
<td class="col-center tender-cell" th:text="${entry.tender_count}"
|
|
style="font-weight: 700;">0</td>
|
|
|
|
<!-- Editable Available Count -->
|
|
<td class="col-center">
|
|
<input type="number" min="0" class="sheet-input-number available-input"
|
|
th:value="${entry.available_count}" th:data-id="${entry.id}"
|
|
oninput="recalculateTotals()">
|
|
</td>
|
|
|
|
<td th:text="${entry.working_location}">Location</td>
|
|
<td class="col-center" th:text="${entry.registered_count}">0</td>
|
|
|
|
<!-- Editable Remarks -->
|
|
<td>
|
|
<input type="text" class="sheet-input-text remarks-input"
|
|
th:value="${entry.vacant_remarks}" th:data-id="${entry.id}"
|
|
oninput="recalculateTotals()">
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="totals-row">
|
|
<td colspan="3" style="text-align: right; padding-right: 20px;">Total No</td>
|
|
<td class="col-center" id="total-tender">319</td>
|
|
<td class="col-center" id="total-available">292</td>
|
|
<td></td>
|
|
<td></td>
|
|
<td id="total-vacant" style="font-weight: 800; padding-left: 20px;">30</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="action-bar">
|
|
<button class="save-btn" onclick="saveManpowerData()">
|
|
<i class="fa-solid fa-floppy-disk"></i> Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Employee Details Modal -->
|
|
<div class="modal fade" id="employeeDetailsModal" tabindex="-1" aria-labelledby="employeeDetailsModalLabel"
|
|
aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
|
<div class="modal-content"
|
|
style="border-radius: 14px; border: none; box-shadow: 0 10px 40px rgba(0,0,0,0.2);">
|
|
<div class="modal-custom-header">
|
|
<h5 class="modal-custom-title" id="employeeDetailsModalLabel">
|
|
<i class="fa-solid fa-users-viewfinder"></i> Employee Details - <span id="modal-vendor-name"
|
|
style="text-decoration: underline;">Vendor</span>
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Search and Export Toolbar -->
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-3 mb-4">
|
|
<div class="modal-search-box flex-grow-1 m-0">
|
|
<i class="fa-solid fa-magnifying-glass"></i>
|
|
<input type="text" id="employee-search-input"
|
|
placeholder="Search employees by name, designation, location, code..."
|
|
oninput="filterEmployees()">
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2 w-100 w-md-auto justify-content-md-end">
|
|
<!-- Work Order PDF Link -->
|
|
<a id="modal-workorder-file-link" href="#" target="_blank" class="btn btn-danger text-nowrap align-items-center justify-content-center gap-2" style="height: 44px; border-radius: 8px; font-weight: 600; display: none; padding: 0 15px; background-color: #c0392b; border-color: #c0392b; color: white;">
|
|
<i class="fa-solid fa-file-pdf"></i> Work Order File
|
|
</a>
|
|
|
|
<select id="modal-designation-filter" class="form-select form-select-sm"
|
|
style="height: 44px; border-radius: 8px; border: 1px solid #cbd5e1; padding: 0 35px 0 15px; font-weight: 600; min-width: 200px;"
|
|
onchange="filterEmployees()">
|
|
<option value="">- All Designations -</option>
|
|
</select>
|
|
<button class="export-btn text-nowrap" onclick="exportEmployeesToCSV()">
|
|
<i class="fa-solid fa-file-csv"></i> Export to CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table of Employees -->
|
|
<div class="table-responsive" style="max-height: 450px; border: 1px solid #eef2f6;">
|
|
<table class="table table-hover modal-table" id="modal-employee-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 60px;" class="text-center">S.No</th>
|
|
<th>Emp Code</th>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Mobile</th>
|
|
<th>User Type</th>
|
|
<th>Designation</th>
|
|
<th class="text-center" style="width: 150px;">Last Updated On</th>
|
|
<th>Location</th>
|
|
<th>Vendor Name</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modal-employee-tbody">
|
|
<!-- Dynamically populated -->
|
|
</tbody>
|
|
</table>
|
|
<div id="no-employees-found" class="text-center text-muted py-5" style="display: none;">
|
|
<i class="fa-solid fa-user-slash fs-1 mb-3 text-secondary"></i>
|
|
<p class="fw-bold mb-0">No matching employees found for this vendor.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<th:block th:replace="~{fragments/footer :: footer}"></th:block>
|
|
|
|
<!-- Inline Spreadsheet Script -->
|
|
<script th:inline="javascript">
|
|
const contextPath = /*[[@{/}]]*/ '';
|
|
const vendorDetailsData = /*[[${vendorDetails}]]*/[];
|
|
const allDesignations = /*[[${designations}]]*/[];
|
|
|
|
// Globals to store current modal state
|
|
let currentVendorName = "";
|
|
let currentVendorEmployees = [];
|
|
let filteredVendorEmployees = [];
|
|
|
|
// Handle switching between dynamic dashboard and spreadsheet
|
|
function switchTab(tabId) {
|
|
const tabs = document.querySelectorAll(".dashboard-tab");
|
|
const tabContents = document.querySelectorAll(".tab-content");
|
|
|
|
tabs.forEach(t => t.classList.remove("active"));
|
|
tabContents.forEach(c => c.style.display = "none");
|
|
|
|
if (tabId === 'vendor-tab') {
|
|
document.getElementById("btn-vendor-tab").classList.add("active");
|
|
document.getElementById("vendor-tab-content").style.display = "block";
|
|
} else if (tabId === 'manpower-tab') {
|
|
document.getElementById("btn-manpower-tab").classList.add("active");
|
|
document.getElementById("manpower-tab-content").style.display = "block";
|
|
}
|
|
}
|
|
|
|
// Fetch employee details from API dynamically and render in Modal
|
|
function fetchAndShowEmployees(vendorName, designationFilter = null) {
|
|
currentVendorName = vendorName;
|
|
|
|
let displayTitle = vendorName;
|
|
document.getElementById("modal-vendor-name").textContent = displayTitle;
|
|
|
|
// Populate modal designation dropdown based on selected vendor ID
|
|
const modalDesigFilter = document.getElementById("modal-designation-filter");
|
|
modalDesigFilter.innerHTML = '<option value="">- All Designations -</option>';
|
|
|
|
const vendor = vendorDetailsData.find(v => v.vendor_name === vendorName);
|
|
if (vendor && vendor.designationDetails) {
|
|
vendor.designationDetails.forEach(des => {
|
|
const opt = document.createElement("option");
|
|
opt.value = des.designation;
|
|
|
|
let displayText = des.designation;
|
|
const fileNo = des.File_no || des.file_no;
|
|
if (fileNo && fileNo !== '-' && fileNo !== '') {
|
|
displayText += " - " + fileNo;
|
|
}
|
|
opt.textContent = displayText;
|
|
modalDesigFilter.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
if (designationFilter) {
|
|
modalDesigFilter.value = designationFilter;
|
|
} else {
|
|
modalDesigFilter.value = "";
|
|
}
|
|
|
|
// Show Loading state inside tbody
|
|
const tbody = document.getElementById("modal-employee-tbody");
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="10" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-secondary fw-semibold">Fetching employee records from database...</p>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
document.getElementById("no-employees-found").style.display = "none";
|
|
document.getElementById("employee-search-input").value = "";
|
|
|
|
// Show Modal using Bootstrap
|
|
const modalElement = document.getElementById("employeeDetailsModal");
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
|
modal.show();
|
|
|
|
// Fetch from REST API
|
|
fetch(contextPath + 'api/vendor-employees?vendorName=' + encodeURIComponent(vendorName))
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
currentVendorEmployees = data;
|
|
filterEmployees();
|
|
})
|
|
.catch(err => {
|
|
console.error("Error fetching employees:", err);
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="10" class="text-center py-5 text-danger fw-bold">
|
|
<i class="fa-solid fa-circle-exclamation fs-1 mb-2"></i>
|
|
<p>Failed to connect to server and fetch database records. Please try again.</p>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
// Render the employee list inside the modal table
|
|
function renderEmployeeTable(employees) {
|
|
const tbody = document.getElementById("modal-employee-tbody");
|
|
const noEmployeesEl = document.getElementById("no-employees-found");
|
|
tbody.innerHTML = "";
|
|
|
|
if (employees.length === 0) {
|
|
noEmployeesEl.style.display = "block";
|
|
return;
|
|
}
|
|
|
|
noEmployeesEl.style.display = "none";
|
|
|
|
employees.forEach((emp, index) => {
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td class="text-center fw-bold text-secondary">${index + 1}</td>
|
|
<td class="fw-semibold text-primary">${escapeHtml(emp.emp_code || '-')}</td>
|
|
<td class="fw-bold text-dark">${escapeHtml(emp.name || '-')}</td>
|
|
<td>${escapeHtml(emp.email || '-')}</td>
|
|
<td>${escapeHtml(emp.mobile || '-')}</td>
|
|
<td><span class="badge bg-secondary">${escapeHtml(emp.user_type || '-')}</span></td>
|
|
<td class="fw-semibold">${escapeHtml(emp.designation || '-')}</td>
|
|
<td class="text-center fw-semibold text-primary">${escapeHtml(formatDateTime(emp.updated_at || emp.cdate))}</td>
|
|
<td><span class="badge bg-light text-dark border">${escapeHtml(emp.location || '-')}</span></td>
|
|
<td class="fw-semibold text-secondary">${escapeHtml(emp.vendor_name || '-')}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
// Instant fuzzy client-side search inside the modal
|
|
function filterEmployees() {
|
|
const query = document.getElementById("employee-search-input").value.toLowerCase().trim();
|
|
const selectedDesignation = document.getElementById("modal-designation-filter").value.toLowerCase().trim();
|
|
|
|
let list = currentVendorEmployees;
|
|
|
|
// Apply designation filter
|
|
if (selectedDesignation !== "") {
|
|
list = list.filter(emp => emp.designation && emp.designation.trim().toLowerCase() === selectedDesignation);
|
|
}
|
|
|
|
if (query === "") {
|
|
filteredVendorEmployees = list;
|
|
} else {
|
|
filteredVendorEmployees = list.filter(emp => {
|
|
return (emp.name && emp.name.toLowerCase().includes(query)) ||
|
|
(emp.emp_code && emp.emp_code.toLowerCase().includes(query)) ||
|
|
(emp.designation && emp.designation.toLowerCase().includes(query)) ||
|
|
(emp.location && emp.location.toLowerCase().includes(query)) ||
|
|
(emp.email && emp.email.toLowerCase().includes(query)) ||
|
|
(emp.mobile && emp.mobile.toLowerCase().includes(query)) ||
|
|
(emp.vendor_name && emp.vendor_name.toLowerCase().includes(query)) ||
|
|
(emp.user_type && emp.user_type.toLowerCase().includes(query));
|
|
});
|
|
}
|
|
|
|
renderEmployeeTable(filteredVendorEmployees);
|
|
updateWorkOrderFileLink();
|
|
}
|
|
|
|
function updateWorkOrderFileLink() {
|
|
const selectEl = document.getElementById("modal-designation-filter");
|
|
const fileLink = document.getElementById("modal-workorder-file-link");
|
|
if (!selectEl || !fileLink) return;
|
|
|
|
const selectedVal = selectEl.value.trim().toLowerCase();
|
|
if (selectedVal === "") {
|
|
fileLink.classList.add("d-none");
|
|
fileLink.href = "#";
|
|
return;
|
|
}
|
|
|
|
let filePath = null;
|
|
|
|
if (currentVendorName === "All Active Employees") {
|
|
// Find matching designation in allDesignations
|
|
const matchedDes = allDesignations.find(d => d.designation && d.designation.trim().toLowerCase() === selectedVal);
|
|
filePath = matchedDes ? matchedDes.file_path : null;
|
|
} else {
|
|
// Find matching designation details for current vendor
|
|
const vendor = vendorDetailsData.find(v => v.vendor_name === currentVendorName);
|
|
if (vendor && vendor.designationDetails) {
|
|
const matchedDes = vendor.designationDetails.find(d => d.designation && d.designation.trim().toLowerCase() === selectedVal);
|
|
filePath = matchedDes ? matchedDes.file_path : null;
|
|
}
|
|
}
|
|
|
|
if (filePath) {
|
|
filePath = filePath.trim();
|
|
}
|
|
if (filePath && filePath !== '-' && filePath !== '') {
|
|
if (!filePath.toLowerCase().endsWith('.pdf')) {
|
|
filePath += '.pdf';
|
|
}
|
|
const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
|
fileLink.href = contextPath + normalizedPath;
|
|
fileLink.classList.remove("d-none");
|
|
fileLink.classList.add("d-inline-flex");
|
|
return;
|
|
}
|
|
|
|
fileLink.classList.add("d-none");
|
|
fileLink.href = "#";
|
|
}
|
|
|
|
// Premium CSV Exporter
|
|
function exportEmployeesToCSV() {
|
|
if (filteredVendorEmployees.length === 0) {
|
|
alert("No employee records to export!");
|
|
return;
|
|
}
|
|
|
|
let csvContent = "data:text/csv;charset=utf-8,";
|
|
csvContent += "S.No,Employee Code,Name,Email,Mobile,User Type,Designation,Location,Vendor Name\n";
|
|
|
|
filteredVendorEmployees.forEach((emp, index) => {
|
|
const row = [
|
|
index + 1,
|
|
`"${(emp.emp_code || '-').replace(/"/g, '""')}"`,
|
|
`"${(emp.name || '-').replace(/"/g, '""')}"`,
|
|
`"${(emp.email || '-').replace(/"/g, '""')}"`,
|
|
`"${(emp.mobile || '-').replace(/"/g, '""')}"`,
|
|
`"${(emp.user_type || '-').replace(/"/g, '""')}"`,
|
|
`"${(emp.designation || '-').replace(/"/g, '""')}"`,
|
|
`"${(emp.location || '-').replace(/"/g, '""')}"`,
|
|
`"${(emp.vendor_name || '-').replace(/"/g, '""')}"`
|
|
];
|
|
csvContent += row.join(",") + "\n";
|
|
});
|
|
|
|
const encodedUri = encodeURI(csvContent);
|
|
const link = document.createElement("a");
|
|
link.setAttribute("href", encodedUri);
|
|
|
|
// Filename format: [vendor_name]_employees.csv
|
|
const sanitizedVendorName = currentVendorName.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
link.setAttribute("download", `${sanitizedVendorName}_employees.csv`);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
// Helper function to safely escape HTML to prevent XSS
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
// Helper function to format timestamp to dd/mm/yyyy HH:MM:ss
|
|
function formatDateTime(dateTimeStr) {
|
|
if (!dateTimeStr || dateTimeStr === '-' || dateTimeStr.trim() === '') return '-';
|
|
dateTimeStr = dateTimeStr.trim();
|
|
let dateObj = new Date(dateTimeStr.replace('T', ' '));
|
|
if (isNaN(dateObj.getTime())) {
|
|
dateObj = new Date(dateTimeStr);
|
|
}
|
|
if (isNaN(dateObj.getTime())) return dateTimeStr;
|
|
const day = String(dateObj.getDate()).padStart(2, '0');
|
|
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
|
const year = dateObj.getFullYear();
|
|
const hours = String(dateObj.getHours()).padStart(2, '0');
|
|
const minutes = String(dateObj.getMinutes()).padStart(2, '0');
|
|
const seconds = String(dateObj.getSeconds()).padStart(2, '0');
|
|
return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
|
|
}
|
|
|
|
// Dynamic calculation when page loads
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Update the date badge dynamically to current date format
|
|
const d = new Date();
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
const year = d.getFullYear();
|
|
document.getElementById("current-date-badge").textContent = `As on ${day}.${month}.${year}`;
|
|
|
|
recalculateTotals();
|
|
});
|
|
|
|
|
|
// Recalculates Tender, Available, and Vacant counts dynamically in the footer
|
|
function recalculateTotals() {
|
|
const table = document.getElementById("manpower-sheet-table");
|
|
const rows = table.querySelectorAll("tbody tr");
|
|
|
|
let tenderSum = 0;
|
|
let availableSum = 0;
|
|
let vacantSum = 0;
|
|
|
|
rows.forEach(row => {
|
|
// Tender sum
|
|
const tenderVal = parseInt(row.querySelector(".tender-cell").textContent) || 0;
|
|
tenderSum += tenderVal;
|
|
|
|
// Available sum
|
|
const availableInput = row.querySelector(".available-input");
|
|
const availableVal = parseInt(availableInput.value) || 0;
|
|
availableSum += availableVal;
|
|
|
|
// Vacant Sum parsing: Extract leading digit or number from vacant input
|
|
const remarksInput = row.querySelector(".remarks-input");
|
|
const remarksVal = remarksInput.value.trim();
|
|
|
|
// Extract the first number found in the remarks input
|
|
const numMatch = remarksVal.match(/^-?\d+/);
|
|
if (numMatch) {
|
|
vacantSum += parseInt(numMatch[0]);
|
|
}
|
|
});
|
|
|
|
document.getElementById("total-tender").textContent = tenderSum;
|
|
document.getElementById("total-available").textContent = availableSum;
|
|
document.getElementById("total-vacant").textContent = vacantSum;
|
|
}
|
|
|
|
// Save dynamic spreadsheet updates back to database
|
|
function saveManpowerData() {
|
|
const table = document.getElementById("manpower-sheet-table");
|
|
const rows = table.querySelectorAll("tbody tr");
|
|
const payload = [];
|
|
|
|
rows.forEach(row => {
|
|
const availableInput = row.querySelector(".available-input");
|
|
const remarksInput = row.querySelector(".remarks-input");
|
|
|
|
payload.push({
|
|
id: parseInt(availableInput.dataset.id),
|
|
availableCount: parseInt(availableInput.value) || 0,
|
|
vacantRemarks: remarksInput.value
|
|
});
|
|
});
|
|
|
|
// CSRF tokens
|
|
const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
|
|
const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json'
|
|
};
|
|
|
|
if (csrfToken && csrfHeader) {
|
|
headers[csrfHeader] = csrfToken;
|
|
}
|
|
|
|
// Save to API
|
|
fetch(contextPath + 'api/save-manpower-stats', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert("Changes saved successfully to MySQL database!");
|
|
window.location.reload();
|
|
} else {
|
|
alert("Failed to save changes: " + (data.error || "Unknown error"));
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error("Error saving data:", err);
|
|
alert("Something went wrong. Failed to communicate with server.");
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|