403Webshell
Server IP : 173.209.174.21  /  Your IP : 216.73.216.89
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux wcfs-server 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64
User : nodor ( 1000)
PHP Version : 8.3.6
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/grangestation/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/grangestation//owner.js
const ownerCode = "grange";
const ownerSessionTimeoutMs = 30 * 60 * 1000;
let ownerSessionTimer = null;
let managerState = {
  settings: getSiteConfig(),
  confirmedBookings: [],
  availabilityBlocks: [],
  requests: [],
  archivedRequests: []
};
let requestView = "active";

const login = document.querySelector("[data-owner-login]");
const desk = document.querySelector("[data-owner-desk]");
const codeInput = document.querySelector("[data-owner-code]");
const unlockButton = document.querySelector("[data-owner-unlock]");
const logoutButton = document.querySelector("[data-owner-logout]");
const toast = document.querySelector("[data-toast]");
const requestList = document.querySelector("[data-request-list]");
const confirmedList = document.querySelector("[data-confirmed-list]");
const blockList = document.querySelector("[data-block-list]");
const confirmedForm = document.querySelector("[data-confirmed-form]");
const blockForm = document.querySelector("[data-block-form]");
const brandingForm = document.querySelector("[data-branding-form]");
const settingsForm = document.querySelector("[data-settings-form]");
const ownerTabs = document.querySelectorAll("[data-owner-tab]");
const ownerSections = document.querySelectorAll("[data-owner-section]");

function showToast(message) {
  toast.textContent = message;
  toast.classList.add("visible");
  window.setTimeout(() => toast.classList.remove("visible"), 2600);
}

function escapeHtml(value) {
  return String(value ?? "")
    .replaceAll("&", "&")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function formatMoney(value) {
  return `$${Number(value || 0).toLocaleString()}`;
}

function formatPriceRange(low, high) {
  const lowValue = Number(low || 0);
  const highValue = Number(high || 0);
  if (!lowValue && !highValue) return "Quote";
  if (lowValue === highValue) return formatMoney(lowValue);
  return `${formatMoney(lowValue)} - ${formatMoney(highValue)}`;
}

function addonPriceRange(addon) {
  const low = Number(addon.priceLow ?? 0);
  const high = Number(addon.priceHigh ?? 0);
  const price = low || high ? formatPriceRange(low, high) : "Free";
  return addon.priceUnit && price !== "Free" ? `${price} ${addon.priceUnit}` : price;
}

function packageIncludedText(pkg) {
  if (Array.isArray(pkg.included) && pkg.included.length) return pkg.included.join("\n");
  return pkg.includes || "";
}

function hydratedAddons() {
  const defaults = defaultSiteConfig.addons || [];
  const configured = managerState.settings.addons || [];
  if (!configured.length) return defaults;
  return configured.map((addon) => {
    const fallback = defaults.find((item) => (item.value || item.label) === (addon.value || addon.label)) || {};
    return { ...fallback, ...addon };
  });
}

function packageOptions() {
  const configured = (managerState.settings.packages || []).filter((pkg) => pkg.enabled !== false);
  if (configured.length) return configured;
  return (defaultSiteConfig.packages || []).filter((pkg) => pkg.enabled !== false);
}

function packageDisplayPrice(pkg) {
  if (/simple/i.test(pkg.name || "")) return "$1,500 Fri/Sun, $1,800 Sat";
  return formatPriceRange(pkg.priceLow, pkg.priceHigh);
}

function numberValue(value) {
  const number = Number(value || 0);
  return Number.isFinite(number) ? number : 0;
}

function selectedPackage(packageName) {
  return packageOptions().find((pkg) => pkg.name === packageName);
}

function addonByValue(value) {
  return hydratedAddons().find((addon) => (addon.value || addon.label) === value);
}

function packageBaseAmount(packageName, dateValue, extraHours) {
  const pkg = selectedPackage(packageName);
  if (!pkg) return 0;
  const low = numberValue(pkg.priceLow);
  const high = numberValue(pkg.priceHigh);
  const date = dateValue ? new Date(`${dateValue}T12:00:00`) : null;
  const isSaturday = date ? date.getDay() === 6 : false;
  const base = /simple/i.test(pkg.name) ? (isSaturday ? high : low) : low;
  return base + (extraHours * 100);
}

function ownerTally({ packageName = "", date = "", addons = [], extraHours = 0, priceAdjustment = 0 }) {
  const billableExtraHours = numberValue(extraHours);
  let total = packageBaseAmount(packageName, date, billableExtraHours);
  const lines = [];
  if (packageName) lines.push(`Package: ${formatMoney(packageBaseAmount(packageName, date, billableExtraHours))}`);
  addons.forEach((value) => {
    const addon = addonByValue(value);
    if (!addon) return;
    if (billableExtraHours > 0 && /setup time|extra hours/i.test(addon.label || value)) return;
    const addonAmount = numberValue(addon.priceLow);
    total += addonAmount;
    lines.push(`${addon.label || value}: ${addonAmount ? formatMoney(addonAmount) : "Free"}`);
  });
  const adjustment = numberValue(priceAdjustment);
  total += adjustment;
  if (adjustment) lines.push(`Owner adjustment: ${adjustment > 0 ? "+" : ""}${formatMoney(adjustment)}`);
  return { total: Math.max(0, total), lines };
}

function tallyForRecord(record) {
  const addons = record.addons || selectedBookingAddons(record.notes, hydratedAddons());
  const packageName = record.package || selectedPackageFromNotes(record.notes);
  return ownerTally({
    packageName,
    date: record.date,
    addons,
    extraHours: record.extraHours,
    priceAdjustment: record.priceAdjustment
  });
}

function tallyForForm(form) {
  const formData = new FormData(form);
  return ownerTally({
    packageName: formData.get("package") || "",
    date: formData.get("date") || "",
    addons: formData.getAll("addons"),
    extraHours: formData.get("extraHours") || 0,
    priceAdjustment: formData.get("priceAdjustment") || 0
  });
}

function refreshTally(form, overwriteAmount = true) {
  const tally = tallyForForm(form);
  const amountInput = form.elements.amountOwed;
  if (overwriteAmount && amountInput) amountInput.value = tally.total.toFixed(2);
  const output = form.querySelector("[data-owner-tally]");
  if (output) {
    output.textContent = tally.lines.length
      ? `Suggested tally: ${formatMoney(tally.total)} (${tally.lines.join(" + ")})`
      : `Suggested tally: ${formatMoney(tally.total)}`;
  }
}

function tallyFieldChanged(target) {
  return ["package", "addons", "extraHours", "priceAdjustment", "date"].includes(target.name);
}

async function apiRequest(action, payload) {
  const options = payload
    ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }
    : {};
  const response = await fetch(`api/index.php?action=${encodeURIComponent(action)}`, options);
  const data = await response.json();
  if (!response.ok || !data.ok) {
    if (response.status === 401 && /session|login|required|timed out/i.test(data.error || "")) lockOwnerDesk(data.error || "Please enter the owner code again.", false);
    throw new Error(data.error || "Request failed.");
  }
  return data;
}

