OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

orders.js

26 KB
// Deep-Link auf eine Bestellung (#order-N): Liegt die Karte in einem anderen
// als dem aktiven Tab (z. B. alter Ticket-Link auf "Offen", Bestellung ist
// inzwischen archiviert), zum richtigen Tab wechseln und hinscrollen.
document.addEventListener('DOMContentLoaded', function () {
    var m = /^#order-(\d+)$/.exec(window.location.hash);
    if (!m) { return; }
    var card = document.getElementById('order-' + m[1]);
    if (!card) { return; }
    var panel = card.closest('.tab-panel');
    if (panel && !panel.classList.contains('active')) {
        switchTab(panel.getAttribute('data-panel'));
    }
    card.scrollIntoView({ block: 'center' });
});

// Tab-Umschaltung (clientseitig im Browse-Modus; im Such-/Filtermodus ohne
// lokale Panels wird serverseitig zum Tab gewechselt).
function switchTab(tab) {
    var target = document.querySelector('.tab-panel[data-panel="' + tab + '"]');
    if (!target) {
        window.location.href = '/orders?tab=' + encodeURIComponent(tab);
        return;
    }
    document.querySelectorAll('.subnav-tab').forEach(function(btn) {
        btn.classList.toggle('active', btn.getAttribute('data-tab') === tab);
    });
    document.querySelectorAll('.tab-panel').forEach(function(panel) {
        panel.classList.toggle('active', panel.getAttribute('data-panel') === tab);
    });
    var hidden = document.getElementById('ordersTabInput');
    if (hidden) { hidden.value = tab; }
    var url = new URL(window.location);
    url.searchParams.set('tab', tab);
    history.replaceState(null, '', url);
}

// Doppel-Submit verhindern (z. B. „Neue Bestellung" zweimal angeklickt).
function guardSubmit(form) {
    if (form.dataset.submitting) { return false; }
    form.dataset.submitting = '1';
    setTimeout(function () {
        var btn = form.querySelector('[type=submit]');
        if (btn) { btn.disabled = true; }
    }, 0);
    return true;
}

// Kurze Bestätigungs-Meldung (überlebt einen Karten-Reload, da am Body).
function ordersToast(msg) {
    var t = document.createElement('div');
    t.textContent = msg;
    t.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);'
        + 'background:#16a34a;color:#fff;padding:.55rem 1.1rem;border-radius:6px;font-size:.85rem;'
        + 'z-index:3000;box-shadow:0 4px 16px rgba(0,0,0,.2);opacity:0;transition:opacity .2s;';
    document.body.appendChild(t);
    requestAnimationFrame(function () { t.style.opacity = '1'; });
    setTimeout(function () { t.style.opacity = '0'; setTimeout(function () { t.remove(); }, 250); }, 1800);
}

// ── Modals: einheitliches Muster (.modal-overlay/.modal-card) ──────
// Öffnen/Schließen via style.display, plus Escape + Backdrop-Klick +
// data-modal-close. Gilt für alle Order-Modals (Neue Bestellung,
// Abholbereit-Bestätigung), damit sie sich projektweit gleich verhalten.
function openModal(modal) {
    if (typeof modal === 'string') { modal = document.getElementById(modal); }
    if (!modal) { return; }
    modal.style.display = 'flex';
    document.body.style.overflow = 'hidden';
    var first = modal.querySelector('input:not([type=hidden]), select, textarea, button');
    if (first) { first.focus(); }
}
function closeModal(modal) {
    if (typeof modal === 'string') { modal = document.getElementById(modal); }
    if (!modal) { return; }
    modal.style.display = 'none';
    // Body-Scroll nur freigeben, wenn kein weiteres Modal offen ist.
    var stillOpen = Array.prototype.some.call(document.querySelectorAll('.modal-overlay'), function (m) {
        return m.style.display === 'flex';
    });
    if (!stillOpen) { document.body.style.overflow = ''; }
}
// Backdrop-Klick + Close-Buttons
document.addEventListener('click', function (e) {
    if (e.target.classList && e.target.classList.contains('modal-overlay')) {
        closeModal(e.target);
        return;
    }
    var closer = e.target.closest('[data-modal-close]');
    if (closer) {
        var m = closer.closest('.modal-overlay');
        if (m) { closeModal(m); }
    }
});
// Escape schließt das oberste offene Modal (person-search fängt Escape
// bei offenem Dropdown vorher ab -> dann bleibt das Modal offen).
document.addEventListener('keydown', function (e) {
    if (e.key !== 'Escape') { return; }
    var open = Array.prototype.filter.call(document.querySelectorAll('.modal-overlay'), function (m) {
        return m.style.display === 'flex';
    });
    if (open.length) { closeModal(open[open.length - 1]); }
});

