This commit is contained in:
2026-01-26 00:40:04 +01:00
parent 2d3b3fc06d
commit 4e17211cf0
3 changed files with 229 additions and 46 deletions

View File

@@ -59,21 +59,79 @@ async function openContentEditor(item, section) {
}
}
async function openTemplateEditor(item, section) {
const dlg = document.getElementById('editTemplateDialog');
const form = document.getElementById('editTemplateForm');
const inpName = document.getElementById('edit_tpl_name');
const inpApiName = document.getElementById('edit_tpl_api_name');
const apiWarn = document.getElementById('edit_tpl_api_warn');
const btnCancel = document.getElementById('editTemplateCancel');
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 previewFrame = document.getElementById('manage_tpl_preview');
const btnClose = document.getElementById('manageTemplateClose');
const btnSave = document.getElementById('manageTemplateSave');
const btnDelete = document.getElementById('manageTemplateDelete');
const deleteHint = document.getElementById('manage_tpl_delete_hint');
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 renderPreview = (html) => {
if (!previewFrame) return;
previewFrame.srcdoc = '<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body>' + (html || '<em>(leer)</em>') + '</body></html>';
};
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 = '<div class=\"text-xs text-slate-500\">Keine Versionen vorhanden</div>';
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)' : '');
return `<div class=\"flex items-center gap-2 border rounded-lg px-3 py-2\" data-version-row=\"${v.id}\">
<div class=\"text-xs text-slate-600\">${label}</div>
<div class=\"ms-auto flex gap-2\">
<button class=\"btn\" data-version-preview=\"${v.id}\">Vorschau</button>
<button class=\"btn\" data-version-edit=\"${v.id}\">Bearbeiten</button>
${isActive ? `<button class=\"btn\" data-version-deactivate=\"${v.id}\">Deaktivieren</button>` : `<button class=\"btn\" data-version-activate=\"${v.id}\">Aktivieren</button>`}
<button class=\"btn btn-danger\" data-version-delete=\"${v.id}\" ${isActive ? 'disabled' : ''}>Löschen</button>
</div>
</div>`;
}).join('');
};
const onApiInput = () => {
if (!inpApiName) return;
@@ -84,33 +142,97 @@ async function openTemplateEditor(item, section) {
}
};
function cleanup() {
form && form.removeEventListener('submit', onSubmit);
btnCancel && (btnCancel.onclick = null);
const cleanup = () => {
inpApiName && inpApiName.removeEventListener('input', onApiInput);
}
versionsWrap && versionsWrap.removeEventListener('click', onVersionsClick);
if (btnClose) btnClose.onclick = null;
if (btnSave) btnSave.onclick = null;
if (btnDelete) btnDelete.onclick = null;
};
async function onSubmit(ev) {
ev.preventDefault();
const onSave = async () => {
try {
const res = await apiUpdate('content', item.id, {
const payload = {
name: inpName ? inpName.value : '',
api_name: inpApiName ? inpApiName.value : '',
section_id: section.id,
});
toast(res && res.ok ? 'Template gespeichert' : 'Speichern fehlgeschlagen', !!(res && res.ok));
dlg && dlg.close();
cleanup();
if (typeof window.loadList === 'function') window.loadList(section);
};
if (section?.is_template) {
payload.api_name = inpApiName ? inpApiName.value : '';
}
const res = await apiUpdate('content', item.id, payload);
toast(res && res.ok ? 'Gespeichert' : 'Speichern fehlgeschlagen', !!(res && res.ok));
if (res?.ok && typeof window.loadList === 'function') window.loadList(section);
} catch {
toast('Speichern fehlgeschlagen', false);
}
}
};
const onDeleteItem = async () => {
if (activeId) return;
if (!confirm('Template wirklich löschen?')) return;
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);
};
const onVersionsClick = async (ev) => {
const target = ev.target;
if (!target || !target.dataset) return;
const vid = Number(target.dataset.versionPreview || target.dataset.versionEdit || target.dataset.versionActivate || target.dataset.versionDeactivate || target.dataset.versionDelete || 0);
if (!vid) return;
if (target.dataset.versionPreview !== undefined) {
const res = await apiAction('content_versions.get', { method: 'GET', data: { id: vid, content_id: item.id } }).catch(() => ({}));
const html = res?.item?.html || res?.item?.content || res?.html || res?.content || '';
renderPreview(html);
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();
return;
}
if (target.dataset.versionDeactivate !== undefined) {
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();
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();
return;
}
};
inpApiName && inpApiName.addEventListener('input', onApiInput);
form && form.addEventListener('submit', onSubmit, { once: false });
btnCancel && (btnCancel.onclick = () => { dlg && dlg.close(); cleanup(); });
btnClose && (btnClose.onclick = () => { dlg && dlg.close(); cleanup(); });
btnSave && (btnSave.onclick = onSave);
btnDelete && (btnDelete.onclick = onDeleteItem);
versionsWrap && versionsWrap.addEventListener('click', onVersionsClick);
await loadVersions();
if (activeId) {
const res = await apiAction('content_versions.get', { method: 'GET', data: { id: activeId, content_id: item.id } }).catch(() => ({}));
const html = res?.item?.html || res?.item?.content || res?.html || res?.content || '';
renderPreview(html);
} else {
renderPreview('');
}
dlg && dlg.addEventListener('close', cleanup, { once: true });
dlg && dlg.showModal();
}
@@ -262,8 +384,8 @@ export async function loadList(section) {
<div class='font-medium truncate' title="${name}">${name || '(ohne Name)'}</div>
${apiLine}
</div>`;
const openBtn = `<button class='btn' data-open='${item.id}'>Im Editor öffnen</button>`;
const editTplBtn = isTemplate ? `<button class='btn' data-edit='${item.id}'>Bearbeiten</button>` : '';
const openBtn = `<button class='btn' data-open='${item.id}'>Bearbeiten</button>`;
const editTplBtn = isTemplate ? `<button class='btn' data-edit='${item.id}'>Verwaltung</button>` : '';
const testBtn = isTemplate ? `<button class='btn' data-test='${item.id}' data-name='${name}'>Testversand</button>` : '';
const prevBtn = `<button class='btn' data-preview='${item.id}'>Vorschau</button>`;
const delBtn = `<button class='btn btn-danger' data-del='${item.id}' data-name='${name}'>Löschen</button>`;
@@ -341,26 +463,31 @@ export async function loadList(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) openTemplateEditor(item, section);
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 fetchContentItem(id, section.id);
const obj = await apiAction('content.get', { method: 'GET', data: { id, section_id: section.id, active_only: 1 } });
const html = (obj?.html || obj?.content || '<em>(leer)</em>');
prevFrame.srcdoc = '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>' + html + '</body></html>';
prevDlg.showModal();
}));
scope.querySelectorAll('[data-test]').forEach(btn => btn.addEventListener('click', () => {
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 {

View File

@@ -161,26 +161,47 @@ require __DIR__ . '/../partials/structure/layout_start.php';
</form>
</dialog>
<!-- Edit Template Dialog -->
<dialog id="editTemplateDialog" class="rounded-2xl p-0 w-[700px]">
<form id="editTemplateForm" method="dialog" class="p-4 bg-white rounded-2xl">
<h3 class="text-lg font-semibold mb-2">Template bearbeiten</h3>
<div class="space-y-3">
<label class="block">
<span class="text-sm text-slate-600">Name</span>
<input id="edit_tpl_name" type="text" class="w-full border rounded-lg px-3 py-2" />
</label>
<label class="block">
<span class="text-sm text-slate-600">API Name (ohne Leerzeichen)</span>
<input id="edit_tpl_api_name" type="text" class="w-full border rounded-lg px-3 py-2" />
<p id="edit_tpl_api_warn" class="text-xs text-amber-700 mt-1 hidden">Warnung: Das Ändern des API-Namens kann bestehende API-Integrationen brechen.</p>
</label>
<!-- Template Verwaltung Dialog -->
<dialog id="manageTemplateDialog" class="rounded-2xl p-0 w-[980px]">
<div class="p-4 bg-white rounded-2xl">
<div class="flex items-center gap-3">
<h3 class="text-lg font-semibold">Template Verwaltung</h3>
<span id="manage_tpl_badge" class="text-xs px-2 py-1 rounded-full bg-slate-100 text-slate-600">ID </span>
<div class="ms-auto flex gap-2">
<button type="button" id="manageTemplateClose" class="btn">Schließen</button>
<button type="button" id="manageTemplateSave" class="btn">Speichern</button>
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<button type="button" id="editTemplateCancel" class="btn">Abbrechen</button>
<button type="submit" id="editTemplateSave" class="btn">Speichern</button>
<div class="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="space-y-3">
<label class="block">
<span class="text-sm text-slate-600">Name</span>
<input id="manage_tpl_name" type="text" class="w-full border rounded-lg px-3 py-2" />
</label>
<label id="manage_tpl_api_wrap" class="block">
<span class="text-sm text-slate-600">API Name (nur Templates)</span>
<input id="manage_tpl_api_name" type="text" class="w-full border rounded-lg px-3 py-2" />
<p id="manage_tpl_api_warn" class="text-xs text-amber-700 mt-1 hidden">Warnung: Das Ändern des API-Namens kann bestehende API-Integrationen brechen.</p>
</label>
<div class="mt-2">
<div class="text-sm font-semibold mb-2">Versionen</div>
<div id="manage_tpl_versions" class="space-y-2 text-sm text-slate-700"></div>
</div>
<div class="mt-3 flex items-center gap-2">
<button type="button" id="manageTemplateDelete" class="btn btn-danger">Template löschen</button>
<span id="manage_tpl_delete_hint" class="text-xs text-slate-500"></span>
</div>
</div>
<div class="space-y-2">
<div class="text-sm font-semibold">Vorschau (gewählte Version)</div>
<div class="rounded-lg border bg-slate-50 p-2">
<iframe id="manage_tpl_preview" class="w-full h-[360px] bg-white rounded-md"></iframe>
</div>
</div>
</div>
</form>
</div>
</dialog>
<div id="toast-root"></div>

View File

@@ -1315,6 +1315,38 @@ class ApiKernel
$this->respond(['ok' => true, 'deactivated' => true, 'content_id' => $contentId]);
}
private function handleContentVersionsDelete(): void
{
$auth = $this->requireAuth();
$customerId = (int)($auth['customer_id'] ?? 0);
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
$versionId = (int)$this->val($this->in, ['id', 'version_id', 'version'], 0);
if ($versionId <= 0) $this->fail('version id required', null, 422);
$contentId = (int)$this->val($this->in, ['content_id', 'content'], 0);
$table = $this->contentVersionsTable();
if (!$this->tableExists($table)) $this->fail('Versions table not available', null, 500);
$sql = "SELECT `id`,`content_id`,`customer_id`,`is_active` FROM `$table` WHERE `id` = :id AND `customer_id` = :cid";
$params = [':id' => $versionId, ':cid' => $customerId];
if ($contentId > 0) {
$sql .= " AND `content_id` = :content";
$params[':content'] = $contentId;
}
$sql .= " LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
if (!$row) $this->fail('Not found', ['id' => $versionId], 404);
if ((int)($row['is_active'] ?? 0) === 1) {
$this->fail('Active versions cannot be deleted', ['id' => $versionId], 422);
}
$stmt = $this->pdo->prepare("DELETE FROM `$table` WHERE `id` = :id AND `customer_id` = :cid LIMIT 1");
$stmt->execute([':id' => $versionId, ':cid' => $customerId]);
$this->respond(['ok' => true, 'deleted' => true, 'id' => $versionId]);
}
private function handleSectionsConfigList(): void
{
$auth = $this->requireAuth();
@@ -2516,6 +2548,9 @@ class ApiKernel
case 'content_versions.deactivate':
$this->handleContentVersionsDeactivate();
break;
case 'content_versions.delete':
$this->handleContentVersionsDelete();
break;
/* ---------- CRUD HANDLER ---------- */
default: