Quelltext des aktuellen Standes
// 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
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 = '⋮⋮';
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() });
}
});
})();