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 isTemplateSection = () => {
if (section?.is_template) return true;
const slug = (section?.slug || '').toString().toLowerCase();
return slug === 'emailtemplate';
};
const fetchTemplateReferences = async () => {
if (!isTemplateSection()) 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 (!isTemplateSection()) 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 = ``;
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;
}