813 lines
28 KiB
JavaScript
813 lines
28 KiB
JavaScript
import { apiAction, toast } from './api.js';
|
||
|
||
const state = {
|
||
settings: {},
|
||
rotate: { bridge: false, sender: false, external: false },
|
||
users: [],
|
||
userMap: new Map(),
|
||
senders: [],
|
||
senderMap: new Map(),
|
||
bridgeTables: [],
|
||
currentTab: 'profile',
|
||
loading: false,
|
||
};
|
||
|
||
const pageType = document.body?.dataset?.page || 'account';
|
||
const DEBUG_EMAIL = 'madmin@papa-kind-treff.info';
|
||
const MAX_CONSOLE_LINES = 200;
|
||
|
||
let avatarBtn;
|
||
let userMenuPanel;
|
||
let profileForm;
|
||
let passwordForm;
|
||
let settingsForm;
|
||
let teamTable;
|
||
let userForm;
|
||
let senderTable;
|
||
let senderForm;
|
||
let bridgePreview;
|
||
let validateBridgeBtn;
|
||
let menuInitialized = false;
|
||
let menuOpen = false;
|
||
let debugButton;
|
||
let debugDialog;
|
||
let debugPhpLoaded = false;
|
||
let debugPhpLoading = false;
|
||
let debugActiveTab = 'php';
|
||
let phpInfoContainer;
|
||
let consoleContainer;
|
||
let debugStylesInjected = false;
|
||
let consolePatched = false;
|
||
const consoleBuffer = [];
|
||
|
||
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');
|
||
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')));
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
}
|
||
|
||
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 enforcePageAccess() {
|
||
if (pageType !== 'admin') return;
|
||
if (isAdmin()) return;
|
||
toast('Kein Zugriff auf diesen Bereich', false, { duration: 2500 });
|
||
window.location.href = '/account.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;
|
||
if (!settingsForm) return;
|
||
settingsForm.bridge_url.value = settings.bridge_url || '';
|
||
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 };
|
||
}
|
||
|
||
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 = {
|
||
bridge_url: settingsForm.bridge_url.value.trim(),
|
||
bridge_token: settingsForm.bridge_token.value.trim(),
|
||
sender_token: settingsForm.sender_token.value.trim(),
|
||
external_api_token: settingsForm.external_api_token.value.trim(),
|
||
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 });
|
||
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 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' });
|
||
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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function refreshDebugAccess() {
|
||
const allowed = (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}
|
||
`;
|
||
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>
|
||
</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>`;
|
||
document.body.appendChild(debugDialog);
|
||
phpInfoContainer = debugDialog.querySelector('#debugPhpContent');
|
||
consoleContainer = debugDialog.querySelector('#debugConsoleContent');
|
||
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 (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 {
|
||
renderConsolePanel();
|
||
}
|
||
}
|
||
|
||
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('');
|
||
}
|
||
|
||
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 });
|
||
}
|