OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

dashboard.js

42 KB
// Dashboard-Kern-JS (Widget-DnD, Touch-Sortierung, To-Dos, Konfig-Panels,
// Tastatur-Reordering, Lazy-Load der schweren Widget-Bodies).
// Aus templates/dashboard/index.php ausgelagert; PHP-Werte kommen über
// window.__DASHBOARD__ (inline gesetzt). Wird via <script defer> geladen.

// ============================================================
// Gemeinsame Helfer (CSRF / JSON-POST) – einmal statt pro IIFE
// ============================================================
var DASH_CSRF = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
function dashPost(url, data) {
    return fetch(url, {
        method: 'POST',
        credentials: 'same-origin',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': DASH_CSRF,
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify(data || {})
    });
}

// ============================================================
// Slide-over-Panels: Fokus-Management & Tastatur (Barrierefreiheit)
// aria-modal + Fokus-Trap + Escape-zum-Schließen + Fokus zurückgeben
// ============================================================
var _dashPanelState = null;

function dashOpenPanel(panelId, backdropId) {
    var panel    = document.getElementById(panelId);
    var backdrop = document.getElementById(backdropId);
    if (!panel || !backdrop) return;
    if (_dashPanelState) dashClosePanel();

    var returnTo = document.activeElement;
    backdrop.classList.add('open');
    panel.classList.add('open');
    panel.setAttribute('aria-hidden', 'false');

    function focusables() {
        return Array.prototype.filter.call(
            panel.querySelectorAll('button:not([disabled]), a[href], input:not([disabled]), [tabindex]:not([tabindex="-1"])'),
            function(el) { return el.offsetParent !== null; }
        );
    }
    function onKeydown(e) {
        if (e.key === 'Escape') { e.preventDefault(); dashClosePanel(); return; }
        if (e.key !== 'Tab') return;
        var f = focusables();
        if (!f.length) return;
        var first = f[0], last = f[f.length - 1];
        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
    }
    panel.addEventListener('keydown', onKeydown);

    var f = focusables();
    if (f.length) f[0].focus();

    _dashPanelState = {
        panel: panel, backdrop: backdrop, returnTo: returnTo,
        cleanup: function() { panel.removeEventListener('keydown', onKeydown); }
    };
}

function dashClosePanel() {
    if (!_dashPanelState) return;
    var s = _dashPanelState;
    _dashPanelState = null;
    s.cleanup();
    s.backdrop.classList.remove('open');
    s.panel.classList.remove('open');
    s.panel.setAttribute('aria-hidden', 'true');
    if (s.returnTo && typeof s.returnTo.focus === 'function') s.returnTo.focus();
}

// ============================================================
// Widget Drag & Drop (spaltenübergreifend)
// ============================================================
(function() {
    var dragEl  = null;
    var dragCol = null;

    function cols() { return Array.from(document.querySelectorAll('.dashboard-col')); }

    function syncEmptyState() {
        cols().forEach(function(col) {
            col.classList.toggle('is-empty', col.querySelectorAll('.dashboard-card').length === 0);
        });
    }

    function initWidgetDrag() {
        syncEmptyState();
        document.querySelectorAll('.dashboard-card').forEach(function(card) {
            var handle = card.querySelector('.widget-drag-handle');
            if (!handle) return;
            handle.setAttribute('draggable', 'true');
            card.setAttribute('draggable', 'false');

            // Barrierefreiheit: Griff per Tastatur fokussier- und bedienbar.
            // Maus nutzt DnD, Touch den Touch-Helper, Tastatur die Pfeiltasten.
            if (!handle.hasAttribute('data-kbd')) {
                handle.setAttribute('data-kbd', '1');
                handle.setAttribute('tabindex', '0');
                handle.setAttribute('role', 'button');
                handle.setAttribute('aria-label', window.onitText('Widget verschieben – Pfeiltasten zum Umordnen, Pfeil links/rechts für Spalte'));
                handle.addEventListener('keydown', function(e) {
                    var curCol = card.closest('.dashboard-col');
                    if (!curCol) return;
                    var moved = false;
                    if (e.key === 'ArrowUp') {
                        var prev = card.previousElementSibling;
                        if (prev) { curCol.insertBefore(card, prev); moved = true; }
                    } else if (e.key === 'ArrowDown') {
                        var next = card.nextElementSibling;
                        if (next) { curCol.insertBefore(next, card); moved = true; }
                    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
                        var visible = cols().slice(0, (typeof _dashCols !== 'undefined' ? _dashCols : 3));
                        var idx = visible.indexOf(curCol);
                        var target = e.key === 'ArrowLeft' ? visible[idx - 1] : visible[idx + 1];
                        if (target) { target.appendChild(card); moved = true; }
                    }
                    if (moved) {
                        e.preventDefault();
                        syncEmptyState();
                        saveWidgetOrderFromDOM();
                        handle.focus();
                    }
                });
            }

            handle.addEventListener('dragstart', function(e) {
                dragEl  = card;
                dragCol = card.closest('.dashboard-col');
                card.classList.add('widget-dragging');
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', card.dataset.widget || '');
            });
            handle.addEventListener('dragend', function() {
                card.classList.remove('widget-dragging');
                document.querySelectorAll('.widget-drag-over,.col-drag-over').forEach(function(el) {
                    el.classList.remove('widget-drag-over', 'col-drag-over');
                });
                dragEl  = null;
                dragCol = null;
                syncEmptyState();
                saveWidgetOrderFromDOM();
            });

            // Auf anderen Karten einordnen
            card.addEventListener('dragover', function(e) {
                e.preventDefault(); e.stopPropagation();
                if (dragEl && dragEl !== card) card.classList.add('widget-drag-over');
            });
            card.addEventListener('dragleave', function() { card.classList.remove('widget-drag-over'); });
            card.addEventListener('drop', function(e) {
                e.preventDefault(); e.stopPropagation();
                card.classList.remove('widget-drag-over');
                if (!dragEl || dragEl === card) return;
                var col    = card.closest('.dashboard-col');
                var cards  = Array.from(col.querySelectorAll('.dashboard-card'));
                var toIdx  = cards.indexOf(card);
                var fromIdx = (dragCol === col) ? cards.indexOf(dragEl) : -1;
                if (fromIdx >= 0 && fromIdx < toIdx) {
                    col.insertBefore(dragEl, card.nextElementSibling);
                } else {
                    col.insertBefore(dragEl, card);
                }
            });
        });

        // Spalten als Drop-Ziel (für leere Spalten oder am Ende)
        cols().forEach(function(col) {
            col.addEventListener('dragover', function(e) {
                e.preventDefault();
                if (dragEl) col.classList.add('col-drag-over');
            });
            col.addEventListener('dragleave', function(e) {
                if (!col.contains(e.relatedTarget)) col.classList.remove('col-drag-over');
            });
            col.addEventListener('drop', function(e) {
                e.preventDefault();
                col.classList.remove('col-drag-over');
                // Nur wenn auf den Spalten-Hintergrund gedroppt (nicht auf eine Karte)
                if (dragEl && !e.target.closest('.dashboard-card')) {
                    col.appendChild(dragEl);
                }
            });
        });
    }

    window._initWidgetDrag   = initWidgetDrag;
    window._syncDashColEmpty = syncEmptyState; // für Touch-Drag wiederverwendet
    initWidgetDrag();
})();