function startOwnerSessionTimer() {
  window.clearTimeout(ownerSessionTimer);
  ownerSessionTimer = window.setTimeout(() => {
    lockOwnerDesk("Owner desk timed out. Please enter the owner code again.");
  }, ownerSessionTimeoutMs);
}

function noteOwnerActivity() {
  if (!desk.hidden) startOwnerSessionTimer();
}

async function lockOwnerDesk(message = "Owner desk locked.", notifyServer = true) {
  window.clearTimeout(ownerSessionTimer);
  ownerSessionTimer = null;
  sessionStorage.removeItem("grangeStationOwnerUnlocked");
  login.hidden = false;
  desk.hidden = true;
  codeInput.value = "";
  if (notifyServer) {
    try {
      await fetch("api/index.php?action=owner_logout");
    } catch (error) {
      // The visual lock still matters even if the network request fails.
    }
  }
  showToast(message);
}

async function uploadImage(file) {
  const formData = new FormData();
  formData.append("image", file);
  const response = await fetch("api/index.php?action=upload_image", {
    method: "POST",
    body: formData
  });
  const data = await response.json();
  if (!response.ok || !data.ok) throw new Error(data.error || "Upload failed.");
  return data.path;
}

function formatDate(dateString) {
  if (!dateString) return "Date not selected";
  const date = new Date(`${dateString}T12:00:00`);
  return date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric" });
}

function formatTime(time) {
  if (!time) return "time TBD";
  const [rawHours, rawMinutes = "00"] = String(time).split(":");
  const hours = Number(rawHours);
  if (Number.isNaN(hours)) return time;
  const suffix = hours >= 12 ? "PM" : "AM";
  const displayHours = hours % 12 || 12;
  return `${displayHours}:${String(rawMinutes).padStart(2, "0")} ${suffix}`;
}

function cleanBookingNotes(notes) {
  return String(notes || "")
    .split("\n")
    .filter((line) => !/^(selected package|package|selected options|add-ons):/i.test(line.trim()))
    .join("\n")
    .trim();
}

function selectedPackageFromNotes(notes) {
  const line = String(notes || "").split("\n").find((item) => /^(selected package|package):/i.test(item.trim()));
  return line ? line.replace(/^(selected package|package):\s*/i, "").trim() : "";
}

function selectedBookingAddons(notes, addonOptions) {
  const normalized = String(notes || "").toLowerCase();
  return addonOptions
    .map((addon) => addon.value || addon.label)
    .filter((value) => value && normalized.includes(String(value).toLowerCase()));
}

function composeBookingNotes(notes, addons, packageName = "") {
  const cleanNotes = cleanBookingNotes(notes);
  const packageLine = packageName ? `Selected package: ${packageName}` : "";
  const addonLine = addons.length ? `Selected options: ${addons.join(", ")}` : "";
  return [packageLine, addonLine, cleanNotes].filter(Boolean).join("\n");
}

async function loadOwnerData() {
  const data = await apiRequest("owner_get");
  managerState = {
    settings: mergeConfig(data.settings || {}),
    confirmedBookings: data.confirmedBookings || [],
    availabilityBlocks: data.availabilityBlocks || [],
    requests: data.requests || [],
    archivedRequests: data.archivedRequests || []
  };
  renderAll();
}

async function unlock() {
  try {
    await apiRequest("owner_login", { code: codeInput.value });
    sessionStorage.setItem("grangeStationOwnerUnlocked", "true");
    login.hidden = true;
    desk.hidden = false;
    startOwnerSessionTimer();
    await loadOwnerData();
  } catch (error) {
    showToast("Owner code did not match or the server is not connected.");
  }
}

function renderAll() {
  renderConfirmedBookings();
  renderBlocks();
  renderRequests();
  renderBrandingForm();
  renderSettingsForm();
  renderAddons();
  renderPackages();
  renderVendors();
  renderGalleryManager();
  renderFaqManager();
}

function showOwnerSection(sectionName) {
  ownerTabs.forEach((tab) => tab.classList.toggle("active", tab.dataset.ownerTab === sectionName));
  ownerSections.forEach((section) => section.classList.toggle("active", section.dataset.ownerSection === sectionName));
}

function renderConfirmedBookings() {
  if (!managerState.confirmedBookings.length) {
    confirmedList.innerHTML = '<div class="empty">No confirmed bookings yet.</div>';
    return;
  }
  const addonOptions = hydratedAddons().filter((addon) => addon.enabled !== false);
  const packageOptionsList = packageOptions();
  confirmedList.innerHTML = managerState.confirmedBookings.map((booking) => `
    <article class="request-card booking-card">
      <details class="request-details booking-details">
        <summary>
          <strong>${escapeHtml(booking.title || "Confirmed booking")}</strong>
          <span>${escapeHtml(formatDate(booking.date))} - ${escapeHtml(formatTime(booking.start))} to ${escapeHtml(formatTime(booking.end))}${booking.guests ? ` - ${escapeHtml(booking.guests)} guests` : ""}</span>
          <span>Amount owed: ${escapeHtml(formatMoney(booking.amountOwed || tallyForRecord(booking).total))}</span>
          ${booking.notes ? `<span>${escapeHtml(booking.notes)}</span>` : ""}
        </summary>
      <form class="booking-edit-form" data-booking-edit>
        <input type="hidden" name="id" value="${escapeHtml(booking.id)}">
        <label>Event name<input required name="title" value="${escapeHtml(booking.title || "")}"></label>
        <label>Date<input required name="date" type="date" value="${escapeHtml(booking.date || "")}"></label>
        <label>Start<input required name="start" type="time" value="${escapeHtml(booking.start || "14:00")}"></label>
        <label>End<input required name="end" type="time" value="${escapeHtml(booking.end || "22:00")}"></label>
        <label>Guests<input name="guests" type="number" min="0" max="250" value="${escapeHtml(booking.guests || "")}"></label>
        <label>Extra hours<input name="extraHours" type="number" min="0" step="0.25" value="${escapeHtml(booking.extraHours || 0)}"></label>
        <label>Package
          <select name="package">
            <option value="">Not selected</option>
            ${packageOptionsList.map((pkg) => `<option value="${escapeHtml(pkg.name)}" ${selectedPackageFromNotes(booking.notes) === pkg.name ? "selected" : ""}>${escapeHtml(pkg.name)} - ${escapeHtml(packageDisplayPrice(pkg))}</option>`).join("")}
          </select>
        </label>
        <label>Owner adjustment<input name="priceAdjustment" type="number" step="0.01" value="${escapeHtml(booking.priceAdjustment || 0)}"></label>
        <label>Amount owed<input name="amountOwed" type="number" min="0" step="0.01" value="${escapeHtml(booking.amountOwed || tallyForRecord(booking).total)}"></label>
        <fieldset class="request-addon-editor booking-addon-editor">
          <legend>Features / add-ons</legend>
          ${addonOptions.map((addon) => {
            const value = addon.value || addon.label;
            const checked = selectedBookingAddons(booking.notes, addonOptions).includes(value) ? "checked" : "";
            return `<label><input type="checkbox" name="addons" value="${escapeHtml(value)}" ${checked}> ${escapeHtml(addon.label || value)} <small>${escapeHtml(addonPriceRange(addon))}</small></label>`;
          }).join("")}
        </fieldset>
        <div class="owner-tally" data-owner-tally>${escapeHtml(`Suggested tally: ${formatMoney(tallyForRecord(booking).total)}`)}</div>
        <label class="booking-notes-field">Booking notes<textarea name="notes" rows="3">${escapeHtml(cleanBookingNotes(booking.notes))}</textarea></label>
        <div class="booking-edit-actions">
          <button type="button" data-refresh-tally>Update Tally</button>
          <button type="submit">Save</button>
          <button type="button" data-delete-confirmed="${escapeHtml(booking.id)}">Remove</button>
        </div>
      </form>
      </details>
    </article>
  `).join("");
}

function renderBlocks() {
  if (!managerState.availabilityBlocks.length) {
    blockList.innerHTML = '<div class="empty">No unavailable dates or holds yet.</div>';
    return;
  }
  blockList.innerHTML = managerState.availabilityBlocks.map((block) => `
    <article class="request-card">
      <div>
        <strong>${escapeHtml(block.title || "Unavailable")}</strong>
        <p>${escapeHtml(formatDate(block.date))} - ${escapeHtml(formatTime(block.start))} to ${escapeHtml(formatTime(block.end))} - ${escapeHtml(block.reason || "owner hold")}</p>
      </div>
      <button type="button" data-delete-block="${escapeHtml(block.id)}">Remove</button>
    </article>
  `).join("");
}

function renderRequests() {
  const rows = requestView === "archived" ? managerState.archivedRequests : managerState.requests;
  const addonOptions = hydratedAddons().filter((addon) => addon.enabled !== false);
  const packageOptionsList = packageOptions();
  document.querySelectorAll("[data-request-view]").forEach((button) => {
    button.classList.toggle("active", button.dataset.requestView === requestView);
  });
  const archiveButton = document.querySelector("[data-request-action]");
  const restoreButton = document.querySelector("[data-request-restore]");
  archiveButton.textContent = requestView === "archived" ? "Delete Selected" : "Archive Selected";
  restoreButton.hidden = requestView !== "archived";

  if (!rows.length) {
    requestList.innerHTML = `<div class="empty">No ${requestView === "archived" ? "archived" : "saved"} requests yet.</div>`;
    return;
  }
  requestList.innerHTML = rows.map((request) => `
    <article class="request-card request-card-selectable">
      <label class="request-select"><input type="checkbox" data-request-select value="${escapeHtml(request.id)}"> Select</label>
      <details class="request-details">
        <summary>
          <strong>${escapeHtml(request.requestType)}: ${escapeHtml(request.name)}</strong>
          <span>${escapeHtml(formatDate(request.date))} at ${escapeHtml(formatTime(request.time))}${request.endTime ? ` to ${escapeHtml(formatTime(request.endTime))}` : ""} - ${escapeHtml(request.guests || "guest count TBD")} guests</span>
          <span>${escapeHtml(request.email)} - ${escapeHtml(request.phone)}</span>
        </summary>
        <dl class="request-detail-list">
          <div><dt>Event type</dt><dd>${escapeHtml(request.eventType || "Not selected")}</dd></div>
          <div><dt>Space</dt><dd>${escapeHtml(request.space || "Not selected")}</dd></div>
          <div><dt>Vendor help</dt><dd>${escapeHtml(request.vendorHelp || "No vendor help selected")}</dd></div>
          <div><dt>Package</dt><dd>${escapeHtml(request.package || "Not selected")}</dd></div>
          <div><dt>Add-ons</dt><dd>${escapeHtml((request.addons || []).join(", ") || "None selected")}</dd></div>
          <div><dt>Amount owed</dt><dd>${escapeHtml(formatMoney(request.amountOwed || tallyForRecord(request).total))}</dd></div>
          <div><dt>Message</dt><dd>${escapeHtml(request.message || "No message")}</dd></div>
        </dl>
        <form class="request-edit-form" data-request-edit>
          <input type="hidden" name="id" value="${escapeHtml(request.id)}">
          <label>Request type
            <select name="requestType">
              ${["Event Reservation", "Private Viewing"].map((option) => `<option ${request.requestType === option ? "selected" : ""}>${option}</option>`).join("")}
            </select>
          </label>
          <label>Name<input required name="name" value="${escapeHtml(request.name || "")}"></label>
          <label>Email<input required name="email" type="email" value="${escapeHtml(request.email || "")}"></label>
          <label>Phone<input required name="phone" value="${escapeHtml(request.phone || "")}"></label>
          <label>Date<input required name="date" type="date" value="${escapeHtml(request.date || "")}"></label>
          <label>Start<input required name="time" type="time" value="${escapeHtml(request.time || "16:00")}"></label>
          <label>End<input required name="endTime" type="time" value="${escapeHtml(request.endTime || "22:00")}"></label>
          <label>Guests<input name="guests" type="number" min="0" max="250" value="${escapeHtml(request.guests || "")}"></label>
          <label>Extra hours<input name="extraHours" type="number" min="0" step="0.25" value="${escapeHtml(request.extraHours || 0)}"></label>
          <label>Package
            <select name="package">
              <option value="">Not sure yet</option>
              ${packageOptionsList.map((pkg) => `<option value="${escapeHtml(pkg.name)}" ${request.package === pkg.name ? "selected" : ""}>${escapeHtml(pkg.name)} - ${escapeHtml(packageDisplayPrice(pkg))}</option>`).join("")}
            </select>
          </label>
          <label>Owner adjustment<input name="priceAdjustment" type="number" step="0.01" value="${escapeHtml(request.priceAdjustment || 0)}"></label>
          <label>Amount owed<input name="amountOwed" type="number" min="0" step="0.01" value="${escapeHtml(request.amountOwed || tallyForRecord(request).total)}"></label>
          <label>Event type<input name="eventType" value="${escapeHtml(request.eventType || "")}"></label>
          <label>Space<input name="space" value="${escapeHtml(request.space || "")}"></label>
          <label>Vendor help<input name="vendorHelp" value="${escapeHtml(request.vendorHelp || "")}"></label>
          <fieldset class="request-addon-editor">
            <legend>Add-ons</legend>
            ${addonOptions.map((addon) => {
              const value = addon.value || addon.label;
              const checked = (request.addons || []).includes(value) ? "checked" : "";
              return `<label><input type="checkbox" name="addons" value="${escapeHtml(value)}" ${checked}> ${escapeHtml(addon.label || value)} <small>${escapeHtml(addonPriceRange(addon))}</small></label>`;
            }).join("")}
          </fieldset>
          <div class="owner-tally" data-owner-tally>${escapeHtml(`Suggested tally: ${formatMoney(tallyForRecord(request).total)}`)}</div>
          <label class="request-message-field">Message / wants<textarea name="message" rows="3">${escapeHtml(request.message || "")}</textarea></label>
          <button type="button" data-refresh-tally>Update Tally</button>
          <button type="submit">Save Request Changes</button>
        </form>
      </details>
      <div class="request-card-actions">
        <span>${escapeHtml(request.status || "new")}</span>
        ${requestView === "active" && request.status !== "confirmed" ? `<button type="button" data-approve-request="${escapeHtml(request.id)}">Approve Request</button>` : ""}
      </div>
    </article>
  `).join("");
}

function renderBrandingForm() {
  const branding = managerState.settings.branding || {};
  ["venueName", "tagline", "logoImage", "heroImage", "socialImage"].forEach((key) => {
    if (brandingForm.elements[key]) brandingForm.elements[key].value = branding[key] || "";
  });
}