function openNewOrderModal() {
    resetOrderCompRows();
    openModal('newOrderModal');
    var titleInput = document.querySelector('#newOrderModal input[name="title"]');
    if (titleInput) { titleInput.focus(); }
}
function closeNewOrderModal() { closeModal('newOrderModal'); }

// ── Komponente bearbeiten: Inline-Edit-Form ein-/ausblenden ────────
function toggleCompEdit(btn) {
    var item = btn.closest('.comp-item');
    var form = item ? item.nextElementSibling : btn.closest('.comp-edit-form');
    if (!form || !form.classList.contains('comp-edit-form')) { return; }
    var row = form.previousElementSibling;
    if (form.hasAttribute('hidden')) {
        form.removeAttribute('hidden');
        if (row) { row.style.display = 'none'; }
        var inp = form.querySelector('.comp-edit-name');
        if (inp) { inp.focus(); inp.select(); }
    } else {
        form.setAttribute('hidden', '');
        if (row) { row.style.display = ''; }
    }
}

// ── Kommentar bearbeiten: Inline-Edit-Form ein-/ausblenden ─────────
function toggleCommentEdit(btn) {
    var item = btn.closest('.comment-item');
    if (!item) { return; }
    var form = item.querySelector('.comment-edit-form');
    var body = item.querySelector('.comment-body');
    if (!form) { return; }
    form.removeAttribute('hidden');
    if (body) { body.style.display = 'none'; }
    var inp = form.querySelector('textarea');
    if (inp) { inp.focus(); inp.select(); }
}
function cancelCommentEdit(btn) {
    var item = btn.closest('.comment-item');
    if (!item) { return; }
    var form = item.querySelector('.comment-edit-form');
    var body = item.querySelector('.comment-body');
    if (form) { form.setAttribute('hidden', ''); }
    if (body) { body.style.display = ''; }
}

// Komponenten-Zeilen (Name + Menge) im Neue-Bestellung-Modal
function ocBuildCompRow() {
    var row = document.createElement('div');
    row.className = 'oc-row';
    row.innerHTML =
        '<input type="text" name="comp_name[]" class="form-control oc-name" placeholder="z. B. Monitor Dell 27" autocomplete="off">' +
        '<input type="number" name="comp_qty[]" class="form-control oc-qty" value="1" min="1" max="9999" title="Menge" aria-label="Menge">' +
        '<button type="button" class="oc-row-remove" onclick="removeOrderCompRow(this)" aria-label="Zeile entfernen" title="Zeile entfernen">&times;</button>';
    return row;
}
function addOrderCompRow() {
    var rows = document.getElementById('order-comp-rows');
    if (!rows) return;
    var row = ocBuildCompRow();
    rows.appendChild(row);
    var inp = row.querySelector('.oc-name');
    if (inp) inp.focus();
}
function removeOrderCompRow(btn) {
    var rows = document.getElementById('order-comp-rows');
    var row  = btn.closest('.oc-row');
    if (!rows || !row) return;
    if (rows.querySelectorAll('.oc-row').length > 1) {
        row.remove();
    } else {
        // Letzte Zeile nicht entfernen, nur leeren.
        var n = row.querySelector('.oc-name'); if (n) n.value = '';
        var q = row.querySelector('.oc-qty');  if (q) q.value = '1';
    }
}
function resetOrderCompRows() {
    var rows = document.getElementById('order-comp-rows');
    if (!rows) return;
    rows.innerHTML = '';
    rows.appendChild(ocBuildCompRow());
}
// Enter im Namensfeld legt eine neue Zeile an, statt das Formular abzuschicken.
document.addEventListener('keydown', function (e) {
    if (e.key !== 'Enter') return;
    var t = e.target;
    if (t && t.classList && t.classList.contains('oc-name')) {
        e.preventDefault();
        addOrderCompRow();
    }
});

// Backdrop-Klick und Escape werden zentral von openModal/closeModal behandelt.