// ============================================================
// Pin Drag & Drop (innerhalb des Pin-Grids)
// ============================================================
(function() {
    var grid = document.getElementById('pinGrid');
    if (!grid) return;

    var dragEl = null;

    // Aktuelle Reihenfolge der Pin-IDs aus dem DOM lesen (von Maus- und
    // Touch-Drag genutzt).
    function savePinOrder() {
        return Array.from(grid.querySelectorAll('.pin-tile')).map(function(t) { return parseInt(t.dataset.serviceId); });
    }
    window._savePinOrder = savePinOrder; // für Touch-Drop

    grid.querySelectorAll('.pin-tile').forEach(function(tile) {
        tile.addEventListener('dragstart', function(e) {
            dragEl = this;
            this.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
            e.dataTransfer.setData('text/plain', this.dataset.serviceId);
        });
        tile.addEventListener('dragend', function() {
            this.classList.remove('dragging');
            grid.querySelectorAll('.drag-over').forEach(function(el) { el.classList.remove('drag-over'); });
            dragEl = null;
        });
        tile.addEventListener('dragover', function(e) {
            e.preventDefault();
            e.stopPropagation();
            e.dataTransfer.dropEffect = 'move';
            if (this !== dragEl) { this.classList.add('drag-over'); }
        });
        tile.addEventListener('dragleave', function() { this.classList.remove('drag-over'); });
        tile.addEventListener('drop', function(e) {
            e.preventDefault();
            e.stopPropagation();
            this.classList.remove('drag-over');
            if (dragEl && dragEl !== this) {
                var allTiles = Array.from(grid.querySelectorAll('.pin-tile'));
                var fromIdx = allTiles.indexOf(dragEl);
                var toIdx   = allTiles.indexOf(this);
                if (fromIdx < toIdx) { grid.insertBefore(dragEl, this.nextSibling); }
                else                 { grid.insertBefore(dragEl, this); }

                dashPost('/services/pin-order', { service_ids: savePinOrder() });
            }
        });
        tile.addEventListener('click', function(e) { if (e.target.closest('.pin-drag-handle')) e.preventDefault(); });
    });
})();

// ============================================================
// Widget-Konfig-Panel
// ============================================================
function openWidgetConfig()  { dashOpenPanel('widgetConfigPanel', 'widgetConfigBackdrop'); }
function closeWidgetConfig() { dashClosePanel(); }