function renderSettingsForm() {
  const pricingLabels = {
    weekendLow: "Weekend low",
    weekendHigh: "Weekend high",
    weekdayLow: "Weekday low",
    weekdayHigh: "Weekday high",
    over100Low: "Over 100 guests low",
    over100High: "Over 100 guests high",
    standardAddonLow: "Standard add-on low",
    standardAddonHigh: "Standard add-on high",
    rentalAddonLow: "Rental add-on low",
    rentalAddonHigh: "Rental add-on high",
    vendorIntroLow: "Vendor intro low",
    vendorIntroHigh: "Vendor intro high"
  };
  document.querySelector("[data-pricing-fields]").innerHTML = Object.entries(pricingLabels).map(([key, label]) => `
    <label>${escapeHtml(label)}<input type="number" min="0" name="${key}" value="${escapeHtml(managerState.settings.pricing[key])}"></label>
  `).join("");

  const colorLabels = { ink: "Text", paper: "Page", cream: "Cards", barn: "Accent red", sage: "Available green", gold: "Gold" };
  document.querySelector("[data-color-fields]").innerHTML = Object.entries(colorLabels).map(([key, label]) => `
    <label>${escapeHtml(label)}<input type="color" name="${key}" value="${escapeHtml(managerState.settings.colors[key])}"></label>
  `).join("");
}

function renderAddons() {
  document.querySelector("[data-addon-manager]").innerHTML = `
    <h3>Add-ons</h3>
    ${hydratedAddons().map((addon, index) => `
      <div class="manager-row" data-addon-row="${index}">
        <label><input type="checkbox" data-addon-enabled ${addon.enabled !== false ? "checked" : ""}> Show</label>
        <input data-addon-label value="${escapeHtml(addon.label)}">
        <input data-addon-value value="${escapeHtml(addon.value)}">
        <label>Low<input type="number" min="0" data-addon-low value="${escapeHtml(addon.priceLow ?? 0)}"></label>
        <label>High<input type="number" min="0" data-addon-high value="${escapeHtml(addon.priceHigh ?? 0)}"></label>
        <input data-addon-unit value="${escapeHtml(addon.priceUnit || "")}" placeholder="per hour, per table, etc.">
        <button class="button ghost danger" type="button" data-remove-addon="${index}">Remove</button>
      </div>
    `).join("")}
  `;
}

function renderPackages() {
  document.querySelector("[data-package-manager]").innerHTML = `
    ${managerState.settings.packages.map((pkg, index) => `
      <div class="manager-row tall package-manager-row" data-package-row="${index}">
        <label><input type="checkbox" data-package-enabled ${pkg.enabled !== false ? "checked" : ""}> Show</label>
        <input data-package-name value="${escapeHtml(pkg.name || "")}" placeholder="Package name">
        <input data-package-subtitle value="${escapeHtml(pkg.subtitle || "")}" placeholder="Subtitle">
        <label>Low<input type="number" min="0" data-package-low value="${escapeHtml(pkg.priceLow || 0)}"></label>
        <label>High<input type="number" min="0" data-package-high value="${escapeHtml(pkg.priceHigh || 0)}"></label>
        <input data-package-investment-note value="${escapeHtml(pkg.investmentNote || "")}" placeholder="Investment note">
        <textarea data-package-time rows="2" placeholder="Time block">${escapeHtml(pkg.timeBlock || "")}</textarea>
        <textarea data-package-description rows="2" placeholder="Description">${escapeHtml(pkg.description || "")}</textarea>
        <textarea data-package-includes rows="4" placeholder="What's included, one item per line">${escapeHtml(packageIncludedText(pkg))}</textarea>
        <textarea data-package-blueprint rows="2" placeholder="Blueprint">${escapeHtml(pkg.blueprint || "")}</textarea>
        <button class="button ghost danger" type="button" data-remove-package="${index}">Remove</button>
      </div>
    `).join("")}
  `;
}

function renderVendors() {
  document.querySelector("[data-vendor-manager]").innerHTML = `
    <h3>Planning Help Cards</h3>
    <p class="manager-hint">Drag the Move handle to reorder these cards, then click Save Offerings.</p>
    ${managerState.settings.vendors.map((vendor, index) => `
      <div class="manager-row tall vendor-manager-row" data-vendor-row="${index}">
        <button class="drag-handle" type="button" draggable="true" data-vendor-drag aria-label="Move planning card">Move</button>
        <label><input type="checkbox" data-vendor-enabled ${vendor.enabled !== false ? "checked" : ""}> Show</label>
        <input data-vendor-category value="${escapeHtml(vendor.category)}">
        <input data-vendor-title value="${escapeHtml(vendor.title)}">
        <textarea data-vendor-description rows="2">${escapeHtml(vendor.description)}</textarea>
        <button class="button ghost danger" type="button" data-remove-vendor="${index}">Remove</button>
      </div>
    `).join("")}
  `;
}

function renderGalleryManager() {
  document.querySelector("[data-gallery-manager]").innerHTML = managerState.settings.gallery.map((photo, index) => `
    <div class="manager-row gallery-manager-row" data-gallery-row="${index}">
      <img src="${escapeHtml(photo.src)}" alt="">
      <label><input type="checkbox" data-gallery-enabled ${photo.enabled !== false ? "checked" : ""}> Show</label>
      <input data-gallery-src value="${escapeHtml(photo.src)}">
      <input data-gallery-caption value="${escapeHtml(photo.caption)}">
      <button class="button ghost danger" type="button" data-remove-gallery="${index}">Remove</button>
    </div>
  `).join("");
}

