/**
 * Fichier  : assets/js/pages/inventory.js
 * Auteur   : KREMER Régis
 * Version  : 2.6.98
 * Date     : 2026-07-20
 * Description : JS — Inventaires persistants : création guidée, reprise, comptage, revue, scanner et historique.
 *
 * Changelog :
 *   v2.2.0 — Version initiale
 *   v2.7.0 — Historique en page (plus de modal), titre obligatoire, impression PDF,
 *             vue détail par inventaire, opérateur enregistré
 *   v2.7.1 (2026-06-04) — Lot 1 inventaire: ScannerService, EAN multiples, cellule écart corrigée, quantités décimales.
 *   v2.7.2 (2026-06-04) — Fix caméra inventaire mobile: démarrage après rendu modale + host Scanner produit.
 *   v2.8.0 (2026-06-04) — Lot 2 inventaire: fiche produit après scan, scan continu, résumé de validation, UX mobile.
 *   v2.8.2 (2026-06-04) — Correctif caméra inventaire: host dédié, démarrage robuste, clavier mobile non automatique.
 *   v2.6.4 (2026-07-08) — Parcours terrain mobile/tablette : QR MageStock, actions rapides de comptage et cartes responsives.
 *   v2.6.8 (2026-07-08) — Impression inventaire via modèle A4 MageStock commun.
 *   v2.6.16 (2026-07-08) — Inventaires : images produit dans le détail et les impressions A4 premium.
 *   v2.6.17 (2026-07-08) — Correctif images inventaire : URL stable après filtres et impression sans page blanche.
 *   v2.6.18 (2026-07-08) — Images inventaire servies via proxy sécurisé et réhydratées après filtres/re-rendus.
 *   v2.6.22 (2026-07-08) — UX inventaires : filtres comptage/écarts, écran plus lisible et impression A4 stabilisée.
 *   v2.6.70 (2026-07-11) — I18N CORE LOCALE CURRENCY1 : dates d’inventaire selon la locale active.
 *   v2.6.74 (2026-07-17) — INVENTORY MOBILE FLOW1 : assistant 3 étapes, listes natives et actions terrain.
 *   v2.6.75 (2026-07-17) — INVENTORY RESUME REVIEW2 : brouillons serveur, reprise, modes complet/ciblé et revue obligatoire.
 *   v2.6.76 (2026-07-17) — INVENTORY FIELD RECETTE3 : recette multi-format, historique par magasin, autosauvegarde PWA robuste et conflits renforcés.
 *   v2.6.77 (2026-07-17) — INVENTORY FIELD DEVICE VALIDATION4 : clavier mobile, veille/changement d’application, rotation et reprise caméra.
 *   v2.6.90 (2026-07-20) — SCANNER CONSUMERS MIGRATION3 : migration vers le composant scanner commun, résolution par contexte Inventaire et suppression du cycle caméra local.
 *   v2.6.92 (2026-07-20) — SCANNER UX POLISH OPTIONAL1 : feedback sonore/haptique commun sans doublon.
 *   v2.6.98 (2026-07-20) — SCANNER TERRAIN CONFIRMATION1 : fiche de comptage alignée
 *                           sur le langage visuel du scanner commun et pictogramme SVG stable.
 */
