Browse Source

Attendance Dashboard - Facial Registered and Not Registerd count added.

master
kanimozhi 8 months ago
parent
commit
6bb4fd6cc5
  1. 3
      src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java
  2. 66
      src/main/java/com/gcc/commissionerreview/service/AttendanceService.java
  3. 493
      src/main/resources/templates/modules/attendance-dashboard.html

3
src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java

@ -32,7 +32,6 @@ public class AttendanceController {
System.out.println("Zone Summary Result for "+empType+ ":"+ result); System.out.println("Zone Summary Result for "+empType+ ":"+ result);
return result; return result;
// return attendanceService.getZoneSummary(empType, fromDate, toDate);
} }
@GetMapping("/zoneSummaryPark") @GetMapping("/zoneSummaryPark")
@ -75,8 +74,6 @@ public class AttendanceController {
System.out.println("Detailed Result: " + result); System.out.println("Detailed Result: " + result);
return result; return result;
// return attendanceService.getDetailedList(empType, category, fromDate, toDate, zone, parkName);
} }
} }

66
src/main/java/com/gcc/commissionerreview/service/AttendanceService.java

@ -21,15 +21,19 @@ public class AttendanceService {
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
@Autowired @Autowired
public void setFeedbackDataSource(@Qualifier("mysqlNULMDataSource") DataSource dataSource) {
public void setDataSource(@Qualifier("mysqlNULMDataSource") DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource); this.jdbcTemplate = new JdbcTemplate(dataSource);
} }
private static final DateTimeFormatter INPUT_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy", Locale.ENGLISH); private static final DateTimeFormatter INPUT_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy", Locale.ENGLISH);
public List<Map<String, Object>> getAbstractCounts() { public List<Map<String, Object>> getAbstractCounts() {
String sql = "SELECT emp_type, count(enrollment_id) as totalCount FROM enrollment_table "
+ "where isactive =1 and isdelete = 0 and zone != 0 group by emp_type";
String sql = "SELECT emp_type, COUNT(enrollment_id) AS totalCount, "
+ "SUM(CASE WHEN facial_attendance = 1 THEN 1 ELSE 0 END) AS facialRegistered, "
+ "SUM(CASE WHEN facial_attendance = 0 THEN 1 ELSE 0 END) AS facialNotRegistered "
+ "FROM enrollment_table "
+ "WHERE isactive = 1 AND isdelete = 0 AND zone != 0 "
+ "GROUP BY emp_type";
return jdbcTemplate.queryForList(sql); return jdbcTemplate.queryForList(sql);
} }
@ -55,16 +59,29 @@ public class AttendanceService {
System.out.println("EmpType: " + empType + ", From: " + fromDateTime + ", To: " + toDateTime); System.out.println("EmpType: " + empType + ", From: " + fromDateTime + ", To: " + toDateTime);
String sql = "SELECT et.zone, count(distinct et.enrollment_id) as TotalCount, "
+ "count(distinct case when att.indatetime >= ? and att.indatetime <= ? then att.enrollment_id end) as AttendanceMarked, "
+ "count(distinct case when att.oddatetime >= ? and att.oddatetime <= ? then att.enrollment_id end) as ODCount, "
+ "count(distinct case when att.leavedatetime >= ? and att.leavedatetime <= ? then att.enrollment_id end) as LeaveCount "
String sql = "WITH facial_data AS ( "
+ "SELECT zone, "
+ "SUM(CASE WHEN facial_attendance = 1 THEN 1 ELSE 0 END) AS facialRegistered, "
+ "SUM(CASE WHEN facial_attendance = 0 THEN 1 ELSE 0 END) AS facialNotRegistered "
+ "FROM enrollment_table "
+ "WHERE isactive = 1 AND isdelete = 0 AND emp_type = ? AND zone != 0 "
+ "GROUP BY zone "
+ ") "
+ "SELECT et.zone, "
+ "COUNT(DISTINCT et.enrollment_id) AS TotalCount, "
+ "COALESCE(fd.facialRegistered, 0) AS facialRegistered, "
+ "COALESCE(fd.facialNotRegistered, 0) AS facialNotRegistered, "
+ "COUNT(DISTINCT CASE WHEN att.indatetime >= ? AND att.indatetime <= ? THEN att.enrollment_id END) AS AttendanceMarked, "
+ "COUNT(DISTINCT CASE WHEN att.oddatetime >= ? AND att.oddatetime <= ? THEN att.enrollment_id END) AS ODCount, "
+ "COUNT(DISTINCT CASE WHEN att.leavedatetime >= ? AND att.leavedatetime <= ? THEN att.enrollment_id END) AS LeaveCount "
+ "FROM enrollment_table et " + "FROM enrollment_table et "
+ "left join attendance att on et.enrollment_id = att.enrollment_id "
+ "where et.isactive =1 and et.isdelete = 0 and et.emp_type = ? and et.zone !=0 "
+ "group by et.zone order by et.zone";
+ "LEFT JOIN attendance att ON et.enrollment_id = att.enrollment_id "
+ "LEFT JOIN facial_data fd ON et.zone = fd.zone "
+ "WHERE et.isactive = 1 AND et.isdelete = 0 AND et.emp_type = ? AND et.zone != 0 "
+ "GROUP BY et.zone, fd.facialRegistered, fd.facialNotRegistered "
+ "ORDER BY et.zone";
return jdbcTemplate.queryForList(sql, new Object[] { fromDateTime, toDateTime, fromDateTime, toDateTime, fromDateTime, toDateTime, empType });
return jdbcTemplate.queryForList(sql, new Object[] { empType, fromDateTime, toDateTime, fromDateTime, toDateTime, fromDateTime, toDateTime, empType });
} }
public List<Map<String, Object>> getDetailedList(String empType, String category, String fromDateStr, String toDateStr, Integer zone, String parkName) { public List<Map<String, Object>> getDetailedList(String empType, String category, String fromDateStr, String toDateStr, Integer zone, String parkName) {
@ -155,16 +172,27 @@ public class AttendanceService {
System.out.println("Zone: " + zone + ", From: " + fromDateTime + ", To: " + toDateTime); System.out.println("Zone: " + zone + ", From: " + fromDateTime + ", To: " + toDateTime);
String sql = "SELECT et.park_name, count(distinct et.enrollment_id) as TotalCount, "
+ "count(distinct case when att.indatetime >= ? and att.indatetime <= ? then att.enrollment_id end) as AttendanceMarked, "
+ "count(distinct case when att.oddatetime >= ? and att.oddatetime <= ? then att.enrollment_id end) as ODCount, "
+ "count(distinct case when att.leavedatetime >= ? and att.leavedatetime <= ? then att.enrollment_id end) as LeaveCount "
String sql = "WITH facial_data AS ( "
+ "SELECT park_name, "
+ "SUM(CASE WHEN facial_attendance = 1 THEN 1 ELSE 0 END) AS facialRegistered, "
+ "SUM(CASE WHEN facial_attendance = 0 THEN 1 ELSE 0 END) AS facialNotRegistered "
+ "FROM enrollment_table "
+ "WHERE isactive = 1 AND isdelete = 0 AND emp_type = 'Park' AND zone = ? AND park_name != '' "
+ "GROUP BY park_name "
+ ") "
+ "SELECT et.park_name, COUNT(DISTINCT et.enrollment_id) AS TotalCount, "
+ "COALESCE(fd.facialRegistered, 0) AS facialRegistered, COALESCE(fd.facialNotRegistered, 0) AS facialNotRegistered, "
+ "COUNT(DISTINCT CASE WHEN att.indatetime >= ? AND att.indatetime <= ? THEN att.enrollment_id END) AS AttendanceMarked, "
+ "COUNT(DISTINCT CASE WHEN att.oddatetime >= ? AND att.oddatetime <= ? THEN att.enrollment_id END) AS ODCount, "
+ "COUNT(DISTINCT CASE WHEN att.leavedatetime >= ? AND att.leavedatetime <= ? THEN att.enrollment_id END) AS LeaveCount "
+ "FROM enrollment_table et " + "FROM enrollment_table et "
+ "left join attendance att on et.enrollment_id = att.enrollment_id "
+ "where et.isactive =1 and et.isdelete = 0 and et.emp_type = 'Park' and et.zone !=0 and et.zone = ? and et.park_name != '' "
+ "group by et.park_name order by et.park_name";
+ "LEFT JOIN attendance att ON et.enrollment_id = att.enrollment_id "
+ "LEFT JOIN facial_data fd ON et.park_name = fd.park_name "
+ "WHERE et.isactive = 1 AND et.isdelete = 0 AND et.emp_type = 'Park' AND et.zone != 0 AND et.zone = ? AND et.park_name != '' "
+ "GROUP BY et.park_name, fd.facialRegistered, fd.facialNotRegistered "
+ "ORDER BY et.park_name";
return jdbcTemplate.queryForList(sql, new Object[] { fromDateTime, toDateTime, fromDateTime, toDateTime, fromDateTime, toDateTime, zone });
return jdbcTemplate.queryForList(sql, new Object[] { zone, fromDateTime, toDateTime, fromDateTime, toDateTime, fromDateTime, toDateTime, zone });
} }
} }