// Lieferart: Textfeld ein-/ausblenden
function handleDeliveryMethod(select) {
    var noteInput = select.parentElement.querySelector('[name=delivery_note]');
    if (!noteInput) return;
    if (select.value === 'Abgeholt') {
        noteInput.style.display = 'block';
        noteInput.placeholder = 'Abgeholt von...';
        noteInput.required = true;
    } else if (select.value === 'Freitext') {
        noteInput.style.display = 'block';
        noteInput.placeholder = 'Notiz...';
        noteInput.required = true;
    } else {
        noteInput.style.display = 'none';
        noteInput.value = '';
        noteInput.required = false;
    }
}

// Verlauf togglen
function toggleHistory(orderId) {
    var el = document.getElementById('history-' + orderId);
    if (!el) return;
    el.style.display = (el.style.display === 'none' || el.style.display === '') ? 'block' : 'none';
}

// AJAX Komponenten-Toggle
function toggleComponentAjax(checkbox) {
    var compId    = checkbox.getAttribute('data-comp-id');
    var csrf      = checkbox.getAttribute('data-csrf');
    var compItem  = checkbox.closest('.comp-item');
    var orderCard = checkbox.closest('.order-card');

    checkbox.disabled = true;

    var formData = new FormData();
    formData.append('_csrf', csrf);

    fetch('/orders/components/' + compId + '/toggle', {
        method: 'POST',
        headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
        body: formData
    })
    .then(function(resp) { return resp.json(); })
    .then(function(data) {
        if (data.error) {
            alert(data.error);
            checkbox.checked = !checkbox.checked;
            checkbox.disabled = false;
            return;
        }

        checkbox.disabled = false;
        var comp   = data.component;
        var nameEl = compItem.querySelector('.comp-name');
        var metaEl = compItem.querySelector('.comp-meta');

        if (comp && parseInt(comp.is_done) === 1) {
            nameEl.classList.add('done');
            metaEl.innerHTML = '<div>' + (comp.done_by_name || '') + '</div>';
            if (comp.done_at) {
                var d = new Date(comp.done_at);
                metaEl.innerHTML += '<div>' + d.toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit'}) + ' ' + d.toLocaleTimeString('de-DE', {hour:'2-digit',minute:'2-digit'}) + '</div>';
            }
            var existingDel = compItem.querySelector('.comp-delete');
            if (!existingDel) {
                var delForm = document.createElement('form');
                delForm.method = 'POST';
                delForm.action = '/orders/components/' + compId;
                delForm.onsubmit = function() { return confirm(window.onitText('Eintrag löschen?')); };
                delForm.innerHTML = '<input type="hidden" name="_csrf" value="' + csrf + '">'
                    + '<input type="hidden" name="_method" value="DELETE">'
                    + '<button type="submit" class="comp-delete">&times;</button>';
                compItem.appendChild(delForm);
            }
        } else {
            nameEl.classList.remove('done');
            metaEl.innerHTML = '';
            var delForm = compItem.querySelector('form[action*="/orders/components/"]');
            if (delForm) delForm.remove();
        }

        if (orderCard) {
            var done  = data.order_done;
            var total = data.order_total;
            var pct   = total > 0 ? Math.floor(done * 100 / total) : 0;
            var progLabel = orderCard.querySelector('.progress-label');
            if (progLabel) progLabel.innerHTML = '<span>' + done + '/' + total + '</span><span>' + pct + '%</span>';
            var progFill = orderCard.querySelector('.progress-fill');
            if (progFill) {
                progFill.style.width = pct + '%';
                progFill.classList.toggle('complete', pct === 100);
            }
            var panel = orderCard.closest('.tab-panel');
            if (panel && data.order_status !== panel.getAttribute('data-panel') && data.order_status !== 'all') {
                window.location.reload();
            }
        }
    })
    .catch(function(err) {
        console.error('Toggle-Fehler:', err);
        checkbox.checked = !checkbox.checked;
        checkbox.disabled = false;
    });
}

// Empfänger- und Lieferanten-Suche laufen über die wiederverwendbaren
// Komponenten person-search.js und company-search.js (data-person-search bzw.
// data-company-search an den Feldern in _card.php / index.php).

// ============================================================
// Metadaten: ein gemeinsamer Speichern-Button + AJAX-Speichern
// (kein Seiten-Reload -> die geöffnete Bestellung bleibt offen)
// ============================================================
function clearSupplier(orderId) {
    document.getElementById('supplier-id-' + orderId).value = '0';
    var input = document.querySelector('[data-order-id="' + orderId + '"].supplier-search-input');
    if (input) input.value = '';
}
function clearRecipient(orderId) {
    document.getElementById('person-id-' + orderId).value = '0';
    var input = document.querySelector('[data-order-id="' + orderId + '"].person-search-input');
    if (input) input.value = '';
    var emailEl = document.getElementById('person-email-' + orderId);
    if (emailEl) { emailEl.value = ''; emailEl.readOnly = false; }
}

