|
|
<!doctype html><html lang="en">
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Attendance Dashboard</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<style> .spinner-container { display: flex; justify-content: center; align-items: center; min-height: 200px; }
.spinner-border { width: 3rem; height: 3rem; color: #007bff; }
.zone-summary-row { display: grid; grid-template-columns: 1fr; gap: 20px; }
@media (max-width: 768px) { .zone-summary-row { grid-template-columns: 1fr; } }
.zone-summary-card { border: 1px solid #ddd; border-radius: 4px; padding: 15px; background-color: #f9f9f9; }
.disabled-count { color: #6c757d; cursor: default; text-decoration: none; display: inline-block; padding: 0 4px; } </style></head>
<body class="p-3"> <div class="container"> <h1 class="mb-4">Attendance Dashboard</h1>
<div class="row mb-3"> <div class="col-md-3"> <label class="form-label">From</label> <input id="fromDate" type="date" class="form-control" /> </div> <div class="col-md-3"> <label class="form-label">To</label> <input id="toDate" type="date" class="form-control" /> </div> <div class="col-md-6 d-flex align-items-end"> <div> <button id="loadSummaryBtn" class="btn btn-secondary">Load</button> </div> </div> </div>
<h4>Abstract Counts</h4> <div id="abstractCounts" class="mb-4"></div>
<h4>Zone Wise Attendance %</h4> <div id="zoneWisePercentage" class="mb-4"></div>
<h4>Zone Summary</h4> <div class="zone-summary-row"> <div class="zone-summary-card"> <div class="d-flex justify-content-between align-items-center mb-2"> <h5>Park</h5> <button class="btn btn-sm btn-primary" onclick="downloadExcelFromTable(document.getElementById('zoneSummaryPark'), 'park_summary.xlsx')"> <i class="bi bi-download"></i> Download Excel </button> </div> <div id="zoneSummaryPark"></div> </div> <!-- NULM zone summary card commented out
<div class="zone-summary-card"> <div class="d-flex justify-content-between align-items-center mb-2"> <h5>NULM</h5> <button class="btn btn-sm btn-primary" onclick="downloadExcelFromTable(document.getElementById('zoneSummaryNulm'), 'nulm_summary.xlsx')"> <i class="bi bi-download"></i> Download Excel </button> </div> <div id="zoneSummaryNulm"></div> </div> --> </div> </div>
<script>
function showImageModal(imageSrc) { document.getElementById("previewImage").src = imageSrc; var modal = new bootstrap.Modal(document.getElementById('imagePreviewModal')); modal.show(); }
(function () { const apiBase = '/commissionerreview/api/attendanceController';
function pad(n) { return n < 10 ? '0' + n : n; }
// convert Date -> yyyy-mm-dd for input[type=date] function toIsoYyyyMmDd(date) { return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); }
// convert input value (either yyyy-mm-dd or dd-mm-yyyy) -> dd-mm-yyyy for API function toApiDdMmYyyy(val) { if (!val) return val; // if already in dd-mm-yyyy (contains two '-') and first part length 2 const parts = val.split('-'); if (parts.length === 3) { if (parts[0].length === 4) { // yyyy-mm-dd -> dd-mm-yyyy return pad(parseInt(parts[2], 10)) + '-' + pad(parseInt(parts[1], 10)) + '-' + parts[0]; } else { // assume already dd-mm-yyyy return val; } } return val; }
function setDefaults() { const today = new Date(); const iso = toIsoYyyyMmDd(today); document.getElementById('fromDate').value = iso; document.getElementById('toDate').value = iso; }
function setDateLimits() { const from = document.getElementById('fromDate'); const to = document.getElementById('toDate'); const todayIso = toIsoYyyyMmDd(new Date()); from.max = todayIso; to.max = todayIso; if (from.value) to.min = from.value;
from.addEventListener('change', () => { const fv = from.value; if (fv) { to.min = fv; if (to.value && to.value < fv) to.value = fv; } else { to.min = ''; } });
to.addEventListener('change', () => { if (from.value && to.value && to.value < from.value) { to.value = from.value; } }); }
async function loadAbstractCounts() { const target = document.getElementById('abstractCounts'); target.innerHTML = createLoader(); try { const res = await fetch(apiBase + '/abstractCounts'); const data = await res.json(); if (!Array.isArray(data)) { target.innerText = JSON.stringify(data); return; } let html = '<div class="row">'; data.forEach(item => { const facialRegistered = item.facialRegistered || 0; const facialNotRegistered = item.facialNotRegistered || 0; const totalCount = item.totalCount || 0; const facialRegPct = totalCount > 0 ? ((facialRegistered / totalCount) * 100).toFixed(2) : '0.00'; html += `<div class="col-md-3"><div class="card p-3 mb-2"> <strong>${item.emp_type}</strong> <div class="mt-2"> <div class="small">Total: <strong>${totalCount}</strong></div> <div class="small">Facial Registered: <strong>${facialRegistered}</strong></div> <div class="small">Facial Not Registered: <strong>${facialNotRegistered}</strong></div> <div class="small">Facial Registered %: <strong>${facialRegPct}%</strong></div> <div class="small">Present %: <strong><span id="presentPercent">—</span></strong></div> </div> </div></div>`; }); html += '</div>'; target.innerHTML = html; } catch (e) { target.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; } }
async function loadZoneWisePercentage() { const target = document.getElementById('zoneWisePercentage'); target.innerHTML = createLoader(); try { const rawFrom = document.getElementById('fromDate').value; const rawTo = document.getElementById('toDate').value; const fromDate = toApiDdMmYyyy(rawFrom); const toDate = toApiDdMmYyyy(rawTo); const res = await fetch(`${apiBase}/zoneWisePercentage?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`); const data = await res.json(); if (!Array.isArray(data) || data.length === 0) { target.innerHTML = '<div class="alert alert-info">No zone percentage data available</div>'; return; } let html = '<div class="row">'; data.forEach(item => { const zoneName = item.ZoneName || 'Unknown Zone'; const facialPct = item.facialPercentage != null ? item.facialPercentage : 0; const presentPct = item.presentPercentage != null ? item.presentPercentage : 0; html += `<div class="col-md-4"><div class="card p-3 mb-2 text-center"> <strong>${zoneName}</strong> <div class="row mt-2"> <div class="col-6"> <div style="font-size: 1.3rem; font-weight: bold;"> ${facialPct}% </div> <div class="small text-muted">Facial Registered %</div> </div> <div class="col-6"> <div style="font-size: 1.3rem; font-weight: bold;"> ${presentPct}% </div> <div class="small text-muted">Present %</div> </div> </div> </div></div>`; }); html += '</div>'; target.innerHTML = html; } catch (e) { target.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; } }
function createLoader() { return '<div class="spinner-container"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>'; }
function downloadExcelFromTable(tableElement, fileName) { if (typeof XLSX === 'undefined') { alert('Excel library is not loaded. Please refresh the page and try again.'); console.error('XLSX library failed to load from CDN'); return; } if (!tableElement) { alert('Table not found'); return; } const table = tableElement.querySelector('table'); if (!table) { alert('No table to download'); return; } const workbook = XLSX.utils.table_to_book(table); XLSX.writeFile(workbook, fileName); }
// Expose to global scope for onclick handlers window.downloadExcelFromTable = downloadExcelFromTable;
function renderTable(target, data, empType) { if (!Array.isArray(data)) { target.innerText = JSON.stringify(data); return; } if (data.length === 0) { target.innerHTML = '<div class="alert alert-info">No records</div>'; return; } const rows = data.slice().sort((a, b) => String(a.zone || '').localeCompare(String(b.zone || ''), undefined, { numeric: true })); // let html = '<table class="table table-sm table-striped"><thead><tr><th>Zone</th><th class="text-center">Total</th><th class="text-center">Facial Registered</th><th class="text-center">Facial Not Registered</th><th class="text-center">Supervisor</th><th class="text-center">Present</th><th class="text-center">OD</th><th class="text-center">Leave</th><th class="text-center">Not Marked</th></tr></thead><tbody>'; let html = '<table class="table table-sm table-striped"><thead><tr><th>Zone</th><th class="text-center">Total</th><th class="text-center">Facial Registered</th><th class="text-center">Facial Not Registered</th><th class="text-center">Present</th><th class="text-center">OD</th><th class="text-center">Leave</th><th class="text-center">Not Marked</th></tr></thead><tbody>'; rows.forEach(r => { const zone = r.zone; const total = Number(r.TotalCount) || 0; const facialRegistered = Number(r.facialRegistered) || 0; const facialNotRegistered = Number(r.facialNotRegistered) || 0; const present = Number(r.AttendanceMarked) || 0; const od = Number(r.ODCount) || 0; const leave = Number(r.LeaveCount) || 0; const notMarked = Math.max(0, total - present - od - leave - facialNotRegistered); // const supervisor = Number(r.SupervisorCount) || 0;
html += `<tr><td>${zone || '-'}</td>`; html += `<td class="text-center">` + (total > 0 ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="total">${total}</a>` : `<span class="disabled-count">${total}</span>`) + `</td>`; html += `<td class="text-center">${facialRegistered}</td>`; html += `<td class="text-center">` + (facialNotRegistered > 0 ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="notregistered">${facialNotRegistered}</a>` : `<span class="disabled-count">${facialNotRegistered}</span>`) + `</td>`; // html += `<td class="text-center">` + (supervisor > 0 // ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="supervisor">${supervisor}</a>` // : `<span class="disabled-count">${supervisor}</span>`) + `</td>`; html += `<td class="text-center">` + (present > 0 ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="present">${present}</a>` : `<span class="disabled-count">${present}</span>`) + `</td>`; html += `<td class="text-center">` + (od > 0 ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="od">${od}</a>` : `<span class="disabled-count">${od}</span>`) + `</td>`; html += `<td class="text-center">` + (leave > 0 ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="leave">${leave}</a>` : `<span class="disabled-count">${leave}</span>`) + `</td>`; html += `<td class="text-center">` + (notMarked > 0 ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="notmarked">${notMarked}</a>` : `<span class="disabled-count">${notMarked}</span>`) + `</td>`; html += '</tr>'; });
// Compute grand totals let grandTotal = 0, grandSupervisor = 0, grandFacialReg = 0, grandFacialNotReg = 0, grandPresent = 0, grandOd = 0, grandLeave = 0, grandNotMarked = 0; rows.forEach(r => { const total = Number(r.TotalCount) || 0; const facialRegistered = Number(r.facialRegistered) || 0; const facialNotRegistered = Number(r.facialNotRegistered) || 0; const present = Number(r.AttendanceMarked) || 0; const od = Number(r.ODCount) || 0; const leave = Number(r.LeaveCount) || 0; grandTotal += total; // grandSupervisor += (Number(r.SupervisorCount) || 0); grandFacialReg += facialRegistered; grandFacialNotReg += facialNotRegistered; grandPresent += present; grandOd += od; grandLeave += leave; grandNotMarked += Math.max(0, total - present - od - leave - facialNotRegistered); }); html += '</tbody><tfoot><tr style="font-weight:bold; background-color:#e9ecef;">'; html += '<td>Total</td>'; html += `<td class="text-center">${grandTotal}</td>`; html += `<td class="text-center">${grandFacialReg}</td>`; html += `<td class="text-center">${grandFacialNotReg}</td>`; // html += `<td class="text-center">${grandSupervisor}</td>`; html += `<td class="text-center">${grandPresent}</td>`; html += `<td class="text-center">${grandOd}</td>`; html += `<td class="text-center">${grandLeave}</td>`; html += `<td class="text-center">${grandNotMarked}</td>`; html += '</tr></tfoot></table>'; target.innerHTML = html; }
async function loadZoneSummary() { const rawFrom = document.getElementById('fromDate').value; const rawTo = document.getElementById('toDate').value; const fromDate = toApiDdMmYyyy(rawFrom); const toDate = toApiDdMmYyyy(rawTo); const parkTarget = document.getElementById('zoneSummaryPark'); // const nulmTarget = document.getElementById('zoneSummaryNulm'); // NULM commented out
// Show loaders parkTarget.innerHTML = createLoader(); // nulmTarget.innerHTML = createLoader(); // NULM commented out
try { // Load Park independently fetch(`${apiBase}/zoneSummaryPark?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`) .then(res => { if (!res.ok) { console.error('Park zoneSummary error', res.status); parkTarget.innerHTML = `<div class="alert alert-danger">Error loading Park summary (${res.status})</div>`; return Promise.reject(new Error('Park fetch failed')); } return res.json(); }) .then(parkData => { console.log('Park data loaded:', parkData); renderTable(parkTarget, parkData, 'Park'); // Compute Present % (Present + OD) / Total if (Array.isArray(parkData) && parkData.length > 0) { let facialRegisteredAll = 0, presentOdAll = 0; parkData.forEach(r => { facialRegisteredAll += (Number(r.facialRegistered) || 0); presentOdAll += (Number(r.AttendanceMarked) || 0) + (Number(r.ODCount) || 0); }); const pct = facialRegisteredAll > 0 ? ((presentOdAll / facialRegisteredAll) * 100).toFixed(1) : '0.0'; const el = document.getElementById('presentPercent'); if (el) el.textContent = pct + '%'; } }) .catch(e => { console.error('Park error:', e); parkTarget.innerHTML = `<div class="alert alert-danger">Error: ${e.message}</div>`; });
// NULM loading commented out // fetch(`${apiBase}/zoneSummaryNULM?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`) // .then(res => { // if (!res.ok) { // console.error('NULM zoneSummary error', res.status); // nulmTarget.innerHTML = `<div class="alert alert-danger">Error loading NULM summary (${res.status})</div>`; // return Promise.reject(new Error('NULM fetch failed')); // } // return res.json(); // }) // .then(nulmData => { // console.log('NULM data loaded:', nulmData); // renderTable(nulmTarget, nulmData, 'NULM'); // }) // .catch(e => { // console.error('NULM error:', e); // nulmTarget.innerHTML = `<div class="alert alert-danger">Error: ${e.message}</div>`; // }); } catch (e) { parkTarget.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; // nulmTarget.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; // NULM commented out } }
async function loadParkWiseSummary(zone, category) { if (!zone) { alert('Please select a zone first'); return; } const modalEl = document.getElementById('parkWiseSummaryModal'); const modalBody = document.getElementById('parkWiseSummaryModalBody'); const modalTitle = document.getElementById('parkWiseSummaryModalLabel'); modalTitle.innerText = `Park-wise Summary — Zone ${zone} — ${(category || 'All').toUpperCase()}`; modalBody.innerHTML = createLoader(); new bootstrap.Modal(modalEl).show(); try { const rawFrom = document.getElementById('fromDate').value; const rawTo = document.getElementById('toDate').value; const fromDate = toApiDdMmYyyy(rawFrom); const toDate = toApiDdMmYyyy(rawTo); const url = `${apiBase}/parkWiseSummary?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}&zone=${encodeURIComponent(zone)}`; const res = await fetch(url); if (!res.ok) { modalBody.innerHTML = `<div class="alert alert-danger">Error loading park-wise summary (${res.status})</div>`; return; } const data = await res.json(); if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; } if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No park-wise records</div>'; return; }
// Filter parks: when a category is selected, only show parks with count > 0 for that category const catLower = (category || '').toLowerCase(); const displayData = !catLower ? data : data.filter(r => { const total = Number(r.TotalCount) || 0; const present = Number(r.AttendanceMarked) || 0; const od = Number(r.ODCount) || 0; const leave = Number(r.LeaveCount) || 0; const facialNotRegistered = Number(r.facialNotRegistered) || 0; const notMarked = Math.max(0, total - present - od - leave - facialNotRegistered); if (catLower === 'total') return total > 0; if (catLower === 'present') return present > 0; if (catLower === 'od') return od > 0; if (catLower === 'leave') return leave > 0; if (catLower === 'notmarked') return notMarked > 0; if (catLower === 'notregistered') return facialNotRegistered > 0; return true; });
if (displayData.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No parks with ' + (category || '').toUpperCase() + ' count</div>'; return; }
const showAll = !catLower;
let html = '<div class="table-responsive"><table class="table table-sm table-striped"><thead><tr>'; html += '<th style="display:none;">Park ID</th><th>Park Name</th><th>Supervisor Name</th><th>Supervisor Mobile</th>'; if (showAll) { html += '<th class="text-center">Total</th><th class="text-center">Facial Registered</th><th class="text-center">Facial Not Registered</th><th class="text-center">Present</th><th class="text-center">OD</th><th class="text-center">Leave</th><th class="text-center">Not Marked</th>'; } else { const categoryLabel = { present: 'Present', od: 'OD', leave: 'Leave', notmarked: 'Not Marked', notregistered: 'Facial Not Registered' }; html += `<th class="text-center">${categoryLabel[catLower] || category}</th>`; } html += '</tr></thead><tbody>'; displayData.forEach(r => { const parkId = r.park_id || ''; const park = r.park_name || ''; const parkDisplay = park === '' ? (parkId || 'Unknown') : park; const supervisorName = r.supervisor_name || '-'; const supervisorMobile = r.supervisor_mobile || '-'; const total = Number(r.TotalCount) || 0; const facialRegistered = Number(r.facialRegistered) || 0; const facialNotRegistered = Number(r.facialNotRegistered) || 0; const present = Number(r.AttendanceMarked) || 0; const od = Number(r.ODCount) || 0; const leave = Number(r.LeaveCount) || 0; const notMarked = Math.max(0, total - present - od - leave - facialNotRegistered); html += '<tr>'; html += `<td style="display:none;">${parkId}</td>`; html += `<td>${parkDisplay}</td>`; html += `<td>${supervisorName}</td>`; html += `<td>${supervisorMobile}</td>`; if (showAll) { html += `<td class="text-center">${total}</td>`; html += `<td class="text-center">${facialRegistered}</td>`; html += `<td class="text-center">` + (facialNotRegistered > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" data-parkname="${parkDisplay}" data-category="notregistered">${facialNotRegistered}</a>` : `<span class="disabled-count">${facialNotRegistered}</span>`) + `</td>`; html += `<td class="text-center">` + (present > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" data-parkname="${parkDisplay}" data-category="present">${present}</a>` : `<span class="disabled-count">${present}</span>`) + `</td>`; html += `<td class="text-center">` + (od > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" data-parkname="${parkDisplay}" data-category="od">${od}</a>` : `<span class="disabled-count">${od}</span>`) + `</td>`; html += `<td class="text-center">` + (leave > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" data-parkname="${parkDisplay}" data-category="leave">${leave}</a>` : `<span class="disabled-count">${leave}</span>`) + `</td>`; html += `<td class="text-center">` + (notMarked > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" data-parkname="${parkDisplay}" data-category="notmarked">${notMarked}</a>` : `<span class="disabled-count">${notMarked}</span>`) + `</td>`; } else { // Show only the selected category column let catValue = 0; if (catLower === 'present') catValue = present; else if (catLower === 'od') catValue = od; else if (catLower === 'leave') catValue = leave; else if (catLower === 'notmarked') catValue = notMarked; else if (catLower === 'notregistered') catValue = facialNotRegistered; html += `<td class="text-center">` + (catValue > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" data-parkname="${parkDisplay}" data-category="${catLower}">${catValue}</a>` : `<span class="disabled-count">${catValue}</span>`) + `</td>`; } html += '</tr>'; });
// Compute grand totals from ALL data to match zone summary (no count mismatch) let grandTotal = 0, grandFacialReg = 0, grandFacialNotReg = 0, grandPresent = 0, grandOd = 0, grandLeave = 0, grandNotMarked = 0; data.forEach(r => { const total = Number(r.TotalCount) || 0; const facialRegistered = Number(r.facialRegistered) || 0; const facialNotRegistered = Number(r.facialNotRegistered) || 0; const present = Number(r.AttendanceMarked) || 0; const od = Number(r.ODCount) || 0; const leave = Number(r.LeaveCount) || 0; grandTotal += total; grandFacialReg += facialRegistered; grandFacialNotReg += facialNotRegistered; grandPresent += present; grandOd += od; grandLeave += leave; grandNotMarked += Math.max(0, total - present - od - leave - facialNotRegistered); }); html += '</tbody><tfoot><tr style="font-weight:bold; background-color:#e9ecef;">'; html += '<td>Total</td>'; html += '<td></td>'; html += '<td></td>'; if (showAll) { html += `<td class="text-center">${grandTotal}</td>`; html += `<td class="text-center">${grandFacialReg}</td>`; html += `<td class="text-center">${grandFacialNotReg}</td>`; html += `<td class="text-center">${grandPresent}</td>`; html += `<td class="text-center">${grandOd}</td>`; html += `<td class="text-center">${grandLeave}</td>`; html += `<td class="text-center">${grandNotMarked}</td>`; } else { let grandCatValue = 0; if (catLower === 'present') grandCatValue = grandPresent; else if (catLower === 'od') grandCatValue = grandOd; else if (catLower === 'leave') grandCatValue = grandLeave; else if (catLower === 'notmarked') grandCatValue = grandNotMarked; else if (catLower === 'notregistered') grandCatValue = grandFacialNotReg; html += `<td class="text-center">${grandCatValue}</td>`; } html += '</tr></tfoot></table></div>'; modalBody.innerHTML = html; } catch (e) { modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; } }
async function loadParkDetails(zone, type) { const modalEl = document.getElementById('detailModal'); const modalBody = document.getElementById('detailModalBody'); const modalTitle = document.getElementById('detailModalLabel'); modalTitle.innerText = `Parks — Zone ${zone} — ${type}`; modalBody.innerHTML = createLoader(); new bootstrap.Modal(modalEl).show(); try { const url = `/commissionerreview/api/parks/getParkDataByZone?zone=${encodeURIComponent(zone)}&type=${encodeURIComponent(type)}`; const res = await fetch(url); if (!res.ok) { modalBody.innerHTML = `<div class="alert alert-danger">Error ${res.status}</div>`; return; } const data = await res.json(); if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; } if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No records</div>'; return; }
// Build table from keys of first row const cols = Object.keys(data[0]); let html = '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr><th>S.No</th>'; cols.forEach(c => html += `<th>${c.replace(/_/g, ' ')}</th>`); html += '</tr></thead><tbody>'; data.forEach((row, idx) => { html += '<tr>'; html += `<td>${idx + 1}</td>`; cols.forEach(c => html += `<td>${row[c] != null ? row[c] : '-'}</td>`); html += '</tr>'; }); html += '</tbody></table></div>'; modalBody.innerHTML = html; } catch (e) { modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; } }
// loadDetailed may be called with explicit empType/category/zone (when clicking a zone count) async function loadDetailed(empType, category, zoneVal, parkId, parkName) { // show details in modal const rawFrom = document.getElementById('fromDate').value; const rawTo = document.getElementById('toDate').value; const fromDate = toApiDdMmYyyy(rawFrom); const toDate = toApiDdMmYyyy(rawTo); const modalEl = document.getElementById('detailModal'); const modalBody = document.getElementById('detailModalBody'); const modalTitle = document.getElementById('detailModalLabel'); if (!empType || !category || !zoneVal) { modalTitle.innerText = 'Details'; modalBody.innerHTML = '<div class="text-warning">Missing parameters. Click a count to view details.</div>'; new bootstrap.Modal(modalEl).show(); return; }
let titleSuffix = parkName ? ` — ${parkName} - ${parkId}` : ''; modalTitle.innerText = `${empType} — Zone ${zoneVal}${titleSuffix} — ${category.toUpperCase()} (${rawFrom} → ${rawTo})`; modalBody.innerHTML = createLoader(); // Show modal immediately with loader new bootstrap.Modal(modalEl).show();
try { let url = `${apiBase}/detailed?empType=${encodeURIComponent(empType)}&category=${encodeURIComponent(category)}&fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`; url += `&zone=${encodeURIComponent(zoneVal)}`; if (parkId) { url += `&parkId=${encodeURIComponent(parkId)}`; } const res = await fetch(url); if (!res.ok) { const text = await res.text().catch(() => ''); modalBody.innerHTML = `<div class="alert alert-danger">Error ${res.status}: ${text}</div>`; return; } const data = await res.json(); if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; } if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No records</div>'; return; }
// Define columns to display based on category let displayColumns = []; const catLower = (category || 'present').toLowerCase();
if (catLower === 'total') { displayColumns = ['park_name', 'supervisor_name', 'supervisor_mobile', 'enrollment_id', 'name', 'emp_mobile']; } else if (catLower === 'present') { displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location']; if (empType === 'Park') displayColumns.push('park_name'); displayColumns.push('intime', 'inphoto', 'outtime', 'outphoto'); } else if (catLower === 'leave') { displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location']; if (empType === 'Park') displayColumns.push('park_name'); displayColumns.push('leavedatetime', 'leaveby'); } else if (catLower === 'od') { displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location']; if (empType === 'Park') displayColumns.push('park_name'); displayColumns.push('odtime', 'odby'); } else if (catLower === 'notmarked') { displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'image']; // if (empType === 'Park') displayColumns.push('park_name'); } else if (catLower === 'notregistered') { displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno']; // if (empType === 'Park') displayColumns.push('park_name'); } else if (catLower === 'supervisor') { displayColumns = ['supervisor_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno']; } else { displayColumns = Object.keys(data[0]); }
// Filter data to include only displayColumns, handle case-insensitive matching const filteredData = data.map(row => { const filtered = {}; displayColumns.forEach(col => { // Try exact match first, then case-insensitive const key = Object.keys(row).find(k => k === col || k.toLowerCase() === col.toLowerCase()); if (key) { filtered[col] = row[key]; } }); return filtered; });
function prettyName(key) { const map = { enrollment_id: 'Enrollment ID', name: 'Name', designation: 'Designation', department: 'Department', park_name: 'Park Name', intime: 'In Time', inphoto: 'In Photo', outtime: 'Out Time', outphoto: 'Out Photo', leavedatetime: 'Leave DateTime', leaveby: 'Leave By', odtime: 'OD DateTime', odby: 'OD By', incharge_name: 'Incharge Name', incharge_phoneno: 'Incharge Contact', appversion: 'App Version', apptype: 'App Type', location: 'Location', image: 'Image', supervisor_id: 'Supervisor ID', supervisor_name: 'Supervisor Name', supervisor_mobile: 'Supervisor Mobile', emp_mobile: 'Employee Mobile', }; if (map[key]) return map[key]; return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); }
let html = '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr>'; // add serial number header html += '<th>S.No</th>'; displayColumns.forEach(k => { html += `<th>${prettyName(k)}</th>`; }); html += '</tr></thead><tbody>'; filteredData.forEach((row, idx) => { html += '<tr>'; // serial number cell html += `<td>${idx + 1}</td>`; displayColumns.forEach(k => { const val = row[k]; // Handle photo columns - show as thumbnail if it's an image // if ((k === 'inphoto' || k === 'outphoto') && val) { // html += `<td><img src="${val}" alt="photo" style="max-width: 100px; max-height: 100px; cursor: pointer;" onclick="this.style.maxWidth='500px'; this.style.maxHeight='500px';"></td>`; // } else { // html += `<td>${val != null ? val : '-'}</td>`; // }
if ((k === 'inphoto' || k === 'outphoto' || k === 'image') && val) { html += `<td> <img src="${val}" alt="photo" style="max-width: 100px; max-height: 100px; cursor: pointer;" onclick="showImageModal('${val}')"> </td>`; } else { html += `<td>${val != null ? val : '-'}</td>`; } }); html += '</tr>'; }); html += '</tbody></table></div>'; modalBody.innerHTML = html; } catch (e) { modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; } }
document.addEventListener('DOMContentLoaded', () => { setDefaults(); document.getElementById('loadSummaryBtn').addEventListener('click', () => { const rawFrom = document.getElementById('fromDate').value; const rawTo = document.getElementById('toDate').value; if (rawFrom && rawTo && rawTo < rawFrom) { alert('To date must be the same as or later than From date'); return; } loadAbstractCounts(); loadZoneWisePercentage(); loadZoneSummary(); }); // delegate clicks: zone links -> park-wise summary, zone counts -> detailed, park counts -> park-details document.addEventListener('click', (ev) => { const zoneLink = ev.target.closest && ev.target.closest('.zone-link'); if (zoneLink) { ev.preventDefault(); const zone = zoneLink.getAttribute('data-zone'); loadParkWiseSummary(zone); return; }
const zoneAnchor = ev.target.closest && ev.target.closest('.zone-count'); if (zoneAnchor) { ev.preventDefault(); const zone = zoneAnchor.getAttribute('data-zone'); const empType = zoneAnchor.getAttribute('data-emptype'); const category = zoneAnchor.getAttribute('data-category'); // Supervisor & Total: show employee details directly (no park-wise drill-down) if (category === 'supervisor' || category === 'total') { loadDetailed(empType, category, zone); } else if (empType === 'Park') { loadParkWiseSummary(zone, category); } // NULM branch commented out // else { // loadDetailed(empType, category, zone); // } return; }
const parkAnchor = ev.target.closest && ev.target.closest('.park-count'); if (parkAnchor) { ev.preventDefault(); const zone = parkAnchor.getAttribute('data-zone'); const type = parkAnchor.getAttribute('data-type'); loadParkDetails(zone, type); return; }
const parkEmployeeAnchor = ev.target.closest && ev.target.closest('.park-employee-count'); if (parkEmployeeAnchor) { ev.preventDefault(); const zone = parkEmployeeAnchor.getAttribute('data-zone'); const parkId = parkEmployeeAnchor.getAttribute('data-parkid'); const parkName = parkEmployeeAnchor.getAttribute('data-parkname'); const category = parkEmployeeAnchor.getAttribute('data-category'); loadDetailed('Park', category, zone, parkId, parkName); return; } }); // set date constraints and load initial setDateLimits(); // load initial loadAbstractCounts(); loadZoneWisePercentage(); loadZoneSummary(); });
})(); </script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <!-- Park-wise Summary Modal --> <div class="modal fade" id="parkWiseSummaryModal" tabindex="-1" aria-labelledby="parkWiseSummaryModalLabel" aria-hidden="true"> <!-- <div class="modal-dialog modal-lg modal-dialog-scrollable"> --> <div class="modal-dialog modal-xl modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="parkWiseSummaryModalLabel">Park-wise Summary</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body" id="parkWiseSummaryModalBody"> Loading... </div> <div class="modal-footer"> <button type="button" class="btn btn-primary" onclick="downloadExcelFromTable(document.getElementById('parkWiseSummaryModal'), 'park_wise_summary.xlsx')"> <i class="bi bi-download"></i> Download Excel </button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> </div> </div> </div> </div>
<!-- Detail modal --> <div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true"> <div class="modal-dialog modal-xl modal-dialog-scrollable"> <!-- <div class="modal-dialog modal-fullscreen modal-dialog-scrollable"> --> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="detailModalLabel">Details</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body" id="detailModalBody"> Loading... </div> <div class="modal-footer"> <button type="button" class="btn btn-primary" onclick="downloadExcelFromTable(document.getElementById('detailModal'), 'employee_details.xlsx')"> <i class="bi bi-download"></i> Download Excel </button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> </div> </div> </div> </div>
<!-- Image Preview Modal --> <div class="modal fade" id="imagePreviewModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">Photo Preview</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body text-center"> <img id="previewImage" src="" class="img-fluid" style="max-height: 500px;"> </div> </div> </div> </div></body>
</html>
|