function renderFaqManager() {
  document.querySelector("[data-faq-manager]").innerHTML = `
    <p class="manager-hint">Drag the Move handle to reorder these questions, then click Save Helpful Answers.</p>
    ${managerState.settings.faqs.map((faq, index) => `
    <div class="manager-row tall faq-manager-row" data-faq-row="${index}">
      <button class="drag-handle" type="button" draggable="true" data-faq-drag aria-label="Move helpful answer">Move</button>
      <label><input type="checkbox" data-faq-enabled ${faq.enabled !== false ? "checked" : ""}> Show</label>
      <input data-faq-question value="${escapeHtml(faq.question)}">
      <textarea data-faq-answer rows="3">${escapeHtml(faq.answer)}</textarea>
      <button class="button ghost danger" type="button" data-remove-faq="${index}">Remove</button>
    </div>
  `).join("")}
  `;
}

function collectSettingsFromManagers() {
  document.querySelectorAll("[data-addon-row]").forEach((row) => {
    const index = Number(row.dataset.addonRow);
    managerState.settings.addons[index] = {
      enabled: row.querySelector("[data-addon-enabled]").checked,
      label: row.querySelector("[data-addon-label]").value,
      value: row.querySelector("[data-addon-value]").value,
      priceLow: Number(row.querySelector("[data-addon-low]").value || 0),
      priceHigh: Number(row.querySelector("[data-addon-high]").value || 0),
      priceUnit: row.querySelector("[data-addon-unit]").value
    };
  });
  managerState.settings.packages = [...document.querySelectorAll("[data-package-row]")].map((row) => ({
    enabled: row.querySelector("[data-package-enabled]").checked,
    name: row.querySelector("[data-package-name]").value,
    subtitle: row.querySelector("[data-package-subtitle]").value,
    priceLow: Number(row.querySelector("[data-package-low]").value || 0),
    priceHigh: Number(row.querySelector("[data-package-high]").value || 0),
    investmentNote: row.querySelector("[data-package-investment-note]").value,
    timeBlock: row.querySelector("[data-package-time]").value,
    description: row.querySelector("[data-package-description]").value,
    included: row.querySelector("[data-package-includes]").value.split("\n").map((item) => item.trim()).filter(Boolean),
    includes: row.querySelector("[data-package-includes]").value,
    blueprint: row.querySelector("[data-package-blueprint]").value
  }));
  managerState.settings.vendors = [...document.querySelectorAll("[data-vendor-row]")].map((row) => ({
      enabled: row.querySelector("[data-vendor-enabled]").checked,
      category: row.querySelector("[data-vendor-category]").value,
      title: row.querySelector("[data-vendor-title]").value,
      description: row.querySelector("[data-vendor-description]").value
  }));
  document.querySelectorAll("[data-gallery-row]").forEach((row) => {
    const index = Number(row.dataset.galleryRow);
    managerState.settings.gallery[index] = {
      enabled: row.querySelector("[data-gallery-enabled]").checked,
      src: row.querySelector("[data-gallery-src]").value,
      caption: row.querySelector("[data-gallery-caption]").value
    };
  });
  managerState.settings.faqs = [...document.querySelectorAll("[data-faq-row]")].map((row) => ({
      enabled: row.querySelector("[data-faq-enabled]").checked,
      question: row.querySelector("[data-faq-question]").value,
      answer: row.querySelector("[data-faq-answer]").value
  }));
}

async function saveSettings() {
  collectSettingsFromManagers();
  await apiRequest("save_settings", { settings: managerState.settings });
  showToast("Site settings saved.");
}

unlockButton.addEventListener("click", unlock);
codeInput.addEventListener("keydown", (event) => {
  if (event.key === "Enter") unlock();
});

logoutButton.addEventListener("click", () => {
  lockOwnerDesk("Owner desk locked.");
});

["click", "keydown", "input", "change", "scroll", "touchstart"].forEach((eventName) => {
  window.addEventListener(eventName, noteOwnerActivity, { passive: true });
});

ownerTabs.forEach((tab) => {
  tab.addEventListener("click", () => showOwnerSection(tab.dataset.ownerTab));
});

confirmedForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  await apiRequest("add_confirmed", Object.fromEntries(new FormData(confirmedForm)));
  confirmedForm.reset();
  await loadOwnerData();
  showToast("Confirmed booking added.");
});

blockForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  await apiRequest("add_block", Object.fromEntries(new FormData(blockForm)));
  blockForm.reset();
  await loadOwnerData();
  showToast("Unavailable time added.");
});

brandingForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  const data = Object.fromEntries(new FormData(brandingForm));
  managerState.settings.branding = {
    venueName: data.venueName || "Grange Station",
    tagline: data.tagline || "Wilson Creek, WA",
    logoImage: data.logoImage || "",
    heroImage: data.heroImage || "assets/grange-station.png",
    socialImage: data.socialImage || data.heroImage || "assets/grange-station.png"
  };
  await saveSettings();
});

brandingForm.addEventListener("change", async (event) => {
  const input = event.target.closest("[data-upload-image]");
  if (!input || !input.files.length) return;
  const targetField = input.dataset.uploadImage;
  try {
    showToast("Uploading image...");
    const path = await uploadImage(input.files[0]);
    brandingForm.elements[targetField].value = path;
    input.value = "";
    showToast("Image uploaded. Click Save Branding when ready.");
  } catch (error) {
    showToast(error.message || "Image upload failed.");
  }
});

settingsForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  const data = Object.fromEntries(new FormData(settingsForm));
  Object.keys(managerState.settings.pricing).forEach((key) => managerState.settings.pricing[key] = Number(data[key] || 0));
  Object.keys(managerState.settings.colors).forEach((key) => managerState.settings.colors[key] = data[key] || managerState.settings.colors[key]);
  await saveSettings();
});

