<?php
require_once __DIR__ . '/auth.php';
require_login();
require_permission('orders');
if (isset($_GET['logout'])) logout();
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Orders — Grove Admin</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
  --nav: #1a1a1a; --bg: #f5f4f0; --white: #fff; --border: #e5e3de;
  --muted: #888; --danger: #e74c3c; --radius: 10px;
  --c-new: #dc2626; --c-prep: #d97706; --c-ready: #16a34a; --c-done: #64748b;
}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans Arabic', Tahoma, sans-serif;
  background: var(--bg); color: #1a1a1a; min-height: 100vh; display: flex; flex-direction: column; }
button { font: inherit; cursor: pointer; border: none; background: transparent; color: inherit; }
a { color: inherit; }

/* NAV (matches admin/index.php styling) */
.topnav { background: var(--nav); color: #fff; padding: 0 24px; display: flex; align-items: center;
  justify-content: space-between; height: 58px; box-shadow: 0 1px 0 rgba(0,0,0,0.05); flex-wrap: wrap; }
.brand { font-size: 18px; font-weight: 600; letter-spacing: 0.3px; }
.nav-tabs { display: flex; gap: 4px; }
.nav-tab { padding: 10px 16px; border-radius: 8px; cursor: pointer; font-size: 14px; color: #c9c9c9;
  text-decoration: none; display: inline-block; transition: background 0.15s, color 0.15s; }
