Files
emailtemplate.it/public/assets/js/ui-user.js
2025-12-08 00:07:43 +01:00

594 lines
20 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, 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';
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;
export function initUserPanel() {
avatarBtn = document.getElementById('btn-user');
userMenuPanel = document.getElementById('userMenuPanel');
updateAvatar();
updateRoleVisibility();
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 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;
updateAvatar();
updateRoleVisibility();
enforcePageAccess();
}
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();
}
}
} 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;
updateAvatar();
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}