jQuery(document).ready(function($) {
    'use strict';

    // --- i18n-Helfer (wp.i18n + sprintf-Fallback) ---
    var __ = (window.wp && wp.i18n && wp.i18n.__) ? wp.i18n.__ : function(s){ return s; };
    var _n = (window.wp && wp.i18n && wp.i18n._n) ? wp.i18n._n : function(s, p, n){ return n === 1 ? s : p; };
    var sprintfFn = (window.wp && wp.i18n && wp.i18n.sprintf)
        ? wp.i18n.sprintf
        : function(fmt) {
            var args = Array.prototype.slice.call(arguments, 1);
            var i = 0;
            return String(fmt).replace(/%(\d+\$)?[sd]/g, function(m, pos) {
                if (pos) return args[parseInt(pos, 10) - 1];
                return args[i++];
            });
        };

    // Der frühere replaceState-Hack, der einmalige Aktions-Parameter
    // (save_preset_name, apply_preset, set_default_preset, clear_default_preset,
    // preset_delete) aus der URL putzte, ist entfallen: Diese Aktionen laufen
    // jetzt serverseitig per POST/Redirect (QSFW_Columns::maybe_handle_view_post,
    // QSFW_Presets::maybe_handle_tab_actions) und erreichen die gerenderte Seite
    // gar nicht mehr.

    // SelectWoo für Kategorie
    if ($.fn.selectWoo) {
        $('.hb-cat-select').selectWoo({
            placeholder: __('Select category…', 'quickstock-for-woocommerce'),
            allowClear: true,
            width: '220px'
        });
        // Multi-Select im erweiterten Filter (Drawer): Dropdown an den Drawer
        // hängen, damit es im richtigen Stacking-Context erscheint und nicht
        // hinter dem Drawer-Overlay verschwindet.
        $('.hb-cat-multi').each(function() {
            var $sel = $(this);
            var $parent = $sel.closest('#hb-adv-drawer');
            $sel.selectWoo({
                placeholder: __('Pick categories…', 'quickstock-for-woocommerce'),
                allowClear: false,
                width: '100%',
                dropdownParent: $parent.length ? $parent : $(document.body)
            });
        });
    }

    // --- AUTO-SUBMIT ---
    $(document).on('change', '#hb-filter-form select[name="stock_sort"], #hb-filter-form select[name="per_page"], #hb-filter-form input[name="stock_start_date"], #hb-filter-form select[name="cat_filter"]', function() {
        $('#hb-filter-form').trigger('submit');
    });
    $(document).on('change', 'details.hb-dropdown input[type="checkbox"]', function() {
        $(this).closest('details.hb-dropdown').data('hb-changed', true);
    });
    $('details.hb-dropdown').each(function() {
        this.addEventListener('toggle', function() {
            if (!this.open && $(this).data('hb-changed')) {
                $(this).removeData('hb-changed');
                $('#hb-filter-form').trigger('submit');
            }
        }.bind(this));
    });
    // Aktive Live-Suche bei jedem Filter-Submit (Sortier-Dropdown, Kategorie,
    // Seitenanzahl, erweiterte Filter …) als hb_q mitschicken. Das Suchfeld
    // hat kein name-Attribut und wird sonst nicht serialisiert, sodass der
    // Suchbegriff beim Reload verloren ginge. Beim Laden stellt der
    // hb_q-Handler die Suche wieder her und führt sie neu aus.
    $(document).on('submit', '#hb-filter-form', function() {
        var $form = $(this);
        $form.find('input[name="hb_q"]').remove();
        var formQ = $.trim($('#stock-search').val() || '');
        if (formQ !== '') {
            $('<input>', { type: 'hidden', name: 'hb_q', value: formQ }).appendTo($form);
        }
    });

    // --- SPALTEN-SORTIERUNG (Excel-Style Mini-Menü pro Spaltenkopf) ---
    function hbCloseSortMenus($except) {
        $('.hb-th-sortable').each(function() {
            if ($except && this === $except[0]) return;
            $(this).removeClass('hb-menu-open');
        });
    }
    $(document).on('click', '.hb-sort-toggle', function(e) {
        e.preventDefault();
        e.stopPropagation();
        var $th = $(this).closest('.hb-th-sortable');
        var willOpen = !$th.hasClass('hb-menu-open');
        hbCloseSortMenus($th);
        $th.toggleClass('hb-menu-open', willOpen);
    });
    $(document).on('click', '.hb-sort-opt', function(e) {
        e.preventDefault();
        var val = $(this).data('sort');
        if (!val) return;
        var param = $(this).data('param') || 'stock_sort';
        var pagedParam = $(this).data('paged-param') || 'paged';

        // ACHTUNG: Denselben Handler nutzt auch der Verlauf-Tab mit
        // data-param="log_sort". log_sort schreibt serverseitig nichts
        // (render_log() liest nur $_GET) und bleibt deshalb GET-Navigation.
        if (param !== 'stock_sort') {
            var url = new URL(window.location.href);
            url.searchParams.set(param, val);
            url.searchParams.delete(pagedParam); // bei neuer Sortierung auf Seite 1
            // Aktive Live-Suche über den Reload hinweg erhalten: hb_q wird beim
            // Laden wieder ins Suchfeld geschrieben und neu ausgeführt (gegen die
            // neue Sortierung), statt verloren zu gehen.
            var sortQ = $.trim($('#stock-search').val() || '');
            if (sortQ !== '') url.searchParams.set('hb_q', sortQ);
            window.location.assign(url.toString());
            return;
        }

        // stock_sort schreibt user_meta und läuft deshalb über das
        // nonce-geprüfte Cockpit-Formular statt über einen GET-Link.
        // Das Formular muss KOMPLETT mitgehen: ein Teil-POST ohne visible_cols
        // hiesse „keine Spalte sichtbar". hb_q hängt der submit-Handler an.
        var $form = $('#hb-filter-form');
        if (!$form.length) return;
        // Spaltensortierungen (col__*) stehen nicht als Option im
        // Cockpit-Dropdown. Select abklemmen (disabled wird nicht serialisiert)
        // und den Wert als Hidden mitgeben, damit genau EIN stock_sort im POST
        // landet.
        $form.find('select[name="stock_sort"]').prop('disabled', true);
        $form.find('input[name="stock_sort"]').remove();
        $('<input>', { type: 'hidden', name: 'stock_sort', value: val }).appendTo($form);
        $form.trigger('submit');
    });
    $(document).on('click', function() { hbCloseSortMenus(); });
    $(document).on('keydown', function(e) {
        if (e.key === 'Escape') hbCloseSortMenus();
    });

    // --- SPALTEN-WERTFILTER (Excel-Style: Suche + Checkbox-Liste pro Spalte) ---
    var colfDebounce = {};

    // Aktive Spalten-Wertfilter aller Spalten (Server-Zustand) als Objekt.
    function colfActiveMap() {
        var m = (window.QSFW && QSFW.colFilters) ? QSFW.colFilters : {};
        var out = {};
        Object.keys(m).forEach(function(k) { out[k] = m[k]; });
        return out;
    }

    // Select-all-Checkbox an die Einzel-Checkboxen angleichen.
    function colfSyncAll($box) {
        var $cbs = $box.find('.hb-colf-list .hb-colf-cb');
        var total = $cbs.length;
        var checked = $cbs.filter(':checked').length;
        var $all = $box.find('.hb-colf-all-cb');
        $all.prop('checked', total > 0 && checked === total);
        $all.prop('indeterminate', checked > 0 && checked < total);
    }

    var COLF_EMPTY = (window.QSFW && QSFW.emptyToken) ? QSFW.emptyToken : '__qsfw_empty__';

    function colfRenderList($box, values, capped, hasEmpty) {
        // Merken, ob die Liste serverseitig gekappt wurde – „Alle auswählen"
        // ist dann nur eine Teilmenge und darf NICHT als „kein Filter" gelten.
        $box.data('hb-capped', !!capped);
        var $list = $box.find('.hb-colf-list').empty();
        var active = $box.data('hb-active') || null;
        // „Ist nicht leer"-Filter aktiv → alle Werte vorab anhaken, (Leere) nicht.
        var nonempty = !!(active && active.nonempty);
        var inSet = {};
        if (active && active.in) active.in.forEach(function(v) { inSet[String(v)] = true; });

        if (!values.length && !hasEmpty) {
            $list.append($('<li class="hb-colf-note"></li>').text(__('No values', 'quickstock-for-woocommerce')));
        } else {
            // „(Leere)" wie in Excel: nur wenn die Spalte wirklich leere Felder hat.
            if (hasEmpty) {
                var $ecb = $('<input type="checkbox" class="hb-colf-cb hb-colf-empty-cb">').val(COLF_EMPTY);
                if (inSet[COLF_EMPTY]) $ecb.prop('checked', true);
                var $elbl = $('<label></label>').append($ecb).append(' ')
                    .append($('<span class="hb-colf-empty-label"></span>').text(__('(Empty)', 'quickstock-for-woocommerce')));
                $list.append($('<li class="hb-colf-empty"></li>').append($elbl));
            }
            values.forEach(function(item) {
                var $cb = $('<input type="checkbox" class="hb-colf-cb">').val(String(item.value));
                if (nonempty || inSet[String(item.value)]) $cb.prop('checked', true);
                var $lbl = $('<label></label>').append($cb).append(' ')
                    .append($('<span></span>').text(item.label));
                $list.append($('<li></li>').append($lbl));
            });
            if (capped) {
                $list.append($('<li class="hb-colf-note"></li>')
                    .text(__('Showing first matches — refine your search.', 'quickstock-for-woocommerce')));
            }
        }
        colfSyncAll($box);
    }

    function colfLoad($box, search) {
        var $list = $box.find('.hb-colf-list').empty()
            .append($('<li class="hb-colf-note"></li>').text(__('Loading…', 'quickstock-for-woocommerce')));
        $.post(QSFW.ajaxUrl, {
            action: 'qsfw_column_values',
            nonce:  QSFW.nonce,
            col:    $box.data('col'),
            q:      search || '',
            mode:   QSFW.mode || 'all'
        }).done(function(resp) {
            if (resp && resp.success && resp.data) {
                colfRenderList($box, resp.data.values || [], !!resp.data.capped, !!resp.data.has_empty);
            } else {
                $list.empty().append($('<li class="hb-colf-note"></li>').text(__('Error loading values.', 'quickstock-for-woocommerce')));
            }
        }).fail(function() {
            $list.empty().append($('<li class="hb-colf-note"></li>').text(__('Error loading values.', 'quickstock-for-woocommerce')));
        });
    }

    // „Alle Treffer für ‚x‘ filtern (enthält)" – Affordance bei aktiver Suche.
    function colfUpdateContains($box, q) {
        var $c = $box.find('.hb-colf-contains');
        if (q === '') { $c.attr('hidden', 'hidden').empty(); return; }
        $c.removeAttr('hidden').empty();
        // translators: %s is the user's search term
        var label = sprintfFn(__('Filter all containing “%s”', 'quickstock-for-woocommerce'), q);
        $c.append($('<a href="#" class="hb-colf-contains-link"></a>').text(label));
    }

    // Filter setzen: per POST absenden statt als GET-URL. So gibt es keine
    // URL-Längenbegrenzung – egal wie viele/lange Werte ausgewählt sind (sonst
    // nginx „414 Request-URI Too Large", z. B. bei vielen Händlerlinks). Der
    // Server speichert die Filter in user_meta und leitet auf eine saubere URL
    // ohne colf-Parameter um (Post/Redirect/Get).
    function colfApply(col, val) {
        var map = colfActiveMap();
        if (val === null) { delete map[col]; } else { map[col] = val; }

        // Action-URL = aktuelle URL ohne colf*/paged. Kurze Parameter (page,
        // mode, Sortierung, Suche …) bleiben als GET erhalten; nur die langen
        // colf-Werte wandern in den POST-Body.
        var url = new URL(window.location.href);
        var toDelete = [];
        url.searchParams.forEach(function(v, k) { if (k.indexOf('colf') === 0) toDelete.push(k); });
        toDelete.forEach(function(k) { url.searchParams.delete(k); });
        url.searchParams.delete('paged');

        var sq = $.trim($('#stock-search').val() || '');
        if (sq !== '') url.searchParams.set('hb_q', sq); else url.searchParams.delete('hb_q');

        var $form = $('<form method="post"></form>').attr('action', url.toString());
        $form.append($('<input type="hidden" name="colf_submitted">').val('1'));
        // Schreibt user_meta -> nonce-pflichtig (geprüft in maybe_handle_colf_post).
        $form.append($('<input type="hidden" name="_wpnonce">').val(QSFW.viewNonce));
        Object.keys(map).forEach(function(c) {
            var f = map[c];
            if (!f) return;
            if (f.nonempty) {
                $form.append($('<input type="hidden">').attr('name', 'colf[' + c + '][nonempty]').val('1'));
            } else if (f.like != null && f.like !== '') {
                $form.append($('<input type="hidden">').attr('name', 'colf[' + c + '][like]').val(f.like));
            } else if (f.in && f.in.length) {
                f.in.forEach(function(v) {
                    $form.append($('<input type="hidden">').attr('name', 'colf[' + c + '][in][]').val(v));
                });
            }
        });
        $form.appendTo(document.body).trigger('submit');
    }

    // Klicks INNERHALB des Menüs sollen es nicht schließen (Suche/Checkboxen).
    $(document).on('click', '.hb-col-menu', function(e) { e.stopPropagation(); });

    // Beim Öffnen eines Spaltenmenüs die Werte einmalig laden (läuft NACH dem
    // bestehenden Toggle-Handler, der hb-menu-open setzt).
    $(document).on('click', '.hb-sort-toggle', function() {
        var $th = $(this).closest('.hb-th-sortable');
        if (!$th.hasClass('hb-menu-open')) return;

        // Überlauf am rechten Rand verhindern: Menü ggf. rechtsbündig ausrichten.
        var $menu = $th.find('.hb-sort-menu').removeClass('hb-flip-right');
        if ($menu.length) {
            var rect = $menu[0].getBoundingClientRect();
            var vw = document.documentElement.clientWidth;
            if (rect.right > vw - 8) $menu.addClass('hb-flip-right');
        }

        var $box = $th.find('.hb-colf');
        if (!$box.length) return;
        if (!$box.data('hb-init')) {
            $box.data('hb-init', true);
            var act = $box.attr('data-active');
            if (act) { try { $box.data('hb-active', JSON.parse(act)); } catch (e) {} }
            var a = $box.data('hb-active');
            if (a && a.like) $box.find('.hb-colf-search').val(a.like);
            colfLoad($box, (a && a.like) ? a.like : '');
        }
        var $search = $box.find('.hb-colf-search');
        setTimeout(function() { $search.trigger('focus'); }, 0);
    });

    // Tippen im Suchfeld: serverseitige Werte-Suche (debounced) + Contains-Link.
    $(document).on('input', '.hb-colf-search', function() {
        var $box = $(this).closest('.hb-colf');
        var col = $box.data('col');
        var q = $.trim($(this).val() || '');
        colfUpdateContains($box, q);
        clearTimeout(colfDebounce[col]);
        colfDebounce[col] = setTimeout(function() { colfLoad($box, q); }, 300);
    });

    // Tastatur im Suchfeld: Shortcuts der App nicht auslösen; Enter = enthält-Filter.
    $(document).on('keydown', '.hb-colf-search', function(e) {
        if (e.key === 'Escape') return; // globaler Handler schließt das Menü
        if (e.key === 'Enter') {
            e.preventDefault();
            var $box = $(this).closest('.hb-colf');
            var q = $.trim($(this).val() || '');
            if (q !== '') colfApply($box.data('col'), { like: q });
            return;
        }
        e.stopPropagation();
    });

    $(document).on('click', '.hb-colf-contains-link', function(e) {
        e.preventDefault();
        var $box = $(this).closest('.hb-colf');
        var q = $.trim($box.find('.hb-colf-search').val() || '');
        if (q !== '') colfApply($box.data('col'), { like: q });
    });

    $(document).on('change', '.hb-colf-all-cb', function() {
        var $box = $(this).closest('.hb-colf');
        $box.find('.hb-colf-list .hb-colf-cb').prop('checked', $(this).prop('checked'));
    });

    $(document).on('change', '.hb-colf-cb', function() {
        colfSyncAll($(this).closest('.hb-colf'));
    });

    $(document).on('click', '.hb-colf-apply', function() {
        var $box = $(this).closest('.hb-colf');
        var $cbs = $box.find('.hb-colf-list .hb-colf-cb');
        var $checked = $cbs.filter(':checked');
        var $empty = $box.find('.hb-colf-empty-cb');
        var $valCbs = $cbs.not('.hb-colf-empty-cb');
        var capped = !!$box.data('hb-capped');
        var search = $.trim($box.find('.hb-colf-search').val() || '');

        var hasEmptyOpt  = $empty.length > 0;
        var emptyChecked = hasEmptyOpt && $empty.prop('checked');
        var allVals      = $valCbs.length > 0 && $valCbs.filter(':checked').length === $valCbs.length;

        // Bei aktiver Suche ist (Leere) ausgeblendet: „alle Treffer markiert"
        // bleibt – wie bisher – identisch zum „enthält <suche>"-Filter; nur
        // gültig, wenn die Liste nicht gekappt ist (sonst nur eine Teilmenge).
        if (search !== '') {
            if ($cbs.length && $checked.length === $cbs.length && !capped) {
                colfApply($box.data('col'), { like: search });
                return;
            }
            var vs = [];
            $checked.each(function() { vs.push($(this).val()); });
            colfApply($box.data('col'), vs.length ? { in: vs } : null);
            return;
        }

        // Ohne Suche – die „Massenauswahl" wird semantisch abgebildet, statt
        // Einzelwerte aufzuzählen. Das funktioniert UNABHÄNGIG von der
        // Werteliste-Deckelung (500), weil keine konkreten Werte gespeichert
        // werden, sondern die Absicht.
        //  1) Alle Werte + (Leere) markiert (oder keine (Leere) vorhanden)
        //     → alles ausgewählt → gar kein Filter.
        if (allVals && (!hasEmptyOpt || emptyChecked)) {
            colfApply($box.data('col'), null);
            return;
        }
        //  2) Alle Werte markiert, aber (Leere) abgewählt → „ist nicht leer".
        if (allVals && hasEmptyOpt && !emptyChecked) {
            colfApply($box.data('col'), { nonempty: 1 });
            return;
        }

        //  3) Teilauswahl → explizite Werteliste (inkl. evtl. (Leere)-Token).
        var vals = [];
        $checked.each(function() { vals.push($(this).val()); });
        colfApply($box.data('col'), vals.length ? { in: vals } : null);
    });

    $(document).on('click', '.hb-colf-clear', function() {
        colfApply($(this).closest('.hb-colf').data('col'), null);
    });

    // --- ERWEITERTE FILTER (Drawer) ---
    function advOpen(open) {
        $('#hb-adv-drawer').toggleClass('is-open', open).attr('aria-hidden', open ? 'false' : 'true');
        $('#hb-adv-backdrop').toggleClass('is-open', open).attr('aria-hidden', open ? 'false' : 'true');
        // Body-Scroll sperren, solange der Drawer offen ist – verhindert,
        // dass ein offenes SelectWoo-Dropdown beim Hintergrund-Scroll
        // aus dem Sichtfeld läuft.
        $('body').toggleClass('qsfw-no-scroll', open);
        if (!open) {
            // Beim Schließen alle offenen SelectWoo-Dropdowns im Drawer schließen.
            $('#hb-adv-drawer .hb-cat-multi').each(function() {
                if ($(this).hasClass('select2-hidden-accessible')) $(this).selectWoo('close');
            });
        }
    }
    $(document).on('click', '#hb-adv-btn', function() { advOpen(true); });
    $(document).on('click', '#hb-adv-close, #hb-adv-backdrop', function() { advOpen(false); });
    $(document).on('keydown', function(e) {
        if (e.key === 'Escape' && $('#hb-adv-drawer').hasClass('is-open')) advOpen(false);
    });
    $(document).on('click', '#hb-adv-reset', function() {
        // Vollständiges Zurücksetzen über den nonce-gesicherten Server-Link –
        // löscht auch die separat gespeicherten Spalten-Wertfilter (COLF_META),
        // die ein reines Leeren der Drawer-Felder nicht erfassen würde.
        var url = $(this).data('reset-url');
        if (url) { window.location.href = url; }
    });

    // --- HAUPTBILD ÄNDERN (über WP-Media-Uploader) ---
    var hbImgFrame = null;
    var hbImgCurrent = null;
    $(document).on('click', '.hb-img-edit', function(e) {
        e.preventDefault();
        var $cell = $(this);
        if ($cell.hasClass('is-busy')) return;
        hbImgCurrent = $cell;

        if (hbImgFrame) {
            hbImgFrame.off('select');
        } else {
            hbImgFrame = wp.media({
                title: __('Select main image', 'quickstock-for-woocommerce'),
                button: { text: __('Set as main image', 'quickstock-for-woocommerce') },
                library: { type: 'image' },
                multiple: false
            });
        }

        hbImgFrame.on('select', function() {
            var att = hbImgFrame.state().get('selection').first().toJSON();
            var $c  = hbImgCurrent;
            var pid = $c.data('id');
            $c.addClass('is-busy');
            $.post(QSFW.ajaxUrl, {
                action: 'qsfw_set_thumbnail',
                nonce:  QSFW.nonce,
                product_id: pid,
                attachment_id: att.id
            }).done(function(resp) {
                if (resp && resp.success) {
                    var url = resp.data && resp.data.url ? resp.data.url
                        : (att.sizes && att.sizes.thumbnail ? att.sizes.thumbnail.url : att.url);
                    $c.html('<img src="' + url + '">');
                } else {
                    alert((resp && resp.data && resp.data.message) ? resp.data.message : __('Save failed.', 'quickstock-for-woocommerce'));
                }
            }).fail(function() {
                alert(__('Save failed.', 'quickstock-for-woocommerce'));
            }).always(function() {
                $c.removeClass('is-busy');
            });
        });

        hbImgFrame.open();
    });

    // --- NACHBESTELL-PDF LOGO (Einstellungen, über WP-Media-Uploader) ---
    var hbPdfLogoFrame = null;
    $(document).on('click', '.qsfw-pdf-logo-pick', function(e) {
        e.preventDefault();
        if (!hbPdfLogoFrame) {
            hbPdfLogoFrame = wp.media({
                title: __('Choose logo', 'quickstock-for-woocommerce'),
                button: { text: __('Choose logo', 'quickstock-for-woocommerce') },
                library: { type: 'image' },
                multiple: false
            });
            hbPdfLogoFrame.on('select', function() {
                var att = hbPdfLogoFrame.state().get('selection').first().toJSON();
                var url = (att.sizes && att.sizes.medium) ? att.sizes.medium.url : att.url;
                $('#qsfw_pdf_logo_id').val(att.id);
                $('#qsfw_pdf_logo_preview').html('<img src="' + url + '" alt="">');
                $('.qsfw-pdf-logo-remove').show();
            });
        }
        hbPdfLogoFrame.open();
    });
    $(document).on('click', '.qsfw-pdf-logo-remove', function(e) {
        e.preventDefault();
        $('#qsfw_pdf_logo_id').val('');
        $('#qsfw_pdf_logo_preview').empty();
        $(this).hide();
    });

    // --- COPY-BUTTON ---
    $(document).on('click', '.copy-btn', function() {
        var $b = $(this);
        navigator.clipboard.writeText($b.data('title'));
        $b.text('✔️').delay(800).queue(function(n){ $b.text('📋'); n(); });
    });

    // --- ETIKETTEN-DRUCK (vorlagengesteuert, Canvas -> PNG) ---

    // Textbreite inkl. optionalem Letter-Spacing (px).
    function qsfwTextWidth(ctx, str, ls) {
        str = String(str);
        if (!ls) return ctx.measureText(str).width;
        var w = 0;
        for (var i = 0; i < str.length; i++) w += ctx.measureText(str[i]).width + ls;
        return w > 0 ? w - ls : 0;
    }

    // Durchstreichungslinie mittig durch eine Textbox.
    function qsfwStrike(ctx, x, yTop, w, px) {
        ctx.save();
        ctx.strokeStyle = ctx.fillStyle;
        ctx.lineWidth = Math.max(1, px * 0.075);
        var my = yTop + px * 0.42;
        ctx.beginPath();
        ctx.moveTo(x, my);
        ctx.lineTo(x + w, my);
        ctx.stroke();
        ctx.restore();
    }

    // Wickelt Text in max. maxLines Zeilen; letzte Zeile ggf. mit Ellipsis.
    function qsfwWrapText(ctx, text, maxW, maxLines, ls) {
        var words = String(text).split(/\s+/), result = [], line = '';
        for (var i = 0; i < words.length; i++) {
            var test = line ? line + ' ' + words[i] : words[i];
            if (qsfwTextWidth(ctx, test, ls) > maxW && line) { result.push(line); line = words[i]; }
            else { line = test; }
        }
        if (line) result.push(line);
        if (result.length > maxLines) {
            var last = result[maxLines - 1];
            for (var j = maxLines; j < result.length; j++) last += ' ' + result[j];
            while (qsfwTextWidth(ctx, last + '…', ls) > maxW && last.length > 1) last = last.slice(0, -1);
            result = result.slice(0, maxLines - 1);
            result.push(last + '…');
        }
        return result;
    }

    // Zeichnet eine einzelne (links beginnende) Textzeile mit Letter-Spacing.
    function qsfwDrawLine(ctx, str, x, y, ls) {
        if (!ls) { ctx.fillText(str, x, y); return; }
        str = String(str);
        for (var i = 0; i < str.length; i++) {
            ctx.fillText(str[i], x, y);
            x += ctx.measureText(str[i]).width + ls;
        }
    }

    // Zeichnet einen Textstring AUSGERICHTET INNERHALB einer Box
    // [boxX .. boxX+boxW]. boxW <= 0 => Box = Textbreite (Ausrichtung
    // dann wirkungslos, Text sitzt bei boxX). Gibt {left,right} zurueck.
    function qsfwDrawText(ctx, boxX, boxW, str, bold, align, font, letterspacing, strike) {
        ctx.font = (bold ? 'bold ' : '') + Math.round(font) + 'px Arial, Helvetica, sans-serif';
        ctx.textBaseline = 'top';
        ctx.textAlign = 'left';
        var totalW = qsfwTextWidth(ctx, str, letterspacing);
        var bw = boxW > 0 ? boxW : totalW;
        var x;
        if (align === 'center')      x = boxX + (bw - totalW) / 2;
        else if (align === 'right')  x = boxX + bw - totalW;
        else                         x = boxX;
        qsfwDrawLine(ctx, str, x, ctx._hbY, letterspacing);
        if (strike) qsfwStrike(ctx, x, ctx._hbY, totalW, Math.round(font));
        return { left: x, right: x + totalW };
    }

    // Rendert eine Etikette. Liefert { canvas, rects } (rects = Bounding-
    // Boxen je Element in Canvas-Pixeln, fuer Drag im Vorschaufenster).
    function qsfwRenderLabelCanvas(tpl, data, opts) {
        opts = opts || {};
        var dpi = parseInt(tpl.dpi, 10) || 300, mmpx = dpi / 25.4;
        function mm(v) { return Math.round((parseFloat(v) || 0) * mmpx); }
        var W = mm(tpl.width_mm), H = mm(tpl.height_mm);
        var c = document.createElement('canvas'); c.width = W; c.height = H;
        var ctx = c.getContext('2d');
        ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H);
        ctx.fillStyle = '#000';

        var els = tpl.elements || {};
        var boxes = {};       // {left,right} fuer "after"
        var rects = {};       // {x,y,w,h} fuer Drag/Guides

        function priceText(el, val) {
            return (el.prefix || '') + (parseFloat(val) || 0).toFixed(2);
        }
        function drawSimple(key, str) {
            var el = els[key];
            if (!el || !el.enabled || str === '' || str == null) return;
            var ls = (el.letterspacing || 0) * mmpx;
            var bw = el.w > 0 ? mm(el.w) : 0, bh = el.h > 0 ? mm(el.h) : 0;
            var fontpx = el.font * mmpx; // Schrift ist fix (nur per Input)
            var boxX = mm(el.x);
            ctx._hbY = mm(el.y);
            var box = qsfwDrawText(ctx, boxX, bw, str, el.bold, el.align, fontpx, ls, el.strike);
            boxes[key] = box;
            rects[key] = {
                x: boxX, y: mm(el.y),
                w: bw || (box.right - box.left),
                h: bh || Math.round(fontpx * 1.25),
                resizable: true
            };
        }

        // Reihenfolge: Preis zuerst (Name kann darauf referenzieren)
        if (els.price && els.price.enabled) drawSimple('price', priceText(els.price, data.price));
        

        // Produktname (mehrzeilig, optional rechts neben Preis)
        var t = els.title;
        if (t && t.enabled && data.title) {
            var ls = (t.letterspacing || 0) * mmpx;
            var nameX = mm(t.x);
            if (t.after === 'price' && boxes.price) nameX = boxes.price.right + mm(t.gap);
            var bw = t.w > 0 ? mm(t.w) : 0, bh = t.h > 0 ? mm(t.h) : 0;
            var autoW = W - nameX - mm(1.5);
            if (autoW < mm(5)) autoW = W - nameX;
            var wrapW = bw > 0 ? bw : autoW;
            // Schrift ist fix (nur per Input). Box = reine Umbruchbreite.
            var fs = Math.round(t.font * mmpx);
            ctx.font = (t.bold ? 'bold ' : '') + fs + 'px Arial, Helvetica, sans-serif';
            var lines = qsfwWrapText(ctx, data.title, wrapW, t.maxlines, ls);
            ctx.textBaseline = 'top'; ctx.textAlign = 'left';
            var lineH = fs * 1.2, maxLineW = 0, lineW = [];
            for (var lm = 0; lm < lines.length; lm++) {
                lineW[lm] = qsfwTextWidth(ctx, lines[lm], ls);
                if (lineW[lm] > maxLineW) maxLineW = lineW[lm];
            }
            // Ausrichtungs-Box: explizite Breite oder Inhaltsbreite
            var alignW = bw > 0 ? bw : maxLineW;
            for (var li = 0; li < lines.length; li++) {
                var ly = mm(t.y) + li * lineH;
                var lx = nameX;
                if (t.align === 'center')     lx = nameX + (alignW - lineW[li]) / 2;
                else if (t.align === 'right') lx = nameX + alignW - lineW[li];
                qsfwDrawLine(ctx, lines[li], lx, ly, ls);
                if (t.strike) qsfwStrike(ctx, lx, ly, lineW[li], fs);
            }
            rects.title = {
                x: nameX, y: mm(t.y),
                w: bw || maxLineW,
                h: bh || Math.round(lines.length * lineH),
                resizable: true
            };
        }

        // Barcode
        var b = els.barcode;
        if (b && b.enabled && data.sku) {
            var bcCanvas = document.createElement('canvas');
            try {
                JsBarcode(bcCanvas, String(data.sku), {
                    format: b.format || 'CODE128',
                    width: b.modulewidth || 3,
                    height: mm(b.height_mm),
                    displayValue: false, margin: 0
                });
                ctx.drawImage(bcCanvas, mm(b.x), mm(b.y));
                rects.barcode = { x: mm(b.x), y: mm(b.y), w: bcCanvas.width, h: mm(b.height_mm) };
            } catch (e) {
                ctx.fillStyle = '#c00';
                ctx.font = Math.round(2 * mmpx) + 'px Arial';
                ctx.textBaseline = 'top';
                ctx.fillText('Barcode-Fehler', mm(b.x), mm(b.y));
                ctx.fillStyle = '#000';
            }
        }

        

        // Hilfslinien (nur Vorschau): Rahmen + Kantengriffe NUR fuer das
        // aktuell selektierte Element, damit die Vorschau nicht ueberladen ist.
        if (opts.guides && opts.selected && rects[opts.selected]) {
            var r = rects[opts.selected];
            ctx.save();
            ctx.strokeStyle = 'rgba(34,113,177,.7)';
            ctx.lineWidth = 1;
            ctx.setLineDash([4, 3]);
            ctx.strokeRect(r.x - 2, r.y - 2, r.w + 4, r.h + 4);
            if (rects[opts.selected].resizable) {
                ctx.setLineDash([]);
                ctx.fillStyle = 'rgba(34,113,177,.95)';
                var th = Math.max(5, Math.round(mmpx * 0.7));      // Griff-Dicke
                var bx = Math.max(12, Math.min(r.w * 0.5, mmpx * 5)); // Laenge horiz.
                var by = Math.max(12, Math.min(r.h * 0.5, mmpx * 5)); // Laenge vert.
                var cx = r.x + r.w / 2, cy = r.y + r.h / 2;
                // links / rechts (Breite)
                ctx.fillRect(r.x - th / 2,        cy - by / 2, th, by);
                ctx.fillRect(r.x + r.w - th / 2,  cy - by / 2, th, by);
                // oben / unten (Hoehe)
                ctx.fillRect(cx - bx / 2, r.y - th / 2,        bx, th);
                ctx.fillRect(cx - bx / 2, r.y + r.h - th / 2,  bx, th);
            }
            ctx.restore();
        }

        return { canvas: c, rects: rects };
    }

    (function() {
        var $sel = $('#qsfw-label-tpl-select');
        if (!$sel.length) return;
        var labels = (window.QSFW && QSFW.labels) || {};
        var saved  = null;
        try { saved = window.localStorage.getItem('qsfw_label_tpl'); } catch (e) {}
        if (saved && labels[saved]) $sel.val(saved);
        $sel.on('change', function() {
            try { window.localStorage.setItem('qsfw_label_tpl', $(this).val()); } catch (e) {}
        });
    })();

    function qsfwGetTemplate(id) {
        var labels = (window.QSFW && QSFW.labels) || {};
        if (id && labels[id]) return labels[id];
        var did = window.QSFW && QSFW.defaultLabel;
        if (did && labels[did]) return labels[did];
        for (var k in labels) { if (labels.hasOwnProperty(k)) return labels[k]; }
        return null;
    }

    // Gemeinsamer Druck-Helfer: legt eine flache Liste fertig gerenderter Etiketten-
    // Bilder (je ein Bild = eine Druckseite in Etikettengröße) in einen versteckten
    // iframe und ruft print(). `onDone` (optional) feuert nach dem Druck (afterprint
    // + Fallback-Timeout) — genutzt vom Wareneingang, um danach neu zu laden.
    // `onProgress(phase, done, total)` (optional) meldet den Ladefortschritt.  Auf
    // document-ready-Ebene (nicht in der Bulk-IIFE), damit auch die PO-Detailseite ihn erreicht.
    function qsfwPrintLabelImages(tpl, pages, skippedCount, onDone, onProgress) {
        var wmm = (parseFloat(tpl.width_mm) || 54) + 'mm';
        var hmm = (parseFloat(tpl.height_mm) || 17) + 'mm';
        var imgsHtml = '';
        for (var i = 0; i < pages.length; i++) {
            imgsHtml += '<img src="' + pages[i] + '" />';
        }

        var iframe = document.createElement('iframe');
        iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;';
        document.body.appendChild(iframe);
        var doc = iframe.contentWindow.document;
        doc.open();
        doc.write([
            /* translators: %d is the total number of labels in the print job */
            '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + sprintfFn(__('Labels (%d)', 'quickstock-for-woocommerce'), pages.length) + '</title>',
            '<style>',
            '@page { size:' + wmm + ' ' + hmm + '; margin:0; }',
            '@media print { body{margin:0;padding:0;} img{display:block; page-break-after:always; break-after:page;} img:last-child{page-break-after:auto; break-after:auto;} }',
            'body { margin:0; padding:0; }',
            'img { width:' + wmm + '; height:' + hmm + '; image-rendering:crisp-edges; image-rendering:pixelated; display:block; }',
            '</style></head><body>',
            imgsHtml,
            '</body></html>'
        ].join('\n'));
        doc.close();

        var finished = false;
        function finish() {
            if (finished) return;
            finished = true;
            try { document.body.removeChild(iframe); } catch (e) {}
            if (typeof onDone === 'function') onDone();
        }

        var imgs     = doc.querySelectorAll('img');
        var total    = imgs.length;
        var loaded   = 0;
        var printed  = false;
        var watchdog = null;

        // Druck anstossen — genau einmal, egal ob alle Bilder gemeldet haben oder
        // der Watchdog zugeschlagen hat.
        function ready() {
            if (printed) return;
            printed = true;
            if (watchdog) { clearTimeout(watchdog); watchdog = null; }
            if (typeof onProgress === 'function') onProgress('print', total, total);
            try {
                iframe.contentWindow.focus();
                iframe.contentWindow.onafterprint = function() { setTimeout(finish, 200); };
                iframe.contentWindow.print();
            } catch (e) {
                // Druck nicht möglich (Blocker, iframe weg): trotzdem aufräumen und
                // onDone feuern, sonst wartet der Aufrufer ewig.
                finish();
                return;
            }
            // Notausgang, falls afterprint nicht feuert (manche Browser). Bewusst
            // lang: ein kurzer Timer zöge das iframe unter einem noch offenen
            // Druckdialog weg und bräche den Druck ab.
            setTimeout(finish, 60000);
            if (skippedCount) {
                /* translators: %d is the number of products skipped because they have no SKU */
                alert(sprintfFn(_n('%d product was skipped (no SKU).', '%d products were skipped (no SKU).', skippedCount, 'quickstock-for-woocommerce'), skippedCount));
            }
        }

        function tick() {
            if (typeof onProgress === 'function') onProgress('load', loaded, total);
            if (loaded >= total) ready();
        }

        if (total === 0) { ready(); return; }

        // Watchdog: meldet auch nur ein Bild nie load/error (bekanntes Rennen mit dem
        // frisch angehängten iframe), wurde früher nie gedruckt UND nie aufgeräumt —
        // der Aufrufer hing still fest, der Klick sah aus wie wirkungslos. Nach 8 s
        // wird trotzdem gedruckt.
        watchdog = setTimeout(ready, 8000);

        for (var n = 0; n < total; n++) {
            (function(im) {
                if (im.complete && im.naturalWidth > 0) { loaded++; return; }
                im.onload = im.onerror = function() { loaded++; tick(); };
            })(imgs[n]);
        }
        tick();
    }

    // Etikettendruck aus expliziten Daten (Etappe 4, Wareneingang): jobs =
    // [{sku,title,regular,sale,price,qty}]. Rendert je Job qty Etiketten und druckt.
    // `onProgress(phase, done, total)` (optional) meldet den Fortschritt.
    //
    // Gerendert wird häppchenweise über setTimeout statt in einer Schleife am Stück:
    // sonst blockiert das Zeichnen den UI-Thread, der Browser käme nicht zum Neu-
    // zeichnen und der Fortschrittsbalken stünde still, bis alles fertig ist.
    function qsfwPrintLabelsFromData(tpl, jobs, onDone, onProgress) {
        var today = new Date().toLocaleDateString();
        var list  = jobs || [];
        var pages = [];
        var i     = 0;

        // Fehler werden hier drin abgefangen, nicht beim Aufrufer: step() läuft
        // asynchron weiter, ein try/catch um den Aufruf von aussen würde einen
        // späteren Häppchen-Fehler nicht mehr sehen — und onDone (der Reload)
        // bliebe aus. Genau dieses stille Steckenbleiben soll es nicht mehr geben.
        function step() {
            try {
                var end = Math.min(i + 5, list.length);
                for (; i < end; i++) {
                    var j   = list[i];
                    var sku = String(j.sku || '');
                    if (!sku) continue;
                    var reg  = parseFloat(j.regular);
                    var sale = parseFloat(j.sale);
                    var disp = parseFloat(j.price);
                    var data = {
                        sku:     sku,
                        title:   String(j.title || ''),
                        price:   isNaN(reg) ? (isNaN(disp) ? 0 : disp) : reg,
                        regular: isNaN(reg) ? 0 : reg
                        
                    };
                    var url = qsfwRenderLabelCanvas(tpl, data).canvas.toDataURL('image/png');
                    var qty = parseInt(j.qty, 10) || 0;
                    for (var k = 0; k < qty; k++) pages.push(url);
                }
            } catch (e) {
                // Ein defektes Etikett hält den Ablauf nicht an: Rest überspringen
                // und mit dem drucken, was fertig ist.
                i = list.length;
            }
            if (typeof onProgress === 'function') onProgress('render', i, list.length);
            if (i < list.length) { setTimeout(step, 0); return; }
            if (!pages.length) { if (typeof onDone === 'function') onDone(); return; }
            try {
                qsfwPrintLabelImages(tpl, pages, 0, onDone, onProgress);
            } catch (e) {
                if (typeof onDone === 'function') onDone();
            }
        }
        step();
    }

    $(document).on('click', '.label-print-btn', function() {
        var $b   = $(this);
        var sku  = String($b.data('sku') || '');
        if (!sku) { alert(__('No SKU set — cannot generate barcode.', 'quickstock-for-woocommerce')); return; }

        var tpl = qsfwGetTemplate($('#qsfw-label-tpl-select').val() || null);
        if (!tpl) { alert(__('No label template configured.', 'quickstock-for-woocommerce')); return; }

        var reg  = parseFloat($b.data('regular'));
        var sale = parseFloat($b.data('sale'));
        var disp = parseFloat($b.data('price'));
        var data = {
            sku:     sku,
            title:   String($b.data('title') || ''),
            price:   isNaN(reg) ? (isNaN(disp) ? 0 : disp) : reg,
            regular: isNaN(reg) ? 0 : reg
            
        };

        var canvas  = qsfwRenderLabelCanvas(tpl, data).canvas;
        var imgData = canvas.toDataURL('image/png');
        var wmm = (parseFloat(tpl.width_mm) || 54) + 'mm';
        var hmm = (parseFloat(tpl.height_mm) || 17) + 'mm';

        var iframe = document.createElement('iframe');
        iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;';
        document.body.appendChild(iframe);
        var doc = iframe.contentWindow.document;
        doc.open();
        doc.write([
            /* translators: %s is the product SKU printed on the label */
            '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + sprintfFn(__('Label %s', 'quickstock-for-woocommerce'), sku) + '</title>',
            '<style>',
            '@page { size:' + wmm + ' ' + hmm + '; margin:0; }',
            '@media print { body{margin:0;padding:0;} img{display:block;} }',
            'body { margin:0; padding:0; }',
            'img { width:' + wmm + '; height:' + hmm + '; image-rendering:crisp-edges; image-rendering:pixelated; }',
            '</style></head><body>',
            '<img src="' + imgData + '" />',
            '</body></html>'
        ].join('\n'));
        doc.close();
        var img = doc.querySelector('img');
        img.onload = function() {
            iframe.contentWindow.focus();
            iframe.contentWindow.print();
            setTimeout(function() { document.body.removeChild(iframe); }, 1000);
        };
    });

    // --- LIVE-VORSCHAU im Etiketten-Designer (Settings) ---
    (function() {
        var $prev = $('#qsfw-label-preview');
        if (!$prev.length) return;
        var $form = $prev.closest('form');

        function readTemplate() {
            var tpl = { name: '', width_mm: 54, height_mm: 17, dpi: 300, elements: {} };
            tpl.width_mm  = parseFloat($form.find('[name="lbl_width"]').val())  || 54;
            tpl.height_mm = parseFloat($form.find('[name="lbl_height"]').val()) || 17;
            tpl.dpi       = parseInt($form.find('[name="lbl_dpi"]').val(), 10)  || 300;
            $form.find('[data-el]').each(function() {
                var $w = $(this), type = $w.data('el'), el = {};
                $w.find('[data-f]').each(function() {
                    var $i = $(this), f = $i.data('f');
                    if ($i.is(':checkbox')) {
                        var v = $i.attr('value');
                        if (v && v !== '1') el[f] = $i.is(':checked') ? v : '';
                        else el[f] = $i.is(':checked') ? 1 : 0;
                    } else {
                        el[f] = $i.val();
                    }
                });
                tpl.elements[type] = el;
            });
            return tpl;
        }

        var SAMPLE = {
            sku: '7612345678900',
            title: __('Sample product with a longer name', 'quickstock-for-woocommerce'),
            price: 14.00, regular: 14.00
            
        };
        var lastRects = {}, dragging = null, selectedKey = null;

        function render() {
            var tpl = readTemplate();
            var res;
            try { res = qsfwRenderLabelCanvas(tpl, SAMPLE, { guides: true, selected: selectedKey }); }
            catch (e) { return; }
            var canvas = res.canvas;
            lastRects = res.rects;
            var maxW = ($prev.parent().width() || 520);
            var scale = Math.min(1, maxW / canvas.width);
            $prev.attr('width', canvas.width).attr('height', canvas.height);
            $prev.css({ width: Math.round(canvas.width * scale) + 'px', height: 'auto' });
            $prev[0].getContext('2d').drawImage(canvas, 0, 0);
        }

        // --- Drag im Vorschaufenster ---
        function pointerToCanvas(ev) {
            var el = $prev[0];
            var rect = el.getBoundingClientRect();
            var sx = el.width / rect.width;
            var sy = el.height / rect.height;
            return {
                x: (ev.clientX - rect.left) * sx,
                y: (ev.clientY - rect.top) * sy
            };
        }
        function hitTest(p) {
            var hit = null;
            for (var k in lastRects) {
                if (!lastRects.hasOwnProperty(k)) continue;
                var r = lastRects[k];
                if (p.x >= r.x - 3 && p.x <= r.x + r.w + 3 &&
                    p.y >= r.y - 3 && p.y <= r.y + r.h + 3) {
                    // kleinste Box gewinnt (praeziser bei Ueberlappung)
                    if (!hit || (r.w * r.h) < (hit.r.w * hit.r.h)) hit = { key: k, r: r };
                }
            }
            return hit;
        }
        // Welche Kante des selektierten Elements liegt unter dem Zeiger?
        // Liefert 'l' | 'r' | 't' | 'b' oder null.
        function edgeAt(p) {
            if (!selectedKey) return null;
            var r = lastRects[selectedKey];
            if (!r || !r.resizable) return null;
            var mmpx = (parseInt($form.find('[name="lbl_dpi"]').val(), 10) || 300) / 25.4;
            var zone = Math.max(10, mmpx * 1.3);
            var inY = p.y >= r.y - zone && p.y <= r.y + r.h + zone;
            var inX = p.x >= r.x - zone && p.x <= r.x + r.w + zone;
            // Mittlere Haelfte der Kante bevorzugen (Ecken bleiben „verschieben")
            var midY = p.y > r.y + r.h * 0.2 && p.y < r.y + r.h * 0.8;
            var midX = p.x > r.x + r.w * 0.2 && p.x < r.x + r.w * 0.8;
            if (inY && midY && Math.abs(p.x - (r.x + r.w)) <= zone) return 'r';
            if (inY && midY && Math.abs(p.x - r.x) <= zone) return 'l';
            if (inX && midX && Math.abs(p.y - (r.y + r.h)) <= zone) return 'b';
            if (inX && midX && Math.abs(p.y - r.y) <= zone) return 't';
            return null;
        }
        function setField(type, field, val) {
            var $i = $form.find('[data-el="' + type + '"] [data-f="' + field + '"]');
            if (!$i.length) return;
            if ($i.is(':checkbox')) $i.prop('checked', !!val);
            else $i.val(val);
        }

        var flashTimer = null;
        function focusElement(type) {
            var $fs = $form.find('[data-el="' + type + '"]');
            if (!$fs.length) return;
            var node = $fs[0];
            if (node.scrollIntoView) node.scrollIntoView({ behavior: 'smooth', block: 'center' });
            $form.find('.hb-label-el').removeClass('hb-flash');
            // Reflow erzwingen, damit die Animation auch bei gleichem Element neu startet
            void node.offsetWidth;
            $fs.addClass('hb-flash');
            clearTimeout(flashTimer);
            flashTimer = setTimeout(function() { $fs.removeClass('hb-flash'); }, 1600);
        }

        $prev.on('mousedown', function(ev) {
            ev.preventDefault();
            var p = pointerToCanvas(ev);
            var common = {
                mmpx: (parseInt($form.find('[name="lbl_dpi"]').val(), 10) || 300) / 25.4,
                wmm: parseFloat($form.find('[name="lbl_width"]').val())  || 54,
                hmm: parseFloat($form.find('[name="lbl_height"]').val()) || 17
            };
            // Resize nur an einer Kante des bereits selektierten Elements
            var edge = edgeAt(p);
            if (edge) {
                var rr = lastRects[selectedKey];
                dragging = {
                    mode: 'resize', key: selectedKey, edge: edge,
                    x0: rr.x, y0: rr.y, w0: rr.w, h0: rr.h,
                    mmpx: common.mmpx, wmm: common.wmm, hmm: common.hmm
                };
                $prev.css('cursor', (edge === 'l' || edge === 'r') ? 'ew-resize' : 'ns-resize');
                return;
            }
            var h = hitTest(p);
            if (!h) {
                if (selectedKey !== null) { selectedKey = null; render(); }
                return;
            }
            selectedKey = h.key;
            focusElement(h.key);
            dragging = {
                mode: 'move', key: h.key,
                offX: p.x - h.r.x, offY: p.y - h.r.y,
                mmpx: common.mmpx, wmm: common.wmm, hmm: common.hmm
            };
            $prev.css('cursor', 'grabbing');
            render(); // Auswahl-Rahmen + Kantengriffe sofort anzeigen
        });

        $(document).on('mousemove.qsfwlbl', function(ev) {
            if (!dragging) return;
            var p = pointerToCanvas(ev);
            var type = dragging.key;
            var mmpx = dragging.mmpx;

            if (dragging.mode === 'resize') {
                var e = dragging.edge;
                var x0 = dragging.x0, y0 = dragging.y0, w0 = dragging.w0, h0 = dragging.h0;
                if (e === 'r') {
                    var rw = Math.min(dragging.wmm, Math.max(1, Math.round(((p.x - x0) / mmpx) * 10) / 10));
                    setField(type, 'w', rw);
                } else if (e === 'l') {
                    // Linke Box-Kante: rechte Kante bleibt fix, X wandert mit.
                    var right0 = x0 + w0;
                    var newLeft = Math.min(right0 - 2, p.x);
                    var lw = Math.min(dragging.wmm, Math.max(1, Math.round(((right0 - newLeft) / mmpx) * 10) / 10));
                    setField(type, 'w', lw);
                    if (type === 'title') setField('title', 'after', false);
                    setField(type, 'x', Math.max(0, Math.round((newLeft / mmpx) * 10) / 10));
                } else if (e === 'b') {
                    var rh = Math.min(dragging.hmm, Math.max(1, Math.round(((p.y - y0) / mmpx) * 10) / 10));
                    setField(type, 'h', rh);
                } else if (e === 't') {
                    var bottom0 = y0 + h0;
                    var newTop = Math.min(bottom0 - 2, p.y);
                    var th = Math.min(dragging.hmm, Math.max(1, Math.round(((bottom0 - newTop) / mmpx) * 10) / 10));
                    setField(type, 'h', th);
                    setField(type, 'y', Math.max(0, Math.round((newTop / mmpx) * 10) / 10));
                }
                render();
                return;
            }

            // X = linke Box-Kante (gilt fuer jede Ausrichtung), Y = Oberkante.
            var xmm = (p.x - dragging.offX) / mmpx;
            var ymm = (p.y - dragging.offY) / mmpx;
            xmm = Math.min(dragging.wmm, Math.max(0, Math.round(xmm * 10) / 10));
            ymm = Math.min(dragging.hmm, Math.max(0, Math.round(ymm * 10) / 10));
            if (type === 'title') setField('title', 'after', false);
            setField(type, 'x', xmm);
            setField(type, 'y', ymm);
            render();
        });

        $(document).on('mouseup.qsfwlbl', function() {
            if (dragging) { dragging = null; $prev.css('cursor', ''); }
        });

        $prev.on('mousemove', function(ev) {
            if (dragging) return;
            var p = pointerToCanvas(ev);
            var e = edgeAt(p);
            if (e) { $prev.css('cursor', (e === 'l' || e === 'r') ? 'ew-resize' : 'ns-resize'); return; }
            $prev.css('cursor', hitTest(p) ? 'grab' : 'default');
        });

        $form.on('input change', 'input, select, textarea', render);
        render();
    })();

    // --- Erledigt-Checkbox ---
    $(document).on('change', '.stock-check', function() {
        var id = $(this).data('id');
        $.post(QSFW.ajaxUrl, {action: 'qsfw_update_value', nonce: QSFW.nonce, product_id: id, type: 'check', value: this.checked ? 1 : 0});
        $(this).closest('tr').toggleClass('stock-done', this.checked);
    });

    // --- Variationen auf-/zuklappen (Gruppierung im Alle-Produkte-Tab) ---
    function qsfwToggleGroup($row, expand) {
        var id = $row.attr('data-product-id');
        if (typeof expand === 'undefined') expand = !$row.hasClass('qsfw-expanded');
        $row.toggleClass('qsfw-expanded', expand);
        $row.find('.qsfw-var-toggle').attr('aria-expanded', expand ? 'true' : 'false');
        $row.find('.qsfw-var-arrow').text(expand ? '▼' : '▶');
        $('#stock-tbody').find('.qsfw-variation-row[data-parent="' + id + '"]')
            .prop('hidden', !expand)
            .toggleClass('qsfw-collapsed', !expand);
    }

    // Klick auf den Pfeil-Button
    $(document).on('click', '.qsfw-var-toggle', function(e) {
        e.preventDefault();
        e.stopPropagation();
        qsfwToggleGroup($(this).closest('.qsfw-parent-row'));
    });

    // Klick irgendwo auf die Eltern-Zeile (ausser auf Bedienelementen) klappt ebenfalls
    $(document).on('click', '.qsfw-parent-row', function(e) {
        if ($(e.target).closest('input, select, textarea, button, a, label, .num-spinner, .hb-img-edit, .label-print-btn, .copy-btn, .action-icon').length) return;
        qsfwToggleGroup($(this));
    });

    // --- Rechen-Ausdruck in Zahlenfeldern (z.B. 100*.8 → 80) ---
    // Sicherer rekursiver Descent-Parser (+ - * /, Klammern, unäres -/+,
    // Dezimal inkl. führendem Punkt). KEIN eval/Function. Rückgabe: Zahl
    // oder null bei ungültigem Ausdruck / Division durch 0.
    function hbCalc(raw) {
        var s = String(raw).trim().replace(/^=/, '').replace(/,/g, '.');
        if (s === '' || !/^[0-9+\-*/(). ]+$/.test(s)) return null;
        var i = 0;
        function skip() { while (i < s.length && s[i] === ' ') i++; }
        function parseExpr() {
            var v = parseTerm();
            if (v === null) return null;
            for (;;) {
                skip();
                var op = s[i];
                if (op !== '+' && op !== '-') return v;
                i++;
                var r = parseTerm();
                if (r === null) return null;
                v = (op === '+') ? v + r : v - r;
            }
        }
        function parseTerm() {
            var v = parseFactor();
            if (v === null) return null;
            for (;;) {
                skip();
                var op = s[i];
                if (op !== '*' && op !== '/') return v;
                i++;
                var r = parseFactor();
                if (r === null) return null;
                if (op === '/') { if (r === 0) return null; v = v / r; }
                else v = v * r;
            }
        }
        function parseFactor() {
            skip();
            var op = s[i];
            if (op === '+' || op === '-') {
                i++;
                var f = parseFactor();
                if (f === null) return null;
                return (op === '-') ? -f : f;
            }
            if (op === '(') {
                i++;
                var e = parseExpr();
                if (e === null) return null;
                skip();
                if (s[i] !== ')') return null;
                i++;
                return e;
            }
            var m = /^[0-9]*\.?[0-9]+/.exec(s.slice(i));
            if (!m) return null;
            i += m[0].length;
            return parseFloat(m[0]);
        }
        var result = parseExpr();
        skip();
        if (result === null || i !== s.length) return null;
        if (!isFinite(result)) return null;
        return result;
    }

    /* =====================================================================
     * AUTO-SAVE: zentrale Speicher-Helfer der Inline-Bearbeitung
     * Sofort speichern bei Blur/Enter; entprellt bei Pfeil-/Spinner-Schnellklicks.
     * Im Manuell-Modus ungenutzt – dort übernimmt der eigene qsfw:save-Block.
     * ===================================================================== */
    var qsfwManualMode = (QSFW.autosaveMode || 'auto') === 'manual';
    var qsfwPending = []; // DOM-Elemente mit offenem Timer, laufender Anfrage oder Fehler

    function qsfwMarkPending(el)  { if (qsfwPending.indexOf(el) === -1) qsfwPending.push(el); }
    function qsfwClearPending(el) { var k = qsfwPending.indexOf(el); if (k !== -1) qsfwPending.splice(k, 1); }

    function qsfwResetIndicators($t) {
        var $spinner = $t.closest('.num-spinner');
        $t.removeClass('saving saved failed');
        $t.next('.save-indicator').remove();
        $spinner.next('.save-indicator').remove();
    }

    // Rechen-Ausdruck auswerten (nur markierte Zahlenfelder). false = ungültiger Ausdruck.
    function qsfwEvalCalc($t) {
        if ($t.is('[data-calc]') && !$t.is(':checkbox')) {
            var raw = $.trim($t.val());
            if (raw !== '' && !/^-?\d*[.,]?\d+$/.test(raw)) {
                var res = hbCalc(raw);
                if (res === null) {
                    $t.addClass('failed');
                    $t.after('<span class="save-indicator fail" title="' + __('Invalid expression', 'quickstock-for-woocommerce') + '">✗</span>');
                    return false;
                }
                var calc = $t.data('calc');
                if (calc === 'int')        res = Math.round(res);
                else if (calc === 'money') res = Math.round(res * 100) / 100;
                else                       res = Math.round(res * 1e6) / 1e6;
                $t.val(String(res));
            }
        }
        return true;
    }

    // Eigentlicher AJAX-Speichervorgang. Erfolg → pending gelöscht; Fehler → bleibt pending.
    function qsfwDoSave($t) {
        var el = $t[0];
        var tm = $t.data('qsfwTimer'); if (tm) { clearTimeout(tm); $t.removeData('qsfwTimer'); }
        var $spinner = $t.closest('.num-spinner');
        $t.addClass('saving');
        qsfwMarkPending(el);

        var sendVal = $t.is(':checkbox') ? ($t.is(':checked') ? '1' : '0') : $t.val();
        $.post(QSFW.ajaxUrl, {
            action: 'qsfw_update_value',
            nonce: QSFW.nonce,
            product_id: $t.data('id'),
            type: $t.data('type'),
            value: sendVal
        })
        .done(function(response) {
            $t.removeClass('saving');
            if (response && response.success) {
                qsfwClearPending(el);
                // Lager erhöht → Erledigt-Markierung serverseitig entfernt:
                // Zeile clientseitig wieder aktiv schalten.
                if (response.data && response.data.uncheck) {
                    var $row = $t.closest('tr');
                    $row.removeClass('stock-done');
                    $row.find('.stock-check').prop('checked', false);
                }
                $t.addClass('saved');
                var $ok = $('<span class="save-indicator ok">✓</span>');
                ($spinner.length ? $spinner : $t).after($ok);
                setTimeout(function() {
                    $t.removeClass('saved');
                    $ok.fadeOut(300, function(){ $(this).remove(); });
                }, 1500);
            } else {
                // Fehlschlag bleibt „pending" → beforeunload warnt, Verlust nicht still.
                $t.addClass('failed');
                ($spinner.length ? $spinner : $t).after('<span class="save-indicator fail" title="' + __('Error', 'quickstock-for-woocommerce') + '">✗</span>');
            }
        })
        .fail(function() {
            // Netzwerkfehler bleibt ebenfalls „pending".
            $t.removeClass('saving').addClass('failed');
            ($spinner.length ? $spinner : $t).after('<span class="save-indicator fail" title="' + __('Connection error', 'quickstock-for-woocommerce') + '">✗</span>');
        });
    }

    // Sofort speichern (Blur, Enter, programmatischer change).
    function qsfwSaveNow($t) {
        qsfwResetIndicators($t);
        if (!qsfwEvalCalc($t)) return;
        qsfwDoSave($t);
    }

    // Entprellt speichern (Pfeiltasten / Spinner-Schnellklicks): nur der Endwert geht raus.
    function qsfwSaveDebounced($t, ms) {
        qsfwResetIndicators($t);
        qsfwMarkPending($t[0]); // offener Timer zählt bereits als pending
        var tm = $t.data('qsfwTimer'); if (tm) clearTimeout(tm);
        tm = setTimeout(function() {
            $t.removeData('qsfwTimer');
            if (!qsfwEvalCalc($t)) { qsfwClearPending($t[0]); return; }
            qsfwDoSave($t);
        }, ms || 500);
        $t.data('qsfwTimer', tm);
    }

    // --- Num-Spinner Buttons (Lager / Schwellenwert) ---
    $(document).on('click', '.ns-btn', function() {
        var $input = $(this).closest('.num-spinner').find('.edit-input');
        if ($input.prop('disabled')) return;
        var val = parseInt($input.val(), 10);
        if (isNaN(val)) val = 0;
        val += $(this).hasClass('ns-up') ? 1 : -1;
        $input.val(val);
        if (qsfwManualMode) $input.trigger('change');   // Manuell: als dirty markieren
        else                qsfwSaveDebounced($input, 500);
    });

    // --- Universeller Save ---
    // Textareas: speichern auch beim Verlassen (blur)
    $(document).on('blur', '.textarea-input', function() {
        $(this).trigger('change');
    });
    // Im Auto-Modus aktiv; der Manuell-Block hängt diesen Handler per .off() wieder aus.
    $(document).on('change', '.edit-input', function() {
        qsfwSaveNow($(this));
    });

    // Enter speichert sofort (außer in mehrzeiligen Textfeldern → dort Zeilenumbruch).
    // Schließt die Lücke „getippt, aber nie verlassen → nie gespeichert".
    $(document).on('keydown', '.edit-input', function(e) {
        if (qsfwManualMode) return;
        if (e.key !== 'Enter') return;
        if ($(this).is('textarea')) return;
        e.preventDefault();
        qsfwSaveNow($(this));
    });

    // Vor dem Seitenverlassen warnen, solange im Auto-Modus etwas aussteht:
    // offener Debounce-Timer, laufende Anfrage oder fehlgeschlagene Speicherung.
    if (!qsfwManualMode) {
        window.addEventListener('beforeunload', function(e) {
            if (qsfwPending.length > 0) {
                e.preventDefault();
                e.returnValue = '';
            }
        });
    }

    // --- Lagerverwaltung-Toggle ---
    $(document).on('change', '.manage-stock-toggle', function() {
        var $t = $(this);
        var checked = this.checked;

        $.post(QSFW.ajaxUrl, {
            action: 'qsfw_update_value',
            nonce: QSFW.nonce,
            product_id: $t.data('id'),
            type: 'manage_stock',
            value: checked ? 'yes' : 'no'
        }).done(function() {
            $t.css('outline', '2px solid #46b450');
            setTimeout(function(){ $t.css('outline', ''); }, 1000);
        });

        var $stock = $t.closest('tr').find('input[data-type="stock"]');
        if (checked) {
            $stock.prop('disabled', false).attr('placeholder', '').removeClass('stock-disabled');
        } else {
            $stock.prop('disabled', true).attr('placeholder', '—').val('').addClass('stock-disabled');
        }
    });

    $('#reset-checks').on('click', function() {
        if(confirm(__('Clear all checkmarks?', 'quickstock-for-woocommerce'))) {
            $.post(QSFW.ajaxUrl, {action: 'qsfw_reset_checks', nonce: QSFW.nonce}, function() { location.reload(); });
        }
    });

    // --- LIVE-SUCHE ---
    var searchTimer = null, savedTbody = null, savedTablenav = null;
    $('#stock-search').on('input', function() {
        var query = $(this).val().trim();
        var $tbody = $('#stock-tbody');
        var $tablenav = $('.tablenav');
        var $status = $('#search-status');

        if (query === '') {
            if (savedTbody !== null) {
                $tbody.html(savedTbody);
                if (savedTablenav !== null) $tablenav.show();
                savedTbody = null; savedTablenav = null;
            }
            $status.html('');
            return;
        }
        if (savedTbody === null) {
            savedTbody = $tbody.html();
            savedTablenav = $tablenav.html();
        }
        $status.html('<span class="searching">' + __('Searching…', 'quickstock-for-woocommerce') + '</span>');
        clearTimeout(searchTimer);
        searchTimer = setTimeout(function() {
            $.post(QSFW.ajaxUrl, {
                action: 'qsfw_search_products',
                nonce: QSFW.nonce,
                query: query,
                date: QSFW.date,
                sort: QSFW.sort,
                mode: QSFW.mode,
                category: QSFW.category
            }, function(response) {
                if (response && response.success) {
                    $tbody.html(response.data.html);
                    $tablenav.hide();
                    /* translators: %d is the number of search hits on the current page */
                    var hitsHtml = '<span class="hits">' + sprintfFn(_n('%d hit', '%d hits', response.data.count, 'quickstock-for-woocommerce'), response.data.count) + '</span>';
                    /* translators: 1: hit count HTML (e.g. "12 hits"), 2: total number of matching products */
                    $status.html(sprintfFn(__('%1$s of %2$d', 'quickstock-for-woocommerce'), hitsHtml, response.data.total));
                } else {
                    $status.html('<span style="color:#dc3232;">' + __('Search failed', 'quickstock-for-woocommerce') + '</span>');
                }
            });
        }, 250);
    });
    $('#stock-search').on('keydown', function(e) {
        if (e.key === 'Escape') $(this).val('').trigger('input');
        else if (e.key === 'Enter') e.preventDefault();
    });

    // Suchbegriff nach Bulk-Reload wiederherstellen (hb_q aus der URL),
    // dann den Parameter still aus der URL entfernen, damit er beim
    // nächsten normalen Filterwechsel nicht hängen bleibt.
    (function() {
        var $s = $('#stock-search');
        if (!$s.length) return;
        var params = new URLSearchParams(window.location.search);
        var q = params.get('hb_q');
        if (q && q.trim() !== '') {
            $s.val(q).trigger('input');
        }
        if (params.has('hb_q') && window.history && window.history.replaceState) {
            params.delete('hb_q');
            var url = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
            window.history.replaceState(null, '', url);
        }
    })();

    // Dropdowns schliessen bei Klick ausserhalb
    $(document).on('click', function(e) {
        $('details.hb-dropdown[open]').each(function() {
            if (!$(this).is(e.target) && $(this).has(e.target).length === 0) {
                this.removeAttribute('open');
            }
        });
    });
    // === PRESET-EDITOR: Drag & Drop + Checkboxen ===
    var presetList = document.getElementById('hb-preset-sortable');
    if (presetList) {
        var syncPresetCols = function() {
            var keys = [];
            $('#hb-preset-sortable .hb-sortable-item').each(function() {
                if ($(this).find('.hb-preset-check').is(':checked')) {
                    keys.push($(this).data('key'));
                }
            });
            $('#hb-preset-cols-input').val(keys.join(','));
        };
        if (typeof Sortable !== 'undefined') {
            Sortable.create(presetList, {
                handle: '.hb-drag-handle',
                animation: 150,
                ghostClass: 'sortable-ghost',
                chosenClass: 'sortable-chosen',
                onEnd: syncPresetCols
            });
        }
        $(document).on('change', '#hb-preset-sortable .hb-preset-check', syncPresetCols);
    }

    // === SPALTEN-DROPDOWN: Drag & Drop (im Tab) ===
    var colDropSortable = document.getElementById('hb-col-dropdown-sortable');
    if (colDropSortable && typeof Sortable !== 'undefined') {
        var syncTabColOrder = function() {
            var keys = [];
            $('#hb-col-dropdown-sortable .hb-sortable-item').each(function() {
                keys.push($(this).data('key'));
            });
            $('#hb-tab-col-order').val(keys.join(','));
            // Dropdown als "geändert" markieren damit Toggle-Handler submittet
            $('.hb-col-dropdown').data('hb-changed', true);
        };
        Sortable.create(colDropSortable, {
            handle: '.hb-drag-handle',
            animation: 150,
            ghostClass: 'sortable-ghost',
            chosenClass: 'sortable-chosen',
            onEnd: syncTabColOrder
        });
    }

    

    // === AUSWAHL-CHECKBOXEN im Reorder-Tab ===
    // Im „Alle Produkte"-Tab übernimmt die Bulk-IIFE die Auswahl-Logik (inkl.
    // Bulk-Leisten-Update). Im Reorder-Tab gibt es keine Bulk-Leiste, deshalb
    // braucht es hier einen eigenen, schlanken „Alle auswählen"-Handler.
    $(document).on('change', '#hb-bulk-all', function() {
        if ($('#hb-bulk-bar').length || $('#hb-reorder-bulk-bar').length) return; // Bulk-IIFE übernimmt
        $('.hb-bulk-check').prop('checked', this.checked);
    });

    // === PRESET SPEICHERN (💾 Button im Ansicht-Dropdown) ===
    $(document).on('click', '#hb-save-preset-btn', function(e) {
        e.stopPropagation(); // Dropdown nicht schliessen
        var name = $.trim($('#hb-save-preset-name').val());
        if (!name) {
            $('#hb-save-preset-name').focus();
            return;
        }
        $('#hb-filter-save-preset-name').val(name);
        $('#hb-filter-form').trigger('submit');
    });
    // Aktives (angepasstes) Preset direkt überschreiben
    $(document).on('click', '.hb-preset-updbtn', function(e) {
        e.stopPropagation(); // Dropdown nicht schliessen
        var name = $(this).data('presetName');
        if (!name) return;
        $('#hb-filter-save-preset-name').val(name);
        $('#hb-filter-form').trigger('submit');
    });
    // Enter im Namensfeld = Speichern
    $(document).on('keydown', '#hb-save-preset-name', function(e) {
        if (e.key === 'Enter') {
            e.preventDefault();
            $('#hb-save-preset-btn').trigger('click');
        }
        e.stopPropagation(); // nicht ans globale Enter-Handler weitergeben
    });

    // === SPALTENLISTE (Einstellungen) durchsuchen ===
    $(document).on('input', '#qsfw-col-search', function() {
        var q = $.trim($(this).val()).toLowerCase();
        $('#qsfw-col-table tbody tr').each(function() {
            var $r = $(this);
            $r.toggle(q === '' || $r.text().toLowerCase().indexOf(q) !== -1);
        });
    });

    // Preset löschen bestätigen
    $(document).on('click', '.hb-preset-delete', function(e) {
        if (!confirm(__('Really delete this preset?', 'quickstock-for-woocommerce'))) e.preventDefault();
    });

    /* =====================================================================
     * MASSENBEARBEITUNG (nur Tab „Alle Produkte")
     * ===================================================================== */
    (function() {
        var $bar = $('#hb-bulk-bar');
        if (!$bar.length) return;

        // Lazy-Getter: Übersetzungen werden erst beim ersten Bulk-Aufruf gelesen,
        // damit Locale-Daten via wp_set_script_translations() sicher geladen sind.
        function getStatusLists() {
            return {
                stock_status: [
                    ['instock',     __('In stock', 'quickstock-for-woocommerce')],
                    ['outofstock',  __('Out of stock', 'quickstock-for-woocommerce')],
                    ['onbackorder', __('On backorder', 'quickstock-for-woocommerce')]
                ],
                backorders: [
                    ['no',     __('Do not allow', 'quickstock-for-woocommerce')],
                    ['notify', __('Allow, but notify customer', 'quickstock-for-woocommerce')],
                    ['yes',    __('Allow', 'quickstock-for-woocommerce')]
                ],
                post_status: [
                    ['publish', __('Published', 'quickstock-for-woocommerce')],
                    ['draft',   __('Draft', 'quickstock-for-woocommerce')],
                    ['private', __('Private', 'quickstock-for-woocommerce')],
                    ['pending', __('Pending review', 'quickstock-for-woocommerce')]
                ]
            };
        }

        function selectedIds() {
            var ids = [];
            $('.hb-bulk-check:checked').each(function() { ids.push($(this).data('id')); });
            return ids;
        }

        // Massendruck: alle ausgewählten Produkte mit Menge qty pro Stück drucken.
        // Jede Etikette = eigene Druckseite in Etikettengröße (gleicher Modus wie Einzeldruck).
        function qsfwBulkPrintLabels(ids, qty) {
            var tpl = qsfwGetTemplate($('#qsfw-label-tpl-select').val() || null);
            if (!tpl) { alert(__('No label template configured.', 'quickstock-for-woocommerce')); return; }

            var today = new Date().toLocaleDateString();
            var images = [];
            var skipped = [];

            ids.forEach(function(id) {
                var $btn = $('.hb-bulk-check[data-id="' + id + '"]').closest('tr').find('.label-print-btn').first();
                if (!$btn.length) { skipped.push(id); return; }
                var sku = String($btn.data('sku') || '');
                if (!sku) { skipped.push(id); return; }
                var reg  = parseFloat($btn.data('regular'));
                var sale = parseFloat($btn.data('sale'));
                var disp = parseFloat($btn.data('price'));
                var data = {
                    sku:     sku,
                    title:   String($btn.data('title') || ''),
                    price:   isNaN(reg) ? (isNaN(disp) ? 0 : disp) : reg,
                    regular: isNaN(reg) ? 0 : reg
                    
                };
                var canvas = qsfwRenderLabelCanvas(tpl, data).canvas;
                images.push(canvas.toDataURL('image/png'));
            });

            if (!images.length) {
                alert(__('No printable labels — none of the selected products has a SKU.', 'quickstock-for-woocommerce'));
                return;
            }

            // qty Kopien je Produkt → flache Seitenliste, dann gemeinsamer Druck-Helfer.
            var pages = [];
            for (var i = 0; i < images.length; i++) {
                for (var k = 0; k < qty; k++) pages.push(images[i]);
            }
            // Nach dem Druck die Bulk-Auswahl zurücksetzen und die Leiste schliessen
            // (analog zu „Cancel"), damit die Aufgabe nicht offen stehen bleibt.
            qsfwPrintLabelImages(tpl, pages, skipped.length, resetBulk);
        }

        // Setzt die Bulk-Auswahl zurück und blendet die Leiste aus.
        function resetBulk() {
            $('.hb-bulk-check, #hb-bulk-all').prop('checked', false);
            $('#hb-bulk-op').val('');
            $('#hb-bulk-value').empty();
            refreshBar();
        }

        function refreshBar() {
            var n = $('.hb-bulk-check:checked').length;
            $('#hb-bulk-count').text(n);
            if (n > 0) { $bar.addClass('is-visible').attr('aria-hidden', 'false'); }
            else       { $bar.removeClass('is-visible').attr('aria-hidden', 'true'); }
            var total = $('.hb-bulk-check').length;
            $('#hb-bulk-all').prop('checked', total > 0 && n === total);
        }

        function termOptions(list) {
            var h = '<option value="0">— —</option>';
            (list || []).forEach(function(t) {
                h += '<option value="' + t.id + '">' + $('<i>').text(t.name).html() + '</option>';
            });
            return h;
        }

        function enumOptions(list) {
            var h = '';
            (list || []).forEach(function(o) {
                h += '<option value="' + o[0] + '">' + o[1] + '</option>';
            });
            return h;
        }

        

        // Baut das passende Wertfeld je nach gewählter Aktion
        function renderValue(op) {
            var T = (QSFW.bulkTerms || {});
            var cur = (QSFW.currencySymbol || '').toString();
            switch (op) {
                case 'stock_set':
                case 'limit_set':
                case 'restock_set':
                    return '<input type="text" class="hb-bulk-input" id="hb-bulk-v" data-calc="int" placeholder="' + __('e.g. 0 or 50*.8', 'quickstock-for-woocommerce') + '">';
                case 'stock_inc':
                    return '<input type="text" class="hb-bulk-input" id="hb-bulk-v" data-calc="int" placeholder="' + __('e.g. +5 or -2', 'quickstock-for-woocommerce') + '">';
                case 'price_set':
                    return '<input type="text" class="hb-bulk-input" id="hb-bulk-v" data-calc="money" placeholder="' + __('e.g. 9.90 or 12*.8', 'quickstock-for-woocommerce') + '"> ' + cur;
                case 'stock_status':
                case 'backorders':
                case 'post_status':
                    return '<select class="hb-bulk-input" id="hb-bulk-v">' + enumOptions(getStatusLists()[op]) + '</select>';
                
                case 'shipping_class':
                    return '<select class="hb-bulk-input hb-bulk-term" id="hb-bulk-v">' + termOptions(T.shipping_class) + '</select>';
                
                case 'cat_add':
                case 'cat_remove':
                    return '<select class="hb-bulk-input hb-bulk-term" id="hb-bulk-v">' + termOptions(T.product_cat) + '</select>';
                case 'sale_set':
                    /* translators: %s is the shop currency symbol (e.g. "CHF") — marks the input as a fixed amount, not a percentage */
                    var fixedLabel = sprintfFn(__('%s (fixed)', 'quickstock-for-woocommerce'), cur || __('Amount', 'quickstock-for-woocommerce'));
                    return '<input type="text" class="hb-bulk-input" id="hb-bulk-v" data-calc="money" placeholder="' + __('Value (e.g. 9.90 or 12*.8)', 'quickstock-for-woocommerce') + '"> '
                         + '<select class="hb-bulk-input" id="hb-bulk-sub"><option value="fixed">' + fixedLabel + '</option><option value="pct">' + __('% discount off regular', 'quickstock-for-woocommerce') + '</option></select> '
                         + '<select class="hb-bulk-input" id="hb-bulk-v2" title="' + __('Rounding (only for % discount)', 'quickstock-for-woocommerce') + '"><option value="exact">' + __('Exact', 'quickstock-for-woocommerce') + '</option><option value="0.05">' + __('to 0.05', 'quickstock-for-woocommerce') + '</option><option value="0.1">' + __('to 0.10', 'quickstock-for-woocommerce') + '</option></select>';
                case 'sale_dates':
                    return __('from', 'quickstock-for-woocommerce') + ' <input type="date" class="hb-bulk-input" id="hb-bulk-v"> '
                         + __('to', 'quickstock-for-woocommerce') + ' <input type="date" class="hb-bulk-input" id="hb-bulk-v2">';
                case 'label_print':
                    return '<input type="number" class="hb-bulk-input" id="hb-bulk-v" min="1" max="999" step="1" value="1" style="width:70px;"> ' + __('× per product', 'quickstock-for-woocommerce');
                case 'manage_stock_on':
                case 'manage_stock_off':
                case 'sale_clear':
                case 'sale_dates_clear':
                case 'export_csv':
                case 'trash':
                case '':
                    return '';
                default:
                    
            }
        }

        $(document).on('change', '.hb-bulk-check', refreshBar);

        $(document).on('change', '#hb-bulk-all', function() {
            var on = this.checked;
            $('.hb-bulk-check').prop('checked', on);
            refreshBar();
        });

        // selectWoo (Suchfeld) auf die term-basierten Bulk-Wertfelder anwenden.
        // WICHTIG: KEIN dropdownParent auf #hb-bulk-bar setzen — die Leiste ist
        // position:fixed mit transform + backdrop-filter. Diese Kombination bricht
        // die Positionsberechnung von Select2 (das Dropdown landet weit unterhalb
        // des Viewports und wirkt, als ginge es gar nicht auf). Stattdessen am
        // Default-Parent <body> hängen lassen: dort flippt Select2 das Dropdown bei
        // wenig Platz automatisch nach oben, und .select2-container--open
        // (z-index:100010, admin.css) hält es über der Leiste.
        function initBulkTermSearch() {
            if (!$.fn.selectWoo) return;
            $('#hb-bulk-value .hb-bulk-term').each(function() {
                $(this).selectWoo({
                    placeholder: __('Search category…', 'quickstock-for-woocommerce'),
                    allowClear: false,
                    width: '240px'
                });
            });
        }

        $('#hb-bulk-op').on('change', function() {
            $('#hb-bulk-value').html(renderValue(this.value));
            initBulkTermSearch();
        });

        $('#hb-bulk-cancel').on('click', resetBulk);

        // --- Anwenden ---
        $('#hb-bulk-apply').on('click', function() {
            var ids = selectedIds();
            var op  = $('#hb-bulk-op').val();
            if (!ids.length) { alert(__('No products selected.', 'quickstock-for-woocommerce')); return; }
            if (!op)         { alert(__('Please select an action.', 'quickstock-for-woocommerce')); return; }

            // CSV-Export: direkter Datei-Download, kein Bulk-Request
            if (op === 'export_csv') {
                $('#hb-bulk-export-ids').val(ids.join(','));
                $('#hb-bulk-export-form')[0].submit();
                return;
            }

            // Etikettendruck: clientseitig, kein Bulk-Request
            if (op === 'label_print') {
                var qty = parseInt($('#hb-bulk-v').val(), 10);
                if (!qty || qty < 1) { alert(__('Please enter a valid quantity (≥ 1).', 'quickstock-for-woocommerce')); return; }
                if (qty > 999) qty = 999;
                qsfwBulkPrintLabels(ids, qty);
                return;
            }

            // Rechen-Ausdruck im Wertfeld auswerten (z.B. 50*.8 → 40),
            // analog zur normalen Tabelle.
            var $bv = $('#hb-bulk-v');
            if ($bv.length && $bv.is('[data-calc]')) {
                var rawv = $.trim($bv.val());
                if (rawv !== '' && !/^-?\d*[.,]?\d+$/.test(rawv)) {
                    var rv = hbCalc(rawv);
                    if (rv === null) { alert(__('Invalid calculation in value field.', 'quickstock-for-woocommerce')); return; }
                    var cm = $bv.data('calc');
                    if (cm === 'int')        rv = Math.round(rv);
                    else if (cm === 'money') rv = Math.round(rv * 100) / 100;
                    else                     rv = Math.round(rv * 1e6) / 1e6;
                    $bv.val(String(rv));
                }
            }

            var val  = $('#hb-bulk-v').length  ? String($('#hb-bulk-v').val())  : '';
            var val2 = $('#hb-bulk-v2').length ? String($('#hb-bulk-v2').val()) : '';
            var sub  = $('#hb-bulk-sub').length ? String($('#hb-bulk-sub').val()) : '';
            var opLabel = $('#hb-bulk-op option:selected').text();

            // Bestätigungs-Dialog gemäss Einstellung
            var bcMode   = (QSFW.bulkConfirmMode  || 'always');
            var bcThresh = parseInt(QSFW.bulkConfirmThreshold, 10) || 50;
            var needConfirm = (bcMode === 'always') ||
                              (bcMode === 'threshold' && ids.length >= bcThresh);

            if (!needConfirm) {
                // Ohne Modal kein Opt-In möglich → konservativer Default:
                // sale_set überschreibt nicht, sale_dates wird auf alles angewendet.
                runBulk(ids, op, val, val2, sub, {});
                return;
            }

            /* translators: %d is the number of selected products to be changed by the bulk operation */
            var countText = sprintfFn(_n('%d product will be changed', '%d products will be changed', ids.length, 'quickstock-for-woocommerce'), ids.length);
            /* translators: 1: count phrase (e.g. "12 products will be changed"), 2: bulk operation label */
            $('#hb-bulk-modal-msg').text(sprintfFn(__('%1$s: “%2$s”. Continue?', 'quickstock-for-woocommerce'), countText, opLabel));

            // Aktionsspezifische Optionen (Checkboxen) ins Modal rendern.
            var optsHtml = '';
            if (op === 'sale_set') {
                optsHtml = '<label class="hb-bulk-opt"><input type="checkbox" id="hb-bulk-opt-overwrite"> ' +
                    __('Also overwrite existing sale prices', 'quickstock-for-woocommerce') + '</label>';
            } else if (op === 'sale_dates') {
                optsHtml = '<label class="hb-bulk-opt"><input type="checkbox" id="hb-bulk-opt-only-onsale"> ' +
                    __('Only for products with an existing sale price', 'quickstock-for-woocommerce') + '</label>';
            }
            $('#hb-bulk-modal-opts').html(optsHtml);

            $('#hb-bulk-progress').hide();
            $('#hb-bulk-result').hide();
            $('#hb-bulk-modal-actions').show();
            $('#hb-bulk-modal').addClass('is-visible').attr('aria-hidden', 'false');

            $('#hb-bulk-confirm').off('click').on('click', function() {
                var opts = {};
                if (op === 'sale_set')   opts.overwrite   = $('#hb-bulk-opt-overwrite').is(':checked') ? '1' : '0';
                if (op === 'sale_dates') opts.only_onsale = $('#hb-bulk-opt-only-onsale').is(':checked') ? '1' : '0';
                runBulk(ids, op, val, val2, sub, opts);
            });
        });

        $('#hb-bulk-abort').on('click', function() {
            $('#hb-bulk-modal').removeClass('is-visible').attr('aria-hidden', 'true');
        });
        $('#hb-bulk-done').on('click', function() {
            // Der Ansichts-State (Spalten, Sortierung, Filter) liegt in user_meta
            // und muss nicht mehr serialisiert werden – das Formular ist jetzt
            // POST und liesse sich ohnehin nicht als GET-Query nachbauen.
            // Wie bisher zurück auf Seite 1, aktive Suche bleibt erhalten.
            var url = new URL(window.location.href);
            url.searchParams.delete('paged');
            var q = $.trim($('#stock-search').val() || '');
            if (q !== '') { url.searchParams.set('hb_q', q); } else { url.searchParams.delete('hb_q'); }
            window.location.href = url.toString();
        });

        function runBulk(ids, op, val, val2, sub, opts) {
            var chunk = parseInt(QSFW.bulkChunk, 10) || 25;
            var total = ids.length, done = 0, skipped = [];
            var blocks = [];
            for (var i = 0; i < ids.length; i += chunk) blocks.push(ids.slice(i, i + chunk));
            opts = opts || {};

            $('#hb-bulk-modal-actions').hide();
            // Optionen-Checkboxen ausblenden, sobald die Verarbeitung läuft –
            // ihr Wert wurde bereits beim Bestätigen gelesen, weitere Klicks
            // hätten keinen Effekt und wären für den Nutzer irreführend.
            $('#hb-bulk-modal-opts').empty();
            $('#hb-bulk-progress').show();
            $('#hb-bulk-ptotal').text(total);
            $('#hb-bulk-pdone').text(0);
            $('.hb-bulk-pfill').css('width', '0%');
            $('#hb-bulk-modal-msg').text(__('Processing…', 'quickstock-for-woocommerce'));

            var idx = 0;
            function next() {
                if (idx >= blocks.length) { finish(); return; }
                $.post(QSFW.ajaxUrl, {
                    action: 'qsfw_bulk_apply',
                    nonce:  QSFW.nonce,
                    ids:    blocks[idx],
                    op:     op,
                    value:  val,
                    value2: val2,
                    submode: sub,
                    opts:   opts
                }).done(function(resp) {
                    if (resp && resp.success) {
                        done += resp.data.processed;
                        if (resp.data.skipped && resp.data.skipped.length)
                            skipped = skipped.concat(resp.data.skipped);
                    } else {
                        skipped = skipped.concat(blocks[idx].map(function(id) {
                            return { id: id, reason: (resp && resp.data && resp.data.message) || __('Error', 'quickstock-for-woocommerce') };
                        }));
                    }
                }).fail(function() {
                    skipped = skipped.concat(blocks[idx].map(function(id) {
                        return { id: id, reason: __('Connection error', 'quickstock-for-woocommerce') };
                    }));
                }).always(function() {
                    idx++;
                    var seen = Math.min(idx * chunk, total);
                    $('#hb-bulk-pdone').text(seen);
                    $('.hb-bulk-pfill').css('width', Math.round(seen / total * 100) + '%');
                    next();
                });
            }

            function finish() {
                /* translators: %d is the number of products successfully updated by the bulk operation */
                var msg = sprintfFn(_n('%d product changed.', '%d products changed.', done, 'quickstock-for-woocommerce'), done);
                if (skipped.length) {
                    var reasons = {};
                    skipped.forEach(function(s) {
                        reasons[s.reason] = (reasons[s.reason] || 0) + 1;
                    });
                    var parts = [];
                    Object.keys(reasons).forEach(function(r) {
                        parts.push(reasons[r] + '× ' + r);
                    });
                    /* translators: %d is the number of products skipped during the bulk operation */
                    msg += ' ' + sprintfFn(_n('%d skipped', '%d skipped', skipped.length, 'quickstock-for-woocommerce'), skipped.length) + ' (' + parts.join(', ') + ').';
                }
                $('#hb-bulk-progress').hide();
                $('#hb-bulk-result-msg').text(msg);
                $('#hb-bulk-result').show();
            }

            next();
        }

        refreshBar();
    })();

    /* =====================================================================
     * MASSENBEARBEITUNG — Tab „Nachbestellung"
     * ===================================================================== */
    (function() {
        var $bar = $('#hb-reorder-bulk-bar');
        if (!$bar.length) return;

        function refreshReorderBar() {
            var n = $('.hb-bulk-check:checked').length;
            $('#hb-reorder-bulk-count').text(n);
            if (n > 0) { $bar.addClass('is-visible').attr('aria-hidden', 'false'); }
            else        { $bar.removeClass('is-visible').attr('aria-hidden', 'true'); }
            var total = $('.hb-bulk-check').length;
            $('#hb-bulk-all').prop('checked', total > 0 && n === total);
        }

        function selectedIds() {
            var ids = [];
            $('.hb-bulk-check:checked').each(function() { ids.push($(this).data('id')); });
            return ids;
        }

        $(document).on('change', '.hb-bulk-check', refreshReorderBar);

        $(document).on('change', '#hb-bulk-all', function() {
            $('.hb-bulk-check').prop('checked', this.checked);
            refreshReorderBar();
        });

        $('#hb-reorder-bulk-cancel').on('click', function() {
            $('.hb-bulk-check, #hb-bulk-all').prop('checked', false);
            $('#hb-reorder-bulk-op').val('');
            refreshReorderBar();
        });

        $('#hb-reorder-bulk-apply').on('click', function() {
            var ids = selectedIds();
            var op  = $('#hb-reorder-bulk-op').val();
            if (!ids.length) { alert(__('No products selected.', 'quickstock-for-woocommerce')); return; }
            if (!op)         { alert(__('Please select an action.', 'quickstock-for-woocommerce')); return; }
            
            runReorderBulk(ids, op);
        });

        

        function runReorderBulk(ids, op) {
            var chunk = parseInt(QSFW.bulkChunk, 10) || 25;
            var blocks = [];
            for (var i = 0; i < ids.length; i += chunk) blocks.push(ids.slice(i, i + chunk));
            var skipped = [];

            $bar.find('button, select').prop('disabled', true);

            var idx = 0;
            function next() {
                if (idx >= blocks.length) { finish(); return; }
                $.post(QSFW.ajaxUrl, {
                    action: 'qsfw_bulk_apply',
                    nonce:  QSFW.nonce,
                    ids:    blocks[idx],
                    op:     op,
                    value:  '', value2: '', submode: '', opts: {}
                }).done(function(resp) {
                    if (resp && resp.success) {
                        applyRowState(blocks[idx], op);
                        if (resp.data.skipped && resp.data.skipped.length)
                            skipped = skipped.concat(resp.data.skipped);
                    } else {
                        skipped = skipped.concat(blocks[idx].map(function(id) {
                            return { id: id, reason: (resp && resp.data && resp.data.message) || __('Error', 'quickstock-for-woocommerce') };
                        }));
                    }
                }).fail(function() {
                    skipped = skipped.concat(blocks[idx].map(function(id) {
                        return { id: id, reason: __('Connection error', 'quickstock-for-woocommerce') };
                    }));
                }).always(function() { idx++; next(); });
            }

            function finish() {
                $bar.find('button, select').prop('disabled', false);
                $('.hb-bulk-check, #hb-bulk-all').prop('checked', false);
                $('#hb-reorder-bulk-op').val('');
                refreshReorderBar();
                if (skipped.length) {
                    /* translators: %d is the number of products that could not be updated */
                    alert(sprintfFn(_n('%d product could not be updated.', '%d products could not be updated.', skipped.length, 'quickstock-for-woocommerce'), skipped.length));
                }
            }

            next();
        }

        function applyRowState(ids, op) {
            var markDone = (op === 'check_set');
            ids.forEach(function(id) {
                var $row = $('.hb-bulk-check[data-id="' + id + '"]').closest('tr');
                if (!$row.length) return;
                $row.toggleClass('stock-done', markDone);
                $row.find('.stock-check').prop('checked', markDone);
            });
        }

        refreshReorderBar();
    })();

    

    

    /* =====================================================================
     * AUTO-SAVE MODUS (manual)
     * ===================================================================== */
    (function() {
        if ((QSFW.autosaveMode || 'auto') !== 'manual') return;

        var $dirtyBar = null;
        var dirtyInputs = []; // Array von jQuery-Objekten

        function initDirtyBar() {
            if ($dirtyBar) return;
            /* translators: %d is the number of unsaved inline edits in manual autosave mode */
            var dirtyCountText = sprintfFn(_n('%d unsaved change', '%d unsaved changes', 0, 'quickstock-for-woocommerce'), 0);
            $dirtyBar = $('<div id="qsfw-dirty-bar" style="display:none;">' +
                '<span id="qsfw-dirty-count">' + dirtyCountText + '</span>' +
                '<button type="button" id="qsfw-save-all-btn" class="button button-primary">💾 ' + __('Save all', 'quickstock-for-woocommerce') + '</button>' +
                '<button type="button" id="qsfw-discard-btn" class="button button-secondary">✕ ' + __('Discard', 'quickstock-for-woocommerce') + '</button>' +
                '</div>');
            $('body').append($dirtyBar);

            $('#qsfw-save-all-btn').on('click', function() {
                saveAllDirty();
            });
            $('#qsfw-discard-btn').on('click', function() {
                discardAllDirty();
            });
        }

        function updateDirtyBar() {
            if (!$dirtyBar) initDirtyBar();
            var n = dirtyInputs.length;
            if (n > 0) {
                $('#qsfw-dirty-count').text(sprintfFn(_n('%d unsaved change', '%d unsaved changes', n, 'quickstock-for-woocommerce'), n));
                $dirtyBar.css('display', 'flex');
            } else {
                $dirtyBar.css('display', 'none');
            }
        }

        function saveAllDirty() {
            var toSave = dirtyInputs.slice();
            dirtyInputs = [];
            updateDirtyBar();
            // Direkt speichern statt per trigger('qsfw:save'): die Live-Suche kann
            // den tbody zwischenzeitlich neu gerendert haben, wodurch bereits
            // bearbeitete Inputs aus dem DOM gelöst sind. Ein delegierter Handler
            // an document feuert für losgelöste Elemente NICHT (kein Bubbling),
            // sodass sonst nur das zuletzt gescannte Produkt gespeichert würde.
            toSave.forEach(function($t) {
                qsfwManualSave($t);
            });
        }

        function discardAllDirty() {
            dirtyInputs.forEach(function($t) {
                $t.removeClass('qsfw-dirty');
                if ($t.data('qsfw-original') !== undefined) {
                    if ($t.is(':checkbox')) $t.prop('checked', !!$t.data('qsfw-original'));
                    else $t.val($t.data('qsfw-original'));
                }
            });
            dirtyInputs = [];
            updateDirtyBar();
        }

        // Original-Wert merken beim ersten Focus/Touch
        $(document).on('focus', '.edit-input', function() {
            var $t = $(this);
            if ($t.data('qsfw-original') === undefined) {
                $t.data('qsfw-original', $t.is(':checkbox') ? $t.is(':checked') : $t.val());
            }
        });

        // Beim blur/change: als dirty markieren statt sofort speichern
        $(document).off('change', '.edit-input');
        $(document).off('blur', '.textarea-input');

        $(document).on('change', '.edit-input', function() {
            var $t = $(this);
            if ($t.hasClass('qsfw-dirty')) return; // already tracked
            $t.addClass('qsfw-dirty');
            dirtyInputs.push($t);
            updateDirtyBar();
        });
        $(document).on('blur', '.textarea-input', function() {
            var $t = $(this);
            if (!$t.hasClass('qsfw-dirty')) {
                $t.addClass('qsfw-dirty');
                dirtyInputs.push($t);
                updateDirtyBar();
            }
        });

        // Eigentlicher Speichervorgang. Wird direkt (saveAllDirty) ODER per
        // delegiertem qsfw:save-Event aufgerufen. Funktioniert auch für aus dem
        // DOM gelöste Inputs, da product_id/type aus den data-Attributen kommen.
        function qsfwManualSave($t) {
            var $spinner = $t.closest('.num-spinner');
            $t.removeClass('saving saved failed qsfw-dirty');
            $t.next('.save-indicator').remove();
            $spinner.next('.save-indicator').remove();

            if ($t.is('[data-calc]') && !$t.is(':checkbox')) {
                var raw = $.trim($t.val());
                if (raw !== '' && !/^-?\d*[.,]?\d+$/.test(raw)) {
                    var res = hbCalc(raw);
                    if (res === null) {
                        $t.addClass('failed');
                        $t.after('<span class="save-indicator fail" title="' + __('Invalid expression', 'quickstock-for-woocommerce') + '">✗</span>');
                        return;
                    }
                    var calc = $t.data('calc');
                    if (calc === 'int')        res = Math.round(res);
                    else if (calc === 'money') res = Math.round(res * 100) / 100;
                    else                       res = Math.round(res * 1e6) / 1e6;
                    $t.val(String(res));
                }
            }

            $t.addClass('saving');
            var sendVal = $t.is(':checkbox') ? ($t.is(':checked') ? '1' : '0') : $t.val();
            $.post(QSFW.ajaxUrl, {
                action: 'qsfw_update_value',
                nonce: QSFW.nonce,
                product_id: $t.data('id'),
                type: $t.data('type'),
                value: sendVal
            })
            .done(function(response) {
                $t.removeClass('saving');
                if (response && response.success) {
                    if (response.data && response.data.uncheck) {
                        var $row = $t.closest('tr');
                        $row.removeClass('stock-done');
                        $row.find('.stock-check').prop('checked', false);
                    }
                    $t.addClass('saved');
                    $t.removeData('qsfw-original');
                    var $ok = $('<span class="save-indicator ok">✓</span>');
                    ($spinner.length ? $spinner : $t).after($ok);
                    setTimeout(function() {
                        $t.removeClass('saved');
                        $ok.fadeOut(300, function(){ $(this).remove(); });
                    }, 1500);
                } else {
                    $t.addClass('failed');
                    ($spinner.length ? $spinner : $t).after('<span class="save-indicator fail" title="' + __('Error', 'quickstock-for-woocommerce') + '">✗</span>');
                }
            })
            .fail(function() {
                $t.removeClass('saving').addClass('failed');
                ($spinner.length ? $spinner : $t).after('<span class="save-indicator fail" title="' + __('Connection error', 'quickstock-for-woocommerce') + '">✗</span>');
            });
        }

        // Delegierter Trigger (analog zum alten change-Handler) – ruft nur die Funktion auf.
        $(document).on('qsfw:save', '.edit-input', function() {
            qsfwManualSave($(this));
        });

        // Vor dem Seitenverlassen warnen
        window.addEventListener('beforeunload', function(e) {
            if (dirtyInputs.length > 0) {
                e.preventDefault();
                e.returnValue = '';
            }
        });
    })();

    /* =====================================================================
     * TASTATUR-SHORTCUTS + CHEAT-SHEET OVERLAY
     * ===================================================================== */
    (function() {
        if (!QSFW.keyboardShortcuts) return;

        // Shortcut-Definitionen für das Cheat-Sheet (Lazy: Übersetzungen erst bei Aufruf)
        function getGlobalShortcuts() {
            return [
                { keys: ['/'],            desc: __('Focus the live search', 'quickstock-for-woocommerce') },
                { keys: ['s'],            desc: __('Save all changes (manual mode only)', 'quickstock-for-woocommerce') },
                { keys: ['1'],            desc: __('Tab “📦 All Products”', 'quickstock-for-woocommerce') },
                { keys: ['2'],            desc: __('Tab “📋 Reorder”', 'quickstock-for-woocommerce') },
                { keys: ['a'],            desc: __('Toggle all visible rows', 'quickstock-for-woocommerce') },
                { keys: ['?'],            desc: __('Show / hide this shortcut overview', 'quickstock-for-woocommerce') },
                { keys: ['Esc'],          desc: __('Close overlay / drawer, clear search', 'quickstock-for-woocommerce') }
            ];
        }
        function getFieldShortcuts() {
            return [
                { keys: ['↑', '↓'],       desc: __('Value ±1 (stock / threshold / price)', 'quickstock-for-woocommerce') },
                { keys: ['Shift', '↑/↓'], desc: __('Value ±10', 'quickstock-for-woocommerce') }
            ];
        }

        function renderRows(list) {
            var html = '';
            list.forEach(function(s) {
                var keys = s.keys.map(function(k) {
                    return '<kbd>' + $('<i>').text(k).html() + '</kbd>';
                }).join(' ');
                html += '<tr><td style="width:32%;">' + keys + '</td><td>' + $('<i>').text(s.desc).html() + '</td></tr>';
            });
            return html;
        }

        var $overlay = $(
            '<div id="qsfw-kbd-overlay" role="dialog" aria-modal="true" aria-label="' + __('Keyboard shortcuts', 'quickstock-for-woocommerce') + '" style="display:none;">' +
                '<div class="qsfw-kbd-box">' +
                    '<h2>⌨️ ' + __('Keyboard shortcuts', 'quickstock-for-woocommerce') + '</h2>' +
                    '<h3>' + __('Global', 'quickstock-for-woocommerce') + '</h3>' +
                    '<table><tbody>' + renderRows(getGlobalShortcuts()) + '</tbody></table>' +
                    '<h3>' + __('In stock / number fields', 'quickstock-for-woocommerce') + '</h3>' +
                    '<table><tbody>' + renderRows(getFieldShortcuts()) + '</tbody></table>' +
                    /* translators: %s is the keyboard shortcut key (e.g. "s") wrapped in a <kbd> element */
                    '<p class="qsfw-kbd-hint">' + sprintfFn(__('Global shortcuts only fire outside input fields. In manual inline edit mode, %s saves all flagged changes.', 'quickstock-for-woocommerce'), '<kbd>s</kbd>') + '</p>' +
                '</div>' +
            '</div>'
        );
        $('body').append($overlay);

        function openOverlay()  { $overlay.css('display', 'flex'); }
        function closeOverlay() { $overlay.css('display', 'none'); }
        $overlay.on('click', function(e) {
            if ($(e.target).is($overlay)) closeOverlay();
        });

        function isEditable(el) {
            if (!el) return false;
            var tag = (el.tagName || '').toLowerCase();
            return tag === 'input' || tag === 'textarea' || tag === 'select' || !!el.isContentEditable;
        }

        function navTabUrl(modeValue) {
            var $tab = $('.nav-tab').filter(function() {
                var href = $(this).attr('href') || '';
                return href.indexOf('mode=' + modeValue) !== -1;
            }).first();
            return $tab.length ? $tab.attr('href') : null;
        }

        // --- Pfeiltasten / Shift+Pfeiltasten in Bestand- & Schwellenwert-Feldern ---
        $(document).on('keydown', '.edit-input', function(e) {
            var $t = $(this);
            // Nur reine Zahlenfelder (data-calc="int" / "money" / "decimal")
            if (!$t.is('[data-calc]') || $t.is(':checkbox')) return;

            if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
            if (e.altKey || e.ctrlKey || e.metaKey) return;

            e.preventDefault();
            var step = e.shiftKey ? 10 : 1;
            var dir  = (e.key === 'ArrowUp') ? 1 : -1;

            var raw = $.trim($t.val());
            var num = parseFloat(raw.replace(',', '.'));
            if (isNaN(num)) num = 0;

            var calc = $t.data('calc');
            var next = num + (dir * step);
            if (calc === 'int')        next = Math.round(next);
            else if (calc === 'money') next = Math.round(next * 100) / 100;
            else                       next = Math.round(next * 1e6) / 1e6;

            $t.val(String(next));
            if (qsfwManualMode) $t.trigger('change');   // Manuell: als dirty markieren
            else                qsfwSaveDebounced($t, 500);
        });

        // --- Globale Shortcuts (nur außerhalb von Eingabefeldern) ---
        $(document).on('keydown', function(e) {
            if (!QSFW.keyboardShortcuts) return;

            // Esc schließt Overlay auch aus Inputs heraus
            if (e.key === 'Escape' && $overlay.css('display') !== 'none') {
                closeOverlay();
                return;
            }

            if (isEditable(e.target)) return;
            if (e.altKey || e.ctrlKey || e.metaKey) return;

            switch (e.key) {
                case '?':
                    e.preventDefault();
                    if ($overlay.css('display') === 'none') openOverlay();
                    else closeOverlay();
                    break;

                case '/':
                    var $search = $('#stock-search');
                    if ($search.length) {
                        e.preventDefault();
                        $search.trigger('focus').select();
                    }
                    break;

                case 's':
                case 'S':
                    var $saveBtn = $('#qsfw-save-all-btn:visible');
                    if ($saveBtn.length) {
                        e.preventDefault();
                        $saveBtn.trigger('click');
                    }
                    break;

                case '1':
                    var urlAll = navTabUrl('all');
                    if (urlAll) { e.preventDefault(); window.location.href = urlAll; }
                    break;

                case '2':
                    var urlReorder = navTabUrl('reorder');
                    if (urlReorder) { e.preventDefault(); window.location.href = urlReorder; }
                    break;

                case 'a':
                case 'A':
                    var $bulkAll = $('#hb-bulk-all');
                    if ($bulkAll.length) {
                        e.preventDefault();
                        $bulkAll.prop('checked', !$bulkAll.prop('checked')).trigger('change');
                    }
                    break;
            }
        });
    })();

    // --- COCKPIT COLLAPSE ---
    var COCKPIT_KEY = 'qsfw_cockpit_collapsed';
    var $cockpit = $('.hb-filter-container');
    var $cform   = $('#hb-filter-form');

    function cockpitCollapse(animate) {
        if (animate) {
            // Messschritt: explizite Höhe setzen, damit die Transition von einem
            // definierten Wert startet (scrollHeight = tatsächliche Inhaltshöhe).
            $cform.css('max-height', $cform[0].scrollHeight + 'px');
        }
        // overflow:hidden nur während/nach dem Collapse – nicht dauerhaft im CSS,
        // da es sonst absolut positionierte Dropdowns abschneidet.
        $cform.css('overflow', 'hidden');
        $cockpit.addClass('is-collapsed');
        if (animate) {
            requestAnimationFrame(function() {
                $cform.css({ 'max-height': '0', 'opacity': '0' });
            });
        } else {
            $cform.css({ 'max-height': '0', 'opacity': '0' });
        }
    }
    function cockpitExpand(animate) {
        var h = $cform[0].scrollHeight;
        $cockpit.removeClass('is-collapsed');
        if (animate) {
            $cform.css({ 'max-height': h + 'px', 'opacity': '1' });
            // Nach der Transition beide Constraints entfernen – overflow muss weg,
            // damit Dropdowns nicht mehr abgeschnitten werden.
            setTimeout(function() { $cform.css({ 'max-height': '', 'overflow': '' }); }, 380);
        } else {
            $cform.css({ 'max-height': '', 'opacity': '', 'overflow': '' });
        }
    }

    // Zustand beim Laden wiederherstellen (ohne sichtbare Animation)
    try {
        if (window.localStorage.getItem(COCKPIT_KEY) === '1') { cockpitCollapse(false); }
    } catch(e) {}

    $(document).on('click', '#qsfw-cockpit-toggle', function() {
        var isCollapsed = $cockpit.hasClass('is-collapsed');
        if (isCollapsed) { cockpitExpand(true); } else { cockpitCollapse(true); }
        try { window.localStorage.setItem(COCKPIT_KEY, isCollapsed ? '0' : '1'); } catch(e) {}
    });

    
});
