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.

1478 lines
43 KiB

1 year ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>VPH - Registration</title>
  7. <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
  8. <!-- jQuery -->
  9. <script src="https://code.jquery.com/jquery-3.6.4.min.js"
  10. integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8="
  11. crossorigin="anonymous"></script>
  12. </head>
  13. <style>
  14. * {
  15. margin: 0;
  16. padding: 0;
  17. box-sizing: border-box;
  18. }
  19. body {
  20. font-family: 'Montserrat';
  21. background: linear-gradient(135deg, #eff6ff 0%, #f0fdfa 100%);
  22. min-height: 100vh;
  23. padding: 2rem 1rem;
  24. }
  25. .container {
  26. max-width: 64rem;
  27. margin: 0 auto;
  28. }
  29. .header {
  30. text-align: center;
  31. margin-bottom: 2rem;
  32. }
  33. .header h1 {
  34. font-size: 2.5rem;
  35. font-weight: bold;
  36. color: #1f2937;
  37. margin-bottom: 0.5rem;
  38. }
  39. .header p {
  40. color: #6b7280;
  41. font-size: 1.1rem;
  42. }
  43. .progress-bar {
  44. margin-bottom: 2rem;
  45. }
  46. .progress-steps {
  47. display: flex;
  48. justify-content: center;
  49. align-items: center;
  50. margin-bottom: 1rem;
  51. }
  52. .step {
  53. width: 2rem;
  54. height: 2rem;
  55. border-radius: 50%;
  56. display: flex;
  57. align-items: center;
  58. justify-content: center;
  59. font-size: 0.875rem;
  60. font-weight: 500;
  61. background-color: #e5e7eb;
  62. color: #6b7280;
  63. transition: all 0.3s ease;
  64. }
  65. .step.active {
  66. background-color: #3b82f6;
  67. color: white;
  68. }
  69. .step.completed {
  70. background-color: #10b981;
  71. color: white;
  72. }
  73. .step-line {
  74. width: 3rem;
  75. height: 0.25rem;
  76. background-color: #e5e7eb;
  77. margin: 0 0.5rem;
  78. transition: all 0.3s ease;
  79. }
  80. .step-line.completed {
  81. background-color: #3b82f6;
  82. }
  83. .step-label {
  84. text-align: center;
  85. margin-top: 1rem;
  86. }
  87. .step-label p {
  88. font-size: 0.875rem;
  89. color: #6b7280;
  90. }
  91. .form-container {
  92. background: white;
  93. border-radius: 0.5rem;
  94. box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
  95. padding: 2rem;
  96. }
  97. .step-content {
  98. display: none;
  99. }
  100. .step-content.active {
  101. display: block;
  102. }
  103. .step-header {
  104. text-align: center;
  105. margin-bottom: 1.5rem;
  106. }
  107. .step-header h2 {
  108. font-size: 1.5rem;
  109. font-weight: bold;
  110. color: #1f2937;
  111. margin-bottom: 0.5rem;
  112. }
  113. .step-header p {
  114. color: #6b7280;
  115. }
  116. .booking-type-grid {
  117. display: grid;
  118. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  119. gap: 1rem;
  120. margin-bottom: -24px;
  121. }
  122. .booking-type-btn {
  123. padding: 0.5rem;
  124. border: 2px solid #e5e7eb;
  125. border-radius: 0.5rem;
  126. background: white;
  127. cursor: pointer;
  128. transition: all 0.2s ease;
  129. text-align: center;
  130. }
  131. .booking-type-btn:hover {
  132. transform: scale(1.02);
  133. border-color: #d1d5db;
  134. }
  135. .booking-type-btn.selected {
  136. border-color: #3b82f6;
  137. background-color: #eff6ff;
  138. color: #1d4ed8;
  139. }
  140. .booking-type-btn .icon {
  141. font-size: 2rem;
  142. margin-bottom: 0.75rem;
  143. }
  144. .booking-type-btn h3 {
  145. font-size: 1.125rem;
  146. font-weight: 600;
  147. margin-bottom: 0.5rem;
  148. }
  149. .booking-type-btn p {
  150. font-size: 0.875rem;
  151. opacity: 0.75;
  152. }
  153. .form-grid {
  154. display: grid;
  155. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  156. gap: 1.5rem;
  157. margin-bottom: 1.5rem;
  158. }
  159. .form-group {
  160. display: flex;
  161. flex-direction: column;
  162. }
  163. .form-group label {
  164. font-size: 0.875rem;
  165. font-weight: 500;
  166. color: #374151;
  167. margin-bottom: 0.5rem;
  168. }
  169. .form-input {
  170. padding: 0.75rem;
  171. border: 1px solid #d1d5db;
  172. border-radius: 0.5rem;
  173. font-size: 1rem;
  174. transition: all 0.2s ease;
  175. }
  176. .form-input:focus {
  177. outline: none;
  178. border-color: #3b82f6;
  179. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  180. }
  181. .help-text {
  182. font-size: 0.875rem;
  183. color: #6b7280;
  184. margin-top: 0.25rem;
  185. }
  186. .time-slots-grid {
  187. display: grid;
  188. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  189. gap: 1rem;
  190. margin-bottom: 2rem;
  191. }
  192. .time-slot-btn {
  193. padding: 1rem;
  194. border: 2px solid #e5e7eb;
  195. border-radius: 0.5rem;
  196. background: white;
  197. cursor: pointer;
  198. transition: all 0.2s ease;
  199. text-align: center;
  200. }
  201. .time-slot-btn:hover {
  202. transform: scale(1.02);
  203. border-color: #d1d5db;
  204. }
  205. .time-slot-btn.selected {
  206. border-color: #3b82f6;
  207. background-color: #eff6ff;
  208. color: #1d4ed8;
  209. }
  210. .time-slot-btn .icon {
  211. font-size: 1.5rem;
  212. margin-bottom: 0.5rem;
  213. }
  214. .time-slot-btn h3 {
  215. font-weight: 600;
  216. margin-bottom: 0.25rem;
  217. }
  218. .time-slot-btn p {
  219. font-size: 0.875rem;
  220. opacity: 0.75;
  221. }
  222. .slot-selection {
  223. margin-top: 2rem;
  224. }
  225. .slot-header {
  226. background-color: #eff6ff;
  227. padding: 1rem;
  228. border-radius: 0.5rem;
  229. margin-bottom: 1.5rem;
  230. }
  231. .slot-header h3 {
  232. font-size: 1.125rem;
  233. font-weight: 600;
  234. color: #1e40af;
  235. margin-bottom: 0.5rem;
  236. }
  237. .selection-info {
  238. display: flex;
  239. justify-content: space-between;
  240. align-items: center;
  241. font-size: 0.875rem;
  242. color: #1d4ed8;
  243. }
  244. .selection-count {
  245. background-color: #dbeafe;
  246. padding: 0.25rem 0.75rem;
  247. border-radius: 9999px;
  248. font-weight: 500;
  249. }
  250. .selection-complete {
  251. display: flex;
  252. align-items: center;
  253. gap: 0.5rem;
  254. margin-top: 0.5rem;
  255. color: #059669;
  256. font-weight: 500;
  257. font-size: 0.875rem;
  258. }
  259. .slots-container {
  260. display: grid;
  261. grid-template-columns: repeat(5, 1fr);
  262. gap: 1rem;
  263. margin: 0 auto;
  264. padding-bottom: 0;
  265. overflow: visible; /* no more horizontal scroll */
  266. }
  267. .slot-group {
  268. background-color: #f9fafb;
  269. padding: 1rem;
  270. border-radius: 0.5rem;
  271. flex: 1 1 140px; /* grow, shrink, base 140px */
  272. min-width: 120px; /* never smaller than this */
  273. max-width: 180px;
  274. }
  275. .slot-group h4 {
  276. font-size: 0.875rem;
  277. font-weight: 500;
  278. color: #374151;
  279. margin-bottom: 0.75rem;
  280. text-align: center;
  281. padding-bottom: 0.25rem;
  282. }
  283. .slot-group h4::after {
  284. width: 20px;
  285. height: 2px;
  286. }
  287. .slot-grid {
  288. display: grid;
  289. grid-template-columns: repeat(3, 1fr);
  290. gap: 0.5rem;
  291. }
  292. .slot-btn {
  293. aspect-ratio: 1;
  294. border: 2px solid #d1d5db;
  295. border-radius: 0.5rem;
  296. background: white;
  297. cursor: pointer;
  298. font-size: 0.75rem;
  299. font-weight: 500;
  300. transition: all 0.2s ease;
  301. position: relative;
  302. display: flex;
  303. align-items: center;
  304. justify-content: center;
  305. padding: 0.4rem 0;
  306. }
  307. .slot-btn:hover:not(:disabled) {
  308. transform: scale(1.05);
  309. border-color: #3b82f6;
  310. background-color: #eff6ff;
  311. }
  312. .slot-btn.selected {
  313. border-color: #10b981;
  314. background-color: #d1fae5;
  315. color: #065f46;
  316. }
  317. .slot-btn.occupied {
  318. border-color: #fca5a5;
  319. background-color: #fee2e2;
  320. color: #dc2626;
  321. cursor: not-allowed;
  322. }
  323. .slot-btn.disabled {
  324. border-color: #e5e7eb;
  325. background-color: #f9fafb;
  326. color: #9ca3af;
  327. cursor: not-allowed;
  328. }
  329. .slot-btn .check-icon {
  330. position: absolute;
  331. top: -0.25rem;
  332. right: -0.25rem;
  333. width: 0.75rem;
  334. height: 0.75rem;
  335. background-color: #10b981;
  336. border-radius: 50%;
  337. display: flex;
  338. align-items: center;
  339. justify-content: center;
  340. font-size: 0.5rem;
  341. color: white;
  342. }
  343. .slot-legend {
  344. display: flex;
  345. justify-content: center;
  346. gap: 1.5rem;
  347. margin-top: 1.5rem;
  348. font-size: 0.875rem;
  349. }
  350. .legend-item {
  351. display: flex;
  352. align-items: center;
  353. gap: 0.5rem;
  354. }
  355. .legend-box {
  356. width: 1rem;
  357. height: 1rem;
  358. border: 2px solid;
  359. border-radius: 0.25rem;
  360. }
  361. .legend-box.available {
  362. background: white;
  363. border-color: #d1d5db;
  364. }
  365. .legend-box.selected {
  366. background: #d1fae5;
  367. border-color: #10b981;
  368. }
  369. .legend-box.occupied {
  370. background: #fee2e2;
  371. border-color: #fca5a5;
  372. }
  373. .legend-box.disabled {
  374. background: #f9fafb;
  375. border-color: #e5e7eb;
  376. }
  377. .confirmation-header {
  378. text-align: center;
  379. margin-bottom: 1.5rem;
  380. }
  381. .success-icon {
  382. font-size: 4rem;
  383. margin-bottom: 1rem;
  384. }
  385. .confirmation-header h2 {
  386. font-size: 1.5rem;
  387. font-weight: bold;
  388. color: #1f2937;
  389. margin-bottom: 0.5rem;
  390. }
  391. .confirmation-header p {
  392. color: #6b7280;
  393. }
  394. .booking-summary {
  395. background-color: #f9fafb;
  396. padding: 1.5rem;
  397. border-radius: 0.5rem;
  398. margin-bottom: 1rem;
  399. }
  400. .booking-summary h3 {
  401. font-weight: 600;
  402. font-size: 1.125rem;
  403. margin-bottom: 1rem;
  404. }
  405. .summary-item {
  406. display: flex;
  407. justify-content: space-between;
  408. padding: 0.75rem 0;
  409. border-bottom: 1px solid #e5e7eb;
  410. }
  411. .summary-item:last-child {
  412. border-bottom: none;
  413. }
  414. .summary-item span:first-child {
  415. color: #6b7280;
  416. }
  417. .summary-item span:last-child {
  418. font-weight: 500;
  419. text-transform: capitalize;
  420. }
  421. .important-note {
  422. background-color: #eff6ff;
  423. padding: 1rem;
  424. border-radius: 0.5rem;
  425. font-size: 0.875rem;
  426. color: #1e40af;
  427. }
  428. .navigation-buttons {
  429. display: flex;
  430. justify-content: space-between;
  431. margin-top: 2rem;
  432. }
  433. .btn {
  434. padding: 0.75rem 1.5rem;
  435. border-radius: 0.5rem;
  436. font-weight: 500;
  437. cursor: pointer;
  438. transition: all 0.2s ease;
  439. border: none;
  440. font-size: 1rem;
  441. }
  442. .btn:disabled {
  443. opacity: 0.5;
  444. cursor: not-allowed;
  445. }
  446. .btn-primary {
  447. background-color: #3b82f6;
  448. color: white;
  449. }
  450. .btn-primary:hover:not(:disabled) {
  451. background-color: #2563eb;
  452. transform: scale(1.02);
  453. }
  454. .btn-secondary {
  455. background-color: #e5e7eb;
  456. color: #374151;
  457. }
  458. .btn-secondary:hover:not(:disabled) {
  459. background-color: #d1d5db;
  460. }
  461. .error-message {
  462. color: #dc2626;
  463. font-size: 0.875rem;
  464. margin-top: 1.5rem;
  465. display: none;
  466. align-items: center;
  467. gap: 0.5rem;
  468. }
  469. .error-message.show {
  470. display: flex;
  471. }
  472. .error-message::before {
  473. content: "⚠️";
  474. font-size: 1rem;
  475. }
  476. /* batch-card status colors */
  477. .time-slot-btn.status-ok {
  478. border-color: #10b981; /* green */
  479. background: #ecfdf5;
  480. }
  481. .time-slot-btn.status-low {
  482. border-color: #f59e0b; /* amber */
  483. background: #fffbeb;
  484. }
  485. .time-slot-btn.status-critical {
  486. border-color: #ef4444; /* red */
  487. background: #fee2e2;
  488. }
  489. .time-slot-btn.status-full {
  490. border-color: #9ca3af; /* gray */
  491. background: #f3f4f6;
  492. color: #9ca3af;
  493. }
  494. .time-slot-btn.status-full .available-count { display: none; }
  495. .time-slot-btn:disabled { cursor: not-allowed; opacity: .65; }
  496. @media (max-width: 768px) {
  497. .container {
  498. padding: 1rem;
  499. }
  500. .booking-type-btn h3 {
  501. font-size: 1.125rem;
  502. font-weight: 600;
  503. margin-bottom: 1.5rem;
  504. }
  505. .slots-container {
  506. display: grid;
  507. grid-template-columns: repeat(2, 1fr);
  508. gap: 1rem;
  509. margin: 0 auto;
  510. padding-bottom: 0;
  511. overflow: visible;
  512. }
  513. .form-container {
  514. padding: 1rem;
  515. }
  516. .booking-type-grid,
  517. .time-slots-grid {
  518. grid-template-columns: repeat(2, 1fr);
  519. height: 225px;
  520. }
  521. .form-grid {
  522. display: grid !important;
  523. grid-template-columns: 1fr !important;
  524. gap: 1rem !important;
  525. }
  526. .form-group.full-width {
  527. grid-column: auto !important;
  528. }
  529. .slot-legend {
  530. flex-wrap: wrap;
  531. gap: 1rem;
  532. }
  533. .navigation-buttons {
  534. flex-direction: row !important;
  535. justify-content: space-between;
  536. gap: 0;
  537. margin-top: 5rem;
  538. }
  539. .navigation-buttons .btn {
  540. flex: 1; /* let them grow equally */
  541. max-width: 30%; /* so there’s a little gutter */
  542. }
  543. .slot-selection {
  544. margin-top: 6rem;
  545. }
  546. }
  547. </style>
  548. <body>
  549. <div class="container">
  550. <div class="header">
  551. <h1>Victoria Public Hall <br> Experiential Museum - Registration</h1>
  552. </div>
  553. <div class="progress-bar" id="progressBar">
  554. <div class="progress-steps">
  555. <div class="step active" data-step="1">1</div>
  556. <div class="step-line"></div>
  557. <div class="step" data-step="2">2</div>
  558. <div class="step-line"></div>
  559. <div class="step" data-step="3">3</div>
  560. <div class="step-line"></div>
  561. <div class="step" data-step="4">4</div>
  562. </div>
  563. <div class="step-label">
  564. <p>Step <span id="currentStepNum">1</span> of 4: <span id="stepName">Booking Type</span></p>
  565. </div>
  566. </div>
  567. <div class="form-container">
  568. <!-- Step 1: Booking Type Selection -->
  569. <div class="step-content active" id="step1">
  570. <div class="step-header">
  571. <h2>Book your Visit to VPH</h2>
  572. </div>
  573. <div class="booking-type-grid">
  574. <button class="booking-type-btn" data-type="individual">
  575. <div class="icon">👤</div>
  576. <h3>Individual Booking</h3>
  577. <p>Max 10</p>
  578. </button>
  579. <button class="booking-type-btn" data-type="group">
  580. <div class="icon">👥</div>
  581. <h3>Bulk Booking</h3>
  582. <p>Max 50</p>
  583. </button>
  584. </div>
  585. <div class="error-message" id="typeError"></div>
  586. </div>
  587. <!-- Step 2: Visit Details -->
  588. <div class="step-content" id="step2">
  589. <div class="step-header">
  590. <h2>Visit Details</h2>
  591. </div>
  592. <div class="form-grid">
  593. <div class="form-group">
  594. <label>📅 Select Date</label>
  595. <input type="date" id="visitDate" class="form-input">
  596. <div class="error-message" id="dateError"></div>
  597. </div>
  598. <div class="form-group" id="membersGroup">
  599. <label>👥 Number of Persons</label>
  600. <input type="number" id="memberCount" class="form-input" min="1" max="10" step="1" oninput="this.value = this.value.replace(/\D+/g, '')"/>
  601. <p class="help-text">Maximum 50 members allowed</p>
  602. <div class="error-message" id="membersError"></div>
  603. </div>
  604. </div>
  605. </div>
  606. <!-- Step 3: Time Slot Selection -->
  607. <div class="step-content" id="step3">
  608. <div class="step-header">
  609. <h2>Choose Your batch and number of tickets</h2>
  610. </div>
  611. <div class="time-slots-grid">
  612. <button class="time-slot-btn" data-batch="1">
  613. <div class="icon">🕐</div>
  614. <h3>8:00-10:00 AM</h3>
  615. <p><span class="available-count">100</span> available slots</p>
  616. </button>
  617. <button class="time-slot-btn" data-batch="2">
  618. <div class="icon">🕐</div>
  619. <h3>10:30 AM-12:30 PM</h3>
  620. <p><span class="available-count">100</span> available slots</p>
  621. </button>
  622. <button class="time-slot-btn" data-batch="3">
  623. <div class="icon">🕐</div>
  624. <h3>1:00-3:00 PM</h3>
  625. <p><span class="available-count">100</span> available slots</p>
  626. </button>
  627. <button class="time-slot-btn" data-batch="4">
  628. <div class="icon">🕐</div>
  629. <h3>3:30-5:30 PM</h3>
  630. <p><span class="available-count">100</span> available slots</p>
  631. </button>
  632. </div>
  633. <div class="slot-selection" id="slotSelection" style="display: none;">
  634. <div class="slot-header">
  635. <!--
  636. <h3>Select Slots for <span id="selectedTimeSlot"></span></h3>
  637. -->
  638. <h3>Select group-wise to experience Chennaiyin Cinema and Chennaiyin Olichithram</h3>
  639. <div class="selection-info">
  640. <span id="selectionText">Select 1 slot for individual booking</span>
  641. <span class="selection-count">
  642. <span id="selectedCount">0</span> / <span id="requiredCount">1</span> selected
  643. </span>
  644. </div>
  645. <div class="selection-complete" id="selectionComplete" style="display: none;">
  646. ✅ Selection complete!
  647. </div>
  648. </div>
  649. <div class="slot-legend">
  650. <div class="legend-item">
  651. <div class="legend-box available"></div>
  652. <span>Available</span>
  653. </div>
  654. <div class="legend-item">
  655. <div class="legend-box selected"></div>
  656. <span>Selected</span>
  657. </div>
  658. <div class="legend-item">
  659. <div class="legend-box occupied"></div>
  660. <span>Occupied</span>
  661. </div>
  662. <div class="legend-item">
  663. <div class="legend-box disabled"></div>
  664. <span>Disabled</span>
  665. </div>
  666. </div>
  667. <br>
  668. <div class="slots-container" id="slotsContainer">
  669. <!-- Slots will be generated by JavaScript -->
  670. </div>
  671. </div>
  672. <div class="error-message" id="batchError"></div>
  673. <div class="error-message" id="slotError"></div>
  674. </div>
  675. <!-- Step 4: Personal Details -->
  676. <div class="step-content" id="step4">
  677. <div class="step-header">
  678. <h2>Please provide your details</h2>
  679. </div>
  680. <div class="form-grid">
  681. <div class="form-group">
  682. <label>Name</label>
  683. <input type="text" id="fullName" class="form-input" placeholder="Enter your full name">
  684. <div class="error-message" id="nameError"></div>
  685. </div>
  686. <div class="form-group">
  687. <label>Phone Number</label>
  688. <input type="tel" id="phoneNumber" class="form-input" placeholder="Enter 10-digit phone number" maxlength="10">
  689. <div class="error-message" id="phoneError"></div>
  690. </div>
  691. <div class="form-group full-width">
  692. <label>Address</label>
  693. <textarea id="address" class="form-input" placeholder="Enter your complete address" rows="3"></textarea>
  694. <div class="error-message" id="addressError"></div>
  695. </div>
  696. <div class="form-group">
  697. <label>Aadhaar Number</label>
  698. <input type="text" id="aadharNumber" class="form-input" placeholder="Enter 12-digit Aadhaar number" maxlength="12">
  699. <div class="error-message" id="aadharError"></div>
  700. </div>
  701. <div class="form-group">
  702. <label>Age</label>
  703. <input type="number" id="age" class="form-input" placeholder="Enter your age" min="1" max="120">
  704. <div class="error-message" id="ageError"></div>
  705. </div>
  706. </div>
  707. </div>
  708. <div class="navigation-buttons" id="navigationButtons">
  709. <button class="btn btn-secondary" id="backBtn" disabled>Back</button>
  710. <button class="btn btn-primary" id="nextBtn">Next</button>
  711. </div>
  712. </div>
  713. </div>
  714. <script>
  715. // --- Batch start/end times (in minutes from midnight) ---
  716. const SLOT_WINDOWS = {
  717. '1': { startM: 8 * 60, endM: 10 * 60 }, // 8:00–10:00
  718. '2': { startM: 10 * 60 + 30, endM: 12 * 60 + 30 }, // 10:30–12:30
  719. '3': { startM: 13 * 60, endM: 15 * 60 }, // 1:00–3:00
  720. '4': { startM: 15 * 60 + 30, endM: 17 * 60 + 30 } // 3:30–5:30
  721. };
  722. function todayYMD() {
  723. return new Date().toISOString().split('T')[0];
  724. }
  725. // Return true if the batch should be time-blocked for the current selection
  726. function isSlotTimeBlocked(batchId) {
  727. if (!bookingData.date) return false;
  728. const chosen = bookingData.date;
  729. const today = todayYMD();
  730. if (chosen < today) return true; // past date ⇒ block all
  731. if (chosen > today) return false; // future date ⇒ allow all
  732. // Same day: block if the batch has started already
  733. const nowMins = new Date().getHours() * 60 + new Date().getMinutes();
  734. const win = SLOT_WINDOWS[batchId];
  735. if (!win) return false;
  736. // Block as soon as start time has passed
  737. return nowMins >= win.startM;
  738. }
  739. // Apply/refresh disabled state on the four batch buttons
  740. function applyTemporalDisabling() {
  741. document.querySelectorAll('.time-slot-btn').forEach(btn => {
  742. const b = btn.dataset.batch;
  743. const blocked = isSlotTimeBlocked(b);
  744. // keep the visual "full" gray when blocked by time
  745. btn.classList.toggle('status-full', blocked);
  746. btn.disabled = blocked;
  747. // If currently selected and it just became blocked, clear selection & UI
  748. if (blocked && bookingData.batch === b) {
  749. bookingData.batch = '';
  750. bookingData.subSlot = [];
  751. document.getElementById('slotSelection').style.display = 'none';
  752. clearError('batchError');
  753. }
  754. });
  755. }
  756. const GROUP_SIZE = 10;
  757. // Booking form state
  758. let bookingData = {
  759. type: '',
  760. date: '',
  761. members: 1,
  762. batch: '',
  763. subSlot: [],
  764. personalDetails: {
  765. fullName: '',
  766. phone: '',
  767. address: '',
  768. aadhar: '',
  769. age: ''
  770. }
  771. };
  772. let currentStep = 1;
  773. const totalSteps = 4;
  774. // Time slots data
  775. const timeSlots = {
  776. '1': { time: '8:00-10:00 AM', capacity: 0, slots: [] },
  777. '2': { time: '10:30 AM-12:30 PM', capacity: 0, slots: [] },
  778. '3': { time: '1:00-3:00 PM', capacity: 0, slots: [] },
  779. '4': { time: '3:30-5:30 PM', capacity: 0, slots: [] }
  780. };
  781. // --- Helper to build slots array ---
  782. function buildSlots(capacity, occupiedSet) {
  783. const arr = [];
  784. for (let i = 1; i <= capacity; i++) {
  785. arr.push({ id: i, occupied: occupiedSet.has(i) });
  786. }
  787. return arr;
  788. }
  789. function fetchOccupiedSeats(slotId) {
  790. return $.ajax({
  791. url: '/victoriapublichall/api/occupied-seats',
  792. method: 'GET',
  793. dataType: 'json',
  794. data: { bookingDate: bookingData.date, slotId }
  795. });
  796. }
  797. function setBatchVisual(btn, available, capacity) {
  798. const pct = capacity ? (available / capacity) : 0;
  799. btn.classList.remove('status-ok','status-low','status-critical','status-full');
  800. btn.disabled = false;
  801. if (available <= 0) {
  802. btn.classList.add('status-full');
  803. btn.disabled = true; // full: cannot pick
  804. } else if (pct <= 0.10 || available <= 5) {
  805. btn.classList.add('status-critical'); // red: almost full
  806. } else if (pct <= 0.25) {
  807. btn.classList.add('status-low'); // amber: getting low
  808. } else {
  809. btn.classList.add('status-ok'); // greenish: plenty
  810. }
  811. const countEl = btn.querySelector('.available-count');
  812. if (countEl) countEl.textContent = available;
  813. }
  814. // Refresh all batches for a date
  815. function refreshAllAvailability() {
  816. if (!bookingData.date) return;
  817. document.querySelectorAll('.time-slot-btn').forEach(btn => {
  818. const b = btn.dataset.batch;
  819. fetchOccupiedSeats(b).done(data => {
  820. const c = btn.querySelector('.available-count');
  821. if (c) c.textContent = data.availableCount;
  822. timeSlots[b].capacity = data.capacity;
  823. timeSlots[b].slots = buildSlots(data.capacity, new Set(data.occupiedSeats || []));
  824. setBatchVisual(btn, data.availableCount, data.capacity);
  825. if (bookingData.batch === b) generateSlotGrid();
  826. applyTemporalDisabling();
  827. });
  828. });
  829. }
  830. // Initialize form
  831. document.addEventListener('DOMContentLoaded', function() {
  832. initializeEventListeners();
  833. updateProgressBar();
  834. updateNavigationButtons();
  835. const today = new Date().toISOString().split('T')[0];
  836. document.getElementById('visitDate').min = today;
  837. });
  838. function initializeEventListeners() {
  839. document.querySelectorAll('.booking-type-btn').forEach(btn => {
  840. btn.addEventListener('click', function() {
  841. selectBookingType(this.dataset.type);
  842. clearError('typeError');
  843. });
  844. });
  845. document.querySelectorAll('.time-slot-btn').forEach(btn => {
  846. btn.addEventListener('click', function() {
  847. selectTimeSlot(this.dataset.batch); });
  848. });
  849. document.getElementById('visitDate').addEventListener('change', e => {
  850. bookingData.date = e.target.value;
  851. clearError('dateError');
  852. refreshAllAvailability();
  853. applyTemporalDisabling();
  854. });
  855. document.getElementById('memberCount').addEventListener('input', e => {
  856. let val = parseInt(e.target.value, 10) || 0; // parse or 0 if empty/non-numeric
  857. if (val < 1) {
  858. val = 1; // force minimum
  859. showError('membersError', 'Number of persons must be at least 1');
  860. } else {
  861. clearError('membersError');
  862. }
  863. // also respect max based on booking type
  864. const max = bookingData.type === 'individual' ? 10 : 50;
  865. if (val > max) {
  866. val = max;
  867. showError('membersError', `Maximum allowed is ${max} persons`);
  868. }
  869. e.target.value = val;
  870. bookingData.members = val;
  871. // clear previously selected seats if necessary
  872. if (bookingData.subSlot.length > val) {
  873. bookingData.subSlot = [];
  874. generateSlotGrid();
  875. }
  876. updateSlotSelection();
  877. });
  878. document.getElementById('fullName').addEventListener('input', e => bookingData.personalDetails.fullName = e.target.value);
  879. document.getElementById('phoneNumber').addEventListener('input', e => {
  880. e.target.value = e.target.value.replace(/\D/g, '');
  881. bookingData.personalDetails.phone = e.target.value;
  882. });
  883. document.getElementById('address').addEventListener('input', e => bookingData.personalDetails.address = e.target.value);
  884. document.getElementById('aadharNumber').addEventListener('input', e => {
  885. e.target.value = e.target.value.replace(/\D/g, '');
  886. bookingData.personalDetails.aadhar = e.target.value;
  887. });
  888. document.getElementById('age').addEventListener('input', e => bookingData.personalDetails.age = e.target.value);
  889. document.getElementById('backBtn').addEventListener('click', goBack);
  890. document.getElementById('nextBtn').addEventListener('click', goNext);
  891. }
  892. function selectBookingType(type) {
  893. bookingData.type = type;
  894. document.querySelectorAll('.booking-type-btn').forEach(b => b.classList.toggle('selected', b.dataset.type === type));
  895. const countInput = document.getElementById('memberCount');
  896. const helpText = document.querySelector('#membersGroup .help-text');
  897. if (type === 'individual') {
  898. countInput.max = 10;
  899. helpText.textContent = 'Max 10 members for individual booking';
  900. } else {
  901. countInput.max = 50;
  902. helpText.textContent = 'Max 50 members for bulk booking';
  903. }
  904. countInput.value = 1;
  905. bookingData.members = 1;
  906. clearError('membersError');
  907. updateSlotSelection();
  908. }
  909. function selectTimeSlot(batch) {
  910. if (!bookingData.date) {
  911. showError('dateError', 'Please select a visit date first');
  912. return;
  913. }
  914. if (isSlotTimeBlocked(batch)) {
  915. showError('batchError', 'This batch is no longer available for the selected date/time.');
  916. return;
  917. }
  918. bookingData.batch = batch;
  919. bookingData.subSlot = [];
  920. document.querySelectorAll('.time-slot-btn').forEach(b => b.classList.remove('selected'));
  921. document.querySelector(`.time-slot-btn[data-batch="${batch}"]`).classList.add('selected');
  922. document.getElementById('slotSelection').style.display = 'block';
  923. updateSlotSelection();
  924. clearError('batchError');
  925. fetchOccupiedSeats(batch)
  926. .done(data => {
  927. const countEl = document.querySelector(`.time-slot-btn[data-batch="${batch}"] .available-count`);
  928. if (countEl) countEl.textContent = data.availableCount;
  929. const occupied = new Set(data.occupiedSeats || []);
  930. timeSlots[batch].capacity = data.capacity;
  931. timeSlots[batch].slots = buildSlots(data.capacity, occupied);
  932. // update only this card’s UI + count
  933. const btn = document.querySelector(`.time-slot-btn[data-batch="${batch}"]`);
  934. if (btn) setBatchVisual(btn, data.availableCount, data.capacity);
  935. generateSlotGrid();
  936. })
  937. .fail(xhr => {
  938. console.error(xhr);
  939. showError('batchError', xhr.responseJSON?.message || 'Unable to load availability');
  940. });
  941. }
  942. function updateSlotSelection() {
  943. const requiredSlots = bookingData.members;
  944. document.getElementById('requiredCount').textContent = requiredSlots;
  945. document.getElementById('selectedCount').textContent = bookingData.subSlot.length;
  946. document.getElementById('selectionText').textContent = `Select ${requiredSlots} slot${requiredSlots>1?'s':''}`;
  947. document.getElementById('selectionComplete').style.display =
  948. (bookingData.subSlot.length === requiredSlots) ? 'block' : 'none';
  949. }
  950. function generateSlotGrid() {
  951. const container = document.getElementById('slotsContainer');
  952. container.innerHTML = '';
  953. if (!bookingData.batch) return;
  954. const slots = timeSlots[bookingData.batch].slots;
  955. const capacity = timeSlots[bookingData.batch].capacity;
  956. const groupCount = Math.ceil(capacity / GROUP_SIZE);
  957. for (let gi = 0; gi < groupCount; gi++) {
  958. const start = gi * GROUP_SIZE;
  959. const end = Math.min(start + GROUP_SIZE, capacity);
  960. const group = slots.slice(start, end);
  961. const availableCount = group.filter(s => !s.occupied).length;
  962. const div = document.createElement('div');
  963. div.className = 'slot-group';
  964. const h4 = document.createElement('h4');
  965. h4.textContent = `Group ${gi + 1}: Slots ${start + 1}-${end} (Available: ${availableCount})`;
  966. div.appendChild(h4);
  967. const chkLabel = document.createElement('label');
  968. chkLabel.style = 'display:flex;justify-content:center;font-size:.75rem;margin:.5rem 0';
  969. const chk = document.createElement('input');
  970. chk.type = 'checkbox';
  971. chk.checked = group.filter(s => !s.occupied).every(s => bookingData.subSlot.includes(s.id));
  972. chk.addEventListener('change', () => handleGroupSelect(gi, chk.checked));
  973. chkLabel.append(chk, document.createTextNode(' Select All'));
  974. div.appendChild(chkLabel);
  975. const grid = document.createElement('div');
  976. grid.className = 'slot-grid';
  977. group.forEach(s => {
  978. const btn = document.createElement('button');
  979. btn.className = 'slot-btn';
  980. btn.textContent = s.id;
  981. if (s.occupied) {
  982. btn.classList.add('occupied');
  983. btn.disabled = true;
  984. } else if (bookingData.subSlot.includes(s.id)) {
  985. btn.classList.add('selected');
  986. const icon = document.createElement('div');
  987. icon.className = 'check-icon';
  988. icon.textContent = '✓';
  989. btn.appendChild(icon);
  990. } else if (bookingData.subSlot.length >= bookingData.members) {
  991. btn.classList.add('disabled');
  992. btn.disabled = true;
  993. }
  994. btn.addEventListener('click', () => selectSlot(s.id));
  995. grid.appendChild(btn);
  996. });
  997. div.appendChild(grid);
  998. container.appendChild(div);
  999. }
  1000. }
  1001. function handleGroupSelect(gi, checked) {
  1002. const slots = timeSlots[bookingData.batch].slots;
  1003. const start = gi * GROUP_SIZE;
  1004. const end = Math.min(start + GROUP_SIZE, timeSlots[bookingData.batch].capacity);
  1005. const group = slots.slice(start, end);
  1006. const cap = bookingData.members;
  1007. if (checked) {
  1008. for (let s of group) {
  1009. if (!s.occupied && bookingData.subSlot.length < cap && !bookingData.subSlot.includes(s.id)) {
  1010. bookingData.subSlot.push(s.id);
  1011. }
  1012. }
  1013. } else {
  1014. group.forEach(s => {
  1015. const idx = bookingData.subSlot.indexOf(s.id);
  1016. if (idx > -1) bookingData.subSlot.splice(idx, 1);
  1017. });
  1018. }
  1019. updateSlotSelection();
  1020. generateSlotGrid();
  1021. }
  1022. function selectSlot(id) {
  1023. const cap = bookingData.members;
  1024. const idx = bookingData.subSlot.indexOf(id);
  1025. if (idx > -1) bookingData.subSlot.splice(idx, 1);
  1026. else if (bookingData.subSlot.length < cap) bookingData.subSlot.push(id);
  1027. updateSlotSelection();
  1028. generateSlotGrid();
  1029. clearError('slotError');
  1030. }
  1031. function validateStep(step) {
  1032. clearAllErrors();
  1033. let ok = true;
  1034. if (step===1 && !bookingData.type) {
  1035. showError('typeError','Please select a booking type');
  1036. ok = false;
  1037. }
  1038. if (step===2) {
  1039. if (!bookingData.date) {
  1040. showError('dateError','Please select a visit date');
  1041. ok = false;
  1042. }
  1043. if (bookingData.members < 1) {
  1044. showError('membersError','Number of persons must be at least 1');
  1045. ok = false;
  1046. } else if (bookingData.members > (bookingData.type==='individual'?10:50)) {
  1047. showError('membersError','Number of persons exceeds maximum allowed');
  1048. ok = false;
  1049. }
  1050. }
  1051. if (step===3) {
  1052. if (!bookingData.batch) {
  1053. showError('batchError','Please select a time batch');
  1054. ok = false;
  1055. }
  1056. if (bookingData.subSlot.length !== bookingData.members) {
  1057. showError('slotError',`Please select exactly ${bookingData.members} slot(s)`);
  1058. ok = false;
  1059. }
  1060. }
  1061. if (step===4) {
  1062. if (!bookingData.personalDetails.fullName.trim()) { showError('nameError','Full name is required'); ok=false; }
  1063. if (!bookingData.personalDetails.phone.match(/^[6-9]\d{9}$/)) { showError('phoneError','Enter valid 10-digit phone'); ok=false; }
  1064. if (!bookingData.personalDetails.address.trim()) { showError('addressError','Address is required'); ok=false; }
  1065. if (!bookingData.personalDetails.aadhar.match(/^\d{12}$/)) { showError('aadharError','Aadhar must be 12 digits'); ok=false; }
  1066. if (!bookingData.personalDetails.age.match(/^\d+$/) || parseInt(bookingData.personalDetails.age)<=0) {
  1067. showError('ageError','Enter a valid age'); ok=false;
  1068. }
  1069. }
  1070. return ok;
  1071. }
  1072. // Attach listeners after DOM loads
  1073. document.addEventListener("DOMContentLoaded", () => {
  1074. // Name
  1075. document.getElementById("fullName").addEventListener("input", (e) => {
  1076. if (e.target.value.trim()) {
  1077. clearError("nameError");
  1078. bookingData.personalDetails.fullName = e.target.value;
  1079. }
  1080. });
  1081. // Phone
  1082. document.getElementById("phoneNumber").addEventListener("input", (e) => {
  1083. const val = e.target.value.trim();
  1084. bookingData.personalDetails.phone = val;
  1085. if (/^[6-9]\d{9}$/.test(val)) {
  1086. clearError("phoneError");
  1087. }
  1088. });
  1089. // Address
  1090. document.getElementById("address").addEventListener("input", (e) => {
  1091. const val = e.target.value.trim();
  1092. bookingData.personalDetails.address = val;
  1093. if (val.length > 0) {
  1094. clearError("addressError");
  1095. }
  1096. });
  1097. // Aadhaar
  1098. document.getElementById("aadharNumber").addEventListener("input", (e) => {
  1099. const val = e.target.value.trim();
  1100. bookingData.personalDetails.aadhar = val;
  1101. if (/^\d{12}$/.test(val)) {
  1102. clearError("aadharError");
  1103. }
  1104. });
  1105. // Age
  1106. document.getElementById("age").addEventListener("input", (e) => {
  1107. const val = e.target.value.trim();
  1108. bookingData.personalDetails.age = val;
  1109. if (/^\d+$/.test(val) && parseInt(val) > 0) {
  1110. clearError("ageError");
  1111. }
  1112. });
  1113. });
  1114. function showError(elementId, message) {
  1115. const errorElement = document.getElementById(elementId);
  1116. errorElement.textContent = message;
  1117. errorElement.classList.add('show');
  1118. }
  1119. function clearError(elementId) {
  1120. const errorElement = document.getElementById(elementId);
  1121. errorElement.classList.remove('show');
  1122. }
  1123. function clearAllErrors() {
  1124. document.querySelectorAll('.error-message').forEach(error => {
  1125. error.classList.remove('show');
  1126. });
  1127. }
  1128. function goNext() {
  1129. if (validateStep(currentStep)) {
  1130. if (currentStep < totalSteps) {
  1131. currentStep++;
  1132. updateStep();
  1133. } else {
  1134. // Submit form
  1135. submitForm();
  1136. }
  1137. }
  1138. }
  1139. function goBack() {
  1140. if (currentStep > 1) {
  1141. currentStep--;
  1142. updateStep();
  1143. }
  1144. }
  1145. function updateStep() {
  1146. // Hide all steps
  1147. document.querySelectorAll('.step-content').forEach(step => {
  1148. step.classList.remove('active');
  1149. });
  1150. // Show current step
  1151. document
  1152. .getElementById(`step${currentStep}`)
  1153. .classList.add('active');
  1154. updateProgressBar();
  1155. updateNavigationButtons();
  1156. clearAllErrors();
  1157. }
  1158. function updateProgressBar() {
  1159. // Update step indicators
  1160. document.querySelectorAll('.step').forEach((step, index) => {
  1161. const stepNum = index + 1;
  1162. step.classList.remove('active', 'completed');
  1163. if (stepNum < currentStep) {
  1164. step.classList.add('completed');
  1165. } else if (stepNum === currentStep) {
  1166. step.classList.add('active');
  1167. }
  1168. });
  1169. // Update step lines
  1170. document.querySelectorAll('.step-line').forEach((line, index) => {
  1171. line.classList.remove('completed');
  1172. if (index + 1 < currentStep) {
  1173. line.classList.add('completed');
  1174. }
  1175. });
  1176. // Update step label
  1177. const stepNames = ['Booking Type', 'Visit Details', 'Time Selection', 'Personal Details'];
  1178. document.getElementById('currentStepNum').textContent = currentStep;
  1179. document.getElementById('stepName').textContent = stepNames[currentStep - 1];
  1180. // Hide progress bar on confirmation step
  1181. const progressBar = document.getElementById('progressBar');
  1182. if (currentStep === 5) {
  1183. progressBar.style.display = 'none';
  1184. } else {
  1185. progressBar.style.display = 'block';
  1186. }
  1187. }
  1188. function updateNavigationButtons() {
  1189. const backBtn = document.getElementById('backBtn');
  1190. const nextBtn = document.getElementById('nextBtn');
  1191. const navigationButtons = document.getElementById('navigationButtons');
  1192. if (currentStep === 5) {
  1193. // Hide navigation buttons on confirmation step
  1194. navigationButtons.style.display = 'none';
  1195. return;
  1196. }
  1197. navigationButtons.style.display = 'flex';
  1198. // Update back button
  1199. backBtn.disabled = currentStep === 1;
  1200. // Update next button text
  1201. if (currentStep === totalSteps) {
  1202. nextBtn.textContent = 'Submit Booking';
  1203. } else {
  1204. nextBtn.textContent = 'Next';
  1205. }
  1206. }
  1207. function submitForm() {
  1208. if (!validateStep(4)) return;
  1209. const payload = {
  1210. name: bookingData.personalDetails.fullName,
  1211. mobileno: bookingData.personalDetails.phone,
  1212. address: bookingData.personalDetails.address,
  1213. aadhaarNo: bookingData.personalDetails.aadhar,
  1214. age: parseInt(bookingData.personalDetails.age || '0', 10),
  1215. bookingType: bookingData.type,
  1216. noofpersons: bookingData.members,
  1217. bookingDate: bookingData.date,
  1218. slotId: parseInt(bookingData.batch, 10),
  1219. selectedSlots: bookingData.subSlot
  1220. };
  1221. $.ajax({
  1222. url: '/victoriapublichall/api/savebooking',
  1223. type: 'POST',
  1224. contentType: 'application/json',
  1225. data: JSON.stringify(payload),
  1226. success: function(summary) {
  1227. window.location.href = `/victoriapublichall/booking-summary?refId=${encodeURIComponent(summary.ref_id)}`;
  1228. },
  1229. error: function(xhr) {
  1230. console.error('Error:', xhr.responseText);
  1231. alert(xhr.responseJSON?.message || 'Error saving booking');
  1232. }
  1233. });
  1234. }
  1235. function displayBookingSummary() {
  1236. // Basic booking info
  1237. document.getElementById('summaryMembers').textContent = bookingData.members;
  1238. document.getElementById('summaryDate').textContent = bookingData.date;
  1239. document.getElementById('summaryTimeSlot').textContent =
  1240. bookingData.confirmation?.slotTiming || timeSlots[bookingData.batch].time;
  1241. // Group with count
  1242. const byGroup = {};
  1243. bookingData.subSlot.forEach(id => {
  1244. const g = Math.ceil(id / 10); // group number
  1245. byGroup[g] = (byGroup[g] || 0) + 1; // increment count
  1246. });
  1247. const lines = Object.keys(byGroup)
  1248. .sort((a, b) => a - b)
  1249. .map(g => `G${g}: ${byGroup[g]}`)
  1250. .join(', ');
  1251. document.getElementById('summarySlots').textContent = lines;
  1252. // Personal details
  1253. document.getElementById('summaryName').textContent = bookingData.personalDetails.fullName;
  1254. document.getElementById('summaryPhone').textContent = bookingData.personalDetails.phone;
  1255. }
  1256. // DOWNLOAD handler
  1257. document.getElementById('downloadBtn').addEventListener('click', () => {
  1258. const element = document.getElementById('bookingSummary');
  1259. html2pdf()
  1260. .set({ margin: 10,
  1261. filename: 'VPH_booking_summary.pdf',
  1262. html2canvas: { scale: 2 },
  1263. jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
  1264. })
  1265. .from(element)
  1266. .save();
  1267. });
  1268. </script>
  1269. </body>
  1270. </html>