PODS

PandaPodHotel

Get ready for something different! Our pods are clean, stylish and contemporary. And you’ll rest easy, because every pod is very spacious, thanks to thoughtful design that considers every detail — right down to your electronics. For the ultimate serene stay, each pod has privacy blinds and is soundproofed. (Get ready to drift into dreams about unicorns.) For anyone with scent sensitivities, we’ve ensured that each pod is ducted with industrial air filtration — so bad smells begone! And by booking a highly efficient pod, you’re doing your part for the environment, because it has a small carbon footprint and intuitive lighting triggered by movement.

Pods Specifications & Usage Guidelines

PandaPodHotel

Pod Usage Guidelines:

  • Occupancy: Only one person is allowed in a pod at any given time. Multiple occupancy, even for short durations, is strictly prohibited.
  • Weight Restrictions: For the safety of our guests and to ensure the structural integrity of the pods, any individual occupying a pod should not exceed the weight limit of 253 lb (115 kg). Guests are kindly requested to adhere to this limit.
  • Age Restriction: Panda Pod Hotel only accepts individual guests who are 18 years of age or older.
    • For safety and comfort, we do not place children or minors in shared rooms with unrelated adult guests.
  • Exceptions may be made for school groups, youth organizations, or similar group bookings when the organization books the entire room and is responsible for the minors’ supervision.

AMENITIES WE PROVIDE

PandaPodHotel

We promise hotel standards, and hotel standards you shall get! Each guest has free use of our parking, plus complimentary shampoo, body wash, hairdryers and toothbrushes. During your stay we will provide you with slippers, towels, hairdryers and charging stations. When checking in, an authorization hold (also known as a deposit) of $80 is required. Deposits are released shortly after check out, if damages have not been incurred.

At Panda Pod Hotel, we take pride in offering thoughtful, guest-focused service. Our front desk is open daily from 8:00 AM to 12:00 AM, ready to assist with check-in, taxi bookings, luggage storage, umbrella rentals, and more. If there’s something you need that we don’t have, we’ll help you find it — because we genuinely care about making your stay as smooth and comfortable as possible.

PandaPodHotel, locker , comfort

LOCKER

Shower Facilities

SHOWERS

PandaPodHotel, High Speed Wifi

WIFI

PandaPodHotel

POWER

comfort, shower, accommodation, budget

HAIR DRIERS

PandaPodHotel

HANGERS

PandaPodHotel

TOWELS

PandaPodHotel, Budget Hotel

ONLINE BOOKING

Panda Pod Hotel, accommodation, comfort

COFFEE + TEA

PandaPodHotel

NO SMOKING

PandaPodHotel

SLIPPERS

Skytrain, YVR Airport

TRANSIT

