/**
 * Fichier  : assets/js/pages/scanner.js
 * Auteur   : KREMER Régis
 * Version  : 2.6.98
 * Date     : 2026-07-20
 * Description : JS — Scanner QR/barcode : détection, création produit, intégration mouvements
 *
 * Changelog :
 *   v2.1.0 (2026-02-21) — Version initiale auditée et sécurisée
 *   v2.1.1 (2026-02-21) — Corrections sécurité, utils.js centralisé
 *   v2.2.0 (2026-02-21) — Permissions magasins, bons de commande, rapport d'écart,
 *                      import CSV, historique prix, bon de livraison multi-lignes
 *   v2.3.7 (2026-06-02) — UX scanner terrain : arrêt auto après détection, fiche produit visuelle,
 *                      saisie manuelle accessible et historique persistant
 *   v2.3.8 (2026-06-02) — Mouvements terrain fiabilisés : payload commun, validations,
 *                      boîte d'envoi hors ligne et anti double-validation
 *   v2.3.9 (2026-06-04) — Lot 1 inventaire: quantité quick move vide par défaut.
 *   v2.6.4 (2026-07-08) — Parcours terrain mobile/tablette : emplacements corrigés, stock visible, actions tactiles fiabilisées.
 *   v2.6.18 (2026-07-08) — Images produits locales via proxy sécurisé.
 *   v2.6.70 (2026-07-11) — I18N CORE LOCALE CURRENCY1 : dates de la file terrain selon la locale active.
 *   v2.6.89 (2026-07-20) — SCANNER MOBILE EXPERIENCE2 : page camera-first, résultat en volet,
 *                           historique et outils tactiles unifiés sur le noyau scanner commun.
 *   v2.6.91 (2026-07-20) — SCANNER FULL FIELD RECETTE4 : bouton caméra central,
 *                           résultat produit hiérarchisé et derniers correctifs terrain.
 *   v2.6.92 (2026-07-20) — SCANNER UX POLISH OPTIONAL1 : barre premium, historique unifié,
 *                           fiche produit clarifiée et scan en chaîne sécurisé.
 *   v2.6.93 (2026-07-20) — SCANNER UX POLISH FIX2 : Historique et Attente séparés.
 *   v2.6.94 (2026-07-20) — SCANNER UX FINAL FIELD VALIDATION2 : amorçage audio mobile,
 *                           prévention des doubles retours et validation cumulative des contextes.
 *   v2.6.95 (2026-07-20) — SCANNER RESULT DISPLAY FIX3 : première correction du volet résultat.
 *   v2.6.97 (2026-07-20) — SCANNER OVERLAY CONSUMER FIX5 : suppression du résultat source
 *                           derrière le volet mobile et cycle d’affichage nettoyé.
 *   v2.6.98 (2026-07-20) — SCANNER TERRAIN CONFIRMATION1 : source résultat rendue hidden/inert
 *                           pendant le volet tactile et restauration bureau déterministe.
 *   v2.6.96 (2026-07-20) — SCANNER RESULT FLOW FIX4 : surface résultat indépendante,
 *                           affichage fiable de chaque produit successif et fiche compacte restructurée.
 */
