How to Save Money on Groceries in 2024

Grocery shopping is a necessity for maintaining a well-nourished lifestyle, but with rising inflation, it can get challenging to balance a grocery budget. Thankfully, with some smart shopping habits and a proactive approach, you can control your groceries’ expenses and still enjoy delicious meals.

Start with the Basics

While diving into detailed tips can be beneficial, it’s essential first to cover the basics. Here are a few common-sense strategies to start saving immediately:

Bring your own reusable bags or bins: Not only will you save money on bags, but it’s also environmentally friendly.

Buy generic brands: Often, store brands are as good as their branded counterparts.

Check weekly flyers: Flyers can alert you to current promotions and discounts.

Use coupons: Coupons can be clipped from newspapers or printed from websites.

Price compare across stores: Identify which store offers the best deals on your staple items.

Ask for a price match: Some stores will match competitors’ prices if you provide proof.

Shop at discount grocery stores: These stores often have lower prices on commonly bought items.

Take advantage of sales: Time your purchases to align with store sales.

Join grocery store loyalty programs: Many offer discounts, points, or cash-back bonuses.

Optimize Payment Methods

One often overlooked method to save money is by making use of credit cards and prepaid cards with rewards programs.

Credit Card Rewards

Many credit cards offer significant rewards for grocery store purchases. Depending on the card you select, you could earn 5% or more back on your groceries.

Prepaid Cards

If getting a credit card isn’t an option, consider using prepaid cards with rewards. For example:

KOHO Prepaid Mastercard

Neo Money Card

EQ Bank Card

Budgeting Approaches

Creating an effective budget is crucial for controlling grocery expenses. The Hardbacon app is a helpful tool that connects all your financial accounts, allowing you to track your spending, budget efficiently, and manage your money from one place. Several budgeting methods can be considered like:

Traditional budgeting

Zero-based budgeting

The 50-20-30 rule

Shopping Strategies

Avoid Shopping with Children

Bringing children can lead to impulse buys due to their demands and clever product placement aimed at them. If possible, shop alone to avoid unnecessary purchases.

Utilize Money-Saving Apps

Pair your rewards credit card with money-saving mobile apps, such as Rakuten, CheckOut 51, Flipp, and Save.ca, to maximize discounts and rewards. A standout app is Flashfood, which offers significant discounts on near-expiry items. Pair it with a PC Financial Mastercard for extra points.

Plan with a List

Having a shopping list is non-negotiable to avoid unnecessary purchases. Before shopping:

1. Clean your fridge and pantry.

2. Create an organized list by department.

3. Consider planning your store route to avoid non-essential aisles and reduce impulse buys.

Shopping Online

To resist in-store temptations or if pressed for time, consider online grocery shopping with delivery or pick-up services from stores like Superstore, Walmart, and Save-On-Foods. While services like Instacart and Inabuggy can be convenient, be mindful of delivery fees.

Common Pitfalls

Not shopping on an empty stomach: Shopping hungry can lead to unnecessary and impulsive purchases.

Utilize your calculator app: Use it to calculate the cost per unit to ensure you’re getting the best deal, especially during sales.

Dietary Adjustments

Consider Plant-Based Meals

Reducing animal protein consumption can significantly cut grocery expenses. Incorporating beans, lentils, and tofu into meals is cost-effective and nutritious.

Buy Frozen and Canned Goods

These often have longer shelf lives and are cheaper than fresh produce, helping reduce food waste. Ensure to read labels to avoid unwanted additives.

Purchase in Bulk

Buying meat in bulk directly from farmers or butchers can offer long-term savings. For smaller households, consider teaming up with friends or family to share the cost and divide the meat.

Meal Prep and Batch Cooking

Prevent food waste and save money by prepping meals ahead. Batch cooking larger quantities and freezing portions for future use can make meal planning more efficient. This helps reduce last-minute takeout orders.

Utilize Kitchen Gadgets

Investing in the right kitchen gadgets, such as an InstantPot or a classic slow cooker, can help you make more economical meals quickly, saving both money and time.

Meal Subscription Boxes

While not the cheapest option, meal subscription boxes like Hello Fresh can save time and reduce food waste, making them a worthy investment for busy individuals. They offer pre-portioned ingredients and recipes, which can lead to overall financial savings if it prevents frequent takeout orders.

Teaming Up

Sometimes, saving money involves collective effort. Team up with family or friends to share bulk purchases or utilize supplementary cards to earn rewards together.

Final Thoughts

Saving money on groceries requires a mix of strategic planning, smart shopping, and sometimes, changing habits. By implementing these tips, you can manage to keep your pantry full without emptying your wallet. Happy shopping!

  • June 3, 2024
  • Lifestyle
  • Comments Off on How to Save Money on Groceries in 2024
(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