(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();
}());
}());
Need help?
Recent Comments