const InvPage = {
    whs:          [],
    cats:         [],
    invData:      null,
    _scannerConsumer: null,
    _currentInv:  null,
    _scannerFoundIdx: null,
    itemStatusFilter: 'all',
    detailStatusFilter: 'all',
    newWizardStep: 1,
    mobileVisibleLimit: 30,
    _newDraft: null,
    _flowMedia: null,
    _draftKey: 'magestock_inventory_new_draft_v2',
    drafts: [],
    reviewData: null,
    _draftSaveTimer: null,
    _draftSavePromise: null,
    _backgroundSavePromise: null,
    _draftSaveQueued: false,
    _draftSaveFailed: false,
    _draftConflictOpen: false,
    _draftLocked: false,
    _localMutationVersion: 0,
    _recoveryPrefix: 'magestock_inventory_recovery_v1_',
    _viewportFrame: null,
    _viewportListenersReady: false,
    _viewportBaselineHeight: 0,
    _lastOrientationKey: '',

    // ══════════════════════════════════════════════════════════════
    // INIT
    // ══════════════════════════════════════════════════════════════
    async init() {
        const [whs, cats, prods, stockAll] = await Promise.all([
            Api.warehouses(), Api.categories(), Api.products(), Api.stock()
        ]);
        this.whs   = whs   || [];
        this.cats  = cats  || [];
        this.prods = prods || [];
        this.stockAll = stockAll || [];

        this._flowMedia = window.matchMedia ? window.matchMedia('(max-width: 1199px), (pointer: coarse)') : null;
        this._newDraft = this._restoreNewDraft();

        // Select magasin + catégories réellement disponibles dans le magasin choisi.
        const whSel = document.getElementById('invWhSelect');
        if (whSel) {
            whSel.innerHTML = '<option value="">Sélectionner un magasin</option>' +
                this.whs.map(w => `<option value="${w.id}">${this._esc(w.name)}</option>`).join('');
            if (this._newDraft?.warehouseId) whSel.value = String(this._newDraft.warehouseId);
        }
        const titleInput = document.getElementById('invTitle');
        if (titleInput && this._newDraft?.title) titleInput.value = this._newDraft.title;
        const restoredMode = this._newDraft?.mode === 'partial' ? 'partial' : 'full';
        const modeInput = document.querySelector(`input[name="invMode"][value="${restoredMode}"]`);
        if (modeInput) modeInput.checked = true;

        this._initCatChips();
        const refreshCats = (wid, selected = []) => {
            const cats = !wid ? [] : (window.StockCascade
                ? StockCascade.getWarehouseCategories(this.whs || [], wid, this.prods || [], this.cats || [], this.stockAll || [])
                : this.cats);
            this._buildCatChips(cats, selected);
            this._updateNewReview();
        };
        refreshCats(whSel?.value || '', this._newDraft?.categories || []);
        if (whSel) whSel.addEventListener('change', () => {
            refreshCats(whSel.value, []);
            this._saveNewDraft();
        });
        titleInput?.addEventListener('input', () => { this._saveNewDraft(); this._updateNewReview(); });
        document.querySelectorAll('input[name="invMode"]').forEach(input => {
            input.addEventListener('change', () => {
                this._syncModeUI();
                this._saveNewDraft();
                this._updateNewReview();
            });
        });
        this._syncModeUI();

        // Parcours de création.
        document.getElementById('btnNewInventory')?.addEventListener('click', () => this.openNewInventory());
        document.getElementById('btnBackToHome')?.addEventListener('click', () => this._showView('home'));
        document.getElementById('btnCancelNew')?.addEventListener('click', () => this._showView('home'));
        document.getElementById('btnInvWizardBack')?.addEventListener('click', () => this.goWizardStep(this.newWizardStep - 1));
        document.getElementById('btnInvWizardNext')?.addEventListener('click', () => this.nextWizardStep());
        document.querySelectorAll('.inv-wizard-step').forEach(btn => {
            btn.addEventListener('click', () => {
                const step = Number(btn.dataset.step) || 1;
                if (step <= this.newWizardStep) this.goWizardStep(step);
            });
        });
        document.getElementById('btnStartInv')?.addEventListener('click', () => this.startInventory());
        this._flowMedia?.addEventListener?.('change', () => this._syncWizardUI(false));
        this._syncWizardUI(false);

        // Boutons en cours
        document.getElementById('btnInvCancel')?.addEventListener('click', () => this.cancelInventory());
        document.getElementById('btnInvValidate')?.addEventListener('click', () => this.validateInventory());
        document.getElementById('btnInvScan')?.addEventListener('click', () => this.openScannerModal());
        document.getElementById('btnInvPrint')?.addEventListener('click', () => this.printCurrent());
        document.getElementById('btnInvManualAdd')?.addEventListener('click', () => this.searchAndFocus());
        document.getElementById('btnInvValidateAll')?.addEventListener('click', () => this.validateAll());
        document.getElementById('btnInvResetAll')?.addEventListener('click', () => this.resetAll());
        ['btnInvMobileScanTop','btnInvMobileScanDock'].forEach(id => document.getElementById(id)?.addEventListener('click', () => this.openScannerModal()));
        document.getElementById('btnInvMobileValidate')?.addEventListener('click', () => this.validateInventory());
        document.getElementById('btnInvMobilePrint')?.addEventListener('click', () => { this._closeMobileMore(); this.printCurrent(); });
        document.getElementById('btnInvMobileValidateAll')?.addEventListener('click', () => { this._closeMobileMore(); this.validateAll(); });
        document.getElementById('btnInvMobileResetAll')?.addEventListener('click', () => { this._closeMobileMore(); this.resetAll(); });
        document.getElementById('btnInvMobileCancel')?.addEventListener('click', () => { this._closeMobileMore(); this.cancelInventory(); });
        document.getElementById('btnInvLoadMore')?.addEventListener('click', () => this.loadMoreItems());
        document.getElementById('btnInvReloadConflict')?.addEventListener('click', () => {
            if (this.invData?.id) this.resumeDraft(this.invData.id);
        });
        document.querySelectorAll('.inv-status-filter').forEach(btn => {
            btn.addEventListener('click', () => {
                this.itemStatusFilter = btn.dataset.filter || 'all';
                this.mobileVisibleLimit = 30;
                this._syncFilterButtons('.inv-status-filter', this.itemStatusFilter);
                this.renderItems();
            });
        });
        document.querySelectorAll('.inv-detail-filter').forEach(btn => {
            btn.addEventListener('click', () => {
                this.detailStatusFilter = btn.dataset.filter || 'all';
                this._syncFilterButtons('.inv-detail-filter', this.detailStatusFilter);
                this.renderDetailLines();
            });
        });

        const scanInput = document.getElementById('invScanInput');
        if (scanInput) scanInput.onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); this.searchAndFocus(); } };

        // Revue et détail
        ['btnBackToCounting','btnReviewBackBottom'].forEach(id => document.getElementById(id)?.addEventListener('click', () => this.backToCounting()));
        document.getElementById('btnReviewPrint')?.addEventListener('click', () => this.printCurrent(true));
        document.getElementById('invReviewConfirm')?.addEventListener('change', () => this._syncApplyButton());
        document.getElementById('btnApplyInventory')?.addEventListener('click', () => this.confirmValidateInventory());
        document.getElementById('btnBackFromDetail')?.addEventListener('click', () => this._showView('home'));
        document.getElementById('btnDetailPrint')?.addEventListener('click', () => this.printDetail());

        this._initFieldDeviceSupport();
        document.addEventListener('visibilitychange', () => this._handleVisibilityChange());
        window.addEventListener('pagehide', () => {
            this._flushDraftForBackground();
        });
        window.addEventListener('pageshow', () => this._syncVisualViewport());
        window.addEventListener('online', () => {
            if (!this.invData?.id || this._draftLocked) return;
            this._setSaveStatus('saving');
            this._saveDraftNow(true).catch(() => {});
        });
        window.addEventListener('offline', () => {
            if (!this.invData?.id) return;
            this._writeLocalRecovery();
            this._setSaveStatus('offline');
        });
        // Charger les brouillons et l'historique.
        await Promise.all([this.loadDrafts(), this.loadHistory()]);
    },

    _initFieldDeviceSupport() {
        if (this._viewportListenersReady) return;
        this._viewportListenersReady = true;
        const sync = () => {
            if (this._viewportFrame) cancelAnimationFrame(this._viewportFrame);
            this._viewportFrame = requestAnimationFrame(() => {
                this._viewportFrame = null;
                this._syncVisualViewport();
            });
        };
        this._lastOrientationKey = this._orientationKey();
        window.visualViewport?.addEventListener?.('resize', sync, { passive: true });
        window.visualViewport?.addEventListener?.('scroll', sync, { passive: true });
        window.addEventListener('resize', sync, { passive: true });
        document.addEventListener('focusin', event => {
            sync();
            if (!this._isInventoryFormControl(event.target) || !this._isCompactFlow()) return;
            setTimeout(() => {
                try { event.target.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); } catch (_) {}
            }, 280);
        }, true);
        document.addEventListener('focusout', () => setTimeout(sync, 160), true);
        const onOrientation = () => this._handleOrientationChange();
        window.addEventListener('orientationchange', onOrientation, { passive: true });
        window.screen?.orientation?.addEventListener?.('change', onOrientation);
        sync();
    },

    _orientationKey() {
        const orientation = window.screen?.orientation?.type || '';
        if (orientation) return orientation;
        return (window.innerWidth || 0) > (window.innerHeight || 0) ? 'landscape' : 'portrait';
    },

    _isInventoryFormControl(element) {
        const root = document.getElementById('pgInventory');
        const isControl = !!element?.matches?.('input, textarea, select, [contenteditable="true"]');
        const insideInventory = !!(root && element && root.contains(element));
        const insideScanner = !!element?.closest?.('.ms-scanner-consumer, .inv-scan-product-card');
        return isControl && (insideInventory || insideScanner);
    },

    _syncVisualViewport() {
        const root = document.getElementById('pgInventory');
        if (!root) return;
        const viewport = window.visualViewport;
        const visualHeight = Math.max(1, Math.round(viewport?.height || window.innerHeight || document.documentElement.clientHeight || 1));
        const visualWidth = Math.max(1, Math.round(viewport?.width || window.innerWidth || document.documentElement.clientWidth || 1));
        const activeControl = this._isInventoryFormControl(document.activeElement);
        if (!activeControl) this._viewportBaselineHeight = Math.max(this._viewportBaselineHeight || 0, visualHeight);
        if (!this._viewportBaselineHeight) this._viewportBaselineHeight = visualHeight;
        const keyboardGap = Math.max(0, Math.round(this._viewportBaselineHeight - visualHeight));
        const keyboardOpen = activeControl && keyboardGap >= 120 && visualHeight <= this._viewportBaselineHeight * 0.84;
        document.documentElement.style.setProperty('--inv-visual-height', `${visualHeight}px`);
        document.documentElement.style.setProperty('--inv-visual-width', `${visualWidth}px`);
        document.documentElement.style.setProperty('--inv-keyboard-gap', `${keyboardOpen ? keyboardGap : 0}px`);
        document.body?.classList.toggle('inv-soft-keyboard-open', keyboardOpen);
        const coarse = !!window.matchMedia?.('(pointer: coarse)').matches;
        document.body?.classList.toggle('inv-field-landscape', coarse && visualWidth > visualHeight && visualHeight < 700);
        root.dataset.keyboard = keyboardOpen ? 'open' : 'closed';
    },

    _handleVisibilityChange() {
        if (document.visibilityState === 'hidden') {
            if (this.invData?.id) this._flushDraftForBackground();
            return;
        }
        this._syncVisualViewport();
        if (this.invData?.id && !this._draftLocked && navigator.onLine !== false && this._readLocalRecovery(this.invData.id)) {
            const resumeSave = () => this._saveDraftNow(true).catch(() => {});
            if (this._backgroundSavePromise) this._backgroundSavePromise.finally(resumeSave);
            else resumeSave();
        }
    },

    _handleOrientationChange() {
        const next = this._orientationKey();
        if (next === this._lastOrientationKey) { this._syncVisualViewport(); return; }
        this._lastOrientationKey = next;
        this._viewportBaselineHeight = 0;
        this._closeMobileMore();
        this._syncWizardUI(false);
        setTimeout(() => this._syncVisualViewport(), 180);
        // Le noyau MageStockScanner gère lui-même la suspension et le redémarrage
        // après rotation. Aucun cycle caméra local n'est conservé ici.
    },

    _showView(view) {
        ['home','new','progress','review','detail'].forEach(v => {
            const el = document.getElementById(`invView${v.charAt(0).toUpperCase()+v.slice(1)}`);
            if (el) el.style.display = v === view ? 'block' : 'none';
        });
        const root = document.getElementById('pgInventory');
        if (root) root.dataset.invView = view;
        document.body?.classList.toggle('inventory-progress-active', view === 'progress' || view === 'review');
        if (view !== 'progress') this._closeMobileMore();
        window.scrollTo({ top: 0, behavior: 'auto' });
        const focusTarget = view === 'new' ? document.getElementById('invNewTitle')
            : view === 'progress' ? document.getElementById('invHeaderTitle')
            : view === 'review' ? document.getElementById('invReviewPageTitle')
            : view === 'detail' ? document.getElementById('invDetailTitle') : null;
        requestAnimationFrame(() => focusTarget?.focus({ preventScroll: true }));
    },

    _isCompactFlow() {
        return !!(this._flowMedia?.matches || window.innerWidth < 1200);
    },

    openNewInventory() {
        this.newWizardStep = 1;
        this._showView('new');
        this._syncWizardUI(true);
        this._updateNewReview();
    },

    nextWizardStep() {
        if (this.newWizardStep === 1 && !this._validateWizardIdentity()) return;
        if (this.newWizardStep === 2 && !this._validateWizardScope()) return;
        this.goWizardStep(Math.min(3, this.newWizardStep + 1));
    },

    goWizardStep(step) {
        const target = Math.max(1, Math.min(3, Number(step) || 1));
        if (target > 1 && !this._validateWizardIdentity()) return;
        if (target > 2 && !this._validateWizardScope()) return;
        this.newWizardStep = target;
        this._saveNewDraft();
        this._updateNewReview();
        this._syncWizardUI(true);
    },

    _validateWizardIdentity() {
        const title = document.getElementById('invTitle')?.value?.trim() || '';
        const wid = document.getElementById('invWhSelect')?.value || '';
        if (!title) {
            toast('Saisissez un titre pour continuer.', 'warning');
            document.getElementById('invTitle')?.focus();
            this._announce('Le titre de l’inventaire est obligatoire.');
            return false;
        }
        if (!wid) {
            toast('Sélectionnez un magasin pour continuer.', 'warning');
            document.getElementById('invWhSelect')?.focus();
            this._announce('Le magasin est obligatoire.');
            return false;
        }
        return true;
    },

    _getInventoryMode() {
        return document.querySelector('input[name="invMode"]:checked')?.value === 'partial' ? 'partial' : 'full';
    },

    _syncModeUI() {
        const partial = this._getInventoryMode() === 'partial';
        const fullNotice = document.getElementById('invFullScopeNotice');
        const partialGroup = document.getElementById('invPartialScopeGroup');
        if (fullNotice) fullNotice.hidden = partial;
        if (partialGroup) partialGroup.hidden = !partial;
        document.querySelectorAll('.inv-mode-option').forEach(option => {
            option.classList.toggle('is-selected', !!option.querySelector('input:checked'));
        });
        this._syncCatUI();
    },

    _validateWizardScope() {
        if (this._getInventoryMode() !== 'partial') return true;
        const selected = this._getSelectedCategories();
        if (selected.length) return true;
        toast('Sélectionnez au moins une catégorie pour un contrôle ciblé.', 'warning');
        document.getElementById('invCatSearch')?.focus();
        this._announce('Le contrôle ciblé exige au moins une catégorie.');
        return false;
    },

    _syncWizardUI(moveFocus = true) {
        const compact = this._isCompactFlow();
        const panels = [...document.querySelectorAll('.inv-wizard-panel')];
        panels.forEach(panel => {
            const active = Number(panel.dataset.step) === this.newWizardStep;
            panel.hidden = compact ? !active : false;
            panel.setAttribute('aria-hidden', compact && !active ? 'true' : 'false');
        });

        document.querySelectorAll('.inv-wizard-step').forEach(btn => {
            const step = Number(btn.dataset.step);
            const active = step === this.newWizardStep;
            const done = step < this.newWizardStep;
            btn.classList.toggle('is-active', active);
            btn.classList.toggle('is-done', done);
            btn.toggleAttribute('disabled', compact && step > this.newWizardStep);
            if (active) btn.setAttribute('aria-current', 'step'); else btn.removeAttribute('aria-current');
        });

        const back = document.getElementById('btnInvWizardBack');
        const next = document.getElementById('btnInvWizardNext');
        const start = document.getElementById('btnStartInv');
        if (compact) {
            if (back) back.style.display = this.newWizardStep > 1 ? '' : 'none';
            if (next) next.style.display = this.newWizardStep < 3 ? '' : 'none';
            if (start) start.style.display = this.newWizardStep === 3 ? '' : 'none';
        } else {
            if (back) back.style.display = 'none';
            if (next) next.style.display = 'none';
            if (start) start.style.display = '';
        }

        if (moveFocus && compact) {
            const heading = document.getElementById(`invStep${this.newWizardStep}Title`);
            requestAnimationFrame(() => heading?.focus({ preventScroll: true }));
            this._announce(`Étape ${this.newWizardStep} sur 3 : ${heading?.textContent || ''}`);
        }
    },

    _restoreNewDraft() {
        try {
            const raw = sessionStorage.getItem(this._draftKey);
            const parsed = raw ? JSON.parse(raw) : null;
            return parsed && typeof parsed === 'object' ? parsed : { title: '', warehouseId: '', categories: [], mode: 'full' };
        } catch (_) {
            return { title: '', warehouseId: '', categories: [], mode: 'full' };
        }
    },

    _saveNewDraft() {
        const draft = {
            title: document.getElementById('invTitle')?.value?.trim() || '',
            warehouseId: document.getElementById('invWhSelect')?.value || '',
            categories: this._getInventoryMode() === 'partial' ? this._getSelectedCategories() : [],
            mode: this._getInventoryMode(),
        };
        this._newDraft = draft;
        try { sessionStorage.setItem(this._draftKey, JSON.stringify(draft)); } catch (_) {}
    },

    _clearNewDraft() {
        this._newDraft = { title: '', warehouseId: '', categories: [], mode: 'full' };
        try { sessionStorage.removeItem(this._draftKey); } catch (_) {}
    },

    _resetNewForm() {
        const title = document.getElementById('invTitle');
        const warehouse = document.getElementById('invWhSelect');
        if (title) title.value = '';
        if (warehouse) warehouse.value = '';
        const fullMode = document.getElementById('invModeFull');
        if (fullMode) fullMode.checked = true;
        this._buildCatChips([], []);
        this._syncModeUI();
        this.newWizardStep = 1;
        this._updateNewReview();
    },

    _updateNewReview() {
        const title = document.getElementById('invTitle')?.value?.trim() || '—';
        const wid = document.getElementById('invWhSelect')?.value || '';
        const wh = this.whs.find(w => String(w.id) === String(wid));
        const mode = this._getInventoryMode();
        const cats = mode === 'partial' ? this._getSelectedCategories() : [];
        const count = this._estimateInventoryProductCount(wid, cats);
        const setText = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; };
        setText('invReviewTitle', title);
        setText('invReviewMode', mode === 'partial' ? 'Contrôle ciblé' : 'Inventaire complet');
        setText('invReviewWarehouse', wh?.name || '—');
        setText('invReviewCategories', mode === 'partial' ? `${cats.length} catégorie${cats.length > 1 ? 's' : ''} sélectionnée${cats.length > 1 ? 's' : ''}` : 'Tout le magasin');
        setText('invReviewProducts', wid ? `${count} produit${count > 1 ? 's' : ''}` : 'Sélectionnez un magasin');
    },

    _estimateInventoryProductCount(wid, cats = []) {
        if (!wid) return 0;
        const productIds = new Set((this.stockAll || [])
            .filter(s => String(s.warehouseId) === String(wid))
            .map(s => String(s.productId)));
        return (this.prods || []).filter(p => productIds.has(String(p.id)) && (!cats.length || cats.includes(p.category || ''))).length;
    },

    _announce(message) {
        const live = document.getElementById('invA11yLive');
        if (!live) return;
        live.textContent = '';
        requestAnimationFrame(() => { live.textContent = message; });
    },

    _closeMobileMore() {
        const menu = document.getElementById('invMobileMore');
        if (menu) menu.open = false;
    },

    _setText(id, text) {
        const el = document.getElementById(id);
        if (el) el.textContent = text;
    },

    _resolveInventoryMode(value) {
        let mode = value;
        if (value && typeof value === 'object') {
            mode = value.mode;
            if (mode !== 'full' && mode !== 'partial') {
                const categories = Array.isArray(value.categories) ? value.categories.filter(Boolean) : [];
                mode = categories.length || String(value.category || '').trim() ? 'partial' : 'full';
            }
        }
        return mode === 'partial' ? 'partial' : 'full';
    },

    _fmtMode(value) {
        return this._resolveInventoryMode(value) === 'partial' ? 'Contrôle ciblé' : 'Inventaire complet';
    },

    _setSaveStatus(state, iso = null) {
        const el = document.getElementById('invSaveStatus');
        if (!el) return;
        el.classList.remove('is-saving', 'is-error', 'is-saved', 'is-conflict', 'is-offline');
        if (state === 'saving') {
            el.classList.add('is-saving');
            el.textContent = 'Enregistrement sur le serveur…';
        } else if (state === 'conflict') {
            el.classList.add('is-error', 'is-conflict');
            el.textContent = 'Version serveur plus récente — rechargement nécessaire';
        } else if (state === 'offline') {
            el.classList.add('is-offline');
            el.textContent = 'Hors ligne — modifications conservées sur cet appareil';
        } else if (state === 'error') {
            el.classList.add('is-error');
            el.textContent = 'Échec de l’enregistrement — copie de secours conservée';
        } else {
            el.classList.add('is-saved');
            const date = iso ? this._fmtTime(iso) : '';
            el.textContent = date ? `Enregistré sur le serveur à ${date}` : 'Enregistré sur le serveur';
        }
    },

    _draftCountPayload(lines = null) {
        return (Array.isArray(lines) ? lines : (this.invData?.lines || [])).map(line => ({
            productId: line.productId,
            counted: line.counted === '' || line.counted === undefined ? null : line.counted,
        }));
    },

    _recoveryKey(id = null) {
        const draftId = id || this.invData?.id;
        return draftId ? `${this._recoveryPrefix}${draftId}` : '';
    },

    _writeLocalRecovery() {
        if (!this.invData?.id || this.invData.status !== 'draft') return;
        const key = this._recoveryKey();
        if (!key) return;
        try {
            localStorage.setItem(key, JSON.stringify({
                draftId: this.invData.id,
                baseRevision: Number(this.invData.revision || 1),
                mutationVersion: Number(this._localMutationVersion || 0),
                savedAt: new Date().toISOString(),
                lines: this._draftCountPayload(),
            }));
        } catch (_) {}
    },

    _readLocalRecovery(id) {
        const key = this._recoveryKey(id);
        if (!key) return null;
        try {
            const parsed = JSON.parse(localStorage.getItem(key) || 'null');
            return parsed && String(parsed.draftId) === String(id) && Array.isArray(parsed.lines) ? parsed : null;
        } catch (_) {
            return null;
        }
    },

    _clearLocalRecovery(id = null) {
        const key = this._recoveryKey(id);
        if (!key) return;
        try { localStorage.removeItem(key); } catch (_) {}
    },

    _mergeRecoveredCounts(lines, recoveryLines) {
        const counts = new Map((recoveryLines || []).map(line => [String(line.productId), line.counted]));
        return (lines || []).map(line => counts.has(String(line.productId))
            ? { ...line, counted: counts.get(String(line.productId)) }
            : line);
    },

    _restoreLocalRecovery(draft) {
        const recovery = this._readLocalRecovery(draft?.id);
        if (!recovery) return;
        const applyRecovery = () => {
            this.invData.lines = this._mergeRecoveredCounts(this.invData.lines || [], recovery.lines || []);
            this.invData.stage = 'counting';
            this.invData.reviewedRevision = null;
            this.reviewData = null;
            this._localMutationVersion = Math.max(Number(this._localMutationVersion || 0), Number(recovery.mutationVersion || 0), 1);
            this.updateStats();
            this.renderItems();
            this._writeLocalRecovery();
            this._setSaveStatus(navigator.onLine === false ? 'offline' : 'saving');
            if (navigator.onLine !== false) this._saveDraftNow(true).catch(() => {});
            this._announce('Les comptages non transmis de cet appareil ont été restaurés.');
        };
        if (Number(recovery.baseRevision || 0) === Number(draft.revision || 0)) {
            applyRecovery();
            return;
        }
        Modal.show('Comptages locaux non transmis', `
            <div class="inv-exit-choice">
                <p>Une copie de secours existe sur cet appareil, mais le serveur contient une version plus récente de l’inventaire.</p>
                <div class="inv-exit-choice__warning"><strong>Choix nécessaire</strong><span>Conservez la version serveur ou réappliquez explicitement les comptages locaux avant de poursuivre.</span></div>
            </div>
        `, [
            { label: 'Conserver la version serveur', cls: 'btn-secondary', onclick: () => { InvPage._clearLocalRecovery(draft.id); Modal.close(); } },
            { label: 'Réappliquer les comptages locaux', cls: 'btn-primary', onclick: () => { Modal.close(); applyRecovery(); } },
        ], 'warning');
    },

    _setDraftLocked(locked) {
        this._draftLocked = !!locked;
        const root = document.getElementById('pgInventory');
        root?.classList.toggle('is-draft-locked', this._draftLocked);
        root?.setAttribute('aria-busy', this._draftLocked ? 'true' : 'false');
        const banner = document.getElementById('invConflictBanner');
        if (banner) banner.hidden = !this._draftLocked;
        ['btnInvScan','btnInvValidate','btnInvMobileScanTop','btnInvMobileScanDock','btnInvMobileValidate','btnInvManualAdd','btnInvValidateAll','btnInvResetAll'].forEach(id => {
            const el = document.getElementById(id);
            if (el) el.disabled = this._draftLocked;
        });
        document.querySelectorAll('#invViewProgress input, #invViewProgress button[data-inv-quick], #invViewProgress .inv-card-reset').forEach(el => {
            el.disabled = this._draftLocked;
        });
    },

    _flushDraftForBackground() {
        if (!this.invData?.id || this.invData.status !== 'draft') return null;
        this._writeLocalRecovery();
        if (this._draftLocked || navigator.onLine === false || typeof Api.invSaveDraftKeepalive !== 'function') return null;
        // Ne jamais doubler une écriture déjà en vol : la copie locale assure la reprise si le navigateur l'interrompt.
        if (this._draftSavePromise || this._backgroundSavePromise) return this._draftSavePromise || this._backgroundSavePromise;

        const draftId = this.invData.id;
        const expectedRevision = Number(this.invData.revision || 1);
        const saveMutationVersion = Number(this._localMutationVersion || 0);
        const payload = {
            draftId,
            expectedRevision,
            lines: this._draftCountPayload(),
        };
        this._backgroundSavePromise = Api.invSaveDraftKeepalive(payload)
            .then(saved => {
                if (!this.invData || String(this.invData.id) !== String(draftId)) return saved;
                // Une autre sauvegarde locale a pu terminer entre-temps : ne pas revenir à une ancienne révision.
                if (Number(this.invData.revision || 0) !== expectedRevision) return saved;
                const newerLocalChanges = Number(this._localMutationVersion || 0) !== saveMutationVersion;
                const currentLines = this.invData.lines || [];
                this.invData = { ...this.invData, ...saved, lines: newerLocalChanges ? currentLines : (saved.lines || currentLines) };
                if (newerLocalChanges) {
                    this._draftSaveQueued = true;
                    this._writeLocalRecovery();
                } else {
                    this._clearLocalRecovery(draftId);
                    this._setSaveStatus('saved', saved.updatedAt);
                }
                return saved;
            })
            .catch(error => {
                // Une réponse 409 n'est un vrai conflit que si notre révision locale n'a pas déjà progressé.
                if (Number(error?.status) === 409 && this.invData && String(this.invData.id) === String(draftId)
                    && Number(this.invData.revision || 0) === expectedRevision) {
                    this._setDraftLocked(true);
                    this._setSaveStatus('conflict');
                }
                return null;
            })
            .finally(() => {
                this._backgroundSavePromise = null;
                if (this._draftSaveQueued && !this._draftLocked && document.visibilityState === 'visible' && navigator.onLine !== false) {
                    this._draftSaveQueued = false;
                    this._saveDraftNow(true).catch(() => {});
                }
            });
        return this._backgroundSavePromise;
    },

    async loadDrafts() {
        const shell = document.getElementById('invDraftsShell');
        const list = document.getElementById('invDraftsList');
        const count = document.getElementById('invDraftsCount');
        if (!shell || !list) return;
        try {
            const drafts = await Api.invDrafts();
            this.drafts = Array.isArray(drafts) ? drafts : [];
            shell.style.display = this.drafts.length ? '' : 'none';
            if (count) count.textContent = String(this.drafts.length);
            list.innerHTML = this.drafts.map(draft => this._renderDraftCard(draft)).join('');
        } catch (error) {
            shell.style.display = 'none';
            this.drafts = [];
        }
    },

    _renderDraftCard(draft) {
        const warehouse = this.whs.find(item => String(item.id) === String(draft.warehouseId));
        const lines = Array.isArray(draft.lines) ? draft.lines : [];
        const counted = lines.filter(line => this._lineStatus(line) !== 'pending').length;
        const differences = lines.filter(line => this._lineStatus(line) === 'diff').length;
        const percent = lines.length ? Math.round((counted / lines.length) * 100) : 0;
        const reviewReady = draft.stage === 'review' && Number(draft.reviewedRevision || 0) === Number(draft.revision || 0);
        return `<article class="inv-draft-card" role="listitem">
            <div class="inv-draft-card__top">
                <div>
                    <span class="inv-mode-badge ${draft.mode === 'partial' ? 'is-partial' : ''}">${this._fmtMode(draft.mode)}</span>
                    <h4>${this._esc(draft.title || 'Inventaire sans titre')}</h4>
                    <p>${this._esc(warehouse?.name || 'Magasin inconnu')} · ${this._fmtCategorySummary(draft)}</p>
                </div>
                <span class="inv-draft-card__state ${reviewReady ? 'is-review' : ''}">${reviewReady ? 'À clôturer' : 'En cours'}</span>
            </div>
            <div class="inv-draft-card__progress" aria-label="${percent}% compté">
                <div><span style="width:${percent}%"></span></div>
                <strong>${counted} / ${lines.length}</strong>
            </div>
            <div class="inv-draft-card__meta">
                <span>${differences} écart${differences > 1 ? 's' : ''}</span>
                <span>Dernière sauvegarde ${this._fmtRelativeDate(draft.updatedAt || draft.createdAt)}</span>
            </div>
            <div class="inv-draft-card__actions">
                <button type="button" class="btn btn-primary" onclick="InvPage.resumeDraft('${this._esc(draft.id)}')">${reviewReady ? 'Reprendre la revue' : 'Reprendre'}</button>
                <button type="button" class="btn btn-ghost" onclick="InvPage.discardDraftById('${this._esc(draft.id)}')">Supprimer</button>
            </div>
        </article>`;
    },

    async resumeDraft(id) {
        try {
            const draft = await Api.invDraft(id);
            this.invData = draft;
            this.reviewData = null;
            this._localMutationVersion = 0;
            this._setDraftLocked(false);
            this._restoreLocalRecovery(draft);
            this.itemStatusFilter = this._isCompactFlow() ? 'pending' : 'all';
            this.mobileVisibleLimit = 30;
            this._syncFilterButtons('.inv-status-filter', this.itemStatusFilter);
            this._hydrateProgressHeader(draft);
            this.renderItems();
            this.updateStats();
            this._setSaveStatus('saved', draft.updatedAt || draft.createdAt);
            const reviewed = draft.stage === 'review'
                && Number(draft.reviewedRevision || 0) === Number(draft.revision || 0)
                && draft.reviewSummary;
            if (reviewed) {
                this.reviewData = { draft, summary: draft.reviewSummary };
                this.renderReview(this.reviewData);
                this._showView('review');
                this._announce('Brouillon repris à l’étape de revue.');
            } else {
                this._showView('progress');
                this._announce('Brouillon repris. Les comptages sont enregistrés sur le serveur.');
            }
        } catch (error) {
            toast(error.message || 'Impossible de reprendre ce brouillon', 'error');
            await this.loadDrafts();
        }
    },

    async discardDraftById(id) {
        if (!confirm('Supprimer définitivement ce brouillon d’inventaire ?')) return;
        try {
            const draft = this.drafts.find(item => String(item.id) === String(id));
            await Api.invDiscardDraft({ draftId: id, expectedRevision: draft?.revision });
            if (this.invData && String(this.invData.id) === String(id)) this.invData = null;
            this._clearLocalRecovery(id);
            toast('Brouillon supprimé', 'success');
            await this.loadDrafts();
        } catch (error) {
            toast(error.message || 'Impossible de supprimer le brouillon', 'error');
        }
    },

    _hydrateProgressHeader(draft) {
        const warehouse = this.whs.find(item => String(item.id) === String(draft.warehouseId));
        const categoryLabel = draft.mode === 'partial'
            ? `${(draft.categories || []).length} catégorie${(draft.categories || []).length > 1 ? 's' : ''}`
            : 'Tout le magasin';
        this._setText('invHeaderTitle', draft.title || 'Inventaire en cours');
        this._setText('invHeaderMeta', `${warehouse?.name || ''} · ${categoryLabel} · ${this._fmtDateShort(draft.startedAt || draft.createdAt || draft.date)}`);
        this._setText('invProgressMode', draft.mode === 'partial' ? 'Ciblé' : 'Complet');
        document.getElementById('invProgressMode')?.classList.toggle('is-partial', draft.mode === 'partial');
    },

    _scheduleDraftSave() {
        if (!this.invData?.id || this.invData.status !== 'draft' || this._draftLocked) return;
        this._localMutationVersion++;
        this._writeLocalRecovery();
        clearTimeout(this._draftSaveTimer);
        if (navigator.onLine === false) {
            this._setSaveStatus('offline');
            return;
        }
        this._setSaveStatus('saving');
        this._draftSaveTimer = setTimeout(() => this._saveDraftNow(true).catch(() => {}), 500);
    },

    async _saveDraftNow(silent = false) {
        if (!this.invData?.id || this.invData.status !== 'draft') return this.invData;
        clearTimeout(this._draftSaveTimer);
        if (this._draftLocked) throw new Error('Brouillon verrouillé après conflit.');
        if (navigator.onLine === false) {
            this._writeLocalRecovery();
            this._setSaveStatus('offline');
            return this.invData;
        }
        if (this._draftSavePromise || this._backgroundSavePromise) {
            this._draftSaveQueued = true;
            return this._draftSavePromise || this._backgroundSavePromise;
        }
        this._setSaveStatus('saving');
        const saveMutationVersion = Number(this._localMutationVersion || 0);
        const payload = {
            draftId: this.invData.id,
            expectedRevision: this.invData.revision,
            lines: this._draftCountPayload(),
        };
        this._draftSavePromise = Api.invSaveDraft(payload)
            .then(saved => {
                const newerLocalChanges = Number(this._localMutationVersion || 0) !== saveMutationVersion;
                const currentLines = this.invData?.lines || [];
                this.invData = { ...this.invData, ...saved, lines: newerLocalChanges ? currentLines : (saved.lines || currentLines) };
                this._draftSaveFailed = false;
                this._setDraftLocked(false);
                if (newerLocalChanges) {
                    this._draftSaveQueued = true;
                    this._writeLocalRecovery();
                    this._setSaveStatus('saving');
                } else {
                    this._clearLocalRecovery(this.invData.id);
                    this._setSaveStatus('saved', saved.updatedAt);
                }
                return this.invData;
            })
            .catch(error => {
                this._draftSaveFailed = true;
                this._writeLocalRecovery();
                if (Number(error?.status) === 409) {
                    this._setDraftLocked(true);
                    this._setSaveStatus('conflict');
                    this._showDraftConflict();
                } else if (navigator.onLine === false) {
                    this._setSaveStatus('offline');
                } else {
                    this._setSaveStatus('error');
                    if (!silent) toast(error.message || 'Impossible d’enregistrer le brouillon', 'error');
                }
                throw error;
            })
            .finally(async () => {
                this._draftSavePromise = null;
                if (this._draftSaveQueued && !this._draftLocked) {
                    this._draftSaveQueued = false;
                    try { await this._saveDraftNow(true); } catch (_) {}
                }
            });
        return this._draftSavePromise;
    },

    _showDraftConflict() {
        if (this._draftConflictOpen || !this.invData?.id) return;
        this._draftConflictOpen = true;
        const draftId = this.invData.id;
        Modal.show('Inventaire modifié sur un autre appareil', `
            <div class="inv-exit-choice">
                <p>Une version plus récente de ce brouillon existe sur le serveur. MageStock ne remplace pas automatiquement vos comptages locaux afin d’éviter une perte silencieuse.</p>
                <div class="inv-exit-choice__warning"><strong>Rechargement nécessaire</strong><span>Rechargez la version serveur avant de poursuivre le comptage.</span></div>
            </div>
        `, [
            { label: 'Consulter sans modifier', cls: 'btn-secondary', onclick: () => { InvPage._draftConflictOpen = false; Modal.close(); } },
            { label: 'Recharger la version serveur', cls: 'btn-primary', onclick: () => { InvPage._draftConflictOpen = false; Modal.close(); InvPage.resumeDraft(draftId); } },
        ], 'warning');
    },

    _fmtTime(iso) {
        if (!iso) return '';
        try { return Locale.formatDateTime(iso, { hour: '2-digit', minute: '2-digit' }); } catch (_) { return ''; }
    },

    _fmtRelativeDate(iso) {
        if (!iso) return 'récemment';
        const time = new Date(iso).getTime();
        if (!Number.isFinite(time)) return this._fmtDate(iso);
        const minutes = Math.max(0, Math.round((Date.now() - time) / 60000));
        if (minutes < 1) return 'à l’instant';
        if (minutes < 60) return `il y a ${minutes} min`;
        if (minutes < 1440) return `il y a ${Math.round(minutes / 60)} h`;
        return this._fmtDateShort(iso);
    },

    // ══════════════════════════════════════════════════════════════
    // HISTORIQUE (page principale)
    // ══════════════════════════════════════════════════════════════
    async loadHistory() {
        const tbody = document.getElementById('invHistoryBody');
        const cards = document.getElementById('invHistoryCards');
        const countEl = document.getElementById('invHistoryCount');
        try {
            const invs = await Api.invHistory();
            if (countEl) countEl.textContent = `${invs.length || 0} inventaire${invs.length > 1 ? 's' : ''}`;

            if (!invs.length) {
                if (tbody) tbody.innerHTML = `<tr><td colspan="9" class="tbl-empty">Aucun inventaire — utilisez « Nouvel inventaire » pour commencer.</td></tr>`;
                if (cards) cards.innerHTML = `<div class="inv-empty-state" role="listitem"><strong>Aucun inventaire</strong><span>Lancez votre premier comptage depuis le bouton « Nouvel inventaire ».</span></div>`;
                return;
            }

            const warehouseById = new Map((this.whs || []).map(warehouse => [String(warehouse.id), warehouse]));
            const groupsMap = new Map();
            invs.forEach(inv => {
                const key = String(inv.warehouseId ?? 'unknown');
                if (!groupsMap.has(key)) groupsMap.set(key, []);
                groupsMap.get(key).push(inv);
            });
            const collator = new Intl.Collator(window.Locale?.current || document.documentElement.lang || 'fr', { sensitivity: 'base', numeric: true });
            const groups = [...groupsMap.entries()].map(([warehouseId, inventories]) => ({
                warehouseId,
                warehouse: warehouseById.get(warehouseId) || null,
                inventories,
            })).sort((a, b) => {
                if (!a.warehouse && b.warehouse) return 1;
                if (a.warehouse && !b.warehouse) return -1;
                return collator.compare(a.warehouse?.name || 'Magasin inconnu', b.warehouse?.name || 'Magasin inconnu');
            });

            const tableRow = inv => {
                const lines = inv.lines || [];
                const counted = lines.filter(line => line.counted !== null && line.counted !== undefined && line.counted !== '').length;
                const disc = Number(inv.discrepancies || 0);
                const title = inv.title || `Inventaire du ${this._fmtDateShort(inv.date)}`;
                const cat = this._fmtCategories(inv);
                const resultBadge = disc === 0
                    ? `<span class="badge badge-green">✓ Sans écart</span>`
                    : `<span class="badge badge-orange">${disc} écart${disc > 1 ? 's' : ''}</span>`;
                return `<tr class="inv-history-row" onclick="InvPage.showDetail('${this._esc(inv.id)}')">
                    <td><strong>${this._esc(title)}</strong></td>
                    <td class="text-muted">${this._fmtDate(inv.date)}</td>
                    <td>${this._esc(warehouseById.get(String(inv.warehouseId))?.name || 'Magasin inconnu')}</td>
                    <td class="text-muted"><span class="inv-mode-badge ${this._resolveInventoryMode(inv) === 'partial' ? 'is-partial' : ''}">${this._fmtMode(inv)}</span><span class="inv-history-table-scope">${this._esc(cat)}</span></td>
                    <td class="cell-number">${counted} / ${lines.length}</td>
                    <td class="cell-number"><strong ${disc > 0 ? 'style="color:var(--orange)"' : ''}>${disc}</strong></td>
                    <td class="text-muted">${this._esc(inv.operator || '—')}</td>
                    <td>${resultBadge}</td>
                    <td class="cell-actions" onclick="event.stopPropagation()">
                        <button class="btn btn-sm btn-secondary" title="Voir le détail" onclick="InvPage.showDetail('${this._esc(inv.id)}')">Voir</button>
                        <button class="btn btn-sm btn-ghost" title="Imprimer" onclick="InvPage.printById('${this._esc(inv.id)}')">Imprimer</button>
                    </td>
                </tr>`;
            };

            const mobileCard = inv => {
                const lines = inv.lines || [];
                const counted = lines.filter(line => line.counted !== null && line.counted !== undefined && line.counted !== '').length;
                const disc = Number(inv.discrepancies || 0);
                const title = inv.title || `Inventaire du ${this._fmtDateShort(inv.date)}`;
                const categorySummary = this._fmtCategorySummary(inv);
                return `<article class="inv-history-card" role="listitem">
                    <button type="button" class="inv-history-card__main" onclick="InvPage.showDetail('${this._esc(inv.id)}')">
                        <span class="inv-history-card__eyebrow">${this._fmtDateShort(inv.date)}</span>
                        <span class="inv-mode-badge ${this._resolveInventoryMode(inv) === 'partial' ? 'is-partial' : ''}">${this._fmtMode(inv)}</span>
                        <strong class="inv-history-card__title">${this._esc(title)}</strong>
                        <span class="inv-history-card__scope">${this._esc(categorySummary)}</span>
                        <span class="inv-history-card__metrics">
                            <span><strong>${counted}</strong> / ${lines.length} comptés</span>
                            <span class="${disc ? 'is-warning' : 'is-ok'}"><strong>${disc}</strong> écart${disc > 1 ? 's' : ''}</span>
                        </span>
                        <span class="inv-history-card__operator">${this._esc(inv.operator || 'Opérateur non renseigné')}</span>
                    </button>
                    <div class="inv-history-card__actions">
                        <button type="button" class="btn btn-secondary" onclick="InvPage.showDetail('${this._esc(inv.id)}')">Voir le détail</button>
                        <button type="button" class="btn btn-ghost" onclick="InvPage.printById('${this._esc(inv.id)}')">Imprimer</button>
                    </div>
                </article>`;
            };

            if (tbody) {
                tbody.innerHTML = groups.map(group => {
                    const name = group.warehouse?.name || 'Magasin inconnu';
                    const amount = group.inventories.length;
                    return `<tr class="inv-history-group-row"><th colspan="9" scope="rowgroup"><span>${this._esc(name)}</span><small>${amount} inventaire${amount > 1 ? 's' : ''}</small></th></tr>${group.inventories.map(tableRow).join('')}`;
                }).join('');
            }

            if (cards) {
                cards.innerHTML = groups.map((group, index) => {
                    const name = group.warehouse?.name || 'Magasin inconnu';
                    const amount = group.inventories.length;
                    const headingId = `invHistoryWarehouse-${index}`;
                    return `<section class="inv-history-group" role="listitem" aria-labelledby="${headingId}">
                        <header class="inv-history-group__header">
                            <div><span class="inv-page-kicker">Magasin</span><h4 id="${headingId}">${this._esc(name)}</h4></div>
                            <span class="badge badge-blue">${amount}</span>
                        </header>
                        <div class="inv-history-group__list" role="list" aria-label="Inventaires — ${this._esc(name)}">${group.inventories.map(mobileCard).join('')}</div>
                    </section>`;
                }).join('');
            }
        } catch (error) {
            if (tbody) tbody.innerHTML = `<tr><td colspan="9" class="tbl-empty" style="color:var(--red)">Erreur de chargement</td></tr>`;
            if (cards) cards.innerHTML = `<div class="inv-empty-state is-error">Impossible de charger l’historique.</div>`;
        }
    },

    // ══════════════════════════════════════════════════════════════
    // VUE DÉTAIL
    // ══════════════════════════════════════════════════════════════
    async showDetail(invId) {
        try {
            const invs = await Api.invHistory();
            const inv  = invs.find(i => String(i.id) === String(invId));
            if (!inv) { toast('Inventaire introuvable', 'error'); return; }

            this._currentInv = inv;
            const wh    = this.whs.find(w => String(w.id) === String(inv.warehouseId));
            const title = inv.title || `Inventaire du ${this._fmtDateShort(inv.date)}`;
            const lines = inv.lines || [];
            const counted = lines.filter(l => l.counted !== null && l.counted !== undefined && l.counted !== '').length;
            const disc  = inv.discrepancies || 0;

            // Titre page
            document.getElementById('invDetailTitle').textContent = title;

            // Méta card
            document.getElementById('invDetailMeta').innerHTML = `
                <div class="card-body inv-detail-meta-grid">
                    <div class="inv-detail-meta-item">
                        <span class="inv-detail-meta-label">Date</span>
                        <span class="inv-detail-meta-val">${this._fmtDate(inv.date)}</span>
                    </div>
                    <div class="inv-detail-meta-item">
                        <span class="inv-detail-meta-label">Magasin</span>
                        <span class="inv-detail-meta-val">${this._esc(wh?.name || '?')}</span>
                    </div>
                    <div class="inv-detail-meta-item">
                        <span class="inv-detail-meta-label">Type</span>
                        <span class="inv-detail-meta-val">${this._esc(this._fmtMode(inv))}</span>
                    </div>
                    <div class="inv-detail-meta-item">
                        <span class="inv-detail-meta-label">Périmètre</span>
                        <span class="inv-detail-meta-val">${this._esc(this._fmtCategories(inv))}</span>
                    </div>
                    <div class="inv-detail-meta-item">
                        <span class="inv-detail-meta-label">Opérateur</span>
                        <span class="inv-detail-meta-val">${this._esc(inv.operator || '—')}</span>
                    </div>
                    <div class="inv-detail-meta-item">
                        <span class="inv-detail-meta-label">Produits comptés</span>
                        <span class="inv-detail-meta-val">${counted} / ${lines.length}</span>
                    </div>
                    <div class="inv-detail-meta-item">
                        <span class="inv-detail-meta-label">Écarts</span>
                        <span class="inv-detail-meta-val ${disc > 0 ? 'text-orange' : 'text-green'}">
                            <strong>${disc === 0 ? '✓ Aucun écart' : disc + ' écart(s)'}</strong>
                        </span>
                    </div>
                </div>`;

            // Stats badges
            document.getElementById('invDetailStats').innerHTML = `
                <span class="badge badge-blue">${lines.length} produits</span>
                <span class="badge badge-green">${counted} comptés</span>
                ${disc > 0 ? `<span class="badge badge-orange">${disc} écarts</span>` : ''}`;

            this.detailStatusFilter = 'all';
            this._syncFilterButtons('.inv-detail-filter', this.detailStatusFilter);
            this.renderDetailLines();

            this._showView('detail');
        } catch(e) {
            toast(e.message || 'Erreur', 'error');
        }
    },

    // ══════════════════════════════════════════════════════════════
    // DÉMARRER
    // ══════════════════════════════════════════════════════════════
    async startInventory() {
        const title = document.getElementById('invTitle')?.value?.trim() || '';
        const wid = document.getElementById('invWhSelect')?.value || '';
        const mode = this._getInventoryMode();
        const cats = mode === 'partial' ? this._getSelectedCategories() : [];
        if (!this._validateWizardIdentity() || !this._validateWizardScope()) return;

        try {
            const startBtn = document.getElementById('btnStartInv');
            if (startBtn) startBtn.disabled = true;
            const data = await Api.invStart({ title, warehouseId: parseInt(wid, 10), mode, categories: cats });
            this.invData = { ...data };
            this.reviewData = null;
            this._localMutationVersion = 0;
            this._setDraftLocked(false);
            this._clearLocalRecovery(data.id);
            this.itemStatusFilter = this._isCompactFlow() ? 'pending' : 'all';
            this.mobileVisibleLimit = 30;
            this._syncFilterButtons('.inv-status-filter', this.itemStatusFilter);

            const wh = this.whs.find(w => String(w.id) === String(wid));
            const catLabel = mode === 'partial' ? `${cats.length} catégorie${cats.length > 1 ? 's' : ''}` : 'Tout le magasin';
            document.getElementById('invHeaderTitle').textContent = title;
            document.getElementById('invHeaderMeta').textContent = `${wh?.name || ''} · ${catLabel} · ${this._fmtDateShort(data.startedAt || data.createdAt || new Date().toISOString())}`;
            this._setText('invProgressMode', mode === 'partial' ? 'Ciblé' : 'Complet');
            this._setSaveStatus('saved', data.updatedAt || data.createdAt);

            this._resetNewForm();
            this._clearNewDraft();
            this._showView('progress');
            this.renderItems();
            this.updateStats();
            if (this._shouldAutoFocusInputs()) setTimeout(() => document.getElementById('invScanInput')?.focus(), 100);
            toast(`Inventaire démarré et enregistré — ${data.lines.length} produits`, 'success');
            this._announce(`Inventaire démarré. ${data.lines.length} produits à contrôler.`);
        } catch(e) {
            toast(e.message || 'Impossible de démarrer l’inventaire', 'error');
        } finally {
            const startBtn = document.getElementById('btnStartInv');
            if (startBtn) startBtn.disabled = false;
        }
    },

    // ══════════════════════════════════════════════════════════════
    // RENDU ITEMS
    // ══════════════════════════════════════════════════════════════
    // Sort state for inventory items table
    _invSort: { col: null, asc: true },

    sortItems(col) {
        if (!this.invData || !this.invData.lines) return;
        if (this._invSort.col === col) this._invSort.asc = !this._invSort.asc;
        else { this._invSort.col = col; this._invSort.asc = true; }

        const asc = this._invSort.asc;
        this.invData.lines.sort((a, b) => {
            let va, vb;
            if (col === 'name') { va = (a.name || '').toLowerCase(); vb = (b.name || '').toLowerCase(); }
            else if (col === 'sku') { va = (a.sku || '').toLowerCase(); vb = (b.sku || '').toLowerCase(); }
            else if (col === 'category') { va = (a.category || '').toLowerCase(); vb = (b.category || '').toLowerCase(); }
            else if (col === 'expected') { va = a.expected || 0; vb = b.expected || 0; return asc ? va - vb : vb - va; }
            else if (col === 'counted') {
                va = a.counted != null ? a.counted : -1;
                vb = b.counted != null ? b.counted : -1;
                return asc ? va - vb : vb - va;
            }
            else if (col === 'diff') {
                va = a.counted != null ? a.counted - a.expected : -99999;
                vb = b.counted != null ? b.counted - b.expected : -99999;
                return asc ? va - vb : vb - va;
            }
            else { va = ''; vb = ''; }
            return asc ? String(va).localeCompare(String(vb), 'fr') : String(vb).localeCompare(String(va), 'fr');
        });
        this.renderItems();
    },

    _sortArrow(col) {
        if (this._invSort.col !== col) return ' <span style="opacity:.3;font-size:10px">⇅</span>';
        return this._invSort.asc ? ' <span style="font-size:10px">↑</span>' : ' <span style="font-size:10px">↓</span>';
    },

    renderItems() {
        const tbody = document.getElementById('invItemsBody');
        const cards = document.getElementById('invItemsCards');
        const loadMore = document.getElementById('btnInvLoadMore');
        if ((!tbody && !cards) || !this.invData) return;

        const lines = this.invData.lines || [];
        const visible = this._filteredLineEntries(lines, this.itemStatusFilter);
        this._updateFilterBadges(lines);

        const thead = document.getElementById('invItemsTable')?.querySelector('thead tr');
        if (thead) thead.innerHTML = `
            <th style="width:52px"></th>
            <th class="inv-sortable" onclick="InvPage.sortItems('name')">Produit${this._sortArrow('name')}</th>
            <th class="inv-sortable" onclick="InvPage.sortItems('sku')">SKU${this._sortArrow('sku')}</th>
            <th class="inv-sortable" onclick="InvPage.sortItems('category')">Catégorie${this._sortArrow('category')}</th>
            <th class="cell-number inv-sortable" onclick="InvPage.sortItems('expected')">Théorique${this._sortArrow('expected')}</th>
            <th class="cell-number inv-sortable" onclick="InvPage.sortItems('counted')">Compté${this._sortArrow('counted')}</th>
            <th class="cell-number inv-sortable" onclick="InvPage.sortItems('diff')">Écart${this._sortArrow('diff')}</th>
            <th class="cell-actions"></th>`;

        if (!lines.length) {
            if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="tbl-empty">Aucun produit dans cette sélection</td></tr>';
            if (cards) cards.innerHTML = '<div class="inv-empty-state" role="listitem">Aucun produit dans cette sélection.</div>';
            if (loadMore) loadMore.style.display = 'none';
            return;
        }
        if (!visible.length) {
            if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="tbl-empty">Aucun produit pour ce filtre.</td></tr>';
            if (cards) cards.innerHTML = '<div class="inv-empty-state" role="listitem"><strong>Aucun produit pour ce filtre</strong><span>Choisissez un autre état pour poursuivre.</span></div>';
            if (loadMore) loadMore.style.display = 'none';
            return;
        }

        if (tbody) tbody.innerHTML = visible.map(({line, index:i}) => {
            const status = this._lineStatus(line);
            const hasCounted = status !== 'pending';
            const diff = hasCounted ? Number(line.counted) - Number(line.expected) : null;
            const diffDisplay = !hasCounted ? '—' : diff === 0 ? '✓ 0' : `${diff > 0 ? '+' : ''}${this._fmtQty(diff)}`;
            const diffClass = !hasCounted ? '' : diff === 0 ? 'text-success' : diff > 0 ? 'text-warning' : 'text-danger';
            const rowClass = status === 'ok' ? 'inv-row-ok' : status === 'diff' ? 'inv-row-diff' : '';
            const imgHtml = this._lineImageHtml(line, 'inv-prod-thumb', 'inv-prod-thumb-fb', 'Photo produit');
            return `<tr id="invRow-${i}" class="${rowClass}">
                <td class="inv-td-img">${imgHtml}</td>
                <td><strong>${this._esc(line.name)}</strong></td>
                <td class="col-mono">${this._esc(line.sku || '—')}</td>
                <td class="text-muted" style="font-size:12px">${this._esc(line.category || '—')}</td>
                <td class="cell-number">${this._fmtQty(line.expected)}</td>
                <td class="cell-number"><input type="number" class="inp inv-count-input ${hasCounted ? 'has-value' : ''}" id="invCount-${i}" min="0" step="0.01" inputmode="decimal" value="${hasCounted ? this._esc(line.counted) : ''}" placeholder="Quantité" onchange="InvPage.onCountChange(${i},'invCount-${i}')" onfocus="InvPage.onFocusLine(${i})"></td>
                <td class="cell-number ${diffClass}" id="invDiff-${i}"><strong>${diffDisplay}</strong></td>
                <td class="cell-actions">
                    <button class="btn btn-sm btn-secondary" onclick="InvPage.quickCount(${i})">Conforme</button>
                    <button class="btn btn-sm btn-primary" onclick="InvPage.openLineCountModal(${i})">Compter</button>
                    <button class="btn btn-sm btn-ghost" onclick="InvPage.resetLine(${i})" aria-label="Effacer le comptage">×</button>
                </td>
            </tr>`;
        }).join('');

        const mobileEntries = visible.slice(0, this.mobileVisibleLimit);
        if (cards) cards.innerHTML = mobileEntries.map(({line, index:i}) => this._renderInventoryCard(line, i)).join('');
        if (loadMore) {
            const remaining = Math.max(0, visible.length - mobileEntries.length);
            loadMore.style.display = remaining > 0 ? '' : 'none';
            loadMore.textContent = remaining > 30 ? `Afficher 30 produits de plus (${remaining} restants)` : `Afficher les ${remaining} derniers produits`;
        }
    },

    _renderInventoryCard(line, i) {
        const status = this._lineStatus(line);
        const hasCounted = status !== 'pending';
        const diff = hasCounted ? Number(line.counted) - Number(line.expected) : null;
        const diffLabel = !hasCounted ? 'À compter' : diff === 0 ? 'Conforme' : `Écart ${diff > 0 ? '+' : ''}${this._fmtQty(diff)}`;
        const statusClass = status === 'ok' ? 'is-ok' : status === 'diff' ? 'is-diff' : 'is-pending';
        const imgHtml = this._lineImageHtml(line, 'inv-card-thumb', 'inv-card-thumb-fb', `Photo ${line.name || 'produit'}`);
        return `<article class="inv-product-card ${statusClass}" id="invCard-${i}" role="listitem">
            <div class="inv-product-card__head">
                <div class="inv-product-card__image">${imgHtml}</div>
                <div class="inv-product-card__identity">
                    <span class="inv-product-card__category">${this._esc(line.category || 'Sans catégorie')}</span>
                    <h4>${this._esc(line.name || 'Produit')}</h4>
                    <span class="inv-product-card__sku">${this._esc(line.sku || line.barcode || 'Sans référence')}</span>
                </div>
                <span class="inv-product-card__status ${statusClass}" id="invCardStatus-${i}">${diffLabel}</span>
            </div>
            <div class="inv-product-card__stock">
                <div><span>Théorique</span><strong>${this._fmtQty(line.expected)}</strong></div>
                <label for="invCardCount-${i}"><span>Compté</span>
                    <input type="number" class="form-input inv-card-count ${hasCounted ? 'has-value' : ''}" id="invCardCount-${i}" min="0" step="0.01" inputmode="decimal" value="${hasCounted ? this._esc(line.counted) : ''}" placeholder="0" onchange="InvPage.onCountChange(${i},'invCardCount-${i}')" onfocus="InvPage.onFocusLine(${i})">
                </label>
                <div><span>Écart</span><strong id="invCardDiff-${i}" class="${statusClass}">${!hasCounted ? '—' : `${diff > 0 ? '+' : ''}${this._fmtQty(diff)}`}</strong></div>
            </div>
            <div class="inv-product-card__actions">
                <button type="button" class="btn btn-secondary" onclick="InvPage.quickCount(${i})">Conforme : ${this._fmtQty(line.expected)}</button>
                <button type="button" class="btn btn-primary" onclick="InvPage.openLineCountModal(${i})">Saisir</button>
                ${hasCounted ? `<button type="button" class="btn btn-ghost inv-card-reset" onclick="InvPage.resetLine(${i})" aria-label="Effacer le comptage de ${this._esc(line.name)}">Effacer</button>` : ''}
            </div>
        </article>`;
    },

    loadMoreItems() {
        this.mobileVisibleLimit += 30;
        this.renderItems();
        this._announce('Trente produits supplémentaires sont affichés.');
    },

    _lineStatus(line) {
        const hasCounted = line && line.counted !== null && line.counted !== undefined && line.counted !== '';
        if (!hasCounted) return 'pending';
        return Number(line.counted) === Number(line.expected) ? 'ok' : 'diff';
    },

    _filteredLineEntries(lines, filter) {
        const wanted = filter || 'all';
        return (lines || []).map((line, index) => ({ line, index })).filter(entry => {
            if (wanted === 'all') return true;
            if (wanted === 'counted') return this._lineStatus(entry.line) !== 'pending';
            return this._lineStatus(entry.line) === wanted;
        });
    },

    _syncFilterButtons(selector, active) {
        document.querySelectorAll(selector).forEach(btn => {
            const on = (btn.dataset.filter || 'all') === (active || 'all');
            btn.classList.toggle('active', on);
            btn.setAttribute('aria-pressed', on ? 'true' : 'false');
        });
    },

    _updateFilterBadges(lines) {
        const counts = { all: 0, counted: 0, ok: 0, diff: 0, pending: 0 };
        (lines || []).forEach(line => {
            counts.all++;
            const st = this._lineStatus(line);
            counts[st]++;
            if (st !== 'pending') counts.counted++;
        });
        document.querySelectorAll('[data-inv-filter-count]').forEach(el => {
            const key = el.dataset.invFilterCount || 'all';
            el.textContent = counts[key] ?? 0;
        });
    },

    renderDetailLines() {
        const tbody = document.getElementById('invDetailBody');
        const cards = document.getElementById('invDetailCards');
        if ((!tbody && !cards) || !this._currentInv) return;
        const lines = this._currentInv.lines || [];
        this._updateFilterBadges(lines);
        const visible = this._filteredLineEntries(lines, this.detailStatusFilter);
        if (!visible.length) {
            if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="tbl-empty">Aucune ligne pour ce filtre.</td></tr>';
            if (cards) cards.innerHTML = '<div class="inv-empty-state" role="listitem">Aucune ligne pour ce filtre.</div>';
            return;
        }

        if (tbody) tbody.innerHTML = visible.map(({ line: l }) => {
            const status = this._lineStatus(l);
            const hasCounted = status !== 'pending';
            const diff = hasCounted ? Number(l.counted) - Number(l.expected) : null;
            const diffHtml = !hasCounted ? '<span class="text-muted">—</span>'
                : diff === 0 ? '<span class="text-green font-bold">✓ 0</span>'
                : `<span class="${diff > 0 ? 'text-orange' : 'text-red'} font-bold">${diff > 0 ? '+' : ''}${this._fmtQty(diff)}</span>`;
            const resultHtml = status === 'pending' ? '<span class="badge badge-gray">Non compté</span>'
                : status === 'ok' ? '<span class="badge badge-green">Conforme</span>'
                : `<span class="badge badge-orange">Écart ${diff > 0 ? '+' : ''}${this._fmtQty(diff)}</span>`;
            const imageHtml = this._lineImageHtml(l, 'inv-prod-thumb inv-detail-thumb', 'inv-prod-thumb-fb inv-detail-thumb-fb', 'Photo produit');
            return `<tr class="${status === 'ok' ? 'inv-row-ok' : status === 'diff' ? 'inv-row-diff' : ''}">
                <td class="inv-td-img">${imageHtml}</td>
                <td><strong>${this._esc(l.name)}</strong><div class="inv-line-sub">${this._esc(l.barcode || '')}</div></td>
                <td class="col-mono">${this._esc(l.sku || '—')}</td>
                <td class="text-muted">${this._esc(l.category || '—')}</td>
                <td class="cell-number">${this._fmtQty(l.expected)}</td>
                <td class="cell-number">${hasCounted ? this._fmtQty(l.counted) : '<span class="text-muted">—</span>'}</td>
                <td class="cell-number">${diffHtml}</td><td>${resultHtml}</td>
            </tr>`;
        }).join('');

        if (cards) cards.innerHTML = visible.map(({ line: l }) => {
            const status = this._lineStatus(l);
            const hasCounted = status !== 'pending';
            const diff = hasCounted ? Number(l.counted) - Number(l.expected) : null;
            const statusClass = status === 'ok' ? 'is-ok' : status === 'diff' ? 'is-diff' : 'is-pending';
            const imageHtml = this._lineImageHtml(l, 'inv-card-thumb', 'inv-card-thumb-fb', `Photo ${l.name || 'produit'}`);
            return `<article class="inv-detail-card ${statusClass}" role="listitem">
                <div class="inv-detail-card__head">
                    <div class="inv-product-card__image">${imageHtml}</div>
                    <div><span>${this._esc(l.category || 'Sans catégorie')}</span><h4>${this._esc(l.name || 'Produit')}</h4><small>${this._esc(l.sku || l.barcode || 'Sans référence')}</small></div>
                </div>
                <dl class="inv-detail-card__values">
                    <div><dt>Théorique</dt><dd>${this._fmtQty(l.expected)}</dd></div>
                    <div><dt>Compté</dt><dd>${hasCounted ? this._fmtQty(l.counted) : '—'}</dd></div>
                    <div><dt>Écart</dt><dd class="${statusClass}">${!hasCounted ? '—' : `${diff > 0 ? '+' : ''}${this._fmtQty(diff)}`}</dd></div>
                </dl>
                <div class="inv-detail-card__result ${statusClass}">${status === 'pending' ? 'Non compté' : status === 'ok' ? 'Conforme' : `Écart ${diff > 0 ? '+' : ''}${this._fmtQty(diff)}`}</div>
            </article>`;
        }).join('');
    },

    onCountChange(i, sourceId = '') {
        if (this._draftLocked) return;
        const input = document.getElementById(sourceId || `invCount-${i}`) || document.getElementById(`invCardCount-${i}`);
        if (!input || !this.invData?.lines?.[i]) return;
        const raw = input.value.trim();
        const value = raw === '' ? null : this._toNumber(raw);
        if (value !== null && value < 0) {
            toast('La quantité ne peut pas être négative', 'warning');
            input.value = '';
            return;
        }
        this.invData.lines[i].counted = value;
        this.updateStats();
        this._scheduleDraftSave();
        if (this.itemStatusFilter !== 'all') this.renderItems(); else this.updateRow(i);
        this._announce(value === null ? 'Comptage effacé.' : `Quantité ${this._fmtQty(value)} enregistrée pour ${this.invData.lines[i].name}.`);
    },

    _syncCountInputs(i) {
        const line = this.invData?.lines?.[i];
        if (!line) return;
        const hasCounted = this._lineStatus(line) !== 'pending';
        [`invCount-${i}`, `invCardCount-${i}`].forEach(id => {
            const input = document.getElementById(id);
            if (!input) return;
            input.value = hasCounted ? line.counted : '';
            input.classList.toggle('has-value', hasCounted);
        });
    },

    updateRow(i) {
        const line = this.invData?.lines?.[i];
        if (!line) return;
        const status = this._lineStatus(line);
        const hasCounted = status !== 'pending';
        const diff = hasCounted ? Number(line.counted) - Number(line.expected) : null;
        const diffText = !hasCounted ? '—' : `${diff > 0 ? '+' : ''}${this._fmtQty(diff)}`;
        this._syncCountInputs(i);

        const row = document.getElementById(`invRow-${i}`);
        if (row) {
            row.classList.remove('inv-row-ok', 'inv-row-diff');
            if (status === 'ok') row.classList.add('inv-row-ok');
            if (status === 'diff') row.classList.add('inv-row-diff');
        }
        const diffCell = document.getElementById(`invDiff-${i}`);
        if (diffCell) {
            diffCell.className = `cell-number ${status === 'ok' ? 'text-success' : status === 'diff' ? (diff > 0 ? 'text-warning' : 'text-danger') : ''}`;
            diffCell.innerHTML = `<strong>${status === 'ok' ? '✓ 0' : diffText}</strong>`;
        }

        const card = document.getElementById(`invCard-${i}`);
        if (card) {
            card.classList.remove('is-ok', 'is-diff', 'is-pending');
            card.classList.add(status === 'ok' ? 'is-ok' : status === 'diff' ? 'is-diff' : 'is-pending');
        }
        const cardStatus = document.getElementById(`invCardStatus-${i}`);
        if (cardStatus) {
            cardStatus.className = `inv-product-card__status ${status === 'ok' ? 'is-ok' : status === 'diff' ? 'is-diff' : 'is-pending'}`;
            cardStatus.textContent = status === 'pending' ? 'À compter' : status === 'ok' ? 'Conforme' : `Écart ${diffText}`;
        }
        const cardDiff = document.getElementById(`invCardDiff-${i}`);
        if (cardDiff) {
            cardDiff.className = status === 'ok' ? 'is-ok' : status === 'diff' ? 'is-diff' : 'is-pending';
            cardDiff.textContent = diffText;
        }
    },

    onFocusLine(i) {
        document.querySelectorAll('#invItemsBody tr, #invItemsCards .inv-product-card').forEach(r => r.classList.remove('inv-row-active'));
        document.getElementById(`invRow-${i}`)?.classList.add('inv-row-active');
        document.getElementById(`invCard-${i}`)?.classList.add('inv-row-active');
    },

    quickCount(i) {
        if (this._draftLocked) return;
        const line = this.invData?.lines?.[i];
        if (!line) return;
        line.counted = line.expected;
        this.updateStats();
        this._scheduleDraftSave();
        if (this.itemStatusFilter !== 'all') this.renderItems(); else this.updateRow(i);
        this._announce(`${line.name} marqué conforme.`);
        this.focusNextEmpty(i);
    },

    resetLine(i) {
        if (this._draftLocked) return;
        const line = this.invData?.lines?.[i];
        if (!line) return;
        line.counted = null;
        this.updateStats();
        this._scheduleDraftSave();
        if (this.itemStatusFilter !== 'all') this.renderItems(); else this.updateRow(i);
        this._announce(`Comptage effacé pour ${line.name}.`);
    },

    validateAll() {
        if (this._draftLocked) return;
        if (!this.invData || !confirm('Marquer tous les produits comme conformes au stock théorique ?')) return;
        this.invData.lines.forEach(line => { line.counted = line.expected; });
        this.updateStats();
        this._scheduleDraftSave();
        this.renderItems();
        toast('Tous les produits sont marqués conformes', 'success');
    },

    resetAll() {
        if (this._draftLocked) return;
        if (!this.invData || !confirm('Effacer tous les comptages ?')) return;
        this.invData.lines.forEach(line => { line.counted = null; });
        this.updateStats();
        this._scheduleDraftSave();
        this.renderItems();
        toast('Les comptages ont été réinitialisés', 'success');
    },

    focusNextEmpty(cur, options = {}) {
        const allowFocus = options.force === true || this._shouldAutoFocusInputs();
        for (let i = cur + 1; i < this.invData.lines.length; i++) {
            const line = this.invData.lines[i];
            if (this._lineStatus(line) === 'pending') {
                this._ensureMobileLineVisible(i);
                const input = document.getElementById(this._isCompactFlow() ? `invCardCount-${i}` : `invCount-${i}`)
                    || document.getElementById(`invCount-${i}`);
                this.scrollToRow(i);
                if (input && allowFocus) { input.focus(); input.select(); }
                return;
            }
        }
    },

    _ensureMobileLineVisible(i) {
        const entries = this._filteredLineEntries(this.invData?.lines || [], this.itemStatusFilter);
        const pos = entries.findIndex(entry => entry.index === i);
        if (pos >= this.mobileVisibleLimit) {
            this.mobileVisibleLimit = Math.ceil((pos + 1) / 30) * 30;
            this.renderItems();
        }
    },

    scrollToRow(i) {
        const target = this._isCompactFlow() ? document.getElementById(`invCard-${i}`) : document.getElementById(`invRow-${i}`);
        target?.scrollIntoView({ behavior: 'smooth', block: 'center' });
    },

    updateStats() {
        if (!this.invData) return;
        const lines = this.invData.lines || [];
        let checked = 0, diff = 0, pending = 0;
        lines.forEach(line => {
            const status = this._lineStatus(line);
            if (status === 'pending') pending++;
            else if (status === 'ok') checked++;
            else diff++;
        });
        const total = lines.length;
        const counted = checked + diff;
        const pct = total > 0 ? Math.round((counted / total) * 100) : 0;
        const setText = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; };
        setText('invTotalCount', total);
        setText('invCheckedCount', checked);
        setText('invDiffCount', diff);
        setText('invPendingCount', pending);
        setText('invProgressPct', `${pct}%`);
        setText('invMobileProgressText', `${counted} sur ${total} produit${total > 1 ? 's' : ''} compté${counted > 1 ? 's' : ''}`);
        setText('invMobileProgressSub', `${diff} écart${diff > 1 ? 's' : ''} · ${pending} restant${pending > 1 ? 's' : ''}`);
        const bar = document.getElementById('invProgressBar');
        if (bar) bar.style.width = `${pct}%`;
        this._updateFilterBadges(lines);
    },

    searchAndFocus() {
        const input = document.getElementById('invScanInput');
        if (!input || !this.invData) return;
        const code = input.value.trim().toLowerCase();
        if (!code) return;
        const idx = this.invData.lines.findIndex(line => this._lineMatchesCode(line, code));
        if (idx < 0) {
            toast(`Produit non trouvé : ${code}`, 'warning');
            this._announce('Aucun produit correspondant.');
            return;
        }
        this.itemStatusFilter = 'all';
        this._syncFilterButtons('.inv-status-filter', 'all');
        const allEntries = this._filteredLineEntries(this.invData.lines, 'all');
        const pos = allEntries.findIndex(entry => entry.index === idx);
        this.mobileVisibleLimit = Math.max(this.mobileVisibleLimit, Math.ceil((pos + 1) / 30) * 30);
        this.renderItems();
        const countInput = document.getElementById(this._isCompactFlow() ? `invCardCount-${idx}` : `invCount-${idx}`)
            || document.getElementById(`invCount-${idx}`);
        this.scrollToRow(idx);
        if (countInput) { countInput.focus(); countInput.select(); }
        const target = document.getElementById(this._isCompactFlow() ? `invCard-${idx}` : `invRow-${idx}`);
        target?.classList.add('inv-row-highlight');
        setTimeout(() => target?.classList.remove('inv-row-highlight'), 1500);
        input.value = '';
        this._announce(`${this.invData.lines[idx].name} trouvé.`);
    },

    openLineCountModal(i) {
        if (!this.invData?.lines?.[i]) return;
        const line = this.invData.lines[i];
        const current = line.counted !== null && line.counted !== undefined ? line.counted : '';
        Modal.show('Compter ce produit', `
            <div class="inv-line-count-modal">
                <h3>${this._esc(line.name)}</h3>
                <p class="text-muted">${this._esc(line.sku || 'Sans SKU')} — stock théorique : <strong>${this._fmtQty(line.expected)}</strong></p>
                <label class="lbl" for="invLineModalQty">Quantité réelle</label>
                <input type="number" class="inp" id="invLineModalQty" min="0" step="0.01" inputmode="decimal" value="${current}" placeholder="Quantité réelle">
            </div>
        `, [
            { label: 'Conforme', cls: 'btn-secondary', onclick: () => { const el = document.getElementById('invLineModalQty'); if (el) el.value = line.expected; InvPage.applyLineCountFromModal(i); } },
            { label: 'Valider', cls: 'btn-primary', onclick: () => InvPage.applyLineCountFromModal(i) },
            { label: 'Annuler', cls: 'btn-ghost', onclick: () => Modal.close() }
        ], 'info');
        setTimeout(() => {
            const el = document.getElementById('invLineModalQty');
            if (el) {
                if (this._shouldAutoFocusInputs()) { el.focus(); el.select(); }
                el.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); InvPage.applyLineCountFromModal(i); } });
            }
        }, 50);
    },

    applyLineCountFromModal(i) {
        if (this._draftLocked) return;
        const el = document.getElementById('invLineModalQty');
        const raw = el?.value?.trim() || '';
        if (raw === '') { toast('Saisissez la quantité réelle', 'warning'); el?.focus(); return; }
        const val = this._toNumber(raw);
        if (val < 0) { toast('La quantité ne peut pas être négative', 'warning'); el?.focus(); return; }
        this.invData.lines[i].counted = val;
        this.updateStats();
        this._scheduleDraftSave();
        if (this.itemStatusFilter !== 'all') this.renderItems(); else this.updateRow(i);
        Modal.close();
        this._announce(`Quantité ${this._fmtQty(val)} enregistrée pour ${this.invData.lines[i].name}.`);
        this.focusNextEmpty(i);
    },

    // ══════════════════════════════════════════════════════════════
    // REVUE ET CLÔTURE
    // ══════════════════════════════════════════════════════════════
    async validateInventory() {
        if (!this.invData?.id) return;
        const localSummary = this._getInventorySummary();
        if (!localSummary.counted) {
            toast('Comptez au moins un produit avant de lancer la revue.', 'warning');
            return;
        }
        if (this.invData.mode !== 'partial' && localSummary.pending > 0) {
            toast(`L’inventaire complet comporte encore ${localSummary.pending} produit(s) à compter.`, 'warning');
        }
        try {
            await this._saveDraftNow(false);
            const payload = await Api.invReviewDraft({
                draftId: this.invData.id,
                expectedRevision: this.invData.revision,
                lines: this._draftCountPayload(),
            });
            this.reviewData = payload;
            this.invData = { ...this.invData, ...(payload.draft || {}) };
            this.renderReview(payload);
            this._showView('review');
            this._announce('Revue des écarts ouverte. Aucune correction de stock n’est encore appliquée.');
        } catch (error) {
            toast(error.message || 'Impossible de préparer la revue', 'error');
        }
    },

    _getInventorySummary() {
        const lines = this.invData?.lines || [];
        const countedLines = lines.filter(line => this._lineStatus(line) !== 'pending');
        const differenceLines = countedLines.filter(line => this._lineStatus(line) === 'diff');
        const conformLines = countedLines.filter(line => this._lineStatus(line) === 'ok');
        return {
            total: lines.length,
            counted: countedLines.length,
            pending: lines.length - countedLines.length,
            conform: conformLines.length,
            differences: differenceLines.length,
            positive: differenceLines.filter(line => Number(line.counted) > Number(line.expected)).length,
            negative: differenceLines.filter(line => Number(line.counted) < Number(line.expected)).length,
            differenceLines,
        };
    },

    renderReview(payload) {
        const draft = payload?.draft || this.invData || {};
        const summary = payload?.summary || draft.reviewSummary || {};
        this.reviewData = { draft, summary };
        const warehouse = this.whs.find(item => String(item.id) === String(draft.warehouseId));
        this._setText('invReviewPageMeta', `${draft.title || 'Inventaire'} · ${warehouse?.name || ''} · ${this._fmtMode(draft.mode)}`);
        this._setText('invReviewDiffCount', String(summary.corrections ?? summary.differences ?? 0));

        const summaryHost = document.getElementById('invReviewSummaryCards');
        if (summaryHost) {
            summaryHost.innerHTML = `
                <div class="inv-review-summary-card"><span>Comptés</span><strong>${summary.counted || 0}<small> / ${summary.total || 0}</small></strong></div>
                <div class="inv-review-summary-card is-ok"><span>Conformes</span><strong>${summary.conform || 0}</strong></div>
                <div class="inv-review-summary-card is-warning"><span>Écarts constatés</span><strong>${summary.differences || 0}</strong><small>${summary.corrections ?? summary.differences ?? 0} correction(s) à appliquer</small></div>
                <div class="inv-review-summary-card is-muted"><span>Non comptés</span><strong>${summary.pending || 0}</strong><small>${draft.mode === 'partial' ? 'ignorés' : 'bloquants'}</small></div>`;
        }

        const blocking = document.getElementById('invReviewBlocking');
        if (blocking) {
            blocking.style.display = summary.canApply ? 'none' : '';
            blocking.innerHTML = `<strong>Clôture impossible</strong><span>${this._esc(summary.blockingReason || 'Revenez au comptage pour terminer.')}</span>`;
        }
        const changed = document.getElementById('invReviewStockChanged');
        if (changed) {
            const count = Number(summary.stockChanged || 0);
            changed.style.display = count ? '' : 'none';
            changed.innerHTML = `<strong>Stock modifié pendant le comptage</strong><span>${count} produit${count > 1 ? 's ont' : ' a'} changé depuis le lancement. La colonne « État actuel » permet de vérifier ces cas avant application.</span>`;
        }

        const differences = Array.isArray(summary.differenceLines) ? summary.differenceLines : [];
        const tbody = document.getElementById('invReviewBody');
        const cards = document.getElementById('invReviewCards');
        const noDiff = document.getElementById('invReviewNoDiff');
        const tableWrap = document.getElementById('invReviewTableWrap');
        if (noDiff) noDiff.style.display = differences.length ? 'none' : '';
        if (tableWrap) tableWrap.style.display = differences.length ? '' : 'none';
        if (tbody) tbody.innerHTML = differences.map(line => this._renderReviewRow(line)).join('');
        if (cards) {
            cards.style.display = differences.length ? '' : 'none';
            cards.innerHTML = differences.map(line => this._renderReviewCard(line)).join('');
        }

        const pending = Array.isArray(summary.pendingLines) ? summary.pendingLines : [];
        const pendingShell = document.getElementById('invReviewPendingShell');
        this._setText('invReviewPendingCount', String(summary.pending || 0));
        this._setText('invReviewPendingHint', draft.mode === 'partial'
            ? 'Ces produits resteront inchangés lors de la clôture du contrôle ciblé.'
            : 'Ils doivent être comptés avant de pouvoir clôturer l’inventaire complet.');
        if (pendingShell) pendingShell.style.display = Number(summary.pending || 0) ? '' : 'none';
        const pendingList = document.getElementById('invReviewPendingList');
        if (pendingList) {
            pendingList.innerHTML = pending.map(line => `<span><strong>${this._esc(line.name || 'Produit')}</strong><small>${this._esc(line.sku || 'Sans référence')}</small></span>`).join('')
                + (Number(summary.pending || 0) > pending.length ? `<span class="is-more">+ ${Number(summary.pending || 0) - pending.length} autre(s)</span>` : '');
        }

        const checkbox = document.getElementById('invReviewConfirm');
        if (checkbox) checkbox.checked = false;
        const confirmBox = document.getElementById('invReviewConfirmBox');
        if (confirmBox) confirmBox.style.display = summary.canApply ? '' : 'none';
        const apply = document.getElementById('btnApplyInventory');
        if (apply) {
            const corrections = Number(summary.corrections ?? summary.differences ?? 0);
            apply.textContent = corrections > 0
                ? `Appliquer ${corrections} correction${corrections > 1 ? 's' : ''} et clôturer`
                : 'Clôturer sans correction';
        }
        this._syncApplyButton();
    },

    _renderReviewRow(line) {
        const difference = Number(line.difference || 0);
        const correction = Number(line.appliedDifference ?? line.difference ?? 0);
        const changed = !!line.stockChanged;
        return `<tr class="${changed ? 'inv-review-stock-changed' : ''}">
            <td><strong>${this._esc(line.name || 'Produit')}</strong><small class="inv-review-line-cat">${this._esc(line.category || '')}</small></td>
            <td class="col-mono">${this._esc(line.sku || '—')}</td>
            <td class="cell-number">${this._fmtQty(line.expected)}</td>
            <td class="cell-number"><strong>${this._fmtQty(line.counted)}</strong></td>
            <td class="cell-number ${correction > 0 ? 'text-orange' : 'text-red'}"><strong>${correction > 0 ? '+' : ''}${this._fmtQty(correction)}</strong></td>
            <td>${changed ? `<span class="badge badge-orange">Actuel : ${this._fmtQty(line.currentStock)}</span>` : '<span class="badge badge-gray">Inchangé</span>'}</td>
        </tr>`;
    },

    _renderReviewCard(line) {
        const difference = Number(line.difference || 0);
        const correction = Number(line.appliedDifference ?? line.difference ?? 0);
        const image = this._lineImageHtml(line, 'inv-card-thumb', 'inv-card-thumb-fb', `Photo ${line.name || 'produit'}`);
        return `<article class="inv-review-card ${line.stockChanged ? 'is-stock-changed' : ''}" role="listitem">
            <div class="inv-review-card__head"><div class="inv-product-card__image">${image}</div><div><span>${this._esc(line.category || 'Sans catégorie')}</span><h4>${this._esc(line.name || 'Produit')}</h4><small>${this._esc(line.sku || 'Sans référence')}</small></div></div>
            <dl><div><dt>Théorique</dt><dd>${this._fmtQty(line.expected)}</dd></div><div><dt>Compté</dt><dd>${this._fmtQty(line.counted)}</dd></div><div><dt>Correction</dt><dd class="${correction > 0 ? 'is-plus' : 'is-minus'}">${correction > 0 ? '+' : ''}${this._fmtQty(correction)}</dd></div></dl>
            ${line.stockChanged ? `<p class="inv-review-card__warning">Stock actuel : <strong>${this._fmtQty(line.currentStock)}</strong> — modifié depuis le lancement.</p>` : ''}
        </article>`;
    },

    _syncApplyButton() {
        const summary = this.reviewData?.summary || {};
        const checkbox = document.getElementById('invReviewConfirm');
        const apply = document.getElementById('btnApplyInventory');
        if (!apply) return;
        apply.disabled = !summary.canApply || !checkbox?.checked;
    },

    backToCounting() {
        if (!this.invData) return;
        this._showView('progress');
        this._announce('Retour au comptage. Toute modification invalidera la revue précédente.');
    },

    async confirmValidateInventory() {
        if (!this.invData?.id || !this.reviewData?.summary?.canApply) return;
        const checkbox = document.getElementById('invReviewConfirm');
        if (!checkbox?.checked) {
            toast('Confirmez la vérification des écarts avant de clôturer.', 'warning');
            checkbox?.focus();
            return;
        }
        const button = document.getElementById('btnApplyInventory');
        try {
            if (button) button.disabled = true;
            const validated = await Api.invValidate({
                draftId: this.invData.id,
                expectedRevision: this.invData.revision,
            });
            const appliedCorrections = Number(validated.appliedCorrections ?? validated.discrepancies ?? 0);
            toast(appliedCorrections
                ? `Inventaire clôturé — ${appliedCorrections} correction(s) appliquée(s).`
                : (validated.discrepancies ? `Inventaire clôturé — ${validated.discrepancies} écart(s), stock déjà à jour.` : 'Inventaire clôturé sans écart.'), 'success');
            const closedDraftId = this.invData.id;
            this.invData = null;
            this.reviewData = null;
            this._clearLocalRecovery(closedDraftId);
            this._setDraftLocked(false);
            await this.stopScanner();
            this._showView('home');
            await Promise.all([this.loadDrafts(), this.loadHistory()]);
        } catch (error) {
            if (Number(error?.status) === 409 && /stock a changé|actualisez les écarts/i.test(error.message || '')) {
                toast('Le stock a évolué depuis la revue. Les écarts vont être actualisés.', 'warning');
                await this.refreshReviewAfterStockChange();
            } else if (Number(error?.status) === 409) {
                this._setDraftLocked(true);
                this._setSaveStatus('conflict');
                this._showDraftConflict();
            } else {
                toast(error.message || 'Impossible de clôturer l’inventaire', 'error');
            }
            if (button) button.disabled = false;
        }
    },

    async refreshReviewAfterStockChange() {
        if (!this.invData?.id) return;
        try {
            const payload = await Api.invReviewDraft({
                draftId: this.invData.id,
                expectedRevision: this.invData.revision,
                lines: this._draftCountPayload(),
            });
            this.invData = { ...this.invData, ...(payload.draft || {}) };
            this.reviewData = payload;
            this.renderReview(payload);
            this._setSaveStatus('saved', this.invData.updatedAt);
            this._announce('La revue a été actualisée avec le stock courant. Vérifiez à nouveau les écarts.');
        } catch (error) {
            toast(error.message || 'Impossible d’actualiser la revue', 'error');
        }
    },

    cancelInventory() {
        if (!this.invData?.id) {
            this.invData = null;
            this._showView('home');
            return;
        }
        Modal.show('Quitter l’inventaire', `
            <div class="inv-exit-choice">
                <p>Vos comptages sont enregistrés sur le serveur. Vous pouvez reprendre cet inventaire plus tard, y compris depuis un autre appareil.</p>
                <div class="inv-exit-choice__warning"><strong>Supprimer le brouillon</strong><span>Cette action efface définitivement tous les comptages de cette session.</span></div>
            </div>
        `, [
            { label: 'Continuer le comptage', cls: 'btn-secondary', onclick: () => Modal.close() },
            { label: 'Enregistrer et quitter', cls: 'btn-primary', onclick: () => InvPage.leaveInventory() },
            { label: 'Supprimer le brouillon', cls: 'btn-danger', onclick: () => InvPage.discardCurrentDraft() },
        ], 'info');
    },

    async leaveInventory() {
        try {
            await this._saveDraftNow(false);
            Modal.close();
            await this.stopScanner();
            this.invData = null;
            this.reviewData = null;
            this._showView('home');
            await this.loadDrafts();
            toast('Inventaire enregistré. Vous pourrez le reprendre plus tard.', 'success');
        } catch (_) {}
    },

    async discardCurrentDraft() {
        if (!this.invData?.id) return;
        const id = this.invData.id;
        try {
            await Api.invDiscardDraft({ draftId: id, expectedRevision: this.invData.revision });
            this._clearLocalRecovery(id);
            Modal.close();
            await this.stopScanner();
            this.invData = null;
            this.reviewData = null;
            this._showView('home');
            await this.loadDrafts();
            toast('Brouillon supprimé', 'success');
        } catch (error) {
            toast(error.message || 'Impossible de supprimer le brouillon', 'error');
        }
    },

    // ══════════════════════════════════════════════════════════════
    // IMPRESSION
    // ══════════════════════════════════════════════════════════════
    printCurrent(includeCounts = false) {
        if (!this.invData) return;
        const inv = {
            title:      this.invData.title,
            date:       this.invData.startedAt || this.invData.createdAt || new Date().toISOString(),
            warehouseId:this.invData.warehouseId,
            mode:       this.invData.mode,
            categories: Array.isArray(this.invData.categories) ? this.invData.categories : [],
            category:   Array.isArray(this.invData.categories) && this.invData.categories.length > 0
                        ? this.invData.categories.join(', ') : '',
            operator:   this.invData.operator,
            lines:      this.invData.lines,
        };
        const wh = this.whs.find(w => String(w.id) === String(inv.warehouseId));
        this._doPrint(inv, wh, false, includeCounts);
    },

    printDetail() {
        if (!this._currentInv) return;
        const wh = this.whs.find(w => String(w.id) === String(this._currentInv.warehouseId));
        this._doPrint(this._currentInv, wh, true, true);
    },

    async printById(invId) {
        try {
            const invs = await Api.invHistory();
            const inv  = invs.find(i => String(i.id) === String(invId));
            if (!inv) { toast('Inventaire introuvable', 'error'); return; }
            const wh = this.whs.find(w => String(w.id) === String(inv.warehouseId));
            this._doPrint(inv, wh, true, true);
        } catch(e) { toast(e.message, 'error'); }
    },

    _doPrint(inv, wh, validated, includeCounts = validated) {
        const title   = inv.title || `Inventaire du ${this._fmtDateShort(inv.date)}`;
        const date    = this._fmtDate(inv.date || new Date().toISOString());
        const lines   = inv.lines || [];
        const company = document.title.replace(' — MageStock','') || 'MageStock';

        const rows = lines.map(l => {
            const hasCounted = includeCounts && l.counted !== null && l.counted !== undefined && l.counted !== '';
            const diff = hasCounted ? this._toNumber(l.counted) - this._toNumber(l.expected) : null;
            const diffStr = diff === null ? '' : diff === 0 ? '✓ 0' : (diff > 0 ? `+${this._fmtQty(diff)}` : this._fmtQty(diff));
            const diffColor = diff === null ? 'inherit' : diff === 0 ? '#15803d' : diff > 0 ? '#c2410c' : '#b91c1c';
            const productHtml = this._printProductHtml(l);
            const countedCell = hasCounted ? this._fmtQty(l.counted) : '<span style="display:block;min-height:14px;border-bottom:1px dotted #64748b">&nbsp;</span>';
            return `<tr class="ms-print-avoid-break">
                <td>${productHtml}</td>
                <td class="ms-print-muted">${this._esc(l.sku || '—')}</td>
                <td class="ms-print-muted">${this._esc(l.category || '—')}</td>
                <td style="text-align:right;font-weight:800">${this._fmtQty(l.expected)}</td>
                <td style="text-align:right;border-left:1px solid #dbe3ee;border-right:1px solid #dbe3ee;min-width:58px">${countedCell}</td>
                <td style="text-align:right;font-weight:900;color:${diffColor}">${diffStr}</td>
                <td>${hasCounted ? (diff === 0 ? '<span class="badge">Conforme</span>' : '<span class="badge">Écart</span>') : '<span class="badge">Non compté</span>'}</td>
            </tr>`;
        }).join('');

        const countedCount = lines.filter(l => l.counted !== null && l.counted !== undefined && l.counted !== '').length;
        const diffCount = includeCounts ? lines.filter(l => {
            if(l.counted === null || l.counted === undefined || l.counted === '') return false;
            return this._toNumber(l.counted) !== this._toNumber(l.expected);
        }).length : null;

        const tableHtml = `<table class="ms-print-inventory-table">
            <thead><tr>
                <th>Produit</th><th>SKU</th><th>Catégorie</th>
                <th style="text-align:right">Théorique</th>
                <th style="text-align:right">Compté</th>
                <th style="text-align:right">Écart</th><th>Résultat</th>
            </tr></thead>
            <tbody>${rows}</tbody>
        </table>`;

        const summaryHtml = `<div class="ms-print-summary">
            <div class="ms-print-summary-card"><div class="ms-print-summary-value">${lines.length}</div><div class="ms-print-summary-label">Produits</div></div>
            <div class="ms-print-summary-card"><div class="ms-print-summary-value">${countedCount}</div><div class="ms-print-summary-label">Comptés</div></div>
            <div class="ms-print-summary-card"><div class="ms-print-summary-value">${includeCounts ? (diffCount || 0) : '—'}</div><div class="ms-print-summary-label">Écarts</div></div>
            <div class="ms-print-summary-card"><div class="ms-print-summary-value">${validated ? 'Validé' : (includeCounts ? 'En revue' : 'À compter')}</div><div class="ms-print-summary-label">Statut</div></div>
        </div>`;

        const bodyHtml = `
            <div class="ms-print-note">${validated ? 'Document de synthèse de l’inventaire validé.' : (includeCounts ? 'Document de revue avant application des quantités comptées au stock.' : 'Les photos produits facilitent l’identification terrain. Complétez la colonne “Compté”, puis signez la fiche de comptage.')}</div>
            <div class="ms-print-signatures">
                <div class="ms-print-signature">Opérateur (nom + signature)</div>
                <div class="ms-print-signature">Date et visa</div>
            </div>`;

        if(window.MageStockPrint){
            window.MageStockPrint.open({
                title,
                subtitle: validated ? 'Inventaire validé' : (includeCounts ? 'Revue avant clôture' : 'Fiche de comptage'),
                orientation: 'portrait',
                meta: {
                    'Date': date,
                    'Magasin': wh ? wh.name : '?',
                    'Type': this._fmtMode(inv),
                    'Périmètre': this._fmtCategories(inv),
                    'Produits': lines.length,
                    ...(inv.operator ? {'Opérateur': inv.operator} : {}),
                    ...(validated && inv.discrepancies !== undefined ? {'Écarts': inv.discrepancies === 0 ? 'Aucun' : inv.discrepancies} : {})
                },
                summaryHtml,
                tableHtml,
                bodyHtml,
                footer: 'Inventaire imprimé depuis MageStock'
            });
            return;
        }

        const w = window.open('', '_blank');
        if (w) {
            w.document.write(`<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><title>${this._esc(title)}</title></head><body>${tableHtml}<script>window.print();<\/script></body></html>`);
            w.document.close();
        }
    },

    _findProductForLine(line) {
        if (!line) return null;
        const pid = line.productId ?? line.id;
        if (pid !== undefined && pid !== null && this.prods) {
            const found = this.prods.find(p => String(p.id) === String(pid));
            if (found) return found;
        }
        const sku = (line.sku || '').trim();
        if (sku && this.prods) {
            const found = this.prods.find(p => String(p.sku || '').trim() === sku);
            if (found) return found;
        }
        return null;
    },

    _lineImageSrc(line) {
        const product = this._findProductForLine(line);
        const raw = (line && (line.image || line.imageUrl || line.photo)) || (product && (product.image || product.imageUrl || product.photo || product.picture)) || '';
        const img = String(raw || '').trim();
        if (!img) return '';
        if (/^(https?:|data:|blob:)/i.test(img)) return img;
        if (typeof Api !== 'undefined' && typeof Api.imageUrl === 'function') {
            return Api.imageUrl(img);
        }
        if (typeof Api !== 'undefined' && typeof Api.publicUrl === 'function') {
            return Api.publicUrl(img);
        }
        const runtime = window.MageStockRuntime || {};
        const base = String(runtime.basePath || '').replace(/\/+$/, '');
        let path = img.replace(/^\.\//, '').replace(/^\/+/, '');
        const cleanBase = base.replace(/^\/+/, '');
        if(cleanBase && path.startsWith(cleanBase + '/')) path = path.slice(cleanBase.length + 1);
        return (base ? base : '') + '/' + path;
    },

    _lineImageHtml(line, imgClass, fallbackClass, alt) {
        const src = this._lineImageSrc(line);
        const label = alt || (line && line.name ? `Photo ${line.name}` : 'Photo produit');
        if (src) {
            return `<img src="${this._esc(src)}" class="${imgClass}" loading="lazy" alt="${this._esc(label)}" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"><span class="${fallbackClass}" style="display:none" aria-hidden="true">📦</span>`;
        }
        return `<span class="${fallbackClass}" aria-hidden="true">📦</span>`;
    },

    _printProductHtml(line) {
        const src = this._lineImageSrc(line);
        const fallback = '<span class="ms-print-product-ph" aria-hidden="true">📦</span>';
        const img = src
            ? `<img src="${this._esc(src)}" alt="${this._esc(line?.name ? 'Photo ' + line.name : 'Photo produit')}" onerror="this.style.display='none';var n=this.nextElementSibling;if(n)n.style.display='inline-flex'"><span class="ms-print-product-ph" style="display:none" aria-hidden="true">📦</span>`
            : fallback;
        return `<div class="ms-print-product">${img}<div><strong>${this._esc(line?.name || 'Produit')}</strong><div class="ms-print-muted">${this._esc(line?.barcode || '')}</div></div></div>`;
    },

    _shouldAutoFocusInputs() {
        try {
            const coarse = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
            const narrow = window.innerWidth && window.innerWidth <= 1100;
            const touch = ('ontouchstart' in window) || (navigator.maxTouchPoints || 0) > 0;
            return !(coarse || touch || narrow);
        } catch (_) {
            return false;
        }
    },

    // ══════════════════════════════════════════════════════════════
    // SCANNER COMMUN — contexte Inventaire
    // ══════════════════════════════════════════════════════════════
    _lastScanCode: '', _lastScanTime: 0,

    openScannerModal(trigger) {
        if (this._draftLocked) {
            toast('Rechargez la version serveur avant de reprendre le scan.', 'warning');
            return;
        }
        if (!window.MageStockScannerConsumer) {
            toast('Le composant scanner commun est indisponible.', 'error');
            return;
        }
        this._scannerConsumer?.close?.('reopen');
        this._scannerFoundIdx = null;
        this._scannerConsumer = MageStockScannerConsumer.open({
            owner: 'inventory-counting',
            context: 'inventory',
            mode: 'auto',
            title: 'Inventaire par scan',
            description: 'Scannez un produit, saisissez la quantité réelle puis passez au suivant.',
            trigger: trigger || document.activeElement,
            resolverOptions: () => ({
                products: this.prods || [],
                lines: this.invData?.lines || [],
                allowedProductIds: (this.invData?.lines || []).map(line => line.productId ?? line.id)
            }),
            outOfScopeMessage: 'Ce produit existe dans MageStock mais ne fait pas partie de cet inventaire.',
            onResolved: (result, handle) => this._onInventoryScannerResolved(result, handle),
            onOutOfScope: (result, handle) => this._onInventoryScannerOutOfScope(result, handle),
            onNotFound: (result, handle) => this._onInventoryScannerNotFound(result, handle),
            onClose: () => {
                this._scannerConsumer = null;
                this._scannerFoundIdx = null;
            }
        });
    },

    async _onInventoryScannerResolved(result, handle) {
        const productId = result?.product?.id;
        const idx = (this.invData?.lines || []).findIndex(line => String(line.productId ?? line.id) === String(productId));
        if (idx < 0) return this._onInventoryScannerOutOfScope(result, handle);
        const line = this.invData.lines[idx];
        this._scannerFoundIdx = idx;
        handle.setStatus('success', line.name || result.product?.name || 'Produit trouvé', 'Saisissez la quantité réelle constatée.');
        this.renderScanFoundCard(idx, result.raw || result.matchedValue || '', handle);
        this.scrollToRow(idx);
        this.onFocusLine(idx);
        toast(`✅ ${line.name}`, 'success');
        return false;
    },

    _onInventoryScannerOutOfScope(result, handle) {
        const productName = result?.product?.name || 'Ce produit';
        handle.setStatus('warning', productName, 'Produit absent du périmètre de cet inventaire.');
        this._renderInventoryScannerMessage(handle, 'Produit hors inventaire', `${productName} existe dans MageStock, mais il ne figure pas dans les lignes à compter.`, 'warning');
        return false;
    },

    _onInventoryScannerNotFound(result, handle) {
        handle.setStatus('notfound', 'Code non reconnu', result?.raw || 'Aucun produit correspondant.');
        this._renderInventoryScannerMessage(handle, 'Code non reconnu', result?.raw ? `Aucun produit ne correspond au code ${result.raw}.` : 'Aucun produit ne correspond à ce code.', 'notfound');
        return false;
    },

    _renderInventoryScannerMessage(handle, title, detail, state) {
        const node = document.createElement('article');
        node.className = 'inv-scanner-result-card';
        node.dataset.state = state || 'info';
        node.innerHTML = `<strong>${this._esc(title)}</strong><p>${this._esc(detail)}</p><button type="button" class="btn btn-primary">Scanner le suivant</button>`;
        node.querySelector('button')?.addEventListener('click', () => handle.restart());
        handle.setResult(node);
    },

    startScanner() {
        return this._scannerConsumer?.start?.() || Promise.resolve(false);
    },

    async stopScanner() {
        const handle = this._scannerConsumer;
        this._scannerConsumer = null;
        this._scannerFoundIdx = null;
        handle?.close?.('inventory-stop');
        return true;
    },

    pauseScannerForInput() {
        return this._scannerConsumer?.pause?.('inventory-input') || Promise.resolve(true);
    },

    handleScanResult(code) {
        return this._scannerConsumer?.resolve?.(code, 'external') || Promise.resolve(null);
    },

    renderScanFoundCard(idx, code, handle) {
        if (!handle || !this.invData?.lines?.[idx]) return;
        const line = this.invData.lines[idx];
        const hasCounted = line.counted !== null && line.counted !== undefined;
        const current = hasCounted ? line.counted : '';
        const diff = hasCounted ? Number(line.counted) - Number(line.expected) : null;
        const imgSrc = this._lineImageSrc(line);
        const fallbackIcon = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 7 9-4 9 4-9 4-9-4z"></path><path d="m3 7 9 4 9-4v10l-9 4-9-4V7z"></path><path d="M12 11v10"></path></svg>`;
        const imgHtml = imgSrc
            ? `<img src="${this._esc(imgSrc)}" class="inv-scan-prod-img" alt="" loading="lazy"><span class="inv-scan-prod-fb" hidden>${fallbackIcon}</span>`
            : `<span class="inv-scan-prod-fb">${fallbackIcon}</span>`;
        const diffText = diff === null ? 'Non compté' : (diff === 0 ? 'Conforme' : `Écart ${diff > 0 ? '+' : ''}${this._fmtQty(diff)}`);
        const diffClass = diff === null ? 'muted' : (diff === 0 ? 'ok' : 'warn');
        const card = document.createElement('div');
        card.className = 'inv-scan-product-card';
        card.innerHTML = `
            <div class="inv-scan-product-main">
                <div class="inv-scan-product-img">${imgHtml}</div>
                <div class="inv-scan-product-info">
                    <h3>${this._esc(line.name)}</h3>
                    <p>${this._esc(line.sku || 'Sans SKU')} ${line.barcode ? ' · ' + this._esc(line.barcode) : ''}</p>
                    <div class="inv-scan-product-stats">
                        <span>Théorique <strong>${this._fmtQty(line.expected)}</strong></span>
                        <span class="${diffClass}">${diffText}</span>
                    </div>
                </div>
            </div>
            <label class="lbl" for="invScanQty">Quantité réelle constatée</label>
            <input type="number" class="inp inv-scan-qty" id="invScanQty" min="0" step="0.01" inputmode="decimal" value="${current}" placeholder="Saisir la quantité réelle" autocomplete="off">
            <div class="inv-scan-qty-tools" aria-label="Actions rapides de comptage">
                <button class="btn btn-secondary" type="button" data-inv-scan-bump="-1">-1</button>
                <button class="btn btn-secondary" type="button" data-inv-scan-bump="1">+1</button>
                <button class="btn btn-secondary" type="button" data-inv-scan-bump="5">+5</button>
                <button class="btn btn-secondary" type="button" id="btnInvScanExpected">Conforme (${this._fmtQty(line.expected)})</button>
            </div>
            <div class="inv-scan-actions">
                <button class="btn btn-primary" id="btnInvScanValidateNext">Valider + scan suivant</button>
                <button class="btn btn-ghost" id="btnInvScanGoLine">Voir la ligne</button>
            </div>`;
        card.querySelector('img')?.addEventListener('error', event => {
            event.currentTarget.hidden = true;
            const fallback = event.currentTarget.nextElementSibling;
            if (fallback) fallback.hidden = false;
        }, { once: true });
        handle.setResult(card);

        const qty = card.querySelector('#invScanQty');
        card.querySelectorAll('[data-inv-scan-bump]').forEach(btn => {
            btn.addEventListener('click', () => {
                if (!qty) return;
                const delta = this._toNumber(btn.dataset.invScanBump || '0');
                const currentQty = this._toNumber(qty.value || '0');
                qty.value = Math.max(0, currentQty + delta);
            });
        });
        card.querySelector('#btnInvScanExpected')?.addEventListener('click', () => {
            if (qty) qty.value = line.expected;
            this.validateScannedProduct(true);
        });
        card.querySelector('#btnInvScanValidateNext')?.addEventListener('click', () => this.validateScannedProduct(true));
        card.querySelector('#btnInvScanGoLine')?.addEventListener('click', () => this.gotoScannedLine());
        if (qty) {
            if (this._shouldAutoFocusInputs()) { qty.focus(); qty.select(); }
            qty.addEventListener('keydown', event => {
                if (event.key === 'Enter') { event.preventDefault(); this.validateScannedProduct(true); }
            });
        }
    },

    async validateScannedProduct(continueScan) {
        if (this._draftLocked) return;
        const idx = this._scannerFoundIdx;
        if (idx === null || idx === undefined || !this.invData?.lines?.[idx]) return;
        const qty = this._scannerConsumer?.result?.querySelector?.('#invScanQty') || document.getElementById('invScanQty');
        const raw = qty?.value?.trim() || '';
        if (raw === '') { toast('Saisissez la quantité réelle avant de continuer', 'warning'); if (this._shouldAutoFocusInputs()) qty?.focus(); return; }
        const val = this._toNumber(raw);
        if (val < 0) { toast('La quantité ne peut pas être négative', 'warning'); if (this._shouldAutoFocusInputs()) qty?.focus(); return; }

        this.invData.lines[idx].counted = val;
        this.updateStats();
        this._scheduleDraftSave();
        if (this.itemStatusFilter !== 'all') this.renderItems(); else this.updateRow(idx);
        this._announce(`Quantité ${this._fmtQty(val)} enregistrée pour ${this.invData.lines[idx].name}.`);
        this.focusNextEmpty(idx);
        this._scannerFoundIdx = null;

        if (continueScan) await this._scannerConsumer?.restart?.();
    },

    gotoScannedLine() {
        const idx = this._scannerFoundIdx;
        if (idx === null || idx === undefined) return;
        this._scannerConsumer?.close?.('goto-line');
        this._scannerConsumer = null;
        this.itemStatusFilter = 'all';
        this._syncFilterButtons('.inv-status-filter', 'all');
        this.mobileVisibleLimit = Math.max(this.mobileVisibleLimit, idx + 1);
        this.renderItems();
        setTimeout(() => {
            const inp = document.getElementById(this._isCompactFlow() ? `invCardCount-${idx}` : `invCount-${idx}`)
                || document.getElementById(`invCount-${idx}`);
            if (inp) { inp.focus(); inp.select(); this.scrollToRow(idx); this.onFocusLine(idx); }
            const target = document.getElementById(this._isCompactFlow() ? `invCard-${idx}` : `invRow-${idx}`);
            if (target) {
                target.classList.add('inv-row-highlight');
                setTimeout(() => target.classList.remove('inv-row-highlight'), 2000);
            }
        }, 150);
    },


    // ══════════════════════════════════════════════════════════════
    // SÉLECTEUR CATÉGORIES (compact, scroll, recherche)
    // ══════════════════════════════════════════════════════════════
    _allCats: [],

    _initCatChips() {
        const container = document.getElementById('invCatChips');
        const btnAll    = document.getElementById('invCatBtnAll');
        const search    = document.getElementById('invCatSearch');
        const scroll    = document.getElementById('invCatScroll');
        if (!container) return;

        // Click sur chip individuel
        container.addEventListener('click', (e) => {
            const chip = e.target.closest('.inv-cat-chip');
            if (!chip) return;
            chip.classList.toggle('active');
            chip.setAttribute('aria-pressed', chip.classList.contains('active') ? 'true' : 'false');
            this._syncCatUI();
        });

        // Bouton "Toutes" : dans un contrôle ciblé, sélectionne explicitement toutes les catégories.
        if (btnAll) btnAll.addEventListener('click', () => {
            const chips = [...container.querySelectorAll('.inv-cat-chip')];
            const allSelected = chips.length > 0 && chips.every(chip => chip.classList.contains('active'));
            chips.forEach(chip => {
                chip.classList.toggle('active', !allSelected);
                chip.setAttribute('aria-pressed', !allSelected ? 'true' : 'false');
            });
            this._syncCatUI();
        });

        // Recherche temps réel
        if (search) search.addEventListener('input', () => {
            const q = search.value.trim().toLowerCase();
            const chips = container.querySelectorAll('.inv-cat-chip');
            let visible = 0;
            chips.forEach(c => {
                const match = !q || (c.dataset.catValue || '').toLowerCase().includes(q);
                c.classList.toggle('filtered-out', !match);
                if (match) visible++;
            });
            // No match message
            let noMatch = container.querySelector('.inv-cat-no-match');
            if (visible === 0 && q) {
                if (!noMatch) { noMatch = document.createElement('div'); noMatch.className = 'inv-cat-no-match'; container.appendChild(noMatch); }
                noMatch.textContent = 'Aucune catégorie trouvée';
                noMatch.style.display = '';
            } else if (noMatch) {
                noMatch.style.display = 'none';
            }
            this._updateScrollFade();
        });

        // Scroll fade
        if (scroll) scroll.addEventListener('scroll', () => this._updateScrollFade());

        // Click sur tags sélectionnés (retirer)
        const selected = document.getElementById('invCatSelected');
        if (selected) selected.addEventListener('click', (e) => {
            const tag = e.target.closest('.inv-cat-sel-tag');
            if (!tag) return;
            const val = tag.dataset.catValue;
            const chip = [...container.querySelectorAll('.inv-cat-chip')].find(node => node.dataset.catValue === val);
            if (chip) { chip.classList.remove('active'); chip.setAttribute('aria-pressed', 'false'); }
            this._syncCatUI();
        });
    },

    _buildCatChips(cats, selectedValues = []) {
        const container = document.getElementById('invCatChips');
        const search    = document.getElementById('invCatSearch');
        if (!container) return;
        this._allCats = cats || [];

        if (search) search.value = '';

        if (!this._allCats.length) {
            container.innerHTML = '<div class="inv-cat-empty">Sélectionnez un magasin pour voir les catégories</div>';
            this._syncCatUI();
            return;
        }

        const selected = new Set((selectedValues || []).map(String));
        container.innerHTML = this._allCats.map(c => {
            const active = selected.has(String(c));
            return `<button type="button" class="inv-cat-chip ${active ? 'active' : ''}" data-cat-value="${this._esc(c)}" aria-pressed="${active ? 'true' : 'false'}">${this._esc(c)}</button>`;
        }).join('');

        this._syncCatUI();
        // Delay pour que le DOM soit rendu avant de calculer le fade
        requestAnimationFrame(() => this._updateScrollFade());
    },

    _syncCatUI() {
        const container = document.getElementById('invCatChips');
        const btnAll    = document.getElementById('invCatBtnAll');
        const counterEl = document.getElementById('invCatCounter');
        const selEl     = document.getElementById('invCatSelected');
        if (!container) return;

        const selected = this._getSelectedCategories();
        const isAll = this._allCats.length > 0 && selected.length === this._allCats.length;

        // Bouton "Toutes"
        if (btnAll) {
            btnAll.classList.toggle('inactive', !isAll);
            btnAll.setAttribute('aria-pressed', isAll ? 'true' : 'false');
            btnAll.textContent = isAll ? '✓ Toutes les catégories' : 'Sélectionner toutes les catégories';
        }

        // Compteur
        if (counterEl) {
            counterEl.innerHTML = selected.length === 0
                ? `<strong>0</strong> / ${this._allCats.length} sélectionnée`
                : isAll
                    ? `<strong>${this._allCats.length}</strong> catégories sélectionnées`
                    : `<strong>${selected.length}</strong> / ${this._allCats.length} sélectionnée${selected.length > 1 ? 's' : ''}`;
        }

        // Tags sélectionnés
        if (selEl) {
            selEl.innerHTML = selected.map(c =>
                `<button type="button" class="inv-cat-sel-tag" data-cat-value="${this._esc(c)}" aria-label="Retirer ${this._esc(c)}">${this._esc(c)} <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>`
            ).join('');
        }

        this._updateScrollFade();
        this._saveNewDraft();
        this._updateNewReview();
    },

    _updateScrollFade() {
        const scroll = document.getElementById('invCatScroll');
        const fade   = document.getElementById('invCatFade');
        if (!scroll || !fade) return;
        const hasMore = scroll.scrollHeight > scroll.clientHeight + 4;
        const atBottom = scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 4;
        fade.classList.toggle('hidden', !hasMore || atBottom);
    },

    _getSelectedCategories() {
        const container = document.getElementById('invCatChips');
        if (!container) return [];
        return [...container.querySelectorAll('.inv-cat-chip.active')].map(c => c.dataset.catValue).filter(Boolean);
    },

    _fmtCategorySummary(inv) {
        if (inv?.mode === 'full') return 'Tout le magasin';
        const categories = Array.isArray(inv?.categories) ? inv.categories.filter(Boolean) : [];
        if (categories.length === 0 && inv?.category) {
            const legacy = String(inv.category).split(',').map(v => v.trim()).filter(Boolean);
            if (legacy.length) return legacy.length === 1 ? legacy[0] : `${legacy.length} catégories sélectionnées`;
        }
        if (!categories.length) return 'Toutes les catégories';
        if (categories.length === 1) return categories[0];
        if (categories.length === 2) return categories.join(' · ');
        return `${categories[0]} · ${categories[1]} · + ${categories.length - 2} autre${categories.length > 3 ? 's' : ''}`;
    },

    /** Format categories for display — backward compatible (string or array) */
    _fmtCategories(inv) {
        if ((inv?.mode || '') === 'full') return 'Tout le magasin';
        if (Array.isArray(inv.categories) && inv.categories.length > 0) {
            return inv.categories.join(', ');
        }
        // Backward compat: old inventories stored a single string
        if (inv.category && inv.category !== '') return inv.category;
        return 'Toutes';
    },

    // ══════════════════════════════════════════════════════════════
    // HELPERS
    // ══════════════════════════════════════════════════════════════
    _toNumber(value) {
        const n = parseFloat(String(value ?? '').replace(',', '.'));
        return Number.isFinite(n) ? n : 0;
    },

    _fmtQty(value) {
        const n = Number(value);
        if (!Number.isFinite(n)) return '0';
        return Number.isInteger(n) ? Locale.formatInteger(n) : Locale.formatNumber(n, { maximumFractionDigits: 2 });
    },

    _esc(s) {
        return String(s ?? '').replace(/[&<>"']/g, m =>
            ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[m]));
    },
    _fmtDate(iso) {
        if (!iso) return '—';
        try { return Locale.formatDateTime(iso, { day:'2-digit', month:'short', year:'numeric', hour:'2-digit', minute:'2-digit' }); } catch { return iso; }
    },
    _fmtDateShort(iso) {
        if (!iso) return '—';
        try { return Locale.formatDate(iso, { day:'2-digit', month:'2-digit', year:'numeric' }); } catch { return iso; }
    },
};
