This commit is contained in:
2025-12-07 23:12:07 +01:00
parent 3cbbf4dd68
commit bf7971aaa0
6 changed files with 598 additions and 22 deletions

View File

@@ -21,12 +21,16 @@ export function initEditor() {
  const sendInfo     = document.getElementById('send_template_info');
  const btnCancelSend= document.getElementById('btn-cancel-send');
  const btnSendNow   = document.getElementById('btn-send-now');
  const sendSender   = document.getElementById('send_sender');
  const sendSenderHint = document.getElementById('send_sender_hint');
  const prevFrame    = document.getElementById('previewFrame');
  const btnPrevClose = document.getElementById('btn-close-preview');
  let current = null;   // { resource, id, name }
  let bridgeListener = null;
  let reqToken = 0;     // steigender Token pro Öffnen -> ignoriert verspätete Events
  let senderOptions = [];
  let senderLoadPromise = null;
  const ok  = (m) => toast(m, true);
  const err = (m) => toast(m, false);
@@ -228,6 +232,43 @@ export function initEditor() {
    return (rows || []).map(r => ({ id: r.id, name: r.name, html: r.content || r.html || '' }));
  }
  async function loadSenderOptions(force = false) {
    if (!sendSender) return;
    if (senderLoadPromise && !force) return senderLoadPromise;
    senderLoadPromise = apiAction('account.senders.list', { method: 'GET' })
      .then(res => {
        senderOptions = res?.items || [];
        renderSenderOptions();
      })
      .catch(() => {
        senderOptions = [];
        renderSenderOptions();
      })
      .finally(() => {
        senderLoadPromise = null;
      });
    return senderLoadPromise;
  }
  function renderSenderOptions() {
    if (!sendSender) return;
    const previous = sendSender.value;
    let html = '<option value="">Standard (System)</option>';
    senderOptions.forEach(opt => {
      const label = opt.label || opt.from_name || opt.from_email;
      html += `<option value="${opt.id}">${escapeHtml(label)} &lt;${escapeHtml(opt.from_email)}&gt;</option>`;
    });
    sendSender.innerHTML = html;
    if (previous && senderOptions.some(opt => String(opt.id) === previous)) {
      sendSender.value = previous;
    } else {
      sendSender.value = '';
    }
    if (sendSenderHint) {
      sendSenderHint.classList.toggle('hidden', senderOptions.length > 0);
    }
  }
  // ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ----------
async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token, hasJson, json }) {
if (token !== reqToken) return; // veraltete Anfrage ignorieren
@@ -423,9 +464,13 @@ export function initEditor() {
    setSendContext(ctxId, ctxName);
    if (sendSubject) sendSubject.value = ctx?.subject || 'Testversand';
    if (sendTo) sendTo.value = ctx?.to || '';
    await loadSenderOptions(true);
    sendDlg?.showModal?.();
  }
  function closeSend(){ sendDlg?.close?.(); }
  function closeSend(){
    sendDlg?.close?.();
    if (sendSender) sendSender.value = '';
  }
  async function doSend(ev){
    ev?.preventDefault?.();
@@ -437,7 +482,15 @@ export function initEditor() {
    if(!id){ toast("Kein Template geladen", false); return; }
    // Hier wird der gespeicherte HTML-Code verwendet, nicht der Live-HTML, da apiAction
    // keine Live-Daten erwartet. Es geht um template_id.
    const r = await apiAction('templates.test_send', { method:'POST', data:{ template_id: id, to, subject: (sendSubject?.value || 'Testversand') } });
    const payload = {
      template_id: id,
      to,
      subject: (sendSubject?.value || 'Testversand'),
    };
    if (sendSender && sendSender.value) {
      payload.sender_id = Number(sendSender.value);
    }
    const r = await apiAction('templates.test_send', { method:'POST', data: payload });
    if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }
  }
  function closePreview(){ prevDlg?.close?.(); }
@@ -488,6 +541,15 @@ export function initEditor() {
  window.EditorUI = { open, save, close, clear: clearEditor, preview: openPreview };
}
function escapeHtml(str) {
  return String(str || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}
// Default-Export + globaler Fallback
export default initEditor;
window.initEditor = initEditor;

View File

@@ -5,6 +5,9 @@ const state = {
rotate: { bridge: false, sender: false, external: false },
users: [],
userMap: new Map(),
senders: [],
senderMap: new Map(),
bridgeTables: [],
currentTab: 'profile',
loading: false,
};
@@ -15,6 +18,10 @@ let passwordForm;
let settingsForm;
let teamTable;
let userForm;
let senderTable;
let senderForm;
let bridgePreview;
let validateBridgeBtn;
export function initUserPanel() {
avatarBtn = document.getElementById('btn-user');
@@ -27,15 +34,25 @@ export function initAccountPage() {
settingsForm = document.getElementById('settingsForm');
teamTable = document.getElementById('teamTable');
userForm = document.getElementById('userForm');
senderTable = document.getElementById('senderTable');
senderForm = document.getElementById('senderForm');
bridgePreview = document.getElementById('bridgeTablesPreview');
validateBridgeBtn = document.getElementById('btn-validate-bridge');
document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm());
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
userForm?.addEventListener('submit', submitUserForm);
document.getElementById('btn-sender-add')?.addEventListener('click', () => openSenderForm());
document.getElementById('senderFormCancel')?.addEventListener('click', () => closeSenderForm());
senderForm?.addEventListener('submit', submitSenderForm);
profileForm?.addEventListener('submit', submitProfileForm);
passwordForm?.addEventListener('submit', submitPasswordForm);
settingsForm?.addEventListener('submit', submitSettingsForm);
teamTable?.addEventListener('click', handleTeamTableClick);
senderTable?.addEventListener('click', handleSenderTableClick);
validateBridgeBtn?.addEventListener('click', validateBridgeSettings);
document.querySelectorAll('[data-user-tab]').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
@@ -60,12 +77,18 @@ export function initAccountPage() {
switchTab(state.currentTab);
loadAccountData();
updateRoleVisibility();
}
function isOwner() {
return (window.__currentUser?.role || '').toLowerCase() === 'owner';
}
function isAdmin() {
const role = (window.__currentUser?.role || '').toLowerCase();
return role === 'owner' || role === 'admin';
}
function updateAvatar() {
const target = document.getElementById('userAvatar');
if (!target) return;
@@ -73,7 +96,19 @@ function updateAvatar() {
target.textContent = name ? name.trim().charAt(0).toUpperCase() : 'U';
}
function updateOwnerVisibility() {
function updateRoleVisibility() {
const role = (window.__currentUser?.role || '').toLowerCase();
document.querySelectorAll('[data-role]').forEach(el => {
const allowed = (el.getAttribute('data-role') || '').split(/[\s,]+/).filter(Boolean).map(r => r.toLowerCase());
if (!allowed.length) return;
const visible = allowed.some(targetRole => {
if (targetRole === 'owner') return role === 'owner';
if (targetRole === 'admin') return role === 'owner' || role === 'admin';
if (targetRole === 'editor') return role === 'owner' || role === 'admin' || role === 'editor';
return true;
});
el.classList.toggle('hidden', !visible);
});
document.querySelectorAll('.owner-only').forEach(el => {
el.classList.toggle('hidden', !isOwner());
});
@@ -100,13 +135,20 @@ async function loadAccountData() {
if (res.user) {
window.__currentUser = res.user;
updateAvatar();
updateOwnerVisibility();
updateRoleVisibility();
}
fillProfileForm(res.user);
fillSettingsForm(res.settings || {});
if (isOwner()) {
await loadUsers();
}
if (isAdmin()) {
await loadSenders();
} else {
state.senders = [];
state.senderMap = new Map();
renderSenderList();
}
} catch (err) {
console.error(err);
toast(err.message || 'Fehler beim Laden', false);
@@ -128,6 +170,9 @@ function fillSettingsForm(settings) {
settingsForm.bridge_token.value = settings.bridge_token || '';
settingsForm.sender_token.value = settings.sender_token || '';
settingsForm.external_api_token.value = settings.external_api_token || '';
const tables = Array.isArray(settings.bridge_tables) ? settings.bridge_tables : [];
settingsForm.bridge_tables ? settingsForm.bridge_tables.value = tables.join(', ') : null;
applyBridgePreview(tables);
state.rotate = { bridge: false, sender: false, external: false };
}
@@ -174,6 +219,7 @@ async function submitSettingsForm(ev) {
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
rotate_sender_token: state.rotate.sender ? 1 : 0,
rotate_external_token: state.rotate.external ? 1 : 0,
bridge_tables: parseBridgeTablesInput(),
};
try {
const res = await apiAction('account.settings.update', { method: 'POST', data });
@@ -207,6 +253,50 @@ async function downloadFile(type) {
}
}
function parseBridgeTablesInput() {
if (!settingsForm) return [];
const raw = settingsForm.bridge_tables?.value || '';
return raw
.split(/[\s,]+/)
.map(part => part.trim())
.filter(Boolean);
}
function applyBridgePreview(tables) {
state.bridgeTables = Array.isArray(tables) ? tables : [];
if (!bridgePreview) return;
if (!state.bridgeTables.length) {
bridgePreview.innerHTML = '<span class="text-xs text-slate-500">Keine Einschränkung alle Tabellen erlaubt.</span>';
return;
}
bridgePreview.innerHTML = state.bridgeTables.map(name => `<span class="chip">${escapeHtml(name)}</span>`).join('');
}
async function validateBridgeSettings(ev) {
ev?.preventDefault();
if (!settingsForm) return;
const data = {
bridge_url: settingsForm.bridge_url.value.trim(),
bridge_token: settingsForm.bridge_token.value.trim(),
};
if (!data.bridge_url || !data.bridge_token) {
toast('Bitte Bridge-URL und Token angeben', false);
return;
}
try {
const res = await apiAction('account.bridge.test', { method: 'POST', data });
if (!res?.ok) throw new Error(res?.error || 'Prüfung fehlgeschlagen');
const tables = Array.isArray(res.tables) ? res.tables : [];
applyBridgePreview(tables);
if (settingsForm.bridge_tables) {
settingsForm.bridge_tables.value = tables.join(', ');
}
toast('Bridge erfolgreich geprüft', true);
} catch (err) {
toast(err.message || 'Prüfung fehlgeschlagen', false);
}
}
async function loadUsers() {
try {
const res = await apiAction('account.users.list', { method: 'GET' });
@@ -329,6 +419,110 @@ function copyToClipboard(value) {
}
}
async function loadSenders() {
if (!senderTable) return;
try {
const res = await apiAction('account.senders.list', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Absender konnten nicht geladen werden');
state.senders = res.items || [];
state.senderMap = new Map(state.senders.map(item => [item.id, item]));
renderSenderList();
} catch (err) {
toast(err.message || 'Fehler beim Laden der Absender', false);
}
}
function renderSenderList() {
if (!senderTable) return;
const tbody = senderTable.querySelector('tbody');
if (!tbody) return;
if (!state.senders.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-sm text-slate-500">Keine Absender vorhanden.</td></tr>';
return;
}
tbody.innerHTML = state.senders.map(sender => `
<tr>
<td>${escapeHtml(sender.label || sender.from_name || sender.from_email)}</td>
<td>${escapeHtml(sender.from_name || '—')}</td>
<td>${escapeHtml(sender.from_email)}</td>
<td>${escapeHtml(sender.reply_to || '')}</td>
<td class="text-right flex gap-2 justify-end">
<button class="btn" data-sender-action="edit" data-sender-id="${sender.id}">Bearbeiten</button>
<button class="btn btn-danger" data-sender-action="delete" data-sender-id="${sender.id}">Löschen</button>
</td>
</tr>
`).join('');
}
function handleSenderTableClick(ev) {
const btn = ev.target.closest('button[data-sender-action]');
if (!btn) return;
const id = Number(btn.getAttribute('data-sender-id'));
const action = btn.getAttribute('data-sender-action');
const sender = state.senderMap.get(id);
if (!sender) return;
if (action === 'edit') {
openSenderForm(sender);
} else if (action === 'delete') {
if (confirm(`Absender "${sender.label || sender.from_email}" wirklich löschen?`)) {
deleteSender(id);
}
}
}
function openSenderForm(sender = null) {
if (!senderForm) return;
senderForm.classList.remove('hidden');
senderForm.sender_id.value = sender?.id || '';
senderForm.label.value = sender?.label || '';
senderForm.from_name.value = sender?.from_name || '';
senderForm.from_email.value = sender?.from_email || '';
senderForm.reply_to.value = sender?.reply_to || '';
}
function closeSenderForm() {
if (!senderForm) return;
senderForm.classList.add('hidden');
senderForm.reset();
senderForm.sender_id.value = '';
}
async function submitSenderForm(ev) {
ev.preventDefault();
if (!senderForm) return;
const payload = {
sender_id: senderForm.sender_id.value ? Number(senderForm.sender_id.value) : undefined,
label: senderForm.label.value.trim(),
from_name: senderForm.from_name.value.trim(),
from_email: senderForm.from_email.value.trim(),
reply_to: senderForm.reply_to.value.trim(),
};
if (!payload.from_email) {
toast('Bitte eine Absenderadresse angeben', false);
return;
}
try {
const res = await apiAction('account.senders.save', { method: 'POST', data: payload });
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
closeSenderForm();
await loadSenders();
toast('Absender gespeichert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function deleteSender(senderId) {
try {
const res = await apiAction('account.senders.delete', { method: 'POST', data: { sender_id: senderId } });
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
await loadSenders();
toast('Absender gelöscht', true);
} catch (err) {
toast(err.message || 'Fehler beim Löschen', false);
}
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')