|
|
<!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 loadsdocument.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>
|