// Drag & Drop im Konfig-Panel
(function() {
    var list = document.getElementById('wcfgList');
    if (!list) return;
    var dragEl = null;

    list.querySelectorAll('.wcfg-item').forEach(function(item) {
        item.addEventListener('dragstart', function(e) {
            dragEl = this;
            this.classList.add('wcfg-dragging');
            e.dataTransfer.effectAllowed = 'move';
        });
        item.addEventListener('dragend', function() {
            this.classList.remove('wcfg-dragging');
            list.querySelectorAll('.wcfg-drag-over').forEach(function(el) { el.classList.remove('wcfg-drag-over'); });
            dragEl = null;
        });
        item.addEventListener('dragover', function(e) {
            e.preventDefault();
            if (dragEl && dragEl !== this) { this.classList.add('wcfg-drag-over'); }
        });
        item.addEventListener('dragleave', function() { this.classList.remove('wcfg-drag-over'); });
        item.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('wcfg-drag-over');
            if (dragEl && dragEl !== this) {
                var items = Array.from(list.querySelectorAll('.wcfg-item'));
                var fromIdx = items.indexOf(dragEl);
                var toIdx   = items.indexOf(this);
                if (fromIdx < toIdx) { list.insertBefore(dragEl, this.nextSibling); }
                else                 { list.insertBefore(dragEl, this); }
            }
        });
    });
})();

// Verfolgt ausgeblendete Widgets (für saveWidgetOrderFromDOM)
var _DASH = window.__DASHBOARD__ || {};
var _hiddenWidgets = Array.isArray(_DASH.hiddenWidgets) ? _DASH.hiddenWidgets : [];

function saveWidgetOrderFromDOM() {
    var widgets = [];
    [1, 2, 3].forEach(function(colNum) {
        var col = document.getElementById('dashCol' + colNum);
        if (!col) return;
        col.querySelectorAll('.dashboard-card').forEach(function(card) {
            if (card.dataset.widget) widgets.push('col' + colNum + ':' + card.dataset.widget);
        });
    });
    _hiddenWidgets.forEach(function(w) { widgets.push('hidden:' + w); });
    postWidgetConfig(widgets, _dashCols);
}

function toggleWidget(btn) {
    var item = btn.closest('.wcfg-item');
    var isVisible = item.classList.contains('visible');
    item.classList.toggle('visible', !isVisible);
    item.classList.toggle('hidden',   isVisible);
    btn.setAttribute('aria-pressed', isVisible ? 'false' : 'true');
    btn.querySelector('.wcfg-eye').innerHTML = isVisible ? '<i class="fa-solid fa-eye-slash"></i>' : '<i class="fa-solid fa-eye"></i>';
}

var _dashCols = parseInt(_DASH.dashCols, 10) || 2;

function setDashboardColumns(n) {
    _dashCols = n;
    document.querySelectorAll('#wcfgColBtns .wcfg-col-btn').forEach(function(btn) {
        var active = parseInt(btn.dataset.cols) === n;
        btn.style.background = active ? '#3c8dbc' : '#f9fafb';
        btn.style.color      = active ? '#fff'    : '#374151';
    });
    // Live-Vorschau: grid-template-columns aktualisieren
    var grid = document.getElementById('widgetGrid');
    if (grid) grid.style.gridTemplateColumns = 'repeat(' + n + ',1fr)';
}

function saveWidgetConfig() {
    var items = Array.from(document.querySelectorAll('#wcfgList .wcfg-item'));
    var widgets = items.map(function(i) {
        if (i.classList.contains('hidden')) return 'hidden:' + i.dataset.widget;
        return 'col' + (i.dataset.col || '1') + ':' + i.dataset.widget;
    });
    // _hiddenWidgets für nachfolgende Drag-Saves aktualisieren
    _hiddenWidgets = items
        .filter(function(i) { return i.classList.contains('hidden'); })
        .map(function(i) { return i.dataset.widget; });

    // Ein Reload ist nur nötig, wenn ein bisher verstecktes Widget sichtbar
    // werden soll, dessen Karte noch nicht im DOM ist (muss serverseitig
    // gerendert werden). Reine Hide-/Reorder-/Spalten-Änderungen wenden wir
    // ohne Reload direkt im DOM an.
    var needsReload = items.some(function(i) {
        return !i.classList.contains('hidden')
            && !document.querySelector('#widgetGrid .dashboard-card[data-widget="' + i.dataset.widget + '"]');
    });

    postWidgetConfig(widgets, _dashCols, function() {
        if (needsReload) { closeWidgetConfig(); location.reload(); return; }
        var grid = document.getElementById('widgetGrid');
        if (grid) grid.style.gridTemplateColumns = 'repeat(' + _dashCols + ',1fr)';
        items.forEach(function(i) {
            var card = document.querySelector('#widgetGrid .dashboard-card[data-widget="' + i.dataset.widget + '"]');
            if (!card) return;
            if (i.classList.contains('hidden')) { card.remove(); return; }
            var targetCol = document.getElementById('dashCol' + (i.dataset.col || '1'));
            if (targetCol) targetCol.appendChild(card); // Reihenfolge folgt der Panel-Reihenfolge
        });
        if (typeof window._syncDashColEmpty === 'function') window._syncDashColEmpty();
        closeWidgetConfig();
    });
}

