<?php
// ============================================================
//  MediCore — Sale Entry (POS)
//  File: sale/entry.php
// ============================================================
$page  = 'sale/entry';
$title = 'Sale Entry';
// POS screen — the shared .page-header title/actions bar is skipped
// entirely (same $hidePageHeader convention dashboard.php uses) to
// reclaim vertical space for the cart/payment panels. The keyboard
// hints that used to live in that bar's #ph-actions slot now render
// directly inside the Sale Mode bar below instead (see .pos-modebar).
$hidePageHeader = true;
require_once '../includes/header.php';
$today = date('Y-m-d');

$cfg              = $settings;
$cur              = $cfg['currency'];
$invPrefix        = $cfg['inv_prefix'] ?? 'INV';
$maxRetailDisc    = (float)($cfg['max_retail_discount'] ?? 10);
$roundingEnabled  = ($cfg['rounding_enabled'] ?? '0') === '1';
$roundingMode     = $cfg['rounding_mode'] ?? 'nearest';

// Auto-generate next invoice number
$nextINV = next_ref($invPrefix, 'sales', 'invoice');

// Load customers for dropdown
$customers = db_rows(
    "SELECT id, name, phone, due, advance
       FROM customers
      ORDER BY id = 1 DESC, name ASC"   // Walk-in first
);
?>

<!-- ── POS-page content tightener — reclaims the 18px horizontal
     padding that .content adds, giving the grid full usable width ── -->
<style>
  #content { padding-left:10px; padding-right:10px; }

  /* ============================================================
     Sale Entry — modern POS visual layer (page-scoped).
     Purely additive/decorative: spacing, radius, shadow, gradients,
     typography. Never sets background/color/border-color on any
     element the script block below drives dynamically at runtime
     (#mode-retail, #mode-wholesale, #mode-info, #mode-disc-info,
     #items-mode-badge, #barcode-feedback, #disc-mode-btn, #disc-pct,
     #pay-status-badge, #pay-due, #pay-round-adj, #ft-profit,
     #pay-profit, #opt-cash, #opt-advance, #cus-info, .stock-flash
     rows, etc.) — those keep 100% of their existing inline-driven
     color logic; only their shape/spacing moved into these classes.
  ============================================================ */
  .pos-shell{ display:flex; flex-direction:column; gap:10px; }

  .pos-modebar{
    display:flex;align-items:center;gap:12px;
    background:linear-gradient(135deg,var(--surface) 0%,var(--brand-xlight) 130%);
    border:1px solid var(--border);border-radius:12px;
    padding:9px 16px;box-shadow:0 2px 10px rgba(20,25,70,.06);
  }
  .pos-modebar-label{
    font-size:11.5px;font-weight:800;color:var(--text2);
    text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;
    display:flex;align-items:center;gap:6px;
  }
  .pos-mode-switch{
    display:flex;background:var(--surface2);border:1px solid var(--border);
    border-radius:999px;padding:3px;gap:2px;
  }
  .pos-mode-btn{
    padding:6px 18px !important;font-size:12.5px !important;font-weight:800 !important;
    border:none !important;border-radius:999px !important;cursor:pointer;
    transition:all .18s ease;letter-spacing:.2px;
  }
  .pos-mode-info{
    font-size:11.5px !important;font-weight:700;
    padding:5px 13px !important;border-radius:999px !important;
    display:flex;align-items:center;gap:5px;
    border:1px solid var(--border);
  }
  .pos-modebar-right{ margin-left:auto;display:flex;align-items:center;gap:12px; }
  .pos-kbd-hints{
    font-size:10.5px;color:var(--text3);white-space:nowrap;
    display:flex;align-items:center;gap:5px;
  }
  .pos-kbd-hints kbd{
    font-family:inherit;font-size:10px;font-weight:700;color:var(--text2);
    background:var(--surface);border:1px solid var(--border);border-bottom-width:2px;
    border-radius:5px;padding:1px 5px;
  }

  .pos-grid{ display:grid;grid-template-columns:minmax(0,3fr) minmax(0,1fr);
    gap:12px;align-items:start; }
  .pos-col{ display:flex;flex-direction:column;gap:10px;min-width:0; }

  .pos-card{
    border-radius:12px;
    box-shadow:0 2px 10px rgba(20,25,70,.06) !important;
    transition:box-shadow .15s ease;
  }
  .pos-card:hover{ box-shadow:0 4px 18px rgba(20,25,70,.1) !important; }

  .pos-searchbar{ padding:9px 12px !important; gap:9px !important; }
  .pos-searchbar .pos-scan-ic{
    width:30px;height:30px;border-radius:9px;background:var(--warn-light);
    display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px;
  }
  .pos-input{
    border-radius:9px !important;transition:box-shadow .15s ease,border-color .15s ease;
  }
  .pos-input:focus{ box-shadow:0 0 0 3px var(--brand-xlight); }
  .pos-search-input:focus{ box-shadow:0 0 0 3px var(--brand-xlight); }

  .pos-section-label{
    font-size:11px;font-weight:800;color:var(--text3);text-transform:uppercase;
    letter-spacing:.5px;display:flex;align-items:center;gap:6px;margin:2px 2px 0;
  }
</style>

<div class="pos-shell">

<!-- ── Sale Mode Toggle ──────────────────────────── -->
<div class="pos-modebar">
  <span class="pos-modebar-label">🎛️ Sale Mode</span>
  <div class="pos-mode-switch">
    <button id="mode-retail" onclick="setSaleMode('retail')"
            class="sale-mode-btn active pos-mode-btn"
            style="background:var(--brand);color:#fff">
      🏪 Retail
    </button>
    <button id="mode-wholesale" onclick="setSaleMode('wholesale')"
            class="sale-mode-btn pos-mode-btn"
            style="background:transparent;color:var(--text2)">
      📦 Wholesale
    </button>
  </div>
  <div class="pos-modebar-right">
    <div id="mode-info" class="pos-mode-info" style="background:var(--surface2)">
      Max discount: <strong id="mode-disc-info"
                            style="color:var(--danger)"><?= $maxRetailDisc ?>%</strong>
    </div>
    <span class="pos-kbd-hints">
      <kbd>F2</kbd>Search <kbd>F9</kbd>Save <kbd>F4</kbd>Customer <kbd>Esc</kbd>Clear
    </span>
  </div>
</div>

