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.

1053 lines
55 KiB

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