// ============================================================
// Maintenance Filter Popover
// ============================================================
(function() {
var CSRF = DASH_CSRF;
var _panel = null;
var _btn   = null;

function getOrCreatePanel() {
    if (_panel) return _panel;
    // Panel aus der Card herauslösen und an body hängen (escaped overflow:hidden)
    var src = document.getElementById('mfPanel');
    if (!src) return null;
    src.style.removeProperty('display');
    src.style.position = 'fixed';
    src.style.zIndex   = '9999';
    document.body.appendChild(src);
    _panel = src;
    return _panel;
}

function closePanel() {
    var p = getOrCreatePanel();
    if (!p) return;
    p.style.display = 'none';
    if (_btn) _btn.classList.remove('open');
}

window.mfTogglePanel = function(btn) {
    _btn = btn;
    var p = getOrCreatePanel();
    if (!p) return;

    if (p.style.display !== 'none' && p.style.display !== '') {
        closePanel(); return;
    }

    // Position relativ zum Button berechnen
    var r = btn.getBoundingClientRect();
    p.style.top  = (r.bottom + 4) + 'px';
    p.style.left = r.left + 'px';
    p.style.display = 'block';
    btn.classList.add('open');
};

document.addEventListener('click', function(e) {
    if (!_panel || _panel.style.display === 'none') return;
    if (_btn && (_btn.contains(e.target) || _panel.contains(e.target))) return;
    closePanel();
});

// Panel schließen wenn Seite scrollt (verhindert Desync)
window.addEventListener('scroll', function() { closePanel(); }, true);

window.mfSave = function() {
    var sources = Array.from(document.querySelectorAll('.mf-cb:checked')).map(function(cb) { return cb.value; });
    dashPost('/dashboard/maintenance-filter', { sources: sources })
        .then(function(r) { return r.json(); })
        .then(function() {
            // Nur den Body neu laden statt voller Reload
            if (window.loadWidgetBody) window.loadWidgetBody('maintenance');
            else location.reload();
        });
};
})();

// ============================================================
// Lazy-Load der schweren Widget-Bodies
// Jeder Skeleton trägt data-lazy="<name>" und wird über
// /dashboard/widget/<name> nachgeladen. Danach läuft ein optionaler
// after-Hook (z. B. Header-Badge aktualisieren).
// ============================================================
(function() {
    var FAIL = '<div style="padding:1rem;text-align:center;color:#9ca3af;font-size:.82rem">Inhalt konnte nicht geladen werden.</div>';

    var AFTER = {
        maintenance: function(body) {
            var holder  = body.querySelector('[data-urgent]');
            var urgent  = holder ? (parseInt(holder.dataset.urgent, 10) || 0) : 0;
            var badge   = document.getElementById('mtUrgentBadge');
            var section = body.closest('.dashboard-card');
            if (urgent > 0) {
                if (badge) { badge.textContent = urgent + ' dringend'; badge.style.display = ''; }
                if (section) section.classList.add('attention');
            } else {
                if (badge) badge.style.display = 'none';
                if (section) section.classList.remove('attention');
            }
        }
    };

    window.loadWidgetBody = function(name) {
        var body = document.querySelector('[data-lazy="' + name + '"]');
        if (!body) return;
        fetch('/dashboard/widget/' + name, {
            headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/html' },
            credentials: 'same-origin'
        })
        .then(function(r) { return r.ok ? r.text() : null; })
        .then(function(html) {
            if (html == null) { body.innerHTML = FAIL; return; }
            body.innerHTML = html;
            if (AFTER[name]) AFTER[name](body);
        })
        .catch(function() { body.innerHTML = FAIL; });
    };

    // Beim Laden alle Lazy-Bodies anstoßen
    document.querySelectorAll('[data-lazy]').forEach(function(el) {
        window.loadWidgetBody(el.dataset.lazy);
    });
})();