<!-- ── POS grid ───────────────────────────────────── -->
<div class="pos-grid">

  <!-- ════════════════════════════════════════════════
       LEFT — Product search + Sale items table
  ════════════════════════════════════════════════ -->
  <div class="pos-col">

    <!-- Barcode + Search merged into one compact row -->
    <div class="card pos-card pos-searchbar" style="display:flex;align-items:center">
      <span class="pos-scan-ic" title="Barcode scanner">🔫</span>
      <input id="barcode-input"
             placeholder="Scan barcode…"
             autocomplete="off"
             class="pos-input"
             style="width:160px;padding:6px 10px;font-size:13px;
                    font-family:'JetBrains Mono',monospace;
                    border:2px solid var(--warn);
                    outline:none;background:var(--surface);color:var(--text)">
      <label style="font-size:11.5px;display:flex;align-items:center;gap:3px;
                    color:var(--text2);white-space:nowrap;cursor:pointer">
        <input type="checkbox" id="barcode-auto" checked>
        Auto
      </label>
      <span id="barcode-feedback"
            style="font-size:11.5px;color:var(--text3);white-space:nowrap;
                   min-width:60px"></span>

      <div style="width:1px;align-self:stretch;background:var(--border)"></div>

      <div style="position:relative;flex:1">
        <input id="prod-search"
               placeholder="🔍  Type medicine name to add…  (F2)"
               autocomplete="off"
               class="pos-input pos-search-input"
               style="width:100%;padding:7px 12px;font-size:13px;
                      border:2px solid var(--brand);
                      font-family:inherit;outline:none;
                      background:var(--surface);color:var(--text);
                      box-sizing:border-box">
        <!-- Suggestions -->
        <div id="sug-box"
             style="display:none;position:absolute;top:100%;left:0;right:0;
                    background:var(--surface);border:1px solid var(--border);
                    border-top:none;border-radius:0 0 10px 10px;
                    box-shadow:0 8px 24px rgba(20,25,70,.14);z-index:300;max-height:300px;
                    overflow-y:auto"></div>
      </div>
    </div>

    <!-- ── Alternative Medicines Panel ─────────────────── -->
    <div id="alt-panel" style="display:none">
      <div style="border:1px solid var(--brand-light);border-radius:12px;
                  background:var(--brand-xlight);overflow:hidden;
                  box-shadow:0 2px 10px rgba(20,25,70,.05)">
        <!-- Header row — slim single line (padding trimmed ~20% shorter) -->
        <div style="display:flex;align-items:center;gap:6px;
                    padding:3px 8px;border-bottom:1px solid var(--brand-light)">
          <span style="font-size:11px;font-weight:700;color:var(--brand);white-space:nowrap">
            🔄 Alternatives —
          </span>
          <span id="alt-generic-name"
                style="font-size:11px;font-weight:600;color:var(--brand)"></span>
          <span id="alt-count-badge"
                style="font-size:9.5px;padding:1px 6px;border-radius:8px;
                       background:var(--brand);color:#fff;font-weight:700"></span>
          <button id="alt-view-all-btn" onclick="openAllAlternatives()"
                  style="display:none;font-size:9.5px;font-weight:700;padding:2px 9px;
                         border-radius:8px;border:1px solid var(--brand);
                         background:var(--surface);color:var(--brand);cursor:pointer;
                         white-space:nowrap;transition:background .12s"
                  onmouseover="this.style.background='var(--brand-xlight)'"
                  onmouseout="this.style.background='var(--surface)'">
            🔍 View All
          </button>
          <button onclick="closeAltPanel()"
                  style="margin-left:auto;background:none;border:none;cursor:pointer;
                         font-size:13px;color:var(--text3);line-height:1;padding:1px 4px">
            ✕
          </button>
        </div>
        <!-- Cards — compact horizontal strip. Fixed-height (52px) cards
             below keep every box uniform; list padding trimmed to match
             the ~20% overall panel-height reduction. -->
        <div id="alt-list"
             style="display:flex;gap:5px;padding:4px 6px;
                    overflow-x:auto;overflow-y:hidden;flex-wrap:nowrap;align-items:center;
                    scroll-behavior:smooth;
                    scrollbar-width:thin;scrollbar-color:var(--brand-light) transparent">
          <div style="color:var(--text3);font-size:12px;padding:4px">Loading…</div>
        </div>
      </div>
    </div>

    <!-- Items table -->
    <div class="card pos-card" style="padding:0;overflow:hidden">
      <div class="ch" style="padding:10px 14px;margin-bottom:0;
                             background:linear-gradient(135deg,var(--surface) 0%,var(--surface2) 100%);
                             border-bottom:1px solid var(--border)">
        <div class="ct">
        🛒 Sale Items
        <span id="items-mode-badge"
              style="font-size:10px;padding:2px 9px;border-radius:999px;
                     margin-left:8px;font-weight:700;
                     background:var(--brand-xlight);color:var(--brand)">
          🏪 RETAIL
        </span>
      </div>
        <div style="display:flex;align-items:center;gap:10px">
          <span id="item-count"
                style="font-size:12px;color:var(--text3);font-weight:600">0 item(s)</span>
          <button class="btn btn-sm btn-danger"
                  id="clear-btn"
                  onclick="clearAll()"
                  style="display:none;border-radius:999px">✕ Clear All</button>
        </div>
      </div>
      <div class="tw" style="overflow-x:auto">
        <table style="min-width:620px;width:100%">
          <thead>
            <tr>
              <th style="width:30px;position:sticky;top:0;background:var(--surface2);z-index:1">#</th>
              <th style="position:sticky;top:0;background:var(--surface2);z-index:1">Product</th>
              <th style="text-align:center;width:80px;position:sticky;top:0;background:var(--surface2);z-index:1">Qty</th>
              <th style="text-align:right;width:90px;position:sticky;top:0;background:var(--surface2);z-index:1">Sale Rate</th>
              <?php if (can_view_profit()): ?>
              <th style="text-align:right;width:64px;position:sticky;top:0;background:var(--surface2);z-index:1"
                  title="Buy % = Discount received from MRP">Buy %</th>
              <?php endif; ?>
              <th style="text-align:right;width:64px;position:sticky;top:0;background:var(--surface2);z-index:1"
                  title="Sale % = Discount given from MRP">Sale %</th>
              <?php if (can_view_profit()): ?>
              <th style="text-align:right;width:70px;position:sticky;top:0;background:var(--surface2);z-index:1"
                  title="Profit % = Net margin retained from MRP">Profit %</th>
              <?php endif; ?>
              <th style="text-align:center;width:70px;position:sticky;top:0;background:var(--surface2);z-index:1">Disc%</th>
              <th style="text-align:right;width:90px;position:sticky;top:0;background:var(--surface2);z-index:1">Total</th>
              <th style="text-align:right;width:80px;position:sticky;top:0;background:var(--surface2);z-index:1">Profit &#2547;</th>
              <th style="width:30px;position:sticky;top:0;background:var(--surface2);z-index:1"></th>
            </tr>
          </thead>
          <tbody id="items-body">
            <tr id="empty-row">
              <td colspan="<?= (can_view_profit()) ? 11 : 9 ?>" class="empty-state">
                Search and add medicines above
              </td>
            </tr>
          </tbody>
          <!-- Subtotal row -->
          <tfoot id="items-foot" style="display:none">
            <tr style="background:var(--surface2)">
              <td colspan="4"
                  style="text-align:right;padding:5px 10px;
                         font-size:10.5px;color:var(--text3);font-weight:600">
                Avg. Margin
              </td>
              <?php if (can_view_profit()): ?>
              <td class="mono" id="ft-avg-buy"
                  style="text-align:right;padding:5px 10px;font-size:11px;color:var(--text2)">0.00%</td>
              <?php endif; ?>
              <td class="mono" id="ft-avg-sale"
                  style="text-align:right;padding:5px 10px;font-size:11px;color:var(--text2)">0.00%</td>
              <?php if (can_view_profit()): ?>
              <td class="mono" id="ft-avg-profit"
                  style="text-align:right;padding:5px 10px;font-size:11px;color:var(--text2)">0.00%</td>
              <?php endif; ?>
              <td colspan="4"></td>
            </tr>
            <tr style="background:var(--surface2);font-weight:700">
              <td colspan="<?= (can_view_profit()) ? 8 : 6 ?>"
                  style="text-align:right;padding:8px 10px;
                         font-size:13px;color:var(--text2)">
                Sub Total
              </td>
              <td class="mono"
                  style="text-align:right;padding:8px 10px;
                         color:var(--brand)" id="ft-subtotal">
                <?= $cur ?>0.00
              </td>
              <td class="mono"
                  style="text-align:right;padding:8px 10px;
                         color:var(--accent)" id="ft-profit">
                <?= $cur ?>0.00
              </td>
              <td></td>
            </tr>
          </tfoot>
        </table>
      </div>
    </div>

  </div><!-- /left -->

  <!-- ════════════════════════════════════════════════
       RIGHT — Customer + Discount + Payment
  ════════════════════════════════════════════════ -->
  <div class="pos-col" style="position:sticky;top:12px">

    <!-- Customer + Invoice combined -->
    <div class="card pos-card" style="padding:11px 13px">
      <div style="display:grid;grid-template-columns:1fr 1fr 80px;gap:8px;align-items:end">
        <div class="fg" style="gap:2px;position:relative">
          <label>👤 Customer</label>
          <input id="cus-name" autocomplete="off"
                 placeholder="Name / Phone / ID…"
                 oninput="onCustomerNameInput()"
                 onfocus="this.select()"
                 class="pos-input">
          <!-- AJAX suggestion dropdown -->
          <div id="cus-sug-box"
               style="display:none;position:absolute;top:100%;left:0;right:0;
                      background:var(--surface);border:1px solid var(--border);
                      border-top:none;border-radius:0 0 10px 10px;
                      box-shadow:0 8px 24px rgba(20,25,70,.14);z-index:400;max-height:200px;
                      overflow-y:auto"></div>
        </div>
        <div class="fg" style="gap:2px">
          <label>Invoice #</label>
          <input id="inv-no" value="<?= htmlspecialchars($nextINV) ?>"
                 style="font-family:'JetBrains Mono',monospace;font-weight:700;
                        background:var(--surface2);min-width:0;width:100%">
        </div>
        <div class="fg" style="gap:2px">
          <label>Date</label>
          <input id="inv-date" type="date" value="<?= $today ?>">
        </div>
      </div>

      <!-- Hidden selects/data carried for compatibility with rest of app -->
      <select id="customer-id" style="display:none">
        <?php foreach ($customers as $c): ?>
        <option value="<?= $c['id'] ?>"
                data-name="<?= htmlspecialchars($c['name']) ?>"
                data-phone="<?= htmlspecialchars($c['phone']) ?>"
                data-due="<?= $c['due'] ?>"
                data-advance="<?= $c['advance'] ?>"
                <?= $c['id'] == 1 ? 'selected' : '' ?>>
          <?= htmlspecialchars($c['name']) ?>
        </option>
        <?php endforeach; ?>
      </select>

      <!-- Due Sale Validation warning — shown when due > 0 but customer is Walk-in -->
      <div id="due-customer-warning"
           style="display:none;margin-top:6px;padding:7px 10px;
                  background:#fde8e8;border:1px solid #f5a0a0;
                  border-radius:10px;font-size:12px;
                  color:var(--danger);font-weight:600">
        ⚠ Due sales require a registered customer. Please select or add a customer above.
      </div>

      <!-- Customer info strip (hidden for walk-in) -->
      <div id="cus-info" style="display:none;font-size:12px;
           border-radius:10px;padding:7px 10px;margin-top:6px">
        <div id="cus-phone" style="color:var(--text2);margin-bottom:3px"></div>
        <div style="display:flex;gap:10px">
          <span>Previous Due:
            <strong id="cus-due" style="color:var(--danger)"></strong>
          </span>
          <span>Advance:
            <strong id="cus-adv" style="color:var(--accent)"></strong>
          </span>
        </div>
        <!-- Total Outstanding — previous due + this sale's new due, live -->
        <div id="cus-outstanding-row"
             style="display:none;margin-top:5px;padding-top:5px;
                    border-top:1px dashed var(--border);
                    justify-content:space-between;font-weight:700">
          <span style="color:var(--text2);font-weight:600">Total Outstanding</span>
          <strong id="cus-outstanding" style="color:var(--danger)"></strong>
        </div>
      </div>

      <!-- New Customer panel (shown when name has no match) -->
      <div id="new-cus-panel" style="display:none;padding:8px 10px;margin-top:6px;
           background:var(--brand-xlight);border:1px solid var(--brand-light);
           border-radius:10px">
        <div style="font-size:12px;font-weight:700;color:var(--brand);margin-bottom:6px">
          ➕ New Customer — will be created on save
        </div>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
          <div class="fg" style="gap:2px">
            <label style="font-size:10.5px">Mobile Number</label>
            <input id="new-cus-phone" placeholder="01XXXXXXXXX"
                   style="font-size:13px">
          </div>
          <div class="fg" style="gap:2px">
            <label style="font-size:10.5px">Address</label>
            <input id="new-cus-address" placeholder="Optional address"
                   style="font-size:13px">
          </div>
        </div>
      </div>
    </div>

    <!-- Payment panel -->
    <div class="card pos-pay-card"
         style="background:var(--brand-xlight);border:1px solid var(--brand-light);
                padding:14px 13px 12px">
      <div class="ch" style="margin-bottom:8px">
        <div class="ct" style="color:var(--brand);font-size:15.5px">💳 Payment</div>
      </div>
      <div style="display:flex;flex-direction:column;gap:6px;font-size:13px">

        <div style="display:flex;justify-content:space-between;
                    padding:5px 0;border-bottom:1px solid var(--brand-light)">
          <span style="color:var(--text2)">Sub Total</span>
          <span class="mono" id="pay-sub" style="font-weight:600">
            <?= $cur ?>0.00
          </span>
        </div>

        <!-- Discount row -->
        <div style="display:flex;align-items:center;gap:6px">
          <span style="color:var(--text2);flex:1">Discount</span>
          <!-- Mode toggle: % or flat amount -->
          <button id="disc-mode-btn" onclick="setDiscMode(discMode==='pct'?'flat':'pct')"
                  style="padding:2px 9px;font-size:11px;font-weight:700;border-radius:8px;
                         border:1px solid var(--brand-light);cursor:pointer;
                         background:var(--brand);color:#fff;flex-shrink:0">
            %
          </button>
          <input id="disc-pct" type="number" step="0.01" min="0"
                 placeholder="0" oninput="recalc()"
                 style="width:65px;text-align:center;padding:3px 6px">
          <span id="disc-suffix" style="color:var(--text3);font-size:11px;
                                        min-width:12px">%</span>
          <span class="mono" id="pay-disc"
                style="font-weight:600;color:var(--danger);text-align:right;
                       margin-left:auto">
            - <?= $cur ?>0.00
          </span>
        </div>
        <div id="disc-pct-hint" class="input-hint" style="display:none;margin-top:-3px"></div>

        <!-- Net Total (pre-rounding) -->
        <div id="pay-net-total-row" style="display:none;justify-content:space-between;
                    padding:3px 0;font-size:12px;color:var(--text3)">
          <span>Net Total</span>
          <span class="mono" id="pay-net-total"><?= $cur ?>0.00</span>
        </div>

        <!-- Round Adjustment (shown only when rounding is enabled & non-zero) -->
        <div id="pay-round-row" style="display:none;justify-content:space-between;
                    padding:3px 0;font-size:12px;color:var(--text3)">
          <span>Round Adjustment</span>
          <span class="mono" id="pay-round-adj"><?= $cur ?>0.00</span>
        </div>

        <div style="display:flex;justify-content:space-between;align-items:center;
                    padding:9px 10px;margin:2px 0;border-radius:10px;
                    background:linear-gradient(135deg,#fff 0%,var(--brand-xlight) 100%);
                    border:1px solid var(--brand-light)">
          <span style="font-weight:700;color:var(--brand)" id="pay-total-label">Total Amount</span>
          <span class="mono" id="pay-total"
                style="font-weight:800;font-size:19px;color:var(--brand)">
            <?= $cur ?>0.00
          </span>
        </div>

        <!-- Paid input -->
        <div class="fg" style="gap:2px">
          <label>Paid Amount (<?= $cur ?>)</label>
          <input id="pay-paid" type="number" step="0.01" min="0"
                 placeholder="0.00" oninput="recalc()"
                 class="pos-input"
                 style="font-size:15px;font-weight:700;text-align:right;padding:6px 10px">
        </div>

        <!-- Payment Status badge — auto-updates from recalc() — plus a
             quick "Paid" button right beside it (kept as a sibling, not a
             child: recalc() overwrites the badge's textContent on every
             call, which would otherwise wipe out a button placed inside). -->
        <div style="display:flex;align-items:center;gap:6px">
          <div id="pay-status-badge"
               style="flex:1;display:flex;align-items:center;justify-content:center;gap:6px;
                      padding:6px 10px;border-radius:999px;font-weight:700;
                      font-size:12.5px;background:var(--surface2);
                      border:1px solid var(--border);color:var(--text3)">
            ● Awaiting items
          </div>
          <button type="button" onclick="setFullyPaid()"
                  title="Fill Paid Amount with the full payable total"
                  style="padding:6px 10px;font-size:11px;font-weight:700;
                         border-radius:999px;border:1px solid var(--accent);
                         background:var(--accent-xlight);color:var(--accent);
                         cursor:pointer;white-space:nowrap;flex-shrink:0">
            ✅ Paid
          </button>
        </div>

        <!-- Advance adjustment (shown only when customer has advance) -->
        <div id="advance-row" style="display:none;
             background:var(--accent-xlight);border:1px solid #86efac;
             border-radius:10px;padding:8px 10px;margin-top:4px">
          <div style="font-size:11px;font-weight:700;color:var(--accent);
                      margin-bottom:6px">
            💰 Apply Customer Advance
            <span style="font-weight:400;color:var(--text2)">
              (Available: <strong id="adv-available" style="color:var(--accent)">৳0.00</strong>)
            </span>
          </div>
          <div style="display:flex;gap:6px;align-items:center">
            <input id="adv-apply" type="number" step="0.01" min="0"
                   oninput="recalc()"
                   placeholder="0.00"
                   style="flex:1;font-size:12px;font-weight:600">
            <button class="btn btn-sm btn-accent"
                    onclick="applyFullAdvance()"
                    style="white-space:nowrap;font-size:11px">
              Apply All
            </button>
          </div>
          <div style="font-size:10px;color:var(--text3);margin-top:3px">
            Advance amount reduces the cash needed from customer
          </div>
        </div>

        <!-- Payment type -->
        <div class="fg" style="gap:2px">
          <label>Payment Type</label>
          <div style="display:grid;grid-template-columns:1fr 1fr;gap:5px"
               id="pay-type-group">
            <?php foreach (['Cash','Card','Bank','MFS'] as $pt): ?>
            <label style="display:flex;align-items:center;gap:5px;
                          padding:5px 8px;border:1px solid var(--border);
                          cursor:pointer;
                          font-size:12.5px;background:var(--surface)"
                   id="pt-lbl-<?= $pt ?>">
              <input type="radio" name="pay_type"
                     value="<?= $pt ?>"
                     onchange="highlightPayType()"
                     <?= $pt === 'Cash' ? 'checked' : '' ?>>
              <?= $pt === 'Cash' ? '💵' : ($pt === 'Card' ? '💳' : ($pt === 'Bank' ? '🏦' : '📱')) ?>
              <?= $pt ?>
            </label>
            <?php endforeach; ?>
          </div>
        </div>

        <!-- Due row -->
        <div style="display:flex;justify-content:space-between;align-items:center;
                    padding:7px 10px;border-radius:10px;
                    background:var(--surface);border:1px solid var(--border)">
          <span style="color:var(--text2);font-size:13px">Due / Balance</span>
          <span class="mono" id="pay-due"
                style="font-weight:700;font-size:14.5px;color:var(--danger)">
            <?= $cur ?>0.00
          </span>
        </div>

        <!-- Over-payment panel (shown only when Paid > Total) -->
        <div id="change-row"
             style="display:none;flex-direction:column;gap:6px;
                    padding:8px 10px;border-radius:10px;
                    background:#e0f2ff;border:1px solid #7dd3fc">
          <!-- Amount row -->
          <div style="display:flex;justify-content:space-between;align-items:center">
            <span style="color:#0369a1;font-size:13px;font-weight:600">
              💰 Over-Payment
            </span>
            <span class="mono" id="pay-change"
                  style="font-weight:800;font-size:15px;color:#0369a1">
              <?= $cur ?>0.00
            </span>
          </div>
          <!-- Mode toggle — only visible for named customers -->
          <div id="overpay-opts"
               style="display:none;gap:5px">
            <button id="opt-cash"
                    onclick="setOverpayMode('cash')"
                    style="flex:1;padding:5px 8px;font-size:12px;font-weight:700;
                           border-radius:8px;border:1px solid #7dd3fc;
                           cursor:pointer;background:#0369a1;color:#fff;
                           transition:all .15s">
              💵 Return Cash
            </button>
            <button id="opt-advance"
                    onclick="setOverpayMode('advance')"
                    style="flex:1;padding:5px 8px;font-size:12px;font-weight:700;
                           border-radius:8px;border:1px solid #7dd3fc;
                           cursor:pointer;background:rgba(3,105,161,.1);color:#0369a1;
                           transition:all .15s">
              🏦 Save as Advance
            </button>
          </div>
          <!-- Contextual hint -->
          <div id="overpay-hint"
               style="font-size:11.5px;color:#0369a1;margin-top:-2px">
            💵 Change will be returned to customer in cash.
          </div>
        </div>

        <!-- Approx profit -->
        <div style="display:flex;justify-content:space-between;font-size:12.5px">
          <span style="color:var(--text3)">Approx. Profit</span>
          <span class="mono" id="pay-profit"
                style="font-weight:600;color:var(--accent)">
            <?= $cur ?>0.00
          </span>
        </div>

      </div>
    </div><!-- /payment card -->

    <!-- Notes -->
    <div class="fg" style="margin:0;gap:2px">
      <textarea id="inv-notes" rows="1" placeholder="Notes (optional)…"
        class="pos-input"
        style="width:100%;padding:7px 10px;border:1px solid var(--border);
               font-family:inherit;font-size:13px;
               resize:vertical;outline:none;background:var(--surface);
               color:var(--text);box-sizing:border-box"></textarea>
    </div>

    <!-- Hold + Save buttons -->
    <div style="display:flex;gap:6px">
      <button onclick="holdSale()"
              class="btn pos-btn-hold"
              style="flex:1;padding:9px;font-size:13px;font-weight:700;
                     background:#fbbf24;color:#78350f;border-color:#f59e0b"
              title="Hold Sale (Ctrl+H)">
        🔒 Hold
      </button>
      <button onclick="showSaleHolds()"
              class="btn pos-btn-resume"
              style="flex:1;padding:9px;font-size:13px;font-weight:700;
                     background:var(--surface2);color:var(--text2)"
              title="Resume held sale (Ctrl+R)">
        📋 Resume
      </button>
    </div>

    <!-- Save button -->
    <button id="save-btn" onclick="saveSale()"
            class="btn btn-primary pos-btn-save"
            style="width:100%;padding:11px;font-size:15px;
                   font-weight:800;letter-spacing:.3px">
      💾 Create Sale &nbsp; (F9)
    </button>

    <div id="save-err"
         style="display:none;background:#fde8e8;border:1px solid #f5a0a0;
                color:var(--danger);padding:8px 12px;border-radius:10px;
                font-size:13px;line-height:1.5"></div>

    <!-- Auto-draft recovery banner (hidden by default) -->
    <div id="draft-banner"
         style="display:none;background:#fff3dc;border:1px solid #f59e0b;
                border-radius:10px;padding:8px 12px;font-size:12.5px;
                color:#78350f">
      ⚡ Unsaved draft found.
      <button onclick="restoreDraft()"
              style="margin-left:6px;font-weight:700;color:var(--brand);
                     background:none;border:none;cursor:pointer">
        Continue
      </button>
      <button onclick="discardDraft()"
              style="margin-left:4px;color:var(--text3);
                     background:none;border:none;cursor:pointer">
        Discard
      </button>
    </div>

    <!-- Online-order pre-fill banner (hidden by default) — shown when
         routed here from sale/online-orders.php after Approve ── -->
    <div id="online-order-banner"
         style="display:none;background:#e0f2f1;border:1px solid #26a69a;
                border-radius:10px;padding:8px 12px;font-size:12.5px;
                color:#00695c">
      📦 Items pre-filled from online order <strong id="oo-banner-ref"></strong> —
      adjust quantities/discounts as needed, then save to complete the sale.
      <button onclick="document.getElementById('online-order-banner').style.display='none'"
              style="margin-left:6px;color:var(--text3);background:none;border:none;cursor:pointer">
        Dismiss
      </button>
    </div>

  </div><!-- /right -->
</div><!-- /grid -->

</div><!-- /pos-shell -->

<!-- ── Suggestion styles ───────────────────────────── -->
<style>
.sug-row {
  display:grid;
  grid-template-columns:1fr auto auto auto;
  gap:8px;align-items:center;
  padding:9px 12px;font-size:12px;cursor:pointer;
  border-bottom:1px solid var(--border);transition:background .1s;
}
.sug-row:last-child  { border-bottom:none; }
.sug-row:hover       { background:var(--brand-xlight); }
.sug-row.sug-out     { opacity:.45;cursor:not-allowed; }
/* Out of stock but negative-sale IS allowed: still visually faded so
   staff can spot it at a glance, but stays fully clickable/keyboard-
   reachable — unlike .sug-out, which is a genuine dead-end. */
.sug-row.sug-faded   { opacity:.65; }
.sug-row.sug-low     { background:#fffbf0; }
.sug-badge {
  font-size:10px;padding:2px 7px;border-radius:10px;white-space:nowrap;
}
.sug-badge {
  font-size:10px;padding:2px 8px;border-radius:999px;white-space:nowrap;
}
/* active pay type highlight */
.pt-active {
  background:var(--brand-xlight) !important;
  border-color:var(--brand) !important;
  color:var(--brand);font-weight:600;
}
#pt-lbl-Cash, #pt-lbl-Card, #pt-lbl-Bank, #pt-lbl-MFS {
  border-radius:9px !important; transition:all .15s ease;
}
#pt-lbl-Cash:hover, #pt-lbl-Card:hover, #pt-lbl-Bank:hover, #pt-lbl-MFS:hover {
  border-color:var(--brand-light);
}

