This commit is contained in:
2025-12-08 00:03:23 +01:00
parent 3e7d438ab6
commit 352ad4c512
8 changed files with 671 additions and 149 deletions

View File

@@ -2,6 +2,8 @@ import { apiAction } from './api.js';
import { initUserPanel, initAccountPage } from './ui-user.js';
import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
const pageType = document.body?.dataset?.page || 'account';
async function ensureAuthenticated() {
try {
const me = await apiAction('auth.me', { method: 'GET' });
@@ -17,9 +19,19 @@ async function ensureAuthenticated() {
}
}
function ensureAccess() {
const role = (window.__currentUser?.role || '').toLowerCase();
if (pageType === 'admin' && role !== 'owner' && role !== 'admin') {
window.location.href = '/account.php';
return false;
}
return true;
}
document.addEventListener('DOMContentLoaded', async () => {
const ok = await ensureAuthenticated();
if (!ok) return;
if (!ensureAccess()) return;
initUserPanel();
initAccountPage();
mountLogoutButton('#btn-logout', { redirect: '/login.php' });

View File

@@ -0,0 +1,140 @@
import { apiAction, toast } from './api.js';
import { initUserPanel } from './ui-user.js';
import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
const state = {
counts: { templates: 0, sections: 0, blocks: 0, snippets: 0, renders_total: 0 },
usage: [],
};
async function ensureAuthenticated() {
try {
const me = await apiAction('auth.me', { method: 'GET' });
if (!me?.ok || !me?.user) {
window.location.href = '/login.php';
return false;
}
window.__currentUser = me.user;
document.documentElement.classList.remove('auth-pending');
return true;
} catch {
return false;
}
}
function ensureAccess() {
const role = (window.__currentUser?.role || '').toLowerCase();
if (role !== 'owner' && role !== 'admin') {
toast('Kein Zugriff auf das Dashboard', false);
window.location.href = '/account.php';
return false;
}
return true;
}
function renderCounts(counts) {
const mapping = {
templates: 'count-templates',
sections: 'count-sections',
blocks: 'count-blocks',
snippets: 'count-snippets',
renders_total: 'count-usage',
};
Object.entries(mapping).forEach(([key, id]) => {
const el = document.getElementById(id);
if (!el) return;
const value = counts[key] ?? 0;
el.textContent = typeof value === 'number' ? value.toLocaleString('de-DE') : value;
});
}
function formatDate(value) {
if (!value) return '';
try {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString('de-DE');
} catch {
return value;
}
}
function renderUsage(list) {
const table = document.getElementById('usageTable');
if (!table) return;
const tbody = table.querySelector('tbody');
if (!tbody) return;
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="4" class="text-sm text-slate-500">Noch keine Daten vorhanden.</td></tr>';
return;
}
tbody.innerHTML = list.map(item => `
<tr data-template-id="${item.template_id}">
<td>${escapeHtml(item.name)}</td>
<td>${item.render_count.toLocaleString('de-DE')}</td>
<td>${escapeHtml(formatDate(item.last_rendered_at || item.updated_at))}</td>
<td class="text-right">
<button type="button" class="btn" data-reset="${item.template_id}">Zähler zurücksetzen</button>
</td>
</tr>
`).join('');
}
function escapeHtml(str) {
return String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
async function loadMetrics() {
try {
const res = await apiAction('dashboard.metrics', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Dashboard konnte nicht geladen werden');
state.counts = res.counts || state.counts;
state.usage = Array.isArray(res.usage) ? res.usage : [];
renderCounts(state.counts);
renderUsage(state.usage);
} catch (err) {
toast(err.message || 'Fehler beim Laden', false);
}
}
async function resetUsage(templateId) {
try {
await apiAction('dashboard.reset_usage', { method: 'POST', data: { template_id: templateId } });
toast('Zähler zurückgesetzt', true);
await loadMetrics();
} catch (err) {
toast(err.message || 'Zurücksetzen fehlgeschlagen', false);
}
}
function bindEvents() {
const refresh = document.getElementById('btn-refresh-dashboard');
refresh?.addEventListener('click', () => loadMetrics());
const table = document.getElementById('usageTable');
table?.addEventListener('click', ev => {
const btn = ev.target.closest('button[data-reset]');
if (!btn) return;
const id = Number(btn.getAttribute('data-reset'));
if (!id) return;
if (confirm('Zähler für dieses Template wirklich löschen?')) {
resetUsage(id);
}
});
}
document.addEventListener('DOMContentLoaded', async () => {
const ok = await ensureAuthenticated();
if (!ok) return;
if (!ensureAccess()) return;
initUserPanel();
bindEvents();
await loadMetrics();
mountLogoutButton('#btn-logout', { redirect: '/login.php' });
ensureFloatingLogout({ redirect: '/login.php' });
});

View File

@@ -13,6 +13,7 @@ const state = {
};
let avatarBtn;
let userMenuPanel;
let profileForm;
let passwordForm;
let settingsForm;
@@ -22,10 +23,21 @@ 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() {
@@ -58,22 +70,24 @@ export function initAccountPage() {
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
});
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 });
}
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);
settingsForm.querySelectorAll('button[data-download]').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.getAttribute('data-download');
if (type) downloadFile(type);
});
});
});
}
switchTab(state.currentTab);
loadAccountData();
@@ -96,6 +110,40 @@ function updateAvatar() {
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 => {
@@ -139,15 +187,17 @@ async function loadAccountData() {
}
fillProfileForm(res.user);
fillSettingsForm(res.settings || {});
if (isOwner()) {
if (teamTable && isOwner()) {
await loadUsers();
}
if (isAdmin()) {
await loadSenders();
} else {
state.senders = [];
state.senderMap = new Map();
renderSenderList();
if (senderTable) {
if (isAdmin()) {
await loadSenders();
} else {
state.senders = [];
state.senderMap = new Map();
renderSenderList();
}
}
} catch (err) {
console.error(err);