// ============================================================
// To-Do-Widget (Dashboard)
// ============================================================
(function() {
var CSRF = DASH_CSRF;

function api(url, data) {
    return fetch(url, {
        method: 'POST',
        headers: { 'Content-Type':'application/json','X-CSRF-Token':CSRF,'X-Requested-With':'XMLHttpRequest' },
        body: JSON.stringify(data)
    }).then(function(r) { return r.json(); });
}

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

function syncEmpty() {
    var list  = document.getElementById('wTodoList');
    var empty = document.getElementById('wTodoEmpty');
    if (!list) return;
    var items = list.querySelectorAll('.wtd-item');
    if (empty) empty.style.display = items.length ? 'none' : '';
}

function dueBadgeHtml(iso) {
    if (!iso) return '';
    var today = new Date(); today.setHours(0,0,0,0);
    var due   = new Date(iso + 'T00:00:00');
    var diff  = Math.round((due - today) / 86400000);
    var cls   = diff < 0 ? 'wtd-due--overdue' : (diff === 0 ? 'wtd-due--today' : (diff === 1 ? 'wtd-due--soon' : ''));
    var pfx   = diff < 0 ? window.onitText('Überfällig') + ' · ' : (diff === 0 ? window.onitText('Heute') + ' · ' : (diff === 1 ? window.onitText('Morgen') + ' · ' : ''));
    var parts = iso.split('-');
    return '<span class="wtd-due-badge ' + cls + '">' + pfx + parts[2] + '.' + parts[1] + '.' + parts[0] + '</span>';
}

var calSvg  = '<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="11" rx="1.5"/><line x1="5" y1="1" x2="5" y2="5"/><line x1="11" y1="1" x2="11" y2="5"/><line x1="2" y1="7" x2="14" y2="7"/></svg>';
var pencSvg = '<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11.5 2.5a1.414 1.414 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"/></svg>';
var closSvg = '<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>';
var dragSvg = '&#8942;&#8942;';

function buildItem(id, title, notes, due) {
    notes = notes || ''; due = due || '';
    var li = document.createElement('li');
    var isOver = due && new Date(due + 'T00:00:00') < new Date(new Date().setHours(0,0,0,0));
    li.className = 'wtd-item' + (isOver ? ' wtd-item--overdue' : '');
    li.dataset.id  = id;
    li.dataset.due = due;
    li.draggable   = true;
    li.innerHTML =
        '<span class="wtd-drag" title="Ziehen">' + dragSvg + '</span>' +
        '<label class="wtd-check"><input type="checkbox" class="wtd-cb" onchange="wTodoDone(this)"><span class="wtd-cm"></span></label>' +
        '<div class="wtd-content">' +
            '<span class="wtd-title" ondblclick="wTodoStartEdit(this)">' + esc(title) + '</span>' +
            '<input type="text" class="wtd-edit-inp" style="display:none" onblur="wTodoSaveEdit(this)" onkeydown="wTodoEditKey(event,this)" maxlength="500">' +
            (due ? dueBadgeHtml(due) : '') +
            '<input type="date" class="wtd-due-inp" style="display:none" onblur="wTodoSaveDue(this)" onchange="wTodoSaveDue(this)" value="' + esc(due) + '">' +
            (notes ? '<span class="wtd-notes">' + esc(notes) + '</span>' : '') +
            '<textarea class="wtd-notes-inp" style="display:none" onblur="wTodoSaveNotes(this)" placeholder="Notiz…" maxlength="2000">' + esc(notes) + '</textarea>' +
        '</div>' +
        '<div class="wtd-actions">' +
            '<button class="wtd-act-btn' + (due ? ' active' : '') + '" onclick="wTodoToggleDue(this)" title="Fälligkeitsdatum">' + calSvg + '</button>' +
            '<button class="wtd-act-btn' + (notes ? ' active' : '') + '" onclick="wTodoToggleNotes(this)" title="Notiz">' + pencSvg + '</button>' +
            '<button class="wtd-act-btn wtd-del" onclick="wTodoDelete(this)" title="Löschen">' + closSvg + '</button>' +
        '</div>';
    initDrag(li);
    return li;
}

// ── Hinzufügen ──────────────────────────────────────────────
window.wTodoAdd = function(e) {
    e.preventDefault();
    var inp   = document.getElementById('wTodoInput');
    var dueIn = document.getElementById('wTodoDueIn');
    var title = inp.value.trim();
    if (!title) return;
    api('/todos', { title: title, due_date: (dueIn && dueIn.value) ? dueIn.value : null })
    .then(function(res) {
        if (res.error) { alert(res.error); return; }
        inp.value = ''; if (dueIn) dueIn.value = '';
        var list  = document.getElementById('wTodoList');
        var empty = document.getElementById('wTodoEmpty');
        if (empty) empty.remove();
        list.appendChild(buildItem(res.id, title, '', res.due_date || ''));
        syncEmpty();
    });
};

// ── Erledigt markieren ──────────────────────────────────────
window.wTodoDone = function(cb) {
    var li = cb.closest('.wtd-item');
    var id = li.dataset.id;
    api('/todos/' + id + '/done', {}).then(function(res) {
        if (!res.done) return;
        li.style.transition = 'opacity 0.2s';
        li.style.opacity = '0';
        setTimeout(function() { li.remove(); syncEmpty(); }, 200);
    });
};

// ── Titel inline editieren ──────────────────────────────────
window.wTodoStartEdit = function(span) {
    var li  = span.closest('.wtd-item');
    var inp = li.querySelector('.wtd-edit-inp');
    inp.value = span.textContent;
    span.style.display = 'none';
    inp.style.display  = '';
    inp.focus(); inp.select();
};
window.wTodoEditKey = function(e, inp) {
    if (e.key === 'Enter')  { e.preventDefault(); inp.blur(); }
    if (e.key === 'Escape') { inp.dataset.cancel = '1'; inp.blur(); }
};
window.wTodoSaveEdit = function(inp) {
    var li   = inp.closest('.wtd-item');
    var span = li.querySelector('.wtd-title');
    var restore = function() { inp.style.display = 'none'; span.style.display = ''; };

    if (inp.dataset.cancel === '1') {
        delete inp.dataset.cancel;
        restore();
        return;
    }
    var title = inp.value.trim();
    // Leer oder unverändert: nichts speichern, nur den Titel wiederherstellen.
    // (Sonst würde ein Blur ohne Änderung den Titel überschreiben.)
    if (!title || title === span.textContent) { restore(); return; }

    api('/todos/' + li.dataset.id, { title: title }).then(function(res) {
        if (res.error) { alert(res.error); restore(); return; }
        // Den lokal getrimmten Titel setzen – der Endpunkt liefert kein title-Feld.
        span.textContent = title;
        restore();
    });
};

// ── Fälligkeitsdatum ────────────────────────────────────────
window.wTodoToggleDue = function(btn) {
    var content = btn.closest('.wtd-item').querySelector('.wtd-content');
    var inp     = content.querySelector('.wtd-due-inp');
    var badge   = content.querySelector('.wtd-due-badge');
    var show    = inp.style.display === 'none' || inp.style.display === '';
    inp.style.display   = show ? 'block' : 'none';
    if (badge) badge.style.display = show ? 'none' : '';
    if (show) inp.focus();
};
window.wTodoSaveDue = function(inp) {
    var li  = inp.closest('.wtd-item');
    var id  = li.dataset.id;
    var due = inp.value || null;
    api('/todos/' + id, { due_date: due }).then(function(res) {
        if (res.error) { alert(res.error); return; }
        li.dataset.due = due || '';
        var content  = inp.closest('.wtd-content');
        var oldBadge = content.querySelector('.wtd-due-badge');
        var btn      = li.querySelector('.wtd-actions .wtd-act-btn');
        if (due) {
            var tmp = document.createElement('div');
            tmp.innerHTML = dueBadgeHtml(due);
            if (oldBadge) { oldBadge.replaceWith(tmp.firstChild); }
            else           { content.insertBefore(tmp.firstChild, inp); }
            if (btn) btn.classList.add('active');
            li.classList.toggle('wtd-item--overdue', new Date(due + 'T00:00:00') < new Date(new Date().setHours(0,0,0,0)));
        } else {
            if (oldBadge) oldBadge.remove();
            if (btn) btn.classList.remove('active');
            li.classList.remove('wtd-item--overdue');
        }
        inp.style.display = 'none';
        var nb = content.querySelector('.wtd-due-badge');
        if (nb) nb.style.display = '';
    });
};

// ── Notiz ───────────────────────────────────────────────────
window.wTodoToggleNotes = function(btn) {
    var content  = btn.closest('.wtd-item').querySelector('.wtd-content');
    var textarea = content.querySelector('.wtd-notes-inp');
    var noteSpan = content.querySelector('.wtd-notes');
    var show     = textarea.style.display === 'none' || textarea.style.display === '';
    textarea.style.display = show ? 'block' : 'none';
    if (noteSpan) noteSpan.style.display = show ? 'none' : '';
    if (show) textarea.focus();
};
window.wTodoSaveNotes = function(ta) {
    var li    = ta.closest('.wtd-item');
    var notes = ta.value.trim();
    api('/todos/' + li.dataset.id, { notes: notes }).then(function(res) {
        if (res.error) { alert(res.error); return; }
        var content  = ta.closest('.wtd-content');
        var noteSpan = content.querySelector('.wtd-notes');
        var pencBtn  = li.querySelectorAll('.wtd-actions .wtd-act-btn')[1];
        if (notes) {
            if (noteSpan) { noteSpan.textContent = notes; noteSpan.style.display = ''; }
            else {
                var sp = document.createElement('span');
                sp.className = 'wtd-notes'; sp.textContent = notes;
                content.insertBefore(sp, ta);
            }
            if (pencBtn) pencBtn.classList.add('active');
        } else {
            if (noteSpan) noteSpan.remove();
            if (pencBtn) pencBtn.classList.remove('active');
        }
        ta.style.display = 'none';
    });
};

// ── Löschen ─────────────────────────────────────────────────
window.wTodoDelete = function(btn) {
    var li = btn.closest('.wtd-item');
    if (!confirm(window.onitText('Aufgabe wirklich löschen?'))) return;
    api('/todos/' + li.dataset.id + '/delete', {}).then(function() {
        li.remove(); syncEmpty();
    });
};

// ── Drag & Drop Sortierung ──────────────────────────────────
var _wDragEl = null;
function initDrag(li) {
    li.addEventListener('dragstart', function(e) {
        _wDragEl = li; li.classList.add('wtd-dragging');
        e.dataTransfer.effectAllowed = 'move';
    });
    li.addEventListener('dragend', function() {
        li.classList.remove('wtd-dragging');
        document.querySelectorAll('.wtd-drag-over').forEach(function(el) { el.classList.remove('wtd-drag-over'); });
        _wDragEl = null;
        var ids = Array.from(document.querySelectorAll('#wTodoList .wtd-item')).map(function(el) { return parseInt(el.dataset.id); });
        api('/todos/reorder', { ids: ids });
    });
    li.addEventListener('dragover',  function(e) { e.preventDefault(); if (_wDragEl && _wDragEl !== li) li.classList.add('wtd-drag-over'); });
    li.addEventListener('dragleave', function()  { li.classList.remove('wtd-drag-over'); });
    li.addEventListener('drop', function(e) {
        e.preventDefault(); li.classList.remove('wtd-drag-over');
        if (!_wDragEl || _wDragEl === li) return;
        var items = Array.from(li.parentNode.querySelectorAll('.wtd-item'));
        if (items.indexOf(_wDragEl) < items.indexOf(li)) li.parentNode.insertBefore(_wDragEl, li.nextSibling);
        else                                              li.parentNode.insertBefore(_wDragEl, li);
    });
}
document.querySelectorAll('#wTodoList .wtd-item').forEach(initDrag);
syncEmpty();

})();