(function ppc5LoadWhenReady() { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", ppc5LoadWhenReady, { once: true }); return; } (function () { "use strict"; const currentScript = document.currentScript; const widgetRoots = Array.prototype.slice.call(document.querySelectorAll("#ppc5-app")); const root = (currentScript && currentScript.closest("#ppc5-app")) || widgetRoots.find(element => element.dataset.initialized !== "true") || null; if (!root || root.dataset.initialized === "true") return; root.dataset.initialized = "true"; /* ================================================================ PUBLIC DEMO CONFIGURATION — EDIT TEMPORARY DISPLAY VALUES HERE. Set DEMO_MODE to false only after Panda Pod's backend is ready. NEVER add credentials, private promo codes, API keys, or webhooks. ================================================================ */ const DEMO_MODE = true; const DEMO_CONFIG = { timezone: "America/Vancouver", sameDayBookingCutoff: "18:00", defaultDailyCrewAllocation: 5, offerEnabled: true, ratePeriods: [ { validFrom: "2026-08-15", validTo: "2026-09-30", currency: "CAD", taxDisplay: "tax included", lowerPodRate: 57, upperPodRate: 55 } ], allocationOverrides: { "2026-08-22": 0, "2026-08-23": 8, "2026-08-24": 5 }, demoCrewBooked: { "2026-08-24": 5 } }; const API = { config: "/api/v1/crew/public-config", availability: "/api/v1/crew/availability", reservations: "/api/v1/crew/reservations" }; const ROOM_TYPES = Object.freeze({ lower: { roomTypeID: "169530", label: "Lower Pod" }, upper: { roomTypeID: "169527", label: "Upper Pod" } }); const WHATSAPP = "https://wa.me/message/BQOHZPK5JFQCP1?src=qr"; let config = null; let currentStep = 1; let availability = null; let reservationComplete = false; let crewIdPreviewUrls = []; let cardAuthorizationOpened = false; const $ = (selector, context = root) => context.querySelector(selector); const $$ = (selector, context = root) => Array.from(context.querySelectorAll(selector)); async function getCrewPublicConfig() { if (DEMO_MODE) return JSON.parse(JSON.stringify(DEMO_CONFIG)); const response = await fetch(API.config, { credentials: "same-origin", headers: { Accept: "application/json" } }); if (!response.ok) throw new Error("Public crew offer is temporarily unavailable."); return response.json(); } function getVancouverTime() { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: (config && config.timezone) || "America/Vancouver", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" }).formatToParts(new Date()); return Object.fromEntries(parts.filter(part => part.type !== "literal").map(part => [part.type, part.value])); } function getVancouverDate() { const time = getVancouverTime(); return `${time.year}-${time.month}-${time.day}`; } function getSameDayCutoffState() { const time = getVancouverTime(); const [cutoffHour, cutoffMinute] = String((config && config.sameDayBookingCutoff) || "18:00").split(":").map(Number); const currentMinutes = Number(time.hour) * 60 + Number(time.minute); return { date: `${time.year}-${time.month}-${time.day}`, closed: currentMinutes >= cutoffHour * 60 + cutoffMinute }; } function addDays(value, count) { const date = new Date(value + "T12:00:00Z"); date.setUTCDate(date.getUTCDate() + count); return date.toISOString().slice(0, 10); } function dateRange(start, end) { const dates = []; const cursor = new Date(start + "T12:00:00Z"); const stop = new Date(end + "T12:00:00Z"); while (cursor < stop) { dates.push(cursor.toISOString().slice(0, 10)); cursor.setUTCDate(cursor.getUTCDate() + 1); } return dates; } function findRatePeriod(start, end) { return config.ratePeriods.find(period => start >= period.validFrom && end <= addDays(period.validTo, 1)) || null; } function formatDate(value) { return new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" }).format(new Date(value + "T12:00:00Z")); } function formatTime(value) { if (!value) return "—"; const [hours, minutes] = value.split(":").map(Number); return `${hours % 12 || 12}:${String(minutes).padStart(2, "0")} ${hours >= 12 ? "PM" : "AM"}`; } function sanitizePhone(value) { const leadingPlus = String(value).startsWith("+"); const allowed = String(value).replace(/[^0-9+()]/g, "").replace(/\+/g, ""); return `${leadingPlus ? "+" : ""}${allowed}`; } function phoneDigits(value) { const digits = String(value).replace(/\D/g, ""); return digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits; } function formatPhone(value) { const digits = phoneDigits(value); return digits.length === 10 ? `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}` : String(value); } function updateEtaValue() { const hourSelect = $("#ppc5-eta-hour"); const minuteSelect = $("#ppc5-eta-minute"); const selectedPeriod = $("input[name=etaPeriod]:checked"); const period = selectedPeriod ? selectedPeriod.value : "AM"; const eta = $("#ppc5-eta"); if (!hourSelect.value || !minuteSelect.value) { eta.value = ""; return; } let hour = Number(hourSelect.value) % 12; if (period === "PM") hour += 12; eta.value = `${String(hour).padStart(2, "0")}:${minuteSelect.value}`; hourSelect.removeAttribute("aria-invalid"); minuteSelect.removeAttribute("aria-invalid"); $("#ppc5-eta-error").textContent = ""; } function money(value, currency = "CAD") { return new Intl.NumberFormat("en-CA", { style: "currency", currency, minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value); } function selectedPod() { const input = $("input[name=podType]:checked"); return input ? { type: input.value, roomTypeID: input.dataset.roomId, label: ROOM_TYPES[input.value].label } : null; } async function checkCrewAvailability({ checkInDate, checkOutDate, roomTypeID }) { if (!DEMO_MODE) { const query = new URLSearchParams({ checkInDate, checkOutDate, roomTypeID }); const response = await fetch(`${API.availability}?${query}`, { credentials: "same-origin", headers: { Accept: "application/json" } }); if (!response.ok) throw new Error("We could not confirm crew availability."); return response.json(); } await new Promise(resolve => window.setTimeout(resolve, 350)); const podType = Object.keys(ROOM_TYPES).find(key => ROOM_TYPES[key].roomTypeID === roomTypeID); const nights = dateRange(checkInDate, checkOutDate).map(date => { const allocation = Object.prototype.hasOwnProperty.call(config.allocationOverrides, date) ? config.allocationOverrides[date] : config.defaultDailyCrewAllocation; const booked = config.demoCrewBooked[date] || (Number(date.slice(-2)) % 3); let rooms = 6 - (Number(date.slice(-2)) % 4); if ((podType === "lower" && date.endsWith("27")) || (podType === "upper" && date.endsWith("28"))) rooms = 0; return { date, crewAllocation: allocation, crewBooked: booked, crewRemaining: Math.max(0, allocation - booked), cloudbedsRoomsAvailable: rooms }; }); return { available: nights.every(night => night.crewAllocation > 0 && night.crewRemaining > 0 && night.cloudbedsRoomsAvailable > 0), selectedRoomTypeID: roomTypeID, selectedPodType: podType, nights }; } async function createCrewReservation(payload) { if (DEMO_MODE) { await new Promise(resolve => window.setTimeout(resolve, 550)); const cost = calculateCosts(); return { success: true, reservationNumber: `PP-DEMO-${Date.now().toString().slice(-6)}`, paymentStatus: "PENDING", amountDue: cost.total, currency: cost.currency }; } const response = await fetch(API.reservations, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) throw new Error("The reservation could not be created. No booking was made."); return response.json(); } function setError(element, message) { element.setAttribute("aria-invalid", message ? "true" : "false"); const field = element.closest(".ppc5-field"); const holder = field ? field.querySelector(".ppc5-error") : null; if (holder) holder.textContent = message || ""; } function clearErrors(scope = root) { $$("[aria-invalid='true']", scope).forEach(element => element.removeAttribute("aria-invalid")); $$(".ppc5-error", scope).forEach(element => { element.textContent = ""; }); } function focusFirstInvalid(scope) { const element = $("[aria-invalid='true']", scope) || $$(".ppc5-error", scope).find(item => item.textContent); if (!element) return false; element.scrollIntoView({ behavior: "smooth", block: "center" }); const fieldset = element.closest("fieldset"); const focusTarget = element.matches("input,select,button") ? element : (fieldset ? fieldset.querySelector("input") : null) || element; if (typeof focusTarget.focus === "function") focusTarget.focus({ preventScroll: true }); return true; } function validateStep(step) { const panel = $(`.ppc5-panel[data-step="${step}"]`); clearErrors(panel); if (step === 1) { const checkIn = $("#ppc5-checkin"); const checkOut = $("#ppc5-checkout"); const cutoff = getSameDayCutoffState(); const today = cutoff.date; if (!checkIn.value) setError(checkIn, "Choose a check-in date."); else if (checkIn.value < today) setError(checkIn, "Check-in cannot be in the past."); else if (checkIn.value === today && cutoff.closed) { setError(checkIn, "Same-day booking is closed after 6:00 PM. Please contact us on WhatsApp."); showSameDayClosedMessage(); } if (!checkOut.value) setError(checkOut, "Choose a check-out date."); else if (checkIn.value && checkOut.value <= checkIn.value) setError(checkOut, "Check-out must be after check-in."); if (!selectedPod()) { $("#ppc5-pod-error").textContent = "Choose either a Lower Pod or Upper Pod."; $$("input[name=podType]").forEach(input => input.setAttribute("aria-invalid", "true")); } } else if (step === 2) { $$("input[required],select[required]", panel).filter(element => element.type !== "radio").forEach(element => { if (!element.value.trim()) setError(element, "This field is required."); else if (element.type === "email" && !/^\S+@\S+\.\S+$/.test(element.value)) setError(element, "Enter a valid email address."); else if (element.name === "phone" && (!/^\+?[0-9()]+$/.test(element.value) || phoneDigits(element.value).length !== 10)) setError(element, "Enter a 10-digit phone number using numbers and optional + or parentheses."); }); if (!$("input[name=returningCrew]:checked")) { $("#ppc5-returning-error").textContent = "Please select one option."; $$("input[name=returningCrew]").forEach(input => input.setAttribute("aria-invalid", "true")); } else if ($("input[name=returningCrew]:checked").value === "false" && !$("#ppc5-crew-id-file").files.length) { setError($("#ppc5-crew-id-file"), "Select one or more clear RAIC or crew ID images."); } } else if (step === 3) { updateEtaValue(); if (!$("#ppc5-eta").value) { $("#ppc5-eta-error").textContent = "Choose an arrival hour and 15-minute interval."; if (!$("#ppc5-eta-hour").value) $("#ppc5-eta-hour").setAttribute("aria-invalid", "true"); if (!$("#ppc5-eta-minute").value) $("#ppc5-eta-minute").setAttribute("aria-invalid", "true"); } if (!$("input[name=earlyRequested]:checked")) { $("#ppc5-early-error").textContent = "Please select one early check-in option."; $$("input[name=earlyRequested]").forEach(input => input.setAttribute("aria-invalid", "true")); } if (!$("input[name=lateRequested]:checked")) { $("#ppc5-late-error").textContent = "Please select one late checkout option."; $$("input[name=lateRequested]").forEach(input => input.setAttribute("aria-invalid", "true")); } } else if (step === 4) { if (!cardAuthorizationOpened) { $("#ppc5-card-error").textContent = "Open the secure authorization page and complete both card forms first."; $("#ppc5-card-authorization-link").setAttribute("aria-invalid", "true"); } else if (!$("#ppc5-card-complete").checked) { $("#ppc5-card-error").textContent = "Confirm that you completed both secure credit card forms."; $("#ppc5-card-complete").setAttribute("aria-invalid", "true"); } } else if (step === 5 && !$("input[name=policyAccepted]").checked) { $("#ppc5-policy-error").textContent = "You must accept the crew booking terms and hotel policies."; $("input[name=policyAccepted]").setAttribute("aria-invalid", "true"); } return !focusFirstInvalid(panel); } function showStep(step) { currentStep = step; $$(".ppc5-panel").forEach(panel => { const active = Number(panel.dataset.step) === step; panel.classList.toggle("is-active", active); panel.hidden = !active; }); $$(".ppc5-step-tab").forEach((tab, index) => { const number = index + 1; tab.classList.toggle("is-active", number === step); tab.classList.toggle("is-done", number < step); tab.toggleAttribute("disabled", number > step); tab.removeAttribute("aria-current"); if (number === step) tab.setAttribute("aria-current", "step"); }); $("#ppc5-offer").classList.toggle("ppc5-hidden", step !== 1); if (step === 3) updateRequestCharges(); if (step === 4) prepareCardAuthorization(); if (step === 5) renderReview(); const heading = $(`.ppc5-panel[data-step="${step}"] h2`); heading.setAttribute("tabindex", "-1"); heading.focus(); root.scrollIntoView({ behavior: "smooth", block: "start" }); } function getFormData() { return Object.fromEntries(new FormData($("#ppc5-form")).entries()); } function prepareCardAuthorization() { const authorizationUrl = new URL("https://pandapodhotels.com/crewcc/"); authorizationUrl.searchParams.set("checkInDate", $("#ppc5-checkin").value); authorizationUrl.searchParams.set("source", "crew-booking"); $("#ppc5-card-authorization-link").href = authorizationUrl.toString(); } function calculateCosts() { const data = getFormData(); const pod = selectedPod(); const rate = pod && data.checkInDate && data.checkOutDate ? findRatePeriod(data.checkInDate, data.checkOutDate) : null; if (!rate) return { nightly: 0, nights: 0, stay: 0, early: 0, late: 0, total: 0, currency: "CAD", taxDisplay: "" }; const nightly = pod.type === "lower" ? rate.lowerPodRate : rate.upperPodRate; const nights = dateRange(data.checkInDate, data.checkOutDate).length; const stay = nightly * nights; const early = data.earlyRequested === "full_day" ? nightly : data.earlyRequested === "half_day" ? nightly / 2 : 0; const late = data.lateRequested === "full_day" ? nightly : data.lateRequested === "half_day" ? nightly / 2 : 0; return { nightly, nights, stay, early, late, total: stay + early + late, currency: rate.currency, taxDisplay: rate.taxDisplay }; } function updateRequestCharges() { const cost = calculateCosts(); const data = getFormData(); const early = $("#ppc5-early-charge"); const late = $("#ppc5-late-charge"); early.classList.toggle("ppc5-hidden", !data.earlyRequested || data.earlyRequested === "none"); late.classList.toggle("ppc5-hidden", !data.lateRequested || data.lateRequested === "none"); if (!early.classList.contains("ppc5-hidden")) early.textContent = `Estimated early check-in charge: ${money(cost.early, cost.currency)} CAD`; if (!late.classList.contains("ppc5-hidden")) late.textContent = `Estimated late checkout charge: ${money(cost.late, cost.currency)} CAD`; } function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, character => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }[character])); } function renderReview() { const data = getFormData(); const pod = selectedPod(); const cost = calculateCosts(); const earlyLabel = data.earlyRequested === "full_day" ? "Before 11:00 AM — full-day rate" : data.earlyRequested === "half_day" ? "After 11:00 AM — half-day rate" : "Not requested"; const lateLabel = data.lateRequested === "full_day" ? "After 3:00 PM — full-day rate" : data.lateRequested === "half_day" ? "By 3:00 PM — half-day rate" : "Not requested"; const rows = [ ["Check-in", formatDate(data.checkInDate)], ["Check-out", formatDate(data.checkOutDate)], ["Number of nights", cost.nights], ["Pod type", pod.label], ["Nightly crew rate", `${money(cost.nightly, cost.currency)} CAD / night (${cost.taxDisplay})`], ["Stay subtotal", `${money(cost.stay, cost.currency)} CAD`], ["Guest", `${data.firstName} ${data.lastName}`], ["Email", data.email], ["Phone", formatPhone(data.phone)], ["Airline / Company", data.airline], ["Crew role", $("select[name=crewRole] option:checked").textContent], ["ETA", formatTime(data.eta)], ["Early check-in", `${earlyLabel} — ${money(cost.early, cost.currency)} CAD`], ["Late checkout", `${lateLabel} — ${money(cost.late, cost.currency)} CAD`], ["Damage deposit authorization", data.cardAuthorizationComplete === "on" ? "Completed by guest" : "Not completed"], ["Estimated total", `${money(cost.total, cost.currency)} CAD (${cost.taxDisplay})`] ]; $("#ppc5-review").innerHTML = rows.map(([key, value]) => `
${escapeHtml(key)}${escapeHtml(value)}
`).join(""); } function resultCard(title, message, success = false) { return `

${escapeHtml(title)}

${escapeHtml(message)}

${success ? "" : `Contact Us on WhatsApp`}`; } function showSameDayClosedMessage() { const box = $("#ppc5-availability-result"); box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Same-Day Booking Closed", "It is after 6:00 PM in Vancouver. Please contact us on WhatsApp, mention that you are a crew member, and ask for Front Desk."); box.focus(); } function availabilityFailure(result) { if (result.nights.some(night => night.crewAllocation === 0)) return ["Crew Offer Unavailable for Selected Date", "Crew self-booking is not available for one or more selected dates. Please contact us on WhatsApp for alternate availability."]; if (result.nights.some(night => night.crewRemaining === 0)) return ["Crew Allocation Reached", "The crew allocation for one or more selected dates has been reached. Please contact us and we will check whether additional pods can be offered."]; return ["Crew Availability Is Not Available", "Crew availability is not available for one or more selected dates. Please contact us and we will check other options."]; } async function runAvailability(advance) { if (!validateStep(1)) return false; const data = getFormData(); const pod = selectedPod(); const box = $("#ppc5-availability-result"); const button = $("#ppc5-check-availability"); availability = null; box.classList.add("ppc5-hidden"); if (!config) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Crew Offer Is Still Loading", "Please wait a moment, then check availability again."); box.focus(); return false; } if (!config.offerEnabled) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Crew Offer Unavailable", "Crew self-booking is currently unavailable. Please contact us on WhatsApp."); box.focus(); return false; } if (!findRatePeriod(data.checkInDate, data.checkOutDate)) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Crew Offer Unavailable for Selected Date", "The crew offer does not cover all selected stay dates. Please contact us for alternate availability."); box.focus(); return false; } button.disabled = true; button.textContent = "Checking…"; try { const result = await checkCrewAvailability({ checkInDate: data.checkInDate, checkOutDate: data.checkOutDate, roomTypeID: pod.roomTypeID }); availability = result; if (result.available) { const minimum = Math.min(...result.nights.map(night => Math.min(night.crewRemaining, night.cloudbedsRoomsAvailable))); box.className = "ppc5-result ppc5-result--success"; box.innerHTML = resultCard("Crew Availability Confirmed", `${pod.label} is available for all ${result.nights.length} night${result.nights.length === 1 ? "" : "s"}. At least ${minimum} crew pod${minimum === 1 ? "" : "s"} remain per selected night.`, true); $("#ppc5-live").textContent = "Crew availability confirmed for every selected night."; if (advance) showStep(2); } else { const [title, message] = availabilityFailure(result); box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard(title, message); box.focus(); } return result.available; } catch (error) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Availability Check Unavailable", "We could not safely confirm availability. No booking was made. Please retry or contact us on WhatsApp."); box.focus(); return false; } finally { button.disabled = false; button.textContent = "Check Availability"; } } function buildPayload() { const data = getFormData(); const pod = selectedPod(); return { checkInDate: data.checkInDate, checkOutDate: data.checkOutDate, roomTypeID: pod.roomTypeID, podType: pod.type, guest: { firstName: data.firstName.trim(), lastName: data.lastName.trim(), email: data.email.trim(), phone: data.phone.trim(), airline: data.airline.trim(), crewRole: data.crewRole, returningCrew: data.returningCrew === "true" }, arrival: { eta: data.eta, earlyCheckInRequested: data.earlyRequested !== "none", earlyCheckInOption: data.earlyRequested, earlyCheckInTime: null, lateCheckOutRequested: data.lateRequested !== "none", lateCheckOutOption: data.lateRequested, lateCheckOutTime: null }, damageDepositAuthorizationConfirmed: data.cardAuthorizationComplete === "on", policyAccepted: true }; } async function submitBooking(event) { event.preventDefault(); if (!validateStep(5)) return; // Recheck immediately before creation in case the guest crossed 6:00 PM during the form. const cutoff = getSameDayCutoffState(); if ($("#ppc5-checkin").value === cutoff.date && cutoff.closed) { showStep(1); setError($("#ppc5-checkin"), "Same-day booking is closed after 6:00 PM. Please contact us on WhatsApp."); showSameDayClosedMessage(); return; } const button = $("#ppc5-create"); const errorBox = $("#ppc5-submit-error"); errorBox.classList.add("ppc5-hidden"); button.disabled = true; button.textContent = "Creating Reservation…"; try { // FAIL CLOSED: first-time crew images require an approved private upload API. // Do not remove this guard until the backend securely stores the image and // returns an opaque upload token that can be linked to the reservation. const returningCrewSelection = $("input[name=returningCrew]:checked"); if (!DEMO_MODE && returningCrewSelection && returningCrewSelection.value === "false") { throw new Error("Secure RAIC/Crew ID image storage is not connected. No image or reservation was sent."); } if (!(await runAvailability(false))) throw new Error("Availability changed. Please return to Step 1 and review the message."); const payload = buildPayload(); const response = await createCrewReservation(payload); if (!response || !response.success || !response.reservationNumber) throw new Error("The reservation could not be created."); const pod = selectedPod(); const rows = [ ["Reservation number", response.reservationNumber], ["Check-in", formatDate(payload.checkInDate)], ["Check-out", formatDate(payload.checkOutDate)], ["Pod type", pod.label], ["Amount due", response.amountDue != null ? `${money(response.amountDue, response.currency || "CAD")} CAD` : "Provided by Panda Pod payment request"], ["Payment status", response.paymentStatus || "PENDING"] ]; $("#ppc5-success-summary").innerHTML = rows.map(([key, value]) => `
${escapeHtml(key)}${escapeHtml(value)}
`).join(""); reservationComplete = true; $("#ppc5-booking-ui").hidden = true; $("#ppc5-booking-ui").classList.add("ppc5-hidden"); $("#ppc5-success").hidden = false; $("#ppc5-success").classList.remove("ppc5-hidden"); $("#ppc5-success").focus(); root.scrollIntoView({ behavior: "smooth", block: "start" }); } catch (error) { showStep(5); errorBox.innerHTML = resultCard("Reservation Not Created", error.message || "We could not safely create the reservation. Please retry or contact us on WhatsApp."); errorBox.classList.remove("ppc5-hidden"); errorBox.focus(); } finally { button.disabled = false; button.textContent = "Create Reservation"; } } function invalidateAvailability() { availability = null; cardAuthorizationOpened = false; $("#ppc5-card-complete").checked = false; $("#ppc5-availability-result").classList.add("ppc5-hidden"); } function clearCrewIdSelection() { const input = $("#ppc5-crew-id-file"); input.value = ""; input.removeAttribute("aria-invalid"); $("#ppc5-upload-error").textContent = ""; $("#ppc5-upload-status").classList.add("ppc5-hidden"); crewIdPreviewUrls.forEach(url => URL.revokeObjectURL(url)); crewIdPreviewUrls = []; $("#ppc5-upload-previews").innerHTML = ""; } function handleCrewIdSelection(event) { const input = event.currentTarget; const files = Array.from(input.files || []); clearErrors($("#ppc5-crew-id-upload")); if (!files.length) { clearCrewIdSelection(); return; } const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/heic", "image/heif", ""]; const invalidType = files.find(file => !allowedTypes.includes(file.type) || !/\.(jpe?g|png|webp|heic|heif)$/i.test(file.name)); if (invalidType) { clearCrewIdSelection(); setError(input, `${invalidType.name} is not a supported JPG, PNG, WEBP, HEIC, or HEIF image.`); return; } const oversized = files.find(file => file.size > 10 * 1024 * 1024); if (oversized) { clearCrewIdSelection(); setError(input, `${oversized.name} must be 10 MB or smaller.`); return; } crewIdPreviewUrls.forEach(url => URL.revokeObjectURL(url)); crewIdPreviewUrls = []; const previews = $("#ppc5-upload-previews"); previews.innerHTML = ""; files.forEach(file => { const url = URL.createObjectURL(file); crewIdPreviewUrls.push(url); const item = document.createElement("div"); item.className = "ppc5-upload-preview-item"; const image = document.createElement("img"); image.className = "ppc5-upload-preview"; image.src = url; image.alt = "Selected crew ID preview"; const details = document.createElement("div"); details.className = "ppc5-upload-file"; const name = document.createElement("strong"); name.textContent = file.name; const size = document.createElement("small"); size.textContent = `${(file.size / 1024 / 1024).toFixed(1)} MB · selected locally`; details.append(name, size); item.append(image, details); previews.appendChild(item); }); $("#ppc5-upload-status").classList.remove("ppc5-hidden"); $("#ppc5-live").textContent = `${files.length} crew ID image${files.length === 1 ? "" : "s"} selected.`; } function wireEvents() { $("#ppc5-check-availability").addEventListener("click", () => runAvailability(true)); $("#ppc5-form").addEventListener("submit", submitBooking); $("#ppc5-phone").addEventListener("input", event => { event.target.value = sanitizePhone(event.target.value); }); $$('[data-date-field]').forEach(field => field.addEventListener("click", event => { const input = $("input[type=date]", field); if (event.target === input) { const editableTextWidth = Math.min(160, Math.max(110, input.clientWidth * .45)); if (event.offsetX <= editableTextWidth) return; event.preventDefault(); try { if (typeof input.showPicker === "function") input.showPicker(); else input.focus(); } catch (error) { input.focus(); } return; } event.preventDefault(); try { if (typeof input.showPicker === "function") input.showPicker(); else { input.focus(); input.click(); } } catch (error) { input.focus(); } })); $$("[data-next]").forEach(button => button.addEventListener("click", () => { if (validateStep(currentStep)) showStep(Number(button.dataset.next)); })); $$("[data-back]").forEach(button => button.addEventListener("click", () => showStep(Number(button.dataset.back)))); $$("[data-go-step]").forEach(button => button.addEventListener("click", () => { if (!button.disabled) showStep(Number(button.dataset.goStep)); })); $$("input[name=returningCrew]").forEach(input => input.addEventListener("change", () => { const firstTime = input.value === "false"; $("#ppc5-first-time-note").classList.toggle("ppc5-hidden", !firstTime); $("#ppc5-crew-id-upload").classList.toggle("ppc5-hidden", !firstTime); $("#ppc5-crew-id-file").toggleAttribute("required", firstTime); if (!firstTime) clearCrewIdSelection(); })); $("#ppc5-crew-id-file").addEventListener("change", handleCrewIdSelection); $("#ppc5-upload-remove").addEventListener("click", clearCrewIdSelection); $("#ppc5-card-authorization-link").addEventListener("click", () => { cardAuthorizationOpened = true; $("#ppc5-card-authorization-link").removeAttribute("aria-invalid"); $("#ppc5-card-error").textContent = ""; $("#ppc5-live").textContent = "Secure card authorization opened in a new tab. Complete both forms, then return here."; }); $("#ppc5-card-complete").addEventListener("change", () => { if ($("#ppc5-card-complete").checked) { $("#ppc5-card-complete").removeAttribute("aria-invalid"); $("#ppc5-card-error").textContent = ""; } }); $("#ppc5-eta-hour").addEventListener("change", updateEtaValue); $("#ppc5-eta-minute").addEventListener("change", updateEtaValue); $$("input[name=etaPeriod]").forEach(input => input.addEventListener("change", updateEtaValue)); $$("input[name=earlyRequested],input[name=lateRequested]").forEach(input => input.addEventListener("change", updateRequestCharges)); $$("input[name=podType],#ppc5-checkin,#ppc5-checkout").forEach(input => input.addEventListener("change", invalidateAvailability)); $("#ppc5-new-booking").addEventListener("click", () => window.location.reload()); } async function init() { wireEvents(); try { config = await getCrewPublicConfig(); const period = config.ratePeriods[0]; if (period) { $("#ppc5-rate-validity").textContent = `Valid ${formatDate(period.validFrom)} – ${formatDate(period.validTo)}`; $("#ppc5-lower-rate").textContent = money(period.lowerPodRate, period.currency); $("#ppc5-upper-rate").textContent = money(period.upperPodRate, period.currency); $("#ppc5-lower-option-rate").textContent = `${money(period.lowerPodRate, period.currency)} CAD / night`; $("#ppc5-upper-option-rate").textContent = `${money(period.upperPodRate, period.currency)} CAD / night`; $("#ppc5-tax-display").textContent = `Displayed rates: ${period.taxDisplay}. Rates are subject to availability.`; } const today = getVancouverDate(); $("#ppc5-checkin").min = today; $("#ppc5-checkout").min = addDays(today, 1); } catch (error) { $("#ppc5-offer").innerHTML = `

Crew booking is temporarily unavailable

Please retry or contact us on WhatsApp, mention that you are a crew member, and ask for Front Desk.

`; $("#ppc5-form").hidden = true; } } init(); }()); }());
Panda Pod Hotel
Concierge Panda
Panda Pod Hotel Assistant