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.
 
 
 
 

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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 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>