function postWidgetConfig(widgets, columns, cb) {
    var csrf = DASH_CSRF;
    fetch('/dashboard/widget-config', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrf,
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({ widgets: widgets, columns: columns })
    }).then(function(r) { if (r.ok && cb) cb(); });
}

// ============================================================
// Schnellzugriff-Konfig-Panel
// ============================================================
function openQAConfig()  { dashOpenPanel('qaConfigPanel', 'qaConfigBackdrop'); }
function closeQAConfig() { dashClosePanel(); }
function toggleQAItem(btn) {
    var item = btn.closest('.wcfg-item');
    var eye  = btn.querySelector('.wcfg-eye');
    if (item.classList.contains('hidden')) {
        item.classList.remove('hidden');  item.classList.add('visible');
        eye.innerHTML = '<i class="fa-solid fa-eye"></i>';
        btn.setAttribute('aria-pressed', 'true');
    } else {
        item.classList.remove('visible'); item.classList.add('hidden');
        eye.innerHTML = '<i class="fa-solid fa-eye-slash"></i>';
        btn.setAttribute('aria-pressed', 'false');
    }
}
var _qaCols = parseInt(_DASH.qaCols, 10) || 2;
function setQACols(n) {
    _qaCols = n;
    document.querySelectorAll('#qaColBtns button').forEach(function(btn) {
        var active = parseInt(btn.dataset.cols) === n;
        btn.style.background = active ? '#3c8dbc' : '#f9fafb';
        btn.style.color      = active ? '#fff'    : '#374151';
    });
    var grid = document.getElementById('qaGrid');
    if (grid) grid.style.gridTemplateColumns = 'repeat(' + n + ',1fr)';
}
function saveQAConfig() {
    var active = Array.from(document.querySelectorAll('#qaList .wcfg-item:not(.hidden)'));
    var links  = active.map(function(item) { return item.dataset.qakey; });
    var grid   = document.getElementById('qaGrid');

    // Reload nur, wenn eine neu aktivierte Kachel noch keine Grid-Kachel hat
    // (URL/Icon müssen serverseitig kommen). Reorder/Entfernen geht in-place.
    var needsReload = !grid || active.some(function(item) {
        return !grid.querySelector('.quick-action-card[data-qakey="' + item.dataset.qakey + '"]');
    });

    var csrf = DASH_CSRF;
    fetch('/dashboard/save-quickaccess', {
        method: 'POST',
        headers: {'Content-Type':'application/json','X-CSRF-Token':csrf,'X-Requested-With':'XMLHttpRequest'},
        body: JSON.stringify({links: links, cols: _qaCols})
    }).then(function(r) {
        if (!r.ok) return;
        if (needsReload) { closeQAConfig(); location.reload(); return; }
        grid.style.gridTemplateColumns = 'repeat(' + _qaCols + ',1fr)';
        // Deaktivierte Kacheln entfernen
        Array.from(grid.querySelectorAll('.quick-action-card')).forEach(function(tile) {
            if (links.indexOf(tile.dataset.qakey) === -1) tile.remove();
        });
        // Verbleibende Kacheln in Panel-Reihenfolge anordnen
        active.forEach(function(item) {
            var tile = grid.querySelector('.quick-action-card[data-qakey="' + item.dataset.qakey + '"]');
            if (tile) grid.appendChild(tile);
        });
        closeQAConfig();
    });
}
(function() {
    var list = document.getElementById('qaList');
    if (!list) return;
    var dragEl = null;
    list.querySelectorAll('.wcfg-item').forEach(function(item) {
        item.addEventListener('dragstart', function(e) {
            dragEl = this; this.classList.add('wcfg-dragging'); e.dataTransfer.effectAllowed = 'move';
        });
        item.addEventListener('dragend', function() {
            this.classList.remove('wcfg-dragging');
            list.querySelectorAll('.wcfg-drag-over').forEach(function(el) { el.classList.remove('wcfg-drag-over'); });
            dragEl = null;
        });
        item.addEventListener('dragover', function(e) {
            e.preventDefault();
            if (dragEl && dragEl !== this) this.classList.add('wcfg-drag-over');
        });
        item.addEventListener('dragleave', function() { this.classList.remove('wcfg-drag-over'); });
        item.addEventListener('drop', function(e) {
            e.preventDefault(); this.classList.remove('wcfg-drag-over');
            if (dragEl && dragEl !== this) {
                var items = Array.from(list.querySelectorAll('.wcfg-item'));
                var from = items.indexOf(dragEl), to = items.indexOf(this);
                if (from < to) list.insertBefore(dragEl, this.nextSibling);
                else           list.insertBefore(dragEl, this);
            }
        });
    });
})();

