Browse Source

Attendance Dashboard Region wise Percentage Added

master
kanimozhi 8 months ago
parent
commit
ec0fe53a13
  1. 9
      src/main/java/com/gcc/commissionerreview/controller/AttendanceController.java
  2. 53
      src/main/java/com/gcc/commissionerreview/service/AttendanceService.java
  3. 76
      src/main/resources/templates/modules/attendance-dashboard.html

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

@ -23,6 +23,15 @@ public class AttendanceController {
return attendanceService.getAbstractCounts();
}
@GetMapping("/zoneWisePercentage")
public List<Map<String, Object>> getZoneWisePercentage(
@RequestParam(name = "fromDate", required = false) String fromDate,
@RequestParam(name = "toDate", required = false) String toDate) {
List<Map<String, Object>> result = attendanceService.getZoneWisePercentage(fromDate, toDate);
System.out.println("Zone Wise Percentage Result: " + result);
return result;
}
@GetMapping("/zoneSummary")
public List<Map<String, Object>> getZoneSummary(@RequestParam(name = "empType") String empType,
@RequestParam(name = "fromDate", required = false) String fromDate,

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

@ -38,6 +38,59 @@ public class AttendanceService {
return jdbcTemplate.queryForList(sql);
}
public List<Map<String, Object>> 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<Map<String, Object>> getZoneSummary(String empType, String fromDateStr, String toDateStr) {
LocalDate fromDate;
LocalDate toDate;

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

@ -75,6 +75,9 @@
<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">
@ -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 += `<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">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>`;
@ -195,6 +201,50 @@
} 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>';
}
@ -411,11 +461,11 @@
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">` + (facialNotRegistered > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" 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-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-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-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-category="notmarked">${notMarked}</a>` : `<span class="disabled-count">${notMarked}</span>`) + `</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>`;
html += '</tr>';
});
@ -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();
});

Loading…
Cancel
Save