493
src/main/resources/templates/modules/attendance-dashboard.html

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Attendance Dashboard</title> <title>Attendance Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <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>
<style> <style>
.spinner-container { .spinner-container {
display: flex; display: flex;
@ -75,11 +76,23 @@
<h4>Zone Summary</h4> <h4>Zone Summary</h4>
<div class="zone-summary-row"> <div class="zone-summary-row">
<div class="zone-summary-card"> <div class="zone-summary-card">
<h5>Park</h5>
<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 id="zoneSummaryPark"></div>
</div> </div>
<div class="zone-summary-card"> <div class="zone-summary-card">
<h5>NULM</h5>
<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 id="zoneSummaryNulm"></div>
</div> </div>
</div> </div>
@ -155,7 +168,16 @@
if (!Array.isArray(data)) { target.innerText = JSON.stringify(data); return; } if (!Array.isArray(data)) { target.innerText = JSON.stringify(data); return; }
let html = '<div class="row">'; let html = '<div class="row">';
data.forEach(item => { data.forEach(item => {
html += `<div class="col-md-3"><div class="card p-2 mb-2"><strong>${item.emp_type}</strong><div>${item.totalCount}</div></div></div>`;
const facialRegistered = item.facialRegistered || 0;
const facialNotRegistered = item.facialNotRegistered || 0;
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>${item.totalCount}</strong></div>
<div class="small">Facial Registered: <strong>${facialRegistered}</strong></div>
<div class="small">Facial Not Registered: <strong>${facialNotRegistered}</strong></div>
</div>
</div></div>`;
}); });
html += '</div>'; html += '</div>';
target.innerHTML = html; target.innerHTML = html;
@ -166,27 +188,53 @@
return '<div class="spinner-container"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>'; 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) { function renderTable(target, data, empType) {
if (!Array.isArray(data)) { target.innerText = JSON.stringify(data); return; } 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; } 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 })); 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>Total</th><th>Present</th><th>OD</th><th>Leave</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></tr></thead><tbody>';
rows.forEach(r => { rows.forEach(r => {
const zone = r.zone; const zone = r.zone;
const total = Number(r.TotalCount) || 0; 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 present = Number(r.AttendanceMarked) || 0;
const od = Number(r.ODCount) || 0; const od = Number(r.ODCount) || 0;
const leave = Number(r.LeaveCount) || 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
html += `<td class="text-center">${total}</td>`;
html += `<td class="text-center">${facialRegistered}</td>`;
html += `<td class="text-center">${facialNotRegistered}</td>`;
html += `<td class="text-center">` + (present > 0
? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="present">${present}</a>` ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="present">${present}</a>`
: `<span class="disabled-count">${present}</span>`) + `</td>`; : `<span class="disabled-count">${present}</span>`) + `</td>`;
html += `<td>` + (od > 0
html += `<td class="text-center">` + (od > 0
? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="od">${od}</a>` ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="od">${od}</a>`
: `<span class="disabled-count">${od}</span>`) + `</td>`; : `<span class="disabled-count">${od}</span>`) + `</td>`;
html += `<td>` + (leave > 0
html += `<td class="text-center">` + (leave > 0
? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="leave">${leave}</a>` ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="leave">${leave}</a>`
: `<span class="disabled-count">${leave}</span>`) + `</td>`; : `<span class="disabled-count">${leave}</span>`) + `</td>`;
html += '</tr>'; html += '</tr>';
@ -278,20 +326,24 @@
if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No park-wise records</div>'; 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>'; 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 += '<th>Park Name</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>';
html += '</tr></thead><tbody>'; html += '</tr></thead><tbody>';
data.forEach(r => { data.forEach(r => {
const park = r.park_name || '-'; const park = r.park_name || '-';
const total = Number(r.TotalCount) || 0; 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 present = Number(r.AttendanceMarked) || 0;
const od = Number(r.ODCount) || 0; const od = Number(r.ODCount) || 0;
const leave = Number(r.LeaveCount) || 0; const leave = Number(r.LeaveCount) || 0;
html += '<tr>'; html += '<tr>';
html += `<td>${park}</td>`; 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 += `<td class="text-center">${total}</td>`;
html += `<td class="text-center">${facialRegistered}</td>`;
html += `<td class="text-center">${facialNotRegistered}</td>`;
html += `<td class="text-center">` + (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 class="text-center">` + (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 class="text-center">` + (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 += '</tr>';
}); });
html += '</tbody></table></div>'; html += '</tbody></table></div>';
@ -301,230 +353,231 @@
} }
} }
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, 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>';
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(); new bootstrap.Modal(modalEl).show();
return;
}
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; }
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();
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)}`;
// 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>`;
} }
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>`;
}
// loadDetailed may be called with explicit empType/category/zone (when clicking a zone count)
async function loadDetailed(empType, category, zoneVal, 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; 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 === 'present') {
displayColumns = ['enrollment_id', 'name', 'designation', 'department'];
if (empType === 'Park') displayColumns.push('park_name');
displayColumns.push('intime', 'inphoto', 'outtime', 'outphoto');
} else if (catLower === 'leave') {
displayColumns = ['enrollment_id', 'name', 'designation', 'department'];
if (empType === 'Park') displayColumns.push('park_name');
displayColumns.push('leavedatetime', 'leaveby');
} else if (catLower === 'od') {
displayColumns = ['enrollment_id', 'name', 'designation', 'department'];
if (empType === 'Park') displayColumns.push('park_name');
displayColumns.push('odtime', 'odby');
} 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];
}
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();
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(() => '');
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 === 'present') {
displayColumns = ['enrollment_id', 'name', 'designation', 'department'];
if (empType === 'Park') displayColumns.push('park_name');
displayColumns.push('intime', 'inphoto', 'outtime', 'outphoto');
} else if (catLower === 'leave') {
displayColumns = ['enrollment_id', 'name', 'designation', 'department'];
if (empType === 'Park') displayColumns.push('park_name');
displayColumns.push('leavedatetime', 'leaveby');
} else if (catLower === 'od') {
displayColumns = ['enrollment_id', 'name', 'designation', 'department'];
if (empType === 'Park') displayColumns.push('park_name');
displayColumns.push('odtime', 'odby');
} 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;
}); });
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',
};
if (map[key]) return map[key];
return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
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',
};
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>`;
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 => { 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>`;
}
html += `<th>${prettyName(k)}</th>`;
}); });
html += '</tr>';
});
html += '</tbody></table></div>';
modalBody.innerHTML = html;
} catch (e) {
modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
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>`;
}
});
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(); 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;
}
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(); 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');
// For Park: show park-wise summary; For NULM: show employee details directly
if (empType === 'Park') {
loadParkWiseSummary(zone, category);
} else {
loadDetailed(empType, category, zone);
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;
} }
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 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;
}
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();
// load initial
loadAbstractCounts();
loadZoneSummary();
}); });
// set date constraints and load initial
setDateLimits();
// load initial
loadAbstractCounts();
loadZoneSummary();
});
}) ();
})();
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Park-wise Summary Modal --> <!-- Park-wise Summary Modal -->
<div class="modal fade" id="parkWiseSummaryModal" tabindex="-1" aria-labelledby="parkWiseSummaryModalLabel" aria-hidden="true">
<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-lg modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -535,6 +588,10 @@
Loading... Loading...
</div> </div>
<div class="modal-footer"> <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> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
@ -553,6 +610,10 @@
Loading... Loading...
</div> </div>
<div class="modal-footer"> <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> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>

Loading…
Cancel
Save