/* Cart items table — zebra + hover, purely decorative (no color/
   background is ever set inline by the row templates except the
   brief edit-flash, which simply wins for its short duration). */
#items-body tr:not(#empty-row):hover { background:var(--brand-xlight); }
#items-body tr:not(#empty-row):nth-child(even) { background:rgba(20,25,70,.015); }
#items-body td { transition:background .3s ease; }
.tw thead th { font-size:11px !important;letter-spacing:.3px;text-transform:uppercase;
  color:var(--text3) !important;font-weight:800 !important;
  box-shadow:0 1px 0 var(--border); }

/* Payment summary card — receipt-style accent */
.pos-pay-card{
  position:relative;overflow:hidden;border-radius:12px !important;
  box-shadow:0 4px 18px rgba(45,58,140,.09) !important;
}
.pos-pay-card::before{
  content:'';position:absolute;top:0;left:0;right:0;height:4px;
  background:linear-gradient(90deg,var(--brand),var(--brand2),var(--accent));
}

.pos-btn-hold, .pos-btn-resume{
  border-radius:10px !important;box-shadow:0 1px 3px rgba(0,0,0,.06);
  transition:transform .12s ease,box-shadow .12s ease;
}
.pos-btn-hold:hover, .pos-btn-resume:hover{ transform:translateY(-1px); }

.pos-btn-save{
  border-radius:11px !important;
  background:linear-gradient(135deg,var(--brand) 0%,var(--brand2) 100%) !important;
  border:none !important;box-shadow:0 4px 14px rgba(45,58,140,.28);
  transition:transform .12s ease,box-shadow .12s ease;
}
.pos-btn-save:hover{ transform:translateY(-1px); box-shadow:0 6px 18px rgba(45,58,140,.34); }
.pos-btn-save:active{ transform:translateY(0); }
</style>

<script>
// ── State ─────────────────────────────────────────────────────
let items    = [];
let srchTmr  = null;
let _onlineOrderId = null; // set by loadFromOnlineOrder(); used to link the saved sale back to its order
let SALE_MODE   = 'retail';    // 'retail' | 'wholesale'
let overpayMode = 'cash';      // 'cash' | 'advance'
let discMode    = 'pct';       // 'pct' | 'flat'
const CUR     = '<?= addslashes($cur) ?>';
const MAX_RETAIL_DISC = <?= $maxRetailDisc ?>;
const ROUNDING_ENABLED = <?= $roundingEnabled ? 'true' : 'false' ?>;
const ROUNDING_MODE    = '<?= $roundingMode ?>';
// Settings → "Allow negative stock sale". When ON, out-of-stock products
// must remain selectable in the search suggestions and Alternatives
// panel — otherwise they can never reach the cart at all, and the
// setting has no effect even though api/sales.php already honors it.
const ALLOW_NEG_STOCK = <?= (($cfg['allow_negative_stock'] ?? '0') === '1') ? 'true' : 'false' ?>;
// Margin columns (Buy % / Profit %) reveal purchase cost — gated by the
// "Profit Visibility" permission (User Management → Permissions). Sale %
// stays visible to everyone since it's a customer-facing discount
// figure, not a cost one.
const SHOW_MARGIN = <?= (can_view_profit()) ? 'true' : 'false' ?>;

function applyRounding(amount) {
  if (!ROUNDING_ENABLED) return amount;
  switch (ROUNDING_MODE) {
    case 'up':   return Math.ceil(amount);
    case 'down': return Math.floor(amount);
    default:     return Math.round(amount);
  }
}

// ── Core element refs (must exist before barcode scanner / search code below) ──
const searchEl = document.getElementById('prod-search');

// ── Sale mode toggle ──────────────────────────────────────────
function setSaleMode(mode) {
  SALE_MODE = mode;
  const isWholesale = mode === 'wholesale';

  // Update toggle buttons
  document.getElementById('mode-retail').style.background
    = !isWholesale ? 'var(--brand)' : 'transparent';
  document.getElementById('mode-retail').style.color
    = !isWholesale ? '#fff' : 'var(--text2)';
  document.getElementById('mode-wholesale').style.background
    = isWholesale ? 'var(--brand)' : 'transparent';
  document.getElementById('mode-wholesale').style.color
    = isWholesale ? '#fff' : 'var(--text2)';

  // Update info label
  const infoEl = document.getElementById('mode-info');
  const discEl = document.getElementById('mode-disc-info');
  if (isWholesale) {
    infoEl.style.background = 'var(--accent-xlight)';
    discEl.textContent      = 'Product-wise discount';
    discEl.style.color      = 'var(--accent)';
  } else {
    infoEl.style.background = 'var(--surface2)';
    discEl.textContent      = MAX_RETAIL_DISC + '%';
    discEl.style.color      = 'var(--danger)';
  }

  // Re-apply discounts for all current items
  items.forEach((it, i) => {
    if (isWholesale) {
      it.disc_pct = it.wholesale_disc_pct || 0;
    } else {
      // Cap discount at max retail
      it.disc_pct = Math.min(it.disc_pct, MAX_RETAIL_DISC);
    }
  });

  // Update items header badge
  const badge = document.getElementById('items-mode-badge');
  if (badge) {
    badge.textContent = isWholesale ? '📦 WHOLESALE' : '🏪 RETAIL';
    badge.style.background = isWholesale ? 'var(--accent-xlight)' : 'var(--brand-xlight)';
    badge.style.color      = isWholesale ? 'var(--accent)'        : 'var(--brand)';
  }

  renderItems();
  recalc();
}

// ── Barcode scanner ───────────────────────────────────────────
(function() {
  const bEl      = document.getElementById('barcode-input');
  const feedback = document.getElementById('barcode-feedback');
  let bTimer     = null;
  let scanning   = false;

  if (!bEl) return;

  bEl.addEventListener('keydown', async e => {
    if (e.key !== 'Enter') return;
    e.preventDefault();
    const code = bEl.value.trim();
    if (!code) return;

    feedback.textContent = '🔍 Searching…';
    feedback.style.color = 'var(--text3)';

    const r = await apiPost('../api/products.php', {
      action: 'list', q: code
    });

    if (!r.success || !r.data.length) {
      feedback.textContent = '❌ Not found';
      feedback.style.color = 'var(--danger)';
      bEl.select();
      setTimeout(() => { feedback.textContent = ''; }, 2000);
      return;
    }

    // Find exact barcode match first, then fallback to first result
    const exact = r.data.find(p =>
      (p.barcode || '').toLowerCase() === code.toLowerCase()
    );
    const product = exact || r.data[0];

    // Auto-add or show suggestions
    const autoAdd = document.getElementById('barcode-auto').checked;
    if (autoAdd) {
      // Check if already in cart — increment qty instead
      const existing = items.findIndex(it => it.product_id === product.id);
      if (existing >= 0) {
        items[existing].qty++;
        refreshRow(existing);
        recalc();
        feedback.textContent = `✅ +1 ${product.name}`;
      } else {
        addItem(product);
        feedback.textContent = `✅ Added: ${product.name}`;
      }
      feedback.style.color = 'var(--accent)';
    } else {
      // Show in suggestion box instead
      searchEl.value = code;
      fetchSug(code);
      feedback.textContent = '⬆ See suggestions';
      feedback.style.color = 'var(--brand)';
    }

    bEl.value = '';
    setTimeout(() => { feedback.textContent = ''; }, 2500);
    bEl.focus();
  });

  // Visual feedback while typing (scanner speed detection)
  bEl.addEventListener('input', () => {
    clearTimeout(bTimer);
    bTimer = setTimeout(() => {
      const v = bEl.value.trim();
      if (v.length > 3) {
        feedback.textContent = `${v.length} chars — press Enter`;
        feedback.style.color = 'var(--text3)';
      }
    }, 300);
  });

  // Keyboard shortcut F3 = focus barcode input
  document.addEventListener('keydown', e => {
    if (e.key === 'F3') {
      e.preventDefault();
      bEl.focus(); bEl.select();
    }
  });
})();

const sugBox   = document.getElementById('sug-box');

searchEl.addEventListener('input', () => {
  clearTimeout(srchTmr);
  const q = searchEl.value.trim();
  // A single character matches almost the entire catalog — the most
  // expensive possible query, fired on the very first keystroke, for a
  // result the user can't act on yet. Wait for 2+ chars before hitting
  // the server at all.
  if (q.length < 2) { sugBox.style.display = 'none'; return; }
  srchTmr = setTimeout(() => fetchSug(q), 220);
});

searchEl.addEventListener('keydown', e => {
  if (e.key === 'Escape') { searchEl.value = ''; sugBox.style.display = 'none'; }
  if (e.key === 'ArrowDown') sugBox.querySelector('.sug-row:not(.sug-out)')?.focus();
  if (e.key === 'Enter') {
    const first = sugBox.querySelector('.sug-row:not(.sug-out)');
    if (first) first.click();
  }
});

document.addEventListener('click', e => {
  if (!searchEl.contains(e.target) && !sugBox.contains(e.target))
    sugBox.style.display = 'none';
});