// ============================================================
// Touch-Unterstützung für Drag & Drop-Sortierung
// HTML5-DnD feuert auf Touchscreens nicht – die Maus nutzt weiterhin
// die nativen dragstart/drop-Handler, Touch verwendet diesen Fallback.
// Ein gemeinsamer Helper bedient Widget-Grid, To-Do-Liste und beide
// Konfig-Panels (jeweils über den Container-Selektor abgegrenzt).
// ============================================================
(function() {
    var CSRF = DASH_CSRF;

    function enableTouchSort(opts) {
        // opts: { handle, item, container, sibling, draggingClass, onDrop? }
        var item = null, clone = null, lastContainer = null, grabX = 0, grabY = 0;

        function moveClone(touch, rect) {
            if (rect) { grabX = touch.clientX - rect.left; grabY = touch.clientY - rect.top; }
            clone.style.left = (touch.clientX - grabX) + 'px';
            clone.style.top  = (touch.clientY - grabY) + 'px';
        }

        document.addEventListener('touchstart', function(e) {
            var h = e.target.closest(opts.handle);
            if (!h) return;
            var it = h.closest(opts.item);
            if (!it || !it.closest(opts.container)) return; // gehört zu einem anderen Sortable
            item = it;
            var r = item.getBoundingClientRect();
            clone = item.cloneNode(true);
            clone.removeAttribute('id');
            clone.querySelectorAll('[id]').forEach(function(el) { el.removeAttribute('id'); });
            clone.style.cssText = 'position:fixed;z-index:99999;pointer-events:none;margin:0;left:' +
                r.left + 'px;top:' + r.top + 'px;width:' + r.width + 'px;opacity:0.9;box-shadow:0 8px 24px rgba(0,0,0,.25);';
            document.body.appendChild(clone);
            item.classList.add(opts.draggingClass);
            moveClone(e.touches[0], r);
            e.preventDefault();
        }, { passive: false });

        document.addEventListener('touchmove', function(e) {
            if (!item) return;
            e.preventDefault();
            var t = e.touches[0];
            moveClone(t);
            if (t.clientY < 80) window.scrollBy(0, -14);
            else if (t.clientY > window.innerHeight - 80) window.scrollBy(0, 14);

            clone.style.display = 'none';
            var under = document.elementFromPoint(t.clientX, t.clientY);
            clone.style.display = '';
            if (!under) return;
            var container = under.closest(opts.container);
            if (!container) return;
            lastContainer = container;
            var over = under.closest(opts.sibling);
            if (over && over !== item && container.contains(over)) {
                var rr = over.getBoundingClientRect();
                var after = (t.clientY - rr.top) > rr.height / 2;
                container.insertBefore(item, after ? over.nextSibling : over);
            } else if (!over) {
                container.appendChild(item); // leerer Bereich → ans Ende
            }
        }, { passive: false });

        function end() {
            if (!item) return;
            if (clone) { clone.remove(); clone = null; }
            item.classList.remove(opts.draggingClass);
            var dropped = item;
            item = null;
            if (opts.onDrop) opts.onDrop(dropped, lastContainer);
            lastContainer = null;
        }
        document.addEventListener('touchend', end);
        document.addEventListener('touchcancel', end);
    }

    // 1) Widget-Grid (spaltenübergreifend)
    enableTouchSort({
        handle: '.widget-drag-handle', item: '.dashboard-card',
        container: '.dashboard-col', sibling: '.dashboard-card',
        draggingClass: 'widget-touch-dragging',
        onDrop: function() {
            if (typeof window._syncDashColEmpty === 'function') window._syncDashColEmpty();
            if (typeof saveWidgetOrderFromDOM === 'function') saveWidgetOrderFromDOM();
        }
    });

    // 2) To-Do-Liste
    enableTouchSort({
        handle: '.wtd-drag', item: '.wtd-item',
        container: '#wTodoList', sibling: '.wtd-item',
        draggingClass: 'wtd-dragging',
        onDrop: function() {
            var ids = Array.from(document.querySelectorAll('#wTodoList .wtd-item'))
                .map(function(el) { return parseInt(el.dataset.id); });
            fetch('/todos/reorder', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF, 'X-Requested-With': 'XMLHttpRequest' },
                body: JSON.stringify({ ids: ids })
            });
        }
    });

    // 3) Widget-Konfig-Panel  4) Schnellzugriff-Panel
    // (Reihenfolge wird erst beim Klick auf "Speichern" aus dem DOM gelesen)
    enableTouchSort({ handle: '.wcfg-handle', item: '.wcfg-item', container: '#wcfgList', sibling: '.wcfg-item', draggingClass: 'wcfg-dragging' });
    enableTouchSort({ handle: '.wcfg-handle', item: '.wcfg-item', container: '#qaList',   sibling: '.wcfg-item', draggingClass: 'wcfg-dragging' });

    // 5) Angepinnte Services (Pin-Grid)
    enableTouchSort({
        handle: '.pin-drag-handle', item: '.pin-tile',
        container: '#pinGrid', sibling: '.pin-tile',
        draggingClass: 'dragging',
        onDrop: function() {
            if (window._savePinOrder) dashPost('/services/pin-order', { service_ids: window._savePinOrder() });
        }
    });
})();