// ================================================================
// scanner.js — Page Scanner (camera barcode + QR, image decode, manual entry)
// Depends on: Api, Modal, toast, ScannerService, Icons, renderIcons
// ================================================================
const ScanPage = {
  recentScans: [],
  _prodCache: {},
  _lastCode: null,
  _lastTime: 0,
  _cameraRunning: false,
  _cameraMode: null, // 'barcode' | 'qrcode'
  _terrainMode: false,
  _terrainPid: null,
  _terrainP: null,
  _warehousesCache: null,
  _manualClearBound: false,
  _movementSubmitting: false,
  _syncingOffline: false,
  _resultPanelHandle: null,
  _resultPanelTrigger: null,
  _resultPanelSerial: 0,
  _locationsCache: null,
  _cameraContext: null,
  _touchMedia: null,
  _resumeMainAfterQr: false,
  _scanProcessing: false,
  _restartPromise: null,
  _scanRequestToken: 0,
  _activeProduct: null,
  _activeVariantChildren: [],
  _activeAmbiguousProducts: [],
  _activeRawCode: '',

  async init() {

    // Buttons (safe bindings: elements may not exist if template changes)
    const btnStart = document.getElementById("btnStartScan");
    const btnStop = document.getElementById("btnStopScan");
    const btnImg = document.getElementById("btnScanImage");
    const btnQr = document.getElementById("btnScanQR");
    const btnView = document.getElementById("btnScanView");
    const inpFile = document.getElementById("scanFileInput");
    const btnManual = document.getElementById("btnManualSearch");
    const inpManual = document.getElementById("manualCode");
    const btnManualClear = document.getElementById("btnManualClear");
    const btnClearRecent = document.getElementById("btnClearRecent");
    const btnTerrain = document.getElementById("btnTerrainMode");
    const btnOfflineBox = document.getElementById("tsOfflineBox");
    const btnOptions = document.getElementById("btnScannerOptions");
    const btnRecentTouch = document.getElementById("btnScannerRecent");
    const btnManualTouch = document.getElementById("btnScannerManual");
    const btnImageTouch = document.getElementById("btnScannerImage");
    const btnHistoryTouch = document.getElementById("btnScannerHistory");
    const btnQueueTouch = document.getElementById("btnScannerQueue");
    const btnCameraTouch = document.getElementById("btnScannerCamera");

    if (btnStart) btnStart.onclick = () => { this._primeScannerFeedback(); this.startBarcodeCamera(); };
    if (btnStop) btnStop.onclick = () => this.stopCamera();
    if (btnImg && inpFile) btnImg.onclick = () => { this._primeScannerFeedback(); inpFile.click(); };
    if (inpFile) inpFile.onchange = (e) => this.decodeImage(e);
    if (btnQr) btnQr.onclick = () => this.openQrScanModal();
    if (btnManual) btnManual.onclick = () => { this._primeScannerFeedback(); this.manualSearch(); };
    if (inpManual) {
      inpManual.onkeydown = (e) => { if (e.key === "Enter") this.manualSearch(); };
      inpManual.addEventListener('input', () => this._updateManualClear());
    }
    if (btnManualClear) btnManualClear.onclick = () => {
      if (inpManual) { inpManual.value = ''; inpManual.focus(); }
      this._updateManualClear();
    };
    if (btnClearRecent) btnClearRecent.onclick = () => this.clearRecentScans();
    if (btnView) btnView.onclick = () => this.toggleResultCard(true);
    if (btnTerrain) btnTerrain.onclick = () => this.toggleTerrainMode();
    if (btnOfflineBox) btnOfflineBox.onclick = () => this.openOfflineQueueModal();
    if (btnOptions) btnOptions.onclick = () => this.openScannerOptions(btnOptions);
    if (btnRecentTouch) btnRecentTouch.onclick = () => this.openRecentScansPanel(btnRecentTouch);
    if (btnManualTouch) btnManualTouch.onclick = () => { this._primeScannerFeedback(); this.openManualEntryPanel(btnManualTouch); };
    if (btnImageTouch && inpFile) btnImageTouch.onclick = () => { this._primeScannerFeedback(); inpFile.click(); };
    if (btnCameraTouch) btnCameraTouch.onclick = () => this.toggleCameraFromToolbar();
    if (btnHistoryTouch) btnHistoryTouch.onclick = () => this.openRecentScansPanel(btnHistoryTouch);
    if (btnQueueTouch) btnQueueTouch.onclick = () => this.openOfflineQueueModal();

    this._touchMedia = window.matchMedia ? window.matchMedia('(max-width: 1199px)') : null;
    if (this._touchMedia) {
      const onLayoutChange = () => this._handleLayoutChange();
      if (typeof this._touchMedia.addEventListener === 'function') this._touchMedia.addEventListener('change', onLayoutChange);
      else if (typeof this._touchMedia.addListener === 'function') this._touchMedia.addListener(onLayoutChange);
    }

    // Keyboard shortcuts (Lot 5)
    document.addEventListener('keydown', (e) => {
      // Avoid hijacking when typing
      const t = e.target;
      const isTyping = t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable);
      if (isTyping) return;
      if (e.key === ' '){
        e.preventDefault();
        if (this._cameraRunning) this.stopCamera(true); else this.startBarcodeCamera();
      }
      if (e.key === 't' || e.key === 'T') this.toggleTerrainMode();
    });

    // Auto-sync offline queue when back online
    window.addEventListener('online', () => this.syncOfflineQueue());

    this._loadRecentScans();
    this.renderRecent();
    this._updateManualClear();
    this._setScannerStatus('idle', 'Caméra inactive');

    // Show sync button if queue exists
    this._refreshOfflineUi();
    this._handleLayoutChange();

    if (typeof ScannerService === "undefined") {
      console.error("ScannerService not loaded");
      toast("Bibliothèque de scan non chargée.", "warning");
    }
  },


  // ─────────────────────────────────────────────────────────────
  // Helpers affichage / accessibilité
  // ─────────────────────────────────────────────────────────────
  _escapeHtml(value) {
    return String(value ?? '').replace(/[&<>'"]/g, (c) => ({
      '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'
    }[c]));
  },

  _productImageUrl(p) {
    const img = String((p && p.image) || '').trim();
    if (!img) return '';
    if (/^https?:\/\//i.test(img) || img.startsWith('/') || img.startsWith('data:')) return img;
    return img.startsWith('uploads/') ? img : ('uploads/' + img.replace(/^\/+/, ''));
  },

  _variantLabel(p) {
    if (!p) return '';
    const direct = String(p.variantLabel || p.size || '').trim();
    if (direct) return direct;
    const attrs = p.variantAttributes && typeof p.variantAttributes === 'object' ? p.variantAttributes : null;
    if (!attrs) return '';
    const parts = [];
    if (attrs.diameterMm) parts.push('Ø' + attrs.diameterMm + ' mm');
    if (attrs.widthMm && attrs.heightMm) parts.push(attrs.widthMm + ' × ' + attrs.heightMm + ' mm');
    else if (attrs.lengthMm) parts.push('L ' + attrs.lengthMm + ' mm');
    if (attrs.size) parts.push(String(attrs.size));
    return parts.join(' · ');
  },

  _formatVariantAttributes(p) {
    const attrs = p && p.variantAttributes && typeof p.variantAttributes === 'object' ? p.variantAttributes : null;
    if (!attrs) return '';
    const labels = {
      diameterMm: 'Diamètre', widthMm: 'Largeur', heightMm: 'Hauteur', lengthMm: 'Longueur',
      thicknessMm: 'Épaisseur', material: 'Matière', finish: 'Finition', retroClass: 'Classe',
      screwType: 'Type', thread: 'Filetage', headType: 'Tête', driveType: 'Empreinte', support: 'Support', standard: 'Standard'
    };
    return Object.entries(attrs)
      .filter(([, v]) => v !== null && v !== undefined && String(v).trim() !== '')
      .map(([k, v]) => `${labels[k] || k} : ${this._escapeHtml(v)}`)
      .slice(0, 6)
      .join(' · ');
  },

  _setScannerStatus(type, text) {
    const el = document.getElementById('scannerStatus');
    if (!el) return;
    el.className = 'scanner-status ' + (type ? 'scanner-status-' + type : 'scanner-status-idle');
    el.textContent = text || '';
  },

  _primeScannerFeedback() {
    try { return window.MageStockScannerFeedback?.prime?.() || false; } catch (_) { return false; }
  },

  _setCameraToggleState() {
    const button = document.getElementById('btnScannerCamera');
    if (!button) return;
    const label = button.querySelector('span');
    const running = !!this._cameraRunning;
    button.classList.toggle('is-active', running);
    button.setAttribute('aria-pressed', running ? 'true' : 'false');
    button.setAttribute('aria-label', running ? 'Désactiver la caméra' : 'Activer la caméra');
    if (label) label.textContent = running ? 'Arrêter' : 'Caméra';
  },

  toggleCameraFromToolbar() {
    this._primeScannerFeedback();
    if (this._cameraRunning) this.stopCamera();
    else this.startBarcodeCamera();
  },

  _updateManualClear() {
    const inp = document.getElementById('manualCode');
    const btn = document.getElementById('btnManualClear');
    if (!btn) return;
    btn.style.display = (inp && inp.value.trim()) ? '' : 'none';
  },

  _recentKey() { return 'sm_recent_scans_v2'; },
  _loadRecentScans() {
    try {
      const raw = localStorage.getItem(this._recentKey());
      const arr = raw ? JSON.parse(raw) : [];
      this.recentScans = Array.isArray(arr) ? arr.slice(0, 20) : [];
    } catch (_) { this.recentScans = []; }
  },
  _saveRecentScans() {
    try { localStorage.setItem(this._recentKey(), JSON.stringify(this.recentScans.slice(0, 20))); } catch (_) {}
  },
  _refreshRecentUi() {
    const button = document.getElementById('btnScannerHistory');
    const badge = document.getElementById('scannerHistoryBadge');
    const count = Array.isArray(this.recentScans) ? this.recentScans.length : 0;
    if (badge) {
      badge.hidden = count === 0;
      badge.textContent = String(count);
    }
    if (button) {
      button.setAttribute('aria-label', count ? `Historique des scans : ${count}` : 'Historique des scans vide');
      button.classList.toggle('has-items', count > 0);
    }
  },
  clearRecentScans() {
    this.recentScans = [];
    this._saveRecentScans();
    this.renderRecent();
    toast('Historique des scans vidé.', 'info');
  },

  async _getWarehouses() {
    if (Array.isArray(this._warehousesCache)) return this._warehousesCache;
    try { this._warehousesCache = await Api.warehouses(); }
    catch (_) { this._warehousesCache = []; }
    return this._warehousesCache;
  },

  async _getStockSummary(productId) {
    try {
      const [rows, whs, locations] = await Promise.all([
        Api.stock(productId),
        this._getWarehouses(),
        Array.isArray(this._locationsCache)
          ? Promise.resolve(this._locationsCache)
          : Api.locations().then(items => {
              this._locationsCache = Array.isArray(items) ? items : [];
              return this._locationsCache;
            }).catch(() => [])
      ]);
      const arr = Array.isArray(rows) ? rows : [];
      const locArr = Array.isArray(locations) ? locations : [];
      const total = arr.reduce((sum, row) => sum + (parseFloat(row.quantity) || 0), 0);
      const whName = (id) => {
        const warehouse = (whs || []).find(item => String(item.id) === String(id));
        return warehouse ? (warehouse.name || ('Magasin ' + id)) : ('Magasin ' + id);
      };
      const locName = (id) => {
        const location = locArr.find(item => String(item.id) === String(id));
        if (!location) return '';
        return [location.code, location.label, location.zone].filter(Boolean).join(' · ');
      };
      const rowLocations = (row) => {
        const labels = [];
        const push = (value) => {
          const text = String(value || '').trim();
          if (text && !labels.includes(text)) labels.push(text);
        };
        push(locName(row.locationId));
        if (Array.isArray(row.locations)) {
          row.locations.forEach(entry => {
            if (!entry) return;
            const qty = parseFloat(entry.quantity ?? entry.qty ?? 0) || 0;
            if (qty <= 0) return;
            push(locName(entry.locationId || entry.id) || entry.code || entry.label);
          });
        } else if (row.locations && typeof row.locations === 'object') {
          Object.entries(row.locations).forEach(([key, entry]) => {
            const qty = parseFloat(entry && typeof entry === 'object' ? (entry.quantity ?? entry.qty ?? 0) : entry) || 0;
            if (qty <= 0) return;
            push(locName(key) || (entry && typeof entry === 'object' ? (entry.code || entry.label) : ''));
          });
        }
        push([row.location, row.rack, row.shelf].filter(Boolean).join(' · '));
        return labels.slice(0, 3);
      };
      const byWh = arr.map(row => ({
        warehouseId: row.warehouseId,
        name: whName(row.warehouseId),
        qty: parseFloat(row.quantity) || 0,
        min: parseFloat(row.minStock) || 0,
        locations: rowLocations(row)
      })).filter(item => item.qty || item.min || item.locations.length).slice(0, 8);
      return { total, byWh };
    } catch (_) {
      return { total: null, byWh: [] };
    }
  },

  async _getVariantChildren(product) {
    if (!product || !product.id) return [];
    try {
      const all = await Api.products();
      if (!Array.isArray(all)) return [];
      return all.filter(p => String(p.parentId || '') === String(product.id)).slice(0, 12);
    } catch (_) { return []; }
  },

  _renderProductResultHtml(p, searchTerm, stock, children) {
    const img = this._productImageUrl(p);
    const variant = this._variantLabel(p);
    const attrs = this._formatVariantAttributes(p);
    const isParent = Array.isArray(children) && children.length > 0;
    const stockUnit = p.packBaseUnit || p.unitType || 'u';
    const stockValue = stock && stock.total !== null ? this._escapeHtml(stock.total) : '—';
    const warehouseRows = stock && Array.isArray(stock.byWh) ? stock.byWh : [];
    const identity = [p.sku || '', p.category || '', p.brand || '']
      .filter(Boolean)
      .map(value => this._escapeHtml(value));
    const primaryLocation = warehouseRows
      .flatMap(row => Array.isArray(row.locations) ? row.locations : [])
      .map(value => String(value || '').trim())
      .find(Boolean) || '';
    const lowStock = warehouseRows.some(row => Number(row.min || 0) > 0 && Number(row.qty || 0) <= Number(row.min || 0));

    const warehouseHtml = warehouseRows.length
      ? `<details class="scan-product-sheet__details" ${warehouseRows.length <= 2 ? 'open' : ''}>
          <summary><span>Répartition du stock</span><strong>${warehouseRows.length} magasin${warehouseRows.length > 1 ? 's' : ''}</strong></summary>
          <div class="scan-product-sheet__warehouses">${warehouseRows.map(row => {
            const locations = Array.isArray(row.locations) && row.locations.length
              ? row.locations.map(location => this._escapeHtml(location)).join(' · ')
              : 'Rangement non précisé';
            return `<div class="scan-product-sheet__warehouse">
              <span class="scan-product-sheet__warehouse-icon" aria-hidden="true">
                <svg viewBox="0 0 24 24"><path d="M3 9 12 4l9 5v11H3z"></path><path d="M8 20v-7h8v7"></path></svg>
              </span>
              <span class="scan-product-sheet__warehouse-copy"><b>${this._escapeHtml(row.name)}</b><small>${locations}</small></span>
              <strong>${this._escapeHtml(row.qty)} <small>${this._escapeHtml(stockUnit)}</small></strong>
            </div>`;
          }).join('')}</div>
        </details>`
      : `<div class="scan-product-sheet__empty"><strong>Aucun stock renseigné</strong><span>Le produit existe, mais aucune quantité n’est enregistrée.</span></div>`;

    const variantsHtml = isParent
      ? `<details class="scan-product-sheet__details scan-product-sheet__variants">
          <summary><span>Déclinaisons disponibles</span><strong>${children.length}</strong></summary>
          <p>Choisissez la déclinaison exacte avant d’enregistrer un mouvement.</p>
          <div class="scan-variant-list">${children.map(child => `<button type="button" class="scan-variant-chip" data-scan-variant="${this._escapeHtml(child.id)}">${this._escapeHtml(this._variantLabel(child) || child.name || child.sku || child.id)}</button>`).join('')}</div>
        </details>`
      : '';

    return `
      <article class="scan-product-sheet" aria-label="Produit scanné : ${this._escapeHtml(p.name || 'Produit')}">
        <header class="scan-product-sheet__hero">
          <div class="scan-product-sheet__thumb">${img
            ? `<img src="${this._escapeHtml(img)}" alt="${this._escapeHtml(p.name || 'Produit')}" loading="lazy">`
            : `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 3 9 5-9 5-9-5z"></path><path d="m3 8 9 5 9-5"></path><path d="M3 8v8l9 5 9-5V8"></path></svg>`}</div>
          <div class="scan-product-sheet__identity">
            <div class="scan-product-sheet__badges">
              <span class="scan-product-sheet__found"><span aria-hidden="true">✓</span> Produit trouvé</span>
              ${lowStock ? '<span class="scan-product-sheet__alert">Stock faible</span>' : ''}
              ${variant ? `<span class="scan-badge">${this._escapeHtml(variant)}</span>` : ''}
              ${isParent ? '<span class="scan-badge scan-badge-warning">Produit parent</span>' : ''}
            </div>
            <h3>${this._escapeHtml(p.name || 'Produit trouvé')}</h3>
            ${identity.length ? `<p>${identity.join('<span aria-hidden="true">•</span>')}</p>` : ''}
            ${attrs ? `<p class="scan-product-sheet__attrs">${attrs}</p>` : ''}
          </div>
        </header>

        <section class="scan-product-sheet__summary" aria-label="Résumé du produit">
          <div class="scan-product-sheet__stock">
            <span>Stock total</span>
            <strong>${stockValue}</strong>
            <small>${this._escapeHtml(stockUnit)}</small>
          </div>
          <div class="scan-product-sheet__location">
            <span class="scan-product-sheet__summary-icon" aria-hidden="true">
              <svg viewBox="0 0 24 24"><path d="M20 10c0 5-8 11-8 11S4 15 4 10a8 8 0 1 1 16 0z"></path><circle cx="12" cy="10" r="2.5"></circle></svg>
            </span>
            <span><small>Emplacement principal</small><strong>${this._escapeHtml(primaryLocation || 'Non précisé')}</strong></span>
          </div>
        </section>

        ${warehouseHtml}
        ${variantsHtml}

        <footer class="scan-product-sheet__code">
          <span>Code lu</span>
          <code>${this._escapeHtml(searchTerm)}</code>
        </footer>
      </article>
    `;
  },

  _isTouchLayout() {
    try {
      if (window.MageStockMobileUI?.device?.isDesktop) return !window.MageStockMobileUI.device.isDesktop();
      return !!(window.matchMedia && window.matchMedia('(max-width: 1199px)').matches);
    } catch (_) { return false; }
  },

  _scannerEngine() {
    return window.MageStockScanner || window.ScannerService || null;
  },

  _setResultSourceHidden(hidden) {
    const card = document.getElementById('scanResultCard');
    if (!card) return;
    const shouldHide = !!hidden;
    card.classList.toggle('scanner-result-card--sheet-source-hidden', shouldHide);
    card.hidden = shouldHide;
    card.toggleAttribute('inert', shouldHide);
    card.setAttribute('aria-hidden', shouldHide ? 'true' : 'false');
  },

  _handleLayoutChange() {
    const touchLayout = this._isTouchLayout();
    if (!touchLayout && this._resultPanelHandle) {
      this._resultPanelHandle.close('layout');
    }
    const page = document.getElementById('pgScanner');
    if (page) page.dataset.scannerLayout = touchLayout ? 'touch' : 'desktop';

    const card = document.getElementById('scanResultCard');
    if (card) {
      const hasResult = card.dataset.scanResultReady === '1';
      this._setResultSourceHidden(touchLayout && hasResult);
      if (!touchLayout && hasResult) card.style.display = '';
    }
    this._refreshOfflineUi();
  },

  _restoreResultCard(expectedHandle = null, options = {}) {
    if (expectedHandle && this._resultPanelHandle && this._resultPanelHandle !== expectedHandle) return false;
    if (!expectedHandle || this._resultPanelHandle === expectedHandle) {
      this._resultPanelHandle = null;
      this._resultPanelTrigger = null;
    }
    const card = document.getElementById('scanResultCard');
    if (card && options.hide) {
      card.style.display = 'none';
      card.dataset.scanResultReady = '0';
      this._setResultSourceHidden(false);
      card.setAttribute('aria-hidden', 'true');
    }
    return true;
  },

  _closeResultPanel(reason = 'close', options = {}) {
    const handle = this._resultPanelHandle;
    ++this._resultPanelSerial;
    this._resultPanelHandle = null;
    this._resultPanelTrigger = null;
    const card = document.getElementById('scanResultCard');
    if (card && options.hide) {
      card.style.display = 'none';
      card.dataset.scanResultReady = '0';
      this._setResultSourceHidden(false);
      card.setAttribute('aria-hidden', 'true');
    }
    if (handle) {
      try { handle.close(reason); } catch (_) {}
    }
    return new Promise(resolve => window.setTimeout(resolve, handle ? 210 : 0));
  },

  _cloneResultCardForPanel() {
    const source = document.getElementById('scanResultCard');
    if (!source) return null;
    const clone = source.cloneNode(true);
    clone.removeAttribute('id');
    clone.removeAttribute('aria-labelledby');
    clone.removeAttribute('aria-hidden');
    clone.removeAttribute('hidden');
    clone.removeAttribute('inert');
    clone.style.display = '';
    clone.classList.remove('scanner-result-card--sheet-source-hidden');
    clone.classList.add('scanner-result-card--panel');
    clone.removeAttribute('data-scan-result-bound');
    clone.querySelectorAll('[id]').forEach(node => node.removeAttribute('id'));
    clone.querySelectorAll('[data-scan-result-bound]').forEach(node => node.removeAttribute('data-scan-result-bound'));
    clone.querySelector('#terrainPanel, .terrain-panel')?.remove();
    return clone;
  },

  _bindResultInteractions(root) {
    if (!root || root.dataset.scanResultBound === '1') return;
    root.dataset.scanResultBound = '1';
    root.addEventListener('click', (event) => {
      const choice = event.target.closest('[data-resolved-product-index]');
      if (choice) {
        event.preventDefault();
        const product = this._activeAmbiguousProducts[Number(choice.dataset.resolvedProductIndex || 0)];
        if (product) this._showResolvedProduct(product, this._activeRawCode || product.sku || product.barcode || product.id);
        return;
      }

      const variant = event.target.closest('[data-scan-variant]');
      if (variant) {
        event.preventDefault();
        const child = this._activeVariantChildren.find(item => String(item.id) === String(variant.dataset.scanVariant || ''));
        if (child) this._showResolvedProduct(child, child.sku || child.barcode || child.id);
        return;
      }

      const action = event.target.closest('[data-scan-result-action]');
      if (!action) return;
      event.preventDefault();
      const type = action.dataset.scanResultAction;
      const product = this._activeProduct;
      if (type === 'rescan') {
        this.scanAgain(true);
      } else if (type === 'manual') {
        this._closeResultPanel('manual', { hide: true }).then(() => this.openManualEntryPanel(document.getElementById('btnScannerManual')));
      } else if (type === 'create') {
        const code = this._activeRawCode;
        this._closeResultPanel('create', { hide: true }).then(() => this.createFromScan(code));
      } else if (type === 'add' || type === 'remove') {
        if (!product?.id) return;
        this._closeResultPanel(type, { hide: true }).then(() => this.quickMove(String(product.id), type));
      } else if (type === 'product') {
        if (!product) return;
        this._closeResultPanel('product', { hide: true }).then(() => this.openProductModal(product));
      } else if (type === 'locate') {
        if (product) this.localizeProduct(product);
      }
    });
  },

  _presentResultPanel(trigger = null) {
    const card = document.getElementById('scanResultCard');
    if (!card) return;
    card.style.display = '';
    card.dataset.scanResultReady = '1';
    this._setResultSourceHidden(false);
    this._bindResultInteractions(card);

    if (!this._isTouchLayout() || !window.ActionSheet?.open) {
      try { card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (_) {}
      return;
    }

    const previous = this._resultPanelHandle;
    const serial = ++this._resultPanelSerial;
    this._resultPanelHandle = null;
    this._resultPanelTrigger = trigger || document.activeElement;
    if (previous) {
      try { previous.close('replaced'); } catch (_) {}
    }

    const content = this._cloneResultCardForPanel();
    if (!content) return;
    this._bindResultInteractions(content);
    const title = document.getElementById('scanResultTitle')?.textContent || 'Résultat du scan';
    let handle = null;

    // La fiche source reste dans la page pour le bureau, mais elle ne doit jamais
    // apparaître derrière le volet tactile. Le volet travaille sur sa propre copie.
    this._setResultSourceHidden(true);
    try {
      handle = window.ActionSheet.open({
        title,
        description: '',
        content,
        trigger: this._resultPanelTrigger,
        className: 'scanner-result-sheet scanner-result-sheet--isolated',
        closeOnBackdrop: true,
        onClose: () => {
          if (serial !== this._resultPanelSerial || this._resultPanelHandle !== handle) return;
          this._resultPanelHandle = null;
          this._resultPanelTrigger = null;
          // Sur tactile, la source reste masquée : seul un nouveau volet doit
          // présenter le résultat. Un passage en bureau la réaffiche via layout.
          if (this._isTouchLayout() && card.dataset.scanResultReady === '1') {
            this._setResultSourceHidden(true);
          }
        }
      });
    } catch (error) {
      this._setResultSourceHidden(false);
      throw error;
    }
    this._resultPanelHandle = handle;
  },

  openScannerOptions(trigger) {
    if (!window.ActionSheet?.open) return;
    const content = document.createElement('div');
    content.className = 'scanner-option-list';
    content.innerHTML = `
      <p class="scanner-panel-intro">Choisissez une méthode secondaire. Le scanner principal reconnaît déjà les codes-barres et QR MageStock.</p>
      <button type="button" class="scanner-option-button" data-scanner-option="manual">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h16v16H4z"></path><path d="M8 9h8M8 13h5"></path></svg>
        <span><strong>Saisir un code</strong><small>Code-barres, EAN, SKU ou QR interne.</small></span>
      </button>
      <button type="button" class="scanner-option-button" data-scanner-option="image">
        <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><path d="m21 15-5-5L5 21"></path></svg>
        <span><strong>Lire une image</strong><small>Analyser une photo déjà enregistrée.</small></span>
      </button>
      <button type="button" class="scanner-option-button" data-scanner-option="qr">
        <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
        <span><strong>Scanner QR dédié</strong><small>Cadre carré pour un QR difficile à lire.</small></span>
      </button>
      <button type="button" class="scanner-option-button" data-scanner-option="camera">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
        <span><strong>Choisir la caméra</strong><small>Basculer vers une autre caméra du périphérique.</small></span>
      </button>
      <button type="button" class="scanner-option-button" data-scanner-option="terrain">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3"></path><path d="M16 3h3a2 2 0 0 1 2 2v3"></path><path d="M21 16v3a2 2 0 0 1-2 2h-3"></path><path d="M3 16v3a2 2 0 0 0 2 2h3"></path></svg>
        <span><strong>Mode terrain</strong><small>Masquer temporairement le chrome de l’application.</small></span>
      </button>
      <button type="button" class="scanner-option-button" data-scanner-option="recent">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"></path><path d="M3 4v6h6"></path><path d="M12 7v5l3 2"></path></svg>
        <span><strong>Scans récents</strong><small>${this.recentScans.length} élément(s) conservé(s) sur cet appareil.</small></span>
      </button>`;
    const handle = window.ActionSheet.open({ title: 'Options du scanner', content, trigger });
    content.addEventListener('click', (event) => {
      const button = event.target.closest('[data-scanner-option]');
      if (!button) return;
      const option = button.dataset.scannerOption;
      handle.close('option');
      window.setTimeout(() => {
        if (option === 'manual') this.openManualEntryPanel(trigger);
        else if (option === 'image') document.getElementById('scanFileInput')?.click();
        else if (option === 'qr') this.openQrScanModal();
        else if (option === 'camera') this.openCameraSelectionPanel(trigger);
        else if (option === 'terrain') this.toggleTerrainMode();
        else if (option === 'recent') this.openRecentScansPanel(trigger);
      }, 210);
    });
  },

  openManualEntryPanel(trigger) {
    if (!window.ActionSheet?.open) {
      const input = document.getElementById('manualCode');
      if (input) input.focus();
      return;
    }
    const content = document.createElement('div');
    content.className = 'scanner-manual-panel';
    content.innerHTML = `
      <p class="scanner-panel-intro">Saisissez exactement le code-barres, l’EAN, le SKU ou le contenu du QR interne.</p>
      <label class="lbl" for="scannerManualPanelInput">Code produit</label>
      <input class="form-input" id="scannerManualPanelInput" type="search" inputmode="search" enterkeyhint="search" autocomplete="off" placeholder="Ex. 3760000000000 ou PAN-001">`;
    let handle;
    const search = ({ close }) => {
      const input = content.querySelector('#scannerManualPanelInput');
      const code = String(input?.value || '').trim();
      if (!code) {
        input?.focus();
        toast('Saisissez un code à rechercher.', 'warning');
        return false;
      }
      close('search');
      window.setTimeout(() => this.onScan(code), 210);
      return false;
    };
    handle = window.ActionSheet.open({
      title: 'Saisie manuelle',
      content,
      trigger,
      initialFocus: '#scannerManualPanelInput',
      actions: [
        { label: 'Annuler', className: 'btn btn-secondary' },
        { label: 'Rechercher', className: 'btn btn-primary', close: false, onClick: search }
      ]
    });
    content.querySelector('input')?.addEventListener('keydown', (event) => {
      if (event.key !== 'Enter') return;
      event.preventDefault();
      search({ close: handle.close });
    });
  },

  _recentPanelContent() {
    const content = document.createElement('div');
    content.className = 'scanner-panel-recent-list';
    if (!this.recentScans.length) {
      content.innerHTML = '<div class="scanner-panel-empty">Aucun scan récent sur cet appareil.</div>';
      return content;
    }
    content.innerHTML = this.recentScans.map((item, index) => {
      const time = new Date(item.time || Date.now());
      const image = item.image ? `<img src="${this._escapeHtml(item.image)}" alt="" loading="lazy">` : '<span>⌗</span>';
      const action = item.found === false ? 'Relancer' : 'Consulter';
      return `<button type="button" class="recent-item ${item.found === false ? 'recent-notfound' : 'recent-product-link'}" data-panel-recent-index="${index}">
        <span class="ri-thumb">${image}</span>
        <span class="ri-main"><strong>${this._escapeHtml(item.name || item.code || 'Scan')}</strong><small>${this._escapeHtml(item.code || '')}${item.variant ? ' · ' + this._escapeHtml(item.variant) : ''}</small></span>
        <span class="ri-action">${action}</span>
        <span class="ri-time">${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}</span>
      </button>`;
    }).join('');
    return content;
  },

  openRecentScansPanel(trigger) {
    if (!window.ActionSheet?.open) return;
    const content = this._recentPanelContent();
    const actions = this.recentScans.length ? [{
      label: 'Vider l’historique',
      className: 'btn btn-secondary',
      close: false,
      onClick: ({ close }) => {
        this.clearRecentScans();
        close('clear');
        return false;
      }
    }] : [];
    const handle = window.ActionSheet.open({
      title: 'Scans récents',
      description: 'Historique local de cet appareil.',
      content,
      trigger,
      actions
    });
    content.addEventListener('click', (event) => {
      const button = event.target.closest('[data-panel-recent-index]');
      if (!button) return;
      const index = Number(button.dataset.panelRecentIndex || 0);
      handle.close('recent');
      window.setTimeout(() => this.openRecentScan(index), 210);
    });
  },

  async openCameraSelectionPanel(trigger) {
    const engine = this._scannerEngine();
    if (!engine?.listCameras || !window.ActionSheet?.open) {
      toast('Sélection de caméra indisponible.', 'warning');
      return;
    }
    let cameras = [];
    try { cameras = await engine.listCameras(); }
    catch (error) {
      toast(error?.message || 'Impossible de lire les caméras.', 'error');
      return;
    }
    const content = document.createElement('div');
    content.className = 'scanner-option-list';
    content.innerHTML = cameras.length ? cameras.map((camera, index) => `
      <button type="button" class="scanner-option-button" data-camera-id="${this._escapeHtml(camera.id || '')}">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
        <span><strong>${this._escapeHtml(camera.label || ('Caméra ' + (index + 1)))}</strong><small>Utiliser cette caméra pour les prochains scans.</small></span>
      </button>`).join('') : '<div class="scanner-panel-empty">Aucune caméra détectée.</div>';
    const handle = window.ActionSheet.open({ title: 'Choisir la caméra', content, trigger });
    content.addEventListener('click', async (event) => {
      const button = event.target.closest('[data-camera-id]');
      if (!button) return;
      const wasRunning = this._cameraRunning;
      try {
        engine.setPreferredCamera?.(button.dataset.cameraId || '');
        handle.close('camera');
        if (wasRunning) {
          await this.stopCamera(true);
          window.setTimeout(() => this.startBarcodeCamera(), 240);
        }
        toast('Caméra sélectionnée.', 'success');
      } catch (error) {
        toast(error?.message || 'Impossible de sélectionner cette caméra.', 'error');
      }
    });
  },

  localizeProduct(product) {
    const productId = product?.id || product?.productId;
    if (!productId) {
      toast('Produit introuvable pour la localisation.', 'warning');
      return;
    }
    this._closeResultPanel('location');
    window.location.href = (window.Api?.url ? Api.url('plans?productId=' + encodeURIComponent(productId)) : ('plans?productId=' + encodeURIComponent(productId)));
  },

  // ─────────────────────────────────────────────────────────────
  // UI helpers
  // ─────────────────────────────────────────────────────────────
  toggleResultCard(show) {
    const card = document.getElementById("scanResultCard");
    if (!card) return;
    card.style.display = show ? "" : "none";
    if (!show && this._resultPanelHandle) this._closeResultPanel('hide');
  },

  // ─────────────────────────────────────────────────────────────
  // Lot 5 — Terrain mode + offline queue
  // ─────────────────────────────────────────────────────────────
  _isMobile() {
    try { return window.matchMedia && window.matchMedia('(max-width: 768px)').matches; }
    catch(_) { return false; }
  },

  toggleTerrainMode(force) {
    const on = (typeof force === 'boolean') ? force : !this._terrainMode;
    this._terrainMode = on;
    document.body.classList.toggle('sm-terrain-mode', on);

    const btnTerrain = document.getElementById('btnTerrainMode');
    if (btnTerrain) btnTerrain.classList.toggle('btn-primary', on);

    // Best effort fullscreen on mobile
    try {
      if (on && document.documentElement.requestFullscreen) document.documentElement.requestFullscreen().catch(()=>{});
      if (!on && document.fullscreenElement && document.exitFullscreen) document.exitFullscreen().catch(()=>{});
    } catch(_){ }

    // Terrain panel visibility depends on having a product selected
    this._updateTerrainPanelVisibility();
  },

  _updateTerrainPanelVisibility(){
    const panel = document.getElementById('terrainPanel');
    if (!panel) return;
    const showInline = !this._isTouchLayout() && this._terrainMode && this._terrainPid;
    panel.style.display = showInline ? '' : 'none';
  },

  _offlineKey(){ return 'sm_offline_movements_v1'; },
  openScannerHistoryPanel(trigger) {
    // Compatibilité avec les anciens appels : l’historique ne contient plus la boîte d’envoi.
    this.openRecentScansPanel(trigger);
  },

  _getOfflineQueue(){
    try {
      const raw = localStorage.getItem(this._offlineKey());
      const arr = raw ? JSON.parse(raw) : [];
      return Array.isArray(arr) ? arr : [];
    } catch(_){ return []; }
  },
  _setOfflineQueue(arr){
    try { localStorage.setItem(this._offlineKey(), JSON.stringify(arr || [])); } catch(_){ }
  },
  _movementMeta(payload, product){
    const p = product || this._terrainP || this._prodCache[String(payload && payload.productId)] || {};
    return {
      productId: payload ? payload.productId : null,
      productName: p.name || ('Produit #' + ((payload && payload.productId) || '')),
      sku: p.sku || p.barcode || '',
      image: this._productImageUrl(p),
      variant: this._variantLabel(p),
      type: payload ? payload.type : '',
      quantity: payload ? payload.quantity : '',
      unit: payload ? payload.unit : '',
      warehouseId: payload ? payload.warehouseId : '',
      locationId: payload ? payload.locationId : '',
      note: payload ? payload.note : ''
    };
  },
  _queueMovement(payload, product){
    const q = this._getOfflineQueue();
    q.push({
      id: Date.now() + '-' + Math.random().toString(16).slice(2),
      at: Date.now(),
      payload,
      meta: this._movementMeta(payload, product)
    });
    this._setOfflineQueue(q);
    this._refreshOfflineUi();
  },
  _refreshOfflineUi(){
    const btn = document.getElementById('tsSync');
    const box = document.getElementById('tsOfflineBox');
    const hint = document.getElementById('tsHint');
    const touchQueue = document.getElementById('btnScannerQueue');
    const touchBadge = document.getElementById('scannerQueueBadge');
    const q = this._getOfflineQueue();
    if (touchBadge) {
      touchBadge.hidden = !q.length;
      touchBadge.textContent = String(q.length);
    }
    if (touchQueue) {
      touchQueue.setAttribute('aria-label', q.length ? `Mouvements en attente : ${q.length}` : 'Aucun mouvement en attente');
      touchQueue.classList.toggle('has-items', q.length > 0);
    }
    if (btn) {
      btn.style.display = q.length ? '' : 'none';
      btn.disabled = this._syncingOffline;
      btn.textContent = this._syncingOffline ? 'Synchronisation…' : 'Synchroniser';
      btn.onclick = () => this.syncOfflineQueue();
    }
    if (box) {
      box.style.display = q.length ? '' : 'none';
      box.textContent = q.length ? `Voir attente (${q.length})` : 'Voir attente';
      box.onclick = () => this.openOfflineQueueModal();
    }
    if (hint) {
      if (!q.length) { hint.style.display = 'none'; hint.textContent = ''; }
      else {
        hint.style.display = '';
        hint.textContent = `Boîte d’envoi : ${q.length} mouvement(s) en attente de synchronisation.`;
      }
    }
  },
  _isNetworkError(error){
    const msg = String((error && error.message) || error || '').toLowerCase();
    return !navigator.onLine || msg.includes('network') || msg.includes('failed to fetch') || msg.includes('load failed') || msg.includes('timeout');
  },
  _movementErrorMessage(error){
    const msg = String((error && error.message) || error || '').trim();
    if (!msg) return 'Erreur inconnue.';
    if (msg.toLowerCase().includes('failed to fetch')) return 'Erreur réseau ou serveur indisponible.';
    return msg;
  },
  async syncOfflineQueue(){
    const q = this._getOfflineQueue();
    if (!q.length) return;
    if (!navigator.onLine) {
      toast('Toujours hors ligne — synchronisation impossible.', 'warning');
      this._refreshOfflineUi();
      return;
    }
    if (this._syncingOffline) return;
    this._syncingOffline = true;
    this._refreshOfflineUi();

    let ok = 0;
    const rest = [];
    const errors = [];

    for (const it of q){
      try {
        await Api.mvAdd(it.payload);
        ok++;
      } catch(e){
        const errMsg = this._movementErrorMessage(e);
        rest.push({ ...it, lastError: errMsg, lastTryAt: Date.now() });
        errors.push(errMsg);
      }
    }

    this._setOfflineQueue(rest);
    this._syncingOffline = false;
    this._refreshOfflineUi();
    this.openOfflineQueueModal(false);

    if (ok) toast(`Synchronisation terminée : ${ok} mouvement(s) envoyé(s).`, 'success');
    if (rest.length) toast(`${rest.length} mouvement(s) restent en attente.`, 'warning');
    if (!ok && errors[0]) toast(errors[0], 'error', 7000);
  },
  openOfflineQueueModal(showIfEmpty = true){
    const q = this._getOfflineQueue();
    if (!q.length && !showIfEmpty) return;
    const rows = q.length ? q.map((it) => {
      const m = it.meta || this._movementMeta(it.payload || {}, null);
      const d = new Date(it.at || Date.now());
      const typeLabel = (m.type || (it.payload && it.payload.type)) === 'remove' ? 'Sortie' : 'Ajout';
      const img = m.image ? `<img src="${this._escapeHtml(m.image)}" alt="" loading="lazy">` : '<span>📦</span>';
      const err = it.lastError ? `<div class="offline-error">${this._escapeHtml(it.lastError)}</div>` : '';
      return `<div class="offline-move-row" data-offline-id="${this._escapeHtml(it.id)}">
        <div class="offline-thumb">${img}</div>
        <div class="offline-main">
          <strong>${this._escapeHtml(m.productName || 'Produit')}</strong>
          <small>${this._escapeHtml(typeLabel)} · ${this._escapeHtml(m.quantity || '')} ${this._escapeHtml(m.unit || '')} · ${Locale.formatDate(d)} ${Locale.formatTime(d)}</small>
          ${m.variant ? `<em>${this._escapeHtml(m.variant)}</em>` : ''}
          ${err}
        </div>
        <div class="offline-actions">
          <button type="button" class="btn btn-primary btn-sm" data-offline-retry="${this._escapeHtml(it.id)}">Réessayer</button>
          <button type="button" class="btn btn-secondary btn-sm" data-offline-remove="${this._escapeHtml(it.id)}">Supprimer</button>
        </div>
      </div>`;
    }).join('') : '<div class="offline-empty">Aucun mouvement en attente.</div>';

    Modal.show('Boîte d’envoi terrain', `<div class="offline-move-list">${rows}</div>`, [
      { label: 'Synchroniser tout', cls: 'btn-primary', onclick: () => this.syncOfflineQueue() },
      { label: 'Fermer', cls: 'btn-secondary', onclick: () => Modal.close() }
    ], 'info');

    document.querySelectorAll('[data-offline-remove]').forEach(btn => {
      btn.onclick = () => this.removeOfflineMovement(btn.dataset.offlineRemove);
    });
    document.querySelectorAll('[data-offline-retry]').forEach(btn => {
      btn.onclick = () => this.retryOfflineMovement(btn.dataset.offlineRetry, btn);
    });
  },
  removeOfflineMovement(id){
    const q = this._getOfflineQueue().filter(it => String(it.id) !== String(id));
    this._setOfflineQueue(q);
    this._refreshOfflineUi();
    toast('Mouvement retiré de la boîte d’envoi.', 'info');
    this.openOfflineQueueModal();
  },
  async retryOfflineMovement(id, btn){
    const q = this._getOfflineQueue();
    const it = q.find(x => String(x.id) === String(id));
    if (!it) return;
    if (!navigator.onLine) { toast('Toujours hors ligne.', 'warning'); return; }
    this._setButtonLoading(btn, true, 'Envoi…');
    try {
      await Api.mvAdd(it.payload);
      this._setOfflineQueue(q.filter(x => String(x.id) !== String(id)));
      this._refreshOfflineUi();
      toast('Mouvement synchronisé.', 'success');
      this.openOfflineQueueModal(false);
    } catch(e){
      it.lastError = this._movementErrorMessage(e);
      it.lastTryAt = Date.now();
      this._setOfflineQueue(q);
      toast(it.lastError, 'error', 7000);
      this.openOfflineQueueModal();
    } finally {
      this._setButtonLoading(btn, false);
    }
  },

  _setResult({ status, title, text, bodyHtml, actionsHtml }) {
    const card = document.getElementById("scanResultCard");
    const t = document.getElementById("scanResultTitle");
    const s = document.getElementById("scanResultText");
    const a = document.getElementById("scanResultActions");
    if (!card || !t || !s || !a) return;

    card.classList.remove("is-found", "is-notfound", "is-error");
    if (status) card.classList.add(status);
    t.textContent = title || "";
    if (bodyHtml) s.innerHTML = bodyHtml;
    else s.textContent = text || "";
    a.innerHTML = actionsHtml || "";
    card.dataset.scanResultReady = "1";
    this._setResultSourceHidden(false);
    card.style.display = "";
    this._bindResultInteractions(card);
    if (typeof renderIcons === "function") renderIcons(card);
    this._presentResultPanel(document.activeElement);
  },

  _setCameraUi(running, mode) {
    const btnStart = document.getElementById("btnStartScan");
    const btnStop  = document.getElementById("btnStopScan");
    const preview  = document.getElementById("scannerPreview");
    const placeholder = document.getElementById("scannerPlaceholder");

    // ── Boutons ──────────────────────────────────────────────────
    if (btnStart) {
      btnStart.style.display = running ? "none" : "";
    }
    if (btnStop) {
      btnStop.style.display = running ? "" : "none";
    }

    // ── Placeholder ──────────────────────────────────────────────
    if (placeholder) placeholder.style.display = running ? "none" : "";
    this._setScannerStatus(running ? "running" : "idle", running ? "Caméra active — en attente d’un code" : "Caméra inactive");
    this._setCameraToggleState();

    if (!preview) return;

    // ── data-scan-mode → CSS driven show/hide de la frame ────────
    preview.dataset.scanMode = running ? (mode || "barcode") : "";

    const frame = preview.querySelector(".scanner-frame");
    if (!frame) return;

    if (!running) {
      preview.style.removeProperty('--scanner-frame-inset-x');
      preview.style.removeProperty('--scanner-frame-inset-y');
      frame.style.inset = "";
      return;
    }

    // ── Injection des 2 coins bas (une seule fois) ────────────────
    if (!frame.querySelector(".scanner-frame-bot")) {
      const bot = document.createElement("span");
      bot.className = "scanner-frame-bot";
      frame.appendChild(bot);
    }

    // ── Alignement de la frame sur le vrai qrbox html5-qrcode ────
    // html5-qrcode génère un div qrbox dont on lit les coords réelles.
    // Si non disponible (délai), on utilise des % calculés.
    this._alignFrame(preview, frame, mode);

    // Retry après que html5-qrcode ait fini d'initialiser (≈500ms)
    setTimeout(() => this._alignFrame(preview, frame, mode), 600);
  },

  // ─────────────────────────────────────────────────────────────
  // Aligne .scanner-frame exactement sur le qrbox rendu par html5-qrcode
  // ─────────────────────────────────────────────────────────────
  _alignFrame(preview, frame, mode) {
    if (!preview || !frame) return;

    const pRect = preview.getBoundingClientRect();
    if (!pRect.width || !pRect.height) return;

    // html5-qrcode injecte un div positionné avec le qrbox
    // On cherche le div fils direct du host qui contient la vidéo
    const host = document.getElementById("scannerH5");
    let qrboxEl = null;
    if (host) {
      // Le qrbox est un div avec style contenant width/height px
      const candidates = host.querySelectorAll("div[style*='width'][style*='height']");
      for (const el of candidates) {
        const st = el.style;
        if (st.width && st.height && !st.width.includes("%")) {
          qrboxEl = el;
          break;
        }
      }
    }

    if (qrboxEl) {
      // Lire les coords réelles du qrbox
      const qRect = qrboxEl.getBoundingClientRect();
      const top    = qRect.top  - pRect.top;
      const left   = qRect.left - pRect.left;
      const right  = pRect.right  - qRect.right;
      const bottom = pRect.bottom - qRect.bottom;
      preview.style.setProperty('--scanner-frame-inset-y', Math.max(0, Math.min(top, bottom)) + 'px');
      preview.style.setProperty('--scanner-frame-inset-x', Math.max(0, Math.min(left, right)) + 'px');
      frame.style.top = frame.style.left = frame.style.right = frame.style.bottom = '';
    } else {
      // Fallback %  : ScannerService utilise qrbox = 72% du min(w,h)
      const w = pRect.width;
      const h = pRect.height;
      const size = Math.min(w, h) * 0.72;
      const hPad = (w - size) / 2;
      const vPad = (h - size) / 2;
      if (mode === "barcode") {
        // Barcode : large, peu haut (ratio 3:1)
        const bh = size * 0.36;
        preview.style.setProperty('--scanner-frame-inset-y', ((h - bh) / 2) + 'px');
        preview.style.setProperty('--scanner-frame-inset-x', hPad + 'px');
        frame.style.top = frame.style.left = frame.style.right = frame.style.bottom = '';
      } else {
        // QR : carré
        preview.style.setProperty('--scanner-frame-inset-y', vPad + 'px');
        preview.style.setProperty('--scanner-frame-inset-x', hPad + 'px');
        frame.style.top = frame.style.left = frame.style.right = frame.style.bottom = '';
      }
    }
  },

  // ─────────────────────────────────────────────────────────────
  // Camera: BARCODE
  // ─────────────────────────────────────────────────────────────
  async startBarcodeCamera() {
    if (this._cameraRunning) return;

    const preview = document.getElementById("scannerPreview");
    if (!preview) { toast("Zone de prévisualisation introuvable", "error"); return; }
    const engine = this._scannerEngine();
    if (!engine) { toast("Scanner unifié indisponible.", "error"); return; }

    // Ensure host for html5-qrcode injected video
    let host = document.getElementById("scannerH5");
    if (!host) {
      host = document.createElement("div");
      host.id = "scannerH5";
      preview.appendChild(host);
    }
    host.innerHTML = ""; // reset

    // Hide legacy <video> placeholder (avoid conflicts)
    const legacyVideo = document.getElementById("scannerVideo");
    if (legacyVideo) {
      legacyVideo.style.display = "none";
      try { legacyVideo.pause(); legacyVideo.srcObject = null; } catch (_) {}
    }

    this._cameraRunning = true;
    this._cameraMode = "barcode";
    this._setCameraUi(true, "barcode");

    try {
      await engine.start({
        target: host,
        owner: 'scanner-page',
        context: 'general',
        mode: "auto",
        fps: 14,
        managedLifecycle: true,
        autoResume: true,
        autoResolve: false,
        onDetected: (code) => this.onScan(code),
        onError: (err) => {
          console.error("Camera error:", err);
          toast("Impossible d'activer la caméra : " + ((err && err.message) ? err.message : err), "error");
          this.stopCamera();
        }
      });
      this._setScannerStatus("running", "Caméra active — scannez un code");
      toast("Caméra active — Scannez un code", "success");
    } catch (e) {
      console.error(e);
      toast("Impossible d'activer la caméra : " + (e.message || e), "error");
      this.stopCamera();
    }
  },

  async stopCamera(silent = false) {
    if (!this._cameraRunning) return;

    this._cameraRunning = false;
    this._cameraMode = null;
    this._setCameraUi(false);

    const engine = this._scannerEngine();
    if (engine) {
      try { await engine.stop({ reason: silent ? 'detected' : 'user' }); } catch (_) {}
    }

    const host = document.getElementById("scannerH5");
    if (host) host.innerHTML = "";

    // Restore legacy <video> placeholder if needed
    const legacyVideo = document.getElementById("scannerVideo");
    if (legacyVideo) legacyVideo.style.display = "";

    this._setScannerStatus("idle", silent ? "Caméra arrêtée automatiquement" : "Caméra arrêtée");
    if (!silent) toast("Caméra arrêtée", "info");
  },

  // ─────────────────────────────────────────────────────────────
  // QR Camera (modal)
  // ─────────────────────────────────────────────────────────────
  async openQrScanModal() {
    const engine = this._scannerEngine();
    if (!engine) {
      toast("Scanner QR indisponible sur ce navigateur.", "warning");
      return;
    }
    this._resumeMainAfterQr = this._cameraRunning;
    if (this._cameraRunning) await this.stopCamera(true);

    // .qr-scan-wrap contraint le host à un carré responsive (voir CSS)
    const html = `
      <div style="display:flex;flex-direction:column;gap:14px;align-items:center">
        <div class="qr-scan-wrap">
          <div id="scanQrHost"></div>
        </div>
        <p style="font-size:13px;color:var(--text-muted);text-align:center;margin:0">
          Présentez un QR code devant la caméra
        </p>
      </div>
    `;

    Modal.show("Scanner un QR Code", html, [
      { label: "Fermer", cls: "btn-secondary", onclick: () => Modal.close() }
    ], "info");

    Modal.onClose?.(async () => {
      await this._stopQrScan();
      const resume = this._resumeMainAfterQr;
      this._resumeMainAfterQr = false;
      if (resume) window.setTimeout(() => this.startBarcodeCamera(), 220);
    });

    await this._startQrScan();
  },

  async _startQrScan() {
    const host = document.getElementById("scanQrHost");
    if (!host) return;

    try {
      const engine = this._scannerEngine();
      if (!engine) throw new Error('Scanner unifié indisponible.');
      await engine.start({
        target: host,
        owner: 'scanner-page-qr',
        context: 'general',
        mode: "qrcode",
        fps: 10,
        managedLifecycle: true,
        autoResume: true,
        autoResolve: false,
        onDetected: async (code) => {
          const raw = (code || "").trim();
          if (!raw) return;
          this._resumeMainAfterQr = false;
          await this._stopQrScan();
          Modal.close();
          this.onScan(raw);
        },
        onError: async (err) => {
          console.error(err);
          toast(err?.message || "Caméra QR indisponible (autorisation / HTTPS requis).", "error");
          Modal.close();
        }
      });
    } catch (e) {
      console.error(e);
      toast("Caméra QR indisponible.", "error");
      Modal.close();
    }
  },

  async _stopQrScan() {
    const engine = this._scannerEngine();
    if (engine) {
      try { await engine.stop({ reason: 'qr-close' }); } catch (_) {}
    }
    const host = document.getElementById("scanQrHost");
    if (host) host.innerHTML = "";
  },

  // ─────────────────────────────────────────────────────────────
  // Image decode
  // ─────────────────────────────────────────────────────────────
  async decodeImage(e) {
    const file = e && e.target && e.target.files ? e.target.files[0] : null;
    if (!file) return;
    try {
      if (this._cameraRunning) await this.stopCamera(true);
      this._setScannerStatus("running", "Analyse de l’image en cours…");
      const engine = this._scannerEngine();
      if (!engine?.scanImage) throw new Error("Scanner unifié indisponible.");
      const detected = await engine.scanImage(file, {
        owner: 'scanner-page-image',
        context: 'general',
        mode: 'auto',
        resumeCamera: false,
        autoResolve: false
      });
      if (!detected) throw new Error('Aucun code détecté.');
      await this.onScan(detected);
    } catch (error) {
      console.error(error);
      this._setScannerStatus("error", error?.message || "Aucun code détecté sur cette image");
      toast(error?.message || "Aucun code détecté sur cette image", "warning");
    } finally {
      try { e.target.value = ""; } catch (_) {}
    }
  },

  // ─────────────────────────────────────────────────────────────
  // Scan handling
  // ─────────────────────────────────────────────────────────────
  async onScan(code) {
    const raw = String(code || '').trim();
    if (!raw || this._scanProcessing) return;
    if (this._lastCode === raw && Date.now() - this._lastTime < 900) return;
    this._scanProcessing = true;
    const requestToken = ++this._scanRequestToken;
    this._activeRawCode = raw;
    this._activeProduct = null;
    this._activeVariantChildren = [];
    this._activeAmbiguousProducts = [];
    this._lastCode = raw;
    this._lastTime = Date.now();

    if (this._cameraRunning) {
      await this.stopCamera(true);
      this._setScannerStatus('success', 'Code détecté — recherche du produit');
    } else {
      this._setScannerStatus('running', 'Recherche du produit…');
    }

    try {
      const engine = this._scannerEngine();
      let resolved = null;
      if (engine?.resolve) {
        resolved = await engine.resolve(raw, { context: 'general' });
      } else {
        const products = await Api.products(raw);
        const product = Array.isArray(products) && products.length ? products[0] : null;
        resolved = product
          ? { status: 'found', raw, product, products: [product], matchedValue: raw, parsed: { canonical: raw } }
          : { status: 'not_found', raw, product: null, products: [], matchedValue: raw, parsed: { canonical: raw } };
      }

      if (requestToken !== this._scanRequestToken) return;

      if (resolved?.status === 'ambiguous' && Array.isArray(resolved.products) && resolved.products.length > 1) {
        this.playSuccessSound();
        this._activeAmbiguousProducts = resolved.products.slice(0, 8);
        const choices = resolved.products.slice(0, 8).map((product, index) => `
          <button type="button" class="scanner-choice-product" data-resolved-product-index="${index}">
            <strong>${this._escapeHtml(product.name || 'Produit')}</strong>
            <span>${this._escapeHtml(product.sku || product.barcode || product.id || '')}</span>
          </button>`).join('');
        this._setResult({
          status: 'is-found',
          title: 'Plusieurs produits correspondent',
          bodyHtml: `<div class="scanner-choice-list"><p>Sélectionnez le produit exact associé au code <code>${this._escapeHtml(raw)}</code>.</p>${choices}</div>`,
          actionsHtml: `<button class="btn btn-secondary" data-scan-result-action="rescan">Scanner un autre code</button>`
        });
        this._scanProcessing = false;
        return;
      }

      if (resolved?.status === 'found' && resolved.product) {
        await this._showResolvedProduct(resolved.product, resolved.matchedValue || resolved.parsed?.canonical || raw, resolved, requestToken);
        this._scanProcessing = false;
        return;
      }

      if (resolved?.status === 'invalid') {
        this.playErrorSound();
        this._setResult({
          status: 'is-error',
          title: 'Code illisible',
          text: 'Le contenu détecté ne correspond pas à un code exploitable.',
          actionsHtml: `
            <button class="btn btn-primary" data-scan-result-action="rescan">Réessayer</button>
            <button class="btn btn-secondary" data-scan-result-action="manual">Saisir manuellement</button>`
        });
      } else {
        this.playErrorSound();
        const canCreate = window.SM_ROLE === 'admin';
        this._setResult({
          status: 'is-notfound',
          title: 'Code non reconnu',
          bodyHtml: `<div class="scanner-notfound-code"><span>Code détecté</span><code>${this._escapeHtml(raw)}</code><p>Aucun produit MageStock ne correspond à ce code.</p></div>`,
          actionsHtml: `
            <button class="btn btn-primary" data-scan-result-action="rescan">Réessayer</button>
            <button class="btn btn-secondary" data-scan-result-action="manual">Saisir manuellement</button>
            ${canCreate ? '<button class="btn btn-secondary scanner-action-wide" data-scan-result-action="create">Créer le produit</button>' : ''}`
        });
        this._terrainPid = null;
        this._terrainP = null;
        this._updateTerrainPanelVisibility();
        this.recentScans.unshift({ code: raw, name: 'Non trouvé', sku: '', found: false, time: Date.now() });
        if (this.recentScans.length > 20) this.recentScans.pop();
        this._saveRecentScans();
        this.renderRecent();
      }

      this._setScannerStatus('error', resolved?.status === 'invalid' ? 'Code illisible' : 'Produit non trouvé');
    } catch (error) {
      console.error(error);
      this.playErrorSound();
      this._setScannerStatus('error', error?.message || 'Erreur de recherche produit');
      this._setResult({
        status: 'is-error',
        title: 'Recherche impossible',
        text: error?.message || 'Une erreur empêche la recherche du produit.',
        actionsHtml: `
          <button class="btn btn-primary" data-scan-result-action="rescan">Réessayer</button>
          <button class="btn btn-secondary" data-scan-result-action="manual">Saisir manuellement</button>`
      });
    } finally {
      this._scanProcessing = false;
    }
  },

  async _showResolvedProduct(product, searchTerm, resolved = null, requestToken = null) {
    const token = requestToken == null ? ++this._scanRequestToken : requestToken;
    const p = product;
    this._prodCache[String(p.id)] = p;
    this._terrainPid = String(p.id);
    this._terrainP = p;
    this.playSuccessSound();

    const [stockSummary, variantChildren] = await Promise.all([
      this._getStockSummary(p.id),
      this._getVariantChildren(p)
    ]);
    if (token !== this._scanRequestToken) return false;

    this._activeProduct = p;
    this._activeVariantChildren = variantChildren;
    this._activeAmbiguousProducts = [];
    this._activeRawCode = String(searchTerm || '');

    this._setResult({
      status: 'is-found',
      title: 'Produit trouvé',
      bodyHtml: this._renderProductResultHtml(p, searchTerm, stockSummary, variantChildren),
      actionsHtml: `
        <button class="btn btn-primary scanner-action-wide scanner-action-rescan" data-scan-result-action="rescan">Scanner le suivant</button>
        <button class="btn btn-success" data-scan-result-action="add">＋ Ajouter</button>
        <button class="btn btn-danger" data-scan-result-action="remove">− Sortir</button>
        <button class="btn btn-secondary" data-scan-result-action="product">Consulter</button>
        <button class="btn btn-secondary" data-scan-result-action="locate">Localiser</button>`
    });

    await this._setupTerrainPanel(p);
    if (token !== this._scanRequestToken) return false;
    this._updateTerrainPanelVisibility();

    this.recentScans.unshift({
      code: searchTerm,
      productId: p.id || null,
      name: p.name || '',
      sku: p.sku || '',
      image: this._productImageUrl(p),
      variant: this._variantLabel(p),
      found: true,
      time: Date.now()
    });
    if (this.recentScans.length > 20) this.recentScans.pop();
    this._saveRecentScans();
    this.renderRecent();
    this._updateManualClear();
    this._setScannerStatus('success', 'Produit trouvé — choisissez une action');
    return true;
  },

  manualSearch() {
    this._primeScannerFeedback();
    const inp = document.getElementById("manualCode");
    const code = (inp ? inp.value : "").trim();
    if (code) this.onScan(code);
    this._updateManualClear();
  },

  async scanAgain(restartCamera = false) {
    if (this._restartPromise) return this._restartPromise;
    this._restartPromise = (async () => {
      this._scanProcessing = false;
      ++this._scanRequestToken;
      this._activeProduct = null;
      this._activeVariantChildren = [];
      this._activeAmbiguousProducts = [];
      this._activeRawCode = '';
      this._lastCode = null;
      this._lastTime = 0;
      this._terrainPid = null;
      this._terrainP = null;
      this._updateTerrainPanelVisibility();
      if (this._cameraRunning) await this.stopCamera(true);
      await this._closeResultPanel('rescan', { hide: true });
      await new Promise(resolve => window.setTimeout(resolve, 40));
      const card = document.getElementById('scanResultCard');
      if (card) { card.style.display = 'none'; card.hidden = true; card.setAttribute('aria-hidden', 'true'); }
      const title = document.getElementById('scanResultTitle');
      const text = document.getElementById('scanResultText');
      const actions = document.getElementById('scanResultActions');
      if (title) title.textContent = 'Scanner prêt';
      if (text) text.textContent = 'Scannez un nouveau code.';
      if (actions) actions.innerHTML = '';
      this._setScannerStatus('idle', restartCamera ? 'Redémarrage du scanner…' : 'Caméra inactive');
      if (restartCamera) return this.startBarcodeCamera();
      return true;
    })();
    try { return await this._restartPromise; }
    finally { this._restartPromise = null; }
  },

  _parseIdList(raw){
    if (Array.isArray(raw)) return raw.map(v => String(v)).filter(Boolean);
    if (raw == null || raw === '') return [];
    try {
      const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
      return Array.isArray(parsed) ? parsed.map(v => String(v)).filter(Boolean) : [];
    } catch (_) { return []; }
  },

  _locationBucketQty(stockRow, locKey){
    if (!stockRow || !locKey || locKey === 'default') return parseFloat(stockRow && stockRow.quantity) || 0;
    const locs = stockRow.locations && typeof stockRow.locations === 'object' ? stockRow.locations : null;
    if (!locs) return parseFloat(stockRow.quantity) || 0;
    const direct = locs[String(locKey)];
    if (direct && typeof direct === 'object') return parseFloat(direct.quantity) || 0;
    return parseFloat(stockRow.quantity) || 0;
  },

  _renderTerrainStockInfo(p, stockRows){
    const box = document.getElementById('tsStockInfo');
    const whSel = document.getElementById('tsWh');
    const locSel = document.getElementById('tsLoc');
    if (!box || !whSel) return;
    const wid = String(whSel.value || '');
    const row = (Array.isArray(stockRows) ? stockRows : []).find(r => String(r.warehouseId) === wid);
    if (!wid) {
      box.textContent = 'Sélectionnez un magasin pour voir le stock et le rangement disponible.';
      box.className = 'terrain-stock-info';
      return;
    }
    const unit = this._getBaseUnit(p);
    const total = parseFloat(row && row.quantity) || 0;
    const locKey = locSel ? String(locSel.value || 'default') : 'default';
    const locQty = this._locationBucketQty(row, locKey);
    const min = parseFloat(row && (row.minStock ?? row.safetyStock)) || 0;
    const status = total <= 0 ? 'empty' : (min > 0 && total <= min ? 'low' : 'ok');
    box.className = 'terrain-stock-info terrain-stock-info--' + status;
    const locationLabel = locSel && locSel.selectedOptions && locSel.selectedOptions[0] ? locSel.selectedOptions[0].textContent.trim() : 'Défaut';
    box.innerHTML = `Stock magasin : <strong>${this._escapeHtml(total)} ${this._escapeHtml(unit)}</strong>` +
      (locKey && locKey !== 'default' ? ` · Rangement ${this._escapeHtml(locationLabel)} : <strong>${this._escapeHtml(locQty)} ${this._escapeHtml(unit)}</strong>` : '') +
      (min > 0 ? ` · Seuil : ${this._escapeHtml(min)} ${this._escapeHtml(unit)}` : '');
  },

  async _setupTerrainPanel(p){
    const panel = document.getElementById('terrainPanel');
    if (!panel || !p) return;

    let whs = [];
    let stockRows = [];
    try {
      const data = await Promise.all([Api.warehouses(), Api.stock(p.id).catch(() => [])]);
      whs = Array.isArray(data[0]) ? data[0] : [];
      stockRows = Array.isArray(data[1]) ? data[1] : [];
    } catch (_) {
      whs = await this._getWarehouses();
      stockRows = [];
    }

    const stockWhIds = new Set(stockRows.map(r => String(r.warehouseId)).filter(Boolean));
    const allowedWh = this._parseIdList(p.allowedWarehouses);
    let eligibleWhs = whs.filter(w => {
      const wid = String(w.id);
      return !allowedWh.length || allowedWh.includes(wid);
    });
    if (stockWhIds.size) eligibleWhs = eligibleWhs.filter(w => stockWhIds.has(String(w.id)));
    if (!eligibleWhs.length) eligibleWhs = whs;

    const tsWh = document.getElementById('tsWh');
    if (tsWh) {
      const previous = tsWh.value;
      tsWh.innerHTML = eligibleWhs.map(w => {
        const row = stockRows.find(r => String(r.warehouseId) === String(w.id));
        const qty = row ? (parseFloat(row.quantity) || 0) : 0;
        const suffix = stockWhIds.size ? ` — ${qty} ${this._getBaseUnit(p)}` : '';
        return `<option value="${this._esc(String(w.id))}">${this._esc(w.name || ('#'+w.id))}${this._esc(suffix)}</option>`;
      }).join('');
      const preferred = previous || String(p.defaultWarehouse || p.warehouseId || '');
      if (preferred && [...tsWh.options].some(o => String(o.value) === String(preferred))) tsWh.value = preferred;
    }

    // Units
    const tsUnit = document.getElementById('tsUnit');
    if (tsUnit) tsUnit.innerHTML = this._buildUnitOptions(p);

    // Locations per warehouse
    const loadLocs = async () => {
      const wid = tsWh ? tsWh.value : '';
      const tsLoc = document.getElementById('tsLoc');
      if (!tsLoc) return;
      try {
        const locs = await Api.locations(wid);
        const arr = Array.isArray(locs) ? locs : (locs && locs.items ? locs.items : []);
        const filtered = (arr || []).filter(l => String(l.warehouseId||'') === String(wid));
        const list = [{ key:'default', label:'Défaut / non rangé' }].concat(filtered.map(l => ({
          key: l.id || l.key || 'default',
          label: [l.code, l.label || l.name].filter(Boolean).join(' — ') || l.code || l.label || l.name || l.id
        })));
        tsLoc.innerHTML = list.map(l => `<option value="${this._esc(String(l.key))}">${this._esc(String(l.label))}</option>`).join('');
      } catch(_){
        tsLoc.innerHTML = `<option value="default">Défaut / non rangé</option>`;
      }
      this._renderTerrainStockInfo(p, stockRows);
    };
    if (tsWh) tsWh.onchange = () => loadLocs();
    await loadLocs();
    const tsLoc = document.getElementById('tsLoc');
    if (tsLoc) tsLoc.onchange = () => this._renderTerrainStockInfo(p, stockRows);

    // Qty quick buttons
    const qInp = document.getElementById('tsQty');
    const bump = (delta) => {
      if (!qInp) return;
      const v = parseFloat(String(qInp.value || '0').replace(',', '.')) || 0;
      const nv = Math.max(0, v + delta);
      qInp.value = String(nv);
      qInp.dispatchEvent(new Event('input', { bubbles: true }));
    };
    const m1 = document.getElementById('tsMinus1');
    const p1 = document.getElementById('tsPlus1');
    const p5 = document.getElementById('tsPlus5');
    if (m1) m1.onclick = () => bump(-1);
    if (p1) p1.onclick = () => bump(+1);
    if (p5) p5.onclick = () => bump(+5);

    // Actions
    const doAdd = document.getElementById('tsDoAdd');
    const doRem = document.getElementById('tsDoRemove');
    const doQr = document.getElementById('tsQr');
    if (doQr) doQr.onclick = () => this.showInternalQR(p);
    if (doAdd) doAdd.onclick = () => this._terrainMove('add', doAdd);
    if (doRem) doRem.onclick = () => this._terrainMove('remove', doRem);

    this._refreshOfflineUi();
  },

  async _terrainMove(type, btn){
    const p = this._terrainP;
    const pid = this._terrainPid;
    if (!p || !pid) return;
    const wid = (document.getElementById('tsWh') || {}).value;
    const locKey = (document.getElementById('tsLoc') || {}).value || 'default';
    const unit = (document.getElementById('tsUnit') || {}).value || this._getBaseUnit(p);
    const qtyRaw = ((document.getElementById('tsQty') || {}).value || '').trim().replace(',', '.');
    const qty = qtyRaw === '' ? 0 : (parseFloat(qtyRaw) || 0);

    let availableQty = -1;
    if (type === 'remove' && wid) {
      try {
        const stockRows = await Api.stock(pid);
        const row = (Array.isArray(stockRows) ? stockRows : []).find(r => String(r.warehouseId) === String(wid));
        availableQty = row ? (parseFloat(row.quantity) || 0) : 0;
      } catch (_) { availableQty = -1; }
    }

    const payload = this._buildScannerMovementPayload({
      product: p,
      pid,
      type,
      warehouseId: wid,
      locationId: locKey,
      unit,
      quantity: qty,
      note: 'Scan terrain'
    });

    const result = await this._submitScannerMovement(payload, p, {
      button: btn,
      availableQty,
      loadingLabel: type === 'add' ? 'Ajout…' : 'Sortie…'
    });

    if (result.ok) {
      const qInp = document.getElementById('tsQty');
      if (qInp) qInp.value = '';
    }

    if (result.ok && !result.queued) {
      try {
        const stockSummary = await this._getStockSummary(pid);
        const children = await this._getVariantChildren(p);
        const resultText = document.getElementById('scanResultText');
        if (resultText) resultText.innerHTML = this._renderProductResultHtml(p, p.sku || p.barcode || p.id, stockSummary, children);
        await this._setupTerrainPanel(p);
      } catch (_) {}
    }
  },

  async showInternalQR(p){
    const payload = JSON.stringify({ type:'product', id:p.id, sku:p.sku||'', barcode:p.barcode||'', name:p.name||'' });
    const html = `
      <div class="qr-internal-wrap">
        <div id="smQrBox"></div>
        <div class="qr-internal-label">
          QR interne MageStock — ${this._esc(p.sku || p.barcode || p.name || '')}
        </div>
      </div>
    `;
    Modal.show('QR interne', html, [
      { label:'Imprimer', cls:'btn-primary',   onclick: () => { try { window.print(); } catch(_){} } },
      { label:'Fermer',   cls:'btn-secondary', onclick: () => Modal.close() },
    ], 'info');

    if (typeof qrcode === 'undefined') {
      await this._loadScriptOnce('libs/qrcode.js');
    }
    try {
      const el = document.getElementById('smQrBox');
      if (!el) return;
      const qr = qrcode(0, 'M');
      qr.addData(payload);
      qr.make();
      el.innerHTML = qr.createImgTag(5);
      const img = el.querySelector('img');
      if (img) { img.style.cssText = 'width:100%;height:100%;display:block;object-fit:contain'; }
    } catch(e){
      console.error(e);
      toast('Impossible de générer le QR.', 'error');
    }
  },

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


  // ─────────────────────────────────────────────────────────────
  // Lot 2 Scanner — payload commun mouvements terrain / quick move
  // ─────────────────────────────────────────────────────────────
  _setButtonLoading(btn, loading, label) {
    if (!btn) return;
    if (loading) {
      btn.dataset.prevText = btn.textContent;
      btn.disabled = true;
      btn.classList.add('is-loading');
      if (label) btn.textContent = label;
    } else {
      btn.disabled = false;
      btn.classList.remove('is-loading');
      if (btn.dataset.prevText) btn.textContent = btn.dataset.prevText;
      delete btn.dataset.prevText;
    }
  },
  _buildScannerMovementPayload(options) {
    const opt = options || {};
    const pid = opt.pid || (opt.product && opt.product.id) || this._terrainPid;
    const p = opt.product || this._terrainP || this._prodCache[String(pid)] || {};
    return {
      productId: parseInt(pid, 10),
      warehouseId: parseInt(opt.warehouseId, 10),
      type: opt.type,
      quantity: parseFloat(opt.quantity) || 0,
      unit: opt.unit || this._getBaseUnit(p),
      locationId: String(opt.locationId || ''),
      lotNumber: opt.lotNumber || null,
      expiryDate: opt.expiryDate || null,
      unitCost: opt.type === 'add' ? (parseFloat(opt.unitCost) || 0) : 0,
      reason: opt.reason || '',
      site: opt.site || '',
      operator: opt.operator || '',
      note: opt.note || ''
    };
  },
  _validateScannerMovement(payload, product, options) {
    const errors = [];
    const p = product || {};
    const opt = options || {};
    if (!payload || !payload.productId) errors.push('Produit invalide.');
    if (!payload || !payload.warehouseId) errors.push('Magasin obligatoire.');
    if (!payload || !['add', 'remove'].includes(payload.type)) errors.push('Type de mouvement invalide.');
    if (!payload || !(payload.quantity > 0)) errors.push('Quantité obligatoire et supérieure à 0.');
    if (p.trackLot && !payload.lotNumber) errors.push('Numéro de lot obligatoire pour ce produit.');
    if (p.trackExpiry && !payload.expiryDate) errors.push('Date de péremption obligatoire pour ce produit.');
    if (payload && payload.type === 'remove' && typeof opt.availableQty === 'number' && opt.availableQty >= 0 && payload.quantity > opt.availableQty) {
      errors.push(`Stock insuffisant : ${opt.availableQty} disponible.`);
    }
    return errors;
  },
  async _submitScannerMovement(payload, product, options) {
    const opt = options || {};
    const errors = this._validateScannerMovement(payload, product, opt);
    if (errors.length) {
      toast(errors[0], 'error', 7000);
      return { ok: false, queued: false, errors };
    }

    if (this._movementSubmitting && !opt.allowParallel) return { ok: false, queued: false, errors: ['Mouvement déjà en cours.'] };
    this._movementSubmitting = true;
    this._setButtonLoading(opt.button, true, opt.loadingLabel || 'Enregistrement…');

    try {
      if (!navigator.onLine) {
        this._queueMovement(payload, product);
        toast('Hors ligne : mouvement ajouté à la boîte d’envoi.', 'warning');
        return { ok: true, queued: true };
      }
      await Api.mvAdd(payload);
      toast(payload.type === 'add' ? 'Ajout de stock enregistré.' : 'Sortie de stock enregistrée.', 'success');
      this._refreshOfflineUi();
      return { ok: true, queued: false };
    } catch(e) {
      const msg = this._movementErrorMessage(e);
      if (this._isNetworkError(e)) {
        this._queueMovement(payload, product);
        toast('Erreur réseau : mouvement ajouté à la boîte d’envoi.', 'warning');
        return { ok: true, queued: true };
      }
      toast(msg, 'error', 7000);
      return { ok: false, queued: false, errors: [msg] };
    } finally {
      this._movementSubmitting = false;
      this._setButtonLoading(opt.button, false);
    }
  },

  // ─────────────────────────────────────────────────────────────
  // Quick move (kept compatible with existing module)
  // ─────────────────────────────────────────────────────────────
  async _getProductById(pid) {
    const k = String(pid);
    if (this._prodCache[k]) return this._prodCache[k];
    const all = await Api.products();
    const p = all.find(x => String(x.id) === k);
    if (p) this._prodCache[k] = p;
    return p || null;
  },

  _getBaseUnit(p) {
    const u = (p && p.packBaseUnit ? String(p.packBaseUnit) : "").trim();
    if (u) return u;
    const legacy = (p && p.unitType ? String(p.unitType) : "piece");
    if (legacy === "liter") return "litre";
    return legacy || "piece";
  },

  _buildUnitOptions(p) {
    const units = [
      { key: "piece", label: "pièce" },
      { key: "carton", label: "carton" },
      { key: "palette", label: "palette" },
      { key: "bidon", label: "bidon" },
      { key: "fut", label: "fût" },
      { key: "litre", label: "litre" },
      { key: "kg", label: "kg" }
    ];

    const base = this._getBaseUnit(p);
    const hasCarton = (parseFloat(p && p.packCartonQty) || 0) > 0;
    const hasPalette = (parseFloat(p && p.packPaletteQty) || 0) > 0;
    const hasBidon = (parseFloat(p && p.packBidonQty) || 0) > 0;
    const hasFut = (parseFloat(p && p.packFutQty) || 0) > 0;

    const allowed = new Set([base]);
    if (base === "piece") allowed.add("piece");
    if (hasCarton) allowed.add("carton");
    if (hasPalette) allowed.add("palette");
    if (hasBidon) allowed.add("bidon");
    if (hasFut) allowed.add("fut");

    return units
      .filter(u => allowed.has(u.key))
      .map(u => `<option value="${u.key}">${u.label}</option>`)
      .join("");
  },

  _getUnitCostPrefill(p, unit) {
    const map = (p && typeof p.costByUnit === "object" && p.costByUnit) ? p.costByUnit : {};
    const v = map ? map[unit] : null;
    if (v != null && v !== "" && !Number.isNaN(parseFloat(v))) return parseFloat(v);
    const base = this._getBaseUnit(p);
    if (unit === base && !Number.isNaN(parseFloat(p && p.cump))) return parseFloat(p.cump);
    if (unit === base && !Number.isNaN(parseFloat(p && p.cost))) return parseFloat(p.cost);
    if (!Number.isNaN(parseFloat(p && p.cost))) return parseFloat(p.cost);
    return 0;
  },

  // ─────────────────────────────────────────────────────────────
  // quickMove — Ajout/Sortie rapide depuis le scanner
  // Logique : produit connu (scanné) → choisir le magasin
  // (avec stock affiché) → puis quantité + détails
  // ─────────────────────────────────────────────────────────────
  async quickMove(pid, type) {
    const p = await this._getProductById(pid);
    if (!p) { toast("Produit introuvable", "error"); return; }

    const [stockAll, whs] = await Promise.all([Api.stock(pid), Api.warehouses()]);
    const baseUnit = this._getBaseUnit(p);
    const needsLot = !!p.trackLot;
    const needsExp = !!p.trackExpiry;

    // ── Construction des options magasin avec stock ──────────────
    // Filtrer selon la correspondance réelle produit ↔ magasin : le produit doit
    // posséder une ligne stock dans le magasin. allowedWarehouses reste une règle
    // d'autorisation, mais ne suffit plus à afficher le produit comme intégré.
    let allowedWh = p.allowedWarehouses;
    if (typeof allowedWh === 'string') try { allowedWh = JSON.parse(allowedWh||'[]'); } catch(e){ allowedWh=[]; }
    const allowedWhStrs = Array.isArray(allowedWh) ? allowedWh.map(String) : [];

    const stockMap = {};
    const stockRowWhs = new Set();
    (stockAll || []).forEach(s => {
      stockMap[String(s.warehouseId)] = Number(s.quantity) || 0;
      stockRowWhs.add(String(s.warehouseId));
    });

    const eligibleAllWhs = whs.filter(w => {
      const wid = String(w.id);
      if(!stockRowWhs.has(wid)) return false;
      return allowedWhStrs.length ? allowedWhStrs.includes(wid) : true;
    });

    let eligibleWhs = eligibleAllWhs;
    if (type === 'remove') {
      eligibleWhs = eligibleAllWhs.filter(w => (stockMap[String(w.id)] || 0) > 0);
    }

    const whOpts = eligibleWhs.map(w => {
      const qty = stockMap[String(w.id)] || 0;
      const stockLabel = type === 'remove'
        ? ` — stock : ${qty} ${baseUnit}`
        : qty > 0 ? ` — stock actuel : ${qty} ${baseUnit}` : ' — aucun stock';
      return `<option value="${w.id}">${this._esc(w.name)}${stockLabel}</option>`;
    }).join('');

    if (eligibleWhs.length === 0) {
      toast(type === 'remove'
        ? `Aucun stock disponible pour "${p.name}"`
        : `Ce produit n'est intégré dans aucun magasin disponible. Ajoutez-le d'abord depuis la fiche produit.`, 'warning');
      return;
    }

    // ── Unités disponibles pour ce produit ───────────────────────
    const unitOpts = this._buildUnitOptions(p);
    const unitCostPrefill = this._getUnitCostPrefill(p, baseUnit);

    // ── Blocs optionnels selon le type ───────────────────────────
    const costBlock = type === 'add' ? `
      <div class="sq-field-group">
        <label class="lbl">Prix d'achat (pour l'unité choisie) — optionnel</label>
        <input type="number" class="inp" id="sqUnitCost" min="0" step="0.0001" value="${unitCostPrefill}">
      </div>` : '';

    const traceBlock = (needsLot || needsExp) ? `
      <div class="sq-sep"></div>
      <div class="sq-section-title">📋 Traçabilité</div>
      <div class="sq-field-group">
        <label class="lbl">Numéro de lot${needsLot ? ' *' : ''}</label>
        <input type="text" class="inp" id="sqLot" placeholder="Ex: LOT-2026-001">
      </div>
      <div class="sq-field-group">
        <label class="lbl">DLUO / Péremption${needsExp ? ' *' : ''}</label>
        <input type="date" class="inp" id="sqExp">
      </div>` : '';

    const removeBlock = type === 'remove' ? `
      <div class="sq-sep"></div>
      <div class="sq-section-title">📋 Destination</div>
      <div class="sq-field-group">
        <label class="lbl">Raison / Destination</label>
        <select class="sel" id="sqReason">
          <option value="Chantier">Chantier</option>
          <option value="Réparation">Réparation</option>
          <option value="Livraison client">Livraison client</option>
          <option value="Consommable">Consommable</option>
          <option value="Autre">Autre</option>
        </select>
      </div>
      <div class="sq-field-group">
        <label class="lbl">Chantier / Lieu</label>
        <input type="text" class="inp" id="sqSite" placeholder="Ex: Chantier Rue Dupont…">
      </div>
      <div class="sq-field-group">
        <label class="lbl">Opérateur</label>
        <input type="text" class="inp" id="sqOper" placeholder="Nom prénom…">
      </div>` : '';

    const noteBlock = `
      <div class="sq-field-group">
        <label class="lbl">Note</label>
        <input type="text" class="inp" id="sqNote">
      </div>`;

    // ── Formulaire en cascade ────────────────────────────────────
    const html = `
<div class="sq-form">

  <!-- Produit — lecture seule (déjà connu via scan) -->
  <div class="sq-product-banner">
    <div class="sq-product-icon">${type === 'add' ? '＋' : '−'}</div>
    <div class="sq-product-info">
      <div class="sq-product-name">${this._esc(p.name || '')}</div>
      <div class="sq-product-sku">${this._esc(p.sku || p.barcode || '')}</div>
    </div>
  </div>

  <!-- ① Magasin -->
  <div class="sq-step" id="sqStepWh">
    <div class="sq-step-header">
      <span class="sq-step-num">1</span>
      <span class="sq-step-label">Magasin *</span>
      <span class="sq-stock-badge" id="sqStockBadge" style="display:none"></span>
    </div>
    <select class="sel" id="sqWh">
      <option value="">— Choisir un magasin —</option>
      ${whOpts}
    </select>
  </div>

  <!-- ② Quantité & Conditionnement (activé après magasin) -->
  <div class="sq-step sq-step-locked" id="sqStepQty">
    <div class="sq-step-header">
      <span class="sq-step-num">2</span>
      <span class="sq-step-label">Quantité & Conditionnement</span>
    </div>
    <div class="sq-qty-row">
      <div>
        <label class="lbl">Unité</label>
        <select class="sel" id="sqUnit" disabled>${unitOpts}</select>
      </div>
      <div>
        <label class="lbl">Quantité *</label>
        <input type="number" class="inp" id="sqQty" min="0.01" step="0.01" value="" placeholder="Quantité" inputmode="decimal" disabled>
      </div>
    </div>
    <small class="sq-conv-hint">Conversion automatique vers l'unité de base (${baseUnit}) lors de l'enregistrement.</small>
    ${costBlock}
  </div>

  <!-- ③ Détails (traçabilité + destination + note) -->
  <div class="sq-step sq-step-locked" id="sqStepDetails">
    <div class="sq-step-header">
      <span class="sq-step-num">3</span>
      <span class="sq-step-label">Détails</span>
    </div>
    ${traceBlock}
    ${removeBlock}
    ${noteBlock}
  </div>

</div>`;

    Modal.show(
      (type === 'add' ? 'Ajout de stock — ' : 'Sortie de stock — ') + this._esc(p.name || ''),
      html,
      [
        { label: 'Annuler', cls: 'btn-secondary', onclick: () => Modal.close() },
        { label: 'Valider', cls: type === 'add' ? 'btn-success' : 'btn-danger', onclick: () => this._submitQuickMove(p, pid, type, baseUnit, needsLot, needsExp) }
      ],
      type === 'add' ? 'success' : 'error'
    );

    // ── Injecter CSS (une seule fois) ────────────────────────────
    this._injectQuickMoveCss();

    // ── Cascade : sélection magasin → débloque qty ───────────────
    const whSel = document.getElementById('sqWh');
    const stepQty = document.getElementById('sqStepQty');
    const stepDetails = document.getElementById('sqStepDetails');
    const badge = document.getElementById('sqStockBadge');

    const unlock = () => {
      const wid = whSel ? whSel.value : '';
      if (!wid) return;

      // Badge stock
      const qty = stockMap[String(wid)] || 0;
      if (badge) {
        badge.style.display = '';
        if (type === 'remove') {
          badge.className = `sq-stock-badge ${qty <= 0 ? 'sq-stock-zero' : qty < (p.minStock || 5) ? 'sq-stock-low' : 'sq-stock-ok'}`;
          badge.textContent = `Stock disponible : ${qty} ${baseUnit}`;
        } else {
          badge.className = 'sq-stock-badge sq-stock-ok';
          badge.textContent = `Stock actuel : ${qty} ${baseUnit}`;
        }
      }

      // Débloquer étape 2
      if (stepQty) stepQty.classList.remove('sq-step-locked');
      ['sqUnit','sqQty'].forEach(id => {
        const el = document.getElementById(id);
        if (el) el.disabled = false;
      });

      // Unité par défaut
      const unitSel = document.getElementById('sqUnit');
      if (unitSel && [...unitSel.options].some(o => o.value === baseUnit)) {
        unitSel.value = baseUnit;
      }

      // Coût préfill si unité change
      if (unitSel) {
        unitSel.onchange = () => {
          const inp = document.getElementById('sqUnitCost');
          if (inp) inp.value = String(this._getUnitCostPrefill(p, unitSel.value) || 0);
        };
      }

      // Débloquer étape 3
      if (stepDetails) stepDetails.classList.remove('sq-step-locked');
      ['sqLot','sqExp','sqNote','sqSite','sqOper','sqReason'].forEach(id => {
        const el = document.getElementById(id);
        if (el && el.disabled !== undefined) el.disabled = false;
      });

      // Focus quantité
      setTimeout(() => { const q = document.getElementById('sqQty'); if (q) q.focus(); }, 60);
    };

    if (whSel) {
      whSel.addEventListener('change', unlock);
      // Si un seul magasin éligible : présélectionner et déverrouiller
      if (eligibleWhs.length === 1) {
        whSel.value = eligibleWhs[0].id;
        unlock();
      }
    }
  },

  // ─────────────────────────────────────────────────────────────
  // Soumission du quickMove
  // ─────────────────────────────────────────────────────────────
  async _submitQuickMove(p, pid, type, baseUnit, needsLot, needsExp) {
    const wid  = (document.getElementById('sqWh') || {}).value;
    const unit = (document.getElementById('sqUnit') || {}).value || baseUnit;
    const qtyRaw = ((document.getElementById('sqQty') || {}).value || '').trim().replace(',', '.');
    const qty  = qtyRaw === '' ? 0 : (parseFloat(qtyRaw) || 0);
    const lot    = ((document.getElementById('sqLot') || {}).value || '').trim();
    const expiry = (document.getElementById('sqExp') || {}).value || '';
    const unitCost = type === 'add'
      ? parseFloat((document.getElementById('sqUnitCost') || {}).value) || 0
      : 0;

    let availableQty = -1;
    if (type === 'remove' && wid) {
      try {
        const stockRows = await Api.stock(pid);
        const row = (Array.isArray(stockRows) ? stockRows : []).find(r => String(r.warehouseId) === String(wid));
        availableQty = row ? (parseFloat(row.quantity) || 0) : 0;
      } catch (_) { availableQty = -1; }
    }

    const payload = this._buildScannerMovementPayload({
      product: p,
      pid,
      type,
      warehouseId: wid,
      unit,
      quantity: qty,
      lotNumber: lot,
      expiryDate: expiry,
      unitCost,
      reason: (document.getElementById('sqReason') || {}).value || '',
      site: (document.getElementById('sqSite') || {}).value || '',
      operator: (document.getElementById('sqOper') || {}).value || '',
      note: (document.getElementById('sqNote') || {}).value || ''
    });

    const result = await this._submitScannerMovement(payload, p, {
      availableQty,
      loadingLabel: type === 'add' ? 'Ajout…' : 'Sortie…'
    });

    if (result.ok && !result.queued) {
      Modal.close();
      try {
        const stockSummary = await this._getStockSummary(pid);
        const children = await this._getVariantChildren(p);
        const resultText = document.getElementById('scanResultText');
        if (resultText) resultText.innerHTML = this._renderProductResultHtml(p, p.sku || p.barcode || p.id, stockSummary, children);
      } catch (_) {}
    }
  },

  // ─────────────────────────────────────────────────────────────
  // CSS du formulaire quickMove (injecté une seule fois)
  // ─────────────────────────────────────────────────────────────
  _injectQuickMoveCss() {
    if (document.getElementById('sq-modal-css')) return;
    const s = document.createElement('style');
    s.id = 'sq-modal-css';
    s.textContent = `
/* ── Produit banner (lecture seule) ────────────────────────── */
.sq-product-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: var(--bg-elevated);
  border-radius: var(--r-lg, 10px);
  border: 1px solid var(--border);
  margin-bottom: 2px;
}
.sq-product-icon {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--blue-pale, #eff6ff);
  color: var(--blue, #3b82f6);
  display: flex; align-items: center; justify-content: center;
  font-size: 18px; font-weight: 700; flex-shrink: 0;
}
.sq-product-name { font-weight: 600; font-size: 15px; color: var(--text-primary); }
.sq-product-sku  { font-size: 12px; color: var(--text-muted); font-family: monospace; }

/* ── Steps ──────────────────────────────────────────────────── */
.sq-form { display: flex; flex-direction: column; gap: 6px; }
.sq-step {
  border: 1px solid var(--border);
  border-radius: var(--r-lg, 10px);
  padding: 14px 16px;
  background: var(--bg-card);
  transition: opacity .2s, background .2s;
}
.sq-step-locked {
  opacity: .42;
  pointer-events: none;
  background: var(--bg-elevated);
}
.sq-step-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  flex-wrap: wrap;
}
.sq-step-num {
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--blue, #3b82f6);
  color: #fff;
  font-size: 12px; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.sq-step-locked .sq-step-num { background: var(--text-muted, #94a3b8); }
.sq-step-label { font-size: 14px; font-weight: 600; color: var(--text-primary); flex: 1; }

/* ── Stock badge ─────────────────────────────────────────────── */
.sq-stock-badge {
  font-size: 12px; font-weight: 600;
  padding: 3px 10px; border-radius: 999px;
}
.sq-stock-ok   { background: var(--green-pale, #dcfce7); color: var(--green, #16a34a); }
.sq-stock-low  { background: var(--yellow-pale, #fef9c3); color: #b45309; }
.sq-stock-zero { background: var(--red-pale, #fee2e2);   color: var(--red, #dc2626); }

/* ── Qty grid ────────────────────────────────────────────────── */
.sq-qty-row {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 12px;
}
@media (max-width: 400px) {
  .sq-qty-row { grid-template-columns: 1fr; }
}
.sq-conv-hint {
  display: block; font-size: 12px; color: var(--text-muted);
  margin-top: 6px; margin-bottom: 4px;
}

/* ── Misc ─────────────────────────────────────────────────────── */
.sq-sep { height: 1px; background: var(--border); margin: 10px 0 4px; }
.sq-section-title { font-size: 13px; font-weight: 700; color: var(--text-secondary); margin-bottom: 8px; }
.sq-field-group { margin-top: 10px; }
.sq-field-group .lbl { display: block; margin-bottom: 4px; }
`;
    document.head.appendChild(s);
  },


  _bindScannedProductPreview(p) {
    const trigger = document.querySelector('[data-scan-open-product]');
    if (!trigger || trigger._scanProductOpenBound) return;
    trigger._scanProductOpenBound = true;
    trigger.addEventListener('click', (event) => {
      event.preventDefault();
      this._closeResultPanel('product-card');
      window.setTimeout(() => this.openProductModal(p), 210);
    });
  },

  async _ensureProductsModuleReady() {
    if (typeof ProdPage === "undefined") {
      await this._loadScriptOnce((window.Api && Api.url ? Api.url('assets/js/pages/products.js') : 'assets/js/pages/products.js'));
    }

    if (typeof ProdPage === "undefined" || !ProdPage || typeof ProdPage.openModal !== "function") {
      throw new Error("Module produits indisponible");
    }

    const loaders = [];
    if (!Array.isArray(ProdPage.cats) || !ProdPage.cats.length) loaders.push(Api.categories().then(v => { ProdPage.cats = Array.isArray(v) ? v : []; }));
    if (!Array.isArray(ProdPage.whs) || !ProdPage.whs.length) loaders.push(Api.warehouses().then(v => { ProdPage.whs = Array.isArray(v) ? v : []; }));
    if (!Array.isArray(ProdPage.suppliers) || !ProdPage.suppliers.length) loaders.push(Api.suppliers().then(v => { ProdPage.suppliers = Array.isArray(v) ? v : []; }));
    if (!Array.isArray(ProdPage.stock)) loaders.push(Api.stock().then(v => { ProdPage.stock = Array.isArray(v) ? v : []; }).catch(() => { ProdPage.stock = []; }));

    if (loaders.length) await Promise.all(loaders);
  },

  async openProductModal(p) {
    if (!p) return;
    const productId = p.id || p.productId;
    if (!productId) return;

    try {
      await this._ensureProductsModuleReady();
      await ProdPage.openModal(String(productId));
      toast("Fiche produit ouverte", "success");
    } catch (e) {
      console.error(e);
      toast("Ouverture de la fiche indisponible, redirection vers Produits", "warning");
      const q = p.sku || p.barcode || p.name || productId;
      window.location.href = 'products?q=' + encodeURIComponent(q);
    }
  },

  openProductPage(p) {
    return this.openProductModal(p);
  },

  // Create-from-scan preserved (loads products page module)
  async createFromScan(code) {
    if (typeof ProdPage === "undefined") {
      try { await this._loadScriptOnce((window.Api && Api.url ? Api.url('assets/js/pages/products.js') : 'assets/js/pages/products.js')); }
      catch (_) { toast("Impossible de charger le module produits", "error"); return; }
    }
    try {
      if (!Array.isArray(ProdPage.cats) || !Array.isArray(ProdPage.whs) || !Array.isArray(ProdPage.suppliers)) {
        const [cats, whs, suppliers] = await Promise.all([Api.categories(), Api.warehouses(), Api.suppliers()]);
        ProdPage.cats = cats; ProdPage.whs = whs; ProdPage.suppliers = suppliers;
      }
      await ProdPage.openModal();
      setTimeout(() => {
        const bc = document.getElementById("pmBarcode");
        if (bc) bc.value = code;
        const name = document.getElementById("pmName");
        if (name) name.focus();
      }, 120);
    } catch (e) {
      console.error(e);
      toast(e.message || "Erreur ouverture création produit", "error");
    }
  },

  _loadScriptOnce(src) {
    return new Promise((resolve, reject) => {
      if (document.querySelector('script[data-sm-dyn="' + src + '"]')) return resolve();
      const s = document.createElement("script");
      s.src = src;
      s.async = true;
      s.dataset.smDyn = src;
      s.onload = () => resolve();
      s.onerror = () => reject(new Error("load failed"));
      document.head.appendChild(s);
    });
  },

  async openRecentScan(index) {
    const s = this.recentScans && this.recentScans[index] ? this.recentScans[index] : null;
    if (!s) return;

    if (s.found === false) {
      this.onScan(s.code || '');
      return;
    }

    if (s.productId) {
      this.openProductModal({
        id: s.productId,
        name: s.name || '',
        sku: s.sku || '',
        barcode: s.code || '',
        image: s.image || '',
        variantLabel: s.variant || ''
      });
      return;
    }

    // Compatibilité avec les anciens historiques localStorage, créés avant v2.3.10.
    // On tente de retrouver le produit par code/SKU/EAN, puis on ouvre la fiche.
    try {
      const code = (s.code || s.sku || '').trim();
      if (!code) throw new Error('Code absent');
      const rows = await Api.products(code);
      const arr = Array.isArray(rows) ? rows : [];
      const p = arr.find(x =>
        String(x.barcode || '') === String(code) ||
        String(x.sku || '') === String(code) ||
        (Array.isArray(x.eans) && x.eans.some(e => String(e) === String(code)))
      ) || arr[0];
      if (!p) throw new Error('Produit introuvable');

      s.productId = p.id || null;
      s.name = p.name || s.name || '';
      s.sku = p.sku || s.sku || '';
      s.image = this._productImageUrl(p) || s.image || '';
      s.variant = this._variantLabel(p) || s.variant || '';
      this._saveRecentScans();
      this.renderRecent();
      this.openProductModal(p);
    } catch (e) {
      console.warn(e);
      this.onScan(s.code || '');
    }
  },

  renderRecent() {
    this._refreshRecentUi();
    const el = document.getElementById("recentScans");
    const clear = document.getElementById("btnClearRecent");
    if (!el) return;
    if (clear) clear.style.display = this.recentScans.length ? '' : 'none';
    if (!this.recentScans.length) { el.innerHTML = '<div class="recent-empty">Aucun scan récent</div>'; return; }
    el.innerHTML = this.recentScans.map((s, i) => {
      const d = new Date(s.time);
      const img = s.image ? `<img src="${this._escapeHtml(s.image)}" alt="" loading="lazy">` : '<span>⌗</span>';
      const status = s.found === false ? '<em>Non trouvé</em>' : (s.variant ? `<em>${this._escapeHtml(s.variant)}</em>` : '<em>Fiche produit</em>');
      const title = s.found === false ? 'Relancer cette recherche' : 'Ouvrir la fiche produit liée';
      return `<button type="button" class="recent-item ${s.found === false ? 'recent-notfound' : 'recent-product-link'}" data-recent-index="${i}" title="${title}" aria-label="${title} : ${this._escapeHtml(s.name || s.code || '')}">
        <span class="ri-thumb">${img}</span>
        <span class="ri-main"><strong>${this._escapeHtml(s.name || s.code)}</strong><small>${this._escapeHtml(s.code || '')} ${status}</small></span>
        <span class="ri-action">${s.found === false ? 'Relancer' : 'Fiche'}</span>
        <span class="ri-time">${d.getHours()}:${String(d.getMinutes()).padStart(2, "0")}</span>
      </button>`;
    }).join("");
    el.querySelectorAll('[data-recent-index]').forEach(btn => {
      btn.onclick = () => this.openRecentScan(parseInt(btn.dataset.recentIndex || '0', 10));
    });
  },

  // Sounds communs à tous les contextes scanner
  playSuccessSound() {
    if (window.MageStockScannerFeedback?.success) return window.MageStockScannerFeedback.success();
    return false;
  },

  playErrorSound() {
    if (window.MageStockScannerFeedback?.error) return window.MageStockScannerFeedback.error();
    return false;
  }

};