async function fetchSug(q) {
  // Offline-first: use IndexedDB when offline or on network error
  const offline = window.MediOffline && !MediOffline.isOnline();
  let data = [];
  if (offline) {
    data = (await MediOffline.searchProducts(q)) || [];
  } else {
    // suggest:true — lightweight dropdown mode: skips the server's
    // COUNT(*) query and hard-caps rows server-side, since this fires
    // on every keystroke and only ever renders a short list.
    const r = await apiPost('../api/products.php', { action: 'list', q, suggest: true, page_size: 12 }).catch(() => null);
    if (r && r.success) {
      data = r.data;
      if (window.MediOffline) data.forEach(p => MediOffline.cacheOne('products', p));
    } else {
      // Network error — fall back to local cache
      if (window.MediOffline) data = (await MediOffline.searchProducts(q)) || [];
    }
  }
  if (!data.length) {
    sugBox.innerHTML = `<div style="padding:10px 12px;font-size:12px;color:var(--text3)">
      No results for "${esc(q)}"</div>`;
    sugBox.style.display = 'block';
    return;
  }

  sugBox.innerHTML = data.slice(0, 14).map(p => {
    const stock   = parseInt(p.stock_pcs);
    const isOut   = stock <= 0;
    // "Out of Stock" is still shown truthfully (badge below) either way —
    // `blocked` is only about whether the row can actually be picked.
    // With Settings → Allow Negative Stock Sale ON, it must stay pickable.
    const blocked = isOut && !ALLOW_NEG_STOCK;
    const alert   = parseInt(p.low_stock_alert) || 10;
    const isLow   = !isOut && stock <= alert;
    const sBg     = isOut ? '#fde8e8' : isLow ? '#fff3dc' : '#d4f0e6';
    const sCl     = isOut ? 'var(--danger)' : isLow ? 'var(--warn)' : 'var(--accent)';
    const rowCls  = blocked ? 'sug-row sug-out'
                  : isOut   ? 'sug-row sug-faded'
                  : isLow   ? 'sug-row sug-low' : 'sug-row';
    const costPc  = p.qty_in_box > 0 ? (parseFloat(p.btp_vat||0)/p.qty_in_box) : 0;
    const margin  = p.sale_price > 0
      ? Math.round((parseFloat(p.sale_price) - costPc) / parseFloat(p.sale_price) * 100) : 0;

    // addItem() only needs the product's fields, never the photo — leaving
    // image out of the JSON embedded in onclick/onkeydown keeps these
    // (up to 14, rebuilt on every keystroke) attributes from ballooning.
    const pForClick = { ...p }; delete pForClick.image;
    const clickJson = JSON.stringify(pForClick).replace(/"/g,'&quot;');

    // Thumbnail + text share one flex wrapper so the row still hands the
    // grid exactly two top-level children (content, badges) — same as
    // before the photo was added, so .sug-row's grid-template-columns
    // (1fr auto auto auto) doesn't need to change.
    return `<div class="${rowCls}" tabindex="0"
               onclick="${blocked ? '' : `addItem(${clickJson})`}"
               onkeydown="if(event.key==='Enter'&&!${blocked})addItem(${clickJson})">
      <div style="display:flex;align-items:center;gap:8px;min-width:0">
        ${p.image
          ? `<img src="${p.image}" alt="" class="prod-thumb" style="width:52px;height:52px">`
          : `<div class="prod-thumb prod-thumb-ph" style="width:52px;height:52px;font-size:22px">${formIcon(p.form) || '📦'}</div>`}
        <div style="flex:1;min-width:0">
          <div style="display:flex;align-items:baseline;gap:6px;flex-wrap:wrap">
            <span style="font-weight:700;font-size:13.5px">${formIconName(p.form, p.name)}</span>
            ${p.strength ? `<span style="font-size:11px;color:var(--text3)">${esc(p.strength)}</span>` : ''}
          </div>
          <div style="font-size:11px;color:var(--text3);margin-top:2px;display:flex;gap:8px;flex-wrap:wrap">
            ${p.generic_name ? `<span>💊 <em>${esc(p.generic_name)}</em></span>` : ''}
            ${p.company_name ? `<span>🏭 ${esc(p.company_name)}</span>` : ''}
            ${p.form ? `<span>📋 ${esc(p.form)}</span>` : ''}
            ${p.rack ? `<span>📦 Rack ${esc(p.rack)}</span>` : ''}
          </div>
        </div>
      </div>
      <div style="display:flex;flex-direction:column;align-items:flex-end;gap:3px;flex-shrink:0">
        <span class="sug-badge" style="background:${sBg};color:${sCl};font-weight:700">
          ${isOut ? 'Out of Stock' : `${stock} pcs`}
        </span>
        <span class="sug-badge" style="background:var(--brand-xlight);color:var(--brand);font-weight:700">
          ${CUR}${parseFloat(p.sale_price || 0).toFixed(2)}/pc
        </span>
        ${margin > 0 ? `<span class="sug-badge" style="background:#d4f0e6;color:var(--accent)">${margin}% margin</span>` : ''}
      </div>
    </div>`;
  }).join('');

  sugBox.style.display = 'block';
}

// ── Add item ──────────────────────────────────────────────────
function addItem(p) {
  sugBox.style.display = 'none';
  searchEl.value       = '';
  searchEl.focus();

  // If already in list → increase qty by 1
  const idx = items.findIndex(i => i.product_id === p.id);
  if (idx >= 0) {
    // Allow qty to go beyond stock (negative stock sale allowed)
    items[idx].qty++;
    refreshRow(idx);
    recalc();
    return;
  }

  const wsPrice    = parseFloat(p.wholesale_price || 0);
  const wsDisc     = parseFloat(p.wholesale_disc_pct || 0);
  const retailPrice= parseFloat(p.sale_price) || 0;
  const usePrice   = SALE_MODE === 'wholesale' && wsPrice > 0 ? wsPrice : retailPrice;
  const useDisc    = SALE_MODE === 'wholesale' ? wsDisc : 0;

  items.push({
    product_id:         p.id,
    product_name:       p.name,
    strength:           p.strength    || '',
    form:               p.form        || '',
    price:              usePrice,
    retail_price:       retailPrice,
    wholesale_price:    wsPrice,
    wholesale_disc_pct: wsDisc,
    cost:               parseFloat(p.btp_vat) || 0,
    qty_in_box:         parseInt(p.qty_in_box) || 1,
    max_stock:          parseInt(p.stock_pcs)  || 0,
    qty:                0,
    disc_pct:           useDisc,
  });

  renderItems();
  recalc();
  saveDraft(); // auto-save on product add
  const newIdx = items.length - 1;
  setTimeout(() => document.getElementById(`qty-${newIdx}`)?.select(), 40);
  // Trigger alternatives lookup for this newly added product
  fetchAlternatives(p.id, newIdx);
}

// ── Render all rows ───────────────────────────────────────────
function renderItems() {
  const tbody = document.getElementById('items-body');
  const tfoot = document.getElementById('items-foot');
  const clrBtn = document.getElementById('clear-btn');

  document.getElementById('item-count').textContent = `${items.length} item(s)`;
  clrBtn.style.display = items.length ? 'inline-block' : 'none';
  tfoot.style.display  = items.length ? '' : 'none';

  if (!items.length) {
    tbody.innerHTML =
      `<tr id="empty-row"><td colspan="${SHOW_MARGIN ? 11 : 9}" class="empty-state">
        Search and add medicines above</td></tr>`;
    updateMarginFooter();
    return;
  }

  tbody.innerHTML = items.map((it, i) => {
    const lineTotal = lineAmt(it);
    const profit    = lineProfit(it);
    const profitCl  = profit >= 0 ? 'var(--accent)' : 'var(--danger)';
    return `<tr id="row-${i}">
      <td style="color:var(--text3);font-size:11px">${i + 1}</td>
      <td>
        <div style="font-weight:600;font-size:12.5px">${formIconName(it.form, it.product_name)}</div>
        <div style="font-size:10px;color:var(--text3)">
          ${it.strength ? esc(it.strength) + ' · ' : ''}${esc(it.form)}
          · Stock: <strong style="color:${it.max_stock<5?'var(--danger)':'var(--text2)'}">${it.max_stock}</strong>
        </div>
        ${it.max_stock <= 0
          ? `<div style="font-size:10px;color:var(--warn);font-weight:600">
               ⚠ Currently out of stock</div>`
          : it.qty > it.max_stock
            ? `<div style="font-size:10px;color:var(--warn)">
                 ⚠ Selling ${it.qty - it.max_stock} beyond stock</div>`
            : ''}
        ${it.latest_price !== undefined
          ? `<div style="font-size:10px;color:var(--warn)" title="This line stays at the price it was added at — remove and re-add to use the new price.">
               💲 Price changed to ${CUR}${it.latest_price.toFixed(2)} elsewhere</div>`
          : ''}
      </td>
      <td style="text-align:center">
        <input type="number" id="qty-${i}"
               value="${it.qty || ''}" min="1" placeholder="Qty"
               style="width:60px;text-align:center"
               oninput="updQty(${i},this.value)">
      </td>
      <td style="text-align:right">
        ${SALE_MODE === 'wholesale'
          ? `<div style="display:flex;flex-direction:column;align-items:flex-end;gap:2px">
               <input type="number" id="price-${i}"
                      value="${it.price.toFixed(2)}" min="0" step="0.01"
                      style="width:88px;text-align:right;
                             background:var(--accent-xlight);border-color:#86efac"
                      oninput="updPrice(${i},this.value)">
               <button onclick="saveWsDefault(${it.product_id},${i})"
                       style="font-size:9px;padding:1px 5px;background:none;
                              border:1px solid #86efac;border-radius:3px;
                              color:var(--accent);cursor:pointer;white-space:nowrap"
                       title="Save as new default wholesale price">
                 💾 Set Default
               </button>
             </div>`
          : `<span class="mono" style="font-weight:700;color:var(--brand);
                                        display:inline-block;width:80px;
                                        text-align:right"
                  title="Retail price is fixed. Change in Product Management.">
               ${CUR}${it.price.toFixed(2)}
             </span>`
        }
      </td>
      ${SHOW_MARGIN ? `<td class="mono" style="text-align:right;font-size:11px;color:var(--text2)"
          id="buy-${i}" title="Buy % = Discount received from MRP">${buyPct(it).toFixed(2)}%</td>` : ''}
      <td class="mono" style="text-align:right;font-size:11px;color:var(--text2)"
          id="sale-${i}" title="Sale % = Discount given from MRP">${salePct(it).toFixed(2)}%</td>
      ${SHOW_MARGIN ? `<td class="mono" style="text-align:right;font-size:11px;font-weight:700;color:${profitColor(profitPct(it))}"
          id="profit-${i}" title="Profit % = Net margin retained from MRP">${profitPct(it).toFixed(2)}%</td>` : ''}
      <td style="text-align:center">
        <input type="number" id="disc-${i}"
               value="${it.disc_pct || ''}"
               min="0"
               max="${SALE_MODE==='retail' ? MAX_RETAIL_DISC : 100}"
               step="0.5"
               style="width:58px;text-align:center;
                      ${SALE_MODE==='wholesale'&&it.wholesale_disc_pct>0?'background:var(--accent-xlight);border-color:#86efac':''}"
               title="${SALE_MODE==='retail' ? 'Max: '+MAX_RETAIL_DISC+'%' : 'Wholesale disc'}"
               oninput="updDisc(${i},this.value)">
      </td>
      <td class="mono" style="text-align:right;font-weight:700" id="lt-${i}">
        ${CUR}${lineTotal.toFixed(2)}
      </td>
      <td class="mono" style="text-align:right;font-size:11px;
                               color:${profitCl}" id="lp-${i}">
        ${CUR}${profit.toFixed(2)}
      </td>
      <td>
        <button onclick="removeItem(${i})"
                style="background:none;border:none;cursor:pointer;
                       color:var(--danger);font-size:14px;padding:2px 4px"
                title="Remove">✕</button>
      </td>
    </tr>`;
  }).join('');
  updateMarginFooter();
}

// ── Refresh a single row without full re-render ───────────────
function refreshRow(i) {
  const it       = items[i];
  const lineTotal = lineAmt(it);
  const profit    = lineProfit(it);
  const profitCl  = profit >= 0 ? 'var(--accent)' : 'var(--danger)';

  const qtyEl = document.getElementById(`qty-${i}`);
  if (qtyEl) qtyEl.value = it.qty;

  const ltEl = document.getElementById(`lt-${i}`);
  if (ltEl) ltEl.textContent = CUR + lineTotal.toFixed(2);

  const lpEl = document.getElementById(`lp-${i}`);
  if (lpEl) { lpEl.textContent = CUR + profit.toFixed(2); lpEl.style.color = profitCl; }

  // Margin columns (Buy % / Sale % / Profit %) — recalculated live
  // whenever Qty, Sale Rate (Price/pc), or line Disc% changes.
  const buyEl = document.getElementById(`buy-${i}`);
  if (buyEl) buyEl.textContent = buyPct(it).toFixed(2) + '%';

  const saleEl = document.getElementById(`sale-${i}`);
  if (saleEl) saleEl.textContent = salePct(it).toFixed(2) + '%';

  const profEl = document.getElementById(`profit-${i}`);
  if (profEl) {
    const pp = profitPct(it);
    profEl.textContent = pp.toFixed(2) + '%';
    profEl.style.color = profitColor(pp);
  }

  updateMarginFooter();
}

function flashRow(i) {
  const row = document.getElementById(`row-${i}`);
  if (!row) return;
  row.style.background = '#fde8e8';
  setTimeout(() => row.style.background = '', 500);
}

// ── Item field updates ────────────────────────────────────────
function updQty(i, v) {
  items[i].qty = Math.max(0, parseInt(v) || 0);
  refreshRow(i); recalc(); saveDraft();
}
function updPrice(i, v) {
  items[i].price = Math.max(0, parseFloat(v) || 0);
  refreshRow(i); recalc(); saveDraft();
}
function updDisc(i, v) {
  let disc = Math.max(0, parseFloat(v) || 0);
  // Retail mode: cap at max allowed discount
  if (SALE_MODE === 'retail' && disc > MAX_RETAIL_DISC) {
    disc = MAX_RETAIL_DISC;
    const inp = document.getElementById(`disc-${i}`);
    if (inp) inp.value = disc;
    // Flash warning
    if (inp) {
      inp.style.borderColor = 'var(--danger)';
      inp.title = `Max retail discount is ${MAX_RETAIL_DISC}%`;
      setTimeout(() => { inp.style.borderColor = ''; inp.title = ''; }, 2000);
    }
  }
  items[i].disc_pct = Math.min(100, disc);
  refreshRow(i); recalc(); saveDraft();
}
function removeItem(i) {
  items.splice(i, 1);
  renderItems(); recalc(); saveDraft();
}
function clearAll() {
  if (items.length && !confirm('Clear all items?')) return;
  items = [];
  renderItems(); recalc();
}

// ── Line calculations ─────────────────────────────────────────
function lineAmt(it) {
  const gross = it.qty * it.price;
  return gross - (gross * it.disc_pct / 100);
}
function lineProfit(it) {
  const cost = it.qty * (it.cost / it.qty_in_box);
  return lineAmt(it) - cost;
}

// ── Margin-analysis columns (Buy % / Sale % / Profit %) ─────────
// MRP is the product's fixed retail price captured when the item was
// added to the cart (products.sale_price — labelled "MRP/pc" elsewhere
// in this app, e.g. Purchase Entry / Product List). Buy Rate is the
// per-piece purchase cost. Sale Rate is the *effective* per-piece price
// the customer actually pays — Price/pc after the line's Disc% is
// applied — so these columns recalculate instantly from either input.
function pcMRP(it)      { return parseFloat(it.retail_price || it.price || 0); }
function pcBuyRate(it)  { return it.qty_in_box > 0 ? (it.cost / it.qty_in_box) : 0; }
function pcSaleRate(it) { return it.price * (1 - (it.disc_pct || 0) / 100); }

function buyPct(it) {
  const mrp = pcMRP(it);
  return mrp > 0 ? ((mrp - pcBuyRate(it)) / mrp) * 100 : 0;
}
function salePct(it) {
  const mrp = pcMRP(it);
  return mrp > 0 ? ((mrp - pcSaleRate(it)) / mrp) * 100 : 0;
}
function profitPct(it) {
  // Always equals buyPct(it) - salePct(it) by construction:
  // (mrp-buy)/mrp*100 - (mrp-sale)/mrp*100 = (sale-buy)/mrp*100
  const mrp = pcMRP(it);
  return mrp > 0 ? ((pcSaleRate(it) - pcBuyRate(it)) / mrp) * 100 : 0;
}
function profitColor(pct) {
  if (pct < 3)   return 'var(--danger)';  // Red    < 3%
  if (pct < 7)   return '#f59e0b';        // Orange 3%–7%
  if (pct <= 12) return 'var(--accent)';  // Green  7%–12%
  return '#0d6e49';                       // Dark Green > 12%
}

// ── Footer margin averages (Avg Buy % / Avg Sale % / Avg Profit %) ──
function updateMarginFooter() {
  const buyEl  = document.getElementById('ft-avg-buy');
  const saleEl = document.getElementById('ft-avg-sale');
  const profEl = document.getElementById('ft-avg-profit');
  if (!saleEl) return; // averages footer not present on this page load

  if (!items.length) {
    if (buyEl)  buyEl.textContent  = '0.00%';
    saleEl.textContent = '0.00%';
    if (profEl) { profEl.textContent = '0.00%'; profEl.style.color = 'var(--text2)'; }
    return;
  }

  const n = items.length;
  const avgBuy    = items.reduce((s, it) => s + buyPct(it), 0) / n;
  const avgSale   = items.reduce((s, it) => s + salePct(it), 0) / n;
  const avgProfit = items.reduce((s, it) => s + profitPct(it), 0) / n;

  if (buyEl)  buyEl.textContent  = avgBuy.toFixed(2) + '%';
  saleEl.textContent = avgSale.toFixed(2) + '%';
  if (profEl) {
    profEl.textContent = avgProfit.toFixed(2) + '%';
    profEl.style.color = profitColor(avgProfit);
  }
}

// ── Recalc payment panel ──────────────────────────────────────
// ── Discount mode toggle ──────────────────────────────────────
function setDiscMode(mode) {
  discMode = mode;
  const btn    = document.getElementById('disc-mode-btn');
  const suffix = document.getElementById('disc-suffix');
  const input  = document.getElementById('disc-pct');
  input.value  = '';           // clear on mode switch to avoid confusion
  input.step   = mode === 'flat' ? '1' : '0.1';
  input.removeAttribute('max'); // flat has no fixed max (capped at subtotal in recalc)
  if (mode === 'flat') {
    btn.textContent    = CUR;  // show currency symbol when flat
    btn.style.background = 'var(--warn)';
    suffix.textContent = '';
    input.placeholder  = '0.00';
  } else {
    btn.textContent    = '%';
    btn.style.background = 'var(--brand)';
    suffix.textContent = '%';
    input.placeholder  = '0';
  }
  recalc();
}

function recalc() {
  let subTotal = 0, totalProfit = 0;
  items.forEach(it => {
    subTotal    += lineAmt(it);
    totalProfit += lineProfit(it);
  });

  // Validate and compute discount based on active mode
  const discInput = document.getElementById('disc-pct');
  const discHint  = document.getElementById('disc-pct-hint');
  const maxDisc   = SALE_MODE === 'retail' ? MAX_RETAIL_DISC : 100;
  let discPct, discAmt;

  if (discMode === 'flat') {
    discAmt = Math.max(0, parseFloat(discInput.value) || 0);
    // Cap flat discount at subTotal
    if (discAmt > subTotal && subTotal > 0) {
      discAmt = subTotal;
      discInput.value = subTotal.toFixed(2);
      discHint.textContent = `⚠ Discount capped at bill total (${CUR}${subTotal.toFixed(2)})`;
      discHint.style.display = 'block';
      discInput.style.borderColor = 'var(--warn)';
    } else {
      discHint.style.display = 'none';
      discInput.style.borderColor = '';
    }
    discPct = subTotal > 0 ? (discAmt / subTotal) * 100 : 0;
  } else {
    discPct = parseFloat(discInput.value) || 0;
    if (discPct > maxDisc) {
      discInput.value = maxDisc;
      discHint.textContent = `⚠ Max allowed: ${maxDisc}%`;
      discHint.style.display = 'block';
      discInput.style.borderColor = 'var(--warn)';
    } else {
      discHint.style.display = 'none';
      discInput.style.borderColor = '';
    }
    discAmt = subTotal * (discPct / 100);
  }
  const total    = subTotal - discAmt;          // Net Total (pre-rounding)
  const payable  = applyRounding(total);        // Payable Amount (what customer actually owes)
  const roundAdj = Math.round((payable - total) * 100) / 100;
  const paid     = parseFloat(document.getElementById('pay-paid').value)  || 0;
  const due      = Math.max(0, payable - paid);

  // Update footer row
  document.getElementById('ft-subtotal').textContent = CUR + subTotal.toFixed(2);
  document.getElementById('ft-profit').textContent   = CUR + totalProfit.toFixed(2);
  document.getElementById('ft-profit').style.color   =
    totalProfit >= 0 ? 'var(--accent)' : 'var(--danger)';

  // Update payment panel
  document.getElementById('pay-sub').textContent    = CUR + subTotal.toFixed(2);
  document.getElementById('pay-disc').textContent   = '- ' + CUR + discAmt.toFixed(2);

  // Net Total / Round Adjustment rows — only shown when rounding actually
  // changes the figure, so the panel stays uncluttered when rounding is off
  // or when the total already happens to be a whole number.
  const netRow   = document.getElementById('pay-net-total-row');
  const roundRow = document.getElementById('pay-round-row');
  const totalLbl = document.getElementById('pay-total-label');
  if (ROUNDING_ENABLED && Math.abs(roundAdj) > 0.001) {
    netRow.style.display   = 'flex';
    roundRow.style.display = 'flex';
    document.getElementById('pay-net-total').textContent = CUR + total.toFixed(2);
    document.getElementById('pay-round-adj').textContent =
      (roundAdj > 0 ? '+ ' : '- ') + CUR + Math.abs(roundAdj).toFixed(2);
    document.getElementById('pay-round-adj').style.color =
      roundAdj > 0 ? 'var(--accent)' : 'var(--danger)';
    totalLbl.textContent = 'Payable Amount';
  } else {
    netRow.style.display   = 'none';
    roundRow.style.display = 'none';
    totalLbl.textContent   = 'Total Amount';
  }
  document.getElementById('pay-total').textContent  = CUR + payable.toFixed(2);

  // Suggest the full payable amount as a placeholder only — never forces
  // a value into the field, keeps it blank by default per the input rule.
  document.getElementById('pay-paid').placeholder   = payable.toFixed(2);
  // Include advance in effective paid
  const advApply = Math.min(
    parseFloat(document.getElementById('adv-apply')?.value || 0),
    payable
  );
  const effectivePaid = paid + advApply;
  const effectiveDue  = Math.max(0, payable - effectivePaid);
  const changeDue     = Math.max(0, effectivePaid - payable);

  document.getElementById('pay-due').textContent    = CUR + effectiveDue.toFixed(2);
  document.getElementById('pay-due').style.color    =
    effectiveDue > 0 ? 'var(--danger)' : 'var(--accent)';

  // Total Outstanding = customer's existing due + this sale's current due
  const outstandingRow = document.getElementById('cus-outstanding-row');
  const outstandingEl  = document.getElementById('cus-outstanding');
  if (outstandingRow && outstandingEl && _cusId > 1 && _cusState === 'existing') {
    const prevDue = parseFloat(
      document.getElementById('cus-due')?.textContent?.replace(CUR,'').replace(/,/g,'') || 0
    );
    const totalOut = prevDue + effectiveDue;
    if (totalOut > 0.001) {
      outstandingRow.style.display  = 'flex';
      outstandingEl.textContent     = CUR + totalOut.toFixed(2);
    } else {
      outstandingRow.style.display  = 'none';
    }
  } else if (outstandingRow) {
    outstandingRow.style.display = 'none';
  }

  // ── Due Sale Validation ────────────────────────────────────
  // A sale with an outstanding due balance must be attached to a
  // registered customer — Walk-in is only valid when fully paid.
  const dueWarn = document.getElementById('due-customer-warning');
  const isWalkin = !_cusId || _cusId <= 1;
  if (effectiveDue > 0.001 && isWalkin) {
    dueWarn.style.display = 'block';
  } else {
    dueWarn.style.display = 'none';
  }

  // Over-payment panel — shown when total paid exceeds bill total
  const changeRow  = document.getElementById('change-row');
  const overpayOpts = document.getElementById('overpay-opts');
  if (changeDue > 0.001) {
    changeRow.style.display = 'flex';
    document.getElementById('pay-change').textContent = CUR + changeDue.toFixed(2);
    // Show cash-vs-advance toggle only for named customers (not walk-in id=1)
    const isNamedCustomer = _cusId && _cusId > 1;
    overpayOpts.style.display = isNamedCustomer ? 'flex' : 'none';
    if (!isNamedCustomer) {
      // Force cash mode for walk-in — can't save advance without an account
      overpayMode = 'cash';
      document.getElementById('overpay-hint').textContent =
        '💵 Change will be returned to customer in cash.';
    }
  } else {
    changeRow.style.display = 'none';
    overpayMode = 'cash'; // reset mode when no overpayment
  }

  // Payment Status badge — replaces the old Full/Due quick buttons
  const badge = document.getElementById('pay-status-badge');
  if (total <= 0) {
    badge.textContent = '● Awaiting items';
    badge.style.background = 'var(--surface2)';
    badge.style.color      = 'var(--text3)';
    badge.style.borderColor= 'var(--border)';
  } else if (changeDue > 0.001) {
    badge.textContent = '💰 Change Due';
    badge.style.background = '#e0f2ff';
    badge.style.color      = '#0369a1';
    badge.style.borderColor= '#7dd3fc';
  } else if (effectiveDue <= 0.001) {
    badge.textContent = '✓ Paid';
    badge.style.background = 'var(--accent-xlight)';
    badge.style.color      = '#065f46';
    badge.style.borderColor= '#86efac';
  } else if (effectivePaid > 0.001) {
    badge.textContent = '◐ Partial';
    badge.style.background = 'var(--warn-light)';
    badge.style.color      = '#92400e';
    badge.style.borderColor= '#fcd34d';
  } else {
    badge.textContent = '● Due';
    badge.style.background = 'var(--danger-light)';
    badge.style.color      = 'var(--danger)';
    badge.style.borderColor= '#f5a0a0';
  }

  document.getElementById('pay-profit').textContent = CUR + totalProfit.toFixed(2);
  document.getElementById('pay-profit').style.color =
    totalProfit >= 0 ? 'var(--accent)' : 'var(--danger)';
}

// ── Quick pay helpers ─────────────────────────────────────────
function applyFullAdvance() {
  const available = parseFloat(
    document.getElementById('adv-available')?.textContent?.replace(CUR,'') || 0
  );
  const total = parseFloat(
    document.getElementById('pay-total').textContent.replace(CUR,'').replace(/,/g,'') || 0
  );
  document.getElementById('adv-apply').value = Math.min(available, total).toFixed(2);
  recalc();
}

// ── "Paid" quick button (beside Due / Balance) ─────────────────
// Fills Paid Amount with the exact payable total so Due drops to
// 0 and the sale is immediately ready to save — recalc() first to
// make sure the placeholder (kept in sync with `payable` on every
// recalculation, see recalc() above) reflects any last-second change
// to the cart/discount/customer advance before it's copied in.
function setFullyPaid() {
  recalc();
  const input = document.getElementById('pay-paid');
  input.value = input.placeholder;
  recalc();
}

// ── Payment type highlight ────────────────────────────────────
// ── Overpayment mode toggle ───────────────────────────────────
function setOverpayMode(mode) {
  overpayMode = mode;
  const cashBtn = document.getElementById('opt-cash');
  const advBtn  = document.getElementById('opt-advance');
  const hint    = document.getElementById('overpay-hint');
  if (mode === 'advance') {
    cashBtn.style.background = 'rgba(3,105,161,.1)';
    cashBtn.style.color      = '#0369a1';
    advBtn.style.background  = '#0369a1';
    advBtn.style.color       = '#fff';
    hint.textContent = '🏦 Over-paid amount will be saved to customer advance account.';
  } else {
    cashBtn.style.background = '#0369a1';
    cashBtn.style.color      = '#fff';
    advBtn.style.background  = 'rgba(3,105,161,.1)';
    advBtn.style.color       = '#0369a1';
    hint.textContent = '💵 Change will be returned to customer in cash.';
  }
}

function highlightPayType() {
  ['Cash','Card','Bank','MFS'].forEach(t => {
    const lbl = document.getElementById(`pt-lbl-${t}`);
    const inp = lbl?.querySelector('input');
    lbl?.classList.toggle('pt-active', inp?.checked);
  });
}

// ── Customer info display ─────────────────────────────────────
// ── Customer name lookup helpers ──────────────────────────────
// Build a local map from the hidden <select> for instant lookup
const _cusMap = {};   // { lowercaseName: { id, phone, due, advance } }
const _cusSel = document.getElementById('customer-id');
Array.from(_cusSel.options).forEach(o => {
  if (parseInt(o.value) > 1) {
    _cusMap[o.dataset.name.toLowerCase()] = {
      id: parseInt(o.value),
      name: o.dataset.name,
      phone: o.dataset.phone,
      due: parseFloat(o.dataset.due || 0),
      advance: parseFloat(o.dataset.advance || 0),
    };
  }
});

let _cusInputTimer = null;
let _cusState = 'walkin'; // 'walkin' | 'existing' | 'new'
let _cusId    = 1;        // current resolved customer id

function onCustomerNameInput() {
  clearTimeout(_cusInputTimer);
  const raw = (document.getElementById('cus-name').value || '').trim();
  if (!raw) {
    _setCusState('walkin', 1, null);
    document.getElementById('cus-sug-box').style.display = 'none';
    return;
  }
  _cusInputTimer = setTimeout(() => fetchCustomerSuggestions(raw), 250);
}

async function fetchCustomerSuggestions(q) {
  const box = document.getElementById('cus-sug-box');
  box.innerHTML = '<div style="padding:6px 10px;font-size:12px;color:var(--text3)">Searching…</div>';
  box.style.display = 'block';

  const offline = window.MediOffline && !MediOffline.isOnline();
  let cusList = [];
  if (offline) {
    cusList = (await MediOffline.searchCustomers(q)) || [];
  } else {
    const r = await apiPost('../api/customers.php', { action: 'search', q }).catch(() => null);
    if (r && r.success && r.data?.length) {
      cusList = r.data;
      if (window.MediOffline) cusList.forEach(c => MediOffline.cacheOne('customers', c));
    } else if (!r || !r.success) {
      if (window.MediOffline) cusList = (await MediOffline.searchCustomers(q)) || [];
    }
  }
  if (!cusList.length) {
    box.innerHTML = `
      <div style="padding:7px 10px;font-size:12px;color:var(--text3)">
        No match — <strong style="color:var(--brand);cursor:pointer"
                   onclick="document.getElementById('cus-sug-box').style.display='none';
                            _setCusState('new',-1,null)">
          + New customer
        </strong>
      </div>`;
    _setCusState('new', -1, null);
    return;
  }

  box.innerHTML = cusList.map(c => {
    const hasDue = parseFloat(c.due||0) > 0;
    const hasAdv = parseFloat(c.advance||0) > 0;
    return `<div onclick="selectCustomer(${JSON.stringify(c).replace(/"/g,'&quot;')})"
              style="padding:7px 12px;cursor:pointer;border-bottom:1px solid var(--border);
                     display:flex;align-items:center;gap:8px"
              onmouseover="this.style.background='var(--brand-xlight)'"
              onmouseout="this.style.background=''">
      <div style="flex:1;min-width:0">
        <div style="font-weight:700;font-size:13px">${esc(c.name)}</div>
        <div style="font-size:11px;color:var(--text3);display:flex;gap:8px;margin-top:1px">
          ${c.phone ? `<span>📞 ${esc(c.phone)}</span>` : ''}
          <span style="opacity:.6">ID: ${c.id}</span>
        </div>
      </div>
      <div style="font-size:11px;text-align:right;flex-shrink:0">
        ${hasDue ? `<div style="color:var(--danger);font-weight:700">Due: ${CUR}${parseFloat(c.due).toFixed(2)}</div>` : ''}
        ${hasAdv ? `<div style="color:var(--accent);font-weight:700">Adv: ${CUR}${parseFloat(c.advance).toFixed(2)}</div>` : ''}
        ${!hasDue && !hasAdv ? '<div style="color:var(--accent);font-size:10px">✓ Clear</div>' : ''}
      </div>
    </div>`;
  }).join('');
}

function selectCustomer(c) {
  document.getElementById('cus-name').value  = c.name;
  document.getElementById('cus-sug-box').style.display = 'none';
  _setCusState('existing', c.id, {
    phone:   c.phone || '',
    due:     parseFloat(c.due    || 0),
    advance: parseFloat(c.advance|| 0),
  });
}

async function _resolveCus() {
  // Legacy path — only fires if user types and presses Enter without selecting from dropdown
  const raw = (document.getElementById('cus-name').value || '').trim();
  if (!raw || raw.toLowerCase().startsWith('walk')) {
    _setCusState('walkin', 1, null);
    return;
  }
  // Try exact match in local map first
  const localMatch = _cusMap[raw.toLowerCase()];
  if (localMatch) { _setCusState('existing', localMatch.id, localMatch); return; }
  // Otherwise trigger suggestion dropdown
  fetchCustomerSuggestions(raw);
}

function _setCusState(state, id, data) {
  _cusState = state;
  _cusId    = id;
  const inf  = document.getElementById('cus-info');
  const newP = document.getElementById('new-cus-panel');
  const advRow = document.getElementById('advance-row');

  // Sync hidden select value
  _cusSel.value = id > 0 ? id : '1';

  if (state === 'existing' && data) {
    document.getElementById('cus-phone').textContent =
      data.phone ? '📞 ' + data.phone : '';
    document.getElementById('cus-due').textContent =
      CUR + data.due.toFixed(2);
    document.getElementById('cus-adv').textContent =
      CUR + data.advance.toFixed(2);
    inf.style.display    = 'block';
    inf.style.background = data.due > 0 ? '#fff5f5' : 'var(--accent-xlight)';
    newP.style.display   = 'none';

    if (advRow) {
      advRow.style.display = data.advance > 0 ? 'block' : 'none';
      const avEl = document.getElementById('adv-available');
      const apEl = document.getElementById('adv-apply');
      if (avEl) avEl.textContent = CUR + data.advance.toFixed(2);
      if (apEl) { apEl.value = ''; apEl.max = data.advance; }
    }

  } else if (state === 'new') {
    inf.style.display  = 'none';
    newP.style.display = 'block';
    if (advRow) advRow.style.display = 'none';

  } else {
    // walk-in
    inf.style.display  = 'none';
    newP.style.display = 'none';
    if (advRow) advRow.style.display = 'none';
  }
  recalc(); // recalc updates due-warning and total-outstanding live
  saveDraft(); // auto-save on customer change
}

// Legacy compat — called from a few places
function onCustomerChange() {
  const raw = (document.getElementById('cus-name')?.value || '').trim();
  if (!raw) _setCusState('walkin', 1, null);
  else _resolveCus();
}

// Expose for resetForm
function getCustomerId()   { return _cusId > 0 ? _cusId : 1; }
function getCustomerName() {
  return (document.getElementById('cus-name')?.value || '').trim() || 'Walk-in';
}

// ── Pre-fill from an approved online order ─────────────────────
// Called on load when ?online_order=<id> is present (routed here from
// sale/online-orders.php's Approve action). Populates items[] and the
// customer from the order's saved snapshot, but re-reads live product
// data (stock/qty_in_box/cost) since those can change between order
// placement and staff review. Nothing is saved automatically — staff
// still reviews/adjusts (discounts, quantities) and clicks Save like
// any normal sale; saveSale() then calls the `link_sale` action so
// the order stays traceable to the resulting sale.
async function loadFromOnlineOrder(orderId) {
  const banner = document.getElementById('online-order-banner');
  const ref    = document.getElementById('oo-banner-ref');

  const r = await apiPost('../api/online-orders.php', { action: 'detail', id: orderId });
  if (!r.success) {
    alert('⚠ Could not load online order: ' + (r.error || 'Unknown error'));
    return;
  }
  const order      = r.data;
  const orderItems = r.items || [];
  if (!orderItems.length) {
    alert('⚠ This online order has no items.');
    return;
  }

  // Re-fetch each product live — the order snapshot only stored
  // price/cost/qty, not qty_in_box/current stock.
  const productDetails = await Promise.all(
    orderItems.map(it => apiPost('../api/products.php', { action: 'detail', id: it.product_id }))
  );

  items = [];
  orderItems.forEach((it, i) => {
    const pr = productDetails[i];
    const p  = (pr && pr.success) ? pr.data : null;

    items.push({
      product_id:         it.product_id,
      product_name:       it.product_name,
      strength:           p ? (p.strength || '') : '',
      form:               it.form || (p ? p.form : '') || '',
      price:              parseFloat(it.price) || 0,
      retail_price:       p ? (parseFloat(p.sale_price) || 0) : (parseFloat(it.price) || 0),
      wholesale_price:    p ? (parseFloat(p.wholesale_price) || 0) : 0,
      wholesale_disc_pct: p ? (parseFloat(p.wholesale_disc_pct) || 0) : 0,
      cost:               p ? (parseFloat(p.btp_vat) || 0) : (parseFloat(it.cost) || 0),
      qty_in_box:         p ? (parseInt(p.qty_in_box) || 1) : 1,
      max_stock:          p ? (parseInt(p.stock_pcs) || 0) : 0,
      qty:                parseInt(it.qty) || 1,
      disc_pct:           0,
    });
  });

  // Pre-select the customer — falls back to the order's saved
  // name/phone if the live customer lookup fails for any reason.
  if (order.customer_id > 1) {
    const cr = await apiPost('../api/customers.php', { action: 'detail', id: order.customer_id });
    if (cr.success && cr.data) {
      selectCustomer(cr.data);
    } else {
      document.getElementById('cus-name').value = order.customer_name || '';
      _setCusState('existing', order.customer_id, {
        phone: order.customer_phone || '', due: 0, advance: 0,
      });
    }
  }

  // Carry delivery address / customer notes into the invoice notes so
  // staff can see them while finalizing the sale.
  const noteParts = [`Online order ${order.order_no}`];
  if (order.delivery_address) noteParts.push('Deliver to: ' + order.delivery_address);
  if (order.notes) noteParts.push(order.notes);
  document.getElementById('inv-notes').value = noteParts.join(' — ');

  _onlineOrderId = orderId;
  ref.textContent = order.order_no;
  banner.style.display = 'block';

  renderItems();
  recalc();

  // Clean the URL so a page refresh doesn't re-trigger the pre-fill.
  history.replaceState({}, '', window.location.pathname);
}



// ── Keyboard shortcuts ────────────────────────────────────────
document.addEventListener('keydown', e => {
  if (e.key === 'F2') {
    e.preventDefault();
    searchEl.focus(); searchEl.select();
  }
  if (e.key === 'F4') {
    e.preventDefault();
    const cn = document.getElementById('cus-name');
    cn?.focus(); cn?.select();
  }
  if (e.key === 'F9') {
    e.preventDefault();
    saveSale();
  }
});

// ── Save Sale ─────────────────────────────────────────────────
async function saveSale() {
  const errEl   = document.getElementById('save-err');
  const saveBtn = document.getElementById('save-btn');
  errEl.style.display = 'none';

  // ── Validate ─────────────────────────────────────────────
  if (!items.length) {
    showErr('⚠ Add at least one item before saving.'); return;
  }

  const invoice = document.getElementById('inv-no').value.trim();
  const date    = document.getElementById('inv-date').value;
  if (!invoice) { showErr('⚠ Invoice number is required.'); return; }
  if (!date)    { showErr('⚠ Date is required.'); return; }

  // Stock check across all rows
  for (let i = 0; i < items.length; i++) {
    const it = items[i];
    if (it.qty < 1) {
      showErr(`⚠ Row ${i+1} (${it.product_name}): Qty must be at least 1.`); return;
    }
    if (it.price <= 0) {
      showErr(`⚠ Row ${i+1} (${it.product_name}): Price must be greater than zero.`); return;
    }
  }

  // ── Collect totals ────────────────────────────────────────
  let subTotal = 0, totalProfit = 0;
  items.forEach(it => {
    subTotal    += lineAmt(it);
    totalProfit += lineProfit(it);
  });

  const rawDisc  = parseFloat(document.getElementById('disc-pct').value) || 0;
  let discPct, discAmt;
  if (discMode === 'flat') {
    discAmt = Math.min(rawDisc, subTotal);
    discPct = subTotal > 0 ? (discAmt / subTotal) * 100 : 0;
  } else {
    discPct = Math.min(rawDisc, SALE_MODE === 'retail' ? MAX_RETAIL_DISC : 100);
    discAmt = subTotal * (discPct / 100);
  }
  const finalAmt    = subTotal - discAmt;
  const payableAmt  = applyRounding(finalAmt);   // rounded payable amount
  const roundAdj    = Math.round((payableAmt - finalAmt) * 100) / 100;
  const rawPaid     = parseFloat(document.getElementById('pay-paid').value) || 0;
  const advUsed     = Math.min(
    parseFloat(document.getElementById('adv-apply')?.value || 0),
    payableAmt
  );
  // Cap the amount actually applied to the bill at the rounded payable total
  const paid        = Math.min(rawPaid, Math.max(0, payableAmt - advUsed));
  const changeDue   = Math.max(0, (rawPaid + advUsed) - payableAmt);
  const due         = Math.max(0, payableAmt - paid - advUsed);
  const payType     = document.querySelector('input[name="pay_type"]:checked')?.value || 'Cash';
  let   notes       = document.getElementById('inv-notes').value.trim();

  // ── Due Sale Validation ────────────────────────────────────
  // Client-side guard (server also validates, but this gives instant feedback)
  let custId = _cusId > 0 ? _cusId : 1;
  if (due > 0.001 && custId <= 1) {
    showErr('⚠ Due sales require a registered customer. Please select a customer above.');
    return;
  }

  // ── Validate new-customer phone when creating with due ────
  if (_cusState === 'new' && due > 0.001) {
    const newPhone = document.getElementById('new-cus-phone')?.value.trim() || '';
    if (!newPhone) {
      showErr('⚠ Mobile number is required for due sales. Please enter the customer\'s phone number.');
      return;
    }
    if (!/^01[0-9]{9}$/.test(newPhone)) {
      showErr('⚠ Invalid mobile number format. Use Bangladesh format: 01XXXXXXXXX (11 digits).');
      return;
    }
  }

  const saveAsAdvance = changeDue > 0.001 && overpayMode === 'advance' && _cusId > 1;
  if (changeDue > 0.001) {
    if (saveAsAdvance) {
      notes = (notes ? notes + ' — ' : '') +
        `Over-paid: ${CUR}${changeDue.toFixed(2)} saved to customer advance account.`;
    } else {
      notes = (notes ? notes + ' — ' : '') +
        `Cash received: ${CUR}${rawPaid.toFixed(2)}, Change returned: ${CUR}${changeDue.toFixed(2)}`;
    }
  }
  if (roundAdj !== 0) {
    notes = (notes ? notes + ' — ' : '') +
      `Round adjustment: ${roundAdj > 0 ? '+' : ''}${CUR}${roundAdj.toFixed(2)}`;
  }

  // ── Auto-create new customer if needed ────────────────────
  if (_cusState === 'new') {
    const newName  = getCustomerName();
    const newPhone = document.getElementById('new-cus-phone')?.value.trim() || '';
    const newAddr  = document.getElementById('new-cus-address')?.value.trim() || '';

    if (!newName || newName.toLowerCase().startsWith('walk')) {
      custId = 1; // fall back to walk-in
    } else {
      const cr = await apiPost('../api/customers.php', {
        action: 'add',
        name:   newName,
        phone:  newPhone,
        address:newAddr,
        due:    0, advance: 0,
      });
      if (!cr.success) {
        showErr('⚠ Could not create customer: ' + (cr.error || 'Unknown error'));
        return;
      }
      custId = cr.id;
      // Add to local map + select so advance/due panel works on next action
      _cusMap[newName.toLowerCase()] = {
        id: cr.id, name: newName,
        phone: newPhone, due: 0, advance: 0
      };
      const opt = document.createElement('option');
      opt.value = cr.id;
      opt.dataset.name    = newName;
      opt.dataset.phone   = newPhone;
      opt.dataset.due     = '0';
      opt.dataset.advance = '0';
      opt.textContent     = newName;
      _cusSel.appendChild(opt);
    }
  }

  // ── Build payload ─────────────────────────────────────────
  const payload = {
    action:        'save',
    sale_mode:     SALE_MODE,
    invoice,
    customer_id:   custId,
    date,
    sub_total:         subTotal,
    discount_pct:      discPct,
    discount_amt:      discAmt,
    final_price:       finalAmt,
    round_adjustment:  roundAdj,
    payable_amount:    payableAmt,
    paid,
    advance_used:       advUsed,
    overpay_as_advance: saveAsAdvance,
    overpay_amount:     saveAsAdvance ? changeDue : 0,
    due,
    approx_profit: totalProfit,
    payment_type:  payType,
    notes,
    items: items.map(it => ({
      product_id:   it.product_id,
      product_name: it.product_name,
      qty:          it.qty,
      price:        it.price,
      cost:         parseFloat((it.cost / it.qty_in_box).toFixed(4)),
      disc_pct:     it.disc_pct,
      total:        lineAmt(it),
      after_disc:   lineAmt(it),
    }))
  };

  // ── POST ──────────────────────────────────────────────────
  saveBtn.disabled    = true;
  saveBtn.textContent = '⏳ Saving…';

  // If offline, queue locally and show offline success
  if (window.MediOffline && !MediOffline.isOnline()) {
    const offUUID = await MediOffline.queueSale(payload);
    saveBtn.disabled    = false;
    saveBtn.textContent = '💾 Create Sale   (F9)';
    discardDraft();
    resetForm();
    MediOffline.toast(`📴 Sale saved offline (${payload.invoice}) — will sync when connected`, 'warn', 6000);
    return;
  }

  const r = await apiPost('../api/sales.php', payload);

  saveBtn.disabled    = false;
  saveBtn.textContent = '💾 Create Sale   (F9)';

  if (!r.success) {
    // Network error while apparently online — queue and inform user
    if (r.error && r.error.includes('Network error') && window.MediOffline) {
      await MediOffline.queueSale(payload);
      discardDraft();
      resetForm();
      MediOffline.toast(`📴 Sale queued offline — will sync when connection returns`, 'warn', 6000);
      return;
    }
    showErr('⚠ ' + (r.error || 'Save failed. Please try again.'));
    return;
  }

  // Traceability only — link this sale back to its originating online
  // order, if any. Best-effort: never blocks or fails the sale itself.
  if (_onlineOrderId) {
    try {
      await apiPost('../api/online-orders.php', {
        action: 'link_sale', id: _onlineOrderId, sale_id: r.sale_id,
      });
    } catch (e) { /* best-effort */ }
  }

  showSuccess(r.invoice, subTotal, discAmt, payableAmt, paid, due, totalProfit, r.sale_id, changeDue, saveAsAdvance, roundAdj);
  discardDraft(); // clear local crash-recovery draft
  resetForm(); // reset immediately — toast shows above the fresh form
}

// ── Success modal ─────────────────────────────────────────────
// ── Success toast (non-blocking) + immediate form reset ────────
function showSuccess(invoice, sub, disc, total, paid, due, profit, saleId, changeDue, savedAsAdvance, roundAdj = 0) {
  const cusName = getCustomerName();

  // 1. Build the toast element
  const toast = document.createElement('div');
  toast.id = 'sale-toast';
  toast.style.cssText = `
    position:fixed;top:56px;left:50%;transform:translateX(-50%);
    z-index:9999;min-width:340px;max-width:520px;width:max-content;
    background:var(--surface);border:2px solid var(--accent);
    border-radius:var(--r);box-shadow:var(--shmd);
    padding:14px 18px;animation:toastIn .2s ease`;

  // 2. Change / advance row (if applicable)
  const overpayHtml = changeDue > 0.001 ? `
    <div style="display:flex;align-items:center;justify-content:space-between;
                margin-top:8px;padding:6px 10px;border-radius:var(--rsm);
                background:${savedAsAdvance?'#d1fae5':'#e0f2ff'};
                border:1px solid ${savedAsAdvance?'#6ee7b7':'#7dd3fc'}">
      <span style="font-size:12px;font-weight:700;
                   color:${savedAsAdvance?'#065f46':'#0369a1'}">
        ${savedAsAdvance?'🏦 Saved to advance':'💰 Give change'}
      </span>
      <span class="mono" style="font-weight:800;font-size:14px;
                                  color:${savedAsAdvance?'#065f46':'#0369a1'}">
        ${CUR}${changeDue.toFixed(2)}
      </span>
    </div>` : '';

  toast.innerHTML = `
    <div style="display:flex;align-items:center;gap:12px">
      <div style="font-size:26px;line-height:1">✅</div>
      <div style="flex:1;min-width:0">
        <div style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap">
          <span style="font-weight:800;font-size:14px;color:var(--accent)">
            Sale Saved
          </span>
          <span class="mono" style="font-weight:700;color:var(--brand);font-size:13px">
            ${esc(invoice)}
          </span>
          <span style="font-size:12px;color:var(--text3)">${esc(cusName)}</span>
        </div>
        <div style="display:flex;gap:14px;margin-top:5px;font-size:13px">
          <span>Total: <strong class="mono">${CUR}${total.toFixed(2)}</strong></span>
          <span style="color:var(--accent)">
            Paid: <strong class="mono">${CUR}${paid.toFixed(2)}</strong>
          </span>
          ${due > 0.001
            ? `<span style="color:var(--danger)">Due: <strong class="mono">${CUR}${due.toFixed(2)}</strong></span>`
            : `<span style="color:var(--accent)">✓ Fully Paid</span>`
          }
        </div>
        ${overpayHtml}
      </div>
      <div style="display:flex;flex-direction:column;gap:5px;flex-shrink:0">
        <button class="btn btn-sm btn-accent"
                onclick="printInvoice(${saleId})"
                style="white-space:nowrap">
          🖨 Print
        </button>
        <button class="btn btn-sm"
                onclick="document.getElementById('sale-toast')?.remove()"
                style="font-size:11px;color:var(--text3)">
          ✕ Dismiss
        </button>
      </div>
    </div>`;

  // 3. Inject toast keyframe if not already present
  if (!document.getElementById('toast-style')) {
    const s = document.createElement('style');
    s.id = 'toast-style';
    s.textContent = `@keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(-8px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}`;
    document.head.appendChild(s);
  }

  // 4. Remove any existing toast, add the new one
  document.getElementById('sale-toast')?.remove();
  document.body.appendChild(toast);

  // 5. Auto-dismiss after 6 seconds
  setTimeout(() => {
    const t = document.getElementById('sale-toast');
    if (t) {
      t.style.transition = 'opacity .4s';
      t.style.opacity    = '0';
      setTimeout(() => t?.remove(), 400);
    }
  }, 6000);
}

// ── Print invoice ─────────────────────────────────────────────
function printInvoice(saleId) {
  window.open(`../sale/print.php?id=${saleId}`, '_blank',
    'width=800,height=700,scrollbars=yes');
}

// ── Reset form for next sale ──────────────────────────────────
async function resetForm() {
  items = [];
  renderItems();
  recalc();

  _onlineOrderId = null;
  const ooBanner = document.getElementById('online-order-banner');
  if (ooBanner) ooBanner.style.display = 'none';

  closeAltPanel();
  setDiscMode('pct');          // reset discount toggle to % mode
  document.getElementById('cus-name').value  = '';
  document.getElementById('customer-id').value = '1';
  document.getElementById('cus-info').style.display     = 'none';
  document.getElementById('new-cus-panel').style.display= 'none';
  _cusState = 'walkin'; _cusId = 1;
  document.getElementById('disc-pct').value  = '';
  document.getElementById('pay-paid').value  = '';
  document.getElementById('inv-notes').value = '';
  document.getElementById('inv-date').value  =
    new Date().toISOString().split('T')[0];

  // Reset payment type to Cash
  document.querySelector('input[name="pay_type"][value="Cash"]').checked = true;
  highlightPayType();

  // Fetch next invoice number from server
  const r = await apiPost('../api/sales.php', { action: 'next_ref' });
  if (r.success)
    document.getElementById('inv-no').value = r.ref;

  searchEl.focus();
}

// ── Error helper ──────────────────────────────────────────────
function showErr(msg) {
  const el = document.getElementById('save-err');
  el.textContent   = msg;
  el.style.display = 'block';
  el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  setTimeout(() => el.style.display = 'none', 5000);
}

async function saveWsDefault(productId, rowIdx) {
  const newPrice = parseFloat(document.getElementById(`price-${rowIdx}`)?.value) || 0;
  if (newPrice <= 0) { alert('Enter a valid price first.'); return; }
  if (!confirm(`Save ৳${newPrice.toFixed(2)} as the new default wholesale price for this product?`)) return;

  const r = await apiPost('../api/products.php', {
    action: 'save_ws_price',
    product_id: productId,
    wholesale_price: newPrice
  });
  if (r.success) {
    // Update item's stored wholesale price
    const idx = items.findIndex(it => it.product_id === productId);
    if (idx >= 0) items[idx].wholesale_price = newPrice;
    alert('✅ New default wholesale price saved!');
  } else {
    alert('⚠ ' + (r.error || 'Save failed'));
  }
}

// ── Alternative medicines ─────────────────────────────────────
let _altCartIdx    = -1; // which cart row triggered the alternatives panel
let _altAllData    = []; // full alternatives list from the last fetch (strip only shows a slice of this)
let _altRefGeneric = '';
let _altTagMap     = { lowestPriceId: null, highestProfitId: null, mostStockId: null };
let _altModalQuery = '';
let _altModalStock = 'all'; // 'all' | 'in' | 'low' | 'out'

const ALT_STRIP_LIMIT = 6; // cards shown inline before collapsing into "View all N →"

// Highlight tags depend on the FULL alternatives list (not whatever
// slice happens to be visible), computed once per fetch and reused by
// both the inline strip and the "view all" modal so the same product
// never gets a different badge in one view vs. the other.
function computeAltTagMap(data) {
  const inStock = data.filter(p => parseInt(p.stock_pcs) > 0);
  return {
    lowestPriceId: inStock.length
      ? inStock.reduce((a,b) => parseFloat(a.sale_price) < parseFloat(b.sale_price) ? a : b).id : null,
    highestProfitId: inStock.length
      ? inStock.reduce((a,b) => parseFloat(a.profit_pct||0) > parseFloat(b.profit_pct||0) ? a : b).id : null,
    mostStockId: inStock.length
      ? inStock.reduce((a,b) => parseInt(a.stock_pcs) > parseInt(b.stock_pcs) ? a : b).id : null,
  };
}

// Shared card markup — used by both the inline strip and the "view
// all" modal grid, so a product looks identical (badges, price,
// stock) wherever the cashier encounters it. `onPick` is a raw JS
// expression string (already knows which cart row to replace).
// `compact` = true for the inline strip (alt-panel), false for the
// "View All" modal grid. The strip needs every card to be the SAME
// height regardless of how many highlight tags a product happens to
// have — a card with both "Best Profit" AND "Most Stock" used to wrap
// onto a second line and grow taller than its neighbors, breaking the
// uniform-box look of the strip. Compact mode fixes a hard height and
// shows at most ONE tag (priority: Lowest Price > Best Profit > Most
// Stock) so nothing ever wraps. The modal has room to spare and isn't
// laid out in a single row, so it keeps full tags + natural height.
function altCardHtml(p, tagMap, onPick, compact = true) {
  const stock  = parseInt(p.stock_pcs);
  const alert  = parseInt(p.low_stock_alert) || 10;
  const isOut  = stock <= 0;
  // "Out of Stock" is still shown truthfully (label/color below) either
  // way — `blocked` is only about whether the card can actually be
  // picked. With Settings → Allow Negative Stock Sale ON, it must stay
  // pickable as an alternative too.
  const blocked = isOut && !ALLOW_NEG_STOCK;
  const isLow  = !isOut && stock <= alert;
  const stClr  = isOut ? '#ef4444' : isLow ? '#f59e0b' : '#10b981';
  const stBg   = isOut ? '#fde8e8' : isLow ? '#fff3dc' : '#d4f0e6';
  const stLbl  = isOut ? 'Out of Stock' : isLow ? `Low: ${stock}` : `In Stock: ${stock}`;
  const margin = parseFloat(p.profit_pct || 0);

  // Highlight badges — "Best Profit" only shown to users with the
  // Profit Visibility permission; the underlying sort order/data
  // (used for saveSale()'s cost/approx_profit bookkeeping) is
  // untouched either way, only this visual tag is gated.
  let tags = [];
  if (p.id == tagMap.lowestPriceId)                tags.push(['Lowest Price','#3b82f6','#eff6ff']);
  if (SHOW_MARGIN && p.id == tagMap.highestProfitId) tags.push(['Best Profit','#10b981','#d4f0e6']);
  if (p.id == tagMap.mostStockId)                  tags.push(['Most Stock','#0ea5e9','#e0f2fe']);
  if (compact) tags = tags.slice(0, 1); // never let a 2nd tag force a wrap
  const tagHtml = tags.map(([t,cl,bg]) =>
    `<span style="font-size:${compact?8.5:9.5}px;font-weight:700;padding:1px 5px;border-radius:5px;
                  background:${bg};color:${cl};white-space:nowrap">${t}</span>`
  ).join('');

  // Compact (strip) cards: 195px wide (25% narrower than the original
  // 260px) and a fixed 52px height (~20% shorter than the old ~65px
  // auto-height) with overflow hidden — every card is the exact same
  // box no matter what it contains. Modal cards keep the roomier,
  // auto-height layout.
  const cardStyle = compact
    ? `flex-shrink:0;width:195px;height:52px;overflow:hidden;`
    : `flex-shrink:0;width:195px;`;
  const thumbSize = compact ? 26 : 30;

  return `
  <div onclick="${blocked ? '' : onPick}"
       style="${cardStyle}border:1px solid var(--border);border-radius:var(--rsm);
              padding:${compact?'4px 7px':'6px 8px'};background:var(--surface);
              cursor:${blocked?'not-allowed':'pointer'};
              opacity:${isOut?.65:1};transition:border-color .12s,box-shadow .12s;
              display:flex;align-items:center;gap:6px"
       onmouseover="if(!${blocked})this.style.borderColor='var(--brand)',this.style.boxShadow='0 0 0 2px var(--brand-light)'"
       onmouseout="this.style.borderColor='var(--border)',this.style.boxShadow=''">

    <!-- Photo — same .prod-thumb convention as Product List / Sale
         Entry's search dropdown (real image if present, form-icon
         placeholder otherwise; footer.php's click-to-zoom just works). -->
    ${p.image
      ? `<img src="${p.image}" alt="" class="prod-thumb" style="width:${thumbSize}px;height:${thumbSize}px;flex-shrink:0">`
      : `<div class="prod-thumb prod-thumb-ph" style="width:${thumbSize}px;height:${thumbSize}px;font-size:${compact?13:15}px;flex-shrink:0">${formIcon(p.form) || '📦'}</div>`}

    <!-- Middle: name + company -->
    <div style="flex:1;min-width:0">
      <div style="font-weight:700;font-size:${compact?11:11.5}px;line-height:1.2;color:var(--text);
                  white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
        ${formIconName(p.form, p.name)}
      </div>
      <div style="font-size:${compact?9:9.5}px;color:var(--text3);white-space:nowrap;
                  overflow:hidden;text-overflow:ellipsis;margin-top:1px">
        ${esc(p.company_name || '')}
      </div>
      <div style="display:flex;gap:3px;margin-top:2px;${compact?'flex-wrap:nowrap;overflow:hidden':'flex-wrap:wrap'}">
        <span style="font-size:${compact?8.5:9}px;padding:1px 4px;border-radius:4px;font-weight:700;
                     background:${stBg};color:${stClr};white-space:nowrap;flex-shrink:0">${stLbl}</span>
        ${tagHtml}
      </div>
    </div>

    <!-- Right: price + margin -->
    <div style="flex-shrink:0;text-align:right;border-left:1px solid var(--border);
                padding-left:6px">
      <div style="font-size:${compact?11:12}px;font-weight:800;color:var(--brand);white-space:nowrap">
        ${CUR}${parseFloat(p.sale_price).toFixed(2)}
      </div>
      ${(SHOW_MARGIN && margin > 0)
        ? `<div style="font-size:9px;font-weight:700;color:var(--accent)">${margin}%</div>`
        : ''}
    </div>
  </div>`;
}

function altPickExpr(p) {
  return `replaceItemInCart(${JSON.stringify(p).replace(/"/g,'&quot;')},${_altCartIdx})`;
}

async function fetchAlternatives(productId, cartIdx) {
  _altCartIdx = cartIdx;
  const panel = document.getElementById('alt-panel');
  const list  = document.getElementById('alt-list');
  panel.style.display = 'block';
  list.innerHTML = '<div style="color:var(--text3);font-size:12px;padding:8px">Loading alternatives…</div>';

  const r = await apiPost('../api/products.php', {
    action: 'get_alternatives', product_id: productId
  });

  if (!r.success) {
    panel.style.display = 'none';
    return;
  }

  _altAllData    = r.data;
  _altRefGeneric = r.ref_generic || '';

  document.getElementById('alt-generic-name').textContent = _altRefGeneric;
  document.getElementById('alt-count-badge').textContent  =
    r.data.length ? r.data.length + (r.data.length === 1 ? ' alternative' : ' alternatives') : '';

  const viewAllBtn = document.getElementById('alt-view-all-btn');
  viewAllBtn.style.display = r.data.length > ALT_STRIP_LIMIT ? 'inline-block' : 'none';

  if (!r.data.length) {
    panel.style.display = 'none';
    return;
  }

  _altTagMap = computeAltTagMap(r.data);

  // With a handful of alternatives, show them all inline. With a large
  // generic family (79 brands isn't unusual for something like
  // Elemental Calcium), an endless horizontal scroll strip is unusable
  // for a cashier mid-sale — show the best ALT_STRIP_LIMIT picks
  // (server already sorts in-stock-first, best-margin-first, cheapest-
  // first) and hand off to a searchable/filterable modal for the rest.
  const shown = r.data.slice(0, ALT_STRIP_LIMIT);
  list.innerHTML = shown.map(p => altCardHtml(p, _altTagMap, altPickExpr(p), true)).join('');

  if (r.data.length > ALT_STRIP_LIMIT) {
    // Fixed 52px height — matches the compact alt cards exactly (see
    // altCardHtml()'s `compact` mode) so this tile sits flush with them
    // instead of stretching taller.
    list.innerHTML += `
    <div onclick="openAllAlternatives()"
         style="flex-shrink:0;width:90px;height:52px;border:1.5px dashed var(--brand-light);
                border-radius:var(--rsm);background:var(--surface);cursor:pointer;
                display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;
                color:var(--brand);transition:background .12s"
         onmouseover="this.style.background='var(--brand-xlight)'"
         onmouseout="this.style.background='var(--surface)'">
      <span style="font-size:14px">🔍</span>
      <span style="font-size:10px;font-weight:700;text-align:center;line-height:1.2">
        View all<br>${r.data.length}
      </span>
    </div>`;
  }

  // Enable mouse-wheel horizontal scroll on the list container
  const listEl = document.getElementById('alt-list');
  listEl._wheelHandler = (e) => {
    if (e.deltaY !== 0) { e.preventDefault(); listEl.scrollLeft += e.deltaY * 1.5; }
  };
  listEl.removeEventListener('wheel', listEl._wheelHandler);
  listEl.addEventListener('wheel', listEl._wheelHandler, { passive: false });
}

// ── "View all N alternatives" modal ─────────────────────────────
// A searchable/filterable grid over the SAME data the strip already
// fetched (no extra request) — lets a cashier narrow 79 brands down
// to a handful by name/company or by stock status in a couple of
// keystrokes/clicks, instead of scrolling a 20,000px-wide strip.
function openAllAlternatives() {
  _altModalQuery = '';
  _altModalStock = 'all';

  const chip = (val, label) => `
    <button type="button" onclick="setAltModalStockFilter('${val}')" id="alt-chip-${val}"
            style="padding:4px 11px;font-size:11.5px;font-weight:700;border-radius:999px;cursor:pointer;
                   border:1px solid var(--border);background:var(--surface);color:var(--text2);
                   transition:all .12s">
      ${label}
    </button>`;

  showModal(`
    <div style="display:flex;flex-direction:column;gap:10px">
      <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
        <input id="alt-modal-search" placeholder="🔍 Search by name or company…" autocomplete="off"
               oninput="_altModalQuery=this.value;renderAltModalGrid()"
               style="flex:1;min-width:180px;padding:7px 12px;font-size:13px;border:1px solid var(--border);
                      border-radius:9px;font-family:inherit;outline:none">
        <div style="display:flex;gap:5px" id="alt-chip-group">
          ${chip('all','All')}${chip('in','In Stock')}${chip('low','Low')}${chip('out','Out')}
        </div>
      </div>
      <div id="alt-modal-count" style="font-size:11px;color:var(--text3)"></div>
      <div id="alt-modal-grid"
           style="display:flex;flex-wrap:wrap;gap:8px;max-height:440px;overflow-y:auto;padding:2px"></div>
    </div>
  `, `🔄 Alternatives for ${esc(_altRefGeneric)} — ${_altAllData.length} found`, 'xl');

  highlightAltChip();
  renderAltModalGrid();
  setTimeout(() => document.getElementById('alt-modal-search')?.focus(), 60);
}

function setAltModalStockFilter(val) {
  _altModalStock = val;
  highlightAltChip();
  renderAltModalGrid();
}

function highlightAltChip() {
  ['all','in','low','out'].forEach(v => {
    const el = document.getElementById(`alt-chip-${v}`);
    if (!el) return;
    const active = v === _altModalStock;
    el.style.background   = active ? 'var(--brand)' : 'var(--surface)';
    el.style.color        = active ? '#fff' : 'var(--text2)';
    el.style.borderColor  = active ? 'var(--brand)' : 'var(--border)';
  });
}

function renderAltModalGrid() {
  const grid  = document.getElementById('alt-modal-grid');
  const count = document.getElementById('alt-modal-count');
  if (!grid) return;

  const q = _altModalQuery.trim().toLowerCase();
  const filtered = _altAllData.filter(p => {
    if (q && !(`${p.name} ${p.company_name||''}`.toLowerCase().includes(q))) return false;
    const stock = parseInt(p.stock_pcs);
    const alert = parseInt(p.low_stock_alert) || 10;
    const isOut = stock <= 0, isLow = !isOut && stock <= alert;
    if (_altModalStock === 'in'  && !(!isOut && !isLow)) return false;
    if (_altModalStock === 'low' && !isLow)              return false;
    if (_altModalStock === 'out' && !isOut)              return false;
    return true;
  });

  count.textContent = `Showing ${filtered.length} of ${_altAllData.length}`;

  if (!filtered.length) {
    grid.innerHTML = `<div style="width:100%;text-align:center;padding:30px;color:var(--text3)">
      No alternatives match your search/filter.</div>`;
    return;
  }

  grid.innerHTML = filtered.map(p => altCardHtml(p, _altTagMap,
    `${altPickExpr(p)};closeModal();`, false
  )).join('');
}

function replaceItemInCart(p, cartIdx) {
  if (cartIdx < 0 || cartIdx >= items.length) return;

  const oldItem = items[cartIdx];
  const wsPrice    = parseFloat(p.wholesale_price || 0);
  const wsDisc     = parseFloat(p.wholesale_disc_pct || 0);
  const retailPrice= parseFloat(p.sale_price) || 0;
  const usePrice   = SALE_MODE === 'wholesale' && wsPrice > 0 ? wsPrice : retailPrice;
  const useDisc    = SALE_MODE === 'wholesale' ? wsDisc : oldItem.disc_pct;

  // Replace product details, keep qty and disc from old row
  items[cartIdx] = {
    product_id:         p.id,
    product_name:       p.name,
    strength:           p.strength    || '',
    form:               p.form        || '',
    price:              usePrice,
    retail_price:       retailPrice,
    wholesale_price:    wsPrice,
    wholesale_disc_pct: wsDisc,
    cost:               parseFloat(p.btp_vat) || 0,
    qty_in_box:         parseInt(p.qty_in_box) || 1,
    max_stock:          parseInt(p.stock_pcs)  || 0,
    qty:                oldItem.qty,      // ← preserved
    disc_pct:           useDisc,          // ← preserved
  };

  renderItems();
  recalc();
  // Fetch alternatives for the newly selected product
  fetchAlternatives(p.id, cartIdx);
}

function closeAltPanel() {
  document.getElementById('alt-panel').style.display = 'none';
  _altCartIdx = -1;
}

// ── Hold & Resume ─────────────────────────────────────────────
const HOLD_API    = '../api/holds.php';
const DRAFT_KEY   = 'medicore_sale_draft';
const DRAFT_TS    = 'medicore_sale_draft_ts';

function collectSaleState() {
  return {
    customer_id:   _cusId || 1,
    customer_name: document.getElementById('cus-name').value || 'Walk-in',
    sale_mode:     SALE_MODE,
    disc_mode:     discMode,
    disc_value:    parseFloat(document.getElementById('disc-pct').value) || 0,
    paid_amount:   parseFloat(document.getElementById('pay-paid').value) || 0,
    pay_type:      document.querySelector('input[name="pay_type"]:checked')?.value || 'Cash',
    notes:         document.getElementById('inv-notes').value,
    items:         items,
    total_amount:  parseFloat(
                     document.getElementById('pay-total').textContent.replace(CUR,'').replace(/,/g,'')
                   ) || 0,
  };
}

async function holdSale() {
  if (!items.length) { showErr('⚠ Add at least one item before holding.'); return; }
  const state = collectSaleState();

  // Offline-first: queue hold locally if no connection
  if (window.MediOffline && !MediOffline.isOnline()) {
    await MediOffline.queueHold(state, 'sale');
    discardDraft();
    resetForm();
    MediOffline.toast('🔒 Sale held offline — will sync when connected', 'warn', 4000);
    return;
  }

  const r = await apiPost(HOLD_API, { action: 'save_sale', ...state });
  if (!r.success) { showErr('⚠ Hold failed: ' + r.error); return; }
  discardDraft();
  resetForm();
  const t = document.createElement('div');
  t.style.cssText = 'position:fixed;top:56px;left:50%;transform:translateX(-50%);z-index:9999;' +
    'background:#fff3dc;border:2px solid #f59e0b;border-radius:var(--r);padding:10px 18px;' +
    'font-size:13px;font-weight:700;color:#78350f;box-shadow:var(--shmd)';
  t.textContent = '🔒 Sale held — ' + r.hold_ref;
  document.body.appendChild(t);
  setTimeout(() => t.remove(), 3500);
}

async function showSaleHolds(q = '') {
  const r = await apiPost(HOLD_API, { action: 'list_sales', q });
  const rows = r.success ? r.data : [];
  // Note: unrelated to profit visibility — this just controls who can
  // delete a held sale (admin/manager only), kept as the original
  // hardcoded role check rather than can_view_profit().
  const isAdminOrMgr = <?= (is_admin() || current_user_role() === 'manager') ? 'true' : 'false' ?>;

  showModal(`
    <div style="margin-bottom:10px;display:flex;gap:7px;align-items:center">
      <input id="hold-search" placeholder="🔍 Search by ID / customer / user…"
             oninput="showSaleHolds(this.value)"
             style="flex:1;padding:6px 10px;border:1px solid var(--border);
                    border-radius:var(--rsm);font-size:13px;font-family:inherit">
      <span style="font-size:12px;color:var(--text3)">${rows.length} hold(s)</span>
    </div>
    ${!rows.length ? '<div style="text-align:center;padding:20px;color:var(--text3)">No held sales found.</div>' :
      '<div style="display:flex;flex-direction:column;gap:6px;max-height:380px;overflow-y:auto">' +
      rows.map(h => {
        const ts = new Date(h.created_at).toLocaleString();
        return `<div style="border:1px solid var(--border);border-radius:var(--rsm);
                             padding:9px 12px;background:var(--surface)">
          <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
            <span style="font-size:11px;font-family:monospace;font-weight:700;
                         background:var(--warn-light);padding:2px 7px;border-radius:5px;
                         color:#78350f">${esc(h.hold_ref)}</span>
            ${h.is_auto == 1 ? `<span style="font-size:10px;font-weight:700;
                         background:#fde8e8;color:var(--danger);padding:2px 7px;
                         border-radius:5px" title="Automatically saved when the page was
                         closed/reloaded mid-sale, not a deliberate Hold">⚡ Auto-saved</span>` : ''}
            <span style="font-weight:700;font-size:13px">${esc(h.customer_name)}</span>
            <span style="font-size:11px;color:var(--text3)">${h.item_count} item(s)</span>
            <span class="mono" style="font-weight:700;color:var(--brand)">${CUR}${parseFloat(h.total_amount).toFixed(2)}</span>
            <span style="margin-left:auto;font-size:11px;color:var(--text3)">${ts}</span>
          </div>
          <div style="font-size:11px;color:var(--text3);margin-top:4px">by ${esc(h.user_name)}</div>
          <div style="display:flex;gap:6px;margin-top:8px">
            <button class="btn btn-sm"
                    onclick="viewSaleHold(${h.id})">
              👁 View
            </button>
            <button class="btn btn-primary btn-sm"
                    onclick="closeModal();resumeSaleHold(${h.id})">
              ▶ Resume
            </button>
            ${isAdminOrMgr ? `<button class="btn btn-sm btn-danger"
                    onclick="deleteSaleHold(${h.id},this)">
              🗑 Delete
            </button>` : ''}
          </div>
        </div>`;
      }).join('') + '</div>'
    }
  `, '🔒 Held Sales');
}

// Read-only preview — uses action:'view_sale' (does NOT mark the hold
// 'resumed' the way action:'get_sale' does), so accidentally opening a
// preview never burns the hold. Offers Resume right from the preview too.
async function viewSaleHold(id) {
  const r = await apiPost(HOLD_API, { action: 'view_sale', id });
  if (!r.success) { alert('⚠ ' + r.error); return; }
  const d = r.data;
  const itemsHtml = (d.items || []).map(it => `
    <tr>
      <td style="padding:6px 8px;font-size:12.5px">${esc(it.product_name || '')}
        ${it.strength ? `<span style="color:var(--text3);font-size:11px"> ${esc(it.strength)}</span>` : ''}
      </td>
      <td style="padding:6px 8px;text-align:center;font-size:12.5px">${it.qty}</td>
      <td style="padding:6px 8px;text-align:right;font-size:12.5px" class="mono">${CUR}${parseFloat(it.price || 0).toFixed(2)}</td>
      <td style="padding:6px 8px;text-align:right;font-size:12.5px;font-weight:700" class="mono">${CUR}${(parseFloat(it.price || 0) * parseFloat(it.qty || 0)).toFixed(2)}</td>
    </tr>`).join('');

  showModal(`
    <div style="margin-bottom:8px;font-size:12.5px;color:var(--text2)">
      <strong>${esc(d.hold_ref)}</strong> — ${esc(d.customer_name || 'Walk-in')}
      ${d.is_auto == 1 ? ' · <span style="color:var(--danger);font-weight:700">⚡ Auto-saved</span>' : ''}
    </div>
    <div style="max-height:360px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--rsm)">
      <table style="width:100%;border-collapse:collapse">
        <thead>
          <tr style="background:var(--surface2)">
            <th style="padding:6px 8px;text-align:left;font-size:11px">Product</th>
            <th style="padding:6px 8px;text-align:center;font-size:11px">Qty</th>
            <th style="padding:6px 8px;text-align:right;font-size:11px">Rate</th>
            <th style="padding:6px 8px;text-align:right;font-size:11px">Total</th>
          </tr>
        </thead>
        <tbody>${itemsHtml || '<tr><td colspan="4" style="padding:14px;text-align:center;color:var(--text3)">No items.</td></tr>'}</tbody>
      </table>
    </div>
    <div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px">
      <span class="mono" style="font-weight:800;font-size:15px;color:var(--brand)">
        ${CUR}${parseFloat(d.total_amount || 0).toFixed(2)}
      </span>
      <button class="btn btn-primary btn-sm" onclick="closeModal();resumeSaleHold(${d.id})">▶ Resume This</button>
    </div>
  `, '👁 Preview Held Sale', 'lg');
}

async function resumeSaleHold(id) {
  const r = await apiPost(HOLD_API, { action: 'get_sale', id });
  if (!r.success) { alert('⚠ ' + r.error); return; }
  const d = r.data;

  // Restore form state
  await resetForm();
  items = d.items || [];
  if (d.sale_mode !== SALE_MODE) setSaleMode(d.sale_mode);
  setDiscMode(d.disc_mode || 'pct');
  document.getElementById('disc-pct').value  = d.disc_value || '';
  document.getElementById('pay-paid').value  = d.paid_amount || '';
  document.querySelector(`input[name="pay_type"][value="${d.pay_type}"]`).checked = true;
  document.getElementById('inv-notes').value = d.notes || '';
  // Restore customer
  if (d.customer_id > 1) {
    document.getElementById('cus-name').value = d.customer_name;
    _setCusState('existing', d.customer_id, { phone:'', due:0, advance:0 });
  }
  renderItems();
  recalc();
  highlightPayType();

  const t = document.createElement('div');
  t.style.cssText = 'position:fixed;top:56px;left:50%;transform:translateX(-50%);z-index:9999;' +
    'background:#d4f0e6;border:2px solid var(--accent);border-radius:var(--r);padding:10px 18px;' +
    'font-size:13px;font-weight:700;color:#065f46;box-shadow:var(--shmd)';
  t.textContent = '▶ Resumed: ' + d.hold_ref;
  document.body.appendChild(t);
  setTimeout(() => t.remove(), 3000);
}

async function deleteSaleHold(id, btn) {
  if (!confirm('Delete this held sale?')) return;
  const r = await apiPost(HOLD_API, { action: 'delete', id, type: 'sale' });
  if (!r.success) { alert('⚠ ' + r.error); return; }
  btn.closest('div[style*="border"]').remove();
}

// ── Auto-draft (crash recovery) ───────────────────────────────
let _draftTimer = null;
function saveDraft() {
  if (!items.length) return;
  localStorage.setItem(DRAFT_KEY, JSON.stringify(collectSaleState()));
  localStorage.setItem(DRAFT_TS, Date.now());
}
function discardDraft() {
  localStorage.removeItem(DRAFT_KEY);
  localStorage.removeItem(DRAFT_TS);
  document.getElementById('draft-banner').style.display = 'none';
}
async function restoreDraft() {
  try {
    const state = JSON.parse(localStorage.getItem(DRAFT_KEY));
    if (!state) return;
    document.getElementById('draft-banner').style.display = 'none';
    await resetForm();
    items = state.items || [];
    if (state.sale_mode !== SALE_MODE) setSaleMode(state.sale_mode);
    setDiscMode(state.disc_mode || 'pct');
    document.getElementById('disc-pct').value  = state.disc_value || '';
    document.getElementById('pay-paid').value  = state.paid_amount || '';
    const ptEl = document.querySelector(`input[name="pay_type"][value="${state.pay_type}"]`);
    if (ptEl) ptEl.checked = true;
    document.getElementById('inv-notes').value = state.notes || '';
    if (state.customer_id > 1) {
      document.getElementById('cus-name').value = state.customer_name;
      _setCusState('existing', state.customer_id, { phone:'', due:0, advance:0 });
    }
    renderItems();
    recalc();
    highlightPayType();
    discardDraft();
  } catch(e) { discardDraft(); }
}

function esc(s) {
  return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;')
                      .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

// ── Auto-hold on accidental reload/close ───────────────────────
// fetch() can be cancelled mid-flight when the page is navigating away,
// so we use sendBeacon (fire-and-forget, survives page unload) to push
// the in-progress sale into the Hold/Resume list as an auto-saved hold.
window.addEventListener('pagehide', () => {
  try {
    if (!items.length) return;
    const state = collectSaleState();
    const payload = Object.assign({ action: 'save_sale', auto: 1 }, state);
    const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
    navigator.sendBeacon(HOLD_API, blob);
  } catch (e) { /* best-effort */ }
});

// ── Init ──────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
  highlightPayType();
  onCustomerChange();
  recalc();
  searchEl.focus();

  // Draft recovery check
  const ts = parseInt(localStorage.getItem(DRAFT_TS) || '0');
  const age = Date.now() - ts;
  if (localStorage.getItem(DRAFT_KEY) && age < 3600000) { // < 1 hour
    document.getElementById('draft-banner').style.display = 'block';
  }

  // Auto-draft every 15 seconds (spec requirement)
  _draftTimer = setInterval(saveDraft, 15000);

  // Auto-draft on discount/payment/notes change (event-driven, per spec)
  ['disc-pct', 'pay-paid', 'adv-apply', 'inv-notes'].forEach(id => {
    document.getElementById(id)?.addEventListener('input', () => saveDraft());
  });
  document.querySelectorAll('input[name="pay_type"]').forEach(el =>
    el.addEventListener('change', () => saveDraft())
  );

  // Keyboard shortcuts
  document.addEventListener('keydown', e => {
    if (e.ctrlKey && e.key === 'h') { e.preventDefault(); holdSale(); }
    if (e.ctrlKey && e.key === 'r') { e.preventDefault(); showSaleHolds(); }
  });

  // Dismiss customer suggestion box on outside click
  document.addEventListener('click', e => {
    const box  = document.getElementById('cus-sug-box');
    const inp  = document.getElementById('cus-name');
    if (!box.contains(e.target) && e.target !== inp)
      box.style.display = 'none';
  });

  // Pre-fill from an approved online order, if routed here from the
  // Online Orders approval queue (sale/online-orders.php's Approve).
  const _ooId = parseInt(new URLSearchParams(window.location.search).get('online_order'));
  if (_ooId > 0) loadFromOnlineOrder(_ooId);
});