// E-Mail-Feld der Empfänger-Suche sperren, solange eine Verzeichnisperson mit
// hinterlegter E-Mail gewählt ist – serverseitig zählt dann ohnehin die
// Verzeichnis-Mail. Nur für Orders-Felder (.person-search-input).
function _orderRecipientEmailEl(inp) {
    if (!inp || !inp.classList || !inp.classList.contains('person-search-input')) { return null; }
    var id = inp.getAttribute('data-ps-fill-email');
    return id ? document.getElementById(id) : null;
}
document.addEventListener('person-search:select', function (e) {
    var emailEl = _orderRecipientEmailEl(e.target);
    if (emailEl) { emailEl.readOnly = !!(e.detail && e.detail.email); }
});
document.addEventListener('person-search:clear', function (e) {
    var emailEl = _orderRecipientEmailEl(e.target);
    if (emailEl) { emailEl.readOnly = false; }
});

document.addEventListener('submit', function(e) {
    var form = e.target.closest('form.meta-form');
    if (!form) return;
    e.preventDefault();
    var btn    = form.querySelector('.meta-save-btn');
    var status = form.querySelector('.meta-save-status');
    if (status) { status.textContent = ''; status.className = 'meta-save-status'; }
    if (btn) btn.disabled = true;

    fetch(form.action, {
        method: 'POST',
        body: new FormData(form),
        headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
    })
    .then(function(r) { return r.json().catch(function() { return {}; }).then(function(j) { return { ok: r.ok, j: j }; }); })
    .then(function(res) {
        if (res.ok && res.j && res.j.success) {
            // Karte neu laden -> Kopf (Titel/Priorität/Chips) und Verlauf aktualisieren.
            var card    = form.closest('.order-card');
            var orderId = card ? card.id.replace('order-', '') : null;
            ordersToast('Gespeichert');
            if (orderId) { reloadOrderCard(orderId); return; }
            if (btn) btn.disabled = false;
        } else {
            if (btn) btn.disabled = false;
            if (status) {
                status.textContent = (res.j && res.j.error) ? res.j.error : 'Fehler beim Speichern';
                status.className = 'meta-save-status meta-save-err';
            }
        }
    })
    .catch(function() {
        if (btn) btn.disabled = false;
        if (status) { status.textContent = 'Netzwerkfehler'; status.className = 'meta-save-status meta-save-err'; }
    });
});