.nav-tab:hover { background: rgba(255,255,255,0.08); color: #fff; }
.nav-tab.active { background: #fff; color: var(--nav); font-weight: 500; }
.user { font-size: 13px; color: #c9c9c9; display: flex; align-items: center; gap: 12px; }
.user a.logout { color: #fff; opacity: 0.7; text-decoration: none; padding: 6px 10px; border-radius: 6px; transition: opacity 0.15s, background 0.15s; }
.user a.logout:hover { opacity: 1; background: rgba(255,255,255,0.08); }

/* MAIN */
.main { flex: 1; max-width: 1400px; width: 100%; margin: 0 auto; padding: 24px; }

/* Toolbar: title + refresh/auto controls */
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; flex-wrap: wrap; gap: 12px; }
.toolbar h1 { font-size: 22px; font-weight: 600; }
.tool-controls { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--muted); }
.tool-controls label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
.tool-btn { padding: 7px 14px; background: var(--white); border: 1px solid var(--border); border-radius: 8px;
  font-size: 13px; transition: background 0.15s; }
.tool-btn:hover { background: #eee9df; }
.last-refresh { font-size: 11px; color: var(--muted); }

/* Status filter pills */
.status-filter { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
.status-pill { padding: 6px 14px; border-radius: 999px; background: var(--white); border: 1px solid var(--border);
  font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s; display: flex; align-items: center; gap: 6px; }
.status-pill:hover { background: #eee9df; }
.status-pill.active { background: var(--nav); color: #fff; border-color: var(--nav); }
.status-pill .count { background: rgba(0,0,0,0.08); padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
.status-pill.active .count { background: rgba(255,255,255,0.2); }

/* Orders grid */
.orders-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 14px; }
.order-card { background: var(--white); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
  transition: transform 0.12s, box-shadow 0.12s; display: flex; flex-direction: column; }
.order-card:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,0.06); }
.order-card.is-new { border-left: 4px solid var(--c-new); }
.order-card.is-preparing { border-left: 4px solid var(--c-prep); }
.order-card.is-ready { border-left: 4px solid var(--c-ready); }
.order-card.is-completed { border-left: 4px solid var(--c-done); opacity: 0.75; }
.order-card.is-voided { opacity: 0.5; }
.order-card.is-pending_whatsapp { border-left: 4px solid #d4a373; background: #faf7f0; }
.order-card.is-abandoned { opacity: 0.4; }

.card-head { padding: 12px 16px; display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 1px solid var(--border); }
.order-num { font-weight: 700; font-size: 15px; letter-spacing: 0.3px; }
.order-time { font-size: 11px; color: var(--muted); margin-top: 2px; }
.order-status-badge { padding: 3px 10px; border-radius: 999px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.order-status-badge.s-new { background: #fef2f2; color: var(--c-new); }
.order-status-badge.s-preparing { background: #fef3c7; color: var(--c-prep); }
.order-status-badge.s-ready { background: #dcfce7; color: var(--c-ready); }
.order-status-badge.s-completed { background: #f1f5f9; color: var(--c-done); }
.order-status-badge.s-voided { background: #f1f5f9; color: var(--muted); }
.order-status-badge.s-pending_whatsapp { background: #fef7e0; color: #92640b; }
.order-status-badge.s-abandoned { background: #f1f5f9; color: var(--muted); }

/* Branch / mode / table line on the order card. */
.order-fulfil {
  font-size: 12px;
  font-weight: 600;
  color: var(--nav);
  background: #f5f3ee;
  border-radius: 6px;
  padding: 5px 10px;
  margin: 6px 0;
  display: inline-block;
}

/* Payment method badge — sits next to the status badge in the
   card head. Color signals the operational meaning to staff:
     green  → already paid online; just prepare and hand over
     amber  → cash/counter; collect payment when handing over
     grey   → WhatsApp legacy (no payment method recorded)
     red    → payment failed (shouldn't show in active list normally) */
.payment-badge { padding: 3px 10px; border-radius: 999px; font-size: 10px;
                 font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;
                 white-space: nowrap; margin-top: 4px; display: inline-block; }
.payment-badge.p-paid     { background: #dcfce7; color: #14532d; }
.payment-badge.p-cash     { background: #fef3c7; color: #78350f; }
.payment-badge.p-counter  { background: #fef3c7; color: #78350f; }
.payment-badge.p-pending  { background: #fef3c7; color: #78350f; }
.payment-badge.p-failed   { background: #fee2e2; color: #991b1b; }
.payment-badge.p-whatsapp { background: #f1f5f9; color: var(--muted); }

.card-body { padding: 12px 16px; flex: 1; }
.customer-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.customer-name { font-weight: 600; font-size: 14px; }
.customer-phone { font-size: 12px; color: var(--muted); direction: ltr; unicode-bidi: isolate; }
.customer-phone a { color: #25D366; text-decoration: none; font-weight: 500; }
.customer-notes { background: #fef3c7; border-left: 3px solid #f59e0b; padding: 8px 10px; font-size: 12px; border-radius: 4px; margin-bottom: 10px; line-height: 1.4; }
.items-list { font-size: 12px; color: #333; line-height: 1.5; }
.items-list .it-row { display: flex; justify-content: space-between; padding: 2px 0; }
.it-qty { color: var(--muted); margin-right: 6px; }
.it-total { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border); display: flex; justify-content: space-between; font-weight: 600; font-size: 14px; }

.card-actions { padding: 10px 12px; background: #fafaf7; border-top: 1px solid var(--border); display: flex; gap: 6px; flex-wrap: wrap; }
.act-btn { padding: 7px 12px; border-radius: 6px; font-size: 12px; font-weight: 500; transition: background 0.15s; }
.act-btn-primary { background: var(--nav); color: #fff; }
.act-btn-primary:hover { background: #333; }
.act-btn-ghost { background: transparent; color: #555; border: 1px solid var(--border); }
.act-btn-ghost:hover { background: #eee9df; }
.act-btn-print { background: #2563eb; color: #fff; }
.act-btn-print:hover { background: #1d4ed8; }
.act-btn-danger { background: transparent; color: var(--danger); border: 1px solid #fecaca; }
.act-btn-danger:hover { background: #fef2f2; }
/* Accept button — green, prominent, pulses to catch the eye. It's
   the primary call-to-action on an unacknowledged new order. */
.act-btn-accept {
  background: #16a34a; color: #fff;
  font-weight: 600;
  animation: acceptPulse 1.4s ease-in-out infinite;
}
.act-btn-accept:hover { background: #15803d; animation: none; }
/* Confirm button — distinct from Accept because the semantics differ.
   Confirm promotes Pending→New (commits the order + sends email).
   Styled in the signature LTT red so staff knows it's the big one. */
.act-btn-confirm {
  background: #c4362c; color: #fff;
  font-weight: 600;
  padding: 9px 16px;
}
.act-btn-confirm:hover { background: #9d2a22; }
/* WhatsApp send button — uses the brand's familiar green so staff
   instantly recognize it as a WhatsApp action. Shown only on
   completed orders (where pinging the customer is appropriate). */
.act-btn-whatsapp {
  background: #25d366; color: #fff;
  font-weight: 600;
}
.act-btn-whatsapp:hover { background: #1ebd5a; }
@keyframes acceptPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.5); }
  50%      { box-shadow: 0 0 0 8px rgba(22, 163, 74, 0); }
}

/* Sticky alarm banner — spans the top of the page when unacked
   orders exist. Gives staff a giant "Accept All" option for rush
   situations, and visually reinforces that the ringing is real. */
.alarm-banner {
  display: none;
  position: sticky; top: 0; z-index: 50;
  background: linear-gradient(90deg, #dc2626, #ef4444);
  color: #fff;
  padding: 14px 20px;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  box-shadow: 0 4px 12px rgba(220, 38, 38, 0.35);
  animation: bannerFlash 1.2s ease-in-out infinite;
}
.alarm-banner.show { display: flex; }
.alarm-banner-text {
  font-size: 15px; font-weight: 600; letter-spacing: 0.3px;
  display: flex; align-items: center; gap: 10px;
}
.alarm-banner-text::before {
  content: "🔔";
  font-size: 22px;
  animation: bellShake 0.6s ease-in-out infinite;
  display: inline-block;
}
.alarm-banner button {
  padding: 10px 20px;
  background: #fff;
  color: #dc2626;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.3px;
  transition: transform 0.1s, background 0.15s;
}
.alarm-banner button:hover { background: #fef2f2; transform: scale(1.03); }
@keyframes bannerFlash {
  0%, 100% { background: linear-gradient(90deg, #dc2626, #ef4444); }
  50%      { background: linear-gradient(90deg, #ef4444, #f87171); }
}
@keyframes bellShake {
  0%, 100% { transform: rotate(0deg); }
  20% { transform: rotate(-15deg); }
  40% { transform: rotate(15deg); }
  60% { transform: rotate(-10deg); }
  80% { transform: rotate(10deg); }
}

/* Empty state */
.empty-state { text-align: center; padding: 80px 20px; color: var(--muted); font-size: 14px; }
.empty-state h3 { font-size: 18px; color: #555; margin-bottom: 6px; }

/* Toast (reuses pattern from admin/index.php) */
.toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  background: var(--nav); color: #fff; padding: 12px 22px; border-radius: 8px; font-size: 14px;
  opacity: 0; pointer-events: none; transition: opacity 0.2s, transform 0.2s; z-index: 100; }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }
.toast.err { background: var(--danger); }

/* Pulse animation for brand-new unacknowledged orders — draws staff's eye */
@keyframes pulseNew { 0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.4); } 50% { box-shadow: 0 0 0 6px rgba(220, 38, 38, 0); } }
.order-card.is-new { animation: pulseNew 2s ease-in-out 3; }
</style>
</head>
<body>

<!-- NAV -->
<div class="topnav">
  <div class="brand">Grove Admin</div>
  <div class="nav-tabs">
    <a class="nav-tab" href="index.php">Dashboard</a>
    <a class="nav-tab" href="index.php">Categories</a>
    <a class="nav-tab" href="index.php">Menu Items</a>
    <a class="nav-tab active" href="orders.php">Orders</a>
    <a class="nav-tab" href="index.php">Settings</a>
  </div>
  <div class="user">Hi, <?= htmlspecialchars($_SESSION['admin_name'] ?? 'admin') ?> · <a class="logout" href="?logout=1">Sign out</a></div>
</div>

<!-- Sticky alarm banner — appears only when there are new orders
     that staff hasn't acknowledged yet in this browser session. -->
<div class="alarm-banner" id="alarmBanner">
  <div class="alarm-banner-text"><span id="alarmCount">New orders waiting</span></div>
  <button onclick="acceptAllNew()">Accept all</button>
</div>

<!-- MAIN -->
<div class="main">

  <div class="toolbar">
    <h1>Orders</h1>
    <div class="tool-controls">
      <span class="last-refresh" id="lastRefresh">—</span>
      <label><input type="checkbox" id="soundOn" checked> 🔔 Sound</label>
      <label><input type="checkbox" id="notifyOn"> 🖥️ Desktop alerts</label>
      <label><input type="checkbox" id="autoRefresh" checked> Auto-refresh</label>
      <button class="tool-btn" onclick="loadOrders()">Refresh now</button>
    </div>
  </div>

  <!-- Inline banner shown the first time the user enables Desktop alerts.
       Browser permission prompts can be denied silently if they fire too
       fast; this banner lets the user click an explicit "Enable" button
       to trigger the prompt as a deliberate user gesture. -->
  <div id="notifyBanner" style="display:none;background:#fef3c7;border-left:3px solid #f59e0b;border-radius:4px;padding:10px 14px;margin:0 16px 12px;font-size:13px;color:#78350f">
    <strong>Desktop alerts</strong> let you hear the chime even when this tab is in the background or the browser is minimized.
    <button class="tool-btn" style="margin-left:10px" onclick="requestNotifyPermission()">Enable now</button>
    <button class="tool-btn" style="margin-left:6px;background:transparent" onclick="dismissNotifyBanner()">Not now</button>
  </div>

  <!-- Status filter pills — each shows a live count of orders in that status.
       "Pending" is orders the customer placed but staff hasn't confirmed
       from WhatsApp yet. These are hidden from "All" by default. -->
  <div class="status-filter" id="statusFilter">
    <button class="status-pill active" data-status="active">Active <span class="count" id="c-active">0</span></button>
    <button class="status-pill" data-status="new">🔴 New <span class="count" id="c-new">0</span></button>
    <button class="status-pill" data-status="preparing">🟡 Preparing <span class="count" id="c-preparing">0</span></button>
    <button class="status-pill" data-status="ready">🟢 Ready <span class="count" id="c-ready">0</span></button>
    <button class="status-pill" data-status="completed">✓ Completed <span class="count" id="c-completed">0</span></button>
    <button class="status-pill" data-status="pending_whatsapp">⏳ Pending WhatsApp <span class="count" id="c-pending">0</span></button>
  </div>

  <!-- Orders grid — cards rendered by JS -->
  <div id="ordersGrid">
    <div class="empty-state"><h3>Loading…</h3></div>
  </div>

</div>

<div class="toast" id="toast"></div>

<!-- Hidden audio element for the chime. We create a tiny Web Audio "ding"
     rather than loading an mp3 so the page stays self-contained. -->
<script>
// ============================================================
//  ORDERS DASHBOARD
// ============================================================
//  Polls list_orders every 5 seconds. When an order appears
//  that wasn't in the previous poll, we play a chime and pulse
//  the card. Tracks seen-IDs in a Set to avoid re-triggering on
//  refreshes. Staff can filter by status, change status via
//  one-click buttons, and print a thermal receipt.
// ============================================================

let currentFilter = 'active';
let seenIds = new Set();
let firstLoad = true;
let refreshTimer = null;

// ---------- Toast ----------
function toast(msg, ok = true) {
  const t = document.getElementById('toast');
  t.textContent = msg;
  t.classList.toggle('err', !ok);
  t.classList.add('show');
  clearTimeout(t._timer);
  t._timer = setTimeout(() => t.classList.remove('show'), 2500);
}

// ---------- Alarm system (looping, until acknowledged) ----------
//
// When new unacknowledged orders exist, play a louder two-tone
// alert pattern every 2.5 seconds. Volume is significantly
// higher than the old single-shot chime (gain 0.8 vs 0.35) and
// uses square waves with a touch of tremolo for punch.
//
// Acknowledgement is tracked client-side in localStorage so:
//   - Reloading the page doesn't re-trigger the alarm for orders
//     staff has already acknowledged.
//   - Un-acknowledged orders from yesterday don't scream when
//     staff opens the dashboard fresh today — because we also
//     auto-acknowledge anything older than 12 hours.
// The database status stays "new" until staff explicitly clicks
// "Start preparing" — acknowledgement is just "I've seen this."
//
let audioCtx = null;
let alarmTimer = null;            // setInterval handle for the looping alarm
let ACK_KEY = 'qrmenu_acked_orders';

// Load the set of acknowledged order IDs from localStorage.
// We track each ack as "<id>|<created_at>" so an old acked id
// can't accidentally suppress the alarm for a freshly-created
// order that happens to reuse the same auto-increment id (which
// can happen after a TRUNCATE or AUTO_INCREMENT reset on the
// orders table during testing).
//
// The format is intentionally backward-compatible: pre-existing
// integer-only entries are treated as legacy and migrated lazily.
// Entries older than 30 days are dropped on load to keep the set
// bounded.
function loadAcked() {
  try {
    const raw = localStorage.getItem(ACK_KEY);
    if (!raw) return new Set();
    const arr = JSON.parse(raw);
    if (!Array.isArray(arr)) return new Set();
    // Drop legacy integer-only entries — without created_at we
    // can't tell if they're stale, and stale acks cause the alarm
    // to silently fail on order id reuse. Better to re-prompt
    // the user to Accept than to miss a real new order.
    const filtered = arr.filter(v => typeof v === 'string' && v.includes('|'));
    return new Set(filtered);
  } catch (_) { return new Set(); }
}

function saveAcked(set) {
  try {
    // Keep the list bounded — only retain the most recent 500 entries.
    const arr = Array.from(set).slice(-500);
    localStorage.setItem(ACK_KEY, JSON.stringify(arr));
  } catch (_) {}
}

// Build the composite key for an order. Used by both the ack
// store and every ackedIds.has() check, so they're consistent.
function ackKey(o) {
  // The created_at timestamp is what makes the key unique over
  // time. Even after AUTO_INCREMENT reset, two different orders
  // never share both id AND created_at.
  return o.id + '|' + (o.created_at || '');
}

let ackedIds = loadAcked();

function ackOrder(id) {
  // Find the order in the current list to grab its created_at.
  // If we can't find it (shouldn't happen), fall back to id-only
  // which is still better than dropping the ack entirely.
  const order = (lastOrders || []).find(o => o.id === id);
  const key = order ? ackKey(order) : (id + '|');
  ackedIds.add(key);
  saveAcked(ackedIds);
  updateAlarmState();  // if this was the last unacked order, alarm stops
  loadOrders();        // re-render so the Accept button disappears
}

// Play one "burst" of the alert pattern — a louder, more
// attention-grabbing version of the chime. Higher gain, square
// waves, and three tones instead of two.
function playAlertBurst() {
  if (!document.getElementById('soundOn').checked) return;
  try {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    // Some browsers suspend AudioContext until there's a user gesture.
    // Calling resume() is a no-op if already running.
    if (audioCtx.state === 'suspended') audioCtx.resume();

    const now = audioCtx.currentTime;
    const tones = [
      { f: 880,  t: 0.00, d: 0.18 },
      { f: 1175, t: 0.18, d: 0.18 },
      { f: 1480, t: 0.36, d: 0.28 },
    ];
    tones.forEach(({ f, t, d }) => {
      const osc  = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.type = 'square';                 // punchier than sine
      osc.frequency.value = f;
      // Volume envelope: ramp up fast, sustain, ramp down
      gain.gain.setValueAtTime(0, now + t);
      gain.gain.linearRampToValueAtTime(0.25, now + t + 0.02);
      gain.gain.setValueAtTime(0.25, now + t + d - 0.05);
      gain.gain.exponentialRampToValueAtTime(0.001, now + t + d);
      osc.connect(gain).connect(audioCtx.destination);
      osc.start(now + t);
      osc.stop(now + t + d);
    });
  } catch (e) { /* audio unavailable */ }
}

// Count orders that are "new" AND not yet acknowledged by this
// browser. This drives whether the alarm loop runs. Uses the
// composite ack key so a freshly-created order with a recycled
// id (e.g. after AUTO_INCREMENT reset) is correctly treated as
// unacked.
function countUnackedNewOrders(orders) {
  return (orders || []).filter(o =>
    o.status === 'new' && !ackedIds.has(ackKey(o))
  ).length;
}

// Start or stop the alarm loop based on whether any new orders
// are still unacknowledged. Also updates the banner visual.
function updateAlarmState() {
  const n = countUnackedNewOrders(lastOrders);
  const banner = document.getElementById('alarmBanner');

  if (n > 0) {
    banner.classList.add('show');
    document.getElementById('alarmCount').textContent =
      n + ' new order' + (n > 1 ? 's' : '') + ' waiting';
    if (!alarmTimer) {
      playAlertBurst();  // fire immediately on start
      alarmTimer = setInterval(playAlertBurst, 2500);
    }
  } else {
    banner.classList.remove('show');
    if (alarmTimer) { clearInterval(alarmTimer); alarmTimer = null; }
  }
}

// "Accept all" — acknowledges every unacked new order at once.
// Useful during a rush when multiple orders stack up.
function acceptAllNew() {
  (lastOrders || [])
    .filter(o => o.status === 'new' && !ackedIds.has(ackKey(o)))
    .forEach(o => ackedIds.add(ackKey(o)));
  saveAcked(ackedIds);
  updateAlarmState();
  loadOrders();
}

let lastOrders = [];  // most recent order list for alarm recalc
let lastOrdersById = {};  // per-render order index keyed by id (used by sendReadyWhatsApp)

// ─────────────────────────────────────────────────────────────
// DESKTOP NOTIFICATIONS
// ─────────────────────────────────────────────────────────────
// Browsers throttle background tabs aggressively (setInterval slows
// to ~1 minute, audio is suppressed). The Notification API is the
// reliable way to alert staff when the dashboard tab isn't focused —
// notifications work even when the browser is minimized, and they
// can ring an OS-level chime that bypasses tab throttling.
//
// Flow:
//   1. User checks "🖥️ Desktop alerts" → we show an explanatory
//      banner with an explicit Enable button. Clicking it fires
//      Notification.requestPermission() as a deliberate user
//      gesture (browsers reject programmatic prompts).
//   2. Permission preference cached in localStorage so we don't
//      re-prompt on every load.
//   3. When a new "new" order arrives, fire a Notification with
//      title + body + tag (the tag prevents stacking duplicates
//      for the same order).
//   4. Clicking the notification focuses this tab and dismisses
//      the alarm.

const NOTIFY_PREF_KEY = 'qrmenu_notify_enabled';

function notifySupported() {
  return 'Notification' in window;
}

// Sync the checkbox state with the stored preference + browser
// permission. Called on page load and after permission changes.
function refreshNotifyUI() {
  const cb = document.getElementById('notifyOn');
  if (!notifySupported()) {
    cb.disabled = true;
    cb.parentElement.style.opacity = 0.4;
    cb.parentElement.title = 'Your browser does not support desktop notifications';
    return;
  }
  // The checkbox reflects whether desktop alerts will fire — true
  // only if both (a) user has opted in via this checkbox AND
  // (b) browser permission has been granted.
  const wantsAlerts = localStorage.getItem(NOTIFY_PREF_KEY) === '1';
  const granted     = Notification.permission === 'granted';
  cb.checked = wantsAlerts && granted;
}

// Toggle handler — when user enables the checkbox, show the
// explanatory banner if permission isn't granted yet.
function onNotifyToggle() {
  const cb = document.getElementById('notifyOn');
  if (!cb.checked) {
    // User unchecked — record preference, hide banner.
    localStorage.setItem(NOTIFY_PREF_KEY, '0');
    document.getElementById('notifyBanner').style.display = 'none';
    return;
  }
  // User wants alerts. Three cases for permission state:
  if (Notification.permission === 'granted') {
    localStorage.setItem(NOTIFY_PREF_KEY, '1');
    toast('Desktop alerts enabled');
  } else if (Notification.permission === 'denied') {
    cb.checked = false;
    toast('Notifications are blocked. Enable them in browser settings to use this.', false);
  } else {
    // 'default' — never asked. Show banner so user explicitly
    // clicks Enable, which is the user gesture browsers require.
    document.getElementById('notifyBanner').style.display = '';
  }
}

// Triggered by the banner's Enable button — explicit user gesture.
async function requestNotifyPermission() {
  try {
    const result = await Notification.requestPermission();
    document.getElementById('notifyBanner').style.display = 'none';
    if (result === 'granted') {
      localStorage.setItem(NOTIFY_PREF_KEY, '1');
      // Send a confirmation notification so the user sees the
      // chime/popup once and knows it's working.
      new Notification('La Tarte Tropézienne', {
        body: 'Desktop alerts enabled. New orders will notify you here.',
        icon: '/admin/icon.png',  // optional; falls back to favicon
        tag:  'qrmenu-test',
      });
      toast('Desktop alerts enabled');
    } else {
      document.getElementById('notifyOn').checked = false;
      localStorage.setItem(NOTIFY_PREF_KEY, '0');
      toast('Permission not granted', false);
    }
    refreshNotifyUI();
  } catch (e) {
    console.error('Notification permission error:', e);
    toast('Could not enable notifications', false);
  }
}

function dismissNotifyBanner() {
  document.getElementById('notifyBanner').style.display = 'none';
  document.getElementById('notifyOn').checked = false;
  localStorage.setItem(NOTIFY_PREF_KEY, '0');
}

// Fire a desktop notification for newly arrived orders. Called
// from loadOrders() when newUnackedArrived > 0.
//
// We use one notification per batch (not per order) so a flurry
// of orders doesn't spam 10 popups. The tag includes the order
// IDs so identical batches replace rather than stack.
function notifyNewOrders(newOrders) {
  if (!notifySupported()) return;
  if (Notification.permission !== 'granted') return;
  if (localStorage.getItem(NOTIFY_PREF_KEY) !== '1') return;
  if (newOrders.length === 0) return;

  const count = newOrders.length;
  const title = (count === 1)
    ? 'New order: ' + newOrders[0].order_number
    : count + ' new orders';
  const body = (count === 1)
    ? 'From ' + (newOrders[0].customer_name || 'customer') +
      ' — ' + newOrders[0].total + ' ' + (newOrders[0].currency || 'KD')
    : 'Tap to view in dashboard';

  try {
    const n = new Notification(title, {
      body: body,
      icon: '/admin/icon.png',  // optional
      tag:  'qrmenu-orders',     // replaces previous unread notif
      requireInteraction: true,  // stays until user clicks/dismisses
    });
    // Click → focus this tab + dashboard. Works even when browser
    // is minimized (most OSes will raise the window).
    n.onclick = function () {
      window.focus();
      this.close();
    };
  } catch (e) {
    // Some Chromium-on-Windows versions throw if the icon URL is
    // unreachable. Retry without the icon.
    try {
      new Notification(title, { body: body, tag: 'qrmenu-orders', requireInteraction: true });
    } catch (e2) {
      console.error('Notification failed:', e2);
    }
  }
}

// ---------- Fetch orders ----------
async function loadOrders() {
  try {
    // Fetch logic:
    //   - "active" → request all, filter client-side to exclude pending/abandoned
    //   - specific status → server-side filter
    const serverFilter = (currentFilter === 'active') ? '' : currentFilter;
    const url = 'api.php?action=list_orders' +
      (serverFilter ? '&status=' + serverFilter : '&limit=100');
    const res = await fetch(url, { credentials: 'same-origin' });
    const data = await res.json();
    if (data.error) { toast(data.error, false); return; }

    // For "active" view, hide pending_whatsapp + abandoned
    let orders = data.orders || [];
    if (currentFilter === 'active') {
      orders = orders.filter(o => o.status !== 'pending_whatsapp' && o.status !== 'abandoned');
    }

    // Auto-acknowledge orders older than 12 hours so the alarm
    // doesn't scream when staff opens the dashboard the morning
    // after closing. Fresh NEW orders will still trigger the
    // alarm; ancient un-handled ones are treated as "someone
    // already dealt with them, we've just never clicked Accept."
    orders.forEach(o => {
      const age = Date.now() - new Date((o.created_at || '').replace(' ', 'T') + 'Z').getTime();
      if (age > 12 * 3600 * 1000) ackedIds.add(ackKey(o));
    });
    saveAcked(ackedIds);

    // Keep seenIds in sync for the "new orders appeared" toast.
    // We only toast for "new" status (confirmed) — pending doesn't
    // trigger anything because those orders aren't actionable yet.
    // We also collect the actual order objects so the desktop
    // notification can show order # and customer name.
    let newUnackedArrived = 0;
    const newlyArrivedOrders = [];
    orders.forEach(o => {
      if (!seenIds.has(o.id)) {
        if (!firstLoad && o.status === 'new' && !ackedIds.has(ackKey(o))) {
          newUnackedArrived++;
          newlyArrivedOrders.push(o);
        }
        seenIds.add(o.id);
      }
    });
    if (newUnackedArrived > 0) {
      toast(newUnackedArrived + ' new order' + (newUnackedArrived > 1 ? 's' : '') + '!');
      // Fire a desktop notification too. This is the bit that
      // works even when the tab is in the background.
      notifyNewOrders(newlyArrivedOrders);
    }

    lastOrders = orders;
    renderOrders(orders, data.counts || {});
    updateAlarmState();  // start/stop the looping alert as needed

    document.getElementById('lastRefresh').textContent =
      'Last updated ' + new Date().toLocaleTimeString();
    firstLoad = false;
  } catch (e) {
    toast('Could not load orders', false);
    console.error(e);
  }
}

// ---------- Render ----------
function esc(s) {
  return String(s ?? '').replace(/[&<>"']/g, c =>
    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])
  );
}

function timeAgo(iso) {
  const d = new Date(iso.replace(' ', 'T') + 'Z');
  const mins = Math.floor((Date.now() - d.getTime()) / 60000);
  if (mins < 1) return 'just now';
  if (mins < 60) return mins + 'm ago';
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return hrs + 'h ago';
  return d.toLocaleDateString();
}

// Pretty human labels for the status badge. Maps internal
// codes (what's stored in DB) to what staff reads.
function statusLabel(s) {
  return ({
    pending_whatsapp: 'Pending',
    new:              'New',
    preparing:        'Preparing',
    ready:            'Ready',
    completed:        'Completed',
    voided:           'Voided',
    abandoned:        'Abandoned',
  })[s] || s;
}

// Render the payment-method badge for an order card. The label
// emphasizes what staff needs to know at a glance:
//   - "KNET • Paid" / "Card • Paid" → no payment action needed
//   - "Cash on delivery"             → collect cash on handover
//   - "Pay at counter"               → verify payment before handover
//   - "Pay pending"                  → customer is mid-checkout (rare in dashboard)
//   - "Payment failed"               → don't prepare; payment didn't go through
//   - "WhatsApp"                     → legacy order, no payment data
//
// Returns empty string for orders with no payment_method (older
// orders predating the migration) so we don't show a noisy badge.
function paymentBadgeHtml(o) {
  const m = o.payment_method   || '';
  const s = o.payment_status   || '';

  // Online card payments — outcome depends on status.
  if (m === 'knet_card') {
    if (s === 'paid')    return '<div class="payment-badge p-paid">💳 Card • Paid</div>';
    if (s === 'failed')  return '<div class="payment-badge p-failed">💳 Payment failed</div>';
    if (s === 'pending') return '<div class="payment-badge p-pending">💳 Awaiting payment</div>';
    return '<div class="payment-badge p-pending">💳 Card</div>';
  }
  if (m === 'cash')    return '<div class="payment-badge p-cash">💵 Cash on delivery</div>';
  if (m === 'counter') return '<div class="payment-badge p-counter">🏪 Pay at counter</div>';

  // No payment_method = legacy WhatsApp flow (or pre-migration order)
  return '<div class="payment-badge p-whatsapp">📱 WhatsApp</div>';
}

function renderOrders(orders, counts) {
  // Reset the per-render order index used by sendReadyWhatsApp.
  // Without this, stale orders from prior renders would linger
  // and the click handler could open WhatsApp with outdated data.
  lastOrdersById = {};

  // Update filter counters. "Active" is everything except pending + abandoned.
  const activeCount = (counts.new || 0) + (counts.preparing || 0) +
                      (counts.ready || 0) + (counts.completed || 0);
  document.getElementById('c-active').textContent     = activeCount;
  document.getElementById('c-new').textContent        = counts.new        || 0;
  document.getElementById('c-preparing').textContent  = counts.preparing  || 0;
  document.getElementById('c-ready').textContent      = counts.ready      || 0;
  document.getElementById('c-completed').textContent  = counts.completed  || 0;
  document.getElementById('c-pending').textContent    = counts.pending_whatsapp || 0;

  const grid = document.getElementById('ordersGrid');
  if (orders.length === 0) {
    let msg;
    if (currentFilter === 'pending_whatsapp') {
      msg = '<h3>No pending orders</h3><p>Orders appear here when customers place them but haven\'t been confirmed on WhatsApp yet.</p>';
    } else if (currentFilter === 'active') {
      msg = '<h3>No active orders</h3><p>Confirmed orders will appear here once staff accepts them from the Pending tab.</p>';
    } else {
      msg = '<h3>No orders with status <strong>' + currentFilter + '</strong></h3>';
    }
    grid.innerHTML = `<div class="empty-state">${msg}</div>`;
    return;
  }

  grid.className = 'orders-grid';
  grid.innerHTML = orders.map(o => {
    const items = JSON.parse(o.items_json || '[]');
    const itemsHtml = items.map(it => {
      const name = esc(it.name);
      const size = it.size_label ? ' (' + esc(it.size_label) + ')' : '';
      return `<div class="it-row">
        <span><span class="it-qty">${it.quantity}×</span>${name}${size}</span>
        <span>${parseFloat(it.subtotal).toFixed(3)}</span>
      </div>`;
    }).join('');
    const phoneDigits = (o.customer_phone || '').replace(/[^\d]/g, '');
    const waLink = phoneDigits ? `<a href="https://wa.me/${phoneDigits}" target="_blank">${esc(o.customer_phone)}</a>` : esc(o.customer_phone);
    const notesHtml = o.customer_notes ? `<div class="customer-notes"><strong>Note:</strong> ${esc(o.customer_notes)}</div>` : '';
    const actions = renderActions(o);

    // Branch / fulfilment-mode / table line. Only rendered when the
    // order actually carries these (multi-branch orders). Older or
    // single-location orders simply omit the line.
    const modeLabels = { delivery: 'Delivery', pickup: 'Pickup', dinein: 'Dine-in' };
    const fulfilBits = [];
    if (o.branch_name)  fulfilBits.push(`📍 ${esc(o.branch_name)}`);
    if (o.order_mode && modeLabels[o.order_mode]) {
      fulfilBits.push(modeLabels[o.order_mode]);
    }
    if (o.order_table_number) {
      fulfilBits.push(`Table ${esc(o.order_table_number)}`);
    }
    const fulfilHtml = fulfilBits.length
      ? `<div class="order-fulfil">${fulfilBits.join(' &nbsp;·&nbsp; ')}</div>`
      : '';

    return `<div class="order-card is-${esc(o.status)}">
      <div class="card-head">
        <div>
          <div class="order-num">${esc(o.order_number)}</div>
          <div class="order-time">${timeAgo(o.created_at)}</div>
        </div>
        <div style="text-align:right">
          <div class="order-status-badge s-${esc(o.status)}">${esc(statusLabel(o.status))}</div>
          ${paymentBadgeHtml(o)}
        </div>
      </div>
      <div class="card-body">
        <div class="customer-row">
          <span class="customer-name">${esc(o.customer_name)}</span>
          <span class="customer-phone">${waLink}</span>
        </div>
        ${fulfilHtml}
        ${notesHtml}
        <div class="items-list">${itemsHtml}</div>
        <div class="it-total">
          <span>Total</span>
          <span>${esc(o.currency || 'KD')} ${parseFloat(o.total).toFixed(3)}</span>
        </div>
      </div>
      <div class="card-actions">${actions}</div>
    </div>`;
  }).join('');
}

// Per-status action buttons. Each order shows only what makes
// sense for its current state — no need to "reopen" a completed
// order through the normal flow.
function renderActions(o) {
  const printBtn = `<button class="act-btn act-btn-print" onclick="printReceipt(${o.id})">🖨 Print</button>`;

  // Pending orders — customer placed, but staff hasn't yet
  // confirmed the WhatsApp message arrived. Only shown on the
  // Pending tab. Confirm promotes to "new" AND triggers the
  // confirmation email to the restaurant.
  if (o.status === 'pending_whatsapp') {
    return `<button class="act-btn act-btn-confirm" onclick="updateStatus(${o.id},'new')">✓ Confirm from WhatsApp</button>
            <button class="act-btn act-btn-danger" onclick="updateStatus(${o.id},'abandoned')">Mark abandoned</button>`;
  }

  // Show a prominent Accept button only on NEW orders that
  // haven't been acknowledged in this browser. Once clicked,
  // the looping alarm stops (if this was the last unacked).
  const acceptBtn = (o.status === 'new' && !ackedIds.has(ackKey(o)))
    ? `<button class="act-btn act-btn-accept" onclick="ackOrder(${o.id})">✓ Accept</button>`
    : '';
  if (o.status === 'new') {
    return acceptBtn + printBtn +
      `<button class="act-btn act-btn-primary" onclick="updateStatus(${o.id},'preparing')">Start preparing →</button>
       <button class="act-btn act-btn-danger" onclick="updateStatus(${o.id},'voided')">Void</button>`;
  }
  if (o.status === 'preparing') {
    return printBtn +
      `<button class="act-btn act-btn-primary" onclick="updateStatus(${o.id},'ready')">Mark ready →</button>
       <button class="act-btn act-btn-ghost" onclick="updateStatus(${o.id},'new')">← Back</button>`;
  }
  if (o.status === 'ready') {
    // The "Send WhatsApp" button appears here, alongside Complete.
    // Staff's natural flow: kitchen finishes → mark Ready → tap
    // WhatsApp to ping the customer that pickup is available →
    // customer arrives → mark Complete after handover.
    const phoneDigits = (o.customer_phone || '').replace(/[^\d]/g, '');
    const waBtn = phoneDigits
      ? `<button class="act-btn act-btn-whatsapp" onclick="sendReadyWhatsApp(${o.id})">📱 Send WhatsApp</button>`
      : '';
    if (waBtn) {
      // Stash the order so the click handler can retrieve it by id
      // without dealing with HTML-attribute escaping rules. (Names
      // like "O'Brien" would otherwise break onclick='...'.)
      lastOrdersById[o.id] = o;
    }
    return printBtn + waBtn +
      `<button class="act-btn act-btn-primary" onclick="updateStatus(${o.id},'completed')">Complete ✓</button>
       <button class="act-btn act-btn-ghost" onclick="updateStatus(${o.id},'preparing')">← Back</button>`;
  }
  if (o.status === 'completed' || o.status === 'voided' || o.status === 'abandoned') {
    return printBtn;
  }
  return printBtn;
}

// Holds the most recently-rendered order objects keyed by id.
// renderActions populates this when emitting a WhatsApp button so
// the click handler can look up the order without needing a
// fresh API call. Declared at the top of the script; reset each
// renderOrders() call.

// Build the "your order is ready" message in the customer's
// preferred language, then open wa.me with it pre-filled. Staff
// taps Send inside WhatsApp itself — keeps the human in the loop
// (sometimes the customer is already at the counter) and uses
// the same WhatsApp account staff already uses.
function sendReadyWhatsApp(orderId) {
  const order = lastOrdersById[orderId];
  if (!order) {
    toast('Order not found in current view — refresh and try again', false);
    return;
  }
  const phoneDigits = (order.customer_phone || '').replace(/[^\d]/g, '');
  if (!phoneDigits) {
    toast('No phone number on this order', false);
    return;
  }
  const name     = (order.customer_name || '').trim();
  const orderNum = order.order_number || '';
  // Customer's preferred language as captured at order time.
  // Falls back to English when the field is missing or unrecognized.
  const lang = (order.language || '').toLowerCase().startsWith('ar') ? 'ar' : 'en';

  let body;
  if (lang === 'ar') {
    const greeting = name ? `مرحباً ${name}،` : 'مرحباً،';
    body = `${greeting} طلبك رقم ${orderNum} جاهز للاستلام من لا تارت تروبيزيان 🥐\n\nنراك قريباً!`;
  } else {
    const greeting = name ? `Hi ${name},` : 'Hi,';
    body = `${greeting} your order #${orderNum} is ready for pickup at La Tarte Tropézienne 🥐\n\nSee you soon!`;
  }

  // wa.me requires the number with no leading "+" and no spaces.
  // The body is URL-encoded; line breaks survive as %0A.
  const url = `https://wa.me/${phoneDigits}?text=${encodeURIComponent(body)}`;
  window.open(url, '_blank', 'noopener');
}

// ---------- Status updates ----------
async function updateStatus(id, newStatus) {
  try {
    const res = await fetch('api.php?action=update_order_status', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'same-origin',
      body: JSON.stringify({ id, status: newStatus }),
    });
    const data = await res.json();
    if (data.error) { toast(data.error, false); return; }

    // If the transition just fired an email, surface the result
    // so staff knows it worked (or why it didn't).
    if (data.email) {
      if (data.email.sent) {
        toast('Confirmed — email sent to ' + (data.email.to || 'recipient'));
      } else if (data.email.skipped) {
        toast('Confirmed (email: ' + data.email.skipped + ')');
      } else if (data.email.error) {
        toast('Confirmed but email failed: ' + data.email.error, false);
      }
    }
    loadOrders();  // refresh immediately
  } catch (e) {
    toast('Could not update status', false);
  }
}

// ---------- Print ----------
// Opens a receipt view in a new window that auto-prints on load.
function printReceipt(id) {
  const w = window.open('receipt.php?id=' + id, '_blank', 'width=400,height=700');
  if (!w) toast('Please allow pop-ups to print', false);
}

// ---------- Filter pills ----------
document.getElementById('statusFilter').addEventListener('click', e => {
  const pill = e.target.closest('.status-pill');
  if (!pill) return;
  document.querySelectorAll('.status-pill').forEach(p => p.classList.remove('active'));
  pill.classList.add('active');
  currentFilter = pill.dataset.status;
  loadOrders();
});

// ---------- Auto-refresh ----------
function startAutoRefresh() {
  stopAutoRefresh();
  refreshTimer = setInterval(() => {
    if (document.getElementById('autoRefresh').checked) loadOrders();
  }, 5000);
}
function stopAutoRefresh() {
  if (refreshTimer) clearInterval(refreshTimer);
  refreshTimer = null;
}

// Kick off
refreshNotifyUI();
loadOrders();
startAutoRefresh();

// When the tab regains focus, do an immediate refresh so any
// orders that arrived during the background period (and were
// only detected at the throttled interval) are displayed
// promptly. We INTENTIONALLY do not stop auto-refresh when the
// tab is hidden — browsers throttle setInterval to ~1 minute
// in background tabs anyway, but that's enough to detect new
// orders and fire desktop notifications. Stopping it would
// suppress notifications entirely, defeating the purpose.
document.addEventListener('visibilitychange', () => {
  if (!document.hidden) loadOrders();
});

// Wire the "🖥️ Desktop alerts" toggle to its handler.
document.getElementById('notifyOn').addEventListener('change', onNotifyToggle);

// Toggling sound off also silences the looping alarm immediately
// (otherwise it would keep ticking the setInterval pointlessly).
// Toggling it back on re-evaluates the current state.
document.getElementById('soundOn').addEventListener('change', () => {
  if (alarmTimer) { clearInterval(alarmTimer); alarmTimer = null; }
  updateAlarmState();
});
</script>

</body>
</html>
