Database - AWS Connection
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

879 lines
42 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Attendance Dashboard</title>
  7. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
  8. <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js" crossorigin="anonymous"></script>
  9. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
  10. <style>
  11. .spinner-container {
  12. display: flex;
  13. justify-content: center;
  14. align-items: center;
  15. min-height: 200px;
  16. }
  17. .spinner-border {
  18. width: 3rem;
  19. height: 3rem;
  20. color: #007bff;
  21. }
  22. .zone-summary-row {
  23. display: grid;
  24. grid-template-columns: 1fr;
  25. gap: 20px;
  26. }
  27. @media (max-width: 768px) {
  28. .zone-summary-row {
  29. grid-template-columns: 1fr;
  30. }
  31. }
  32. .zone-summary-card {
  33. border: 1px solid #ddd;
  34. border-radius: 4px;
  35. padding: 15px;
  36. background-color: #f9f9f9;
  37. }
  38. .disabled-count {
  39. color: #6c757d;
  40. cursor: default;
  41. text-decoration: none;
  42. display: inline-block;
  43. padding: 0 4px;
  44. }
  45. </style>
  46. </head>
  47. <body class="p-3">
  48. <div class="container">
  49. <h1 class="mb-4">Attendance Dashboard</h1>
  50. <div class="row mb-3">
  51. <div class="col-md-3">
  52. <label class="form-label">From</label>
  53. <input id="fromDate" type="date" class="form-control" />
  54. </div>
  55. <div class="col-md-3">
  56. <label class="form-label">To</label>
  57. <input id="toDate" type="date" class="form-control" />
  58. </div>
  59. <div class="col-md-6 d-flex align-items-end">
  60. <div>
  61. <button id="loadSummaryBtn" class="btn btn-secondary">Load</button>
  62. </div>
  63. </div>
  64. </div>
  65. <h4>Abstract Counts</h4>
  66. <div id="abstractCounts" class="mb-4"></div>
  67. <h4>Zone Wise Attendance %</h4>
  68. <div id="zoneWisePercentage" class="mb-4"></div>
  69. <h4>Zone Summary</h4>
  70. <div class="zone-summary-row">
  71. <div class="zone-summary-card">
  72. <div class="d-flex justify-content-between align-items-center mb-2">
  73. <h5>Park</h5>
  74. <button class="btn btn-sm btn-primary"
  75. onclick="downloadExcelFromTable(document.getElementById('zoneSummaryPark'), 'park_summary.xlsx')">
  76. <i class="bi bi-download"></i> Download Excel
  77. </button>
  78. </div>
  79. <div id="zoneSummaryPark"></div>
  80. </div>
  81. <!-- NULM zone summary card commented out
  82. <div class="zone-summary-card">
  83. <div class="d-flex justify-content-between align-items-center mb-2">
  84. <h5>NULM</h5>
  85. <button class="btn btn-sm btn-primary"
  86. onclick="downloadExcelFromTable(document.getElementById('zoneSummaryNulm'), 'nulm_summary.xlsx')">
  87. <i class="bi bi-download"></i> Download Excel
  88. </button>
  89. </div>
  90. <div id="zoneSummaryNulm"></div>
  91. </div>
  92. -->
  93. </div>
  94. </div>
  95. <script>
  96. function showImageModal(imageSrc) {
  97. document.getElementById("previewImage").src = imageSrc;
  98. var modal = new bootstrap.Modal(document.getElementById('imagePreviewModal'));
  99. modal.show();
  100. }
  101. (function () {
  102. const apiBase = '/commissionerreview/api/attendanceController';
  103. function pad(n) { return n < 10 ? '0' + n : n; }
  104. // convert Date -> yyyy-mm-dd for input[type=date]
  105. function toIsoYyyyMmDd(date) {
  106. return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
  107. }
  108. // convert input value (either yyyy-mm-dd or dd-mm-yyyy) -> dd-mm-yyyy for API
  109. function toApiDdMmYyyy(val) {
  110. if (!val) return val;
  111. // if already in dd-mm-yyyy (contains two '-') and first part length 2
  112. const parts = val.split('-');
  113. if (parts.length === 3) {
  114. if (parts[0].length === 4) {
  115. // yyyy-mm-dd -> dd-mm-yyyy
  116. return pad(parseInt(parts[2], 10)) + '-' + pad(parseInt(parts[1], 10)) + '-' + parts[0];
  117. } else {
  118. // assume already dd-mm-yyyy
  119. return val;
  120. }
  121. }
  122. return val;
  123. }
  124. function setDefaults() {
  125. const today = new Date();
  126. const iso = toIsoYyyyMmDd(today);
  127. document.getElementById('fromDate').value = iso;
  128. document.getElementById('toDate').value = iso;
  129. }
  130. function setDateLimits() {
  131. const from = document.getElementById('fromDate');
  132. const to = document.getElementById('toDate');
  133. const todayIso = toIsoYyyyMmDd(new Date());
  134. from.max = todayIso;
  135. to.max = todayIso;
  136. if (from.value) to.min = from.value;
  137. from.addEventListener('change', () => {
  138. const fv = from.value;
  139. if (fv) {
  140. to.min = fv;
  141. if (to.value && to.value < fv) to.value = fv;
  142. } else {
  143. to.min = '';
  144. }
  145. });
  146. to.addEventListener('change', () => {
  147. if (from.value && to.value && to.value < from.value) {
  148. to.value = from.value;
  149. }
  150. });
  151. }
  152. async function loadAbstractCounts() {
  153. const target = document.getElementById('abstractCounts');
  154. target.innerHTML = createLoader();
  155. try {
  156. const res = await fetch(apiBase + '/abstractCounts');
  157. const data = await res.json();
  158. if (!Array.isArray(data)) { target.innerText = JSON.stringify(data); return; }
  159. let html = '<div class="row">';
  160. data.forEach(item => {
  161. const facialRegistered = item.facialRegistered || 0;
  162. const facialNotRegistered = item.facialNotRegistered || 0;
  163. const totalCount = item.totalCount || 0;
  164. const facialRegPct = totalCount > 0 ? ((facialRegistered / totalCount) * 100).toFixed(2) : '0.00';
  165. html += `<div class="col-md-3"><div class="card p-3 mb-2">
  166. <strong>${item.emp_type}</strong>
  167. <div class="mt-2">
  168. <div class="small">Total: <strong>${totalCount}</strong></div>
  169. <div class="small">Facial Registered: <strong>${facialRegistered}</strong></div>
  170. <div class="small">Facial Not Registered: <strong>${facialNotRegistered}</strong></div>
  171. <div class="small">Facial Registered %: <strong>${facialRegPct}%</strong></div>
  172. <div class="small">Present %: <strong><span id="presentPercent">—</span></strong></div>
  173. </div>
  174. </div></div>`;
  175. });
  176. html += '</div>';
  177. target.innerHTML = html;
  178. } catch (e) { target.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; }
  179. }
  180. async function loadZoneWisePercentage() {
  181. const target = document.getElementById('zoneWisePercentage');
  182. target.innerHTML = createLoader();
  183. try {
  184. const rawFrom = document.getElementById('fromDate').value;
  185. const rawTo = document.getElementById('toDate').value;
  186. const fromDate = toApiDdMmYyyy(rawFrom);
  187. const toDate = toApiDdMmYyyy(rawTo);
  188. const res = await fetch(`${apiBase}/zoneWisePercentage?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`);
  189. const data = await res.json();
  190. if (!Array.isArray(data) || data.length === 0) {
  191. target.innerHTML = '<div class="alert alert-info">No zone percentage data available</div>';
  192. return;
  193. }
  194. let html = '<div class="row">';
  195. data.forEach(item => {
  196. const zoneName = item.ZoneName || 'Unknown Zone';
  197. const facialPct = item.facialPercentage != null ? item.facialPercentage : 0;
  198. const presentPct = item.presentPercentage != null ? item.presentPercentage : 0;
  199. html += `<div class="col-md-4"><div class="card p-3 mb-2 text-center">
  200. <strong>${zoneName}</strong>
  201. <div class="row mt-2">
  202. <div class="col-6">
  203. <div style="font-size: 1.3rem; font-weight: bold;">
  204. ${facialPct}%
  205. </div>
  206. <div class="small text-muted">Facial Registered %</div>
  207. </div>
  208. <div class="col-6">
  209. <div style="font-size: 1.3rem; font-weight: bold;">
  210. ${presentPct}%
  211. </div>
  212. <div class="small text-muted">Present %</div>
  213. </div>
  214. </div>
  215. </div></div>`;
  216. });
  217. html += '</div>';
  218. target.innerHTML = html;
  219. } catch (e) {
  220. target.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  221. }
  222. }
  223. function createLoader() {
  224. return '<div class="spinner-container"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>';
  225. }
  226. function downloadExcelFromTable(tableElement, fileName) {
  227. if (typeof XLSX === 'undefined') {
  228. alert('Excel library is not loaded. Please refresh the page and try again.');
  229. console.error('XLSX library failed to load from CDN');
  230. return;
  231. }
  232. if (!tableElement) {
  233. alert('Table not found');
  234. return;
  235. }
  236. const table = tableElement.querySelector('table');
  237. if (!table) {
  238. alert('No table to download');
  239. return;
  240. }
  241. const workbook = XLSX.utils.table_to_book(table);
  242. XLSX.writeFile(workbook, fileName);
  243. }
  244. // Expose to global scope for onclick handlers
  245. window.downloadExcelFromTable = downloadExcelFromTable;
  246. function renderTable(target, data, empType) {
  247. if (!Array.isArray(data)) { target.innerText = JSON.stringify(data); return; }
  248. if (data.length === 0) { target.innerHTML = '<div class="alert alert-info">No records</div>'; return; }
  249. const rows = data.slice().sort((a, b) => String(a.zone || '').localeCompare(String(b.zone || ''), undefined, { numeric: true }));
  250. // 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">Supervisor</th><th class="text-center">Present</th><th class="text-center">OD</th><th class="text-center">Leave</th><th class="text-center">Not Marked</th></tr></thead><tbody>';
  251. 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><th class="text-center">Not Marked</th></tr></thead><tbody>';
  252. rows.forEach(r => {
  253. const zone = r.zone;
  254. const total = Number(r.TotalCount) || 0;
  255. const facialRegistered = Number(r.facialRegistered) || 0;
  256. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  257. const present = Number(r.AttendanceMarked) || 0;
  258. const od = Number(r.ODCount) || 0;
  259. const leave = Number(r.LeaveCount) || 0;
  260. const notMarked = Math.max(0, total - present - od - leave - facialNotRegistered);
  261. // const supervisor = Number(r.SupervisorCount) || 0;
  262. html += `<tr><td>${zone || '-'}</td>`;
  263. html += `<td class="text-center">` + (total > 0
  264. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="total">${total}</a>`
  265. : `<span class="disabled-count">${total}</span>`) + `</td>`;
  266. html += `<td class="text-center">${facialRegistered}</td>`;
  267. html += `<td class="text-center">` + (facialNotRegistered > 0
  268. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="notregistered">${facialNotRegistered}</a>`
  269. : `<span class="disabled-count">${facialNotRegistered}</span>`) + `</td>`;
  270. // html += `<td class="text-center">` + (supervisor > 0
  271. // ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="supervisor">${supervisor}</a>`
  272. // : `<span class="disabled-count">${supervisor}</span>`) + `</td>`;
  273. html += `<td class="text-center">` + (present > 0
  274. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="present">${present}</a>`
  275. : `<span class="disabled-count">${present}</span>`) + `</td>`;
  276. html += `<td class="text-center">` + (od > 0
  277. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="od">${od}</a>`
  278. : `<span class="disabled-count">${od}</span>`) + `</td>`;
  279. html += `<td class="text-center">` + (leave > 0
  280. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="leave">${leave}</a>`
  281. : `<span class="disabled-count">${leave}</span>`) + `</td>`;
  282. html += `<td class="text-center">` + (notMarked > 0
  283. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="notmarked">${notMarked}</a>`
  284. : `<span class="disabled-count">${notMarked}</span>`) + `</td>`;
  285. html += '</tr>';
  286. });
  287. // Compute grand totals
  288. let grandTotal = 0, grandSupervisor = 0, grandFacialReg = 0, grandFacialNotReg = 0, grandPresent = 0, grandOd = 0, grandLeave = 0, grandNotMarked = 0;
  289. rows.forEach(r => {
  290. const total = Number(r.TotalCount) || 0;
  291. const facialRegistered = Number(r.facialRegistered) || 0;
  292. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  293. const present = Number(r.AttendanceMarked) || 0;
  294. const od = Number(r.ODCount) || 0;
  295. const leave = Number(r.LeaveCount) || 0;
  296. grandTotal += total;
  297. // grandSupervisor += (Number(r.SupervisorCount) || 0);
  298. grandFacialReg += facialRegistered;
  299. grandFacialNotReg += facialNotRegistered;
  300. grandPresent += present;
  301. grandOd += od;
  302. grandLeave += leave;
  303. grandNotMarked += Math.max(0, total - present - od - leave - facialNotRegistered);
  304. });
  305. html += '</tbody><tfoot><tr style="font-weight:bold; background-color:#e9ecef;">';
  306. html += '<td>Total</td>';
  307. html += `<td class="text-center">${grandTotal}</td>`;
  308. html += `<td class="text-center">${grandFacialReg}</td>`;
  309. html += `<td class="text-center">${grandFacialNotReg}</td>`;
  310. // html += `<td class="text-center">${grandSupervisor}</td>`;
  311. html += `<td class="text-center">${grandPresent}</td>`;
  312. html += `<td class="text-center">${grandOd}</td>`;
  313. html += `<td class="text-center">${grandLeave}</td>`;
  314. html += `<td class="text-center">${grandNotMarked}</td>`;
  315. html += '</tr></tfoot></table>';
  316. target.innerHTML = html;
  317. }
  318. async function loadZoneSummary() {
  319. const rawFrom = document.getElementById('fromDate').value;
  320. const rawTo = document.getElementById('toDate').value;
  321. const fromDate = toApiDdMmYyyy(rawFrom);
  322. const toDate = toApiDdMmYyyy(rawTo);
  323. const parkTarget = document.getElementById('zoneSummaryPark');
  324. // const nulmTarget = document.getElementById('zoneSummaryNulm'); // NULM commented out
  325. // Show loaders
  326. parkTarget.innerHTML = createLoader();
  327. // nulmTarget.innerHTML = createLoader(); // NULM commented out
  328. try {
  329. // Load Park independently
  330. fetch(`${apiBase}/zoneSummaryPark?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`)
  331. .then(res => {
  332. if (!res.ok) {
  333. console.error('Park zoneSummary error', res.status);
  334. parkTarget.innerHTML = `<div class="alert alert-danger">Error loading Park summary (${res.status})</div>`;
  335. return Promise.reject(new Error('Park fetch failed'));
  336. }
  337. return res.json();
  338. })
  339. .then(parkData => {
  340. console.log('Park data loaded:', parkData);
  341. renderTable(parkTarget, parkData, 'Park');
  342. // Compute Present % (Present + OD) / Total
  343. if (Array.isArray(parkData) && parkData.length > 0) {
  344. let facialRegisteredAll = 0, presentOdAll = 0;
  345. parkData.forEach(r => {
  346. facialRegisteredAll += (Number(r.facialRegistered) || 0);
  347. presentOdAll += (Number(r.AttendanceMarked) || 0) + (Number(r.ODCount) || 0);
  348. });
  349. const pct = facialRegisteredAll > 0 ? ((presentOdAll / facialRegisteredAll) * 100).toFixed(1) : '0.0';
  350. const el = document.getElementById('presentPercent');
  351. if (el) el.textContent = pct + '%';
  352. }
  353. })
  354. .catch(e => {
  355. console.error('Park error:', e);
  356. parkTarget.innerHTML = `<div class="alert alert-danger">Error: ${e.message}</div>`;
  357. });
  358. // NULM loading commented out
  359. // fetch(`${apiBase}/zoneSummaryNULM?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`)
  360. // .then(res => {
  361. // if (!res.ok) {
  362. // console.error('NULM zoneSummary error', res.status);
  363. // nulmTarget.innerHTML = `<div class="alert alert-danger">Error loading NULM summary (${res.status})</div>`;
  364. // return Promise.reject(new Error('NULM fetch failed'));
  365. // }
  366. // return res.json();
  367. // })
  368. // .then(nulmData => {
  369. // console.log('NULM data loaded:', nulmData);
  370. // renderTable(nulmTarget, nulmData, 'NULM');
  371. // })
  372. // .catch(e => {
  373. // console.error('NULM error:', e);
  374. // nulmTarget.innerHTML = `<div class="alert alert-danger">Error: ${e.message}</div>`;
  375. // });
  376. } catch (e) {
  377. parkTarget.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  378. // nulmTarget.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; // NULM commented out
  379. }
  380. }
  381. async function loadParkWiseSummary(zone, category) {
  382. if (!zone) {
  383. alert('Please select a zone first');
  384. return;
  385. }
  386. const modalEl = document.getElementById('parkWiseSummaryModal');
  387. const modalBody = document.getElementById('parkWiseSummaryModalBody');
  388. const modalTitle = document.getElementById('parkWiseSummaryModalLabel');
  389. modalTitle.innerText = `Park-wise Summary — Zone ${zone} — ${(category || 'All').toUpperCase()}`;
  390. modalBody.innerHTML = createLoader();
  391. new bootstrap.Modal(modalEl).show();
  392. try {
  393. const rawFrom = document.getElementById('fromDate').value;
  394. const rawTo = document.getElementById('toDate').value;
  395. const fromDate = toApiDdMmYyyy(rawFrom);
  396. const toDate = toApiDdMmYyyy(rawTo);
  397. const url = `${apiBase}/parkWiseSummary?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}&zone=${encodeURIComponent(zone)}`;
  398. const res = await fetch(url);
  399. if (!res.ok) {
  400. modalBody.innerHTML = `<div class="alert alert-danger">Error loading park-wise summary (${res.status})</div>`;
  401. return;
  402. }
  403. const data = await res.json();
  404. if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; }
  405. if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No park-wise records</div>'; return; }
  406. // Filter parks: when a category is selected, only show parks with count > 0 for that category
  407. const catLower = (category || '').toLowerCase();
  408. const displayData = !catLower ? data : data.filter(r => {
  409. const total = Number(r.TotalCount) || 0;
  410. const present = Number(r.AttendanceMarked) || 0;
  411. const od = Number(r.ODCount) || 0;
  412. const leave = Number(r.LeaveCount) || 0;
  413. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  414. const notMarked = Math.max(0, total - present - od - leave - facialNotRegistered);
  415. if (catLower === 'total') return total > 0;
  416. if (catLower === 'present') return present > 0;
  417. if (catLower === 'od') return od > 0;
  418. if (catLower === 'leave') return leave > 0;
  419. if (catLower === 'notmarked') return notMarked > 0;
  420. if (catLower === 'notregistered') return facialNotRegistered > 0;
  421. return true;
  422. });
  423. if (displayData.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No parks with ' + (category || '').toUpperCase() + ' count</div>'; return; }
  424. const showAll = !catLower;
  425. let html = '<div class="table-responsive"><table class="table table-sm table-striped"><thead><tr>';
  426. html += '<th style="display:none;">Park ID</th><th>Park Name</th><th>Supervisor Name</th><th>Supervisor Mobile</th>';
  427. if (showAll) {
  428. html += '<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><th class="text-center">Not Marked</th>';
  429. } else {
  430. const categoryLabel = { present: 'Present', od: 'OD', leave: 'Leave', notmarked: 'Not Marked', notregistered: 'Facial Not Registered' };
  431. html += `<th class="text-center">${categoryLabel[catLower] || category}</th>`;
  432. }
  433. html += '</tr></thead><tbody>';
  434. displayData.forEach(r => {
  435. const parkId = r.park_id || '';
  436. const park = r.park_name || '';
  437. const parkDisplay = park === '' ? (parkId || 'Unknown') : park;
  438. const supervisorName = r.supervisor_name || '-';
  439. const supervisorMobile = r.supervisor_mobile || '-';
  440. const total = Number(r.TotalCount) || 0;
  441. const facialRegistered = Number(r.facialRegistered) || 0;
  442. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  443. const present = Number(r.AttendanceMarked) || 0;
  444. const od = Number(r.ODCount) || 0;
  445. const leave = Number(r.LeaveCount) || 0;
  446. const notMarked = Math.max(0, total - present - od - leave - facialNotRegistered);
  447. html += '<tr>';
  448. html += `<td style="display:none;">${parkId}</td>`;
  449. html += `<td>${parkDisplay}</td>`;
  450. html += `<td>${supervisorName}</td>`;
  451. html += `<td>${supervisorMobile}</td>`;
  452. if (showAll) {
  453. html += `<td class="text-center">${total}</td>`;
  454. html += `<td class="text-center">${facialRegistered}</td>`;
  455. 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>`;
  456. 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>`;
  457. 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>`;
  458. 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>`;
  459. 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>`;
  460. } else {
  461. // Show only the selected category column
  462. let catValue = 0;
  463. if (catLower === 'present') catValue = present;
  464. else if (catLower === 'od') catValue = od;
  465. else if (catLower === 'leave') catValue = leave;
  466. else if (catLower === 'notmarked') catValue = notMarked;
  467. else if (catLower === 'notregistered') catValue = facialNotRegistered;
  468. html += `<td class="text-center">` + (catValue > 0 ? `<a href="#" class="park-employee-count" data-zone="${zone}" data-parkid="${parkId}" data-parkname="${parkDisplay}" data-category="${catLower}">${catValue}</a>` : `<span class="disabled-count">${catValue}</span>`) + `</td>`;
  469. }
  470. html += '</tr>';
  471. });
  472. // Compute grand totals from ALL data to match zone summary (no count mismatch)
  473. let grandTotal = 0, grandFacialReg = 0, grandFacialNotReg = 0, grandPresent = 0, grandOd = 0, grandLeave = 0, grandNotMarked = 0;
  474. data.forEach(r => {
  475. const total = Number(r.TotalCount) || 0;
  476. const facialRegistered = Number(r.facialRegistered) || 0;
  477. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  478. const present = Number(r.AttendanceMarked) || 0;
  479. const od = Number(r.ODCount) || 0;
  480. const leave = Number(r.LeaveCount) || 0;
  481. grandTotal += total;
  482. grandFacialReg += facialRegistered;
  483. grandFacialNotReg += facialNotRegistered;
  484. grandPresent += present;
  485. grandOd += od;
  486. grandLeave += leave;
  487. grandNotMarked += Math.max(0, total - present - od - leave - facialNotRegistered);
  488. });
  489. html += '</tbody><tfoot><tr style="font-weight:bold; background-color:#e9ecef;">';
  490. html += '<td>Total</td>';
  491. html += '<td></td>';
  492. html += '<td></td>';
  493. if (showAll) {
  494. html += `<td class="text-center">${grandTotal}</td>`;
  495. html += `<td class="text-center">${grandFacialReg}</td>`;
  496. html += `<td class="text-center">${grandFacialNotReg}</td>`;
  497. html += `<td class="text-center">${grandPresent}</td>`;
  498. html += `<td class="text-center">${grandOd}</td>`;
  499. html += `<td class="text-center">${grandLeave}</td>`;
  500. html += `<td class="text-center">${grandNotMarked}</td>`;
  501. } else {
  502. let grandCatValue = 0;
  503. if (catLower === 'present') grandCatValue = grandPresent;
  504. else if (catLower === 'od') grandCatValue = grandOd;
  505. else if (catLower === 'leave') grandCatValue = grandLeave;
  506. else if (catLower === 'notmarked') grandCatValue = grandNotMarked;
  507. else if (catLower === 'notregistered') grandCatValue = grandFacialNotReg;
  508. html += `<td class="text-center">${grandCatValue}</td>`;
  509. }
  510. html += '</tr></tfoot></table></div>';
  511. modalBody.innerHTML = html;
  512. } catch (e) {
  513. modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  514. }
  515. }
  516. async function loadParkDetails(zone, type) {
  517. const modalEl = document.getElementById('detailModal');
  518. const modalBody = document.getElementById('detailModalBody');
  519. const modalTitle = document.getElementById('detailModalLabel');
  520. modalTitle.innerText = `Parks — Zone ${zone} — ${type}`;
  521. modalBody.innerHTML = createLoader();
  522. new bootstrap.Modal(modalEl).show();
  523. try {
  524. const url = `/commissionerreview/api/parks/getParkDataByZone?zone=${encodeURIComponent(zone)}&type=${encodeURIComponent(type)}`;
  525. const res = await fetch(url);
  526. if (!res.ok) {
  527. modalBody.innerHTML = `<div class="alert alert-danger">Error ${res.status}</div>`;
  528. return;
  529. }
  530. const data = await res.json();
  531. if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; }
  532. if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No records</div>'; return; }
  533. // Build table from keys of first row
  534. const cols = Object.keys(data[0]);
  535. let html = '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr><th>S.No</th>';
  536. cols.forEach(c => html += `<th>${c.replace(/_/g, ' ')}</th>`);
  537. html += '</tr></thead><tbody>';
  538. data.forEach((row, idx) => {
  539. html += '<tr>';
  540. html += `<td>${idx + 1}</td>`;
  541. cols.forEach(c => html += `<td>${row[c] != null ? row[c] : '-'}</td>`);
  542. html += '</tr>';
  543. });
  544. html += '</tbody></table></div>';
  545. modalBody.innerHTML = html;
  546. } catch (e) {
  547. modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  548. }
  549. }
  550. // loadDetailed may be called with explicit empType/category/zone (when clicking a zone count)
  551. async function loadDetailed(empType, category, zoneVal, parkId, parkName) {
  552. // show details in modal
  553. const rawFrom = document.getElementById('fromDate').value;
  554. const rawTo = document.getElementById('toDate').value;
  555. const fromDate = toApiDdMmYyyy(rawFrom);
  556. const toDate = toApiDdMmYyyy(rawTo);
  557. const modalEl = document.getElementById('detailModal');
  558. const modalBody = document.getElementById('detailModalBody');
  559. const modalTitle = document.getElementById('detailModalLabel');
  560. if (!empType || !category || !zoneVal) {
  561. modalTitle.innerText = 'Details';
  562. modalBody.innerHTML = '<div class="text-warning">Missing parameters. Click a count to view details.</div>';
  563. new bootstrap.Modal(modalEl).show();
  564. return;
  565. }
  566. let titleSuffix = parkName ? ` — ${parkName} - ${parkId}` : '';
  567. modalTitle.innerText = `${empType} — Zone ${zoneVal}${titleSuffix} — ${category.toUpperCase()} (${rawFrom} → ${rawTo})`;
  568. modalBody.innerHTML = createLoader();
  569. // Show modal immediately with loader
  570. new bootstrap.Modal(modalEl).show();
  571. try {
  572. let url = `${apiBase}/detailed?empType=${encodeURIComponent(empType)}&category=${encodeURIComponent(category)}&fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`;
  573. url += `&zone=${encodeURIComponent(zoneVal)}`;
  574. if (parkId) {
  575. url += `&parkId=${encodeURIComponent(parkId)}`;
  576. }
  577. const res = await fetch(url);
  578. if (!res.ok) {
  579. const text = await res.text().catch(() => '');
  580. modalBody.innerHTML = `<div class="alert alert-danger">Error ${res.status}: ${text}</div>`;
  581. return;
  582. }
  583. const data = await res.json();
  584. if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; }
  585. if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No records</div>'; return; }
  586. // Define columns to display based on category
  587. let displayColumns = [];
  588. const catLower = (category || 'present').toLowerCase();
  589. if (catLower === 'total') {
  590. displayColumns = ['park_name', 'supervisor_name', 'supervisor_mobile', 'enrollment_id', 'name', 'emp_mobile'];
  591. } else if (catLower === 'present') {
  592. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location'];
  593. if (empType === 'Park') displayColumns.push('park_name');
  594. displayColumns.push('intime', 'inphoto', 'outtime', 'outphoto');
  595. } else if (catLower === 'leave') {
  596. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location'];
  597. if (empType === 'Park') displayColumns.push('park_name');
  598. displayColumns.push('leavedatetime', 'leaveby');
  599. } else if (catLower === 'od') {
  600. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location'];
  601. if (empType === 'Park') displayColumns.push('park_name');
  602. displayColumns.push('odtime', 'odby');
  603. } else if (catLower === 'notmarked') {
  604. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'image'];
  605. // if (empType === 'Park') displayColumns.push('park_name');
  606. } else if (catLower === 'notregistered') {
  607. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno'];
  608. // if (empType === 'Park') displayColumns.push('park_name');
  609. } else if (catLower === 'supervisor') {
  610. displayColumns = ['supervisor_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno'];
  611. } else {
  612. displayColumns = Object.keys(data[0]);
  613. }
  614. // Filter data to include only displayColumns, handle case-insensitive matching
  615. const filteredData = data.map(row => {
  616. const filtered = {};
  617. displayColumns.forEach(col => {
  618. // Try exact match first, then case-insensitive
  619. const key = Object.keys(row).find(k => k === col || k.toLowerCase() === col.toLowerCase());
  620. if (key) {
  621. filtered[col] = row[key];
  622. }
  623. });
  624. return filtered;
  625. });
  626. function prettyName(key) {
  627. const map = {
  628. enrollment_id: 'Enrollment ID',
  629. name: 'Name',
  630. designation: 'Designation',
  631. department: 'Department',
  632. park_name: 'Park Name',
  633. intime: 'In Time',
  634. inphoto: 'In Photo',
  635. outtime: 'Out Time',
  636. outphoto: 'Out Photo',
  637. leavedatetime: 'Leave DateTime',
  638. leaveby: 'Leave By',
  639. odtime: 'OD DateTime',
  640. odby: 'OD By',
  641. incharge_name: 'Incharge Name',
  642. incharge_phoneno: 'Incharge Contact',
  643. appversion: 'App Version',
  644. apptype: 'App Type',
  645. location: 'Location',
  646. image: 'Image',
  647. supervisor_id: 'Supervisor ID',
  648. supervisor_name: 'Supervisor Name',
  649. supervisor_mobile: 'Supervisor Mobile',
  650. emp_mobile: 'Employee Mobile',
  651. };
  652. if (map[key]) return map[key];
  653. return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
  654. }
  655. let html = '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr>';
  656. // add serial number header
  657. html += '<th>S.No</th>';
  658. displayColumns.forEach(k => {
  659. html += `<th>${prettyName(k)}</th>`;
  660. });
  661. html += '</tr></thead><tbody>';
  662. filteredData.forEach((row, idx) => {
  663. html += '<tr>';
  664. // serial number cell
  665. html += `<td>${idx + 1}</td>`;
  666. displayColumns.forEach(k => {
  667. const val = row[k];
  668. // Handle photo columns - show as thumbnail if it's an image
  669. // if ((k === 'inphoto' || k === 'outphoto') && val) {
  670. // 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>`;
  671. // } else {
  672. // html += `<td>${val != null ? val : '-'}</td>`;
  673. // }
  674. if ((k === 'inphoto' || k === 'outphoto' || k === 'image') && val) {
  675. html += `<td>
  676. <img src="${val}" alt="photo"
  677. style="max-width: 100px; max-height: 100px; cursor: pointer;"
  678. onclick="showImageModal('${val}')">
  679. </td>`;
  680. } else {
  681. html += `<td>${val != null ? val : '-'}</td>`;
  682. }
  683. });
  684. html += '</tr>';
  685. });
  686. html += '</tbody></table></div>';
  687. modalBody.innerHTML = html;
  688. } catch (e) {
  689. modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  690. }
  691. }
  692. document.addEventListener('DOMContentLoaded', () => {
  693. setDefaults();
  694. document.getElementById('loadSummaryBtn').addEventListener('click', () => {
  695. const rawFrom = document.getElementById('fromDate').value;
  696. const rawTo = document.getElementById('toDate').value;
  697. if (rawFrom && rawTo && rawTo < rawFrom) {
  698. alert('To date must be the same as or later than From date');
  699. return;
  700. }
  701. loadAbstractCounts(); loadZoneWisePercentage(); loadZoneSummary();
  702. });
  703. // delegate clicks: zone links -> park-wise summary, zone counts -> detailed, park counts -> park-details
  704. document.addEventListener('click', (ev) => {
  705. const zoneLink = ev.target.closest && ev.target.closest('.zone-link');
  706. if (zoneLink) {
  707. ev.preventDefault();
  708. const zone = zoneLink.getAttribute('data-zone');
  709. loadParkWiseSummary(zone);
  710. return;
  711. }
  712. const zoneAnchor = ev.target.closest && ev.target.closest('.zone-count');
  713. if (zoneAnchor) {
  714. ev.preventDefault();
  715. const zone = zoneAnchor.getAttribute('data-zone');
  716. const empType = zoneAnchor.getAttribute('data-emptype');
  717. const category = zoneAnchor.getAttribute('data-category');
  718. // Supervisor & Total: show employee details directly (no park-wise drill-down)
  719. if (category === 'supervisor' || category === 'total') {
  720. loadDetailed(empType, category, zone);
  721. } else if (empType === 'Park') {
  722. loadParkWiseSummary(zone, category);
  723. }
  724. // NULM branch commented out
  725. // else {
  726. // loadDetailed(empType, category, zone);
  727. // }
  728. return;
  729. }
  730. const parkAnchor = ev.target.closest && ev.target.closest('.park-count');
  731. if (parkAnchor) {
  732. ev.preventDefault();
  733. const zone = parkAnchor.getAttribute('data-zone');
  734. const type = parkAnchor.getAttribute('data-type');
  735. loadParkDetails(zone, type);
  736. return;
  737. }
  738. const parkEmployeeAnchor = ev.target.closest && ev.target.closest('.park-employee-count');
  739. if (parkEmployeeAnchor) {
  740. ev.preventDefault();
  741. const zone = parkEmployeeAnchor.getAttribute('data-zone');
  742. const parkId = parkEmployeeAnchor.getAttribute('data-parkid');
  743. const parkName = parkEmployeeAnchor.getAttribute('data-parkname');
  744. const category = parkEmployeeAnchor.getAttribute('data-category');
  745. loadDetailed('Park', category, zone, parkId, parkName);
  746. return;
  747. }
  748. });
  749. // set date constraints and load initial
  750. setDateLimits();
  751. // load initial
  752. loadAbstractCounts();
  753. loadZoneWisePercentage();
  754. loadZoneSummary();
  755. });
  756. })();
  757. </script>
  758. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
  759. <!-- Park-wise Summary Modal -->
  760. <div class="modal fade" id="parkWiseSummaryModal" tabindex="-1" aria-labelledby="parkWiseSummaryModalLabel"
  761. aria-hidden="true">
  762. <!-- <div class="modal-dialog modal-lg modal-dialog-scrollable"> -->
  763. <div class="modal-dialog modal-xl modal-dialog-scrollable">
  764. <div class="modal-content">
  765. <div class="modal-header">
  766. <h5 class="modal-title" id="parkWiseSummaryModalLabel">Park-wise Summary</h5>
  767. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  768. </div>
  769. <div class="modal-body" id="parkWiseSummaryModalBody">
  770. Loading...
  771. </div>
  772. <div class="modal-footer">
  773. <button type="button" class="btn btn-primary"
  774. onclick="downloadExcelFromTable(document.getElementById('parkWiseSummaryModal'), 'park_wise_summary.xlsx')">
  775. <i class="bi bi-download"></i> Download Excel
  776. </button>
  777. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  778. </div>
  779. </div>
  780. </div>
  781. </div>
  782. <!-- Detail modal -->
  783. <div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
  784. <div class="modal-dialog modal-xl modal-dialog-scrollable">
  785. <!-- <div class="modal-dialog modal-fullscreen modal-dialog-scrollable"> -->
  786. <div class="modal-content">
  787. <div class="modal-header">
  788. <h5 class="modal-title" id="detailModalLabel">Details</h5>
  789. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  790. </div>
  791. <div class="modal-body" id="detailModalBody">
  792. Loading...
  793. </div>
  794. <div class="modal-footer">
  795. <button type="button" class="btn btn-primary"
  796. onclick="downloadExcelFromTable(document.getElementById('detailModal'), 'employee_details.xlsx')">
  797. <i class="bi bi-download"></i> Download Excel
  798. </button>
  799. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  800. </div>
  801. </div>
  802. </div>
  803. </div>
  804. <!-- Image Preview Modal -->
  805. <div class="modal fade" id="imagePreviewModal" tabindex="-1" aria-hidden="true">
  806. <div class="modal-dialog modal-dialog-centered modal-lg">
  807. <div class="modal-content">
  808. <div class="modal-header">
  809. <h5 class="modal-title">Photo Preview</h5>
  810. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  811. </div>
  812. <div class="modal-body text-center">
  813. <img id="previewImage" src="" class="img-fluid" style="max-height: 500px;">
  814. </div>
  815. </div>
  816. </div>
  817. </div>
  818. </body>
  819. </html>