// Fallback ohne JS-Fetch / Direktlinks: per #order-<id> verlinkte Bestellung
// öffnen und in den Blick scrollen (z.B. nach klassischem Redirect).
(function() {
    if (!/^#order-\d+$/.test(location.hash)) return;
    var el = document.querySelector(location.hash);
    if (el && el.tagName === 'DETAILS') {
        el.open = true;
        if (typeof loadLazyDetail === 'function') { loadLazyDetail(el); }
        el.scrollIntoView({ block: 'start' });
    }
})();

// ── Datei-Upload: Drag & Drop + Dateiname (idempotent, auch nach Neuladen) ──
function initOrderUploads() {
    document.querySelectorAll('.cs-upload-drop').forEach(function (label) {
        if (label.dataset.uploadBound) return;
        label.dataset.uploadBound = '1';
        var input = label.querySelector('input[type=file]');
        var name  = label.querySelector('.cs-upload-name');
        if (!input || !name) return;
        var placeholder = name.textContent;
        function showName() {
            var has = input.files && input.files[0];
            name.textContent = has ? input.files[0].name : placeholder;
            name.style.color = has ? '#374151' : '';
            name.style.fontWeight = has ? '500' : '';
        }
        input.addEventListener('change', showName);
        ['dragenter', 'dragover'].forEach(function (ev) {
            label.addEventListener(ev, function (e) { e.preventDefault(); label.classList.add('cs-upload-dragging'); });
        });
        ['dragleave', 'drop'].forEach(function (ev) {
            label.addEventListener(ev, function (e) { e.preventDefault(); label.classList.remove('cs-upload-dragging'); });
        });
        label.addEventListener('drop', function (e) {
            if (e.dataTransfer && e.dataTransfer.files.length) {
                input.files = e.dataTransfer.files;
                showName();
            }
        });
    });
}
initOrderUploads();

// ── Detail erst beim Aufklappen laden (Lazy) ────────────────────
// Die Liste rendert nur die Zusammenfassung; das Detail (Komponenten,
// Kommentare, Dateien, Verlauf, Aktionen) wird beim ersten Öffnen der
// Karte per AJAX vom /orders/{id}/card-Endpoint nachgeladen.
function loadLazyDetail(card) {
    var ph = card.querySelector('.order-detail--lazy');
    if (!ph || ph.dataset.loading) { return; }
    ph.dataset.loading = '1';
    fetch(ph.getAttribute('data-card-url'), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function (r) { return r.text(); })
        .then(function (html) {
            var tmp = document.createElement('div');
            tmp.innerHTML = html.trim();
            var detail = tmp.querySelector('.order-detail');
            if (detail) {
                ph.replaceWith(detail);
                initOrderUploads();
                if (window.PersonSearch) { window.PersonSearch.init(detail); }
                if (window.CompanySearch) { window.CompanySearch.init(detail); }
            } else {
                ph.dataset.loading = '';
            }
        })
        .catch(function () {
            ph.dataset.loading = '';
            ph.innerHTML = '<div class="empty-mini" style="padding:1.5rem;">Detail konnte nicht geladen werden.</div>';
        });
}
function initLazyDetails() {
    document.querySelectorAll('.order-card').forEach(function (card) {
        if (card.dataset.lazyBound) { return; }
        card.dataset.lazyBound = '1';
        // toggle deckt alle Öffnungswege ab (Klick, Tastatur, programmatisch).
        card.addEventListener('toggle', function () {
            if (card.open) { loadLazyDetail(card); }
        });
        // Auf Touch-Geräten schon beim Antippen vorab laden, damit das Detail
        // bereits da ist, wenn die Karte aufklappt (kein zweites Tippen nötig).
        var summary = card.querySelector('summary');
        if (summary) {
            summary.addEventListener('pointerdown', function () {
                if (!card.open) { loadLazyDetail(card); }
            });
        }
    });
}
initLazyDetails();

// ── Bestell-Karte ohne Seiten-Reload aktualisieren ──────────────
// Holt die frische Karte vom Server und ersetzt sie; offener Zustand bleibt.
function reloadOrderCard(orderId) {
    var card = document.getElementById('order-' + orderId);
    if (!card) { window.location.reload(); return; }
    var activePanel = document.querySelector('.tab-panel.active');
    var tab = activePanel ? activePanel.getAttribute('data-panel') : 'open';
    var wasOpen = card.hasAttribute('open');
    fetch('/orders/' + orderId + '/card?tab=' + encodeURIComponent(tab), {
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
    })
    .then(function (r) { return r.text(); })
    .then(function (html) {
        var tmp = document.createElement('div');
        tmp.innerHTML = html.trim();
        var fresh = tmp.querySelector('#order-' + orderId);
        if (!fresh) { window.location.reload(); return; }
        if (wasOpen) { fresh.setAttribute('open', ''); } else { fresh.removeAttribute('open'); }
        card.replaceWith(fresh);
        initOrderUploads();
        initLazyDetails();
        if (window.PersonSearch) { window.PersonSearch.init(fresh); }
        if (window.CompanySearch) { window.CompanySearch.init(fresh); }
    })
    .catch(function () { window.location.reload(); });
}

// ── Closed-Tab: weitere abgeschlossene Bestellungen nachladen ────
function loadMoreClosed(btn) {
    var offset = parseInt(btn.getAttribute('data-offset'), 10) || 0;
    var total  = parseInt(btn.getAttribute('data-total'), 10) || 0;
    btn.disabled = true;
    btn.textContent = 'Lädt…';
    // Aktive Sortierung/Filter aus der URL beim Nachladen beibehalten.
    var cur = new URLSearchParams(window.location.search);
    var qs  = 'offset=' + offset;
    if (cur.get('sort'))     { qs += '&sort=' + encodeURIComponent(cur.get('sort')); }
    if (cur.get('priority')) { qs += '&priority=' + encodeURIComponent(cur.get('priority')); }
    fetch('/orders/closed-more?' + qs, {
        headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
    })
    .then(function (r) { return r.json(); })
    .then(function (data) {
        var list = document.getElementById('closed-list');
        if (list && data.html) {
            var tmp = document.createElement('div');
            tmp.innerHTML = data.html.trim();
            while (tmp.firstChild) { list.appendChild(tmp.firstChild); }
            initOrderUploads();
            initLazyDetails();
            if (window.PersonSearch) { window.PersonSearch.init(list); }
            if (window.CompanySearch) { window.CompanySearch.init(list); }
        }
        if (data.hasMore) {
            btn.setAttribute('data-offset', data.nextOffset);
            btn.textContent = 'Weitere anzeigen (' + Math.max(0, total - data.nextOffset) + ')';
            btn.disabled = false;
        } else {
            var wrap = btn.closest('.closed-more-wrap');
            if (wrap) { wrap.remove(); }
        }
    })
    .catch(function () { btn.disabled = false; btn.textContent = 'Weitere anzeigen'; alert('Fehler beim Laden.'); });
}

// ── In-Place-Formulare (Komponenten, Kommentare, Dateien) per AJAX ──
// Statt Seiten-Reload wird nur die betroffene Bestell-Karte neu geladen.
document.addEventListener('submit', function (e) {
    var form = e.target.closest('form.js-card-form');
    if (!form) return;
    e.preventDefault();
    var confirmMsg = form.getAttribute('data-confirm');
    if (confirmMsg && !window.confirm(confirmMsg)) return;
    var card    = form.closest('.order-card');
    var orderId = card ? card.id.replace('order-', '') : null;
    var btn     = form.querySelector('[type=submit]');
    if (btn) btn.disabled = true;
    fetch(form.action, {
        method: 'POST',
        body: new FormData(form),
        headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
    })
    .then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok, j: j }; }); })
    .then(function (res) {
        if (res.ok && res.j && res.j.success) {
            if (orderId) { reloadOrderCard(orderId); } else { window.location.reload(); }
        } else {
            if (btn) btn.disabled = false;
            alert((res.j && res.j.error) ? res.j.error : 'Fehler beim Speichern.');
        }
    })
    .catch(function () { if (btn) btn.disabled = false; alert('Netzwerkfehler.'); });
});

