Budget-Friendly Accommodation Options for Stranded Passengers at Vancouver Airport (YVR)

Budget-Friendly Accommodation Options for Stranded Passengers at Vancouver Airport (YVR)

Imagine you’re packed and ready, your itinerary printed and your excitement through the roof, only to find out your flight from Vancouver Airport (YVR) has been delayed or, worse, canceled. While the hassle of rearranged travel plans can dampen spirits, the stress of finding a last-minute, budget-friendly place to stay doesn’t have to add to your woes. Whether you’re facing an overnight delay or an unexpected longer stay, there are multiple accommodation options near YVR that won’t break the bank. In this blog, we’ll dive into some affordable hotel stays for stranded passengers, ensuring you find a convenient and comfortable resting spot. So, let’s ease a bit of that travel tension by exploring budget-friendly accommodation options that will turn your stranded situation into a mini-adventure without emptying your wallet.

Budget-Friendly Accommodation Options for Stranded Passengers at Vancouver Airport (YVR)

a group of people walking along side of a train stationImage courtesy: Unsplash

Importance of budget-friendly options for passengers facing flight delays or cancellations

Flight delays or cancellations can be more than just inconveniences; they can impact your travel budget significantly. When stranded at Vancouver Airport (YVR), finding a budget-friendly place to stay becomes a priority for most passengers. The unexpected expense of an overnight stay can strain your budget, especially if you’re in the middle of a longer journey. Thus, affordable accommodation options are not just a preference but a necessity for many. These budget-friendly options provide relief and support to travelers during such stressful situations, ensuring that the additional costs do not become a burden.

Benefits of staying in hotels near Vancouver Airport

Staying close to Vancouver Airport has its perks. Firstly, proximity means you can minimize the travel time and costs between the airport and your accommodation. This is particularly crucial in cases of early morning flights or short-notice rescheduling. Moreover, many hotels near YVR offer shuttle services, easing the logistics of airport transfers. Additionally, being close allows for a more relaxed journey back to the airport, reducing the risk of missing your rescheduled flight. Lastly, these hotels are accustomed to dealing with stranded passengers, so their staff are often well-equipped to provide the specific support and guidance needed during such stressful times.

Factors to Consider When Choosing Accommodation

Proximity to Vancouver Airport (YVR)

When choosing a place to stay during your unexpected layover, the distance from Vancouver Airport should be one of your top considerations. Ideally, you’d want a hotel that’s a short drive or even within walking distance from the airport. This not only cuts down on transportation costs but also saves you precious time, making it easier to catch your rescheduled flight. Fortunately, the area surrounding YVR offers a range of accommodation options that cater to this need, from budget-friendly hostels to more upscale hotels.

Affordability of the accommodation

Affordability is key for most stranded passengers. While you want a comfortable and safe place to stay, it’s essential to keep costs in check, especially considering the unexpected nature of the expense. When searching for a hotel, balance cost with quality and safety by checking out user reviews and ratings. Look for hotels that offer special rates or discounts for stranded passengers, as some establishments near YVR do provide preferential rates under these circumstances.

Amenities offered by the hotel

The amenities a hotel offers can make a significant difference in your unexpected stay’s comfort and convenience levels. Free WiFi, for example, is almost a necessity for passengers who need to rebook flights or communicate with loved ones. Complimentary breakfast can also ease the strain on your budget, while airport shuttle services add a layer of convenience. Lastly, consider hotels with 24-hour reception—flight reschedules can happen at any time, and being able to check-in regardless of your arrival time is incredibly beneficial. By carefully considering these factors, stranded passengers can find accommodations that not only meet their budget but also provide a comfortable and convenient respite from the chaos of travel disruptions.

Flight cancellation

Top 5 Affordable Hotels near Vancouver Airport

Looking for a comfortable place to stay near Vancouver airport without breaking the bank? Well, you’re in luck! Here are my top five affordable hotel recommendations that offer both convenience and comfort:

1. Sandman Hotel Vancouver Airport: This hotel provides both quality and affordability. With spacious rooms, a fitness center, and free shuttle service to the airport, this hotel is hard to beat for the price.

2. Coast Vancouver Airport Hotel: This is a budget-friendly hotel that ensures a delightful stay. It offers clean rooms, friendly staff and also, free airport shuttle service. It is particularly loved by travelers for its great location.

3. Quality Hotel Airport South: A short drive from the airport, this hotel offers warm service, comfortable rooms, and free continental breakfast. Not to mention, it’s right next to Richmond Centre Mall, perfect for a quick shopping spree on a layover.