confirmedList.addEventListener("click", async (event) => {
  const tallyButton = event.target.closest("[data-refresh-tally]");
  if (tallyButton) {
    const form = tallyButton.closest("form");
    if (form) refreshTally(form, true);
    return;
  }
  const button = event.target.closest("[data-delete-confirmed]");
  if (!button) return;
  await apiRequest("delete_confirmed", { id: button.dataset.deleteConfirmed });
  await loadOwnerData();
  showToast("Confirmed booking removed.");
});

confirmedList.addEventListener("input", (event) => {
  const form = event.target.closest("[data-booking-edit]");
  if (!form || !tallyFieldChanged(event.target)) return;
  refreshTally(form, true);
});

confirmedList.addEventListener("change", (event) => {
  const form = event.target.closest("[data-booking-edit]");
  if (!form || !tallyFieldChanged(event.target)) return;
  refreshTally(form, true);
});

confirmedList.addEventListener("submit", async (event) => {
  const form = event.target.closest("[data-booking-edit]");
  if (!form) return;
  event.preventDefault();
  const formData = new FormData(form);
  const payload = Object.fromEntries(formData);
  payload.addons = formData.getAll("addons");
  payload.notes = composeBookingNotes(payload.notes, payload.addons, payload.package);
  await apiRequest("update_confirmed", payload);
  await loadOwnerData();
  showToast("Confirmed booking updated.");
});

blockList.addEventListener("click", async (event) => {
  const button = event.target.closest("[data-delete-block]");
  if (!button) return;
  await apiRequest("delete_block", { id: button.dataset.deleteBlock });
  await loadOwnerData();
  showToast("Unavailable time removed.");
});

requestList.addEventListener("click", async (event) => {
  const tallyButton = event.target.closest("[data-refresh-tally]");
  if (tallyButton) {
    const form = tallyButton.closest("form");
    if (form) refreshTally(form, true);
    return;
  }
  const button = event.target.closest("[data-approve-request]");
  if (!button) return;
  await apiRequest("approve_request", { id: button.dataset.approveRequest });
  await loadOwnerData();
  showToast("Request approved and added to confirmed bookings.");
});

requestList.addEventListener("input", (event) => {
  const form = event.target.closest("[data-request-edit]");
  if (!form || !tallyFieldChanged(event.target)) return;
  refreshTally(form, true);
});

requestList.addEventListener("change", (event) => {
  const form = event.target.closest("[data-request-edit]");
  if (!form || !tallyFieldChanged(event.target)) return;
  refreshTally(form, true);
});

requestList.addEventListener("submit", async (event) => {
  const form = event.target.closest("[data-request-edit]");
  if (!form) return;
  event.preventDefault();
  const formData = new FormData(form);
  const payload = Object.fromEntries(formData);
  payload.addons = formData.getAll("addons");
  await apiRequest("update_request", payload);
  await loadOwnerData();
  showToast("Request details updated.");
});

document.querySelector("[data-save-offerings]").addEventListener("click", saveSettings);
document.querySelector("[data-save-packages]").addEventListener("click", saveSettings);
document.querySelector("[data-save-gallery]").addEventListener("click", saveSettings);
document.querySelector("[data-save-faqs]").addEventListener("click", saveSettings);

document.querySelector("[data-add-addon]").addEventListener("click", () => {
  collectSettingsFromManagers();
  managerState.settings.addons.push({ label: "New add-on", value: "New add-on", priceLow: 0, priceHigh: 0, priceUnit: "", enabled: true });
  renderAddons();
});

document.querySelector("[data-addon-manager]").addEventListener("click", (event) => {
  const button = event.target.closest("[data-remove-addon]");
  if (!button) return;
  collectSettingsFromManagers();
  managerState.settings.addons.splice(Number(button.dataset.removeAddon), 1);
  renderAddons();
});

document.querySelector("[data-add-package]").addEventListener("click", () => {
  collectSettingsFromManagers();
  managerState.settings.packages.push({
    name: "New Package",
    subtitle: "Package type",
    priceLow: 0,
    priceHigh: 0,
    investmentNote: "",
    timeBlock: "Describe the access window.",
    description: "Describe who this package fits.",
    included: ["List included items."],
    includes: "List included items.",
    blueprint: "Add planning notes.",
    enabled: true
  });
  renderPackages();
});

document.querySelector("[data-package-manager]").addEventListener("click", (event) => {
  const button = event.target.closest("[data-remove-package]");
  if (!button) return;
  collectSettingsFromManagers();
  managerState.settings.packages.splice(Number(button.dataset.removePackage), 1);
  renderPackages();
});

document.querySelector("[data-add-vendor]").addEventListener("click", () => {
  collectSettingsFromManagers();
  managerState.settings.vendors.push({
    category: "New Category",
    title: "New planning card",
    description: "Describe this offering or vendor help option.",
    enabled: true
  });
  renderVendors();
});

const vendorManager = document.querySelector("[data-vendor-manager]");
let draggedVendorRow = null;

vendorManager.addEventListener("dragstart", (event) => {
  const handle = event.target.closest("[data-vendor-drag]");
  if (!handle) return;
  draggedVendorRow = handle.closest("[data-vendor-row]");
  draggedVendorRow.classList.add("dragging");
  event.dataTransfer.effectAllowed = "move";
  event.dataTransfer.setData("text/plain", draggedVendorRow.dataset.vendorRow || "");
});