// ── Bestellung weiterleiten (Modal) per AJAX ──
// Eigener Handler statt js-card-form: bei Erfolg erst das Modal schliessen
// (gibt body-scroll wieder frei), dann die Karte neu laden (zeigt den
// Weiterleiten-Eintrag im Verlauf).
document.addEventListener('submit', function (e) {
    var form = e.target.closest('form.js-order-forward');
    if (!form) return;
    e.preventDefault();
    var modal   = form.closest('.modal-overlay');
    var card    = form.closest('.order-card');
    var orderId = card ? card.id.replace('order-', '') : null;
    var btn     = form.querySelector('[type=submit]');
    if (btn) btn.disabled = true;
    fetch(form.action, {
        method: 'POST',
        body: new FormData(form),
        headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
    })
    .then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok, j: j }; }); })
    .then(function (res) {
        if (res.ok && res.j && res.j.success) {
            if (modal) { closeModal(modal); }
            if (orderId) { reloadOrderCard(orderId); }
        } else {
            if (btn) btn.disabled = false;
            alert((res.j && res.j.error) ? res.j.error : 'Weiterleitung fehlgeschlagen.');
        }
    })
    .catch(function () { if (btn) btn.disabled = false; alert('Netzwerkfehler.'); });
});

// ── Metadaten: Lese-Ansicht <-> Bearbeiten-Formular umschalten ──
document.addEventListener('click', function (e) {
    var tog = e.target.closest('.meta-edit-toggle');
    if (tog) {
        var p = tog.closest('.order-meta-panel');
        if (!p) return;
        var rd = p.querySelector('.meta-read'); if (rd) rd.setAttribute('hidden', '');
        var ed = p.querySelector('.meta-edit'); if (ed) ed.removeAttribute('hidden');
        tog.style.display = 'none';
        return;
    }
    var cnl = e.target.closest('.meta-edit-cancel');
    if (cnl) {
        var p2 = cnl.closest('.order-meta-panel');
        if (!p2) return;
        var ed2 = p2.querySelector('.meta-edit'); if (ed2) ed2.setAttribute('hidden', '');
        var rd2 = p2.querySelector('.meta-read'); if (rd2) rd2.removeAttribute('hidden');
        var t2 = p2.querySelector('.meta-edit-toggle'); if (t2) t2.style.display = '';
    }
});