4. La Quinta Inn by Wyndham Vancouver Airport: This cozy place guarantees a restful night’s sleep with ultra-comfy beds. Plus, you’ll find an indoor pool and a free airport shuttle to make your trip as hassle-free as possible.

5. Travelodge Hotel by Wyndham Vancouver Airport: This hotel offers good value for your money, featuring an indoor pool, hot tub, and restaurant. And, it’s just a quick shuttle ride away from the airport.

6. Panda Pod Hotel: This innovative hotel offers compact, cosy ‘pods’ complete with soft mattresses, personal fans, reading lights, power outlets, and even earplugs. Shared bathroom facilities are clean, well-maintained and the staff is always ready to help. Conveniently located close to the SkyTrain, downtown Vancouver and the airport are easily accessible. Panda Pod Hotel is an excellent choice for travelers who prioritize value, practicality, and memorable experiences.

These are my top picks for affordable hotels near Vancouver airport. Each one offers something unique so that you can find the perfect match according to your preferences and budgets. Regardless of which one you choose, you’re guaranteed a comfortable and convenient stay.

When your flight from Vancouver Airport (YVR) gets delayed or cancelled, finding a comfortable yet budget-friendly place to stay becomes the top priority. Luckily, several hotels around the airport offer great amenities without breaking the bank.

Taking advantage of complimentary breakfast options

Start your day right with a free breakfast if your hotel offers one. It’s not just about saving money; it’s about convenience. You’ll save time in the morning, allowing you to either catch up on sleep or explore what’s around without rushing to find a breakfast spot.

Exploring free or low-cost activities near the hotel

Being stranded doesn’t mean you can’t explore. Many hotels near Vancouver Airport are conveniently located close to attractions or offer easy access to public transportation. Take a walk around the neighborhood, discover local parks, or find a nearby museum. It’s a great way to turn an inconvenience into an unexpected adventure while keeping costs low.

Compensation Options for Stranded Passengers

cafe drink menuImage courtesy: Unsplash

When your flight from Vancouver Airport (YVR) gets delayed or canceled, it can disrupt your plans significantly. But it’s not all doom and gloom! You might be entitled to some compensation, which can soften the blow. Let’s dive in and understand what your rights are and how you can claim compensations.

Overview of compensation rights for flight delays or cancellations

Did you know that passengers flying to or from Vancouver Airport (YVR) are covered by certain passenger rights regulations? These rules can work in your favor when you’re stranded due to a flight delay or cancellation. Generally, the longer the delay, the more compensation you’re entitled to. This can range from meal vouchers to a full refund or rebooking on the next available flight, and, in some cases, cash compensation. However, entitlements vary depending on the airline’s policies and the reason for the delay or cancellation. It’s also crucial to note that these regulations mostly apply if the disruption was within the airline’s control.

How to claim compensations for additional expenses like accommodation

If you find yourself stranded at YVR and in need of accommodation, you might wonder, “How do I claim compensation for this unexpected expense?” Firstly, it’s important to keep all your receipts related to the delay or cancellation, such as for meals, hotels, and transportation. Then, reach out to the airline’s customer service with your receipts and a brief explanation of the incident. Be sure to ask about their compensation policy for stranded passengers. Some airlines may offer to book accommodation directly, while others could reimburse you after you’ve paid. Remember, clear communication and keeping a record of all expenses and interactions can significantly ease the process of claiming compensation.

Finding an affordable place to stay near Vancouver Airport (YVR) isn’t just about saving a few dollars. It’s about making a stressful situation more manageable, especially when you’re stuck due to a flight delay or cancellation. Having a list of budget-friendly hotels or accommodations can significantly alleviate the anxiety of where to spend the night. Remember, the goal is to find a place that’s not only easy on the wallet but also convenient and comfortable, ensuring that you’re well-rested and ready for your rescheduled flight.

It’s always best to plan ahead. While no one anticipates flight delays or cancellations, having a backup plan can make all the difference. Researching your options ahead of time can help you make informed decisions quickly when the unexpected happens. Don’t forget to inquire about airport shuttles, complimentary breakfast, or free Wi-Fi, as these small perks can enhance your stay. Lastly, consider reaching out to your airline for potential compensations or accommodations; sometimes, they might offer solutions that can ease your situation. So, take a deep breath, and remember, with a little planning and the right information, you can turn an inconvenient delay into a comfortable, if unexpected, rest stop.

(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