diff --git a/src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java b/src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java index 36a7550..6fbb396 100644 --- a/src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java +++ b/src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java @@ -23,6 +23,15 @@ public class AttendanceController { return attendanceService.getAbstractCounts(); } + @GetMapping("/zoneWisePercentage") + public List> getZoneWisePercentage( + @RequestParam(name = "fromDate", required = false) String fromDate, + @RequestParam(name = "toDate", required = false) String toDate) { + List> result = attendanceService.getZoneWisePercentage(fromDate, toDate); + System.out.println("Zone Wise Percentage Result: " + result); + return result; + } + @GetMapping("/zoneSummary") public List> getZoneSummary(@RequestParam(name = "empType") String empType, @RequestParam(name = "fromDate", required = false) String fromDate, diff --git a/src/main/java/com/gcc/commissionerreview/service/AttendanceService.java b/src/main/java/com/gcc/commissionerreview/service/AttendanceService.java index 70183fc..b3da1a8 100644 --- a/src/main/java/com/gcc/commissionerreview/service/AttendanceService.java +++ b/src/main/java/com/gcc/commissionerreview/service/AttendanceService.java @@ -38,6 +38,59 @@ public class AttendanceService { return jdbcTemplate.queryForList(sql); } + public List> getZoneWisePercentage(String fromDateStr, String toDateStr) { + 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 fromDateFormatted = fromDate.toString(); + String toDateFormatted = toDate.toString(); + + String sql = "WITH count_data AS ( " + + "SELECT pd.region_id, " + + "COUNT(et.enrollment_id) AS total_count, " + + "SUM(CASE WHEN et.facial_attendance = 1 THEN 1 ELSE 0 END) AS facial_registered_count, " + + "SUM(CASE WHEN att.indatetime IS NOT NULL OR att.oddatetime IS NOT NULL THEN 1 ELSE 0 END) AS present_od_count " + + "FROM enrollment_table et " + + "LEFT JOIN attendance att ON et.enrollment_id = att.enrollment_id " + + "AND (DATE(att.indatetime) BETWEEN ? AND ? " + + "OR DATE(att.oddatetime) BETWEEN ? AND ?) " + + "LEFT JOIN park_details pd ON et.park_id = pd.park_id " + + "WHERE et.isactive = 1 AND et.emp_type = 'Park' AND pd.park_id != '' " + + "GROUP BY pd.region_id " + + ") " + + "SELECT cd.region_id, " + + "CASE WHEN cd.region_id = 1 THEN 'North Zone' " + + "WHEN cd.region_id = 2 THEN 'Central Zone' " + + "WHEN cd.region_id = 3 THEN 'South Zone' END AS ZoneName, " + + "cd.total_count AS totalCount, " + + "cd.facial_registered_count AS facialRegisteredCount, " + + "cd.present_od_count AS presentOdCount, " + + "CASE WHEN cd.total_count > 0 " + + "THEN ROUND((CAST(cd.facial_registered_count AS DECIMAL) / cd.total_count) * 100, 2) " + + "ELSE 0 END AS facialPercentage, " + + "CASE WHEN cd.facial_registered_count > 0 " + + "THEN ROUND((CAST(cd.present_od_count AS DECIMAL) / cd.facial_registered_count) * 100, 2) " + + "ELSE 0 END AS presentPercentage " + + "FROM count_data cd " + + "ORDER BY cd.region_id"; + + return jdbcTemplate.queryForList(sql, new Object[] { + fromDateFormatted, toDateFormatted, + fromDateFormatted, toDateFormatted }); + } + public List> getZoneSummary(String empType, String fromDateStr, String toDateStr) { LocalDate fromDate; LocalDate toDate; diff --git a/src/main/resources/templates/modules/attendance-dashboard.html b/src/main/resources/templates/modules/attendance-dashboard.html index 47dc0cf..338a156 100644 --- a/src/main/resources/templates/modules/attendance-dashboard.html +++ b/src/main/resources/templates/modules/attendance-dashboard.html @@ -75,6 +75,9 @@

Abstract Counts

+

Zone Wise Attendance %

+
+

Zone Summary

@@ -180,12 +183,15 @@ 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 += `
${item.emp_type}
-
Total: ${item.totalCount}
+
Total: ${totalCount}
Facial Registered: ${facialRegistered}
Facial Not Registered: ${facialNotRegistered}
+
Facial Registered %: ${facialRegPct}%
Present %: —
`; @@ -195,6 +201,50 @@ } catch (e) { target.innerHTML = `
Error: ${e}
`; } } + 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 = '
No zone percentage data available
'; + return; + } + let html = '
'; + 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 += `
+ ${zoneName} +
+
+
+ ${facialPct}% +
+
Facial Registered %
+
+
+
+ ${presentPct}% +
+
Present %
+
+
+
`; + }); + html += '
'; + target.innerHTML = html; + } catch (e) { + target.innerHTML = `
Error: ${e}
`; + } + } + function createLoader() { return '
Loading...
'; } @@ -226,7 +276,7 @@ if (data.length === 0) { target.innerHTML = '
No records
'; return; } const rows = data.slice().sort((a, b) => String(a.zone || '').localeCompare(String(b.zone || ''), undefined, { numeric: true })); // let html = ''; - let html = '
ZoneTotalFacial RegisteredFacial Not RegisteredSupervisorPresentODLeaveNot Marked
'; + let html = '
ZoneTotalFacial RegisteredFacial Not RegisteredPresentODLeaveNot Marked
'; rows.forEach(r => { const zone = r.zone; const total = Number(r.TotalCount) || 0; @@ -411,11 +461,11 @@ html += ``; html += ``; // html += ``; - html += ``; - html += ``; - html += ``; - html += ``; - html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; html += ''; }); @@ -491,7 +541,7 @@ } // loadDetailed may be called with explicit empType/category/zone (when clicking a zone count) - async function loadDetailed(empType, category, zoneVal, parkName) { + async function loadDetailed(empType, category, zoneVal, parkId, parkName) { // show details in modal const rawFrom = document.getElementById('fromDate').value; const rawTo = document.getElementById('toDate').value; @@ -507,7 +557,7 @@ return; } - let titleSuffix = parkName ? ` — Park ${parkName}` : ''; + let titleSuffix = parkName ? ` — ${parkName} - ${parkId}` : ''; modalTitle.innerText = `${empType} — Zone ${zoneVal}${titleSuffix} — ${category.toUpperCase()} (${rawFrom} → ${rawTo})`; modalBody.innerHTML = createLoader(); // Show modal immediately with loader @@ -516,8 +566,8 @@ try { let url = `${apiBase}/detailed?empType=${encodeURIComponent(empType)}&category=${encodeURIComponent(category)}&fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`; url += `&zone=${encodeURIComponent(zoneVal)}`; - if (parkName) { - url += `&parkId=${encodeURIComponent(parkName)}`; + if (parkId) { + url += `&parkId=${encodeURIComponent(parkId)}`; } const res = await fetch(url); if (!res.ok) { @@ -647,7 +697,7 @@ alert('To date must be the same as or later than From date'); return; } - loadAbstractCounts(); loadZoneSummary(); + loadAbstractCounts(); loadZoneWisePercentage(); loadZoneSummary(); }); // delegate clicks: zone links -> park-wise summary, zone counts -> detailed, park counts -> park-details document.addEventListener('click', (ev) => { @@ -692,8 +742,9 @@ 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); + loadDetailed('Park', category, zone, parkId, parkName); return; } }); @@ -701,6 +752,7 @@ setDateLimits(); // load initial loadAbstractCounts(); + loadZoneWisePercentage(); loadZoneSummary(); });
ZoneTotalFacial RegisteredFacial Not RegisteredPresentODLeaveNot Marked
${total}${facialRegistered}${facialNotRegistered}` + (facialNotRegistered > 0 ? `${facialNotRegistered}` : `${facialNotRegistered}`) + `` + (present > 0 ? `${present}` : `${present}`) + `` + (od > 0 ? `${od}` : `${od}`) + `` + (leave > 0 ? `${leave}` : `${leave}`) + `` + (notMarked > 0 ? `${notMarked}` : `${notMarked}`) + `` + (facialNotRegistered > 0 ? `${facialNotRegistered}` : `${facialNotRegistered}`) + `` + (present > 0 ? `${present}` : `${present}`) + `` + (od > 0 ? `${od}` : `${od}`) + `` + (leave > 0 ? `${leave}` : `${leave}`) + `` + (notMarked > 0 ? `${notMarked}` : `${notMarked}`) + `