@ -1,5 +1,6 @@
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
@ -12,27 +13,32 @@
align-items: center;
min-height: 200px;
}
.spinner-border {
width: 3rem;
height: 3rem;
color: #007bff;
}
.zone-summary-row {
display: grid;
grid-template-columns: 1fr 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;
@ -42,6 +48,7 @@
}
< / style >
< / head >
< body class = "p-3" >
< div class = "container" >
< h1 class = "mb-4" > Attendance Dashboard< / h1 >
@ -171,7 +178,7 @@
const od = Number(r.ODCount) || 0;
const leave = Number(r.LeaveCount) || 0;
html += `< tr > < td > ${zone}< / td > `;
html += `< tr > < td > ${zone || '-' }< / td > `;
html += `< td > ${total}< / td > `;
html += `< td > ` + (present > 0
? `< a href = "#" class = "zone-count" data-zone = "${zone}" data-emptype = "${empType}" data-category = "present" > ${present}< / a > `
@ -244,8 +251,94 @@
}
}
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; }
let html = '< div class = "table-responsive" > < table class = "table table-sm table-striped" > < thead > < tr > ';
html += '< th > Park Name< / th > < th > Total< / th > < th > Present< / th > < th > OD< / th > < th > Leave< / th > ';
html += '< / tr > < / thead > < tbody > ';
data.forEach(r => {
const park = r.park_name || '-';
const total = Number(r.TotalCount) || 0;
const present = Number(r.AttendanceMarked) || 0;
const od = Number(r.ODCount) || 0;
const leave = Number(r.LeaveCount) || 0;
html += '< tr > ';
html += `< td > ${park}< / td > `;
html += `< td > ${total}< / td > `;
html += `< td > ` + (present > 0 ? `< a href = "#" class = "park-employee-count" data-zone = "${zone}" data-park = "${park}" data-category = "present" > ${present}< / a > ` : `< span class = "disabled-count" > ${present}< / span > `) + `< / td > `;
html += `< td > ` + (od > 0 ? `< a href = "#" class = "park-employee-count" data-zone = "${zone}" data-park = "${park}" data-category = "od" > ${od}< / a > ` : `< span class = "disabled-count" > ${od}< / span > `) + `< / td > `;
html += `< td > ` + (leave > 0 ? `< a href = "#" class = "park-employee-count" data-zone = "${zone}" data-park = "${park}" data-category = "leave" > ${leave}< / a > ` : `< span class = "disabled-count" > ${leave}< / span > `) + `< / td > `;
html += '< / tr > ';
});
html += '< / tbody > < / 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) {
async function loadDetailed(empType, category, zoneVal, parkName ) {
// show details in modal
const rawFrom = document.getElementById('fromDate').value;
const rawTo = document.getElementById('toDate').value;
@ -261,7 +354,8 @@
return;
}
modalTitle.innerText = `${empType} — Zone ${zoneVal} — ${category.toUpperCase()} (${rawFrom} → ${rawTo})`;
let titleSuffix = parkName ? ` — Park ${parkName}` : '';
modalTitle.innerText = `${empType} — Zone ${zoneVal}${titleSuffix} — ${category.toUpperCase()} (${rawFrom} → ${rawTo})`;
modalBody.innerHTML = createLoader();
// Show modal immediately with loader
new bootstrap.Modal(modalEl).show();
@ -269,6 +363,9 @@
try {
let url = `${apiBase}/detailed?empType=${encodeURIComponent(empType)}&category=${encodeURIComponent(category)}&fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`;
url += `&zone=${encodeURIComponent(zoneVal)}`;
if (parkName) {
url += `&parkName=${encodeURIComponent(parkName)}`;
}
const res = await fetch(url);
if (!res.ok) {
const text = await res.text().catch(() => '');
@ -372,16 +469,49 @@
}
loadAbstractCounts(); loadZoneSummary();
});
// delegate clicks on zone counts to load detailed view
// delegate clicks: zone links -> park-wise summary, zone counts -> detailed, park counts -> park-details
document.addEventListener('click', (ev) => {
const a = ev.target.closest & & ev.target.closest('.zone-count ');
if (!a) return;
const zoneLink = ev.target.closest & & ev.target.closest('.zone-link ');
if (zoneLink) {
ev.preventDefault();
const zone = a.getAttribute('data-zone');
const empType = a.getAttribute('data-emptype');
const category = a.getAttribute('data-category');
// call detailed loader with explicit params
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');
// For Park: show park-wise summary; For NULM: show employee details directly
if (empType === 'Park') {
loadParkWiseSummary(zone, category);
} 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 park = parkEmployeeAnchor.getAttribute('data-park');
const category = parkEmployeeAnchor.getAttribute('data-category');
loadDetailed('Park', category, zone, park);
return;
}
});
// set date constraints and load initial
setDateLimits();
@ -393,6 +523,24 @@
}) ();
< / 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-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-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" >
@ -411,4 +559,5 @@
< / div >
< / div >
< / body >
< / html >