adasd
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user