import { apiAction, apiUpdate, toast } from './api.js'; function esc(s = '') { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatVersionDate(value) { if (!value) return ''; try { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString('de-DE'); } catch { return value; } } function normalizeApiName(v = '') { return String(v) .trim() .toLowerCase() .replace(/\s+/g, '-') .replace(/[^a-z0-9_-]+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } async function fetchContentList(sectionId) { const res = await apiAction('content.list', { method: 'GET', data: { section_id: sectionId } }); return Array.isArray(res?.items) ? res.items : []; } async function fetchContentItem(id, sectionId) { return await apiAction('content.get', { method: 'GET', data: { id, section_id: sectionId } }); } async function openContentEditor(item, section) { const versionId = Number(item?.version_id || 0); const id = Number(item?.id || 0); const name = item?.name || ''; if (!id) return; const detail = await fetchContentItem(id, section.id).catch(() => ({})); const html = detail?.html ?? detail?.item?.html ?? detail?.content ?? ''; window.__currentItemId = id; window.__currentEditorCtx = { id, mode: section.slug, section }; if (window.EditorUI && typeof window.EditorUI.open === 'function') { window.EditorUI.open({ id, name, html, section, version_id: versionId }, 'content'); } else if (window.__openEditor) { window.__openEditor({ resource: 'content', id, name, html, section, version_id: versionId }); } else { toast('Editor ist nicht initialisiert.', false); } } async function openTemplateManager(item, section) { const dlg = document.getElementById('manageTemplateDialog'); const inpName = document.getElementById('manage_tpl_name'); const inpApiName = document.getElementById('manage_tpl_api_name'); const apiWrap = document.getElementById('manage_tpl_api_wrap'); const apiWarn = document.getElementById('manage_tpl_api_warn'); const badge = document.getElementById('manage_tpl_badge'); const versionsWrap = document.getElementById('manage_tpl_versions'); const btnClose = document.getElementById('manageTemplateClose'); const btnDelete = document.getElementById('manageTemplateDelete'); const deleteHint = document.getElementById('manage_tpl_delete_hint'); const delDlg = document.getElementById('deleteDialog'); const delText = document.getElementById('deleteText'); const delForm = document.getElementById('deleteForm'); const delCancel = document.getElementById('deleteCancel'); const detail = await fetchContentItem(item.id, section.id).catch(() => ({})); const row = detail?.item || detail?.data || detail || {}; const initialApi = row.api_name || ''; if (badge) badge.textContent = `ID ${item.id}`; if (inpName) inpName.value = row.name || ''; if (inpApiName) inpApiName.value = initialApi; if (apiWarn) apiWarn.classList.add('hidden'); if (apiWrap) apiWrap.classList.toggle('hidden', !section?.is_template); let versions = []; let activeId = 0; const fetchTemplateReferences = async () => { if (!section?.is_template) return null; const res = await apiAction('templates.references', { method: 'GET', data: { template_id: item.id } }).catch(() => null); if (!res || res.ok === false) return null; return Array.isArray(res?.references) ? res.references : []; }; const confirmTemplateReferences = async (actionLabel) => { if (!section?.is_template) return true; const refs = await fetchTemplateReferences(); if (refs === null) { return confirm(`Referenzen konnten nicht geprüft werden. ${actionLabel} trotzdem?`); } if (!refs.length) return true; const preview = refs.slice(0, 6).map(r => `${r.name || 'Template'} #${r.id}`).join(', '); const more = refs.length > 6 ? ` und ${refs.length - 6} weitere` : ''; return confirm(`Dieses Template wird in ${refs.length} anderen Template(s) verwendet (${preview}${more}). ${actionLabel} trotzdem?`); }; const updateDeleteState = () => { const hasActive = !!activeId; if (btnDelete) btnDelete.disabled = hasActive; if (deleteHint) { deleteHint.textContent = hasActive ? 'Aktive Version vorhanden – Löschen deaktiviert.' : 'Nur möglich, wenn keine aktive Version existiert.'; } }; const loadVersions = async () => { const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: item.id } }).catch(() => ({})); versions = Array.isArray(res?.items) ? res.items : []; activeId = 0; versions.forEach(v => { if (Number(v.is_active) === 1) activeId = Number(v.id || 0); }); renderVersions(); updateDeleteState(); }; const renderVersions = () => { if (!versionsWrap) return; if (!versions.length) { versionsWrap.innerHTML = '
Keine Versionen vorhanden
'; return; } versionsWrap.innerHTML = versions.map(v => { const isActive = Number(v.is_active) === 1; const label = `${isActive ? '✓ ' : ''}#${v.version_no} – ${formatVersionDate(v.created_at)}` + (isActive ? ' (aktiv)' : ''); const deleteBtn = isActive ? '' : ``; return `
${label}
${isActive ? `` : ``} ${deleteBtn}
`; }).join(''); }; const onApiInput = () => { if (!inpApiName) return; const next = normalizeApiName(inpApiName.value); if (next !== inpApiName.value) inpApiName.value = next; if (apiWarn) { apiWarn.classList.toggle('hidden', inpApiName.value.trim() === initialApi); } }; let autoSaveTimer = null; const autoSave = async () => { try { const payload = { name: inpName ? inpName.value : '', section_id: section.id, }; if (section?.is_template) { payload.api_name = inpApiName ? inpApiName.value : ''; } const res = await apiUpdate('content', item.id, payload); if (res?.ok && typeof window.loadList === 'function') window.loadList(section); if (!res?.ok) toast(res?.error || 'Speichern fehlgeschlagen', false); } catch { toast('Speichern fehlgeschlagen', false); } }; const scheduleAutoSave = () => { if (autoSaveTimer) clearTimeout(autoSaveTimer); autoSaveTimer = setTimeout(autoSave, 450); }; const cleanup = () => { inpApiName && inpApiName.removeEventListener('input', onApiInput); inpName && inpName.removeEventListener('input', scheduleAutoSave); inpApiName && inpApiName.removeEventListener('input', scheduleAutoSave); versionsWrap && versionsWrap.removeEventListener('click', onVersionsClick); if (btnClose) btnClose.onclick = null; if (btnDelete) btnDelete.onclick = null; if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; } }; const onDeleteItem = async () => { if (activeId) return; if (section?.is_template) { const ok = await confirmTemplateReferences('Löschen'); if (!ok) return; } if (!delDlg || !delForm) { const res = await apiAction('content.delete', { method: 'POST', data: { id: item.id, section_id: section.id } }); toast(res && res.ok ? 'Gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok), { duration: 3000 }); dlg && dlg.close(); if (typeof window.loadList === 'function') window.loadList(section); return; } if (delText) { delText.innerHTML = `Soll ${item.name || '(ohne Name)'} #${item.id} wirklich gelöscht werden?`; } const cleanupDelete = () => { delForm.onsubmit = null; if (delCancel) delCancel.onclick = null; }; if (delCancel) delCancel.onclick = () => { delDlg.close(); cleanupDelete(); }; delForm.onsubmit = async (ev) => { ev.preventDefault(); if (section?.is_template) { const ok = await confirmTemplateReferences('Löschen'); if (!ok) return; } const res = await apiAction('content.delete', { method: 'POST', data: { id: item.id, section_id: section.id } }); delDlg.close(); cleanupDelete(); toast(res && res.ok ? 'Gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok), { duration: 3000 }); dlg && dlg.close(); if (typeof window.loadList === 'function') window.loadList(section); }; delDlg.showModal(); }; const onVersionsClick = async (ev) => { const target = ev.target; if (!target || !target.dataset) return; const vid = Number(target.dataset.versionEdit || target.dataset.versionActivate || target.dataset.versionDeactivate || target.dataset.versionDelete || 0); if (!vid) return; if (target.dataset.versionEdit !== undefined) { dlg && dlg.close(); const versionItem = { ...item, version_id: vid }; openContentEditor(versionItem, section); return; } if (target.dataset.versionActivate !== undefined) { const res = await apiAction('content_versions.activate', { method: 'POST', data: { id: vid } }); toast(res && res.ok ? 'Version aktiviert' : 'Aktivieren fehlgeschlagen', !!(res && res.ok)); await loadVersions(); if (typeof window.loadList === 'function') window.loadList(section); return; } if (target.dataset.versionDeactivate !== undefined) { if (section?.is_template) { const ok = await confirmTemplateReferences('Deaktivieren'); if (!ok) return; } const res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: item.id } }); toast(res && res.ok ? 'Aktive Version deaktiviert' : 'Deaktivieren fehlgeschlagen', !!(res && res.ok)); await loadVersions(); if (typeof window.loadList === 'function') window.loadList(section); return; } if (target.dataset.versionDelete !== undefined) { const versionRow = versions.find(v => Number(v.id) === vid); if (versionRow && Number(versionRow.is_active) === 1) return; if (!confirm('Version wirklich löschen?')) return; const res = await apiAction('content_versions.delete', { method: 'POST', data: { id: vid, content_id: item.id } }); toast(res && res.ok ? 'Version gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok)); await loadVersions(); if (typeof window.loadList === 'function') window.loadList(section); return; } }; inpApiName && inpApiName.addEventListener('input', onApiInput); inpName && inpName.addEventListener('input', scheduleAutoSave); inpApiName && inpApiName.addEventListener('input', scheduleAutoSave); btnClose && (btnClose.onclick = () => { dlg && dlg.close(); cleanup(); }); btnDelete && (btnDelete.onclick = onDeleteItem); versionsWrap && versionsWrap.addEventListener('click', onVersionsClick); await loadVersions(); dlg && dlg.addEventListener('close', cleanup, { once: true }); dlg && dlg.showModal(); } export async function loadList(section) { const el = document.getElementById('view-content'); if (typeof section === 'string') { const sections = window.__sectionsConfig || []; section = sections.find(s => String(s.slug || '').toLowerCase() === section.toLowerCase()) || sections.find(s => String(s.name || '').toLowerCase() === section.toLowerCase()) || null; } else if (typeof section === 'number') { const sections = window.__sectionsConfig || []; section = sections.find(s => Number(s.id) === section) || null; } section = section || window.__activeSection || null; if (!el || !section) return; const label = section.name || 'Section'; const isTemplate = Number(section.is_template) === 1; el.innerHTML = `
${esc(label)}
Lade …
`; let data = []; try { data = await fetchContentList(section.id); } catch (err) { list.innerHTML = `
${esc(err.message || 'Laden fehlgeschlagen')}
`; return; } const list = el.querySelector('#list-section'); const filterInput = el.querySelector('#filter-section'); const filterReset = el.querySelector('#filter-section-reset'); const sortSelect = el.querySelector('#sort-section'); if (!Array.isArray(data) || data.length === 0) { list.innerHTML = `
Keine Einträge
`; return; } const sortDefault = (window.__listSortDefault || 'created_asc'); if (sortSelect) sortSelect.value = sortDefault; function compareByName(a, b) { const av = String(a?.name || ''); const bv = String(b?.name || ''); return av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' }); } function parseDate(value) { const t = Date.parse(value || ''); return Number.isFinite(t) ? t : 0; } function sortItems(items, key) { const listCopy = items.slice(); if (key === 'name_asc') { return listCopy.sort(compareByName); } if (key === 'name_desc') { return listCopy.sort((a, b) => compareByName(b, a)); } if (key === 'updated_desc') { return listCopy.sort((a, b) => parseDate(b.updated_at) - parseDate(a.updated_at)); } return listCopy.sort((a, b) => parseDate(a.created_at) - parseDate(b.created_at)); } function matchesQuery(item, query) { if (!query) return true; const q = query.toLowerCase(); const name = String(item?.name || '').toLowerCase(); const api = isTemplate ? String(item?.api_name || '').toLowerCase() : ''; return name.includes(q) || (api && api.includes(q)); } const versionCache = new Map(); async function loadVersionOptions(selectEl, itemId) { if (!selectEl || !itemId) return; if (versionCache.has(itemId)) { const cached = versionCache.get(itemId); renderVersionSelect(selectEl, cached.items, cached.activeId); return; } try { const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: itemId } }); const items = Array.isArray(res?.items) ? res.items : []; const active = items.find(v => Number(v.is_active) === 1); const activeId = active ? String(active.id) : (items[0] ? String(items[0].id) : ''); versionCache.set(itemId, { items, activeId }); renderVersionSelect(selectEl, items, activeId); } catch { renderVersionSelect(selectEl, [], ''); } } function renderVersionSelect(selectEl, items, activeId) { selectEl.innerHTML = ''; if (!items.length) { const opt = document.createElement('option'); opt.value = ''; opt.textContent = 'Keine Versionen'; opt.disabled = true; selectEl.appendChild(opt); selectEl.disabled = true; return; } selectEl.disabled = false; const sorted = items.slice().sort((a, b) => { const aActive = Number(a.is_active) === 1 ? 1 : 0; const bActive = Number(b.is_active) === 1 ? 1 : 0; if (aActive !== bActive) return bActive - aActive; return Number(b.id || 0) - Number(a.id || 0); }); sorted.forEach(item => { const opt = document.createElement('option'); const label = `${Number(item.is_active) === 1 ? '✓ ' : ''}#${item.version_no} – ${formatVersionDate(item.created_at)}` + (Number(item.is_active) === 1 ? ' (aktiv)' : ''); opt.value = String(item.id); opt.textContent = label; selectEl.appendChild(opt); }); if (activeId) selectEl.value = activeId; } function render(items) { list.innerHTML = items.map(item => { const name = esc(item.name || ''); const apiName = isTemplate ? esc(item.api_name || '') : ''; const apiLine = (isTemplate && apiName) ? `
API: ${apiName}
` : ''; const versionSelect = ``; const nameCell = `
${name || '(ohne Name)'}
${apiLine}
`; const openBtn = ``; const editTplBtn = ``; const testBtn = isTemplate ? `` : ''; const prevBtn = ``; return `
${nameCell}
#${item.id}
${openBtn} ${versionSelect} ${[testBtn, prevBtn, editTplBtn].filter(Boolean).join('')}
`; }).join(''); bindListHandlers(list); } function applyFilter() { const query = (filterInput?.value || '').trim(); const sortKey = sortSelect?.value || sortDefault; const filtered = data.filter(item => matchesQuery(item, query)); const sorted = sortItems(filtered, sortKey); render(sorted); } async function persistSort(nextValue) { window.__listSortDefault = nextValue; try { await apiAction('account.settings.update', { method: 'POST', data: { list_sort: nextValue } }); } catch {} } if (filterInput) filterInput.addEventListener('input', applyFilter); if (filterReset) { filterReset.addEventListener('click', () => { if (filterInput) { filterInput.value = ''; filterInput.focus(); } applyFilter(); }); } if (sortSelect) { sortSelect.addEventListener('change', () => { const next = sortSelect.value || sortDefault; persistSort(next); applyFilter(); }); } applyFilter(); function bindListHandlers(scope) { scope.querySelectorAll('[data-version-select]').forEach(sel => { const id = Number(sel.getAttribute('data-version-select') || 0); loadVersionOptions(sel, id); sel.addEventListener('focus', () => loadVersionOptions(sel, id)); sel.addEventListener('click', () => loadVersionOptions(sel, id)); sel.addEventListener('change', () => { const versionId = Number(sel.value || 0); if (!versionId) return; const item = data.find(it => Number(it.id) === id); if (!item) return; openContentEditor({ ...item, version_id: versionId }, section); }); }); scope.querySelectorAll('[data-open]').forEach(btn => btn.addEventListener('click', () => { const id = Number(btn.dataset.open || 0); const item = data.find(it => Number(it.id) === id); if (!item) return; openContentEditor(item, section); })); scope.querySelectorAll('[data-edit]').forEach(btn => btn.addEventListener('click', () => { const id = Number(btn.dataset.edit || 0); const item = data.find(it => Number(it.id) === id); if (item) openTemplateManager(item, section); })); const prevDlg = document.getElementById('previewDialog'); const prevFrame = document.getElementById('previewFrame'); scope.querySelectorAll('[data-preview]').forEach(btn => btn.addEventListener('click', async () => { const id = Number(btn.dataset.preview || 0); const obj = await apiAction('content.get', { method: 'GET', data: { id, section_id: section.id, active_only: 1 } }); const html = (obj?.html || obj?.content || '(leer)'); prevFrame.srcdoc = '' + html + ''; prevDlg.showModal(); })); scope.querySelectorAll('[data-test]').forEach(btn => btn.addEventListener('click', async () => { const id = Number(btn.dataset.test || 0); const nm = btn.dataset.name || ''; if (!id) { toast('Testversand: Ungültige ID', false); return; } const activeCheck = await apiAction('content.get', { method: 'GET', data: { id, section_id: section.id, active_only: 1 } }).catch(() => ({})); if (!activeCheck?.active_version_id && !activeCheck?.item?.active_version_id) { toast('Testversand nur mit aktiver Version möglich.', false); return; } if (window.AdminTestSend && typeof window.AdminTestSend.open === 'function') { window.AdminTestSend.open({ id, name: nm }); } else { toast('Testversand ist aktuell nicht verfügbar.', false); } })); // delete handling removed from overview } } export function initLists() { if (window.__sectionsReady && typeof window.__sectionsReady.then === 'function') { window.__sectionsReady.then(() => { if (window.__activeSection) loadList(window.__activeSection); }); } else if (window.__activeSection) { loadList(window.__activeSection); } window.__reloadList = loadList; window.loadList = loadList; }