// ── Live stock sync ──────────────────────────────────────────────
// assets/stock-sync.js polls in the background and fires this event
// every ~2-3s. If another terminal just sold/received stock for a
// product that's already sitting in THIS cart, patch its max_stock
// and re-render — so a cashier here immediately sees an updated
// "Stock: N" line and the "⚠ Selling beyond stock" warning if the
// other sale ate into what was available, instead of finding out
// only after trying to save the invoice.
window.addEventListener('medi:stock-sync', (e) => {
  const changed = e.detail?.changed || [];
  if (!changed.length || !items.length) return;

  const byId = {};
  changed.forEach(p => { byId[p.id] = p; });

  let touched = false;
  items.forEach(it => {
    const p = byId[it.product_id];
    if (!p) return;
    if (it.max_stock !== p.stock_pcs) {
      it.max_stock = p.stock_pcs;
      touched = true;
    }
    // The price actually being charged (it.price) is deliberately left
    // alone — it's locked in the moment an item is added to the cart,
    // same as the existing "Retail price is fixed" tooltip already
    // promises. We just surface a heads-up if the product's price was
    // edited elsewhere mid-sale, via it.latest_price, so the cashier can
    // decide to remove/re-add the line if they want the new price.
    if (typeof p.sale_price === 'number' && Math.abs(p.sale_price - it.price) > 0.001) {
      if (it.latest_price !== p.sale_price) { it.latest_price = p.sale_price; touched = true; }
    } else if (it.latest_price !== undefined) {
      delete it.latest_price; touched = true;
    }
  });

  if (touched) renderItems();
});
</script>

<?php require_once '../includes/footer.php'; ?>
