Files
emailtemplate.it/public/assets/js/ui-user.js
2026-02-09 01:38:39 +01:00

1178 lines
43 KiB
JavaScript
Executable File

import { apiAction, toast } from './api.js';
const state = {
settings: {},
rotate: { bridge: false, sender: false, external: false },
users: [],
userMap: new Map(),
senders: [],
senderMap: new Map(),
currentTab: 'profile',
loading: false,
};
const pageType = document.body?.dataset?.page || 'account';
const DEBUG_EMAIL = 'madmin@papa-kind-treff.info';
const DEBUG_ENV = (window.APP_ENV || '').toLowerCase();
const MAX_CONSOLE_LINES = 200;
let avatarBtn;
let userMenuPanel;
let profileForm;
let passwordForm;
let settingsForm;
let teamTable;
let userForm;
let senderTable;
let senderForm;
let sectionsList;
let sectionsCreateForm;
let sectionNameInput;
let sectionsDeleteDialog;
let sectionsDeleteForm;
let sectionsDeleteTarget;
let sectionsDeleteText;
let sectionsDeleteCancel;
let sectionDragId = null;
let menuInitialized = false;
let menuOpen = false;
let debugButton;
let debugDialog;
let debugPhpLoaded = false;
let debugPhpLoading = false;
let debugLogsLoaded = false;
let debugActiveTab = 'php';
let debugLogsRefreshTimer = null;
let debugSelectedLogName = '';
let phpInfoContainer;
let consoleContainer;
let logsListContainer;
let logDetailContainer;
let debugStylesInjected = false;
let consolePatched = false;
const consoleBuffer = [];
let adminTablesAllSelect;
let adminTablesSelectedSelect;
let adminTablesAddBtn;
let adminTablesRemoveBtn;
let adminLoadBridgeBtn;
ensureConsoleCapture();
export function initUserPanel() {
avatarBtn = document.getElementById('btn-user');
userMenuPanel = document.getElementById('userMenuPanel');
ensureConsoleCapture();
handleUserContextChange();
if (!menuInitialized && avatarBtn && userMenuPanel) {
avatarBtn.addEventListener('click', toggleUserMenu);
document.addEventListener('click', handleDocumentClick, true);
document.addEventListener('keydown', handleMenuKeydown);
userMenuPanel.addEventListener('click', handleMenuItemClick);
menuInitialized = true;
}
}
export function initAccountPage() {
profileForm = document.getElementById('profileForm');
passwordForm = document.getElementById('passwordForm');
settingsForm = document.getElementById('settingsForm');
teamTable = document.getElementById('teamTable');
userForm = document.getElementById('userForm');
senderTable = document.getElementById('senderTable');
senderForm = document.getElementById('senderForm');
adminTablesAllSelect = document.getElementById('adminBridgeTablesAll');
adminTablesSelectedSelect = document.getElementById('adminBridgeTablesSelected');
adminTablesAddBtn = document.getElementById('adminBridgeTablesAdd');
adminTablesRemoveBtn = document.getElementById('adminBridgeTablesRemove');
adminLoadBridgeBtn = document.getElementById('btn-admin-load-bridge');
sectionsList = document.getElementById('sectionsList');
sectionsCreateForm = document.getElementById('sectionsCreateForm');
sectionNameInput = document.getElementById('sectionNameInput');
sectionsDeleteDialog = document.getElementById('sectionsDeleteDialog');
sectionsDeleteForm = document.getElementById('sectionsDeleteForm');
sectionsDeleteTarget = document.getElementById('sectionsDeleteTarget');
sectionsDeleteText = document.getElementById('sectionsDeleteText');
sectionsDeleteCancel = document.getElementById('sectionsDeleteCancel');
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);
document.querySelectorAll('[data-user-tab]').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
});
if (settingsForm) {
settingsForm.querySelectorAll('button[data-rotate]').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.getAttribute('data-rotate');
if (type && state.rotate[type] !== undefined) {
state.rotate[type] = true;
toast('Token wird nach dem Speichern erneuert.', true, { duration: 2000 });
}
});
});
settingsForm.querySelectorAll('button[data-download]').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.getAttribute('data-download');
if (type) downloadFile(type);
});
});
}
adminTablesAddBtn?.addEventListener('click', () => {
addAdminTables(getSelectedOptions(adminTablesAllSelect));
});
adminTablesRemoveBtn?.addEventListener('click', () => {
removeAdminTables(getSelectedOptions(adminTablesSelectedSelect));
});
adminLoadBridgeBtn?.addEventListener('click', () => {
refreshBridgeTablesFromEndpoint();
});
initSectionsManager();
window.addEventListener('bridge-setup-updated', (ev) => {
const setup = ev?.detail || {};
refreshAdminTables(setup.tables || [], state.settings.bridge_tables || []);
});
switchTab(state.currentTab);
loadAccountData();
updateRoleVisibility();
}
function initSectionsManager() {
if (!sectionsList || !sectionsCreateForm || !sectionNameInput) return;
sectionsCreateForm.addEventListener('submit', async (ev) => {
ev.preventDefault();
const name = sectionNameInput.value.trim();
if (!name) return;
try {
const res = await apiAction('sections_config.create', { method: 'POST', data: { name } });
if (!res?.ok) throw new Error(res?.error || 'Erstellen fehlgeschlagen');
sectionNameInput.value = '';
await loadSectionsConfig();
toast('Section erstellt', true);
} catch (err) {
toast(err.message || 'Erstellen fehlgeschlagen', false);
}
});
sectionsDeleteCancel && (sectionsDeleteCancel.onclick = () => {
sectionsDeleteDialog?.close();
});
sectionsDeleteForm?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const id = Number(sectionsDeleteForm?.dataset?.sectionId || 0);
const target = Number(sectionsDeleteTarget?.value || 0);
if (!id || !target) return;
try {
const res = await apiAction('sections_config.delete', { method: 'POST', data: { id, move_to: target } });
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
sectionsDeleteDialog?.close();
await loadSectionsConfig();
toast('Section gelöscht', true);
} catch (err) {
toast(err.message || 'Löschen fehlgeschlagen', false);
}
});
loadSectionsConfig();
}
async function loadSectionsConfig() {
try {
const res = await apiAction('sections_config.list', { method: 'GET' });
const items = Array.isArray(res?.items) ? res.items : [];
renderSectionsList(items);
} catch (err) {
toast(err.message || 'Sections konnten nicht geladen werden', false);
}
}
function renderSectionsList(items) {
if (!sectionsList) return;
const rows = items || [];
sectionsList.innerHTML = rows.map((item) => {
const isTemplate = Number(item.is_template) === 1;
const dragAttr = isTemplate ? '' : 'draggable="true"';
const badge = isTemplate ? '<span class="text-xs text-sky-700 bg-sky-100 px-2 py-0.5 rounded-full">Fix</span>' : '';
const editBtn = isTemplate ? '' : `<button type="button" class="btn text-xs" data-edit="${item.id}">Umbenennen</button>`;
const delBtn = isTemplate ? '' : `<button type="button" class="btn btn-danger text-xs" data-del="${item.id}">Löschen</button>`;
return `<li class="section-item flex items-center gap-3 border rounded-lg px-3 py-2 bg-white" data-id="${item.id}" ${dragAttr}>
<span class="cursor-grab text-slate-400 select-none">☰</span>
<div class="flex-1">
<div class="font-medium">${escapeHtml(item.name || '')}</div>
<div class="text-xs text-slate-500">${escapeHtml(item.slug || '')}</div>
</div>
${badge}
<div class="flex gap-2">${editBtn}${delBtn}</div>
</li>`;
}).join('');
sectionsList.querySelectorAll('[data-edit]').forEach(btn => btn.addEventListener('click', async () => {
const id = Number(btn.dataset.edit || 0);
const current = rows.find(r => Number(r.id) === id);
if (!current) return;
const next = prompt('Neuer Name', current.name || '');
if (!next || next.trim() === current.name) return;
try {
const res = await apiAction('sections_config.update', { method: 'POST', data: { id, name: next.trim() } });
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
await loadSectionsConfig();
toast('Section gespeichert', true);
} catch (err) {
toast(err.message || 'Speichern fehlgeschlagen', false);
}
}));
sectionsList.querySelectorAll('[data-del]').forEach(btn => btn.addEventListener('click', () => {
const id = Number(btn.dataset.del || 0);
const current = rows.find(r => Number(r.id) === id);
if (!current) return;
const targets = rows.filter(r => Number(r.id) !== id);
if (!targets.length) {
toast('Keine Ziel-Section verfügbar', false);
return;
}
if (sectionsDeleteText) {
sectionsDeleteText.textContent = `Section "${current.name}" löschen?`;
}
if (sectionsDeleteTarget) {
sectionsDeleteTarget.innerHTML = targets
.map(r => `<option value="${r.id}">${escapeHtml(r.name || '')}</option>`)
.join('');
}
if (sectionsDeleteForm) {
sectionsDeleteForm.dataset.sectionId = String(id);
}
sectionsDeleteDialog?.showModal?.();
}));
sectionsList.querySelectorAll('[draggable="true"]').forEach(item => {
item.addEventListener('dragstart', (ev) => {
sectionDragId = item.dataset.id || null;
ev.dataTransfer?.setData('text/plain', sectionDragId || '');
});
item.addEventListener('dragend', () => {
sectionDragId = null;
});
item.addEventListener('dragover', (ev) => {
ev.preventDefault();
});
item.addEventListener('drop', async (ev) => {
ev.preventDefault();
const targetId = item.dataset.id || null;
if (!sectionDragId || !targetId || sectionDragId === targetId) return;
const ids = Array.from(sectionsList.querySelectorAll('[data-id]')).map(el => el.getAttribute('data-id'));
const fromIndex = ids.indexOf(sectionDragId);
const toIndex = ids.indexOf(targetId);
if (fromIndex === -1 || toIndex === -1) return;
ids.splice(fromIndex, 1);
ids.splice(toIndex, 0, sectionDragId);
try {
const res = await apiAction('sections_config.reorder', { method: 'POST', data: { order: ids } });
if (!res?.ok) throw new Error(res?.error || 'Sortierung fehlgeschlagen');
await loadSectionsConfig();
toast('Sortierung gespeichert', true);
} catch (err) {
toast(err.message || 'Sortierung fehlgeschlagen', false);
} finally {
sectionDragId = null;
}
});
});
}
function isOwner() {
return (window.__currentUser?.role || '').toLowerCase() === 'owner';
}
function isAdmin() {
const role = (window.__currentUser?.role || '').toLowerCase();
return role === 'owner' || role === 'admin';
}
function enforcePageAccess() {
if (pageType !== 'admin') return;
if (isAdmin()) return;
toast('Kein Zugriff auf diesen Bereich', false, { duration: 2500 });
window.location.href = '/admin/profile.php';
}
function handleUserContextChange() {
updateAvatar();
updateRoleVisibility();
enforcePageAccess();
refreshDebugAccess();
}
function updateAvatar() {
const target = document.getElementById('userAvatar');
if (!target) return;
const name = window.__currentUser?.name || window.__currentUser?.email || '';
target.textContent = name ? name.trim().charAt(0).toUpperCase() : 'U';
}
function toggleUserMenu(ev) {
ev?.preventDefault();
if (!userMenuPanel || !avatarBtn) return;
menuOpen = !menuOpen;
userMenuPanel.classList.toggle('hidden', !menuOpen);
avatarBtn.setAttribute('aria-expanded', menuOpen ? 'true' : 'false');
}
function closeUserMenu() {
if (!menuOpen) return;
menuOpen = false;
if (userMenuPanel) userMenuPanel.classList.add('hidden');
if (avatarBtn) avatarBtn.setAttribute('aria-expanded', 'false');
}
function handleDocumentClick(ev) {
if (!userMenuPanel || !avatarBtn || !menuOpen) return;
const target = ev.target;
if (avatarBtn.contains(target) || userMenuPanel.contains(target)) return;
closeUserMenu();
}
function handleMenuKeydown(ev) {
if (ev.key === 'Escape') {
closeUserMenu();
}
}
function handleMenuItemClick(ev) {
const item = ev.target.closest('.user-menu-item');
if (!item) return;
closeUserMenu();
}
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());
});
}
function switchTab(tab) {
if (!tab) return;
state.currentTab = tab;
document.querySelectorAll('[data-user-panel]').forEach(panel => {
panel.classList.toggle('hidden', panel.getAttribute('data-user-panel') !== tab);
});
document.querySelectorAll('[data-user-tab]').forEach(btn => {
const isActive = btn.getAttribute('data-user-tab') === tab;
btn.classList.toggle('bg-sky-50', isActive);
btn.classList.toggle('text-sky-700', isActive);
});
}
async function loadAccountData() {
try {
state.loading = true;
const res = await apiAction('account.profile.get', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Profil konnte nicht geladen werden');
if (res.user) {
window.__currentUser = res.user;
handleUserContextChange();
}
fillProfileForm(res.user);
fillSettingsForm(res.settings || {});
if (teamTable && isOwner()) {
await loadUsers();
}
if (senderTable) {
if (isAdmin()) {
await loadSenders();
} else {
state.senders = [];
state.senderMap = new Map();
renderSenderList();
}
}
reportViewDebugInfo(res);
} catch (err) {
console.error(err);
toast(err.message || 'Fehler beim Laden', false);
} finally {
state.loading = false;
}
}
function fillProfileForm(user) {
if (!profileForm) return;
profileForm.name.value = user?.name || '';
profileForm.email.value = user?.email || '';
}
function fillSettingsForm(settings) {
state.settings = settings;
window.__editorDefault = settings.editor_default || 'grapesjs';
window.__listSortDefault = settings.list_sort || 'created_asc';
state.rotate = { bridge: false, sender: false, external: false };
if (!settingsForm) return;
if (settingsForm.bridge_url) settingsForm.bridge_url.value = settings.bridge_url || '';
if (settingsForm.bridge_token) settingsForm.bridge_token.value = settings.bridge_token || '';
if (settingsForm.sender_token) settingsForm.sender_token.value = settings.sender_token || '';
if (settingsForm.external_api_token) settingsForm.external_api_token.value = settings.external_api_token || '';
if (settingsForm.editor_default) settingsForm.editor_default.value = settings.editor_default || 'grapesjs';
if (settingsForm.versions_retention) {
const retention = Number.isFinite(Number(settings.versions_retention))
? Number(settings.versions_retention)
: 0;
settingsForm.versions_retention.value = String(Math.max(0, retention));
}
refreshAdminTables(settings.bridge_setup?.tables || [], settings.bridge_tables || []);
}
async function submitProfileForm(ev) {
ev.preventDefault();
const data = {
name: profileForm.name.value.trim(),
email: profileForm.email.value.trim(),
};
try {
const res = await apiAction('account.profile.update', { method: 'POST', data });
if (!res?.ok) throw new Error(res?.error || 'Profil konnte nicht gespeichert werden');
window.__currentUser = res.user;
handleUserContextChange();
toast('Profil aktualisiert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function submitPasswordForm(ev) {
ev.preventDefault();
const data = {
current_password: passwordForm.current_password.value,
new_password: passwordForm.new_password.value,
};
try {
const res = await apiAction('account.password.update', { method: 'POST', data });
if (!res?.ok) throw new Error(res?.error || 'Passwort konnte nicht geändert werden');
passwordForm.reset();
toast('Passwort aktualisiert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function submitSettingsForm(ev) {
ev.preventDefault();
const data = {
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
rotate_sender_token: state.rotate.sender ? 1 : 0,
rotate_external_token: state.rotate.external ? 1 : 0,
};
if (settingsForm.bridge_url) data.bridge_url = settingsForm.bridge_url.value.trim();
if (settingsForm.bridge_token) data.bridge_token = settingsForm.bridge_token.value.trim();
if (settingsForm.sender_token) data.sender_token = settingsForm.sender_token.value.trim();
if (settingsForm.external_api_token) data.external_api_token = settingsForm.external_api_token.value.trim();
if (settingsForm.editor_default) data.editor_default = settingsForm.editor_default.value;
if (settingsForm.versions_retention) {
const raw = settingsForm.versions_retention.value.trim();
const parsed = raw === '' ? 0 : Number(raw);
data.versions_retention = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0;
}
if (adminTablesAllSelect && adminTablesSelectedSelect) {
const bridgeTables = normalizeTableList(state.settings.bridge_tables || []);
data.bridge_tables = bridgeTables;
}
try {
const res = await apiAction('account.settings.update', { method: 'POST', data });
if (!res?.ok) throw new Error(res?.error || 'Einstellungen konnten nicht gespeichert werden');
fillSettingsForm(res.settings || {});
toast('Integrationen gespeichert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function downloadFile(type) {
try {
const action = type === 'sender' ? 'downloads.sender' : 'downloads.bridge';
const res = await apiAction(action, { method: 'POST', data: {} });
if (!res?.ok || !res.content) throw new Error(res?.error || 'Download fehlgeschlagen');
const bytes = atob(res.content);
const buffer = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) buffer[i] = bytes.charCodeAt(i);
const blob = new Blob([buffer], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = res.file_name || `${type}.php`;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (err) {
toast(err.message || 'Download fehlgeschlagen', false);
}
}
function normalizeTableList(input) {
const items = Array.isArray(input) ? input : (typeof input === 'string' ? input.split(/[\s,]+/) : []);
const result = [];
const seen = new Set();
items.forEach(entry => {
let name = '';
if (typeof entry === 'string') {
name = entry;
} else if (entry && typeof entry === 'object') {
name = entry.name || entry.table || entry.label || '';
}
name = String(name || '').trim();
if (name && !seen.has(name)) {
seen.add(name);
result.push(name);
}
});
return result;
}
function refreshAdminTables(availableTables, selectedTables) {
const whitelist = normalizeTableList(availableTables);
let selected = normalizeTableList(selectedTables);
if (!selected.length) {
selected = whitelist.slice();
}
if (whitelist.length) {
selected = selected.filter(name => whitelist.includes(name));
}
state.settings.bridge_tables = selected;
state.settings.bridge_setup = state.settings.bridge_setup || {};
state.settings.bridge_setup.tables = whitelist;
updateAdminTableSelects(whitelist, selected);
}
function updateAdminTableSelects(availableTables, selectedTables) {
const selectedSet = new Set(selectedTables);
const available = availableTables.filter(name => !selectedSet.has(name));
renderSelect(adminTablesAllSelect, available, 'Keine Tabellen freigegeben.');
renderSelect(adminTablesSelectedSelect, selectedTables, 'Noch keine Tabellen ausgewaehlt.');
}
function renderSelect(selectEl, list, emptyLabel) {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!list.length) {
const opt = document.createElement('option');
opt.textContent = emptyLabel;
opt.disabled = true;
selectEl.appendChild(opt);
return;
}
list.forEach(name => {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
selectEl.appendChild(opt);
});
}
function getSelectedOptions(selectEl) {
if (!selectEl) return [];
return Array.from(selectEl.selectedOptions || []).map(opt => opt.value);
}
function addAdminTables(list) {
const whitelist = normalizeTableList(state.settings.bridge_setup?.tables || []);
if (!whitelist.length) return;
const selected = normalizeTableList(state.settings.bridge_tables || []);
const merged = normalizeTableList([...selected, ...list]).filter(name => whitelist.includes(name));
state.settings.bridge_tables = merged;
updateAdminTableSelects(whitelist, merged);
}
async function refreshBridgeTablesFromEndpoint() {
if (state.loading) return;
state.loading = true;
try {
const res = await apiAction('account.bridge.test', { method: 'POST', data: {} });
if (!res?.ok) throw new Error(res?.error || 'Bridge konnte nicht abgefragt werden');
const fetched = normalizeTableList(res.tables || []);
if (!fetched.length) {
toast('Keine Tabellen vom Bridge-Endpunkt erhalten', false);
return;
}
const selected = normalizeTableList(state.settings.bridge_tables || []);
const selectedSet = new Set(selected);
const nextSelected = fetched.filter(name => selectedSet.has(name));
state.settings.bridge_setup = state.settings.bridge_setup || {};
state.settings.bridge_setup.tables = fetched;
state.settings.bridge_tables = nextSelected;
updateAdminTableSelects(fetched, nextSelected);
toast('Tabellen aktualisiert', true);
} catch (err) {
toast(err.message || 'Bridge konnte nicht geprüft werden', false);
} finally {
state.loading = false;
}
}
function removeAdminTables(list) {
const whitelist = normalizeTableList(state.settings.bridge_setup?.tables || []);
if (!whitelist.length) return;
const removeSet = new Set(list);
const next = normalizeTableList(state.settings.bridge_tables || []).filter(name => !removeSet.has(name));
state.settings.bridge_tables = next;
updateAdminTableSelects(whitelist, next);
}
async function loadUsers() {
try {
const res = await apiAction('account.users.list', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Team konnte nicht geladen werden');
state.users = res.items || [];
state.userMap = new Map(state.users.map(u => [u.id, u]));
renderUserList();
} catch (err) {
toast(err.message || 'Fehler beim Laden der Nutzer', false);
}
}
function renderUserList() {
if (!teamTable) return;
const tbody = teamTable.querySelector('tbody');
if (!tbody) return;
if (!state.users.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-sm text-slate-500">Keine Nutzer vorhanden.</td></tr>';
return;
}
tbody.innerHTML = state.users.map(user => {
const badge = user.is_active ? '<span class="badge bg-green-100 text-green-800">Aktiv</span>' : '<span class="badge bg-slate-200 text-slate-700">Inaktiv</span>';
return `<tr>
<td>${escapeHtml(user.name)}</td>
<td>${escapeHtml(user.email)}</td>
<td>${escapeHtml(user.role)}</td>
<td>${badge}</td>
<td class="text-right flex gap-2 justify-end">
<button class="btn" data-user-action="edit" data-user-id="${user.id}">Bearbeiten</button>
<button class="btn" data-user-action="reset" data-user-id="${user.id}">Passwort</button>
<button class="btn btn-danger" data-user-action="delete" data-user-id="${user.id}">Löschen</button>
</td>
</tr>`;
}).join('');
}
function handleTeamTableClick(ev) {
const btn = ev.target.closest('button[data-user-action]');
if (!btn) return;
const id = Number(btn.getAttribute('data-user-id'));
const action = btn.getAttribute('data-user-action');
const user = state.userMap.get(id);
if (!user) return;
if (action === 'edit') {
openUserForm(user);
} else if (action === 'delete') {
if (confirm(`Soll ${user.name} wirklich entfernt werden?`)) deleteUser(id);
} else if (action === 'reset') {
openUserForm(user, true);
}
}
function openUserForm(user = null, resetOnly = false) {
if (!userForm) return;
userForm.classList.remove('hidden');
userForm.user_id.value = user?.id || '';
userForm.name.value = user?.name || '';
userForm.email.value = user?.email || '';
userForm.role.value = user?.role || 'user';
userForm.is_active.checked = user ? !!user.is_active : true;
const resetRow = userForm.querySelector('.reset-only');
if (resetRow) resetRow.classList.toggle('hidden', !user);
if (resetRow) resetRow.querySelector('input').checked = resetOnly;
}
function closeUserForm() {
if (!userForm) return;
userForm.classList.add('hidden');
userForm.reset();
userForm.user_id.value = '';
const resetInput = userForm.querySelector('.reset-only input');
if (resetInput) resetInput.checked = false;
}
async function submitUserForm(ev) {
ev.preventDefault();
const formData = new FormData(userForm);
const payload = {
name: formData.get('name')?.toString().trim() || '',
email: formData.get('email')?.toString().trim() || '',
role: formData.get('role')?.toString() || 'user',
is_active: userForm.is_active.checked ? 1 : 0,
};
const userId = formData.get('user_id')?.toString();
let action = 'account.users.create';
if (userId) {
action = 'account.users.update';
payload.user_id = Number(userId);
payload.reset_password = userForm.querySelector('input[name="reset_password"]').checked ? 1 : 0;
}
try {
const res = await apiAction(action, { method: 'POST', data: payload });
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
closeUserForm();
await loadUsers();
toast('Nutzer gespeichert', true);
if (res.temp_password) {
copyToClipboard(res.temp_password);
alert(`Neues Passwort: ${res.temp_password}`);
}
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function deleteUser(userId) {
try {
const res = await apiAction('account.users.delete', { method: 'POST', data: { user_id: userId } });
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
await loadUsers();
toast('Nutzer gelöscht', true);
} catch (err) {
toast(err.message || 'Fehler beim Löschen', false);
}
}
function copyToClipboard(value) {
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(value).catch(() => {});
}
}
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;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function refreshDebugAccess() {
const isStaging = DEBUG_ENV === 'staging';
const allowed = isStaging && (window.__currentUser?.email || '').toLowerCase() === DEBUG_EMAIL;
if (!allowed) {
debugButton?.remove();
debugButton = null;
if (debugDialog?.open) {
debugDialog.close();
}
return;
}
ensureDebugStyles();
ensureConsoleCapture();
ensureDebugDialog();
if (!debugButton) {
debugButton = document.createElement('button');
debugButton.id = 'debugToggleButton';
debugButton.className = 'debug-floating-btn';
debugButton.type = 'button';
debugButton.textContent = 'Debug';
debugButton.addEventListener('click', () => openDebugDialog('php'));
document.body.appendChild(debugButton);
}
}
function ensureDebugStyles() {
if (debugStylesInjected) return;
const style = document.createElement('style');
style.id = 'debugStyles';
style.textContent = `
.debug-floating-btn{position:fixed;left:16px;bottom:16px;padding:.5rem 1rem;border-radius:999px;border:none;background:#0ea5e9;color:#fff;font-weight:600;box-shadow:0 10px 25px rgba(15,23,42,.25);z-index:60;cursor:pointer}
.debug-floating-btn:hover{background:#0284c7}
dialog#debugDialog::backdrop{background:rgba(15,23,42,.45)}
#debugDialog{border:none;border-radius:1rem;padding:0;width:80vw;max-width:960px}
.debug-shell{display:flex;flex-direction:column;height:80vh}
.debug-header{display:flex;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid #e2e8f0}
.debug-tabs{display:flex;border-bottom:1px solid #e2e8f0}
.debug-tabs button{flex:1;padding:.75rem 1rem;border:none;background:transparent;cursor:pointer;font-weight:600}
.debug-tabs button.active{background:#e0f2fe;color:#0c4a6e}
.debug-panel{flex:1;overflow:auto;padding:1rem;background:#f8fafc}
#debugConsoleContent{font-family:monospace;font-size:.85rem;white-space:pre-wrap}
.debug-console-entry{margin-bottom:.35rem}
.debug-console-entry.log{color:#15803d}
.debug-console-entry.warn{color:#b45309}
.debug-console-entry.error{color:#b91c1c}
.debug-logs-grid{display:grid;grid-template-columns:220px 1fr;gap:12px;height:100%}
.debug-logs-list{background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;padding:.5rem;overflow:auto}
.debug-logs-list button{width:100%;text-align:left;border:none;background:transparent;padding:.35rem .5rem;border-radius:.5rem;cursor:pointer}
.debug-logs-list button.active{background:#e0f2fe;color:#0c4a6e}
.debug-logs-detail{background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;padding:.75rem;overflow:auto;font-family:monospace;font-size:.85rem;white-space:pre-wrap}
`;
document.head.appendChild(style);
debugStylesInjected = true;
}
function ensureDebugDialog() {
if (debugDialog) return;
debugDialog = document.createElement('dialog');
debugDialog.id = 'debugDialog';
debugDialog.innerHTML = `
<div class="debug-shell bg-white rounded-2xl shadow-2xl">
<div class="debug-header">
<strong>Debug Tools</strong>
<button type="button" class="btn" data-debug-close>Schließen</button>
</div>
<div class="debug-tabs">
<button type="button" data-debug-tab="php" class="active">PHP Debug</button>
<button type="button" data-debug-tab="console">Console</button>
<button type="button" data-debug-tab="logs">Logs</button>
</div>
<div class="debug-panel" data-debug-panel="php">
<div id="debugPhpContent" class="text-sm text-slate-700">Lade Daten…</div>
</div>
<div class="debug-panel hidden" data-debug-panel="console">
<pre id="debugConsoleContent"></pre>
</div>
<div class="debug-panel hidden" data-debug-panel="logs">
<div class="debug-logs-grid">
<div class="debug-logs-list" id="debugLogsList">Keine Logs geladen.</div>
<div class="debug-logs-detail" id="debugLogDetail">Bitte Log auswaehlen.</div>
</div>
<div class="mt-3 flex justify-end">
<button type="button" class="btn" id="debugLogsRefresh">Logs aktualisieren</button>
</div>
</div>
</div>`;
document.body.appendChild(debugDialog);
phpInfoContainer = debugDialog.querySelector('#debugPhpContent');
consoleContainer = debugDialog.querySelector('#debugConsoleContent');
logsListContainer = debugDialog.querySelector('#debugLogsList');
logDetailContainer = debugDialog.querySelector('#debugLogDetail');
debugDialog.querySelector('#debugLogsRefresh')?.addEventListener('click', () => loadDebugLogs(true));
debugDialog.querySelector('[data-debug-close]')?.addEventListener('click', () => closeDebugDialog());
debugDialog.addEventListener('close', () => setDebugTab('php'));
debugDialog.querySelectorAll('[data-debug-tab]').forEach(btn => {
btn.addEventListener('click', () => setDebugTab(btn.getAttribute('data-debug-tab')));
});
}
function openDebugDialog(tab = 'php') {
ensureDebugDialog();
setDebugTab(tab);
if (!debugDialog.open) debugDialog.showModal();
if (tab === 'php') {
loadPhpInfo();
} else {
renderConsolePanel();
}
}
function closeDebugDialog() {
if (debugLogsRefreshTimer) {
clearInterval(debugLogsRefreshTimer);
debugLogsRefreshTimer = null;
}
if (debugDialog?.open) debugDialog.close();
}
function setDebugTab(tab) {
if (!debugDialog) return;
debugActiveTab = tab || 'php';
debugDialog.querySelectorAll('[data-debug-tab]').forEach(btn => {
const isActive = btn.getAttribute('data-debug-tab') === debugActiveTab;
btn.classList.toggle('active', isActive);
});
debugDialog.querySelectorAll('[data-debug-panel]').forEach(panel => {
panel.classList.toggle('hidden', panel.getAttribute('data-debug-panel') !== debugActiveTab);
});
if (debugActiveTab === 'php') {
loadPhpInfo();
} else if (debugActiveTab === 'console') {
renderConsolePanel();
} else if (debugActiveTab === 'logs') {
loadDebugLogs(true);
if (!debugLogsRefreshTimer) {
debugLogsRefreshTimer = setInterval(() => {
if (debugDialog?.open && debugActiveTab === 'logs') {
loadDebugLogs(true);
}
}, 5000);
}
} else {
if (debugLogsRefreshTimer) {
clearInterval(debugLogsRefreshTimer);
debugLogsRefreshTimer = null;
}
}
}
async function loadPhpInfo() {
if (debugPhpLoaded || debugPhpLoading || !phpInfoContainer) return;
debugPhpLoading = true;
phpInfoContainer.textContent = 'Lade phpinfo…';
try {
const res = await apiAction('debug.phpinfo', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Fehler beim Laden');
const frame = document.createElement('iframe');
frame.style.width = '100%';
frame.style.height = '100%';
frame.style.minHeight = '400px';
frame.style.border = 'none';
frame.srcdoc = res.html || '<p>Keine Daten</p>';
phpInfoContainer.innerHTML = '';
phpInfoContainer.appendChild(frame);
debugPhpLoaded = true;
} catch (err) {
phpInfoContainer.textContent = err.message || 'Fehler beim Laden';
} finally {
debugPhpLoading = false;
}
}
function renderConsolePanel() {
if (!consoleContainer) return;
if (!consoleBuffer.length) {
consoleContainer.textContent = 'Noch keine Konsolenmeldungen in dieser Sitzung.';
return;
}
const lines = consoleBuffer.map(entry => {
const time = entry.time.toLocaleTimeString();
return `<div class="debug-console-entry ${entry.type}">[${time}] ${escapeHtml(entry.text)}</div>`;
});
consoleContainer.innerHTML = lines.join('');
}
async function loadDebugLogs(force = false) {
if ((!force && debugLogsLoaded) || !logsListContainer || !logDetailContainer) return;
logsListContainer.textContent = 'Lade Logs…';
if (!debugSelectedLogName) {
logDetailContainer.textContent = 'Bitte Log auswaehlen.';
}
try {
const res = await apiAction('debug.logs.list', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Logs konnten nicht geladen werden');
const items = Array.isArray(res.items) ? res.items : [];
if (!items.length) {
logsListContainer.textContent = 'Keine Logs vorhanden.';
return;
}
logsListContainer.innerHTML = '';
items.forEach((item, idx) => {
const name = item.name || item.file || `Log ${idx + 1}`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = name;
btn.addEventListener('click', () => {
logsListContainer.querySelectorAll('button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
debugSelectedLogName = name;
loadDebugLogFile(name);
});
logsListContainer.appendChild(btn);
if (debugSelectedLogName && debugSelectedLogName === name) {
btn.classList.add('active');
}
});
if (!debugSelectedLogName) {
const first = logsListContainer.querySelector('button');
if (first) first.click();
} else {
const active = logsListContainer.querySelector('button.active');
if (!active) {
const first = logsListContainer.querySelector('button');
if (first) first.click();
}
}
debugLogsLoaded = true;
} catch (err) {
logsListContainer.textContent = err.message || 'Logs konnten nicht geladen werden';
}
}
async function loadDebugLogFile(name) {
if (!name || !logDetailContainer) return;
logDetailContainer.textContent = 'Lade Log…';
try {
const res = await apiAction('debug.logs.read', { method: 'GET', data: { name } });
if (!res?.ok) throw new Error(res?.error || 'Log konnte nicht geladen werden');
logDetailContainer.textContent = res.content || '(leer)';
} catch (err) {
logDetailContainer.textContent = err.message || 'Log konnte nicht geladen werden';
}
}
function ensureConsoleCapture() {
if (consolePatched) return;
['log', 'warn', 'error'].forEach(type => {
const original = console[type];
console[type] = function (...args) {
appendConsoleMessage(type, args);
if (typeof original === 'function') {
original.apply(console, args);
}
};
});
consolePatched = true;
}
function appendConsoleMessage(type, args) {
const text = args.map(formatConsoleArg).join(' ');
consoleBuffer.push({ type, text, time: new Date() });
if (consoleBuffer.length > MAX_CONSOLE_LINES) consoleBuffer.shift();
if (debugActiveTab === 'console' && debugDialog?.open) {
renderConsolePanel();
}
}
function formatConsoleArg(arg) {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (err) {
return String(arg);
}
}
function reportViewDebugInfo(apiResponse) {
const role = (window.__currentUser?.role || '').toLowerCase() || 'unknown';
const expected = {
profile: true,
dashboard: role === 'owner' || role === 'admin',
administration: role === 'owner' || role === 'admin',
downloads: role === 'owner',
};
const summary = {
hasUser: !!apiResponse?.user,
userRole: apiResponse?.user?.role ?? null,
settingsKeys: apiResponse?.settings ? Object.keys(apiResponse.settings) : [],
usersCount: Array.isArray(state.users) ? state.users.length : null,
sendersCount: Array.isArray(state.senders) ? state.senders.length : null,
requestedTab: state.currentTab,
};
console.log('[view-debug]', { expectedViews: expected, role, apiSummary: summary });
}