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.

1336 lines
42 KiB

6 days ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head th:replace="~{fragments/user-head :: head}"></head>
  4. <style>
  5. :root {
  6. --accent: #12363b;
  7. --muted: #6b6b6b;
  8. --card-bg: #ffffff;
  9. --slot-radius: 10px;
  10. --green-start: #16a34a;
  11. --green-end: #09905a;
  12. }
  13. body {
  14. font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
  15. min-height: 100vh;
  16. background-attachment: fixed;
  17. background-repeat: no-repeat;
  18. background-size: cover;
  19. -webkit-font-smoothing: antialiased;
  20. min-height: 100vh;
  21. display: flex;
  22. flex-direction: column;
  23. }
  24. /* container */
  25. .container {
  26. width: 100%;
  27. max-width: 1200px;
  28. background-color: white;
  29. }
  30. /******************************
  31. PAGE / HERO / PANEL
  32. ******************************/
  33. .page {
  34. width: 100%;
  35. display: flex;
  36. flex-direction: column;
  37. align-items: center;
  38. justify-content: flex-start;
  39. }
  40. .panel-wrap {
  41. width: 100%;
  42. display: flex;
  43. justify-content: center;
  44. align-items: center;
  45. margin-top: 18px;
  46. }
  47. .hero {
  48. width: 100%;
  49. max-width: 920px;
  50. text-align: center;
  51. margin-bottom: 12px;
  52. }
  53. .input {
  54. width: 100%;
  55. padding: 14px 16px;
  56. border-radius: 12px;
  57. border: 1px solid rgba(0, 0, 0, 0.06);
  58. background: white;
  59. font-size: 16px;
  60. }
  61. .btn {
  62. display: inline-block;
  63. padding: 14px 18px;
  64. border-radius: 12px;
  65. background: var(--accent);
  66. color: #fff;
  67. font-weight: 700;
  68. border: none;
  69. cursor: pointer;
  70. box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
  71. }
  72. /* CSS - responsive behavior */
  73. /* default field behaviour */
  74. .field {
  75. width: 100%;
  76. max-width: 760px;
  77. /* original maximum */
  78. margin: 0 auto 18px;
  79. /* center and spacing */
  80. margin-top: 0 auto 18px;
  81. }
  82. /* compact variant to match your 536x48 measurement */
  83. .field .input.compact {
  84. box-sizing: border-box;
  85. width: 100%;
  86. height: 48px;
  87. /* your requested height */
  88. padding: 10px 14px;
  89. /* inner spacing */
  90. border-radius: 10px;
  91. border: 1px solid rgba(0, 0, 0, 0.06);
  92. font-size: 15px;
  93. background-color: white;
  94. }
  95. /* People and Contact styles removed */
  96. .date-icon {
  97. width: 40px;
  98. height: 40px;
  99. display: flex;
  100. align-items: center;
  101. justify-content: center;
  102. border-radius: 10px;
  103. background: rgba(0, 0, 0, 0.04);
  104. }
  105. .counter {
  106. width: 46px;
  107. text-align: center;
  108. font-weight: 700;
  109. }
  110. /* slots grid and slot styles */
  111. .slots-grid {
  112. display: grid;
  113. grid-template-columns: repeat(4, 1fr);
  114. /* 4 columns */
  115. gap: 15px;
  116. margin-top: 14px;
  117. }
  118. .slot {
  119. display: flex;
  120. align-items: center;
  121. justify-content: space-between;
  122. padding: 10px 12px;
  123. border-radius: 10px;
  124. background: var(--card-bg);
  125. border: 1px solid rgba(0, 0, 0, 0.06);
  126. cursor: pointer;
  127. }
  128. .slot.disabled {
  129. background: #f1f1f1;
  130. color: #8b8b8b;
  131. cursor: not-allowed;
  132. }
  133. .left {
  134. display: flex;
  135. flex-direction: column;
  136. gap: 6px;
  137. }
  138. .time {
  139. font-weight: 700;
  140. }
  141. .meta {
  142. font-size: 13px;
  143. color: var(--muted);
  144. }
  145. .choose {
  146. width: 36px;
  147. height: 36px;
  148. border-radius: 8px;
  149. border: 1px solid rgba(0, 0, 0, 0.08);
  150. display: flex;
  151. align-items: center;
  152. justify-content: center;
  153. font-weight: 700;
  154. font-size: 16px;
  155. }
  156. .slot .time {
  157. font-weight: 700;
  158. color: #000 !important;
  159. /* always black */
  160. }
  161. .slot.chosen {
  162. background: #ffffff !important;
  163. /* keep slot white */
  164. color: #fff;
  165. border-color: transparent;
  166. border-color: rgba(0, 0, 0, 0.06) !important;
  167. /* optional subtle glow around the entire slot (comment out if not wanted) */
  168. box-shadow: 0 6px 18px rgba(9, 144, 90, 0.04);
  169. }
  170. /* .slot.chosen .meta{ color: rgba(255,255,255,0.9); } */
  171. .slot.chosen .choose {
  172. background: linear-gradient(180deg, var(--green-start), var(--green-end)) !important;
  173. color: #fff;
  174. border-color: transparent !important;
  175. box-shadow: 0 6px 16px rgba(9, 144, 90, 0.12) !important;
  176. }
  177. /* container that keeps left (multi-line) and right (inline summary) in one row */
  178. /* ===== Single-column stacked layout (no left/right) ===== */
  179. .info-row {
  180. display: flex !important;
  181. flex-direction: column !important;
  182. /* always stacked */
  183. align-items: center !important;
  184. /* center items horizontally */
  185. justify-content: flex-start;
  186. gap: 14px;
  187. width: 100%;
  188. max-width: 820px;
  189. margin: 0 auto;
  190. box-sizing: border-box;
  191. }
  192. /* preview note becomes a centered block (fluid width) */
  193. .preview-note-box {
  194. width: 100%;
  195. max-width: 640px;
  196. flex: 0 0 auto;
  197. font-size: 13px;
  198. color: #000000;
  199. line-height: 1.45;
  200. text-align: left !important;
  201. /* ← LEFT aligned */
  202. padding: 6px 8px;
  203. }
  204. /* summary-inline becomes a centered row under the note */
  205. /* Inline container */
  206. /* ---- summary inline: left / right alignment (desktop + mobile) ---- */
  207. /* Strong override to force left / right placement */
  208. .summary-inline {
  209. width: 100% !important;
  210. /* use the full container width */
  211. max-width: 100% !important;
  212. /* don't constrain to 420px here */
  213. display: flex !important;
  214. align-items: center !important;
  215. justify-content: flex-end !important;
  216. /* left / right extremes */
  217. gap: 24px !important;
  218. padding: 6px 8px !important;
  219. box-sizing: border-box !important;
  220. margin: 0 auto !important;
  221. white-space: nowrap !important;
  222. }
  223. /* Make each item expand to fill half the line */
  224. .summary-item {
  225. display: flex !important;
  226. justify-content: flex-end !important;
  227. /* both right-aligned */
  228. /* align-items: center !important; */
  229. gap: 8px !important;
  230. line-height: 1 !important;
  231. flex: 0 0 auto !important;
  232. /* allow items to grow and take space */
  233. min-width: 0 !important;
  234. /* prevent overflow due to long text */
  235. text-align: right !important;
  236. }
  237. /* Left block: align contents to left */
  238. .summary-item.unallocated {
  239. justify-content: flex-end !important;
  240. text-align: right !important;
  241. /* color: #E63946 !important; */
  242. }
  243. /* Right block: align contents to right */
  244. .summary-item.allocated {
  245. justify-content: flex-end !important;
  246. text-align: right !important;
  247. /* color: #16A34A !important; */
  248. }
  249. /* Keep numeric counter compact and centered inside its area */
  250. .summary-item strong {
  251. min-width: 28px !important;
  252. display: inline-block !important;
  253. text-align: center !important;
  254. }
  255. /* DEFAULT color = black */
  256. .summary-item span,
  257. .summary-item strong {
  258. color: #000 !important;
  259. }
  260. /* Red */
  261. .text-red span,
  262. .text-red strong {
  263. color: #E63946 !important;
  264. }
  265. /* Green */
  266. .text-green span,
  267. .text-green strong {
  268. color: #16A34A !important;
  269. }
  270. .submit-note {
  271. font-family: "Inter", sans-serif;
  272. font-size: 13px;
  273. color: #000000;
  274. text-align: left;
  275. /* left aligned */
  276. margin: 3px 0 10px 0;
  277. /* top:3px, bottom:10px */
  278. margin-left: 3px;
  279. }
  280. .submit-wrap {
  281. margin-top: 16px;
  282. }
  283. /* ===============================
  284. DEFAULT VISIBILITY STATES
  285. ================================ */
  286. /* Book Visit — visible by default */
  287. .book-vist {
  288. opacity: 1;
  289. pointer-events: auto;
  290. }
  291. /* Cancel Book — HIDDEN by default */
  292. .cancel-book {
  293. display: none;
  294. /* ⬅ start hidden */
  295. }
  296. /* Utility classes that JS toggles */
  297. .section-hidden {
  298. display: none !important;
  299. }
  300. .section-enabled {
  301. display: block !important;
  302. opacity: 1 !important;
  303. pointer-events: auto !important;
  304. }
  305. /* When Cancel is visible, style the button normally */
  306. .cancel-book.section-enabled .btn {
  307. background: #043B55;
  308. color: #ffffff;
  309. border-radius: 10px;
  310. border: none;
  311. cursor: pointer;
  312. font-weight: 700;
  313. box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
  314. }
  315. /* summary card inside cancel section (you already had this) */
  316. .summary-card {
  317. max-width: 100%;
  318. background: #fff;
  319. border-radius: 10px;
  320. padding: 18px 20px;
  321. border: 2px solid rgba(0, 0, 0, 0.12);
  322. box-sizing: border-box;
  323. font-family: Inter, sans-serif;
  324. color: #111;
  325. margin: 0 auto;
  326. }
  327. /* Top row */
  328. .summary-row {
  329. display: flex;
  330. justify-content: space-between;
  331. align-items: flex-start;
  332. margin-bottom: 12px;
  333. }
  334. .summary-left .label {
  335. font-size: 13px;
  336. font-weight: 700;
  337. margin-bottom: 10px;
  338. color: black;
  339. }
  340. .summary-right div {
  341. font-size: 13px;
  342. font-weight: 400;
  343. margin-bottom: 10px;
  344. text-align: right;
  345. color: black;
  346. }
  347. /* main buttons (book visit / preview etc.) */
  348. .submit-wrap {
  349. margin-top: 16px;
  350. }
  351. .btn {
  352. width: 100%;
  353. padding: 12px 26px;
  354. background: #043B55;
  355. color: #ffffff;
  356. border-radius: 10px;
  357. border: none;
  358. cursor: pointer;
  359. font-weight: 700;
  360. box-sizing: border-box;
  361. margin-bottom: 15px;
  362. }
  363. /* ---------- Mobile polish (drop this at the end of your CSS) ---------- */
  364. @media (max-width: 520px) {
  365. /* fields */
  366. .field {
  367. width: calc(100% - 20px);
  368. margin-left: 10px;
  369. margin-right: 10px;
  370. max-width: none;
  371. }
  372. .field .input,
  373. .field .input.compact {
  374. width: 100%;
  375. font-size: 15px;
  376. padding: 12px 14px;
  377. height: 46px;
  378. border-radius: 10px;
  379. }
  380. /* Hover effect for Select Date input */
  381. .date-wrap .input[type="date"]:hover {
  382. border-color: #043B55;
  383. /* brand color */
  384. box-shadow: 0 0 0 2px rgba(4, 59, 85, 0.12);
  385. cursor: pointer;
  386. }
  387. /* Focus (keyboard / click) */
  388. .date-wrap .input[type="date"]:focus {
  389. outline: none;
  390. border-color: #043B55;
  391. box-shadow: 0 0 0 3px rgba(4, 59, 85, 0.18);
  392. }
  393. /* slots grid -> single column */
  394. .slots-grid {
  395. grid-template-columns: 1fr;
  396. gap: 10px;
  397. margin-top: 12px;
  398. }
  399. .slot {
  400. padding: 10px 12px;
  401. border-radius: 10px;
  402. }
  403. /* info row -> stack */
  404. .info-row {
  405. flex-direction: column;
  406. align-items: stretch;
  407. gap: 10px;
  408. margin-top: 10px;
  409. }
  410. .preview-note-box {
  411. width: 100%;
  412. max-width: none;
  413. font-size: 13px;
  414. }
  415. /* submit area */
  416. .submit-wrap {
  417. margin-top: 18px;
  418. padding-bottom: 18px;
  419. }
  420. .btn {
  421. width: 100%;
  422. padding: 12px 14px;
  423. font-size: 16px;
  424. border-radius: 12px;
  425. }
  426. }
  427. /* =========================================
  428. CALENDAR HOVER — FINAL GUARANTEED FIX
  429. ========================================= */
  430. /* Target the REAL visible flatpickr input */
  431. .date-wrap input.input.form-control {
  432. cursor: pointer;
  433. background-color: #fff !important;
  434. transition: border-color 0.15s ease,
  435. box-shadow 0.15s ease,
  436. background-color 0.15s ease;
  437. }
  438. /* Hover */
  439. .date-wrap input.input.form-control:hover {
  440. border-color: #043B55 !important;
  441. box-shadow: 0 0 0 2px rgba(4, 59, 85, 0.18) !important;
  442. background-color: #fafafa !important;
  443. }
  444. /* Focus */
  445. .date-wrap input.input.form-control:focus {
  446. outline: none !important;
  447. border-color: #043B55 !important;
  448. box-shadow: 0 0 0 3px rgba(4, 59, 85, 0.28) !important;
  449. }
  450. /* Optional: hover anywhere in wrapper */
  451. .date-wrap:hover input.input.form-control {
  452. border-color: #043B55 !important;
  453. }
  454. .border-field {
  455. border: 1px solid rgba(0, 0, 0, 0.15);
  456. /* visible but subtle */
  457. border-radius: 12px;
  458. /* matches your UI */
  459. padding: 16px 18px;
  460. /* space inside box */
  461. background: #ffffff;
  462. /* white card look */
  463. }
  464. </style>
  465. <body>
  466. <div th:replace="~{fragments/preloader :: div}"></div>
  467. <div id="pcoded" class="pcoded">
  468. <div class="pcoded-container">
  469. <!-- TOP NAV -->
  470. <nav th:replace="~{fragments/nav-admin :: nav}"></nav>
  471. <nav th:replace="~{fragments/nav-adminmenu :: nav}"></nav>
  472. <div class="pcoded-main-container">
  473. <div class="pcoded-wrapper">
  474. <div class="pcoded-content">
  475. <div class="pcoded-inner-content">
  476. <div class="page-body">
  477. <br>
  478. <br>
  479. <br>
  480. <div class="container">
  481. <br>
  482. <br>
  483. <main class="page" role="main" aria-labelledby="hero-heading">
  484. <div class="panel-wrap">
  485. <div class="panel" role="region" aria-label="Booking panel">
  486. <div id="bookVisitSection" class="book-vist">
  487. <!-- select date -->
  488. <div class="field border-field" style="max-width: 1000px;">
  489. <h5
  490. style="text-align: center;font-weight: bold;font-family:Arial, Helvetica, sans-serif;">
  491. Visit Blocking:
  492. </h5>
  493. <!-- Date Selection -->
  494. <div style="margin-bottom: 30px;">
  495. <input type="hidden" id="blockId"> <!-- Hidden ID -->
  496. <label class="muted-label">Select Blocking Date:</label>
  497. <div class="date-wrap">
  498. <input id="dateInput" class="input" type="date"
  499. aria-label="Select date" placeholder="Select Date">
  500. </div>
  501. </div>
  502. <!-- Slots Section -->
  503. <div
  504. style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
  505. <label style="margin:8px 0 6px 0" class="muted-label">Pick
  506. the slots to be
  507. blocked:</label>
  508. <button id="selectAllBtn" class="btn"
  509. style="width:auto; padding:8px 16px; font-size:14px; margin:0; display:none;">Select
  510. All 8 Slots</button>
  511. </div>
  512. <div id="slotsGrid" class="slots-grid" aria-live="polite">
  513. </div>
  514. </div>
  515. <div class="submit-wrap">
  516. <p class="submit-note">Confirm blocking for the selected
  517. date and slots.</p>
  518. <button id="previewBtn" class="btn">Block Visit</button>
  519. <button id="cancelEditBtn" class="btn"
  520. style="background:var(--muted); display:none; margin-top:10px;">Cancel
  521. Edit</button>
  522. </div>
  523. <br>
  524. <hr><br>
  525. <h5
  526. style="text-align: center;font-weight: bold;font-family:Arial, Helvetica, sans-serif;">
  527. Visit Blocked List
  528. </h5>
  529. <div style="overflow-x:auto;">
  530. <table class="table table-striped table-bordered"
  531. style="width:100%; background:white;">
  532. <thead>
  533. <tr>
  534. <th>Visit Blocked Date</th>
  535. <th>Visit Blocked Slots</th>
  536. <th style="width: 120px;">Action</th>
  537. </tr>
  538. </thead>
  539. <tbody id="activeBlocksTableBody">
  540. <!-- Populated by JS -->
  541. </tbody>
  542. </table>
  543. </div>
  544. </div>
  545. </div>
  546. </div>
  547. </main>
  548. </div>
  549. </div>
  550. </div>
  551. </div>
  552. </div>
  553. </div>
  554. </div>
  555. </div>
  556. </body>
  557. <!-- Conflict Modal -->
  558. <div id="conflictModal" class="modal fade" role="dialog" data-backdrop="static">
  559. <div class="modal-dialog modal-lg">
  560. <div class="modal-content">
  561. <div class="modal-header">
  562. <h5 class="modal-title">Previously booked Visits on selected block date</h5>
  563. <button type="button" class="close" data-dismiss="modal">&times;</button>
  564. </div>
  565. <div class="modal-body">
  566. <div style="max-height: 400px; overflow-y: auto;">
  567. <table class="table table-bordered table-striped">
  568. <thead>
  569. <tr>
  570. <th>Slot ID</th>
  571. <th>Mob No</th>
  572. <th>Visit Date</th>
  573. <th>No of People</th>
  574. <th>Total Amount</th>
  575. </tr>
  576. </thead>
  577. <tbody id="conflictTableBody">
  578. <!-- Populated by JS -->
  579. </tbody>
  580. </table>
  581. <p class="text-danger" style="font-weight:bold; margin-top:10px;">This slot already have bookings.
  582. </p>
  583. </div>
  584. </div>
  585. <div class="modal-footer">
  586. <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
  587. <button type="button" class="btn btn-danger" id="proceedBlockingBtn">Proceed Blocking</button>
  588. </div>
  589. </div>
  590. </div>
  591. </div>
  592. <script th:replace="~{fragments/user-script :: script}"></script>
  593. <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
  594. <script type="text/javascript">
  595. function showLoader() {
  596. $('#pageLoader').show();
  597. }
  598. function hideLoader() {
  599. $('#pageLoader').hide();
  600. }
  601. $('#pageLoader').hide();
  602. //checking
  603. /* =========================================
  604. ADMIN HALL BLOCKING LOGIC (REAL API)
  605. ========================================= */
  606. // Core State
  607. let chosenSlots = new Map(); // time -> { slotId, language, seats }
  608. let conflictModal; // Added global variable
  609. let activeBlocksList = []; // Store active blocks for validation
  610. // Mock Data for Display (Structure only)
  611. // Elements
  612. let dateInput, previewBtn, selectAllBtn, slotsGrid, activeBlocksTableBody;
  613. document.addEventListener("DOMContentLoaded", () => {
  614. dateInput = document.getElementById("dateInput");
  615. previewBtn = document.getElementById("previewBtn");
  616. selectAllBtn = document.getElementById("selectAllBtn");
  617. slotsGrid = document.getElementById("slotsGrid");
  618. activeBlocksTableBody = document.getElementById("activeBlocksTableBody");
  619. // Set min date to today and default to today
  620. if (dateInput) {
  621. flatpickr(dateInput, {
  622. minDate: "today",
  623. dateFormat: "Y-m-d",
  624. disable: [
  625. function (date) {
  626. return (date.getDay() === 2); // 0=Sun, 1=Mon, 2=Tue
  627. }
  628. ],
  629. locale: {
  630. firstDayOfWeek: 1
  631. },
  632. defaultDate: "today",
  633. onChange: function (selectedDates, dateStr, instance) {
  634. if (dateStr) {
  635. loadActiveBlocks();
  636. loadTimeSlotsAndRender(dateStr); // Reload with availability
  637. }
  638. }
  639. });
  640. // Load slots
  641. const initDate = dateInput.value;
  642. loadTimeSlotsAndRender(initDate);
  643. // Load Active Blocks
  644. loadActiveBlocks();
  645. // Native picker events removed for Flatpickr
  646. }
  647. if (selectAllBtn) {
  648. selectAllBtn.addEventListener("click", (e) => {
  649. e.preventDefault();
  650. selectAllSlots();
  651. });
  652. }
  653. if (previewBtn) {
  654. previewBtn.addEventListener("click", handleBlockSubmit);
  655. }
  656. // Modal events
  657. document.getElementById("proceedBlockingBtn").addEventListener("click", () => {
  658. $('#conflictModal').modal('hide');
  659. const dateVal = dateInput.value;
  660. const blockId = document.getElementById("blockId").value;
  661. // Collect selected slots
  662. let slotIds = [];
  663. chosenSlots.forEach((val, key) => {
  664. slotIds.push(val.slotId);
  665. });
  666. slotIds.sort((a, b) => a - b);
  667. const slotString = slotIds.join(",");
  668. // Call submitBlock with autoCancel = false (as per requirement: dont update reg_details)
  669. // The 4th argument 'autoCancel' set to false means we FORCE block but DO NOT cancel existing bookings.
  670. submitBlock(dateVal, slotString, blockId, false);
  671. });
  672. });
  673. // Load slots from API
  674. function loadTimeSlotsAndRender(selectedDate) {
  675. let url = "/victoriapublichall/bookingapi/getAllTimeSlots";
  676. if (selectedDate) {
  677. url += "?visitDate=" + selectedDate;
  678. }
  679. $.ajax({
  680. url: url,
  681. type: "GET",
  682. success: function (response) {
  683. if (response && response.length > 0) {
  684. renderSlotsMap(response);
  685. } else {
  686. console.warn("No time slots found");
  687. if (slotsGrid) slotsGrid.innerHTML = "<p>No slots found</p>";
  688. }
  689. },
  690. error: function (e) {
  691. console.error("Failed to load time slots", e);
  692. }
  693. });
  694. }
  695. // Render the slots (data from API)
  696. function renderSlotsMap(slotsData) {
  697. if (!slotsGrid) return;
  698. slotsGrid.innerHTML = "";
  699. chosenSlots.clear();
  700. slotsData.forEach(item => {
  701. const slotEl = document.createElement("div");
  702. slotEl.className = "slot";
  703. slotEl.tabIndex = 0;
  704. // Map DB fields to UI data attributes
  705. // item.time_id -> slotId
  706. // item.timing -> time
  707. slotEl.dataset.time = item.timing;
  708. slotEl.dataset.slotId = item.time_id;
  709. // Check if this slot is already blocked
  710. const currentDate = dateInput.value;
  711. const activeBlock = activeBlocksList.find(b => b.block_date === currentDate);
  712. let isBlocked = false;
  713. if (activeBlock && activeBlock.time_slot) {
  714. const blockedIds = activeBlock.time_slot.split(',').map(s => s.trim());
  715. if (blockedIds.includes(String(item.time_id))) {
  716. isBlocked = true;
  717. }
  718. }
  719. // Availability
  720. let avail = item.availCap !== undefined ? item.availCap : item.max_capacity;
  721. let max = item.max_capacity;
  722. // Logic for visual style (optional: disable if full?)
  723. // For admin blocking, we allow blocking even if full (logic handles conflict),
  724. // but we show the state.
  725. // Green border initially (available)
  726. slotEl.style.border = "1px solid #28a745";
  727. if (isBlocked) {
  728. // RED BACKGROUND if blocked
  729. slotEl.style.background = "#dc3545";
  730. slotEl.style.color = "white";
  731. slotEl.style.borderColor = "#dc3545";
  732. } else if (avail <= 0) {
  733. slotEl.style.border = "1px solid #dc3545"; // Red border if full but not blocked via this tool
  734. slotEl.style.background = "#fff5f5";
  735. }
  736. slotEl.innerHTML = `
  737. <div class="left">
  738. <div class="time" style="${isBlocked ? 'color:white !important;' : ''}">${item.timing}</div>
  739. <div class="meta" style="font-size:12px; color: ${isBlocked ? '#ffdddd' : (avail > 0 ? 'green' : 'red')};">
  740. ${isBlocked ? 'BLOCKED' : 'Available: ' + avail + ' / ' + max}
  741. </div>
  742. </div>
  743. <div class="choose"></div>
  744. `;
  745. slotEl.addEventListener("click", () => toggleSlot(slotEl));
  746. slotsGrid.appendChild(slotEl);
  747. });
  748. if (selectAllBtn) selectAllBtn.style.display = "block";
  749. }
  750. // Toggle Slot Selection
  751. function toggleSlot(slotEl) {
  752. const time = slotEl.dataset.time;
  753. if (chosenSlots.has(time)) {
  754. // Deselect
  755. chosenSlots.delete(time);
  756. slotEl.classList.remove('chosen');
  757. const choose = slotEl.querySelector('.choose');
  758. if (choose) choose.textContent = '';
  759. } else {
  760. // Select
  761. chosenSlots.set(time, {
  762. slotId: slotEl.dataset.slotId
  763. });
  764. slotEl.classList.add('chosen');
  765. const choose = slotEl.querySelector('.choose');
  766. if (choose) choose.textContent = '✓';
  767. }
  768. updateSelectAllText();
  769. }
  770. // Select All Slots
  771. function selectAllSlots() {
  772. const allSlots = document.querySelectorAll("#slotsGrid .slot");
  773. if (allSlots.length === 0) return;
  774. let allSelected = true;
  775. allSlots.forEach(el => {
  776. if (!el.classList.contains("chosen")) allSelected = false;
  777. });
  778. if (allSelected) {
  779. // Deselect All
  780. allSlots.forEach(el => {
  781. if (el.classList.contains("chosen")) toggleSlot(el);
  782. });
  783. } else {
  784. // Select All
  785. allSlots.forEach(el => {
  786. if (!el.classList.contains("chosen")) toggleSlot(el);
  787. });
  788. }
  789. updateSelectAllText();
  790. }
  791. function updateSelectAllText() {
  792. const allSlots = document.querySelectorAll("#slotsGrid .slot");
  793. let allSelected = true;
  794. allSlots.forEach(el => {
  795. if (!el.classList.contains("chosen")) allSelected = false;
  796. });
  797. if (selectAllBtn) {
  798. selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All 8 Slots";
  799. }
  800. }
  801. // API ACTIONS
  802. function loadActiveBlocks() {
  803. showLoader();
  804. $.ajax({
  805. url: "/victoriapublichall/bookingapi/getActiveBlockTimeSlots",
  806. type: "GET",
  807. success: function (response) {
  808. hideLoader();
  809. renderActiveBlocks(response);
  810. },
  811. error: function (xhr) {
  812. hideLoader();
  813. console.error("Failed to load blocks", xhr);
  814. Swal.fire("Error", "Failed to load active blocked slots.", "error");
  815. }
  816. });
  817. }
  818. function renderActiveBlocks(data) {
  819. activeBlocksList = data || []; // Update global list
  820. activeBlocksTableBody.innerHTML = "";
  821. if (!data || data.length === 0) {
  822. activeBlocksTableBody.innerHTML = "<tr><td colspan='3' class='text-center'>No active blocks found.</td></tr>";
  823. return;
  824. }
  825. data.forEach(row => {
  826. const tr = document.createElement("tr");
  827. tr.innerHTML = `
  828. <td>${row.block_date_formatted}</td>
  829. <td>${row.time_slot}</td>
  830. <td>
  831. <button class="btn btn-primary btn-sm" style="width:auto; padding:6px 12px; margin:0 5px;"
  832. onclick="editBlock(${row.id}, '${row.block_date}', '${row.time_slot}')" title="Edit">
  833. <i class="fa-solid fa-pen"></i>
  834. </button>
  835. <button class="btn btn-danger btn-sm" style="width:auto; padding:6px 12px; margin:0 5px;"
  836. onclick="deleteBlock(${row.id})" title="Delete">
  837. <i class="fa-solid fa-trash"></i>
  838. </button>
  839. </td>
  840. `;
  841. activeBlocksTableBody.appendChild(tr);
  842. });
  843. }
  844. // Edit Block Function
  845. window.editBlock = function (id, date, slots) {
  846. // Set ID
  847. document.getElementById("blockId").value = id;
  848. // Set Date (flatpickr)
  849. if (dateInput && dateInput._flatpickr) {
  850. dateInput._flatpickr.setDate(date);
  851. } else {
  852. dateInput.value = date;
  853. }
  854. // Clear choices
  855. chosenSlots.clear();
  856. document.querySelectorAll("#slotsGrid .slot").forEach(el => {
  857. el.classList.remove('chosen');
  858. const choose = el.querySelector('.choose');
  859. if (choose) choose.textContent = '';
  860. });
  861. // Set slots
  862. if (slots) {
  863. const slotIds = slots.split(',').map(s => s.trim());
  864. slotIds.forEach(sid => {
  865. // Find element by data-slot-id
  866. const el = document.querySelector(`.slot[data-slot-id='${sid}']`);
  867. if (el) {
  868. toggleSlot(el); // This will add it to chosenSlots map
  869. }
  870. });
  871. }
  872. // Scroll to top
  873. window.scrollTo({ top: 0, behavior: 'smooth' });
  874. // Update UI Text
  875. previewBtn.textContent = "Update Block";
  876. document.getElementById("cancelEditBtn").style.display = "inline-block";
  877. }
  878. // Cancel Edit
  879. document.getElementById("cancelEditBtn").addEventListener("click", function () {
  880. resetForm();
  881. });
  882. function resetForm() {
  883. document.getElementById("blockId").value = "";
  884. previewBtn.textContent = "Block Visit";
  885. document.getElementById("cancelEditBtn").style.display = "none";
  886. // Reset date to today
  887. if (dateInput && dateInput._flatpickr) {
  888. dateInput._flatpickr.setDate("today");
  889. }
  890. // Clear slots
  891. chosenSlots.clear();
  892. document.querySelectorAll("#slotsGrid .slot").forEach(el => {
  893. el.classList.remove('chosen');
  894. const choose = el.querySelector('.choose');
  895. if (choose) choose.textContent = '';
  896. });
  897. updateSelectAllText();
  898. }
  899. function handleBlockSubmit() {
  900. const dateVal = dateInput.value;
  901. const blockId = document.getElementById("blockId").value;
  902. const isEdit = blockId && blockId.trim() !== "";
  903. if (!dateVal) {
  904. Swal.fire("Warning", "Please select a date.", "warning");
  905. return;
  906. }
  907. // Check for duplicate blocks on the same date
  908. // If creating new (isEdit false) OR editing (isEdit true) and changing date to one that exists
  909. const duplicate = activeBlocksList.find(b => b.block_date === dateVal && b.id != blockId);
  910. if (duplicate) {
  911. Swal.fire("Warning", "For the selected date, there is already a blocking. Modify it in the table below.", "warning");
  912. return;
  913. }
  914. if (chosenSlots.size === 0) {
  915. Swal.fire("Warning", "Please select at least one slot to block.", "warning");
  916. return;
  917. }
  918. // Collect slot IDs
  919. let slotIds = [];
  920. chosenSlots.forEach((val, key) => {
  921. slotIds.push(val.slotId);
  922. });
  923. slotIds.sort((a, b) => a - b);
  924. const slotString = slotIds.join(",");
  925. // Check for conflicts first
  926. checkConflicts(dateVal, slotString, blockId, isEdit);
  927. }
  928. function checkConflicts(dateVal, slotString, blockId, isEdit) {
  929. showLoader();
  930. $.ajax({
  931. url: "/victoriapublichall/bookingapi/checkConflictingBookings",
  932. type: "GET",
  933. data: { blockDate: dateVal, timeSlots: slotString }, // Pass timeSlots
  934. success: function (response) {
  935. hideLoader();
  936. if (response && response.length > 0) {
  937. showConflictModal(response);
  938. } else {
  939. // No conflicts, proceed with normal confirmation
  940. confirmAndSubmit(dateVal, slotString, blockId, isEdit);
  941. }
  942. },
  943. error: function (xhr) {
  944. hideLoader();
  945. // If check fails, maybe warn user or proceed?
  946. // Creating block shouldn't be blocked by check failure implicitly, but safe to warn.
  947. console.error("Conflict check failed", xhr);
  948. confirmAndSubmit(dateVal, slotString, blockId, isEdit);
  949. }
  950. });
  951. }
  952. function showConflictModal(conflicts) {
  953. const tbody = document.getElementById("conflictTableBody");
  954. tbody.innerHTML = "";
  955. conflicts.forEach(row => {
  956. const tr = document.createElement("tr");
  957. tr.innerHTML = `
  958. <td style="color:orange; font-weight:bold;">${row.time_id || '-'}</td>
  959. <td>${row.mob_no || '-'}</td>
  960. <td>${row.visit_date}</td>
  961. <td>${row.no_of_people}</td>
  962. <td>${row.total_amount}</td>
  963. `;
  964. tbody.appendChild(tr);
  965. });
  966. $('#conflictModal').modal('show');
  967. }
  968. function confirmAndSubmit(dateVal, slotString, blockId, isEdit) {
  969. const actionText = isEdit ? "Update" : "Block";
  970. const confirmText = isEdit ? "Yes, update it!" : "Yes, block it!";
  971. Swal.fire({
  972. title: `Confirm ${actionText}?`,
  973. text: `${actionText} ${dateVal} for slots: ${slotString}?`,
  974. icon: 'warning',
  975. showCancelButton: true,
  976. confirmButtonColor: '#3085d6',
  977. cancelButtonColor: '#d33',
  978. confirmButtonText: confirmText
  979. }).then((result) => {
  980. if (result.isConfirmed) {
  981. submitBlock(dateVal, slotString, blockId, false); // autoCancel = false
  982. }
  983. });
  984. }
  985. function submitBlock(dateVal, slotString, blockId, autoCancel) {
  986. showLoader();
  987. let payload = {
  988. blockDate: dateVal,
  989. timeSlot: slotString,
  990. autoCancel: autoCancel
  991. };
  992. if (blockId) {
  993. payload.id = blockId;
  994. }
  995. $.ajax({
  996. url: "/victoriapublichall/bookingapi/saveBlockTimeSlot",
  997. type: "POST",
  998. data: payload,
  999. success: function (response) {
  1000. hideLoader();
  1001. if (response.status === "success") {
  1002. const msg = blockId ? "Block updated successfully!" : "Visit blocked successfully!";
  1003. Swal.fire("Success", msg, "success");
  1004. resetForm();
  1005. loadActiveBlocks();
  1006. const currentSelectedDate = dateInput.value;
  1007. if (currentSelectedDate) {
  1008. loadTimeSlotsAndRender(currentSelectedDate); // Reload with availability
  1009. }
  1010. } else {
  1011. Swal.fire("Error", response.message, "error");
  1012. }
  1013. },
  1014. error: function (xhr) {
  1015. hideLoader();
  1016. Swal.fire("Error", "Server error while saving block.", "error");
  1017. }
  1018. });
  1019. }
  1020. window.deleteBlock = function (id) {
  1021. Swal.fire({
  1022. title: 'Are you sure?',
  1023. text: "Do you want to delete this block? Slots will become available.",
  1024. icon: 'warning',
  1025. showCancelButton: true,
  1026. confirmButtonColor: '#3085d6',
  1027. cancelButtonColor: '#d33',
  1028. confirmButtonText: 'Yes, delete it!'
  1029. }).then((result) => {
  1030. if (result.isConfirmed) {
  1031. executeDelete(id);
  1032. }
  1033. });
  1034. }
  1035. function executeDelete(id) {
  1036. showLoader();
  1037. $.ajax({
  1038. url: "/victoriapublichall/bookingapi/deleteBlockTimeSlot",
  1039. type: "POST",
  1040. data: { id: id },
  1041. success: function (response) {
  1042. hideLoader();
  1043. if (response.status === "success") {
  1044. Swal.fire("Deleted!", "Block has been deleted.", "success");
  1045. // If we were editing this ID, reset form
  1046. const currentEditId = document.getElementById("blockId").value;
  1047. if (currentEditId == id) {
  1048. resetForm();
  1049. }
  1050. loadActiveBlocks();
  1051. const dateStr = dateInput.value;
  1052. if (dateStr) {
  1053. loadTimeSlotsAndRender(dateStr); // Reload with availability
  1054. }
  1055. } else {
  1056. Swal.fire("Error", response.message, "error");
  1057. }
  1058. },
  1059. error: function (xhr) {
  1060. hideLoader();
  1061. Swal.fire("Error", "Server error while deleting block.", "error");
  1062. }
  1063. });
  1064. }
  1065. </script>
  1066. <script>
  1067. (function () {
  1068. const hambtn = document.getElementById('hambtn');
  1069. const mobileNav = document.getElementById('mobileNav');
  1070. const mobileOverlay = document.getElementById('mobileOverlay');
  1071. const desktopBook = document.getElementById('desktopBookBtn');
  1072. const mobileBook = document.getElementById('mobileBook');
  1073. if (!hambtn || !mobileNav || !mobileOverlay) {
  1074. // nothing to do if elements missing
  1075. } else {
  1076. function openMenu() {
  1077. hambtn.setAttribute('aria-expanded', 'true');
  1078. mobileNav.classList.add('open'); mobileNav.setAttribute('aria-hidden', 'false');
  1079. mobileOverlay.classList.add('show'); mobileOverlay.setAttribute('aria-hidden', 'false');
  1080. document.body.style.overflow = 'hidden';
  1081. const first = mobileNav.querySelector('a'); if (first) first.focus();
  1082. }
  1083. function closeMenu() {
  1084. hambtn.setAttribute('aria-expanded', 'false');
  1085. mobileNav.classList.remove('open'); mobileNav.setAttribute('aria-hidden', 'true');
  1086. mobileOverlay.classList.remove('show'); mobileOverlay.setAttribute('aria-hidden', 'true');
  1087. document.body.style.overflow = '';
  1088. hambtn.focus();
  1089. }
  1090. hambtn.addEventListener('click', () => {
  1091. const isOpen = hambtn.getAttribute('aria-expanded') === 'true';
  1092. if (isOpen) closeMenu(); else openMenu();
  1093. });
  1094. mobileOverlay.addEventListener('click', closeMenu);
  1095. document.addEventListener('keydown', (e) => {
  1096. if (e.key === 'Escape' && mobileNav.classList.contains('open')) closeMenu();
  1097. });
  1098. mobileNav.querySelectorAll('a').forEach(a => {
  1099. a.addEventListener('click', () => {
  1100. // close after clicking any mobile link
  1101. closeMenu();
  1102. });
  1103. });
  1104. // ensure menu closes when resizing to desktop
  1105. window.addEventListener('resize', () => {
  1106. if (window.innerWidth > 900 && mobileNav.classList.contains('open')) closeMenu();
  1107. });
  1108. }
  1109. // Optional: wire desktop Book a Visit to the same booking action as mobile
  1110. // if (desktopBook && mobileBook) {
  1111. // desktopBook.addEventListener('click', (e) => {
  1112. // e.preventDefault();
  1113. // alert('Book a Visit clicked — open booking flow here.');
  1114. // });
  1115. // mobileBook.addEventListener('click', (e) => {
  1116. // });
  1117. // }
  1118. })();
  1119. </script>
  1120. </body>
  1121. </html>