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.
 
 
 
 
 

4109 lines
107 KiB

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/user-head :: head}"></head>
<header th:replace="~{user/razor-pay :: header}"></header>
<style>
:root {
--accent: #12363b;
--muted: #6b6b6b;
--card-bg: #ffffff;
--slot-radius: 10px;
--green-start: #16a34a;
--green-end: #09905a;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0
}
html,
body {
height: 100%
}
body {
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
background:
linear-gradient(rgba(255, 255, 255, 0.70), rgba(255, 255, 255, 0.82)),
url('/victoriapublichall/images/vph-background_v1.jpg') top center / cover no-repeat,
url('/victoriapublichall/images/vph-background_v1.jpg') bottom center / cover no-repeat;
min-height: 100vh;
background-attachment: fixed;
/* keeps both images static */
background-repeat: no-repeat;
background-size: cover;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px 24px;
}
/* container */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
/******************************
NAVBAR (desktop + mobile)
******************************/
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 10px 0;
position: relative;
z-index: 1300;
}
.leftbar {
display: flex;
align-items: center;
gap: 20px;
}
.nav-left {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 72px;
height: 72px;
object-fit: contain;
border-radius: 50%;
}
/* default desktop nav */
nav.primary {
display: flex;
gap: 18px;
align-items: center;
}
/* Desktop Navigation Links (Figma Accurate) */
nav.primary a {
font-family: "Playfair Display", serif !important;
font-weight: 400 !important;
/* Regular */
font-style: normal !important;
font-size: 15px !important;
/* Figma size */
line-height: 100% !important;
letter-spacing: 0 !important;
color: #3C1E12;
/* matches Book a Visit color */
text-decoration: none;
padding: 6px 8px;
/* keep your layout spacing */
border-radius: 6px;
transition: background 0.2s ease;
}
/* Hover state (optional subtle effect like Figma preview) */
nav.primary a:hover {
background: rgba(0, 0, 0, 0.06);
color: #3C1E12;
/* no change in color */
}
/* Mobile menu links */
.mobile-nav a {
font-family: "Playfair Display", serif !important;
font-weight: 400 !important;
font-size: 15px !important;
letter-spacing: 0 !important;
color: #3C1E12;
}
.rightbar {
display: flex;
align-items: center;
gap: 12px;
}
#desktopBookBtn {
width: 174px;
/* Figma button width */
height: 57px;
/* Figma button height */
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border-radius: 8px;
/* Figma corner radius */
/* Border from Figma (thin outline) */
border: 1px solid rgba(60, 30, 18, 0.40);
/* darker visible border */
cursor: pointer;
/* Figma Typography */
font-family: "Playfair Display", serif !important;
font-weight: 700 !important;
/* Bold */
font-style: italic !important;
font-size: 24px !important;
line-height: 100% !important;
/* Letter spacing -6% = -0.06em */
letter-spacing: -0.06em !important;
color: #3C1E12 !important;
}
/* Hover effect (Figma-like subtle shadow) */
#desktopBookBtn:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.10);
}
/* hamburger (hidden on desktop) */
.hamburger {
display: none;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: transparent;
cursor: pointer;
padding: 8px;
}
.hamburger .bar {
display: block;
width: 18px;
height: 2px;
background: var(--dark);
margin: 3px 0;
transition: transform .2s, opacity .2s;
}
/* mobile floating menu + overlay (initial hidden) */
.mobile-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.22);
z-index: 1350;
opacity: 0;
pointer-events: none;
transition: opacity .22s ease;
}
.mobile-overlay.show {
opacity: 1;
pointer-events: auto;
}
.mobile-nav {
position: fixed;
left: 12px;
right: 12px;
top: 68px;
max-width: 420px;
margin: 0 auto;
background: #fff;
border-radius: 12px;
padding: 14px;
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.16);
border: 1px solid rgba(0, 0, 0, 0.06);
z-index: 1400;
transform: translateY(-8px) scale(.995);
opacity: 0;
pointer-events: none;
transition: opacity .22s ease, transform .22s ease;
}
.mobile-nav.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.mobile-nav a {
display: block;
padding: 10px 12px;
color: var(--dark);
text-decoration: none;
border-radius: 8px;
font-weight: 600;
}
.mobile-nav a+a {
margin-top: 8px;
}
.mobile-nav .book-cta {
background: #f7f9f9;
text-align: center;
padding: 12px;
color: var(--accent);
font-weight: 700;
}
/******************************
PAGE / HERO / PANEL
******************************/
.page {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
margin-top: 20px;
}
.panel-wrap {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin-top: 18px;
}
.hero {
width: 100%;
max-width: 920px;
text-align: center;
margin-bottom: 12px;
}
/* Desktop style */
.page h1 {
font-family: "Inter", sans-serif;
font-weight: 700;
font-style: normal;
font-size: 48px;
line-height: 100%;
letter-spacing: 0;
text-align: center;
width: 100%;
max-width: 444px;
/* Figma width */
margin: 0 auto 10px;
/* Center + bottom spacing */
color: #3B1E14 !important;
}
.hero .lead {
font-family: "Inter", sans-serif;
font-weight: 400;
font-style: normal;
font-size: 20px;
line-height: 100%;
letter-spacing: 0;
text-align: center;
width: 100%;
max-width: 417px;
/* From Figma */
margin: 0 auto 20px;
/* Center + spacing */
color: #000000;
/* BLACK COLOR as requested */
}
.muted-label {
font-family: "Inter", sans-serif;
font-weight: 700;
font-style: normal;
font-size: 16px;
line-height: 100%;
letter-spacing: 0;
color: #3B1E14;
width: 100%;
max-width: 100%;
margin-bottom: 8px;
text-align: left;
display: block;
margin-left: 0px !important;
/* ⬅ FIXES CENTER ISSUE */
}
.input {
width: 100%;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #fff;
font-size: 16px;
}
.btn {
display: inline-block;
padding: 14px 18px;
border-radius: 12px;
background: var(--accent);
color: #fff;
font-weight: 700;
border: none;
cursor: pointer;
box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
}
/* CSS - responsive behavior */
/* default field behaviour */
.field {
width: 100%;
max-width: 760px;
/* original maximum */
margin: 0 auto 18px;
/* center and spacing */
margin-top: 0 auto 18px;
}
/* compact variant to match your 536x48 measurement */
.field .input.compact {
box-sizing: border-box;
width: 100%;
height: 48px;
/* your requested height */
padding: 10px 14px;
/* inner spacing */
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.06);
font-size: 15px;
background-color: white;
}
#contactDisplay {
color: #666;
font-weight: 700;
padding: 10px 14px;
border-radius: 10px;
min-width: 180px;
}
#changeContact {
color: #0478D0;
text-decoration: underline;
cursor: pointer;
/*margin-left: 40px;*/
font-size: medium;
}
.muted-label {
display: block;
margin-bottom: 8px;
color: #000;
font-weight: 700;
}
/* Desktop */
.people-row {
display: flex;
align-items: center;
gap: 16px;
/*white-space: nowrap;*/
}
.muted-label-inline {
font-weight: 600;
color: black;
/*margin-right: 320px;*/
font-size: 16px;
}
.small {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
}
.counter {
min-width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
border: 1px solid #ddd;
background: #fff;
border-radius: 8px;
}
.people-support {
margin-top: 10px;
color: #000000 !important;
font-size: 13px;
}
.people-support .link {
color: #0478D0;
text-decoration: none;
}
/* a little spacing when controls are inline with other content */
.spacer {
width: 12px;
}
.date-wrap {
display: flex;
align-items: center;
gap: 10px;
position: relative;
}
/* main date input visual */
.date-wrap .input[type="date"] {
font-family: "Inter", sans-serif !important;
font-size: 16px;
color: #0000 !important;
padding: 12px 44px 12px 12px;
/* reserve space on right for icon */
width: 100%;
background: #fff;
border: 1px solid #ccc;
border-radius: 12px;
box-sizing: border-box;
cursor: pointer;
background-color: white;
/* keep native appearance so browsers show their indicator */
-webkit-appearance: textfield;
appearance: textfield;
}
/* Force inner pieces to be black in WebKit browsers */
.date-wrap .input[type="date"]::-webkit-datetime-edit,
.date-wrap .input[type="date"]::-webkit-datetime-edit-text,
.date-wrap .input[type="date"]::-webkit-datetime-edit-month-field,
.date-wrap .input[type="date"]::-webkit-datetime-edit-day-field,
.date-wrap .input[type="date"]::-webkit-datetime-edit-year-field {
color: #000 !important;
}
.date-wrap .input[type="date"]::-webkit-calendar-picker-indicator {
position: absolute;
/* allow us to place it over the input area */
right: 12px;
/* matches padding-right */
top: 50%;
transform: translateY(-50%);
width: 22px;
height: 22px;
opacity: 1;
/* ensure visible */
cursor: pointer;
filter: none;
/* remove inversion if any */
-webkit-appearance: auto;
/* let browser draw its icon */
}
.date-wrap .input[type="date"]::-moz-focus-inner {
border: 0;
}
/* fallback: custom inline SVG icon placed absolutely (only shows if native icon not visible) */
.date-wrap::after {
content: "";
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 22px;
height: 22px;
pointer-events: none;
/* indicator should not intercept clicks; native indicator will */
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' fill='%23000'><path d='M7 10h5v5H7zm-2 9h14V8H5zm14-13h-2V4h-2v2H9V4H7v2H5v2h14z'/></svg>");
background-repeat: no-repeat;
background-position: center;
background-size: 20px 20px;
opacity: 0.9;
}
.date-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
background: rgba(0, 0, 0, 0.04);
}
.counter {
width: 46px;
text-align: center;
font-weight: 700;
}
/* time range pills */
/* pills */
.time-ranges {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 14px 0 8px;
}
.pill {
padding: 10px 12px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.06);
min-width: 110px;
text-align: center;
cursor: pointer;
user-select: none;
}
.pill small {
display: block;
color: var(--muted);
margin-top: 6px;
font-weight: 600;
font-size: 12px;
}
.pill.active {
background: linear-gradient(180deg, var(--green-start), var(--green-end));
color: #fff;
border-color: transparent;
box-shadow: 0 8px 26px rgba(0, 0, 0, 0.08);
}
.pill.active small {
color: #fff !important;
}
/* info row: left multi-line, right inline summary */
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-top: 6px;
}
.preview-note-box {
width: 320px;
font-size: 13px;
color: var(--muted);
line-height: 1.45;
}
.summary-inline {
display: flex;
gap: 18px;
align-items: center;
/*white-space: nowrap;*/
flex-shrink: 1;
flex-wrap: wrap;
}
.summary-item {
font-size: 13px;
color: var(--muted);
}
.summary-item strong {
margin-left: 6px;
font-weight: 700;
}
/* once a slot is chosen and locked, make it look non-clickable */
.slot.locked {
cursor: not-allowed;
}
/* slots grid and slot styles */
.slots-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 14px;
}
.slot {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-radius: 10px;
background: var(--card-bg);
border: 1px solid rgba(0, 0, 0, 0.06);
cursor: pointer;
}
.slot.disabled {
background: #f1f1f1;
color: #8b8b8b;
cursor: not-allowed;
}
.left {
display: flex;
flex-direction: column;
gap: 6px;
}
.time {
font-weight: 700;
}
.meta {
font-size: 13px;
color: var(--muted);
}
.choose {
width: 36px;
height: 36px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
}
.slot .time {
font-weight: 700;
color: #000 !important;
/* always black */
}
.slot.chosen {
background: #ffffff !important;
/* keep slot white */
color: #fff;
border-color: transparent;
border-color: rgba(0, 0, 0, 0.06) !important;
/* optional subtle glow around the entire slot (comment out if not wanted) */
box-shadow: 0 6px 18px rgba(9, 144, 90, 0.04);
}
/* .slot.chosen .meta{ color: rgba(255,255,255,0.9); } */
.slot.chosen .choose {
background: linear-gradient(180deg, var(--green-start), var(--green-end)) !important;
color: #fff;
border-color: transparent !important;
box-shadow: 0 6px 16px rgba(9, 144, 90, 0.12) !important;
}
.info-row {
display: flex !important;
flex-direction: column !important;
/* always stacked */
align-items: center !important;
/* center items horizontally */
justify-content: flex-start;
gap: 14px;
width: 100%;
max-width: 820px;
margin: 0 auto;
box-sizing: border-box;
}
/* preview note becomes a centered block (fluid width) */
.preview-note-box {
width: 100%;
max-width: 640px;
flex: 0 0 auto;
font-size: 13px;
color: #000000;
line-height: 1.45;
text-align: left !important;
/* ← LEFT aligned */
padding: 6px 8px;
}
.summary-inline {
width: 100% !important;
/* use the full container width */
max-width: 100% !important;
/* don't constrain to 420px here */
display: flex !important;
align-items: center !important;
justify-content: flex-end !important;
/* left / right extremes */
gap: 24px !important;
padding: 6px 8px !important;
box-sizing: border-box !important;
margin: 0 auto !important;
white-space: nowrap !important;
}
/* Make each item expand to fill half the line */
.summary-item {
display: flex !important;
justify-content: flex-end !important;
/* both right-aligned */
/* align-items: center !important; */
gap: 8px !important;
line-height: 1 !important;
flex: 0 0 auto !important;
/* allow items to grow and take space */
min-width: 0 !important;
/* prevent overflow due to long text */
text-align: right !important;
}
/* Left block: align contents to left */
.summary-item.unallocated {
justify-content: flex-end !important;
text-align: right !important;
/* color: #E63946 !important; */
}
/* Right block: align contents to right */
.summary-item.allocated {
justify-content: flex-end !important;
text-align: right !important;
/* color: #16A34A !important; */
}
/* Keep numeric counter compact and centered inside its area */
.summary-item strong {
min-width: 28px !important;
display: inline-block !important;
text-align: center !important;
}
/* DEFAULT color = black */
.summary-item span,
.summary-item strong {
color: #000 !important;
}
/* Red */
.text-red span,
.text-red strong {
color: #E63946 !important;
}
/* Green */
.text-green span,
.text-green strong {
color: #16A34A !important;
}
.submit-note {
font-family: "Inter", sans-serif;
font-size: 13px;
color: #000000;
text-align: left;
/* left aligned */
margin: 3px 0 10px 0;
/* top:3px, bottom:10px */
margin-left: 3px;
}
.submit-wrap {
margin-top: 16px;
}
/* Book Visit — visible by default */
.book-vist {
opacity: 1;
pointer-events: auto;
}
/* Cancel Book — HIDDEN by default */
.cancel-book {
display: none;
/* ⬅ start hidden */
}
/* Utility classes that JS toggles */
.section-hidden {
display: none !important;
}
.section-enabled {
display: block !important;
opacity: 1 !important;
pointer-events: auto !important;
}
/* When Cancel is visible, style the button normally */
.cancel-book.section-enabled .btn {
background: #043B55;
color: #ffffff;
border-radius: 10px;
border: none;
cursor: pointer;
font-weight: 700;
box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
}
/* summary card inside cancel section (you already had this) */
.summary-card {
max-width: 100%;
background: #fff;
border-radius: 10px;
padding: 18px 20px;
border: 2px solid rgba(0, 0, 0, 0.12);
box-sizing: border-box;
font-family: Inter, sans-serif;
color: #111;
margin: 0 auto;
}
/* Top row */
.summary-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.summary-left .label {
font-size: 13px;
font-weight: 700;
margin-bottom: 10px;
color: black;
}
.summary-right div {
font-size: 13px;
font-weight: 400;
margin-bottom: 10px;
text-align: right;
color: black;
}
/* AV */
.av-title {
font-size: 13px;
font-weight: 700;
color: black;
margin: 6px 0 10px;
}
.av-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.av-row {
display: flex;
justify-content: space-between;
font-size: 13px;
}
.av-time,
.av-seats {
font-size: 13px;
font-weight: 400;
margin-bottom: 10px;
}
/* main buttons (book visit / preview etc.) */
.submit-wrap {
margin-top: 16px;
}
.btn {
width: 100%;
padding: 12px 26px;
background: #043B55;
color: #ffffff;
border-radius: 10px;
border: none;
cursor: pointer;
font-weight: 700;
box-sizing: border-box;
margin-bottom: 15px;
}
/* small note text under cards */
.submit-note {
font-family: "Inter", sans-serif;
font-size: 13px;
color: #000000;
text-align: left;
margin: 10px 0;
margin-left: 3px;
font-weight: 700;
}
/* make the button INSIDE cancel-book clearly disabled */
.cancel-book.section-enabled .btn {
background: #fff;
/* SAME background */
color: #000;
border-radius: 10px;
border: 3px solid #000;
/* SAME border */
cursor: pointer;
font-weight: 700;
box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
}
/* ---------- Mobile polish (drop this at the end of your CSS) ---------- */
@media (max-width: 520px) {
/* page / body */
body {
padding: 12px 10px;
background-position: center top;
background-size: cover;
-webkit-font-smoothing: antialiased;
}
/* container & panels */
.container,
.panel-wrap,
.page {
width: 100%;
max-width: 420px;
margin: 0 auto;
padding: 0 8px;
box-sizing: border-box;
}
.panel-wrap {
padding: 6px 0 18px;
}
/* header / nav */
.logo {
width: 61px;
height: 61px;
}
nav.primary {
display: none !important;
}
#desktopBookBtn {
display: none !important;
}
.hamburger {
display: flex !important;
}
/* hero */
.page h1 {
font-size: 32px !important;
width: 100%;
max-width: 360px;
margin: 6px auto 8px;
}
.hero .lead {
font-size: 14px !important;
max-width: 320px;
margin-bottom: 12px;
}
/* fields */
.field {
width: calc(100% - 20px);
margin-left: 10px;
margin-right: 10px;
max-width: none;
}
.field .input,
.field .input.compact {
width: 100%;
font-size: 15px;
padding: 12px 14px;
height: 46px;
border-radius: 10px;
}
/* contact row */
#contactLine {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
#contactDisplay {
min-width: 0;
flex: 1 1 auto;
padding: 10px 12px;
font-weight: 700;
border-radius: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: -14px
}
#changeContact {
margin-left: 10px;
}
/* label and inline people row */
.muted-label {
font-size: 16px;
margin-left: 0 !important;
display: block;
}
/* whole row */
.people-row {
display: flex;
align-items: center;
width: 100%;
gap: 8px;
/* IMPORTANT: allow wrapping so row doesn't overflow */
white-space: normal;
}
/* label on the left */
.muted-label-inline {
flex: 1 1 auto;
/* take left side space */
margin-right: 0;
font-size: 15px;
font-weight: 600;
color: black;
text-align: left;
}
/* Hover effect for Select Date input */
.date-wrap .input[type="date"]:hover {
border-color: #043B55;
/* brand color */
box-shadow: 0 0 0 2px rgba(4, 59, 85, 0.12);
cursor: pointer;
}
/* Focus (keyboard / click) */
.date-wrap .input[type="date"]:focus {
outline: none;
border-color: #043B55;
box-shadow: 0 0 0 3px rgba(4, 59, 85, 0.18);
}
/* push controls to the right */
#minusPeople {
margin-left: auto;
/* this pushes the group to the right */
}
#peopleCount,
#plusPeople {
margin-left: 6px;
/* spacing between controls */
}
/* touch-friendly sizes */
.small {
width: 40px;
height: 40px;
border-radius: 10px;
font-size: 18px;
}
.counter {
min-width: 52px;
height: 40px;
border-radius: 10px;
font-size: 16px;
}
/* pills / time ranges */
.time-ranges {
gap: 8px;
margin: 12px 0;
justify-content: flex-start;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 6px;
}
.time-ranges::-webkit-scrollbar {
display: none;
}
.pill {
min-width: 96px;
padding: 5px;
border-radius: 10px;
flex: 0 0 auto;
}
/* slots grid -> single column */
.slots-grid {
grid-template-columns: 1fr;
gap: 10px;
margin-top: 12px;
}
.slot {
padding: 10px 12px;
border-radius: 10px;
}
/* info row -> stack */
.info-row {
flex-direction: column;
align-items: stretch;
gap: 10px;
margin-top: 10px;
}
.preview-note-box {
width: 100%;
max-width: none;
font-size: 13px;
}
.summary-inline {
justify-content: flex-end !important;
gap: 12px !important;
flex-wrap: wrap !important;
white-space: normal !important;
}
.summary-item {
flex: 0 0 48% !important;
/* two-column layout on small screens */
box-sizing: border-box !important;
}
.summary-item.unallocated {
justify-content: flex-start !important;
text-align: left !important;
}
.summary-item.allocated {
justify-content: flex-end !important;
text-align: right !important;
}
/* submit area */
.submit-wrap {
margin-top: 18px;
padding-bottom: 18px;
}
.btn {
width: 100%;
padding: 12px 14px;
font-size: 16px;
border-radius: 12px;
}
/* small text adjustments */
.muted,
.people-support,
.summary-item {
font-size: 13px;
line-height: 1.3;
}
/* avoid long fixed widths on mobile that break layout */
.page h1,
.hero .lead,
.preview-note-box,
.summary-inline,
.field {
max-width: 100%;
}
/* small helper: ensure form controls never overflow */
input[type="text"],
input[type="date"],
textarea,
select {
max-width: 100%;
box-sizing: border-box;
}
/* interaction: slightly larger tap areas on interactive elements */
.pill,
.slot,
.small,
.btn {
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.06);
}
}
/* small helpers */
.link {
color: #0b76c6;
text-decoration: underline;
cursor: pointer;
font-size: 14px;
}
.vph-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.56);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 28px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.vph-modal-overlay.show {
display: flex !important;
}
/* modal card */
.vph-modal {
width: 500px;
max-width: 100%;
background: #fff;
border-radius: 12px;
box-shadow: 0 30px 80px rgba(7, 25, 30, 0.45);
padding: 20px 22px;
position: relative;
outline: none;
/* keep on-screen */
overflow: hidden;
/* inner area handles overflow */
display: flex;
flex-direction: column;
gap: 12px;
transition: transform .18s ease, opacity .18s ease;
transform: translateY(0);
}
/* when overlay appears the modal can fade in (optional) */
.vph-modal-overlay.show .vph-modal {
transform: translateY(0);
opacity: 1;
}
/* header */
.vph-modal-header {
text-align: center;
margin-top: 2px;
}
.vph-modal-header h3 {
margin: 0;
font-weight: 700;
color: #3B1E14;
font-size: 18px;
letter-spacing: 0;
font-family: "Inter", sans-serif !important;
/* Add Inter font */
}
/* content area (scrollable) */
.vph-modal .vph-modal-content {
flex: 1 1 auto;
overflow-y: auto;
padding-right: 6px;
}
/* summary box (top) */
.vph-summary-box {
min-height: auto;
/* ✅ allow growth */
height: auto;
/* ✅ dynamic */
background: #f3f4f2;
/* subtle off-white */
border-radius: 8px;
padding: 12px;
margin: 0;
border: 1px solid rgba(0, 0, 0, 0.02);
}
/* two-column summary row */
.vph-summary-row {
display: flex;
gap: 12px;
align-items: flex-start;
justify-content: space-between;
}
.vph-summary-left,
.vph-summary-right {
width: 48%;
display: flex;
flex-direction: column;
gap: 10px;
}
.vph-summary-left .vph-label {
color: #000 !important;
/* Black text */
font-size: 13px;
font-weight: 700 !important;
/* Normal, not bold */
font-family: "Inter", sans-serif !important;
/* Add Inter font */
}
.vph-summary-right {
text-align: right;
color: #0b0b0b;
font-size: 14px;
font-weight: 400;
}
/* AV Experience box */
.vph-av-box {
/* width:562px; */
/* height:114px; */
max-height: 30vh;
overflow-y: auto;
background: #f3f4f2;
border-radius: 8px;
padding: 10px 12px;
border: 1px solid rgba(0, 0, 0, 0.02);
}
.vph-av-box::-webkit-scrollbar {
width: 4px;
}
.vph-av-box::-webkit-scrollbar-thumb {
background: #b5b5b5;
border-radius: 5px;
}
.vph-av-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-radius: 6px;
background: transparent;
}
.vph-av-time {
font-weight: 400;
color: #0b0b0b;
font-size: 14px;
}
.vph-av-meta {
font-weight: 400;
color: #0b0b0b;
font-size: 14px;
}
/* footer area — controls */
.vph-modal-footer {
display: flex;
gap: 12px;
align-items: center;
margin-top: auto;
flex-shrink: 0;
}
/* left icon button (circle) */
.vph-modal-icon-btn {
Width: 66px;
Height: 66px;
border-radius: 10px;
border: 1px solid #043B55;
background: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 6px 18px rgba(7, 25, 30, 0.04);
}
.vph-modal-submit {
width: 100%;
/* responsive */
max-width: 482px;
/* desktop size */
height: 66px;
padding: 12px 36px;
border-radius: 10px;
background: #043B55;
color: #fff;
border: none;
font-weight: 700;
cursor: pointer;
box-shadow: 0 10px 30px rgba(11, 75, 72, 0.14);
}
/* close X */
.vph-modal-close {
position: absolute;
right: 12px;
top: 12px;
width: 36px;
height: 36px;
border-radius: 8px;
border: none;
background: transparent;
font-size: 20px;
cursor: pointer;
color: #6b6b6b;
}
/* ===== Cancel Booking modal helpers (re-use existing .vph-modal styles) ===== */
/* two-step views */
.vph-view {
display: none;
}
.vph-view.active {
display: block;
}
/* scrollable content area if you ever need body text */
/* footer: normal row + column variant for success view */
.vph-modal-footer {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-top: 25px;
}
.vph-modal-footer.column {
flex-direction: column;
align-items: stretch;
gap: 12px;
margin-top: 14px;
}
/* icon button on left */
.vph-modal-icon-btn {
width: 66px;
height: 50px;
border-radius: 10px;
border: 1px solid #043B55;
background: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 6px 18px rgba(7, 25, 30, 0.04);
}
/* primary + ghost buttons for cancel modal */
.vph-modal-submit.primary {
height: 50px;
width: 100%;
padding: 14px 18px;
border-radius: 10px;
background: #043B55;
color: #fff;
border: none;
font-weight: 700;
cursor: pointer;
box-shadow: 0 10px 30px rgba(11, 75, 72, 0.14);
}
.vph-modal-submit.ghost {
height: 50px;
width: 100%;
padding: 12px 16px;
border-radius: 10px;
background: #fff;
color: #0b0b0b;
border: 2px solid rgba(0, 0, 0, 0.08);
font-weight: 700;
cursor: pointer;
}
/* small compact info box (if needed later) */
.vph-summary-box.small {
background: #f7f7f6;
border-radius: 8px;
padding: 10px;
border: 1px solid rgba(0, 0, 0, 0.04);
margin-bottom: 6px;
text-align: center;
}
/* small screens: stack and make scrollable */
@media (max-width: 520px) {
.vph-modal {
width: 94%;
padding: 14px;
border-radius: 12px;
max-height: 92vh;
}
.vph-summary-row {
gap: 10px;
flex-direction: row;
}
.vph-summary-left,
.vph-summary-right {
width: 48%;
}
/*.vph-av-list {*/
/* max-height: 30vh;*/
/*}*/
.vph-modal-footer {
gap: 10px;
align-items: center;
}
.vph-modal-submit {
width: 56%;
padding: 12px;
width: 256px;
}
}
/* optional: thin custom scrollbar inside modal for better UX */
.vph-modal ::-webkit-scrollbar {
height: 8px;
width: 8px;
}
.vph-modal ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.08);
border-radius: 6px;
}
/* make sure text wrapping and truncation are tidy */
.vph-summary-right div,
.vph-summary-left div,
.vph-av-row {
word-break: break-word;
}
.panel {
background: transparent;
}
.date-wrap .form-control[readonly] {
background-color: #fff !important;
color: #000 !important;
}
.disabled-pill {
background-color: #eee !important;
border-color: #ddd !important;
}
.no-slots-message {
margin-top: 10px;
color: #c62828;
font-size: 14px;
font-weight: 600;
}
.no-slots-message-box {
padding: 12px;
margin-top: 10px;
text-align: center;
color: #d9534f;
/* red */
font-weight: 600;
font-size: 14px;
/* background: #ffeaea; */
/* soft red background */
border-radius: 6px;
}
/* make sure summary left labels are visible and take up space */
.summary-row {
display: flex;
align-items: flex-start;
gap: 12px;
}
.summary-left,
.summary-right {
width: 50%;
box-sizing: border-box;
}
.summary-left .label {
display: block;
color: #111 !important;
/* force readable color */
font-size: 13px;
font-weight: 700;
margin-bottom: 10px;
}
.summary-right>div {
display: block;
color: #111 !important;
font-weight: 400;
text-align: right;
margin-bottom: 10px;
}
/* =========================================
CALENDAR HOVER — FINAL GUARANTEED FIX
========================================= */
/* Target the REAL visible flatpickr input */
.date-wrap input.input.form-control {
cursor: pointer;
background-color: #fff !important;
transition: border-color 0.15s ease,
box-shadow 0.15s ease,
background-color 0.15s ease;
}
/* Hover */
.date-wrap input.input.form-control:hover {
border-color: #043B55 !important;
box-shadow: 0 0 0 2px rgba(4, 59, 85, 0.18) !important;
background-color: #fafafa !important;
}
/* Focus */
.date-wrap input.input.form-control:focus {
outline: none !important;
border-color: #043B55 !important;
box-shadow: 0 0 0 3px rgba(4, 59, 85, 0.28) !important;
}
/* Optional: hover anywhere in wrapper */
.date-wrap:hover input.input.form-control {
border-color: #043B55 !important;
}
/* FIX: Cancel Booking button should NOT be full width */
#cancelBookSection .submit-wrap {
display: flex;
gap:12px;
justify-content: center;
flex-wrap: wrap; /* allow stacking */
}
#cancelBookSection .submit-wrap .btn {
width: auto !important;
max-width: none !important;
min-width: 240px;
padding: 12px 32px;
margin-right: 4px
}
/*
bugs follows
*/
/* Interchange font sizes */
/* Make preview note text bigger */
.preview-note-box {
font-size: 16px !important;
font-weight: 500;
line-height: 1.6;
}
/* Make the label slightly smaller */
.field>.muted-label {
font-size: 14px !important;
font-weight: 600;
}
@keyframes shake {
0% {
transform: translateX(0);
}
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(5px);
}
60% {
transform: translateX(-5px);
}
80% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
}
.slot.shake {
animation: shake 0.35s ease;
border-color: #dc2626 !important;
}
.border-field {
border: 1px solid rgba(0, 0, 0, 0.15);
/* visible but subtle */
border-radius: 12px;
/* matches your UI */
padding: 16px 18px;
/* space inside box */
background: #ffffff;
/* white card look */
}
/* age group details Container */
/* Container */
.people-type-list {
margin-top: 12px;
display: grid;
/*grid-template-columns: repeat(2, 1fr); !* 2 by 2 *!*/
column-gap: 24px;
row-gap: 16px;
}
/* Each card */
.people-type-row {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Label */
.people-type-label {
font-weight: 600;
}
/* Controls */
.people-type-controls {
display: flex;
align-items: center;
gap: 10px;
}
.people-total-row {
display: flex;
justify-content: flex-end;
/* ✅ pushes content to right */
align-items: center;
margin-top: 20px;
font-weight: 700;
font-size: 20px;
}
.people-total-label {
margin-right: 15px;
}
.people-total-value {
color: #043B55;
}
/* aadhar & passport */
.id-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 6px;
padding-top: 10px;
padding-bottom: 10px;
}
/* ================================
PREVIEW MODAL – EXTRA DETAILS
================================ */
/* Shared box (re-uses your visual language) */
.breakdown-box {
background: #f3f4f2;
border-radius: 8px;
padding: 14px 16px;
margin-top: 14px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Header row */
.breakdown-header {
display: flex;
justify-content: space-between;
align-items: center;
font-family: "Inter", sans-serif;
font-weight: 700;
font-size: 14px;
color: #111;
margin-bottom: 10px;
}
/* Rows container */
.breakdown-body {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Each row */
.breakdown-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
font-family: "Inter", sans-serif;
font-size: 14px;
}
/* Labels */
.breakdown-label {
color: #111;
font-weight: 500;
}
/* Amount values */
.breakdown-value {
font-weight: 600;
text-align: right;
min-width: 70px;
}
/* Divider line */
.breakdown-divider {
margin: 10px 0;
border-top: 1px solid #d1d5db;
}
/* Total row */
.breakdown-row.total {
font-size: 16px;
font-weight: 700;
}
.breakdown-row.total .breakdown-value {
color: #043B55;
}
/* ================================
ID PROOF SECTION
================================ */
.breakdown-idproof {
margin-top: 8px;
font-family: "Inter", sans-serif;
font-size: 14px;
color: #111;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.breakdown-idproof strong {
font-weight: 700;
}
@media (min-width: 768px) {
.muted-label-inline {
margin-right: 320px;
}
.people-type-list {
grid-template-columns: repeat(2, 1fr);
}
}
@media(min-width: 768px) {
#contactLine {
display: flex;
gap: 360px;
margin-left: -14px;
}
}
@media (max-width: 520px) {
.people-type-list {
grid-template-columns: 1fr;
}
}
@media (min-width: 521px) {
#cancelBookSection .submit-wrap .btn {
min-width: 200px;
width: auto !important;
}
}
/* Mobile */
@media (max-width: 520px) {
#cancelBookSection .submit-wrap {
flex-direction: column;
align-items: stretch;
}
#cancelBookSection .submit-wrap .btn {
width: 100% !important;
min-width: unset !important;
margin-right: 0 !important;
}
}
/* Cancel button – Red */
#cancelBtn {
background: #dc2626 !important; /* red-600 */
color: #fff !important;
border: none;
}
#cancelBtn:hover {
background: #b91c1c !important; /* darker red */
}
/* QR button – Green */
#qrBtn {
background: #16a34a !important; /* green-600 */
color: #fff !important;
border: none;
}
#qrBtn:hover {
background: #15803d !important; /* darker green */
}
</style>
<!-- </head> -->
<body>
<div class="container">
<header class="topbar" role="banner">
<div class="leftbar">
<div class="nav-left">
<!-- logo image — use same img src you already have -->
<img th:src="@{images/VPH-logo_main-EN 1.png}" alt="VPH logo" class="logo">
</div>
<!-- desktop nav (hidden on mobile) -->
<nav class="primary" aria-label="Primary navigation">
<a href="/victoriapublichall/home">Home</a>
<!-- <a href="#">About VPH</a>-->
<a href="/victoriapublichall/visit">Visit</a>
<!--<a href="#">Contact Us</a>-->
</nav>
</div>
<!-- right side: hamburger (mobile) + desktop CTA -->
<div class="rightbar">
<button id="hambtn" class="hamburger" aria-label="Open menu" aria-expanded="false" aria-controls="mobileNav">
<span class="bar bar1" aria-hidden="true"></span>
<span class="bar bar2" aria-hidden="true"></span>
<span class="bar bar3" aria-hidden="true"></span>
</button>
</div>
</header>
<!-- overlay (click to close) -->
<div id="mobileOverlay" class="mobile-overlay" aria-hidden="true"></div>
<!-- mobile nav -->
<nav id="mobileNav" class="mobile-nav" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Mobile menu">
<a href="/victoriapublichall/home">Home</a>
<a href="#">About VPH</a>
<a href="/victoriapublichall/visit">Visit</a>
<a href="#">Contact Us</a>
</nav>
<main class="page" role="main" aria-labelledby="hero-heading">
<div class="hero" aria-hidden="false">
<h1 id="hero-heading">Victoria Public Hall</h1>
<div class="lead">Experience Chennai's First Town Hall</div>
</div>
<div class="panel-wrap">
<div class="panel" role="region" aria-label="Booking panel">
<div class="people-support">
For booking more than 10 people, please click here:
<!-- <a class="link" href="/victoriapublichall/bulkbooking" onclick="storeMobileForBulk()"> </a>-->
<a class="link" href="javascript:void(0)" onclick="handleBulkBookingClick()">
Bulk Booking Request
</a>
</div>
<!-- phone/email & OTP -->
<div class="field mt-4">
<label class="muted-label">Phone / Email</label>
<div id="contactLine">
<div id="contactDisplay">
</div>
<a id="changeContact" href="/victoriapublichall/register">
Change
</a>
</div>
</div>
<div id="bookVisitSection" class="book-vist">
<!-- select date -->
<div class="field">
<label class="muted-label">Select Date</label>
<div class="date-wrap">
<input id="dateInput" class="input" type="date" aria-label="Select date">
</div>
</div>
<div class="field">
<div class="date-note">
<strong>Note:</strong> Victoria Public Hall remains closed every Tuesday for maintenance.
</div>
</div>
<div class="field">
<label class="muted-label">Identity Proof</label>
<div class="id-row">
<!-- DROPDOWN -->
<select id="idType" class="input compact">
<option value="">Select ID Proof</option>
</select>
<!-- NUMBER INPUT -->
<input type="text" id="idNumber" class="input compact" placeholder="Enter ID Number" disabled />
<small id="idError" style="color:#dc2626; display:none; font-weight:600;"></small>
</div>
</div>
<div class="field" style="margin-bottom:30px;">
<!-- SINGLE LABEL -->
<label class="muted-label-inline">Number of people</label>
<!-- DYNAMIC LIST -->
<div id="peopleTypeList" class="people-type-list"></div>
<div class="field">
<div class="people-total-row">
<span class="people-total-label">Total Amount</span>
<span id="totalAmount" class="people-total-value">₹0</span>
</div>
</div>
</div>
<hr>
<br>
<!-- Pick your time ranges -->
<!-- time-range pills -->
<div class="field border-field">
<label class="muted-label">Pick your time</label>
<div class="time-ranges" id="timeRanges" role="tablist" aria-label="Time ranges">
</div>
<div id="noAvInfoMsg" class="no-slots-message-box" style="display:none;">
</div>
<!-- <div id="noAvInfoMsg" class="no-slots-message-box" style="display:none;">
During the above time slots, the public will be permitted only within the premises of Victoria Public Hall.
Entry into the building will not be allowed.
</div>-->
<!--<div class="info-row">
<div class="preview-note-box">
<Strong>Your visit includes a audio visual experience, please select as many shows as required
to
accommodate all visitors.</Strong>
</div>
<div class="summary-inline" aria-hidden="false">
<div class="summary-item unallocated" aria-label="Guest unallocated">
<span>Guest unallocated</span>
<strong>03</strong>
</div>
<div class="summary-item allocated" aria-label="Guest allocated">
<span>Guest allocated</span>
<strong>03</strong>
</div>
</div>
</div>-->
</div>
<!-- slots grid -->
<div class="field border-field">
<div class="info-row">
<div class="preview-note-box">
<Strong>Your visit includes a audio visual experience, please select as many shows as required
to
accommodate all visitors.</Strong>
</div>
<div class="summary-inline" aria-hidden="false">
<div class="summary-item unallocated" aria-label="Guest unallocated">
<span>Guest unallocated</span>
<strong>03</strong>
</div>
<div class="summary-item allocated" aria-label="Guest allocated">
<span>Guest allocated</span>
<strong>03</strong>
</div>
</div>
</div>
<label style="margin:8px 0 6px 0" class="muted-label">Pick one or more shows for all visitors</label>
<div id="slotsGrid" class="slots-grid" aria-live="polite"></div>
</div>
<div class="submit-wrap">
<p class="submit-note">Your booking details will be sent to you via email/SMS.</p>
<button id="previewBtn" class="btn">Preview & Submit</button>
</div>
</div>
<!-- CANCEL BOOK SECTION (DISABLED BUT VISIBLE) -->
<div id="cancelBookSection" class="cancel-book">
<div class="summary-card">
<!-- TOP SUMMARY ROW -->
<div class="summary-row">
<div class="summary-left">
<div class="label">Visiting on</div>
<div class="label">Time</div>
<div class="label">Total Guest(s)</div>
</div>
<div class="summary-right">
<div id="confirmDate"></div>
<div id="confirmRange"></div>
<div id="confirmPeople"></div>
</div>
</div>
<div class="av-title">AV Experience</div>
<!-- AV LIST -->
<div class="av-list">
<div class="av-row">
<div class="av-time"></div>
<div class="av-seats"></div>
</div>
</div>
<!-- Disabled cancel button inside cancel-book -->
<div class="submit-wrap">
<button id="cancelBtn" class="btn" type="button">Cancel Booking</button>
<button id="qrBtn" class="btn" type="button">Get BookingQR</button>
</div>
</div>
<p class="submit-note">
You can book additional visits only after completing/canceling your current visit.
</p>
</div>
</div>
</div>
</main>
</div>
<!-- Booking Preview Modal (add to end of <body>) -->
<div id="vphBookingModalOverlay" class="vph-modal-overlay" aria-hidden="true">
<div id="vphBookingModal" class="vph-modal" role="dialog" aria-modal="true" aria-labelledby="vphModalTitle"
tabindex="-1">
<header class="vph-modal-header">
<h3 id="vphModalTitle">Your visit details</h3>
</header>
<section class="vph-summary-box" aria-live="polite">
<div class="vph-summary-row">
<div class="vph-summary-left">
<div class="vph-label">Visiting on</div>
<div class="vph-label">Time</div>
<div class="vph-label">ID Proof</div>
<div class="vph-label">ID Number</div>
<div class="vph-label">Total Guest(s)</div>
</div>
<div class="vph-summary-right">
<div id="vphModalDate">—</div>
<div id="vphModalRange">—</div>
<div id="vphModalIdType">—</div>
<div id="vphModalIdNumber">—</div>
<div id="vphModalPeople">—</div>
</div>
</div>
<!--class="vph-av-title"-->
<div id="vphAvTitle" class="vph-label">AV Experience</div>
<div id="vphModalSlotsList" aria-hidden="false">
<!-- populated by JS -->
</div>
</section>
<!-- Guest Breakdown -->
<section class="vph-summary-box breakdown-box">
<div class="breakdown-header">
<span>Guest Details</span>
<span>Amount</span>
</div>
<div id="previewGuestBreakdown" class="breakdown-body">
<!-- Filled by JS -->
</div>
<div class="breakdown-divider"></div>
<div class="breakdown-row total">
<span>Total Amount</span>
<span id="previewTotalAmount">₹0</span>
</div>
</section>
<footer class="vph-modal-footer">
<button id="vphModalBack" class="vph-modal-icon-btn" aria-label="Back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M15 18l-6-6 6-6" stroke="#0f4b4a" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<button id="vphModalSubmit" class="vph-modal-submit">Proceed to Payment</button>
</footer>
<!-- <button id="vphModalClose" class="vph-modal-close" aria-label="Close dialog">×</button> -->
<button class="vph-modal-close preview-close" aria-label="Close dialog">×</button>
</div>
</div>
<!-- Cancel Booking – 2-step modal -->
<div id="vphModalOverlay" class="vph-modal-overlay" aria-hidden="true">
<div id="vphModal" class="vph-modal" role="dialog" aria-modal="true" aria-labelledby="vphModalTitleConfirm"
tabindex="-1">
<div class="vph-modal-body">
<!-- VIEW 1: Confirmation -->
<div id="vphViewConfirm" class="vph-view" role="document" aria-hidden="true">
<header class="vph-modal-header">
<h3 id="vphModalTitleConfirm">Are you sure you want to cancel your visit?</h3>
</header>
<footer class="vph-modal-footer">
<button id="vphConfirmBack" class="vph-modal-icon-btn" aria-label="Go back">
<!-- left arrow icon -->
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M15 18l-6-6 6-6" stroke="#0f4b4a" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<button id="vphConfirmYes" class="vph-modal-submit primary">
Yes, Cancel booking
</button>
</footer>
</div>
<!-- VIEW 2: Result / Success -->
<div id="vphViewCancelled" class="vph-view" role="document" aria-hidden="true">
<header class="vph-modal-header">
<h3 id="vphModalTitleCancelled">Booking Cancelled!</h3>
</header>
<footer class="vph-modal-footer column">
<button id="vphGoMain" class="vph-modal-submit ghost">
Go back to main page
</button>
<button id="vphBookAnother" class="vph-modal-submit primary">
Book another visit
</button>
</footer>
</div>
</div>
<!-- Close X -->
<!-- <button id="vphModalClose" class="vph-modal-close" aria-label="Close dialog">×</button> -->
<button class="vph-modal-close cancel-close" aria-label="Close dialog">×</button>
</div>
</div>
<div id="pageLoader"
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); z-index: 9999; text-align: center;">
<div style="position: relative; top: 50%; transform: translateY(-50%);">
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;"></div>
<p style="margin-top: 1rem; font-size: 1.2rem; color: #007bff;">Please wait...</p>
</div>
</div>
<script th:replace="~{fragments/user-script :: script}"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script> -->
<script type="text/javascript">
function showLoader() {
$('#pageLoader').show();
}
function hideLoader() {
$('#pageLoader').hide();
}
$('#pageLoader').hide();
//checking
/* Full corrected booking JS */
let activeRange = null;
let activeTimeId = null;
let bookingId = null;
let bookingemail = null;
let bookingmob_no = null;
let people = 0;
// const MIN_PEOPLE = 1;
// const MAX_PEOPLE = 10;
// Selected time slots map
const chosenSlots = new Map();
let slotsGrid;
let unallocEl, allocEl, dateInput;
let peopleTypes = [];
let isIdNumberValid = false;
$(document).ready(function () {
updateGuestSummary();
// DOM refs (safe)
slotsGrid = document.getElementById('slotsGrid');
// minusPeople = document.getElementById('minusPeople');
// plusPeople = document.getElementById('plusPeople');
// peopleCount = document.getElementById('peopleCount');
unallocEl = document.querySelector('.summary-item.unallocated strong');
allocEl = document.querySelector('.summary-item.allocated strong');
dateInput = document.getElementById('dateInput');
//Booking / cancel sections
const bookVisit = document.getElementById("bookVisitSection");
const cancelBook = document.getElementById("cancelBookSection");
const cancelBtn = document.getElementById("cancelBtn");
const previewBtn = document.getElementById('previewBtn');
const qrBtn = document.getElementById("qrBtn");
// Basic session handling
let hasBooking = sessionStorage.getItem("hasBooking") === "true";
let hasuser = sessionStorage.getItem("hasuser");
let bookingdetails = sessionStorage.getItem("bookingdetails");
console.log("→ hasBooking:", hasBooking);
console.log("→ hasuser:", hasuser);
console.log("→ bookingdetails:", bookingdetails);
if (hasuser === null || hasuser.trim() === "") {
window.location.href = "/victoriapublichall/register";
return;
}
const Displayname = document.getElementById("contactDisplay");
if (Displayname && hasuser) Displayname.textContent = hasuser;
let validBooking = false;
if (bookingdetails && bookingdetails.trim() !== "") {
try {
let b = JSON.parse(bookingdetails);
validBooking = b && b.id; // valid only if id exists
} catch (e) {
validBooking = false;
}
}
if (hasBooking || validBooking) {
bookVisit.classList.add("section-hidden");
cancelBook.classList.add("section-enabled");
cancelBtn.disabled = false;
if (validBooking) {
let b = JSON.parse(bookingdetails);
console.log("details>>>>>>>>>",b)
bookingId = b.id;
bookingemail = b.email;
bookingmob_no = b.mob_no;
bookingorderid=b.order_id;
console.log(bookingemail);
console.log(bookingmob_no);
console.log("bookingorderid>>>>",bookingorderid);
document.getElementById("confirmDate").textContent = formatDisplayDate(b.visitingDate);
document.getElementById("confirmRange").textContent = b.timeRange;
document.getElementById("confirmPeople").textContent =
String(b.no_of_people).padStart(2, "0");
const avList = document.querySelector(".av-list");
avList.innerHTML = "";
b.allocatedSlots.forEach(s => {
console.log(s);
avList.innerHTML += `
<div class="av-row">
<div class="av-time">${s.time} (${s.language})</div>
<div class="av-seats">${s.people} Seat(s)</div>
</div>
`;
});
}
} else {
cancelBook.classList.remove("section-enabled");
bookVisit.classList.remove("section-hidden");
cancelBtn.disabled = true;
}
if (!dateInput) return;
// flatpickr setup
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const maxDate = new Date(today);
maxDate.setDate(maxDate.getDate() + 7);
const fp = flatpickr(dateInput, {
dateFormat: "Y-m-d",
altInput: true,
altFormat: "d-m-Y",
minDate: today,
maxDate: maxDate,
allowInput: false,
clickOpens: true,
disable: [
function (date) {
if (!(date instanceof Date)) return false;
return date.getDay() === 2;
}
],
onReady: function (selectedDates, dateStr, instance) {
instance.altInput.setAttribute("placeholder", "dd-mm-yyyy");
},
onChange: function (selectedDates, dateStr, instance) {
if (!dateStr) instance.altInput.setAttribute("placeholder", "dd-mm-yyyy");
// When date changes we must reset state and reload ranges
activeTimeId = null;
activeRange = null;
chosenSlots.clear();
if (slotsGrid) slotsGrid.innerHTML = "";
loadTimeRanges();
}
});
fp.setDate(today);
// enable opening on click/focus
dateInput.addEventListener('focus', () => fp.open());
dateInput.addEventListener('click', () => fp.open());
loadTimeRanges();
// modal submit handler
document.getElementById("vphModalSubmit").addEventListener("click", function () {
const userInput = (document.getElementById("contactDisplay") || {}).textContent?.trim() || "";
const visitDate = (dateInput || {}).value || "";
const no_of_people = people;
const timeId = activeTimeId;
const slotDetails = [...chosenSlots].map(([time, obj]) => {
const el = Array.from(document.querySelectorAll(".slot"))
.find(s => s.dataset.time === time);
return {
slotId: Number(el?.dataset?.slotId || 0),
people: obj.seats
};
});
/*if (slotDetails.length === 0) {
Swal.fire({ icon: "warning", text: "Please select at least one show." });
return;
}
*/
// Close modal before showing any Swal to avoid focus-lock issues
const overlayEl = document.getElementById('vphBookingModalOverlay');
if (overlayEl) {
overlayEl.classList.remove("show");
document.body.style.overflow = "";
// blur modal to remove active focus
const modalEl = document.getElementById('vphBookingModal');
if (modalEl) modalEl.blur();
}
$.ajax({
type: "POST",
url: "/victoriapublichall/bookingapi/saveRegistration",
data: {
userInput: userInput,
visitDate: visitDate,
no_of_people: no_of_people,
timeId: timeId,
slotDetails: JSON.stringify(slotDetails),
peopleDetails: JSON.stringify(
peopleTypes
.filter(p => p.count > 0)
.map(p => ({
id: p.id,
count: p.count
}))
),
idType: $("#idType").val() || null,
idDetails: $("#idNumber").val() || null,
totalAmount: $("#totalAmount").text().replace("₹", ""),
source: 1
},
success: function (response) {
console.log("response", response)
// response might be string or JSON. handle both.
if (response.status === "success") {
// 1️ Save booking reference
const refId = response.refId;
const amount = $("#totalAmount").text().replace("₹", "");
sessionStorage.setItem("confirmrefid", response.refId);
showLoader();
paymentStart(refId, amount);
} else if (response.status === "error") {
hideLoader();
Swal.fire({ icon: "error", title: "Booking Failed", text: "Slots are fully booked. Please select another slot" });
} else {
hideLoader();
Swal.fire({ icon: "error", title: "Booking Failed", text: response.message || "Failed" });
}
},
error: function (xhr) {
Swal.fire({ icon: "error", title: "Booking Failed", text: xhr.responseText || "Something went wrong!" });
}
});
});
/* PEOPLE COUNT – DB DRIVEN (JDBC) */
const MAX_TOTAL_PEOPLE = 10;
/* TOTAL PEOPLE */
function getTotalPeople() {
return peopleTypes.reduce((sum, p) => sum + p.count, 0);
}
/* TOTAL AMOUNT */
function getTotalAmount() {
return peopleTypes.reduce((sum, p) => {
return sum + (p.count * p.amount);
}, 0);
}
/* UPDATE TOTAL AMOUNT UI */
function updateTotalAmount() {
const total = getTotalAmount();
const totalEl = document.getElementById("totalAmount");
if (totalEl) {
totalEl.textContent = "₹" + total;
}
}
/* RENDER PEOPLE UI */
function renderPeopleTypes() {
const container = document.getElementById("peopleTypeList");
if (!container) return;
container.innerHTML = "";
const total = getTotalPeople();
peopleTypes.forEach((item, index) => {
const row = document.createElement("div");
row.className = "people-type-row";
row.innerHTML = `
<div class="people-type-label">${item.user_type}
<span class="people-amount">(₹${item.amount})</span>
</div>
<div class="people-type-controls">
<button class="small minus-btn" data-i="${index}">−</button>
<div class="counter">${String(item.count).padStart(2, "0")}</div>
<button class="small plus-btn" data-i="${index}">+</button>
</div>
`;
container.appendChild(row);
});
// minus
container.querySelectorAll(".minus-btn").forEach(btn => {
btn.onclick = () => {
const i = btn.dataset.i;
if (peopleTypes[i].count > 0) {
peopleTypes[i].count--;
renderPeopleTypes();
syncPeopleWithBooking();
}
};
});
// plus
container.querySelectorAll(".plus-btn").forEach(btn => {
btn.onclick = () => {
if (getTotalPeople() >= MAX_TOTAL_PEOPLE) return;
const i = btn.dataset.i;
peopleTypes[i].count++;
renderPeopleTypes();
syncPeopleWithBooking();
};
});
// disable + buttons when max reached
if (total >= MAX_TOTAL_PEOPLE) {
container.querySelectorAll(".plus-btn").forEach(btn => btn.disabled = true);
}
}
function syncPeopleWithBooking() {
const total = getTotalPeople();
// single source of truth
window.people = total;
people = total;
// auto adjust slot allocations
autoAllocateExtraGuests();
deallocateExtraGuests();
// update summary counts correctly
updateGuestSummary();
updateTotalAmount();
}
/* LOAD PEOPLE TYPES FROM DB */
$.ajax({
url: "/victoriapublichall/bookingapi/getamountbyusertype",
type: "GET",
success: function (data) {
peopleTypes = data.map(row => ({
id: row.id,
user_type: row.user_type,
amount: row.amount,
count: 0,
maxCount: row.user_type &&
row.user_type.trim().toLowerCase() === 'family' ? 5 : 0
}));
renderPeopleTypes();
syncPeopleWithBooking();
},
error: function () {
Swal.fire({
icon: "error",
text: "Unable to load people categories"
});
}
});
$.ajax({
url: "/victoriapublichall/bookingapi/getidproofs",
type: "GET",
success: function (data) {
const select = document.getElementById("idType");
data.forEach(row => {
const opt = document.createElement("option");
opt.value = row.id; // store ID
opt.textContent = row.id_name; // show Aadhar Card / Passport
select.appendChild(opt);
});
},
error: function () {
Swal.fire({
icon: "error",
text: "Unable to load ID proof types"
});
}
});
/* ENABLE ID NUMBER ONLY AFTER SELECT */
const idType = document.getElementById("idType");
const idNumber = document.getElementById("idNumber");
const idError = document.getElementById("idError");
idType.addEventListener("change", function () {
const selectedText = this.options[this.selectedIndex].text.toLowerCase();
idNumber.disabled = !this.value;
idNumber.value = "";
idError.style.display = "none";
idError.textContent = "";
if (!this.value) return;
idNumber.focus();
// 🔹 AADHAR
if (selectedText.includes("aadhar")) {
idNumber.maxLength = 12;
idNumber.placeholder = "Enter 12-digit Aadhar Number";
idNumber.oninput = function () {
this.value = this.value.replace(/\D/g, "");
if (this.value.length === 12) {
idError.style.display = "none";
isIdNumberValid = true;
} else {
idError.textContent = "Aadhar number must be exactly 12 digits";
idError.style.display = "block";
isIdNumberValid = false;
}
};
}
// 🔹 PASSPORT
else if (selectedText.includes("passport")) {
idNumber.maxLength = 9;
idNumber.placeholder = "Enter Passport Number";
idNumber.oninput = function () {
this.value = this.value.replace(/[^a-zA-Z0-9]/g, "");
if (this.value.length >= 6 && this.value.length <= 9) {
idError.style.display = "none";
isIdNumberValid = true;
} else {
idError.textContent = "Passport number must be 6–9 characters";
idError.style.display = "block";
isIdNumberValid = false;
}
};
}
});
qrBtn && qrBtn.addEventListener("click", function () {
if (!bookingorderid) {
Swal.fire({
icon: "error",
text: "Order ID not found"
});
return;
}
const qrUrl =
"/victoriapublichall/bookingQRCode?orderid=" + bookingorderid;
window.open(qrUrl, "_blank");
});
});
function formatDisplayDate(dateStr) {
if (!dateStr || typeof dateStr !== "string") return "--";
// Trim & normalize
const s = dateStr.trim();
// If already in an ISO-ish form, handle both "yyyy-mm-dd" and "dd-mm-yyyy"
// Detect by first token length: if first token has length 4 -> yyyy-mm-dd
const parts = s.split("-");
let yyyy, mm, dd;
if (parts.length === 3) {
if (parts[0].length === 4) {
// format: yyyy-mm-dd
yyyy = parts[0];
mm = parts[1];
dd = parts[2];
} else {
// assume dd-mm-yyyy
dd = parts[0];
mm = parts[1];
yyyy = parts[2];
}
} else {
// fallback parse attempt — try Date constructor
const d = new Date(s);
if (isNaN(d)) return s;
return d.toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" });
}
// safe integer conversion
const y = Number(yyyy), m = Number(mm) - 1, d = Number(dd);
const dateObj = new Date(y, m, d);
if (isNaN(dateObj)) return dateStr;
// Example output: "13 Dec 2025"
return dateObj.toLocaleDateString("en-IN", {
day: "2-digit",
month: "short",
year: "numeric"
});
}
/* ---------- API functions ---------- */
function loadSlotsFromAPI() {
const dateValue = dateInput?.value;
if (!dateValue || !activeTimeId) {
// clear slotsGrid and show message to indicate no selection
if (slotsGrid) slotsGrid.innerHTML = `<div class="no-slots-message-box">Please select a time range to view AV slots.</div>`;
return;
}
$.ajax({
type: "GET",
url: "/victoriapublichall/bookingapi/getTimeSlotDetails",
data: { visitDate: dateValue, timeId: activeTimeId },
success: function (data) {
const slotList = (data || []).map(item => ({
slotId: item.slot_id,
time: item.timing,
seats: item.availCap,
language: item.language
}));
renderSlotsWithData(slotList);
},
error: function () {
console.error("Failed to load slot details");
}
});
}
function renderSlotsWithData(slotArray) {
if (!slotsGrid) return;
slotsGrid.innerHTML = "";
chosenSlots.clear();
if (!slotArray || slotArray.length === 0) {
slotsGrid.innerHTML = `<div class="no-slots-message-box">Sorry, No AV time slots available for the selected date.</div>`;
return;
}
slotArray.forEach(item => {
const time = item.time;
const seats = Number(item.seats);
const slotId = item.slotId;
const language = item.language
const slotEl = document.createElement("div");
slotEl.className = "slot";
slotEl.tabIndex = 0;
slotEl.dataset.time = time;
slotEl.dataset.seats = seats;
slotEl.dataset.originalSeats = seats;
slotEl.dataset.slotId = slotId;
slotEl.dataset.language = language;
if (seats <= 0) {
slotEl.classList.add("disabled");
slotEl.style.border = "1px solid #d9534f";
} else {
slotEl.style.border = "1px solid #28a745";
}
slotEl.innerHTML = `
<div class="left">
<div class="time">${time} <span class="slot-lang">(${language})</span></div>
<div class="meta">${seats <= 0 ? "0 Seats available" : seats + (seats === 1 ? " Seat available" : " Seats available")}</div>
</div>
<div class="choose"></div>
`;
slotEl.addEventListener("click", () => toggleSlot(slotEl));
slotsGrid.appendChild(slotEl);
});
updateGuestSummary();
}
function loadTimeRanges() {
const timeRanges = document.getElementById("timeRanges");
const previewBtn = document.getElementById("previewBtn");
if (!timeRanges) return;
// clear UI + old messages and reset state
timeRanges.innerHTML = "";
const oldMsg = document.getElementById("noSlotsMsg");
if (oldMsg) oldMsg.remove();
activeTimeId = null;
activeRange = null;
chosenSlots.clear();
if (slotsGrid) slotsGrid.innerHTML = "";
if (previewBtn) {
previewBtn.disabled = true;
previewBtn.classList.add("disabled-btn");
}
const visitDate = dateInput?.value;
if (!visitDate) {
return;
}
$.ajax({
type: "GET",
url: "/victoriapublichall/bookingapi/getTimeMaster",
data: { visitDate: visitDate,
view: "book"},
success: function (data) {
if (!data || data.length === 0) {
// No ranges at all
if (slotsGrid) {
slotsGrid.innerHTML = `<div class="no-slots-message-box">Sorry,No AV time slots available for the selected date.</div>`;
}
const msg = document.createElement("div");
msg.id = "noSlotsMsg";
msg.className = "no-slots-message";
msg.textContent = "Sorry, No time slots available for the selected date.";
timeRanges.after(msg);
return;
}
let firstActiveSet = false;
let allZero = true;
// create pills
data.forEach(item => {
if (item.availCap > 0) allZero = false;
const pill = document.createElement("div");
pill.className = "pill";
pill.setAttribute("data-range", item.timing);
pill.setAttribute("data-id", item.time_id);
pill.innerHTML = `${item.timing}<br><small>${item.availCap} Available</small>`;
if (item.availCap === 0) {
pill.classList.add("disabled-pill");
pill.style.pointerEvents = "none";
} else if (!firstActiveSet) {
pill.classList.add("active");
activeRange = item.timing;
activeTimeId = item.time_id;
firstActiveSet = true;
}
timeRanges.appendChild(pill);
});
attachPillClickHandlers();
// if any pill active, load slots for it and enable preview
if (activeTimeId) {
loadSlotsFromAPI();
onTimeSelected(activeTimeId,false);
if (previewBtn) {
previewBtn.disabled = false;
previewBtn.classList.remove("disabled-btn");
}
}
// if every pill had zero availability -> show message in slots area
if (allZero) {
if (slotsGrid) {
slotsGrid.innerHTML = `<div class="no-slots-message-box">Sorry, No AV time slots available for the selected date and time.</div>`;
}
const msg = document.createElement("div");
msg.id = "noSlotsMsg";
msg.className = "no-slots-message";
msg.textContent = "No time slots available for the selected date.";
timeRanges.after(msg);
if (previewBtn) {
previewBtn.disabled = true;
previewBtn.classList.add("disabled-btn");
}
}
},
error: function () {
console.error("Failed to load time ranges");
}
});
}
function attachPillClickHandlers() {
const timeRanges = document.getElementById("timeRanges");
if (!timeRanges) return;
// since we recreate pills every time, it's safe to add listeners directly
timeRanges.querySelectorAll('.pill').forEach(pill => {
pill.addEventListener('click', () => {
// if this pill is disabled, ignore
if (pill.classList.contains('disabled-pill')) return;
// RESET message + Swal state on EVERY time change
resetAvInfoState();
// clear active class
timeRanges.querySelectorAll('.pill').forEach(x => x.classList.remove('active'));
pill.classList.add('active');
activeRange = pill.getAttribute('data-range');
activeTimeId = pill.getAttribute('data-id');
// reset chosen slots and reload slot grid
chosenSlots.clear();
if (slotsGrid) slotsGrid.innerHTML = "";
onTimeSelected(activeTimeId,true);
});
});
}
let requiresSlot = true; // default safe value
let avInfoShownOnce = false;
let lastTimeId = null;
function onTimeSelected(timeId,fromClick = false) {
fetch(`/victoriapublichall/bookingapi/slot-rule?timeId=${timeId}`)
.then(res => {
if (!res.ok) throw new Error("Slot rule API failed");
return res.json();
})
.then(data => {
requiresSlot = Boolean(data.requiresSlot);
const message = (data.message || "").trim();
const slotsGrid = document.getElementById("slotsGrid");
const avSection = slotsGrid ? slotsGrid.closest(".field") : null;
const infoBox = document.getElementById("noAvInfoMsg");
if (!avSection || !infoBox) return;
if (requiresSlot) {
avSection.style.display = "block";
infoBox.style.display = "none";
infoBox.textContent = "";
chosenSlots.clear();
loadSlotsFromAPI();
}
else {
avSection.style.display = "none";
chosenSlots.clear();
updateGuestSummary();
// show DB-driven message
if (message) {
infoBox.textContent = message;
infoBox.style.display = "block";
// show Swal only when switching to a NEW timeId
if (fromClick) {
Swal.fire({
icon: "info",
title: "Important Information",
text: message,
confirmButtonText: "OK",
allowOutsideClick: false
});
}
} else {
infoBox.style.display = "none";
}
}
lastTimeId = timeId;
})
.catch(err => {
console.error("Slot rule error:", err);
});
}
function resetAvInfoState() {
const infoMsg = document.getElementById("noAvInfoMsg");
if (infoMsg) infoMsg.style.display = "none";
avInfoShownOnce = false;
}
function updateGuestSummary() {
let totalAllocated = 0;
chosenSlots.forEach(obj => {
totalAllocated += Number(obj?.seats || 0);
});
const allocated = Math.min(people, totalAllocated);
const unallocated = Math.max(people - allocated, 0);
if (unallocEl) unallocEl.textContent = String(unallocated).padStart(2, '0');
if (allocEl) allocEl.textContent = String(allocated).padStart(2, '0');
const unallocItem = document.querySelector('.summary-item.unallocated');
const allocItem = document.querySelector('.summary-item.allocated');
// Remove old states
unallocItem?.classList.remove('text-red', 'text-green');
allocItem?.classList.remove('text-red', 'text-green');
// Apply new colors
if (allocated === people) {
allocItem?.classList.add('text-green'); // fully allocated
} else {
unallocItem?.classList.add('text-red'); // still missing allocation
}
}
//ak minus function
function deallocateExtraGuests() {
let totalAllocated = 0;
chosenSlots.forEach(obj => {
totalAllocated += Number(obj?.seats || 0);
});
// Nothing to remove
if (totalAllocated <= people) return;
let extraToRemove = totalAllocated - people;
for (const [time, obj] of chosenSlots.entries()) {
if (extraToRemove <= 0) break;
const slotEl = [...document.querySelectorAll('#slotsGrid .slot')]
.find(s => s.dataset.time === time);
if (!slotEl) continue;
const originalSeats = Number(slotEl.dataset.originalSeats || 0);
const usedSoFar = Number(obj.seats || 0);
const removeSeats = Math.min(usedSoFar, extraToRemove);
const newUsed = usedSoFar - removeSeats;
const newRemaining = originalSeats - newUsed;
if (newUsed > 0) {
// preserve language
chosenSlots.set(time, {
seats: newUsed,
language: obj.language
});
} else {
chosenSlots.delete(time);
slotEl.classList.remove('chosen');
const choose = slotEl.querySelector('.choose');
if (choose) choose.textContent = '';
}
slotEl.dataset.seats = newRemaining;
const meta = slotEl.querySelector('.meta');
if (meta) {
meta.textContent =
`${newRemaining} Seat${newRemaining === 1 ? '' : 's'} available`;
}
slotEl.classList.remove('disabled');
extraToRemove -= removeSeats;
}
}
//plus function
function autoAllocateExtraGuests() {
let currentlyAllocated = 0;
chosenSlots.forEach(obj => {
currentlyAllocated += Number(obj?.seats || 0);
});
let remainingGuests = people - currentlyAllocated;
if (remainingGuests <= 0) return;
const chosenSlotNodes = document.querySelectorAll('#slotsGrid .slot.chosen');
chosenSlotNodes.forEach(slotEl => {
if (remainingGuests <= 0) return;
const time = slotEl.dataset.time;
const originalSeats = Number(slotEl.dataset.originalSeats || 0);
const slotObj = chosenSlots.get(time);
const usedSoFar = Number(slotObj?.seats || 0);
const remainingCap = originalSeats - usedSoFar;
if (remainingCap <= 0) return;
const extraUse = Math.min(remainingCap, remainingGuests);
const newUsed = usedSoFar + extraUse;
const newRemaining = originalSeats - newUsed;
// keep language intact
chosenSlots.set(time, {
seats: newUsed,
language: slotObj.language
});
slotEl.dataset.seats = newRemaining;
const meta = slotEl.querySelector('.meta');
if (meta) {
meta.textContent =
`${newRemaining} Seat${newRemaining === 1 ? '' : 's'} available`;
}
if (newRemaining === 0) {
slotEl.classList.add('disabled');
} else {
slotEl.classList.remove('disabled');
}
remainingGuests -= extraUse;
});
}
function toggleSlot(slotEl) {
const time = slotEl.dataset.time;
if (chosenSlots.has(time)) {
slotEl.classList.add('shake');
// remove shake after animation
setTimeout(() => {
slotEl.classList.remove('shake');
}, 350);
return;
}
const currentSeats = Number(slotEl.dataset.seats || 0);
const originalSeats = Number(slotEl.dataset.originalSeats || 0);
/* NOT ENOUGH SEATS FOR SELECTED PEOPLE → SHAKE & STOP */
if (currentSeats < people) {
slotEl.classList.add('shake');
setTimeout(() => slotEl.classList.remove('shake'), 400);
return;
}
if (currentSeats <= 0) return;
document.querySelectorAll('#slotsGrid .slot.chosen').forEach(el => {
el.classList.remove('chosen', 'disabled');
const choose = el.querySelector('.choose');
const meta = el.querySelector('.meta');
const origSeats = Number(el.dataset.originalSeats || 0);
el.dataset.seats = origSeats;
if (choose) choose.textContent = '';
if (meta) {
meta.textContent =
`${origSeats} Seat${origSeats === 1 ? '' : 's'} available`;
}
});
// Reset selection
chosenSlots.clear();
const seatsUsed = Math.min(currentSeats, people);
const remainingSeats = currentSeats - seatsUsed;
//chosenSlots.set(time, seatsUsed);
chosenSlots.set(time, {
seats: seatsUsed,
language: slotEl.dataset.language
});
slotEl.classList.add('chosen');
slotEl.dataset.seats = remainingSeats;
const choose = slotEl.querySelector('.choose');
const meta = slotEl.querySelector('.meta');
if (choose) choose.textContent = '✓';
if (meta) {
meta.textContent =
`${remainingSeats} Seat${remainingSeats === 1 ? '' : 's'} available`;
}
if (remainingSeats === 0) {
slotEl.classList.add('disabled');
}
updateGuestSummary();
}
/* Validation helpers */
function isEmail(text) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text); }
function isPhone(text) { const d = (text || '').replace(/\D/g, ''); return d.length === 10; }
function isValidContact(text) { const v = text.trim(); return isEmail(v) || isPhone(v); }
</script>
<script>
(function () {
const hambtn = document.getElementById('hambtn');
const mobileNav = document.getElementById('mobileNav');
const mobileOverlay = document.getElementById('mobileOverlay');
const desktopBook = document.getElementById('desktopBookBtn');
const mobileBook = document.getElementById('mobileBook');
if (!hambtn || !mobileNav || !mobileOverlay) {
// nothing to do if elements missing
} else {
function openMenu() {
hambtn.setAttribute('aria-expanded', 'true');
mobileNav.classList.add('open'); mobileNav.setAttribute('aria-hidden', 'false');
mobileOverlay.classList.add('show'); mobileOverlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
const first = mobileNav.querySelector('a'); if (first) first.focus();
}
function closeMenu() {
hambtn.setAttribute('aria-expanded', 'false');
mobileNav.classList.remove('open'); mobileNav.setAttribute('aria-hidden', 'true');
mobileOverlay.classList.remove('show'); mobileOverlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
hambtn.focus();
}
hambtn.addEventListener('click', () => {
const isOpen = hambtn.getAttribute('aria-expanded') === 'true';
if (isOpen) closeMenu(); else openMenu();
});
mobileOverlay.addEventListener('click', closeMenu);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && mobileNav.classList.contains('open')) closeMenu();
});
mobileNav.querySelectorAll('a').forEach(a => {
a.addEventListener('click', () => {
// close after clicking any mobile link
closeMenu();
});
});
// ensure menu closes when resizing to desktop
window.addEventListener('resize', () => {
if (window.innerWidth > 900 && mobileNav.classList.contains('open')) closeMenu();
});
}
// Optional: wire desktop Book a Visit to the same booking action as mobile
if (desktopBook && mobileBook) {
desktopBook.addEventListener('click', (e) => {
e.preventDefault();
// mimic mobile behavior for demo: show alert (replace with real action)
alert('Book a Visit clicked — open booking flow here.');
});
mobileBook.addEventListener('click', (e) => {
// mobile link closes menu via handler above and then can navigate or open booking flow
// if you want to override, uncomment below
// e.preventDefault(); alert('Mobile Book clicked (replace with action).');
});
}
})();
</script>
<!-- Booking preview Button -->
<script>
(function () {
const previewBtn = document.getElementById('previewBtn');
const overlay = document.getElementById('vphBookingModalOverlay');
const modal = document.getElementById('vphBookingModal');
const modalDate = document.getElementById('vphModalDate');
const modalRange = document.getElementById('vphModalRange');
const modalPeople = document.getElementById('vphModalPeople');
const modalSlotsList = document.getElementById('vphModalSlotsList');
const closeBtn = document.querySelector('.preview-close');
const backBtn = document.getElementById('vphModalBack');
const submitBtn = document.getElementById('vphModalSubmit');
if (!previewBtn || !overlay || !modal) {
console.warn('VPH modal: required elements not found.');
return;
}
function gatherData() {
// 1️ DATE
const dateInput = document.getElementById('dateInput');
const dateVal = dateInput?.value ? new Date(dateInput.value) : null;
const dateFormat = dateVal
? dateVal.toLocaleDateString("en-IN", { day: '2-digit', month: 'short', year: 'numeric' })
: "";
// TIME RANGE (active pill)
const activePill = document.querySelector('.time-ranges .pill.active');
const timeRangeText = activePill
? activePill.getAttribute("data-range")
: "";
// PEOPLE BREAKDOWN
const peopleBreakdown = peopleTypes
.filter(p => p.count > 0)
.map(p => ({
label: p.user_type,
count: p.count,
amount: p.amount,
total: p.count * p.amount
}));
// TOTAL AMOUNT
const totalAmount = peopleBreakdown.reduce(
(sum, p) => sum + p.total, 0
);
// ID PROOF
const idTypeSelect = document.getElementById("idType");
const idNumberInput = document.getElementById("idNumber");
const idProof = {
type: idTypeSelect?.value || "",
label: idTypeSelect?.selectedOptions[0]?.text || "",
number: idNumberInput?.value || ""
};
const slots = [...chosenSlots].map(([time, obj]) => ({
time,
language: obj.language,
seats: obj.seats + " Seat(s)"
}));
return { dateFormat, timeRangeText, people, slots, peopleBreakdown, totalAmount, idProof };
}
// SHOW MODAL WITH FILLED DATA
function showModal() {
const {
dateFormat,
timeRangeText,
people,
slots, peopleBreakdown,
totalAmount,
idProof
} = gatherData();
// Date validation
if (!dateFormat) {
Swal.fire({
icon: "warning",
text: "Please select a date before previewing."
});
document.getElementById('dateInput').focus();
return;
}
if (!idProof.type) {
Swal.fire({
icon: "warning",
title: "ID proof required",
text: "Please select an identity proof."
}).then(() => {
idTypeSelect?.focus();
});
return;
}
if (!idProof.number || idProof.number.trim() === "") {
Swal.fire({
icon: "warning",
title: "ID Number Required",
text: "Please enter your ID number to continue.",
confirmButtonText: "OK",
allowOutsideClick: false
}).then(() => {
idNumberInput.focus();
});
return;
}
if (!isIdNumberValid) {
Swal.fire({
icon: "error",
title: "Invalid ID Number",
text: "Please enter a valid ID number before proceeding."
});
idNumberInput.focus();
return; // BLOCK SUBMIT
}
if (!people || people <= 0) {
Swal.fire({
icon: "warning",
title: "People count required",
text: "Please select at least one visitor before proceeding."
}).then(() => {
document.querySelector(".people-type-list")?.focus();
});
return;
}
// Slot selection validation
if (requiresSlot && slots.length === 0) {
Swal.fire({
icon: "warning",
text: "Please select at least one show before previewing."
});
return;
}
modalDate.textContent = dateFormat;
modalRange.textContent = timeRangeText;
modalPeople.textContent = people;
// ADD ID PROOF HERE
const idTypeEl = document.getElementById("vphModalIdType");
const idNumberEl = document.getElementById("vphModalIdNumber");
if (idTypeEl) {
idTypeEl.textContent = idProof?.label?.trim() || "—";
}
if (idNumberEl) {
idNumberEl.textContent = idProof?.number?.trim() || "—";
}
modalSlotsList.innerHTML = "";
if (slots.length === 0) {
modalSlotsList.innerHTML = `
<div class="vph-av-row">
<div class="vph-av-time">No shows selected</div>
</div>`;
} else {
slots.forEach(s => {
console.log(s);
const row = document.createElement("div");
row.className = "vph-av-row";
row.innerHTML = `
<div class="vph-av-time">
${s.time} <span class="slot-lang">(${s.language})</span>
</div>
<div class="vph-av-meta">${s.seats}</div>
`;
modalSlotsList.appendChild(row);
});
}
const breakdownEl = document.getElementById("previewGuestBreakdown");
const totalAmountEl = document.getElementById("previewTotalAmount");
breakdownEl.innerHTML = "";
peopleBreakdown.forEach(p => {
const row = document.createElement("div");
row.className = "breakdown-row";
row.innerHTML = `
<span class="breakdown-label">
${p.label} × ${p.count}
</span>
<span class="breakdown-value">
₹${p.total}
</span>
`;
breakdownEl.appendChild(row);
});
totalAmountEl.textContent = "₹" + totalAmount;
overlay.classList.add("show");
modal.focus();
document.body.style.overflow = "hidden";
}
// CLOSE MODAL
function closeModal() {
overlay.classList.remove("show");
document.body.style.overflow = "";
previewBtn.focus();
}
previewBtn.addEventListener(
"click",
function (e) {
e.preventDefault();
showModal();
},
true
);
// Close options
overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); });
closeBtn?.addEventListener("click", closeModal);
backBtn?.addEventListener("click", closeModal);
})();
</script>
<script>
(function () {
// Button inside the cancel-card
const cancelBtn = document.getElementById('cancelBtn');
const overlay = document.getElementById('vphModalOverlay');
const modal = document.getElementById('vphModal');
const viewConfirm = document.getElementById('vphViewConfirm');
const viewCancelled = document.getElementById('vphViewCancelled');
const confirmBack = document.getElementById('vphConfirmBack');
const confirmYes = document.getElementById('vphConfirmYes');
const goMain = document.getElementById('vphGoMain');
const bookAnother = document.getElementById('vphBookAnother');
const closeBtn = document.querySelector('.cancel-close');
if (!cancelBtn || !overlay || !modal) {
console.warn('Cancel modal: required elements not found.');
return;
}
function showView(view) {
[viewConfirm, viewCancelled].forEach(v => {
if (!v) return;
v.classList.remove('active');
v.setAttribute('aria-hidden', 'true');
});
if (view === 'confirm' && viewConfirm) {
viewConfirm.classList.add('active');
viewConfirm.setAttribute('aria-hidden', 'false');
setTimeout(() => confirmYes && confirmYes.focus(), 40);
} else if (view === 'cancelled' && viewCancelled) {
viewCancelled.classList.add('active');
viewCancelled.setAttribute('aria-hidden', 'false');
setTimeout(() => bookAnother && bookAnother.focus(), 40);
}
}
function openModal(initialView) {
overlay.classList.add('show');
overlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
showView(initialView || 'confirm');
}
function closeModal() {
overlay.classList.remove('show');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
cancelBtn && cancelBtn.focus();
}
cancelBtn.addEventListener('click', function (e) {
e.preventDefault();
openModal('confirm');
});
closeBtn && closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
confirmBack && confirmBack.addEventListener('click', closeModal);
confirmYes && confirmYes.addEventListener('click', function () {
if (!bookingId) {
Swal.fire({
icon: "error",
text: "Invalid booking. Try again."
});
return;
}
$.ajax({
type: "POST",
url: "/victoriapublichall/bookingapi/cancelBooking",
data: { id: bookingId },
success: function (res) {
if (res.status === "success") {
showView('cancelled');
} else {
closeCancelModal();
Swal.fire({
icon: "error",
text: res.message || "Cancellation failed"
});
}
},
error: function () {
closeCancelModal();
Swal.fire({
icon: "error",
text: "Server error. Try again later."
});
}
});
});
// success view actions
goMain && goMain.addEventListener('click', function () {
window.location.href = '/victoriapublichall/home'; // change if needed
});
bookAnother && bookAnother.addEventListener('click', function () {
let userentry = null;
if (bookingmob_no && bookingmob_no.trim() !== "") {
userentry = bookingmob_no;
}
else if (bookingemail && bookingemail.trim() !== "") {
userentry = bookingemail;
}
if (userentry) {
// user used mobile or email earlier → continue booking
sessionStorage.setItem("hasBooking", "false");
sessionStorage.setItem("hasuser", userentry);
sessionStorage.setItem("bookingdetails", ""); // very important!
window.location.href = '/victoriapublichall/registerbook';
} else {
// no mobile or email found; force fresh login
window.location.href = '/victoriapublichall/register';
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && overlay.classList.contains('show')) {
closeModal();
}
});
document.addEventListener('focus', function (e) {
if (overlay.classList.contains('show') && !modal.contains(e.target)) {
e.stopPropagation();
modal.focus();
}
}, true);
})();
function closeCancelModal() {
const overlay = document.getElementById('vphModalOverlay');
overlay.classList.remove('show');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
}
</script>
<script>
const redirectPath = '/victoriapublichall/register';
// This runs when the user presses the browser's back button
window.addEventListener('popstate', function (event) {
window.location.href = redirectPath;
});
// Push a dummy state so that the popstate event triggers properly
history.pushState(null, null, location.href);
function storeMobileForBulk() {
const mobile = sessionStorage.getItem("hasuser"); // already stored
if (mobile) {
sessionStorage.setItem("bulkMobile", mobile);
}
}
</script>
<script>
sessionStorage.setItem("bulkMobile", contact);
function handleBulkBookingClick() {
// Get contact from UI / session
const contact =
$("#contactDisplay").text().trim() ||
sessionStorage.getItem("bulkMobile");
if (!contact) {
Swal.fire("Error", "Contact not found", "error");
return;
}
// Call backend to check existing booking
$.get("/victoriapublichall/bookingapi/checkBulkBooking",
{ contact: contact },
function (res) {
if (res.status === "found") {
window.location.href =
"/victoriapublichall/acknowledgement?refId=" + res.refId;
} else {
sessionStorage.setItem("bulkMobile", contact);
window.location.href =
"/victoriapublichall/bulkbooking";
}
}
).fail(function () {
Swal.fire("Error", "Unable to verify booking", "error");
});
}
</script>
</body>
</html>