Browse Source

Attendance Dashboard - Park wise summary added

master
kanimozhi 8 months ago
parent
commit
780731a031
  1. 17
      src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java
  2. 46
      src/main/java/com/gcc/commissionerreview/service/AttendanceService.java
  3. 173
      src/main/resources/templates/modules/attendance-dashboard.html

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

@ -53,19 +53,30 @@ public class AttendanceController {
return result;
}
@GetMapping("/parkWiseSummary")
public List<Map<String, Object>> getParkWiseSummary(
@RequestParam(name = "fromDate", required = false) String fromDate,
@RequestParam(name = "toDate", required = false) String toDate,
@RequestParam(name = "zone", required = false) Integer zone) {
List<Map<String, Object>> result = attendanceService.getParkWiseSummary(fromDate, toDate, zone);
System.out.println("Park Wise Summary Result: " + result);
return result;
}
@GetMapping("/detailed")
public List<Map<String, Object>> getDetailed(@RequestParam(name = "empType") String empType,
@RequestParam(name = "category") String category,
@RequestParam(name = "fromDate", required = false) String fromDate,
@RequestParam(name = "toDate", required = false) String toDate,
@RequestParam(name = "zone", required = false) Integer zone) {
@RequestParam(name = "zone", required = false) Integer zone,
@RequestParam(name = "parkName", required = false) String parkName) {
List<Map<String, Object>> result = attendanceService.getDetailedList(empType, category, fromDate, toDate, zone);
List<Map<String, Object>> result = attendanceService.getDetailedList(empType, category, fromDate, toDate, zone, parkName);
System.out.println("Detailed Result: " + result);
return result;
// return attendanceService.getDetailedList(empType, category, fromDate, toDate, zone);
// return attendanceService.getDetailedList(empType, category, fromDate, toDate, zone, parkName);
}
}

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

@ -50,7 +50,6 @@ public class AttendanceService {
toDate = LocalDate.parse(toDateStr, INPUT_FORMAT);
}
// Convert to datetime range (start of fromDate to end of toDate)
String fromDateTime = fromDate + " 00:00:00";
String toDateTime = toDate + " 23:59:59";
@ -68,7 +67,7 @@ public class AttendanceService {
return jdbcTemplate.queryForList(sql, new Object[] { fromDateTime, toDateTime, fromDateTime, toDateTime, fromDateTime, toDateTime, empType });
}
public List<Map<String, Object>> getDetailedList(String empType, String category, String fromDateStr, String toDateStr, Integer zone) {
public List<Map<String, Object>> getDetailedList(String empType, String category, String fromDateStr, String toDateStr, Integer zone, String parkName) {
LocalDate fromDate;
LocalDate toDate;
@ -84,11 +83,9 @@ public class AttendanceService {
toDate = LocalDate.parse(toDateStr, INPUT_FORMAT);
}
// Convert to datetime range
String fromDateTime = fromDate + " 00:00:00";
String toDateTime = toDate + " 23:59:59";
// determine which datetime column to filter/order by
String dateColumn;
String filterDateColumn;
switch (category == null ? "present" : category.toLowerCase()) {
@ -105,7 +102,6 @@ public class AttendanceService {
filterDateColumn = "att.indatetime";
}
// Base query - simplified and optimized
String sql = "SELECT et.enrollment_id, et.name, et.designation, et.department, et.incharge_name, et.incharge_phoneno, et.emp_id, et.park_name, "
+ "DATE_FORMAT(att.indatetime, '%d-%m-%Y %h:%i %p') as intime, att.inphoto as inphoto, att.inby, "
+ "DATE_FORMAT(att.outdatetime, '%d-%m-%Y %h:%i %p') as outtime, att.outphoto as outphoto, att.outby, "
@ -127,10 +123,48 @@ public class AttendanceService {
params.add(zone);
}
if (parkName != null && !parkName.isEmpty()) {
sql += " and et.park_name = ? ";
params.add(parkName);
}
sql += " order by " + dateColumn + " desc ";
System.out.println("EmpType: " + empType + ", Category: " + category + ", From: " + fromDateTime + ", To: " + toDateTime + ", Zone: " + zone);
System.out.println("EmpType: " + empType + ", Category: " + category + ", From: " + fromDateTime + ", To: " + toDateTime + ", Zone: " + zone + ", ParkName: " + parkName);
return jdbcTemplate.queryForList(sql, params.toArray());
}
public List<Map<String, Object>> getParkWiseSummary(String fromDateStr, String toDateStr, Integer zone) {
LocalDate fromDate;
LocalDate toDate;
if (fromDateStr == null || fromDateStr.trim().isEmpty()) {
fromDate = LocalDate.now();
} else {
fromDate = LocalDate.parse(fromDateStr, INPUT_FORMAT);
}
if (toDateStr == null || toDateStr.trim().isEmpty()) {
toDate = LocalDate.now();
} else {
toDate = LocalDate.parse(toDateStr, INPUT_FORMAT);
}
String fromDateTime = fromDate + " 00:00:00";
String toDateTime = toDate + " 23:59:59";
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 "
+ "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";
return jdbcTemplate.queryForList(sql, new Object[] { fromDateTime, toDateTime, fromDateTime, toDateTime, fromDateTime, toDateTime, zone });
}
}

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

@ -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>
@ -162,7 +169,7 @@
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}));
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>';
rows.forEach(r => {
const zone = r.zone;
@ -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();
@ -390,9 +520,27 @@
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-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>
Loading…
Cancel
Save