vendorManager.addEventListener("dragover", (event) => {
  if (!draggedVendorRow) return;
  event.preventDefault();
  const target = event.target.closest("[data-vendor-row]");
  if (!target || target === draggedVendorRow) return;
  const rect = target.getBoundingClientRect();
  const placeAfter = event.clientY > rect.top + rect.height / 2;
  target.parentNode.insertBefore(draggedVendorRow, placeAfter ? target.nextSibling : target);
});

vendorManager.addEventListener("dragend", () => {
  if (!draggedVendorRow) return;
  draggedVendorRow.classList.remove("dragging");
  draggedVendorRow = null;
  collectSettingsFromManagers();
  renderVendors();
});

vendorManager.addEventListener("click", (event) => {
  const button = event.target.closest("[data-remove-vendor]");
  if (!button) return;
  collectSettingsFromManagers();
  managerState.settings.vendors.splice(Number(button.dataset.removeVendor), 1);
  renderVendors();
});

document.querySelector("[data-add-gallery-image]").addEventListener("click", () => {
  const src = document.querySelector("[data-new-gallery-src]").value.trim();
  const caption = document.querySelector("[data-new-gallery-caption]").value.trim() || "Grange Station venue gallery image.";
  if (!src) return showToast("Add the image path first.");
  managerState.settings.gallery.push({ src, caption, enabled: true });
  renderGalleryManager();
});

document.querySelector("[data-gallery-manager]").addEventListener("click", (event) => {
  const button = event.target.closest("[data-remove-gallery]");
  if (!button) return;
  managerState.settings.gallery.splice(Number(button.dataset.removeGallery), 1);
  renderGalleryManager();
});

document.querySelector("[data-add-faq]").addEventListener("click", () => {
  collectSettingsFromManagers();
  managerState.settings.faqs.push({ question: "New question", answer: "New answer.", enabled: true });
  renderFaqManager();
});

const faqManager = document.querySelector("[data-faq-manager]");
let draggedFaqRow = null;

faqManager.addEventListener("dragstart", (event) => {
  const handle = event.target.closest("[data-faq-drag]");
  if (!handle) return;
  draggedFaqRow = handle.closest("[data-faq-row]");
  draggedFaqRow.classList.add("dragging");
  event.dataTransfer.effectAllowed = "move";
  event.dataTransfer.setData("text/plain", draggedFaqRow.dataset.faqRow || "");
});

faqManager.addEventListener("dragover", (event) => {
  if (!draggedFaqRow) return;
  event.preventDefault();
  const target = event.target.closest("[data-faq-row]");
  if (!target || target === draggedFaqRow) return;
  const rect = target.getBoundingClientRect();
  const placeAfter = event.clientY > rect.top + rect.height / 2;
  target.parentNode.insertBefore(draggedFaqRow, placeAfter ? target.nextSibling : target);
});

faqManager.addEventListener("dragend", () => {
  if (!draggedFaqRow) return;
  draggedFaqRow.classList.remove("dragging");
  draggedFaqRow = null;
  collectSettingsFromManagers();
  renderFaqManager();
});

faqManager.addEventListener("click", (event) => {
  const button = event.target.closest("[data-remove-faq]");
  if (!button) return;
  collectSettingsFromManagers();
  managerState.settings.faqs.splice(Number(button.dataset.removeFaq), 1);
  renderFaqManager();
});

document.querySelector("[data-export]").addEventListener("click", () => {
  const rows = requestView === "archived" ? managerState.archivedRequests : managerState.requests;
  if (!rows.length) return showToast("No requests to export yet.");
  const headers = ["createdAt", "requestType", "name", "email", "phone", "date", "time", "endTime", "guests", "package", "eventType", "space", "vendorHelp", "message"];
  const csv = [headers.join(","), ...rows.map((row) => headers.map((key) => `"${String(row[key] || "").replaceAll('"', '""')}"`).join(","))].join("\n");
  const link = document.createElement("a");
  link.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
  link.download = "grange-station-requests.csv";
  link.click();
});

document.querySelectorAll("[data-request-view]").forEach((button) => {
  button.addEventListener("click", () => {
    requestView = button.dataset.requestView;
    renderRequests();
  });
});

document.querySelector("[data-request-action]").addEventListener("click", async () => {
  const selectedIds = [...document.querySelectorAll("[data-request-select]:checked")].map((input) => input.value);
  if (!selectedIds.length) {
    showToast("Select one or more requests first.");
    return;
  }
  await apiRequest(requestView === "archived" ? "delete_archived_requests" : "archive_requests", { ids: selectedIds });
  await loadOwnerData();
  showToast(requestView === "archived" ? "Selected archived requests deleted." : "Selected requests archived.");
});

document.querySelector("[data-request-restore]").addEventListener("click", async () => {
  const selectedIds = [...document.querySelectorAll("[data-request-select]:checked")].map((input) => input.value);
  if (!selectedIds.length) {
    showToast("Select one or more archived requests first.");
    return;
  }
  await apiRequest("unarchive_requests", { ids: selectedIds });
  await loadOwnerData();
  showToast("Selected requests moved back to active.");
});

if (sessionStorage.getItem("grangeStationOwnerUnlocked") === "true") {
  login.hidden = true;
  desk.hidden = false;
  startOwnerSessionTimer();
  loadOwnerData().catch((error) => {
    sessionStorage.removeItem("grangeStationOwnerUnlocked");
    login.hidden = false;
    desk.hidden = true;
    showToast(error.message === "Owner login required."
      ? "Please enter the owner code again."
      : `Could not load owner data: ${error.message}`);
  });
}

Youez - 2016 - github.com/yon3zu
LinuXploit