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.
 
 

1479 lines
43 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPH - Registration</title>
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"
integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8="
crossorigin="anonymous"></script>
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Montserrat';
background: linear-gradient(135deg, #eff6ff 0%, #f0fdfa 100%);
min-height: 100vh;
padding: 2rem 1rem;
}
.container {
max-width: 64rem;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2.5rem;
font-weight: bold;
color: #1f2937;
margin-bottom: 0.5rem;
}
.header p {
color: #6b7280;
font-size: 1.1rem;
}
.progress-bar {
margin-bottom: 2rem;
}
.progress-steps {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
}
.step {
width: 2rem;
height: 2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 500;
background-color: #e5e7eb;
color: #6b7280;
transition: all 0.3s ease;
}
.step.active {
background-color: #3b82f6;
color: white;
}
.step.completed {
background-color: #10b981;
color: white;
}
.step-line {
width: 3rem;
height: 0.25rem;
background-color: #e5e7eb;
margin: 0 0.5rem;
transition: all 0.3s ease;
}
.step-line.completed {
background-color: #3b82f6;
}
.step-label {
text-align: center;
margin-top: 1rem;
}
.step-label p {
font-size: 0.875rem;
color: #6b7280;
}
.form-container {
background: white;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
.step-content {
display: none;
}
.step-content.active {
display: block;
}
.step-header {
text-align: center;
margin-bottom: 1.5rem;
}
.step-header h2 {
font-size: 1.5rem;
font-weight: bold;
color: #1f2937;
margin-bottom: 0.5rem;
}
.step-header p {
color: #6b7280;
}
.booking-type-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: -24px;
}
.booking-type-btn {
padding: 0.5rem;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
background: white;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.booking-type-btn:hover {
transform: scale(1.02);
border-color: #d1d5db;
}
.booking-type-btn.selected {
border-color: #3b82f6;
background-color: #eff6ff;
color: #1d4ed8;
}
.booking-type-btn .icon {
font-size: 2rem;
margin-bottom: 0.75rem;
}
.booking-type-btn h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.booking-type-btn p {
font-size: 0.875rem;
opacity: 0.75;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.5rem;
}
.form-input {
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.help-text {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.time-slots-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.time-slot-btn {
padding: 1rem;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
background: white;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.time-slot-btn:hover {
transform: scale(1.02);
border-color: #d1d5db;
}
.time-slot-btn.selected {
border-color: #3b82f6;
background-color: #eff6ff;
color: #1d4ed8;
}
.time-slot-btn .icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.time-slot-btn h3 {
font-weight: 600;
margin-bottom: 0.25rem;
}
.time-slot-btn p {
font-size: 0.875rem;
opacity: 0.75;
}
.slot-selection {
margin-top: 2rem;
}
.slot-header {
background-color: #eff6ff;
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
}
.slot-header h3 {
font-size: 1.125rem;
font-weight: 600;
color: #1e40af;
margin-bottom: 0.5rem;
}
.selection-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
color: #1d4ed8;
}
.selection-count {
background-color: #dbeafe;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-weight: 500;
}
.selection-complete {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
color: #059669;
font-weight: 500;
font-size: 0.875rem;
}
.slots-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1rem;
margin: 0 auto;
padding-bottom: 0;
overflow: visible; /* no more horizontal scroll */
}
.slot-group {
background-color: #f9fafb;
padding: 1rem;
border-radius: 0.5rem;
flex: 1 1 140px; /* grow, shrink, base 140px */
min-width: 120px; /* never smaller than this */
max-width: 180px;
}
.slot-group h4 {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.75rem;
text-align: center;
padding-bottom: 0.25rem;
}
.slot-group h4::after {
width: 20px;
height: 2px;
}
.slot-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.slot-btn {
aspect-ratio: 1;
border: 2px solid #d1d5db;
border-radius: 0.5rem;
background: white;
cursor: pointer;
font-size: 0.75rem;
font-weight: 500;
transition: all 0.2s ease;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0.4rem 0;
}
.slot-btn:hover:not(:disabled) {
transform: scale(1.05);
border-color: #3b82f6;
background-color: #eff6ff;
}
.slot-btn.selected {
border-color: #10b981;
background-color: #d1fae5;
color: #065f46;
}
.slot-btn.occupied {
border-color: #fca5a5;
background-color: #fee2e2;
color: #dc2626;
cursor: not-allowed;
}
.slot-btn.disabled {
border-color: #e5e7eb;
background-color: #f9fafb;
color: #9ca3af;
cursor: not-allowed;
}
.slot-btn .check-icon {
position: absolute;
top: -0.25rem;
right: -0.25rem;
width: 0.75rem;
height: 0.75rem;
background-color: #10b981;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.5rem;
color: white;
}
.slot-legend {
display: flex;
justify-content: center;
gap: 1.5rem;
margin-top: 1.5rem;
font-size: 0.875rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.legend-box {
width: 1rem;
height: 1rem;
border: 2px solid;
border-radius: 0.25rem;
}
.legend-box.available {
background: white;
border-color: #d1d5db;
}
.legend-box.selected {
background: #d1fae5;
border-color: #10b981;
}
.legend-box.occupied {
background: #fee2e2;
border-color: #fca5a5;
}
.legend-box.disabled {
background: #f9fafb;
border-color: #e5e7eb;
}
.confirmation-header {
text-align: center;
margin-bottom: 1.5rem;
}
.success-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.confirmation-header h2 {
font-size: 1.5rem;
font-weight: bold;
color: #1f2937;
margin-bottom: 0.5rem;
}
.confirmation-header p {
color: #6b7280;
}
.booking-summary {
background-color: #f9fafb;
padding: 1.5rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.booking-summary h3 {
font-weight: 600;
font-size: 1.125rem;
margin-bottom: 1rem;
}
.summary-item {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #e5e7eb;
}
.summary-item:last-child {
border-bottom: none;
}
.summary-item span:first-child {
color: #6b7280;
}
.summary-item span:last-child {
font-weight: 500;
text-transform: capitalize;
}
.important-note {
background-color: #eff6ff;
padding: 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
color: #1e40af;
}
.navigation-buttons {
display: flex;
justify-content: space-between;
margin-top: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
font-size: 1rem;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background-color: #3b82f6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #2563eb;
transform: scale(1.02);
}
.btn-secondary {
background-color: #e5e7eb;
color: #374151;
}
.btn-secondary:hover:not(:disabled) {
background-color: #d1d5db;
}
.error-message {
color: #dc2626;
font-size: 0.875rem;
margin-top: 1.5rem;
display: none;
align-items: center;
gap: 0.5rem;
}
.error-message.show {
display: flex;
}
.error-message::before {
content: "⚠️";
font-size: 1rem;
}
/* batch-card status colors */
.time-slot-btn.status-ok {
border-color: #10b981; /* green */
background: #ecfdf5;
}
.time-slot-btn.status-low {
border-color: #f59e0b; /* amber */
background: #fffbeb;
}
.time-slot-btn.status-critical {
border-color: #ef4444; /* red */
background: #fee2e2;
}
.time-slot-btn.status-full {
border-color: #9ca3af; /* gray */
background: #f3f4f6;
color: #9ca3af;
}
.time-slot-btn.status-full .available-count { display: none; }
.time-slot-btn:disabled { cursor: not-allowed; opacity: .65; }
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.booking-type-btn h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1.5rem;
}
.slots-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin: 0 auto;
padding-bottom: 0;
overflow: visible;
}
.form-container {
padding: 1rem;
}
.booking-type-grid,
.time-slots-grid {
grid-template-columns: repeat(2, 1fr);
height: 225px;
}
.form-grid {
display: grid !important;
grid-template-columns: 1fr !important;
gap: 1rem !important;
}
.form-group.full-width {
grid-column: auto !important;
}
.slot-legend {
flex-wrap: wrap;
gap: 1rem;
}
.navigation-buttons {
flex-direction: row !important;
justify-content: space-between;
gap: 0;
margin-top: 5rem;
}
.navigation-buttons .btn {
flex: 1; /* let them grow equally */
max-width: 30%; /* so there’s a little gutter */
}
.slot-selection {
margin-top: 6rem;
}
}
</style>
<body>
<div class="container">
<div class="header">
<h1>Victoria Public Hall <br> Experiential Museum - Registration</h1>
</div>
<div class="progress-bar" id="progressBar">
<div class="progress-steps">
<div class="step active" data-step="1">1</div>
<div class="step-line"></div>
<div class="step" data-step="2">2</div>
<div class="step-line"></div>
<div class="step" data-step="3">3</div>
<div class="step-line"></div>
<div class="step" data-step="4">4</div>
</div>
<div class="step-label">
<p>Step <span id="currentStepNum">1</span> of 4: <span id="stepName">Booking Type</span></p>
</div>
</div>
<div class="form-container">
<!-- Step 1: Booking Type Selection -->
<div class="step-content active" id="step1">
<div class="step-header">
<h2>Book your Visit to VPH</h2>
</div>
<div class="booking-type-grid">
<button class="booking-type-btn" data-type="individual">
<div class="icon">👤</div>
<h3>Individual Booking</h3>
<p>Max 10</p>
</button>
<button class="booking-type-btn" data-type="group">
<div class="icon">👥</div>
<h3>Bulk Booking</h3>
<p>Max 50</p>
</button>
</div>
<div class="error-message" id="typeError"></div>
</div>
<!-- Step 2: Visit Details -->
<div class="step-content" id="step2">
<div class="step-header">
<h2>Visit Details</h2>
</div>
<div class="form-grid">
<div class="form-group">
<label>📅 Select Date</label>
<input type="date" id="visitDate" class="form-input">
<div class="error-message" id="dateError"></div>
</div>
<div class="form-group" id="membersGroup">
<label>👥 Number of Persons</label>
<input type="number" id="memberCount" class="form-input" min="1" max="10" step="1" oninput="this.value = this.value.replace(/\D+/g, '')"/>
<p class="help-text">Maximum 50 members allowed</p>
<div class="error-message" id="membersError"></div>
</div>
</div>
</div>
<!-- Step 3: Time Slot Selection -->
<div class="step-content" id="step3">
<div class="step-header">
<h2>Choose Your batch and number of tickets</h2>
</div>
<div class="time-slots-grid">
<button class="time-slot-btn" data-batch="1">
<div class="icon">🕐</div>
<h3>8:00-10:00 AM</h3>
<p><span class="available-count">100</span> available slots</p>
</button>
<button class="time-slot-btn" data-batch="2">
<div class="icon">🕐</div>
<h3>10:30 AM-12:30 PM</h3>
<p><span class="available-count">100</span> available slots</p>
</button>
<button class="time-slot-btn" data-batch="3">
<div class="icon">🕐</div>
<h3>1:00-3:00 PM</h3>
<p><span class="available-count">100</span> available slots</p>
</button>
<button class="time-slot-btn" data-batch="4">
<div class="icon">🕐</div>
<h3>3:30-5:30 PM</h3>
<p><span class="available-count">100</span> available slots</p>
</button>
</div>
<div class="slot-selection" id="slotSelection" style="display: none;">
<div class="slot-header">
<!--
<h3>Select Slots for <span id="selectedTimeSlot"></span></h3>
-->
<h3>Select group-wise to experience Chennaiyin Cinema and Chennaiyin Olichithram</h3>
<div class="selection-info">
<span id="selectionText">Select 1 slot for individual booking</span>
<span class="selection-count">
<span id="selectedCount">0</span> / <span id="requiredCount">1</span> selected
</span>
</div>
<div class="selection-complete" id="selectionComplete" style="display: none;">
✅ Selection complete!
</div>
</div>
<div class="slot-legend">
<div class="legend-item">
<div class="legend-box available"></div>
<span>Available</span>
</div>
<div class="legend-item">
<div class="legend-box selected"></div>
<span>Selected</span>
</div>
<div class="legend-item">
<div class="legend-box occupied"></div>
<span>Occupied</span>
</div>
<div class="legend-item">
<div class="legend-box disabled"></div>
<span>Disabled</span>
</div>
</div>
<br>
<div class="slots-container" id="slotsContainer">
<!-- Slots will be generated by JavaScript -->
</div>
</div>
<div class="error-message" id="batchError"></div>
<div class="error-message" id="slotError"></div>
</div>
<!-- Step 4: Personal Details -->
<div class="step-content" id="step4">
<div class="step-header">
<h2>Please provide your details</h2>
</div>
<div class="form-grid">
<div class="form-group">
<label>Name</label>
<input type="text" id="fullName" class="form-input" placeholder="Enter your full name">
<div class="error-message" id="nameError"></div>
</div>
<div class="form-group">
<label>Phone Number</label>
<input type="tel" id="phoneNumber" class="form-input" placeholder="Enter 10-digit phone number" maxlength="10">
<div class="error-message" id="phoneError"></div>
</div>
<div class="form-group full-width">
<label>Address</label>
<textarea id="address" class="form-input" placeholder="Enter your complete address" rows="3"></textarea>
<div class="error-message" id="addressError"></div>
</div>
<div class="form-group">
<label>Aadhaar Number</label>
<input type="text" id="aadharNumber" class="form-input" placeholder="Enter 12-digit Aadhaar number" maxlength="12">
<div class="error-message" id="aadharError"></div>
</div>
<div class="form-group">
<label>Age</label>
<input type="number" id="age" class="form-input" placeholder="Enter your age" min="1" max="120">
<div class="error-message" id="ageError"></div>
</div>
</div>
</div>
<div class="navigation-buttons" id="navigationButtons">
<button class="btn btn-secondary" id="backBtn" disabled>Back</button>
<button class="btn btn-primary" id="nextBtn">Next</button>
</div>
</div>
</div>
<script>
// --- Batch start/end times (in minutes from midnight) ---
const SLOT_WINDOWS = {
'1': { startM: 8 * 60, endM: 10 * 60 }, // 8:00–10:00
'2': { startM: 10 * 60 + 30, endM: 12 * 60 + 30 }, // 10:30–12:30
'3': { startM: 13 * 60, endM: 15 * 60 }, // 1:00–3:00
'4': { startM: 15 * 60 + 30, endM: 17 * 60 + 30 } // 3:30–5:30
};
function todayYMD() {
return new Date().toISOString().split('T')[0];
}
// Return true if the batch should be time-blocked for the current selection
function isSlotTimeBlocked(batchId) {
if (!bookingData.date) return false;
const chosen = bookingData.date;
const today = todayYMD();
if (chosen < today) return true; // past date ⇒ block all
if (chosen > today) return false; // future date ⇒ allow all
// Same day: block if the batch has started already
const nowMins = new Date().getHours() * 60 + new Date().getMinutes();
const win = SLOT_WINDOWS[batchId];
if (!win) return false;
// Block as soon as start time has passed
return nowMins >= win.startM;
}
// Apply/refresh disabled state on the four batch buttons
function applyTemporalDisabling() {
document.querySelectorAll('.time-slot-btn').forEach(btn => {
const b = btn.dataset.batch;
const blocked = isSlotTimeBlocked(b);
// keep the visual "full" gray when blocked by time
btn.classList.toggle('status-full', blocked);
btn.disabled = blocked;
// If currently selected and it just became blocked, clear selection & UI
if (blocked && bookingData.batch === b) {
bookingData.batch = '';
bookingData.subSlot = [];
document.getElementById('slotSelection').style.display = 'none';
clearError('batchError');
}
});
}
const GROUP_SIZE = 10;
// Booking form state
let bookingData = {
type: '',
date: '',
members: 1,
batch: '',
subSlot: [],
personalDetails: {
fullName: '',
phone: '',
address: '',
aadhar: '',
age: ''
}
};
let currentStep = 1;
const totalSteps = 4;
// Time slots data
const timeSlots = {
'1': { time: '8:00-10:00 AM', capacity: 0, slots: [] },
'2': { time: '10:30 AM-12:30 PM', capacity: 0, slots: [] },
'3': { time: '1:00-3:00 PM', capacity: 0, slots: [] },
'4': { time: '3:30-5:30 PM', capacity: 0, slots: [] }
};
// --- Helper to build slots array ---
function buildSlots(capacity, occupiedSet) {
const arr = [];
for (let i = 1; i <= capacity; i++) {
arr.push({ id: i, occupied: occupiedSet.has(i) });
}
return arr;
}
function fetchOccupiedSeats(slotId) {
return $.ajax({
url: '/victoriapublichall/api/occupied-seats',
method: 'GET',
dataType: 'json',
data: { bookingDate: bookingData.date, slotId }
});
}
function setBatchVisual(btn, available, capacity) {
const pct = capacity ? (available / capacity) : 0;
btn.classList.remove('status-ok','status-low','status-critical','status-full');
btn.disabled = false;
if (available <= 0) {
btn.classList.add('status-full');
btn.disabled = true; // full: cannot pick
} else if (pct <= 0.10 || available <= 5) {
btn.classList.add('status-critical'); // red: almost full
} else if (pct <= 0.25) {
btn.classList.add('status-low'); // amber: getting low
} else {
btn.classList.add('status-ok'); // greenish: plenty
}
const countEl = btn.querySelector('.available-count');
if (countEl) countEl.textContent = available;
}
// Refresh all batches for a date
function refreshAllAvailability() {
if (!bookingData.date) return;
document.querySelectorAll('.time-slot-btn').forEach(btn => {
const b = btn.dataset.batch;
fetchOccupiedSeats(b).done(data => {
const c = btn.querySelector('.available-count');
if (c) c.textContent = data.availableCount;
timeSlots[b].capacity = data.capacity;
timeSlots[b].slots = buildSlots(data.capacity, new Set(data.occupiedSeats || []));
setBatchVisual(btn, data.availableCount, data.capacity);
if (bookingData.batch === b) generateSlotGrid();
applyTemporalDisabling();
});
});
}
// Initialize form
document.addEventListener('DOMContentLoaded', function() {
initializeEventListeners();
updateProgressBar();
updateNavigationButtons();
const today = new Date().toISOString().split('T')[0];
document.getElementById('visitDate').min = today;
});
function initializeEventListeners() {
document.querySelectorAll('.booking-type-btn').forEach(btn => {
btn.addEventListener('click', function() {
selectBookingType(this.dataset.type);
clearError('typeError');
});
});
document.querySelectorAll('.time-slot-btn').forEach(btn => {
btn.addEventListener('click', function() {
selectTimeSlot(this.dataset.batch); });
});
document.getElementById('visitDate').addEventListener('change', e => {
bookingData.date = e.target.value;
clearError('dateError');
refreshAllAvailability();
applyTemporalDisabling();
});
document.getElementById('memberCount').addEventListener('input', e => {
let val = parseInt(e.target.value, 10) || 0; // parse or 0 if empty/non-numeric
if (val < 1) {
val = 1; // force minimum
showError('membersError', 'Number of persons must be at least 1');
} else {
clearError('membersError');
}
// also respect max based on booking type
const max = bookingData.type === 'individual' ? 10 : 50;
if (val > max) {
val = max;
showError('membersError', `Maximum allowed is ${max} persons`);
}
e.target.value = val;
bookingData.members = val;
// clear previously selected seats if necessary
if (bookingData.subSlot.length > val) {
bookingData.subSlot = [];
generateSlotGrid();
}
updateSlotSelection();
});
document.getElementById('fullName').addEventListener('input', e => bookingData.personalDetails.fullName = e.target.value);
document.getElementById('phoneNumber').addEventListener('input', e => {
e.target.value = e.target.value.replace(/\D/g, '');
bookingData.personalDetails.phone = e.target.value;
});
document.getElementById('address').addEventListener('input', e => bookingData.personalDetails.address = e.target.value);
document.getElementById('aadharNumber').addEventListener('input', e => {
e.target.value = e.target.value.replace(/\D/g, '');
bookingData.personalDetails.aadhar = e.target.value;
});
document.getElementById('age').addEventListener('input', e => bookingData.personalDetails.age = e.target.value);
document.getElementById('backBtn').addEventListener('click', goBack);
document.getElementById('nextBtn').addEventListener('click', goNext);
}
function selectBookingType(type) {
bookingData.type = type;
document.querySelectorAll('.booking-type-btn').forEach(b => b.classList.toggle('selected', b.dataset.type === type));
const countInput = document.getElementById('memberCount');
const helpText = document.querySelector('#membersGroup .help-text');
if (type === 'individual') {
countInput.max = 10;
helpText.textContent = 'Max 10 members for individual booking';
} else {
countInput.max = 50;
helpText.textContent = 'Max 50 members for bulk booking';
}
countInput.value = 1;
bookingData.members = 1;
clearError('membersError');
updateSlotSelection();
}
function selectTimeSlot(batch) {
if (!bookingData.date) {
showError('dateError', 'Please select a visit date first');
return;
}
if (isSlotTimeBlocked(batch)) {
showError('batchError', 'This batch is no longer available for the selected date/time.');
return;
}
bookingData.batch = batch;
bookingData.subSlot = [];
document.querySelectorAll('.time-slot-btn').forEach(b => b.classList.remove('selected'));
document.querySelector(`.time-slot-btn[data-batch="${batch}"]`).classList.add('selected');
document.getElementById('slotSelection').style.display = 'block';
updateSlotSelection();
clearError('batchError');
fetchOccupiedSeats(batch)
.done(data => {
const countEl = document.querySelector(`.time-slot-btn[data-batch="${batch}"] .available-count`);
if (countEl) countEl.textContent = data.availableCount;
const occupied = new Set(data.occupiedSeats || []);
timeSlots[batch].capacity = data.capacity;
timeSlots[batch].slots = buildSlots(data.capacity, occupied);
// update only this card’s UI + count
const btn = document.querySelector(`.time-slot-btn[data-batch="${batch}"]`);
if (btn) setBatchVisual(btn, data.availableCount, data.capacity);
generateSlotGrid();
})
.fail(xhr => {
console.error(xhr);
showError('batchError', xhr.responseJSON?.message || 'Unable to load availability');
});
}
function updateSlotSelection() {
const requiredSlots = bookingData.members;
document.getElementById('requiredCount').textContent = requiredSlots;
document.getElementById('selectedCount').textContent = bookingData.subSlot.length;
document.getElementById('selectionText').textContent = `Select ${requiredSlots} slot${requiredSlots>1?'s':''}`;
document.getElementById('selectionComplete').style.display =
(bookingData.subSlot.length === requiredSlots) ? 'block' : 'none';
}
function generateSlotGrid() {
const container = document.getElementById('slotsContainer');
container.innerHTML = '';
if (!bookingData.batch) return;
const slots = timeSlots[bookingData.batch].slots;
const capacity = timeSlots[bookingData.batch].capacity;
const groupCount = Math.ceil(capacity / GROUP_SIZE);
for (let gi = 0; gi < groupCount; gi++) {
const start = gi * GROUP_SIZE;
const end = Math.min(start + GROUP_SIZE, capacity);
const group = slots.slice(start, end);
const availableCount = group.filter(s => !s.occupied).length;
const div = document.createElement('div');
div.className = 'slot-group';
const h4 = document.createElement('h4');
h4.textContent = `Group ${gi + 1}: Slots ${start + 1}-${end} (Available: ${availableCount})`;
div.appendChild(h4);
const chkLabel = document.createElement('label');
chkLabel.style = 'display:flex;justify-content:center;font-size:.75rem;margin:.5rem 0';
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.checked = group.filter(s => !s.occupied).every(s => bookingData.subSlot.includes(s.id));
chk.addEventListener('change', () => handleGroupSelect(gi, chk.checked));
chkLabel.append(chk, document.createTextNode(' Select All'));
div.appendChild(chkLabel);
const grid = document.createElement('div');
grid.className = 'slot-grid';
group.forEach(s => {
const btn = document.createElement('button');
btn.className = 'slot-btn';
btn.textContent = s.id;
if (s.occupied) {
btn.classList.add('occupied');
btn.disabled = true;
} else if (bookingData.subSlot.includes(s.id)) {
btn.classList.add('selected');
const icon = document.createElement('div');
icon.className = 'check-icon';
icon.textContent = '✓';
btn.appendChild(icon);
} else if (bookingData.subSlot.length >= bookingData.members) {
btn.classList.add('disabled');
btn.disabled = true;
}
btn.addEventListener('click', () => selectSlot(s.id));
grid.appendChild(btn);
});
div.appendChild(grid);
container.appendChild(div);
}
}
function handleGroupSelect(gi, checked) {
const slots = timeSlots[bookingData.batch].slots;
const start = gi * GROUP_SIZE;
const end = Math.min(start + GROUP_SIZE, timeSlots[bookingData.batch].capacity);
const group = slots.slice(start, end);
const cap = bookingData.members;
if (checked) {
for (let s of group) {
if (!s.occupied && bookingData.subSlot.length < cap && !bookingData.subSlot.includes(s.id)) {
bookingData.subSlot.push(s.id);
}
}
} else {
group.forEach(s => {
const idx = bookingData.subSlot.indexOf(s.id);
if (idx > -1) bookingData.subSlot.splice(idx, 1);
});
}
updateSlotSelection();
generateSlotGrid();
}
function selectSlot(id) {
const cap = bookingData.members;
const idx = bookingData.subSlot.indexOf(id);
if (idx > -1) bookingData.subSlot.splice(idx, 1);
else if (bookingData.subSlot.length < cap) bookingData.subSlot.push(id);
updateSlotSelection();
generateSlotGrid();
clearError('slotError');
}
function validateStep(step) {
clearAllErrors();
let ok = true;
if (step===1 && !bookingData.type) {
showError('typeError','Please select a booking type');
ok = false;
}
if (step===2) {
if (!bookingData.date) {
showError('dateError','Please select a visit date');
ok = false;
}
if (bookingData.members < 1) {
showError('membersError','Number of persons must be at least 1');
ok = false;
} else if (bookingData.members > (bookingData.type==='individual'?10:50)) {
showError('membersError','Number of persons exceeds maximum allowed');
ok = false;
}
}
if (step===3) {
if (!bookingData.batch) {
showError('batchError','Please select a time batch');
ok = false;
}
if (bookingData.subSlot.length !== bookingData.members) {
showError('slotError',`Please select exactly ${bookingData.members} slot(s)`);
ok = false;
}
}
if (step===4) {
if (!bookingData.personalDetails.fullName.trim()) { showError('nameError','Full name is required'); ok=false; }
if (!bookingData.personalDetails.phone.match(/^[6-9]\d{9}$/)) { showError('phoneError','Enter valid 10-digit phone'); ok=false; }
if (!bookingData.personalDetails.address.trim()) { showError('addressError','Address is required'); ok=false; }
if (!bookingData.personalDetails.aadhar.match(/^\d{12}$/)) { showError('aadharError','Aadhar must be 12 digits'); ok=false; }
if (!bookingData.personalDetails.age.match(/^\d+$/) || parseInt(bookingData.personalDetails.age)<=0) {
showError('ageError','Enter a valid age'); ok=false;
}
}
return ok;
}
// Attach listeners after DOM loads
document.addEventListener("DOMContentLoaded", () => {
// Name
document.getElementById("fullName").addEventListener("input", (e) => {
if (e.target.value.trim()) {
clearError("nameError");
bookingData.personalDetails.fullName = e.target.value;
}
});
// Phone
document.getElementById("phoneNumber").addEventListener("input", (e) => {
const val = e.target.value.trim();
bookingData.personalDetails.phone = val;
if (/^[6-9]\d{9}$/.test(val)) {
clearError("phoneError");
}
});
// Address
document.getElementById("address").addEventListener("input", (e) => {
const val = e.target.value.trim();
bookingData.personalDetails.address = val;
if (val.length > 0) {
clearError("addressError");
}
});
// Aadhaar
document.getElementById("aadharNumber").addEventListener("input", (e) => {
const val = e.target.value.trim();
bookingData.personalDetails.aadhar = val;
if (/^\d{12}$/.test(val)) {
clearError("aadharError");
}
});
// Age
document.getElementById("age").addEventListener("input", (e) => {
const val = e.target.value.trim();
bookingData.personalDetails.age = val;
if (/^\d+$/.test(val) && parseInt(val) > 0) {
clearError("ageError");
}
});
});
function showError(elementId, message) {
const errorElement = document.getElementById(elementId);
errorElement.textContent = message;
errorElement.classList.add('show');
}
function clearError(elementId) {
const errorElement = document.getElementById(elementId);
errorElement.classList.remove('show');
}
function clearAllErrors() {
document.querySelectorAll('.error-message').forEach(error => {
error.classList.remove('show');
});
}
function goNext() {
if (validateStep(currentStep)) {
if (currentStep < totalSteps) {
currentStep++;
updateStep();
} else {
// Submit form
submitForm();
}
}
}
function goBack() {
if (currentStep > 1) {
currentStep--;
updateStep();
}
}
function updateStep() {
// Hide all steps
document.querySelectorAll('.step-content').forEach(step => {
step.classList.remove('active');
});
// Show current step
document
.getElementById(`step${currentStep}`)
.classList.add('active');
updateProgressBar();
updateNavigationButtons();
clearAllErrors();
}
function updateProgressBar() {
// Update step indicators
document.querySelectorAll('.step').forEach((step, index) => {
const stepNum = index + 1;
step.classList.remove('active', 'completed');
if (stepNum < currentStep) {
step.classList.add('completed');
} else if (stepNum === currentStep) {
step.classList.add('active');
}
});
// Update step lines
document.querySelectorAll('.step-line').forEach((line, index) => {
line.classList.remove('completed');
if (index + 1 < currentStep) {
line.classList.add('completed');
}
});
// Update step label
const stepNames = ['Booking Type', 'Visit Details', 'Time Selection', 'Personal Details'];
document.getElementById('currentStepNum').textContent = currentStep;
document.getElementById('stepName').textContent = stepNames[currentStep - 1];
// Hide progress bar on confirmation step
const progressBar = document.getElementById('progressBar');
if (currentStep === 5) {
progressBar.style.display = 'none';
} else {
progressBar.style.display = 'block';
}
}
function updateNavigationButtons() {
const backBtn = document.getElementById('backBtn');
const nextBtn = document.getElementById('nextBtn');
const navigationButtons = document.getElementById('navigationButtons');
if (currentStep === 5) {
// Hide navigation buttons on confirmation step
navigationButtons.style.display = 'none';
return;
}
navigationButtons.style.display = 'flex';
// Update back button
backBtn.disabled = currentStep === 1;
// Update next button text
if (currentStep === totalSteps) {
nextBtn.textContent = 'Submit Booking';
} else {
nextBtn.textContent = 'Next';
}
}
function submitForm() {
if (!validateStep(4)) return;
const payload = {
name: bookingData.personalDetails.fullName,
mobileno: bookingData.personalDetails.phone,
address: bookingData.personalDetails.address,
aadhaarNo: bookingData.personalDetails.aadhar,
age: parseInt(bookingData.personalDetails.age || '0', 10),
bookingType: bookingData.type,
noofpersons: bookingData.members,
bookingDate: bookingData.date,
slotId: parseInt(bookingData.batch, 10),
selectedSlots: bookingData.subSlot
};
$.ajax({
url: '/victoriapublichall/api/savebooking',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload),
success: function(summary) {
window.location.href = `/victoriapublichall/booking-summary?refId=${encodeURIComponent(summary.ref_id)}`;
},
error: function(xhr) {
console.error('Error:', xhr.responseText);
alert(xhr.responseJSON?.message || 'Error saving booking');
}
});
}
function displayBookingSummary() {
// Basic booking info
document.getElementById('summaryMembers').textContent = bookingData.members;
document.getElementById('summaryDate').textContent = bookingData.date;
document.getElementById('summaryTimeSlot').textContent =
bookingData.confirmation?.slotTiming || timeSlots[bookingData.batch].time;
// Group with count
const byGroup = {};
bookingData.subSlot.forEach(id => {
const g = Math.ceil(id / 10); // group number
byGroup[g] = (byGroup[g] || 0) + 1; // increment count
});
const lines = Object.keys(byGroup)
.sort((a, b) => a - b)
.map(g => `G${g}: ${byGroup[g]}`)
.join(', ');
document.getElementById('summarySlots').textContent = lines;
// Personal details
document.getElementById('summaryName').textContent = bookingData.personalDetails.fullName;
document.getElementById('summaryPhone').textContent = bookingData.personalDetails.phone;
}
// DOWNLOAD handler
document.getElementById('downloadBtn').addEventListener('click', () => {
const element = document.getElementById('bookingSummary');
html2pdf()
.set({ margin: 10,
filename: 'VPH_booking_summary.pdf',
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
})
.from(element)
.save();
});
</script>
</body>
</html>