Files
emailtemplate.it/public/assets/js/ui-list.js
2026-01-21 22:44:32 +01:00

389 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { apiAction, apiUpdate, toast } from './api.js';
function esc(s = '') {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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 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');
const detail = await fetchContentItem(item.id, section.id).catch(() => ({}));
const row = detail?.item || detail?.data || detail || {};
const initialApi = row.api_name || '';
if (inpName) inpName.value = row.name || '';
if (inpApiName) inpApiName.value = initialApi;
if (apiWarn) apiWarn.classList.add('hidden');
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);
}
};
function cleanup() {
form && form.removeEventListener('submit', onSubmit);
btnCancel && (btnCancel.onclick = null);
inpApiName && inpApiName.removeEventListener('input', onApiInput);
}
async function onSubmit(ev) {
ev.preventDefault();
try {
const res = await apiUpdate('content', item.id, {
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);
} catch {
toast('Speichern fehlgeschlagen', false);
}
}
inpApiName && inpApiName.addEventListener('input', onApiInput);
form && form.addEventListener('submit', onSubmit, { once: false });
btnCancel && (btnCancel.onclick = () => { dlg && dlg.close(); cleanup(); });
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 = `<div class='rounded-2xl border bg-white overflow-hidden'>
<div class='px-4 py-2 border-b bg-gray-50 text-sm font-medium flex items-center gap-3'>
<span>${esc(label)}</span>
<div class='ms-auto flex items-center gap-2'>
<div class='flex items-center gap-1'>
<input id='filter-section' class='input text-sm' placeholder='Suche Name${isTemplate ? '/API' : ''}' />
<button id='filter-section-reset' class='btn' type='button' title='Suche zurücksetzen'>×</button>
</div>
<select id='sort-section' class='input text-sm'>
<option value='created_asc'>Erstelldatum (aufsteigend)</option>
<option value='name_asc'>Name AZ</option>
<option value='name_desc'>Name ZA</option>
<option value='updated_desc'>Zuletzt bearbeitet</option>
</select>
</div>
</div>
<div id='list-section' class='divide-y'>Lade …</div></div>`;
let data = [];
try {
data = await fetchContentList(section.id);
} catch (err) {
list.innerHTML = `<div class='p-4 text-sm text-gray-500'>${esc(err.message || 'Laden fehlgeschlagen')}</div>`;
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 = `<div class='p-4 text-sm text-gray-500'>Keine Einträge</div>`;
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;
items.forEach(item => {
const opt = document.createElement('option');
opt.value = String(item.id);
opt.textContent = `#${item.version_no}` + (Number(item.is_active) === 1 ? ' (aktiv)' : '');
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) ? `<div class='text-xs text-slate-500'>API: ${apiName}</div>` : '';
const versionSelect = `<select class="input h-8 py-0 text-xs min-w-[160px]" data-version-select="${item.id}" disabled>
<option value="">Versionen laden…</option>
</select>`;
const nameCell = `<div class='min-w-48'>
<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 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>`;
return `<div class='p-3 flex items-center gap-3'>
${nameCell}
<div class='text-xs text-gray-500'>#${item.id}</div>
${versionSelect}
<div class='ms-auto flex gap-2'>${[openBtn, editTplBtn, testBtn, prevBtn, delBtn].filter(Boolean).join('')}</div>
</div>`;
}).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);
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) openTemplateEditor(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 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', () => {
const id = Number(btn.dataset.test || 0);
const nm = btn.dataset.name || '';
if (!id) {
toast('Testversand: Ungültige ID', 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);
}
}));
const delDlg = document.getElementById('deleteDialog');
const delText = document.getElementById('deleteText');
const delForm = document.getElementById('deleteForm');
const delCancel = document.getElementById('deleteCancel');
let pending = null;
delCancel && (delCancel.onclick = () => { pending = null; delDlg.close(); });
scope.querySelectorAll('[data-del]').forEach(btn => btn.addEventListener('click', () => {
const id = Number(btn.dataset.del || 0);
const nm = btn.dataset.name || '';
pending = { id, name: nm };
if (delText) {
delText.innerHTML = `Soll <strong>${nm || '(ohne Name)'} #${id}</strong> wirklich gelöscht werden?`;
}
delDlg.showModal();
}));
delForm && (delForm.onsubmit = async (ev) => {
ev.preventDefault();
if (!pending) return delDlg.close();
const res = await apiAction('content.delete', { method: 'POST', data: { id: pending.id, section_id: section.id } });
delDlg.close();
toast(res && res.ok ? 'Gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok), { duration: 3000 });
loadList(section);
});
}
}
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;
}