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.

824 lines
39 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}</td>`;
  264. html += `<td class="text-center">${facialRegistered}</td>`;
  265. html += `<td class="text-center">` + (facialNotRegistered > 0
  266. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="notregistered">${facialNotRegistered}</a>`
  267. : `<span class="disabled-count">${facialNotRegistered}</span>`) + `</td>`;
  268. // html += `<td class="text-center">` + (supervisor > 0
  269. // ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="supervisor">${supervisor}</a>`
  270. // : `<span class="disabled-count">${supervisor}</span>`) + `</td>`;
  271. html += `<td class="text-center">` + (present > 0
  272. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="present">${present}</a>`
  273. : `<span class="disabled-count">${present}</span>`) + `</td>`;
  274. html += `<td class="text-center">` + (od > 0
  275. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="od">${od}</a>`
  276. : `<span class="disabled-count">${od}</span>`) + `</td>`;
  277. html += `<td class="text-center">` + (leave > 0
  278. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="leave">${leave}</a>`
  279. : `<span class="disabled-count">${leave}</span>`) + `</td>`;
  280. html += `<td class="text-center">` + (notMarked > 0
  281. ? `<a href="#" class="zone-count" data-zone="${zone}" data-emptype="${empType}" data-category="notmarked">${notMarked}</a>`
  282. : `<span class="disabled-count">${notMarked}</span>`) + `</td>`;
  283. html += '</tr>';
  284. });
  285. // Compute grand totals
  286. let grandTotal = 0, grandSupervisor = 0, grandFacialReg = 0, grandFacialNotReg = 0, grandPresent = 0, grandOd = 0, grandLeave = 0, grandNotMarked = 0;
  287. rows.forEach(r => {
  288. const total = Number(r.TotalCount) || 0;
  289. const facialRegistered = Number(r.facialRegistered) || 0;
  290. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  291. const present = Number(r.AttendanceMarked) || 0;
  292. const od = Number(r.ODCount) || 0;
  293. const leave = Number(r.LeaveCount) || 0;
  294. grandTotal += total;
  295. // grandSupervisor += (Number(r.SupervisorCount) || 0);
  296. grandFacialReg += facialRegistered;
  297. grandFacialNotReg += facialNotRegistered;
  298. grandPresent += present;
  299. grandOd += od;
  300. grandLeave += leave;
  301. grandNotMarked += Math.max(0, total - present - od - leave - facialNotRegistered);
  302. });
  303. html += '</tbody><tfoot><tr style="font-weight:bold; background-color:#e9ecef;">';
  304. html += '<td>Total</td>';
  305. html += `<td class="text-center">${grandTotal}</td>`;
  306. html += `<td class="text-center">${grandFacialReg}</td>`;
  307. html += `<td class="text-center">${grandFacialNotReg}</td>`;
  308. // html += `<td class="text-center">${grandSupervisor}</td>`;
  309. html += `<td class="text-center">${grandPresent}</td>`;
  310. html += `<td class="text-center">${grandOd}</td>`;
  311. html += `<td class="text-center">${grandLeave}</td>`;
  312. html += `<td class="text-center">${grandNotMarked}</td>`;
  313. html += '</tr></tfoot></table>';
  314. target.innerHTML = html;
  315. }
  316. async function loadZoneSummary() {
  317. const rawFrom = document.getElementById('fromDate').value;
  318. const rawTo = document.getElementById('toDate').value;
  319. const fromDate = toApiDdMmYyyy(rawFrom);
  320. const toDate = toApiDdMmYyyy(rawTo);
  321. const parkTarget = document.getElementById('zoneSummaryPark');
  322. // const nulmTarget = document.getElementById('zoneSummaryNulm'); // NULM commented out
  323. // Show loaders
  324. parkTarget.innerHTML = createLoader();
  325. // nulmTarget.innerHTML = createLoader(); // NULM commented out
  326. try {
  327. // Load Park independently
  328. fetch(`${apiBase}/zoneSummaryPark?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`)
  329. .then(res => {
  330. if (!res.ok) {
  331. console.error('Park zoneSummary error', res.status);
  332. parkTarget.innerHTML = `<div class="alert alert-danger">Error loading Park summary (${res.status})</div>`;
  333. return Promise.reject(new Error('Park fetch failed'));
  334. }
  335. return res.json();
  336. })
  337. .then(parkData => {
  338. console.log('Park data loaded:', parkData);
  339. renderTable(parkTarget, parkData, 'Park');
  340. // Compute Present % (Present + OD) / Total
  341. if (Array.isArray(parkData) && parkData.length > 0) {
  342. let totalAll = 0, presentOdAll = 0;
  343. parkData.forEach(r => {
  344. totalAll += (Number(r.TotalCount) || 0);
  345. presentOdAll += (Number(r.AttendanceMarked) || 0) + (Number(r.ODCount) || 0);
  346. });
  347. const pct = totalAll > 0 ? ((presentOdAll / totalAll) * 100).toFixed(1) : '0.0';
  348. const el = document.getElementById('presentPercent');
  349. if (el) el.textContent = pct + '%';
  350. }
  351. })
  352. .catch(e => {
  353. console.error('Park error:', e);
  354. parkTarget.innerHTML = `<div class="alert alert-danger">Error: ${e.message}</div>`;
  355. });
  356. // NULM loading commented out
  357. // fetch(`${apiBase}/zoneSummaryNULM?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`)
  358. // .then(res => {
  359. // if (!res.ok) {
  360. // console.error('NULM zoneSummary error', res.status);
  361. // nulmTarget.innerHTML = `<div class="alert alert-danger">Error loading NULM summary (${res.status})</div>`;
  362. // return Promise.reject(new Error('NULM fetch failed'));
  363. // }
  364. // return res.json();
  365. // })
  366. // .then(nulmData => {
  367. // console.log('NULM data loaded:', nulmData);
  368. // renderTable(nulmTarget, nulmData, 'NULM');
  369. // })
  370. // .catch(e => {
  371. // console.error('NULM error:', e);
  372. // nulmTarget.innerHTML = `<div class="alert alert-danger">Error: ${e.message}</div>`;
  373. // });
  374. } catch (e) {
  375. parkTarget.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  376. // nulmTarget.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`; // NULM commented out
  377. }
  378. }
  379. async function loadParkWiseSummary(zone, category) {
  380. if (!zone) {
  381. alert('Please select a zone first');
  382. return;
  383. }
  384. const modalEl = document.getElementById('parkWiseSummaryModal');
  385. const modalBody = document.getElementById('parkWiseSummaryModalBody');
  386. const modalTitle = document.getElementById('parkWiseSummaryModalLabel');
  387. modalTitle.innerText = `Park-wise Summary — Zone ${zone} — ${(category || 'All').toUpperCase()}`;
  388. modalBody.innerHTML = createLoader();
  389. new bootstrap.Modal(modalEl).show();
  390. try {
  391. const rawFrom = document.getElementById('fromDate').value;
  392. const rawTo = document.getElementById('toDate').value;
  393. const fromDate = toApiDdMmYyyy(rawFrom);
  394. const toDate = toApiDdMmYyyy(rawTo);
  395. const url = `${apiBase}/parkWiseSummary?fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}&zone=${encodeURIComponent(zone)}`;
  396. const res = await fetch(url);
  397. if (!res.ok) {
  398. modalBody.innerHTML = `<div class="alert alert-danger">Error loading park-wise summary (${res.status})</div>`;
  399. return;
  400. }
  401. const data = await res.json();
  402. if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; }
  403. if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No park-wise records</div>'; return; }
  404. let html = '<div class="table-responsive"><table class="table table-sm table-striped"><thead><tr>';
  405. html += '<th style="display:none;">Park ID</th><th>Park Name</th><th>Supervisor Name</th><th>Supervisor Mobile</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>';
  406. html += '</tr></thead><tbody>';
  407. data.forEach(r => {
  408. const parkId = r.park_id || '';
  409. const park = r.park_name || '';
  410. const parkDisplay = park === '' ? (parkId || 'Unknown') : park;
  411. const supervisorName = r.supervisor_name || '-';
  412. const supervisorMobile = r.supervisor_mobile || '-';
  413. const total = Number(r.TotalCount) || 0;
  414. const facialRegistered = Number(r.facialRegistered) || 0;
  415. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  416. const present = Number(r.AttendanceMarked) || 0;
  417. const od = Number(r.ODCount) || 0;
  418. const leave = Number(r.LeaveCount) || 0;
  419. const notMarked = Math.max(0, total - present - od - leave - facialNotRegistered);
  420. html += '<tr>';
  421. html += `<td style="display:none;">${parkId}</td>`;
  422. html += `<td>${parkDisplay}</td>`;
  423. html += `<td>${supervisorName}</td>`;
  424. html += `<td>${supervisorMobile}</td>`;
  425. html += `<td class="text-center">${total}</td>`;
  426. html += `<td class="text-center">${facialRegistered}</td>`;
  427. // html += `<td class="text-center">${facialNotRegistered}</td>`;
  428. 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>`;
  429. 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>`;
  430. 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>`;
  431. 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>`;
  432. 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>`;
  433. html += '</tr>';
  434. });
  435. // Compute grand totals for park-wise summary
  436. let grandTotal = 0, grandSupervisor = 0, grandFacialReg = 0, grandFacialNotReg = 0, grandPresent = 0, grandOd = 0, grandLeave = 0, grandNotMarked = 0;
  437. data.forEach(r => {
  438. const total = Number(r.TotalCount) || 0;
  439. const facialRegistered = Number(r.facialRegistered) || 0;
  440. const facialNotRegistered = Number(r.facialNotRegistered) || 0;
  441. const present = Number(r.AttendanceMarked) || 0;
  442. const od = Number(r.ODCount) || 0;
  443. const leave = Number(r.LeaveCount) || 0;
  444. grandTotal += total;
  445. grandFacialReg += facialRegistered;
  446. grandFacialNotReg += facialNotRegistered;
  447. grandPresent += present;
  448. grandOd += od;
  449. grandLeave += leave;
  450. grandNotMarked += Math.max(0, total - present - od - leave - facialNotRegistered);
  451. });
  452. html += '</tbody><tfoot><tr style="font-weight:bold; background-color:#e9ecef;">';
  453. html += '<td>Total</td>';
  454. html += '<td></td>';
  455. html += '<td></td>';
  456. html += `<td class="text-center">${grandTotal}</td>`;
  457. html += `<td class="text-center">${grandFacialReg}</td>`;
  458. html += `<td class="text-center">${grandFacialNotReg}</td>`;
  459. html += `<td class="text-center">${grandPresent}</td>`;
  460. html += `<td class="text-center">${grandOd}</td>`;
  461. html += `<td class="text-center">${grandLeave}</td>`;
  462. html += `<td class="text-center">${grandNotMarked}</td>`;
  463. html += '</tr></tfoot></table></div>';
  464. modalBody.innerHTML = html;
  465. } catch (e) {
  466. modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  467. }
  468. }
  469. async function loadParkDetails(zone, type) {
  470. const modalEl = document.getElementById('detailModal');
  471. const modalBody = document.getElementById('detailModalBody');
  472. const modalTitle = document.getElementById('detailModalLabel');
  473. modalTitle.innerText = `Parks — Zone ${zone} — ${type}`;
  474. modalBody.innerHTML = createLoader();
  475. new bootstrap.Modal(modalEl).show();
  476. try {
  477. const url = `/commissionerreview/api/parks/getParkDataByZone?zone=${encodeURIComponent(zone)}&type=${encodeURIComponent(type)}`;
  478. const res = await fetch(url);
  479. if (!res.ok) {
  480. modalBody.innerHTML = `<div class="alert alert-danger">Error ${res.status}</div>`;
  481. return;
  482. }
  483. const data = await res.json();
  484. if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; }
  485. if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No records</div>'; return; }
  486. // Build table from keys of first row
  487. const cols = Object.keys(data[0]);
  488. let html = '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr><th>S.No</th>';
  489. cols.forEach(c => html += `<th>${c.replace(/_/g, ' ')}</th>`);
  490. html += '</tr></thead><tbody>';
  491. data.forEach((row, idx) => {
  492. html += '<tr>';
  493. html += `<td>${idx + 1}</td>`;
  494. cols.forEach(c => html += `<td>${row[c] != null ? row[c] : '-'}</td>`);
  495. html += '</tr>';
  496. });
  497. html += '</tbody></table></div>';
  498. modalBody.innerHTML = html;
  499. } catch (e) {
  500. modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  501. }
  502. }
  503. // loadDetailed may be called with explicit empType/category/zone (when clicking a zone count)
  504. async function loadDetailed(empType, category, zoneVal, parkId, parkName) {
  505. // show details in modal
  506. const rawFrom = document.getElementById('fromDate').value;
  507. const rawTo = document.getElementById('toDate').value;
  508. const fromDate = toApiDdMmYyyy(rawFrom);
  509. const toDate = toApiDdMmYyyy(rawTo);
  510. const modalEl = document.getElementById('detailModal');
  511. const modalBody = document.getElementById('detailModalBody');
  512. const modalTitle = document.getElementById('detailModalLabel');
  513. if (!empType || !category || !zoneVal) {
  514. modalTitle.innerText = 'Details';
  515. modalBody.innerHTML = '<div class="text-warning">Missing parameters. Click a count to view details.</div>';
  516. new bootstrap.Modal(modalEl).show();
  517. return;
  518. }
  519. let titleSuffix = parkName ? ` — ${parkName} - ${parkId}` : '';
  520. modalTitle.innerText = `${empType} — Zone ${zoneVal}${titleSuffix} — ${category.toUpperCase()} (${rawFrom} → ${rawTo})`;
  521. modalBody.innerHTML = createLoader();
  522. // Show modal immediately with loader
  523. new bootstrap.Modal(modalEl).show();
  524. try {
  525. let url = `${apiBase}/detailed?empType=${encodeURIComponent(empType)}&category=${encodeURIComponent(category)}&fromDate=${encodeURIComponent(fromDate)}&toDate=${encodeURIComponent(toDate)}`;
  526. url += `&zone=${encodeURIComponent(zoneVal)}`;
  527. if (parkId) {
  528. url += `&parkId=${encodeURIComponent(parkId)}`;
  529. }
  530. const res = await fetch(url);
  531. if (!res.ok) {
  532. const text = await res.text().catch(() => '');
  533. modalBody.innerHTML = `<div class="alert alert-danger">Error ${res.status}: ${text}</div>`;
  534. return;
  535. }
  536. const data = await res.json();
  537. if (!Array.isArray(data)) { modalBody.innerText = JSON.stringify(data); return; }
  538. if (data.length === 0) { modalBody.innerHTML = '<div class="alert alert-info">No records</div>'; return; }
  539. // Define columns to display based on category
  540. let displayColumns = [];
  541. const catLower = (category || 'present').toLowerCase();
  542. if (catLower === 'present') {
  543. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location'];
  544. if (empType === 'Park') displayColumns.push('park_name');
  545. displayColumns.push('intime', 'inphoto', 'outtime', 'outphoto');
  546. } else if (catLower === 'leave') {
  547. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location'];
  548. if (empType === 'Park') displayColumns.push('park_name');
  549. displayColumns.push('leavedatetime', 'leaveby');
  550. } else if (catLower === 'od') {
  551. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'App Version', 'App Type', 'Location'];
  552. if (empType === 'Park') displayColumns.push('park_name');
  553. displayColumns.push('odtime', 'odby');
  554. } else if (catLower === 'notmarked') {
  555. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno', 'image'];
  556. // if (empType === 'Park') displayColumns.push('park_name');
  557. } else if (catLower === 'notregistered') {
  558. displayColumns = ['enrollment_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno'];
  559. // if (empType === 'Park') displayColumns.push('park_name');
  560. } else if (catLower === 'supervisor') {
  561. displayColumns = ['supervisor_id', 'name', 'designation', 'department', 'incharge_name', 'incharge_phoneno'];
  562. } else {
  563. displayColumns = Object.keys(data[0]);
  564. }
  565. // Filter data to include only displayColumns, handle case-insensitive matching
  566. const filteredData = data.map(row => {
  567. const filtered = {};
  568. displayColumns.forEach(col => {
  569. // Try exact match first, then case-insensitive
  570. const key = Object.keys(row).find(k => k === col || k.toLowerCase() === col.toLowerCase());
  571. if (key) {
  572. filtered[col] = row[key];
  573. }
  574. });
  575. return filtered;
  576. });
  577. function prettyName(key) {
  578. const map = {
  579. enrollment_id: 'Enrollment ID',
  580. name: 'Name',
  581. designation: 'Designation',
  582. department: 'Department',
  583. park_name: 'Park Name',
  584. intime: 'In Time',
  585. inphoto: 'In Photo',
  586. outtime: 'Out Time',
  587. outphoto: 'Out Photo',
  588. leavedatetime: 'Leave DateTime',
  589. leaveby: 'Leave By',
  590. odtime: 'OD DateTime',
  591. odby: 'OD By',
  592. incharge_name: 'Incharge Name',
  593. incharge_phoneno: 'Incharge Contact',
  594. appversion: 'App Version',
  595. apptype: 'App Type',
  596. location: 'Location',
  597. image: 'Image',
  598. supervisor_id: 'Supervisor ID',
  599. };
  600. if (map[key]) return map[key];
  601. return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
  602. }
  603. let html = '<div class="table-responsive"><table class="table table-sm table-bordered"><thead><tr>';
  604. // add serial number header
  605. html += '<th>S.No</th>';
  606. displayColumns.forEach(k => {
  607. html += `<th>${prettyName(k)}</th>`;
  608. });
  609. html += '</tr></thead><tbody>';
  610. filteredData.forEach((row, idx) => {
  611. html += '<tr>';
  612. // serial number cell
  613. html += `<td>${idx + 1}</td>`;
  614. displayColumns.forEach(k => {
  615. const val = row[k];
  616. // Handle photo columns - show as thumbnail if it's an image
  617. // if ((k === 'inphoto' || k === 'outphoto') && val) {
  618. // 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>`;
  619. // } else {
  620. // html += `<td>${val != null ? val : '-'}</td>`;
  621. // }
  622. if ((k === 'inphoto' || k === 'outphoto' || k === 'image') && val) {
  623. html += `<td>
  624. <img src="${val}" alt="photo"
  625. style="max-width: 100px; max-height: 100px; cursor: pointer;"
  626. onclick="showImageModal('${val}')">
  627. </td>`;
  628. } else {
  629. html += `<td>${val != null ? val : '-'}</td>`;
  630. }
  631. });
  632. html += '</tr>';
  633. });
  634. html += '</tbody></table></div>';
  635. modalBody.innerHTML = html;
  636. } catch (e) {
  637. modalBody.innerHTML = `<div class="alert alert-danger">Error: ${e}</div>`;
  638. }
  639. }
  640. document.addEventListener('DOMContentLoaded', () => {
  641. setDefaults();
  642. document.getElementById('loadSummaryBtn').addEventListener('click', () => {
  643. const rawFrom = document.getElementById('fromDate').value;
  644. const rawTo = document.getElementById('toDate').value;
  645. if (rawFrom && rawTo && rawTo < rawFrom) {
  646. alert('To date must be the same as or later than From date');
  647. return;
  648. }
  649. loadAbstractCounts(); loadZoneWisePercentage(); loadZoneSummary();
  650. });
  651. // delegate clicks: zone links -> park-wise summary, zone counts -> detailed, park counts -> park-details
  652. document.addEventListener('click', (ev) => {
  653. const zoneLink = ev.target.closest && ev.target.closest('.zone-link');
  654. if (zoneLink) {
  655. ev.preventDefault();
  656. const zone = zoneLink.getAttribute('data-zone');
  657. loadParkWiseSummary(zone);
  658. return;
  659. }
  660. const zoneAnchor = ev.target.closest && ev.target.closest('.zone-count');
  661. if (zoneAnchor) {
  662. ev.preventDefault();
  663. const zone = zoneAnchor.getAttribute('data-zone');
  664. const empType = zoneAnchor.getAttribute('data-emptype');
  665. const category = zoneAnchor.getAttribute('data-category');
  666. // Supervisor: show employee details directly (no park-wise drill-down)
  667. if (category === 'supervisor') {
  668. loadDetailed(empType, category, zone);
  669. } else if (empType === 'Park') {
  670. loadParkWiseSummary(zone, category);
  671. }
  672. // NULM branch commented out
  673. // else {
  674. // loadDetailed(empType, category, zone);
  675. // }
  676. return;
  677. }
  678. const parkAnchor = ev.target.closest && ev.target.closest('.park-count');
  679. if (parkAnchor) {
  680. ev.preventDefault();
  681. const zone = parkAnchor.getAttribute('data-zone');
  682. const type = parkAnchor.getAttribute('data-type');
  683. loadParkDetails(zone, type);
  684. return;
  685. }
  686. const parkEmployeeAnchor = ev.target.closest && ev.target.closest('.park-employee-count');
  687. if (parkEmployeeAnchor) {
  688. ev.preventDefault();
  689. const zone = parkEmployeeAnchor.getAttribute('data-zone');
  690. const parkId = parkEmployeeAnchor.getAttribute('data-parkid');
  691. const parkName = parkEmployeeAnchor.getAttribute('data-parkname');
  692. const category = parkEmployeeAnchor.getAttribute('data-category');
  693. loadDetailed('Park', category, zone, parkId, parkName);
  694. return;
  695. }
  696. });
  697. // set date constraints and load initial
  698. setDateLimits();
  699. // load initial
  700. loadAbstractCounts();
  701. loadZoneWisePercentage();
  702. loadZoneSummary();
  703. });
  704. })();
  705. </script>
  706. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
  707. <!-- Park-wise Summary Modal -->
  708. <div class="modal fade" id="parkWiseSummaryModal" tabindex="-1" aria-labelledby="parkWiseSummaryModalLabel"
  709. aria-hidden="true">
  710. <!-- <div class="modal-dialog modal-lg modal-dialog-scrollable"> -->
  711. <div class="modal-dialog modal-xl modal-dialog-scrollable">
  712. <div class="modal-content">
  713. <div class="modal-header">
  714. <h5 class="modal-title" id="parkWiseSummaryModalLabel">Park-wise Summary</h5>
  715. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  716. </div>
  717. <div class="modal-body" id="parkWiseSummaryModalBody">
  718. Loading...
  719. </div>
  720. <div class="modal-footer">
  721. <button type="button" class="btn btn-primary"
  722. onclick="downloadExcelFromTable(document.getElementById('parkWiseSummaryModal'), 'park_wise_summary.xlsx')">
  723. <i class="bi bi-download"></i> Download Excel
  724. </button>
  725. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  726. </div>
  727. </div>
  728. </div>
  729. </div>
  730. <!-- Detail modal -->
  731. <div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
  732. <div class="modal-dialog modal-xl modal-dialog-scrollable">
  733. <!-- <div class="modal-dialog modal-fullscreen modal-dialog-scrollable"> -->
  734. <div class="modal-content">
  735. <div class="modal-header">
  736. <h5 class="modal-title" id="detailModalLabel">Details</h5>
  737. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  738. </div>
  739. <div class="modal-body" id="detailModalBody">
  740. Loading...
  741. </div>
  742. <div class="modal-footer">
  743. <button type="button" class="btn btn-primary"
  744. onclick="downloadExcelFromTable(document.getElementById('detailModal'), 'employee_details.xlsx')">
  745. <i class="bi bi-download"></i> Download Excel
  746. </button>
  747. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  748. </div>
  749. </div>
  750. </div>
  751. </div>
  752. <!-- Image Preview Modal -->
  753. <div class="modal fade" id="imagePreviewModal" tabindex="-1" aria-hidden="true">
  754. <div class="modal-dialog modal-dialog-centered modal-lg">
  755. <div class="modal-content">
  756. <div class="modal-header">
  757. <h5 class="modal-title">Photo Preview</h5>
  758. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  759. </div>
  760. <div class="modal-body text-center">
  761. <img id="previewImage" src="" class="img-fluid" style="max-height: 500px;">
  762. </div>
  763. </div>
  764. </div>
  765. </div>
  766. </body>
  767. </html>