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.

1166 lines
61 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head th:replace="~{fragments/head-officer :: head}"></head>
  4. <meta charset="UTF-8">
  5. <style>
  6. /* ── Multiselect ─────────────────────────────────────── */
  7. .multiselect {
  8. text-wrap: balance;
  9. }
  10. .selectBox {
  11. position: relative;
  12. }
  13. .selectBox select {
  14. width: 100%;
  15. }
  16. .overSelect {
  17. position: absolute;
  18. left: 0;
  19. right: 0;
  20. top: 0;
  21. bottom: 0;
  22. }
  23. .checkboxes {
  24. display: none;
  25. border: 0.5px #7c7c7c solid;
  26. background-color: #fff;
  27. max-height: 200px;
  28. overflow-y: auto;
  29. z-index: 9999;
  30. position: relative;
  31. }
  32. .checkboxes label {
  33. display: block;
  34. font-weight: normal;
  35. white-space: nowrap;
  36. min-height: 1.2em;
  37. padding: 0 2.25rem 0 .75rem;
  38. cursor: pointer;
  39. }
  40. .checkboxes label:hover {
  41. background-color: #1e90ff;
  42. color: #fff;
  43. }
  44. .multiselect .selectBox {
  45. min-width: 150px;
  46. max-width: 100%;
  47. overflow: hidden;
  48. border: 0.5px #7c7c7c solid;
  49. }
  50. .multiselect .selectBox select {
  51. height: auto;
  52. overflow: hidden;
  53. }
  54. /* ── Step badge ──────────────────────────────────────── */
  55. .step-badge {
  56. display: inline-block;
  57. width: 28px;
  58. height: 28px;
  59. line-height: 28px;
  60. border-radius: 50%;
  61. background: #0d6efd;
  62. color: #fff;
  63. font-weight: 700;
  64. font-size: 13px;
  65. text-align: center;
  66. margin-right: 6px;
  67. }
  68. /* ── Visibility helpers ──────────────────────────────── */
  69. #step2Section {
  70. display: none;
  71. }
  72. #blockActionCard {
  73. display: none;
  74. }
  75. #noConflictCard {
  76. display: none;
  77. }
  78. /* ── Conflict pill ───────────────────────────────────── */
  79. .conflict-pill {
  80. display: inline-block;
  81. border-radius: 20px;
  82. padding: 3px 10px;
  83. font-size: 12px;
  84. font-weight: 600;
  85. }
  86. .pill-blocked {
  87. background: #fde8e8;
  88. color: #dc3545;
  89. border: 1px solid #dc3545;
  90. }
  91. .pill-free {
  92. background: #e8f5e9;
  93. color: #198754;
  94. border: 1px solid #198754;
  95. }
  96. /* ── Decision card ───────────────────────────────────── */
  97. .decision-card {
  98. border: 2px solid #0d6efd;
  99. border-radius: 8px;
  100. padding: 16px 20px;
  101. background: #f0f6ff;
  102. }
  103. .btn-decision {
  104. min-width: 230px;
  105. }
  106. /* ── File hint ───────────────────────────────────────── */
  107. .file-hint {
  108. font-size: 11px;
  109. color: #6c757d;
  110. margin-top: 2px;
  111. }
  112. .req {
  113. color: #dc3545;
  114. }
  115. </style>
  116. <body>
  117. <div id="page-container"
  118. class="sidebar-o sidebar-dark enable-page-overlay side-scroll page-header-fixed page-header-dark">
  119. <!-- <aside th:replace="~{fragments/aside-right :: aside}"></aside> -->
  120. <nav th:replace="~{fragments/nav-officer :: nav}"></nav>
  121. <header th:replace="~{fragments/header-officer :: header}"></header>
  122. <main id="main-container">
  123. <div class="content">
  124. <!-- ═══════════════════════════════════════════════════════
  125. PANEL A – Currently Blocked Halls (loads on page open)
  126. ══════════════════════════════════════════════════════════ -->
  127. <div class="block block-rounded" id="blockedListSection">
  128. <div class="block-header block-header-default d-flex justify-content-between align-items-center">
  129. <h3 class="block-title mb-0">
  130. <i class="fa fa-lock text-danger me-2"></i>Currently Blocked Halls
  131. </h3>
  132. <button class="btn btn-sm btn-outline-secondary" onclick="loadCurrentlyBlockedHalls()">
  133. <i class="fa fa-refresh me-1"></i>Refresh
  134. </button>
  135. </div>
  136. <div class="block-content">
  137. <div id="blockedListMsg" class="text-muted fst-italic mb-2">Loading…</div>
  138. <div class="table-responsive">
  139. <table class="table table-bordered table-striped table-vcenter" id="blockedHallsTable"
  140. style="display:none;">
  141. <thead class="thead-light">
  142. <tr>
  143. <th class="text-center">#</th>
  144. <th class="text-center">Hall Name</th>
  145. <!-- <th class="text-center">Zone</th> -->
  146. <th class="text-center">Blocked From</th>
  147. <th class="text-center">Blocked To</th>
  148. <th class="text-center">Reason</th>
  149. <th class="text-center">Status</th>
  150. <th class="text-center">Action</th>
  151. </tr>
  152. </thead>
  153. <tbody id="blockedHallsBody"></tbody>
  154. </table>
  155. </div>
  156. </div>
  157. </div>
  158. <!-- ═══════════════════════════════════════════════════════
  159. STEP 1 – New Blocking Form
  160. ══════════════════════════════════════════════════════════ -->
  161. <div class="block block-rounded">
  162. <div class="block-header block-header-default">
  163. <h3 class="block-title">
  164. <span class="step-badge">1</span>
  165. New GCC Hall Blocking – Selection
  166. </h3>
  167. </div>
  168. <div class="block-content">
  169. <div class="row g-3">
  170. <!-- Zone -->
  171. <div class="col-md-6">
  172. <label class="fw-semibold">Zone <span class="req">*</span></label>
  173. <div id="zoneMultiselect" class="multiselect">
  174. <div id="zoneSelectLabel" class="selectBox"
  175. onclick="toggleDropdown('zoneSelectOptions')">
  176. <select class="form-select">
  177. <option>Select Zones</option>
  178. </select>
  179. <div class="overSelect"></div>
  180. </div>
  181. <div id="zoneSelectOptions" class="checkboxes"></div>
  182. </div>
  183. <!-- Shown only when officer has exactly 1 hall (prefilled, readonly) -->
  184. <input type="text" id="zonePrefilled" class="form-control" readonly
  185. style="display:none; background:#f8f9fa; cursor:default;">
  186. </div>
  187. <!-- Hall -->
  188. <div class="col-md-6">
  189. <label class="fw-semibold">Hall(s) <span class="req">*</span></label>
  190. <div id="hallMultiselect" class="multiselect">
  191. <div id="hallSelectLabel" class="selectBox"
  192. onclick="toggleDropdown('hallSelectOptions')">
  193. <select class="form-select" id="hallSelect">
  194. <option>Select Halls</option>
  195. </select>
  196. <div class="overSelect"></div>
  197. </div>
  198. <div id="hallSelectOptions" class="checkboxes"></div>
  199. </div>
  200. <!-- Shown only when officer has exactly 1 hall (prefilled, readonly) -->
  201. <input type="text" id="hallPrefilled" class="form-control" readonly
  202. style="display:none; background:#f8f9fa; cursor:default;">
  203. </div>
  204. <!-- From Date -->
  205. <div class="col-md-3">
  206. <label class="fw-semibold">From Date <span class="req">*</span></label>
  207. <input type="date" class="form-control" id="fromDate" onkeydown="return false;">
  208. </div>
  209. <!-- To Date -->
  210. <div class="col-md-3">
  211. <label class="fw-semibold">To Date <span class="req">*</span></label>
  212. <input type="date" class="form-control" id="toDate" onkeydown="return false;">
  213. </div>
  214. <!-- Reason -->
  215. <div class="col-md-6">
  216. <label class="fw-semibold">Reason for Blocking <span class="req">*</span></label>
  217. <input type="text" class="form-control" id="reasonforBlocking"
  218. placeholder="Enter reason for blocking" maxlength="300">
  219. </div>
  220. <!-- Document -->
  221. <div class="col-md-6">
  222. <label class="fw-semibold">
  223. Upload Document <span class="req">*</span>
  224. <small class="text-muted">(PDF only, max 2 MB)</small>
  225. </label>
  226. <input type="file" class="form-control" id="uploadDocument" name="document"
  227. accept=".pdf">
  228. <div class="file-hint">Only PDF files up to 2 MB are accepted.</div>
  229. </div>
  230. <!-- Document Description -->
  231. <div class="col-md-6">
  232. <label class="fw-semibold">Document Description</label>
  233. <input type="text" class="form-control" id="documentDescription"
  234. placeholder="Optional description">
  235. </div>
  236. </div><!-- /row -->
  237. <!-- Per-hall conflict alert -->
  238. <div id="conflictAlertBox" class="mt-3" style="display:none;">
  239. <div class="alert alert-danger">
  240. <strong><i class="fa fa-exclamation-circle me-1"></i>Blocking Conflict Detected</strong>
  241. <div id="conflictDetails" class="mt-2"></div>
  242. <hr class="my-2">
  243. <p class="mb-0 text-danger fw-semibold">
  244. Blocking cannot proceed because one or more halls are already blocked for the
  245. selected period.
  246. Please choose different dates or unblock the existing entry first.
  247. </p>
  248. </div>
  249. </div>
  250. <div class="row mt-3">
  251. <div class="col text-center">
  252. <button type="button" class="btn btn-primary px-5" id="viewButton"
  253. onclick="loadBookings()">
  254. <i class="fa fa-search me-1"></i> View Bookings &amp; Check Availability
  255. </button>
  256. </div>
  257. </div>
  258. </div>
  259. </div>
  260. <!-- ═══════════════════════════════════════════════════════
  261. STEP 2 – Bookings Preview + Decision
  262. ══════════════════════════════════════════════════════════ -->
  263. <div id="step2Section">
  264. <!-- Booking Table -->
  265. <div class="block block-rounded">
  266. <div class="block-header block-header-default">
  267. <h3 class="block-title">
  268. <span class="step-badge">2</span>
  269. Existing Bookings in Selected Date Range
  270. </h3>
  271. </div>
  272. <div class="block-content">
  273. <div id="noBookingsMsg" class="alert alert-success" style="display:none;">
  274. <i class="fa fa-check-circle me-1"></i>
  275. No existing bookings found for the selected halls and dates.
  276. </div>
  277. <div id="bookingsFoundMsg" class="alert alert-warning" style="display:none;">
  278. <i class="fa fa-exclamation-triangle me-1"></i>
  279. <strong>Attention:</strong> The following bookings exist within the selected date range.
  280. Choose how to proceed below.
  281. </div>
  282. <!-- Today-booking warning -->
  283. <div id="todayBookingWarning" class="alert alert-danger mt-2" style="display:none;">
  284. <i class="fa fa-exclamation-circle me-1"></i>
  285. <strong>Note:</strong> One or more bookings are for <strong>today's date</strong> and
  286. <strong>cannot be cancelled</strong>.
  287. Use <em>Block Without Cancelling Bookings</em> to block dates without affecting any
  288. bookings.
  289. </div>
  290. <div class="table-responsive" id="bookingTableWrapper" style="display:none;">
  291. <table class="table table-bordered table-striped table-vcenter">
  292. <thead class="thead-light">
  293. <tr>
  294. <th class="text-center">
  295. <input type="checkbox" id="selectAllBookings"
  296. title="Select / Deselect all cancellable bookings">
  297. &nbsp;Select All
  298. </th>
  299. <th class="text-center">#</th>
  300. <th class="text-center">Hall Name</th>
  301. <th class="text-center">Zone</th>
  302. <th class="text-center">Division</th>
  303. <th class="text-center">Reason</th>
  304. <th class="text-center">Days</th>
  305. <th class="text-center">Amount (&#x20B9;)</th>
  306. <th class="text-center">From</th>
  307. <th class="text-center">To</th>
  308. <th class="text-center">Status</th>
  309. </tr>
  310. </thead>
  311. <tbody id="bookingTableBody"></tbody>
  312. </table>
  313. </div>
  314. </div>
  315. </div>
  316. <!-- Decision (bookings exist) -->
  317. <div class="block block-rounded" id="blockActionCard">
  318. <div class="block-header block-header-default">
  319. <h3 class="block-title">
  320. <span class="step-badge">3</span> Choose Blocking Action
  321. </h3>
  322. </div>
  323. <div class="block-content">
  324. <div class="decision-card">
  325. <p class="mb-3 fw-semibold">How would you like to proceed?</p>
  326. <div class="d-flex flex-wrap gap-3">
  327. <button type="button" class="btn btn-danger btn-decision" id="cancelAndBlockBtn"
  328. onclick="proceedCancelAndBlock()">
  329. <i class="fa fa-ban me-2"></i>Cancel Bookings &amp; Block Dates
  330. </button>
  331. <button type="button" class="btn btn-warning btn-decision text-dark"
  332. onclick="proceedBlockOnly()">
  333. <i class="fa fa-lock me-2"></i>Block Without Cancelling Bookings
  334. </button>
  335. </div>
  336. <p class="mt-3 mb-0 text-muted small">
  337. <strong>Cancel &amp; Block</strong> — marks existing bookings as GCC-blocked and
  338. initiates refund.&nbsp;|&nbsp;
  339. <strong>Block Only</strong> — marks dates as blocked; existing bookings remain
  340. untouched.
  341. </p>
  342. </div>
  343. </div>
  344. </div>
  345. <!-- Confirm (no bookings) -->
  346. <div class="block block-rounded" id="noConflictCard">
  347. <div class="block-content text-center py-3">
  348. <p class="text-muted mb-3">No booking conflicts found. You can safely block the selected
  349. dates.</p>
  350. <button type="button" class="btn btn-success px-5" onclick="proceedBlockOnly()">
  351. <i class="fa fa-check me-2"></i> Confirm &amp; Block Dates
  352. </button>
  353. </div>
  354. </div>
  355. </div><!-- /#step2Section -->
  356. </div><!-- /content -->
  357. </main>
  358. <div th:replace="~{fragments/resetpassword :: div}"></div>
  359. <footer th:replace="~{fragments/footer2 :: footer}"></footer>
  360. </div>
  361. <script th:replace="~{fragments/loginscript :: script}"></script>
  362. <script th:inline="javascript">
  363. /* ═══════════════════════════════════════════════════════════════
  364. CONSTANTS (Officer namespace)
  365. ═══════════════════════════════════════════════════════════════ */
  366. const ADMIN_BASE = '/communityhall/api/officer';
  367. const CANCEL_ALL_URL = ADMIN_BASE + '/cancelAll';
  368. const BLOCK_ONLY_URL = ADMIN_BASE + '/blockWithoutCancel';
  369. const FILTER_URL = ADMIN_BASE + '/filter';
  370. const UNBLOCK_URL = ADMIN_BASE + '/cancelByid';
  371. const BLOCKED_URL = ADMIN_BASE + '/blockedevents';
  372. const ZONE_URL = '/communityhall/user/api/getZone';
  373. const HALLS_URL = '/communityhall/user/api/hallsbyzone';
  374. // No zones param → returns all halls (reuses same endpoint)
  375. const ALL_HALLS_URL = '/communityhall/user/api/hallsbyzone';
  376. const MAX_FILE_BYTES = 2 * 1024 * 1024;
  377. /* ── Officer's allocated hall IDs & zone (injected from session via Thymeleaf) ── */
  378. const OFFICER_HALL_IDS = new Set(
  379. /*[[${hallIds}]]*/[]
  380. );
  381. const OFFICER_ZONE = /*[[${zone}]]*/ null;
  382. /* ═══════════════════════════════════════════════════════════════
  383. PAGE INIT
  384. ═══════════════════════════════════════════════════════════════ */
  385. document.addEventListener('DOMContentLoaded', function () {
  386. const today = new Date();
  387. const pad = n => String(n).padStart(2, '0');
  388. const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
  389. document.getElementById('fromDate').min = todayStr;
  390. document.getElementById('toDate').min = todayStr;
  391. document.getElementById('fromDate').addEventListener('change', function () {
  392. const toEl = document.getElementById('toDate');
  393. toEl.min = this.value;
  394. toEl.value = '';
  395. });
  396. document.getElementById('uploadDocument').addEventListener('change', function () {
  397. const file = this.files[0];
  398. if (!file) return;
  399. if (file.type !== 'application/pdf') {
  400. Swal.fire('Invalid File', 'Only PDF files are accepted.', 'error');
  401. this.value = ''; return;
  402. }
  403. if (file.size > MAX_FILE_BYTES) {
  404. Swal.fire('File Too Large', 'Document must be 2 MB or smaller.', 'error');
  405. this.value = '';
  406. }
  407. });
  408. window.addEventListener('click', function (e) {
  409. let t = e.target;
  410. while (t) {
  411. if (t === document.getElementById('zoneMultiselect') ||
  412. t === document.getElementById('hallMultiselect')) return;
  413. t = t.parentNode;
  414. }
  415. closeDropdown('zoneSelectOptions');
  416. closeDropdown('hallSelectOptions');
  417. });
  418. loadOfficerHalls();
  419. loadCurrentlyBlockedHalls();
  420. });
  421. /* ═══════════════════════════════════════════════════════════════
  422. LOAD CURRENTLY BLOCKED HALLS
  423. ═══════════════════════════════════════════════════════════════ */
  424. /* ── Format a java.util.Date JSON value (epoch ms or ISO string)
  425. to a yyyy-MM-dd string using LOCAL date (no timezone shift) ── */
  426. function fmtDate(val) {
  427. if (!val) return '-';
  428. var d = new Date(val);
  429. if (isNaN(d.getTime())) return String(val).substring(0, 10);
  430. var y = d.getFullYear();
  431. var m = String(d.getMonth() + 1).padStart(2, '0');
  432. var day = String(d.getDate()).padStart(2, '0');
  433. return y + '-' + m + '-' + day;
  434. }
  435. function loadCurrentlyBlockedHalls() {
  436. $('#blockedListMsg').text('Loading currently blocked halls…').show();
  437. $('#blockedHallsTable').hide();
  438. const today = new Date();
  439. // Use 5 years in the PAST as fromDate so blocks that started before
  440. // today are still returned by the backend and shown correctly.
  441. const past = new Date(today.getFullYear() - 5, 0, 1);
  442. const far = new Date(today.getFullYear() + 5, 11, 31);
  443. const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  444. // ── Fetch all halls, then scope to officer's allocated hall IDs ──
  445. $.get(ALL_HALLS_URL)
  446. .done(function (allHalls) {
  447. // Build hallMap for ALL halls (needed to resolve names from blocked entries)
  448. const hallMap = {};
  449. (allHalls || []).forEach(h => {
  450. const id = h.hallId || h.hall_id;
  451. const name = h.hallName || h.hall_name || ('Hall#' + id);
  452. hallMap[String(id)] = name;
  453. });
  454. // Filter to only officer's allocated halls
  455. const officerHalls = (allHalls || []).filter(h => {
  456. const id = h.hallId || h.hall_id;
  457. return OFFICER_HALL_IDS.size === 0 || OFFICER_HALL_IDS.has(id) || OFFICER_HALL_IDS.has(String(id));
  458. });
  459. if (!officerHalls.length) {
  460. $('#blockedListMsg').text('No allocated halls found.').show();
  461. return;
  462. }
  463. const officerHallIdSet = new Set(officerHalls.map(h => String(h.hallId || h.hall_id)));
  464. $.ajax({
  465. url: BLOCKED_URL,
  466. type: 'GET',
  467. success: function (blocked) {
  468. // Filter blocked entries to only those involving the officer's halls
  469. const list = (blocked || []).filter(function (item) {
  470. const ids = (item.hallId || '').split(',').map(s => s.trim()).filter(Boolean);
  471. return ids.some(id => officerHallIdSet.has(id));
  472. });
  473. if (!list.length) {
  474. $('#blockedListMsg').text('No halls are currently blocked.').show();
  475. $('#blockedHallsTable').hide();
  476. return;
  477. }
  478. $('#blockedListMsg').hide();
  479. const tbody = document.getElementById('blockedHallsBody');
  480. tbody.innerHTML = '';
  481. const seen = new Set();
  482. let rowNum = 0;
  483. list.forEach(function (item) {
  484. if (seen.has(item.blockingId)) return;
  485. seen.add(item.blockingId);
  486. rowNum++;
  487. const hallIds = (item.hallId || '').split(',').map(s => s.trim()).filter(Boolean);
  488. const hallNames = hallIds.map(id => hallMap[id] || ('Hall#' + id)).join(', ');
  489. // Use fmtDate() to correctly parse java.util.Date JSON values
  490. const startDate = fmtDate(item.startDate);
  491. const endDate = fmtDate(item.endDate);
  492. const tr = document.createElement('tr');
  493. tr.innerHTML = `
  494. <td class="text-center">${rowNum}</td>
  495. <td class="text-center">${escHtml(hallNames)}</td>
  496. <td class="text-center">${startDate}</td>
  497. <td class="text-center">${endDate}</td>
  498. <td class="text-center">${escHtml(item.blockingReason || '-')}</td>
  499. <td class="text-center"><span class="badge bg-danger">BLOCKED</span></td>
  500. <td class="text-center">
  501. <button class="btn btn-sm btn-outline-success"
  502. onclick="unblockEntry(${item.blockingId}, '${startDate}', '${endDate}')">
  503. <i class="fa fa-unlock me-1"></i>Unblock
  504. </button>
  505. </td>
  506. `;
  507. tbody.appendChild(tr);
  508. });
  509. // ── Init / reinit DataTable ──────────────────────────────────────
  510. if ($.fn.DataTable.isDataTable('#blockedHallsTable')) {
  511. $('#blockedHallsTable').DataTable().destroy();
  512. }
  513. $('#blockedHallsTable').show();
  514. $('#blockedHallsTable').DataTable({
  515. pageLength: 10,
  516. lengthChange: false,
  517. order: [],
  518. dom: 'Bfrtip',
  519. buttons: [
  520. {
  521. extend: 'excelHtml5',
  522. text: '<i class="fa fa-file-excel-o me-1"></i>Excel',
  523. className: 'btn btn-sm btn-success me-1',
  524. title: 'Currently Blocked Halls'
  525. },
  526. {
  527. extend: 'pdfHtml5',
  528. text: '<i class="fa fa-file-pdf-o me-1"></i>PDF',
  529. className: 'btn btn-sm btn-danger',
  530. title: 'Currently Blocked Halls',
  531. orientation: 'landscape',
  532. pageSize: 'A4'
  533. }
  534. ]
  535. });
  536. },
  537. error: function () { $('#blockedListMsg').text('Failed to load blocked halls. Please refresh.'); }
  538. });
  539. })
  540. .fail(function () { $('#blockedListMsg').text('Failed to load hall list.'); });
  541. }
  542. /* ═══════════════════════════════════════════════════════════════
  543. UNBLOCK
  544. ═══════════════════════════════════════════════════════════════ */
  545. function unblockEntry(blockingId, fromDate, endDate) {
  546. Swal.fire({
  547. title: 'Unblock these dates?',
  548. html: `This will unblock <strong>${fromDate}</strong> to <strong>${endDate}</strong> (ID: ${blockingId}).<br>
  549. Existing bookings will be restored to active status.`,
  550. icon: 'question',
  551. showCancelButton: true,
  552. confirmButtonColor: '#198754',
  553. cancelButtonColor: '#6c757d',
  554. confirmButtonText: '<i class="fa fa-unlock me-1"></i> Yes, Unblock',
  555. cancelButtonText: 'Cancel'
  556. }).then(result => {
  557. if (!result.isConfirmed) return;
  558. Swal.fire({ title: 'Processing…', allowOutsideClick: false, didOpen: () => Swal.showLoading() });
  559. $.ajax({
  560. url: UNBLOCK_URL, type: 'GET',
  561. data: { blockingId: blockingId, fromDate: fromDate, endDate: endDate },
  562. success: function () {
  563. Swal.fire({ icon: 'success', title: 'Unblocked', text: 'The halls have been unblocked successfully.' })
  564. .then(() => {
  565. location.reload();
  566. });
  567. },
  568. error: function (xhr) {
  569. Swal.fire({
  570. icon: 'error', title: 'Unblock Failed',
  571. text: xhr.responseText || 'An error occurred.'
  572. });
  573. }
  574. });
  575. });
  576. }
  577. /* ═══════════════════════════════════════════════════════════════
  578. HELPERS
  579. ═══════════════════════════════════════════════════════════════ */
  580. function getSelectedZones() {
  581. return Array.from(document.querySelectorAll('#zoneSelectOptions input:checked'))
  582. .map(cb => cb.value).filter(v => v && v !== 'All Zones');
  583. }
  584. function getSelectedHalls() {
  585. return Array.from(document.querySelectorAll('#hallSelectOptions input:checked'))
  586. .map(cb => ({ id: cb.value, name: cb.dataset.name || cb.value }))
  587. .filter(h => !isNaN(h.id) && h.id !== '');
  588. }
  589. function validateForm() {
  590. const zones = getSelectedZones();
  591. const halls = getSelectedHalls();
  592. const from = document.getElementById('fromDate').value;
  593. const to = document.getElementById('toDate').value;
  594. const reason = document.getElementById('reasonforBlocking').value.trim();
  595. const fileEl = document.getElementById('uploadDocument');
  596. if (!zones.length) { Swal.fire('Validation', 'Please select at least one Zone.', 'error'); return false; }
  597. if (!halls.length) { Swal.fire('Validation', 'Please select at least one Hall.', 'error'); return false; }
  598. if (!from) { Swal.fire('Validation', 'From Date is required.', 'error'); return false; }
  599. if (!to) { Swal.fire('Validation', 'To Date is required.', 'error'); return false; }
  600. if (new Date(to) < new Date(from)) {
  601. Swal.fire('Validation', 'To Date cannot be earlier than From Date.', 'error'); return false;
  602. }
  603. if (!reason) { Swal.fire('Validation', 'Reason for Blocking is mandatory.', 'error'); return false; }
  604. if (!fileEl.files || !fileEl.files[0]) {
  605. Swal.fire('Validation', 'Please upload a supporting document (PDF, max 2 MB).', 'error'); return false;
  606. }
  607. const file = fileEl.files[0];
  608. if (file.type !== 'application/pdf') {
  609. Swal.fire('Invalid File', 'Only PDF files are allowed.', 'error'); fileEl.value = ''; return false;
  610. }
  611. if (file.size > MAX_FILE_BYTES) {
  612. Swal.fire('File Too Large', 'Document must be 2 MB or smaller.', 'error'); fileEl.value = ''; return false;
  613. }
  614. return true;
  615. }
  616. /* ═══════════════════════════════════════════════════════════════
  617. STEP 1 – VIEW BOOKINGS (per-hall conflict check)
  618. ═══════════════════════════════════════════════════════════════ */
  619. function loadBookings() {
  620. if (!validateForm()) return;
  621. $('#step2Section').hide();
  622. $('#conflictAlertBox').hide();
  623. $('#conflictDetails').html('');
  624. const halls = getSelectedHalls();
  625. const from = document.getElementById('fromDate').value;
  626. const to = document.getElementById('toDate').value;
  627. const hallIds = halls.map(h => h.id);
  628. $.ajax({
  629. url: FILTER_URL, type: 'GET',
  630. data: { hallIds: hallIds.join(','), fromDate: from, toDate: to },
  631. success: function (response) {
  632. const bookings = response.bookings || [];
  633. const blockedEvents = response.blockedEvents || [];
  634. if (blockedEvents && blockedEvents.length > 0) {
  635. // Build per-hall conflict list
  636. const conflictHallIds = new Set();
  637. blockedEvents.forEach(ev => {
  638. (ev.hallId || '').split(',').forEach(id => conflictHallIds.add(id.trim()));
  639. });
  640. let html = '<ul class="mb-0">';
  641. halls.forEach(function (h) {
  642. if (conflictHallIds.has(String(h.id))) {
  643. html += `<li>
  644. <span class="conflict-pill pill-blocked">&#128274; ${escHtml(h.name)}</span>
  645. is <strong>already blocked</strong> for <strong>${from}</strong> to <strong>${to}</strong>.
  646. Please unblock it first or choose different dates.
  647. </li>`;
  648. }
  649. });
  650. html += '</ul>';
  651. $('#conflictDetails').html(html);
  652. $('#conflictAlertBox').show();
  653. document.getElementById('conflictAlertBox').scrollIntoView({ behavior: 'smooth' });
  654. return; // stop – do not show blocking actions
  655. }
  656. renderBookingsTable(bookings);
  657. $('#step2Section').show();
  658. if (bookings.length === 0) {
  659. $('#noBookingsMsg').show();
  660. $('#bookingsFoundMsg').hide();
  661. $('#bookingTableWrapper').hide();
  662. $('#blockActionCard').hide();
  663. $('#noConflictCard').show();
  664. } else {
  665. $('#noBookingsMsg').hide();
  666. $('#bookingsFoundMsg').show();
  667. $('#bookingTableWrapper').show();
  668. $('#blockActionCard').show();
  669. $('#noConflictCard').hide();
  670. }
  671. document.getElementById('step2Section').scrollIntoView({ behavior: 'smooth' });
  672. },
  673. error: function (xhr) {
  674. console.error('Filter error:', xhr.responseText);
  675. Swal.fire('Error', 'Failed to check hall availability. Please try again.', 'error');
  676. }
  677. });
  678. }
  679. function renderBookingsTable(bookings) {
  680. const tbody = document.getElementById('bookingTableBody');
  681. tbody.innerHTML = '';
  682. $('#todayBookingWarning').hide();
  683. $('#selectAllBookings').prop('checked', true);
  684. const todayStr = (() => {
  685. const d = new Date();
  686. return d.getFullYear() + '-'
  687. + String(d.getMonth() + 1).padStart(2, '0') + '-'
  688. + String(d.getDate()).padStart(2, '0');
  689. })();
  690. let hasTodayBooking = false;
  691. let allToday = true;
  692. bookings.forEach(function (b, idx) {
  693. const bStart = (b.startDate || '').slice(0, 10);
  694. const isToday = (bStart === todayStr);
  695. if (isToday) hasTodayBooking = true;
  696. else allToday = false;
  697. const cbDisabled = isToday ? 'disabled title="Cannot cancel today&#39;s active booking"' : '';
  698. const cbChecked = isToday ? '' : 'checked';
  699. const statusBadge = isToday
  700. ? '<span class="badge bg-warning text-dark">Today – Cannot Cancel</span>'
  701. : '<span class="badge bg-primary">User Booked</span>';
  702. const tr = document.createElement('tr');
  703. tr.setAttribute('data-booking-id', b.bookingId || '');
  704. tr.innerHTML = `
  705. <td class="text-center">
  706. <input type="checkbox" class="booking-cb" data-booking-id="${b.bookingId || ''}"
  707. ${cbChecked} ${cbDisabled}>
  708. </td>
  709. <td class="text-center">${idx + 1}</td>
  710. <td class="text-center">${escHtml(b.hallDetails ? b.hallDetails.hallName : '-')}</td>
  711. <td class="text-center">${escHtml(b.hallDetails ? b.hallDetails.zone : '-')}</td>
  712. <td class="text-center">${escHtml(b.hallDetails ? b.hallDetails.division : '-')}</td>
  713. <td class="text-center">${escHtml(b.reason || '-')}</td>
  714. <td class="text-center">${b.noOfDays || '-'}</td>
  715. <td class="text-center">${b.hallAmount || '-'}</td>
  716. <td class="text-center">${escHtml(b.startDate || '-')}</td>
  717. <td class="text-center">${escHtml(b.endDate || '-')}</td>
  718. <td class="text-center">${statusBadge}</td>
  719. `;
  720. tbody.appendChild(tr);
  721. });
  722. // Wire select-all checkbox
  723. const selAll = document.getElementById('selectAllBookings');
  724. if (selAll) {
  725. // Select All → toggle every enabled row checkbox
  726. selAll.onchange = function () {
  727. document.querySelectorAll('.booking-cb:not(:disabled)')
  728. .forEach(cb => { cb.checked = selAll.checked; });
  729. };
  730. // Individual row checkboxes → sync Select All state
  731. document.querySelectorAll('.booking-cb:not(:disabled)').forEach(cb => {
  732. cb.addEventListener('change', function () {
  733. const total = document.querySelectorAll('.booking-cb:not(:disabled)').length;
  734. const checked = document.querySelectorAll('.booking-cb:not(:disabled):checked').length;
  735. selAll.checked = (total > 0 && checked === total);
  736. });
  737. });
  738. }
  739. if (hasTodayBooking) {
  740. $('#todayBookingWarning').show();
  741. }
  742. // Disable Cancel & Block button if ALL bookings are today's
  743. if (allToday && bookings.length > 0) {
  744. $('#cancelAndBlockBtn')
  745. .prop('disabled', true)
  746. .attr('title', 'Cannot cancel — all conflicting bookings are for today. Use Block Without Cancelling instead.');
  747. } else {
  748. $('#cancelAndBlockBtn').prop('disabled', false).removeAttr('title');
  749. }
  750. }
  751. /* ═══════════════════════════════════════════════════════════════
  752. STEP 3A – Cancel bookings THEN block
  753. ═══════════════════════════════════════════════════════════════ */
  754. function proceedCancelAndBlock() {
  755. // Collect enabled + checked booking IDs
  756. const selectedIds = [];
  757. document.querySelectorAll('.booking-cb:not(:disabled):checked')
  758. .forEach(cb => { if (cb.dataset.bookingId) selectedIds.push(cb.dataset.bookingId); });
  759. if (selectedIds.length === 0) {
  760. Swal.fire({
  761. icon: 'warning',
  762. title: 'No Bookings Selected',
  763. html: 'Please tick at least one booking to cancel, or use <em>Block Without Cancelling Bookings</em>.'
  764. });
  765. return;
  766. }
  767. const todayCount = document.querySelectorAll('.booking-cb:disabled').length;
  768. const extraNote = todayCount > 0
  769. ? `<br><small class="text-warning">&#9888; ${todayCount} today's booking(s) will be skipped and remain active.</small>`
  770. : '';
  771. Swal.fire({
  772. title: 'Cancel Bookings & Block?',
  773. html: `This will <strong>cancel ${selectedIds.length} selected booking(s)</strong> and block the halls.<br><br>
  774. <span class="text-danger fw-bold">This action cannot be undone.</span>${extraNote}`,
  775. icon: 'warning',
  776. showCancelButton: true,
  777. confirmButtonColor: '#dc3545',
  778. cancelButtonColor: '#6c757d',
  779. confirmButtonText: '<i class="fa fa-ban me-1"></i> Yes, Cancel & Block',
  780. cancelButtonText: 'Go Back'
  781. }).then(result => {
  782. if (result.isConfirmed) submitBlockRequest(true);
  783. });
  784. }
  785. /* ═══════════════════════════════════════════════════════════════
  786. STEP 3B – Block ONLY (no booking cancellation)
  787. ═══════════════════════════════════════════════════════════════ */
  788. function proceedBlockOnly() {
  789. Swal.fire({
  790. title: 'Block Dates?',
  791. html: `The selected halls will be blocked for the chosen period.<br>
  792. <strong>Existing bookings will NOT be cancelled.</strong>`,
  793. icon: 'info',
  794. showCancelButton: true,
  795. confirmButtonColor: '#ffc107',
  796. cancelButtonColor: '#6c757d',
  797. confirmButtonText: '<i class="fa fa-lock me-1"></i> Yes, Block',
  798. cancelButtonText: 'Go Back'
  799. }).then(result => {
  800. if (result.isConfirmed) submitBlockOnlyRequest();
  801. });
  802. }
  803. /* ═══════════════════════════════════════════════════════════════
  804. SUBMIT (Cancel & Block)
  805. ═══════════════════════════════════════════════════════════════ */
  806. function submitBlockRequest(cancelExisting) {
  807. const halls = getSelectedHalls();
  808. const from = document.getElementById('fromDate').value;
  809. const to = document.getElementById('toDate').value;
  810. const reason = document.getElementById('reasonforBlocking').value.trim();
  811. const desc = document.getElementById('documentDescription').value.trim();
  812. const fileEl = document.getElementById('uploadDocument');
  813. let selectedBookings = [];
  814. if (cancelExisting) {
  815. // Only collect IDs from checked & enabled checkboxes (skips today's bookings)
  816. document.querySelectorAll('.booking-cb:not(:disabled):checked')
  817. .forEach(cb => { if (cb.dataset.bookingId) selectedBookings.push(cb.dataset.bookingId); });
  818. }
  819. console.log("selectedBookings...." + selectedBookings);
  820. const fd = new FormData();
  821. fd.append('fromDate', from);
  822. fd.append('toDate', to);
  823. fd.append('hallID', halls.map(h => h.id).join(','));
  824. fd.append('reasonforblocking', reason);
  825. if (desc) fd.append('description', desc);
  826. if (fileEl.files && fileEl.files[0]) fd.append('document', fileEl.files[0]);
  827. if (selectedBookings.length) fd.append('selectedBookings', selectedBookings.join(','));
  828. Swal.fire({ title: 'Processing…', allowOutsideClick: false, didOpen: () => Swal.showLoading() });
  829. $.ajax({
  830. url: CANCEL_ALL_URL, type: 'POST',
  831. data: fd, processData: false, contentType: false,
  832. success: function () {
  833. Swal.fire({
  834. icon: 'success', title: 'Success',
  835. text: 'Bookings have been cancelled and the dates are now blocked.'
  836. }).then(() => location.reload());
  837. },
  838. error: function (xhr) {
  839. Swal.fire({
  840. icon: 'error', title: 'Request Failed',
  841. text: 'Something went wrong. Please try again.'
  842. });
  843. }
  844. });
  845. }
  846. /* ═══════════════════════════════════════════════════════════════
  847. SUBMIT (Block Without Cancelling) — calls smart endpoint
  848. ═══════════════════════════════════════════════════════════════ */
  849. function submitBlockOnlyRequest() {
  850. const halls = getSelectedHalls();
  851. const from = document.getElementById('fromDate').value;
  852. const to = document.getElementById('toDate').value;
  853. const reason = document.getElementById('reasonforBlocking').value.trim();
  854. const desc = document.getElementById('documentDescription').value.trim();
  855. const fileEl = document.getElementById('uploadDocument');
  856. const fd = new FormData();
  857. fd.append('fromDate', from);
  858. fd.append('toDate', to);
  859. fd.append('hallID', halls.map(h => h.id).join(','));
  860. fd.append('reasonforblocking', reason);
  861. if (desc) fd.append('description', desc);
  862. if (fileEl.files && fileEl.files[0]) fd.append('document', fileEl.files[0]);
  863. Swal.fire({ title: 'Processing…', allowOutsideClick: false, didOpen: () => Swal.showLoading() });
  864. $.ajax({
  865. url: BLOCK_ONLY_URL, type: 'POST',
  866. data: fd, processData: false, contentType: false,
  867. success: function () {
  868. Swal.fire({
  869. icon: 'success', title: 'Success',
  870. text: 'The selected dates have been blocked. Existing bookings were not affected.'
  871. }).then(() => location.reload());
  872. },
  873. error: function (xhr) {
  874. Swal.fire({
  875. icon: 'error', title: 'Request Failed',
  876. text: 'Something went wrong. Please try again.'
  877. });
  878. }
  879. });
  880. }
  881. /* ═══════════════════════════════════════════════════════════════
  882. ZONE / HALL MULTISELECT – Officer variant
  883. Loads the zone and hall fields pre-filled with the officer's
  884. allocated zone (from session) and their assigned halls.
  885. Key facts from the backend:
  886. • OFFICER_ZONE – injected by Thymeleaf from session "zone"
  887. • OFFICER_HALL_IDS – injected by Thymeleaf from session "hall_ids"
  888. • HALLS_URL (/hallsbyzone?zones=X) returns { hallId, hallName }
  889. (getAllHalls SQL has NO zone column, so zone must come from session)
  890. ═══════════════════════════════════════════════════════════════ */
  891. function loadOfficerHalls() {
  892. if (!OFFICER_ZONE) {
  893. document.getElementById('zoneSelectOptions').innerHTML =
  894. '<label class="text-danger px-2">Zone not assigned to this account.</label>';
  895. return;
  896. }
  897. // Fetch halls for the officer's zone → returns { hallId, hallName }
  898. $.get(HALLS_URL, { zones: OFFICER_ZONE }).done(function (hallsInZone) {
  899. // Keep only the officer's allocated hall IDs
  900. const myHalls = (hallsInZone || []).filter(function (h) {
  901. const id = h.hallId || h.hall_id;
  902. return OFFICER_HALL_IDS.size === 0 ||
  903. OFFICER_HALL_IDS.has(id) ||
  904. OFFICER_HALL_IDS.has(Number(id)) ||
  905. OFFICER_HALL_IDS.has(String(id));
  906. });
  907. if (!myHalls.length) {
  908. document.getElementById('zoneSelectOptions').innerHTML =
  909. '<label class="text-danger px-2">No allocated halls found for zone: ' + OFFICER_ZONE + '</label>';
  910. return;
  911. }
  912. /* ── SINGLE HALL: show readonly prefilled inputs ── */
  913. if (myHalls.length === 1) {
  914. const hall = myHalls[0];
  915. const zone = OFFICER_ZONE;
  916. const hallName = (hall.hallName || hall.hall_name || '').trim().replace(/^, */, '');
  917. const hallId = String(hall.hallId || hall.hall_id);
  918. // Zone – hide multiselect, show readonly text input
  919. document.getElementById('zoneMultiselect').style.display = 'none';
  920. const zonePrefilled = document.getElementById('zonePrefilled');
  921. zonePrefilled.value = zone;
  922. zonePrefilled.style.display = '';
  923. // Inject hidden checked checkbox so getSelectedZones() works
  924. const zoneMenu = document.getElementById('zoneSelectOptions');
  925. zoneMenu.innerHTML = '';
  926. const zCb = document.createElement('input');
  927. zCb.type = 'checkbox'; zCb.value = zone; zCb.checked = true;
  928. zCb.style.display = 'none';
  929. zoneMenu.appendChild(zCb);
  930. // Hall – hide multiselect, show readonly text input
  931. document.getElementById('hallMultiselect').style.display = 'none';
  932. const hallPrefilled = document.getElementById('hallPrefilled');
  933. hallPrefilled.value = hallName;
  934. hallPrefilled.style.display = '';
  935. // Inject hidden checked checkbox so getSelectedHalls() works
  936. const hallMenu = document.getElementById('hallSelectOptions');
  937. hallMenu.innerHTML = '';
  938. const hCb = document.createElement('input');
  939. hCb.type = 'checkbox'; hCb.value = hallId; hCb.checked = true;
  940. hCb.dataset.name = hallName;
  941. hCb.style.display = 'none';
  942. hallMenu.appendChild(hCb);
  943. return; // done
  944. }
  945. /* ── MULTIPLE HALLS: populate dropdowns ── */
  946. // Zone dropdown – officer always belongs to one zone
  947. const zoneMenu = document.getElementById('zoneSelectOptions');
  948. zoneMenu.innerHTML = '';
  949. appendCheckbox(zoneMenu, 'zone_' + OFFICER_ZONE, OFFICER_ZONE, OFFICER_ZONE, null, function () {
  950. updateLabel('zoneSelectOptions', 'zoneSelectLabel');
  951. });
  952. const zoneCb = document.querySelector('#zoneSelectOptions input');
  953. if (zoneCb) { zoneCb.checked = true; }
  954. updateLabel('zoneSelectOptions', 'zoneSelectLabel');
  955. // Hall dropdown – all allocated halls
  956. populateHallDropdown(myHalls);
  957. }).fail(function () {
  958. document.getElementById('zoneSelectOptions').innerHTML =
  959. '<label class="text-danger px-2">Failed to load halls. Please refresh.</label>';
  960. });
  961. }
  962. function toggleAllZones(checked, myHalls) {
  963. document.querySelectorAll('#zoneSelectOptions input').forEach(cb => { cb.checked = checked; });
  964. loadHallsForSelectedZones(myHalls);
  965. updateLabel('zoneSelectOptions', 'zoneSelectLabel');
  966. }
  967. function loadHallsForSelectedZones(myHalls) {
  968. const selected = getSelectedZones();
  969. const menu = document.getElementById('hallSelectOptions');
  970. if (!selected.length) {
  971. // No zone selected — show all officer's halls
  972. populateHallDropdown(myHalls);
  973. return;
  974. }
  975. // Show only halls from selected zones
  976. const filtered = myHalls.filter(h => selected.includes(h.zone || h.zoneName));
  977. populateHallDropdown(filtered);
  978. }
  979. function populateHallDropdown(halls) {
  980. const menu = document.getElementById('hallSelectOptions');
  981. menu.innerHTML = '';
  982. if (!halls.length) return;
  983. appendCheckbox(menu, 'allHallsCb', 'All Halls', '', null,
  984. checked => toggleAllHalls(checked));
  985. halls.forEach(hall => {
  986. const name = (hall.hallName || '').trim().replace(/^, */, '');
  987. appendCheckbox(menu, `hall_${hall.hallId}`, name, hall.hallId, name, () => {
  988. updateLabel('hallSelectOptions', 'hallSelectLabel');
  989. });
  990. });
  991. }
  992. /* Legacy alias — kept for safety in case any other code calls getZone() */
  993. function getZone() { loadOfficerHalls(); }
  994. /* Legacy alias — kept for safety */
  995. function loadHallsbyzone() {
  996. $.get(ALL_HALLS_URL).done(function (allHalls) {
  997. const myHalls = (allHalls || []).filter(h => {
  998. const id = h.hallId || h.hall_id;
  999. return OFFICER_HALL_IDS.size === 0 || OFFICER_HALL_IDS.has(id) || OFFICER_HALL_IDS.has(String(id));
  1000. });
  1001. loadHallsForSelectedZones(myHalls);
  1002. });
  1003. }
  1004. function appendCheckbox(parent, id, text, value, dataName, onChange) {
  1005. const label = document.createElement('label');
  1006. const cb = document.createElement('input');
  1007. cb.type = 'checkbox'; cb.id = id; cb.value = value;
  1008. if (dataName) cb.dataset.name = dataName;
  1009. cb.addEventListener('change', function () { onChange(this.checked); });
  1010. label.textContent = text; label.prepend(cb);
  1011. parent.appendChild(label);
  1012. }
  1013. function toggleAllHalls(checked) {
  1014. document.querySelectorAll('#hallSelectOptions input').forEach(cb => { cb.checked = checked; });
  1015. updateLabel('hallSelectOptions', 'hallSelectLabel');
  1016. }
  1017. function updateLabel(optId, labelId) {
  1018. const opt0 = document.getElementById(labelId).getElementsByTagName('option')[0];
  1019. const names = Array.from(document.querySelectorAll(`#${optId} input:checked`))
  1020. .map(cb => cb.dataset.name || cb.value).filter(v => v && v !== 'All Zones' && v !== '');
  1021. opt0.innerText = names.length ? names.join(', ') : 'Nothing selected';
  1022. }
  1023. function toggleDropdown(id) {
  1024. const el = document.getElementById(id);
  1025. el.style.display = el.style.display === 'block' ? 'none' : 'block';
  1026. }
  1027. function closeDropdown(id) { document.getElementById(id).style.display = 'none'; }
  1028. function escHtml(str) {
  1029. if (!str) return '';
  1030. return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;')
  1031. .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  1032. }
  1033. </script>
  1034. <!-- DataTables 1.13 + Buttons extension (Excel / PDF) -->
  1035. <link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
  1036. <link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css">
  1037. <script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
  1038. <script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
  1039. <script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
  1040. <script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script>
  1041. <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
  1042. <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
  1043. <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
  1044. <script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
  1045. </body>
  1046. </html>