yxy
This commit is contained in:
@@ -28,6 +28,9 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
|||||||
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||||
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||||
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||||
|
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:180px;background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.35rem;z-index:40}
|
||||||
|
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.6rem;font-size:.9rem;color:#0f172a}
|
||||||
|
.user-menu-item:hover{background:#f1f5f9}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-50 text-slate-800" data-page="account">
|
<body class="bg-slate-50 text-slate-800" data-page="account">
|
||||||
@@ -36,8 +39,17 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
|||||||
<a href="./" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
|
<a href="./" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
|
||||||
<h1 class="font-semibold text-lg">Mein Konto</h1>
|
<h1 class="font-semibold text-lg">Mein Konto</h1>
|
||||||
<div class="ms-auto flex gap-2 items-center">
|
<div class="ms-auto flex gap-2 items-center">
|
||||||
<a id="btn-user" href="account.php" class="btn-avatar" aria-disabled="true"><span id="userAvatar">U</span></a>
|
<div class="relative" id="userMenu">
|
||||||
<button id="btn-logout" type="button" class="btn">Logout</button>
|
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span id="userAvatar">U</span>
|
||||||
|
</button>
|
||||||
|
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||||
|
<a href="account.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||||
|
<a href="dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||||
|
<a href="admin.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||||
|
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -46,9 +58,6 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
|||||||
<div class="user-tabs">
|
<div class="user-tabs">
|
||||||
<button type="button" data-user-tab="profile" class="btn bg-sky-50 text-sky-700 flex-1">Profil</button>
|
<button type="button" data-user-tab="profile" class="btn bg-sky-50 text-sky-700 flex-1">Profil</button>
|
||||||
<button type="button" data-user-tab="security" class="btn flex-1">Passwort</button>
|
<button type="button" data-user-tab="security" class="btn flex-1">Passwort</button>
|
||||||
<button type="button" data-user-tab="senders" class="btn flex-1 hidden" data-role="admin">Absender</button>
|
|
||||||
<button type="button" data-user-tab="team" class="btn flex-1 hidden" data-role="owner">Team</button>
|
|
||||||
<button type="button" data-user-tab="integration" class="btn flex-1 hidden" data-role="admin">Integrationen</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section data-user-panel="profile" class="section-card">
|
<section data-user-panel="profile" class="section-card">
|
||||||
@@ -81,126 +90,8 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section data-user-panel="team" class="section-card hidden" data-role="owner">
|
<section class="section-card">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<p class="text-sm text-slate-600">Teammitglieder, Absender und Integrationen verwaltest du jetzt im neuen Bereich <strong>Administration</strong>. Öffne ihn über das Avatar-Menü oben rechts.</p>
|
||||||
<h4>Team</h4>
|
|
||||||
<button type="button" id="btn-user-add" class="btn">+ Nutzer</button>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-auto">
|
|
||||||
<table class="team-table" id="teamTable">
|
|
||||||
<thead>
|
|
||||||
<tr><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th class="text-right">Aktionen</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<form id="userForm" class="space-y-3 mt-4 hidden">
|
|
||||||
<input type="hidden" name="user_id">
|
|
||||||
<label class="block text-sm text-slate-600">Name
|
|
||||||
<input type="text" name="name" class="input mt-1" required>
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">E-Mail
|
|
||||||
<input type="email" name="email" class="input mt-1" required>
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Rolle
|
|
||||||
<select name="role" class="input mt-1">
|
|
||||||
<option value="owner">Owner</option>
|
|
||||||
<option value="admin">Admin</option>
|
|
||||||
<option value="editor">Editor</option>
|
|
||||||
<option value="viewer">Viewer</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center gap-2 text-sm text-slate-600">
|
|
||||||
<input type="checkbox" name="is_active" checked> Aktiv
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center gap-2 text-sm text-slate-600 reset-only hidden">
|
|
||||||
<input type="checkbox" name="reset_password"> Passwort zurücksetzen
|
|
||||||
</label>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<button type="button" id="userFormCancel" class="btn">Abbrechen</button>
|
|
||||||
<button type="submit" class="btn">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section data-user-panel="senders" class="section-card hidden" data-role="admin">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<h4>Absender für Testmails</h4>
|
|
||||||
<button type="button" id="btn-sender-add" class="btn">+ Absender</button>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-auto">
|
|
||||||
<table class="team-table" id="senderTable">
|
|
||||||
<thead>
|
|
||||||
<tr><th>Bezeichnung</th><th>From-Name</th><th>E-Mail</th><th>Reply-To</th><th class="text-right">Aktionen</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<form id="senderForm" class="space-y-3 mt-4 hidden">
|
|
||||||
<input type="hidden" name="sender_id">
|
|
||||||
<label class="block text-sm text-slate-600">Bezeichnung
|
|
||||||
<input type="text" name="label" class="input mt-1" placeholder="Interner Name (optional)">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Absender-Name
|
|
||||||
<input type="text" name="from_name" class="input mt-1" placeholder="z.B. Newsletter Team">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Absender-E-Mail
|
|
||||||
<input type="email" name="from_email" class="input mt-1" required placeholder="news@example.com">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Reply-To (optional)
|
|
||||||
<input type="email" name="reply_to" class="input mt-1" placeholder="support@example.com">
|
|
||||||
</label>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<button type="button" id="senderFormCancel" class="btn">Abbrechen</button>
|
|
||||||
<button type="submit" class="btn">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section data-user-panel="integration" class="section-card hidden" data-role="admin">
|
|
||||||
<h4>Integrationen & Tokens</h4>
|
|
||||||
<form id="settingsForm" class="space-y-3">
|
|
||||||
<label class="block text-sm text-slate-600">Bridge-URL
|
|
||||||
<input type="url" name="bridge_url" class="input mt-1" placeholder="https://domain.tld/emailtemplate_bridge.php">
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-slate-600">Bridge Token</label>
|
|
||||||
<div class="flex gap-2 mt-1">
|
|
||||||
<input type="text" name="bridge_token" class="input" readonly>
|
|
||||||
<button type="button" class="btn" data-rotate="bridge">Neu erstellen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-slate-600">Sender Token</label>
|
|
||||||
<div class="flex gap-2 mt-1">
|
|
||||||
<input type="text" name="sender_token" class="input" readonly>
|
|
||||||
<button type="button" class="btn" data-rotate="sender">Neu erstellen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-slate-600">Externer API-Token</label>
|
|
||||||
<div class="flex gap-2 mt-1">
|
|
||||||
<input type="text" name="external_api_token" class="input" readonly>
|
|
||||||
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-slate-600">Verfügbare Tabellen (kommagetrennt oder Zeilen)</label>
|
|
||||||
<textarea name="bridge_tables" class="input mt-1" rows="3" placeholder="z.B. customers, orders"></textarea>
|
|
||||||
<p class="text-xs text-slate-500 mt-1">Nur diese Tabellen werden als Platzhalter angeboten. Leer = alle.</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<button type="button" id="btn-validate-bridge" class="btn w-max" data-role="admin">Verbindung prüfen & Tabellen laden</button>
|
|
||||||
<div id="bridgeTablesPreview" class="flex flex-wrap gap-2 text-sm text-slate-600">Noch nicht geprüft.</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between gap-2 flex-wrap pt-2">
|
|
||||||
<div class="flex gap-2" data-role="admin">
|
|
||||||
<button type="button" class="btn" data-download="bridge">Bridge-Datei</button>
|
|
||||||
<button type="button" class="btn" data-download="sender">Sender-Datei</button>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn ms-auto">Einstellungen speichern</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
185
public/admin.php
Normal file
185
public/admin.php
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
$base = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') ?: '';
|
||||||
|
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>Email Template System – Administration</title>
|
||||||
|
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||||
|
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||||
|
<link rel="stylesheet" href="assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light; }
|
||||||
|
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||||
|
.btn:hover{background:#f8fafc}
|
||||||
|
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||||
|
.btn-avatar:hover{background:#0284c7}
|
||||||
|
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||||
|
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||||
|
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||||
|
.team-table{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||||
|
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||||
|
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||||
|
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||||
|
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:180px;background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.35rem;z-index:40}
|
||||||
|
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.6rem;font-size:.9rem;color:#0f172a}
|
||||||
|
.user-menu-item:hover{background:#f1f5f9}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-50 text-slate-800" data-page="admin">
|
||||||
|
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
|
||||||
|
<div class="max-w-5xl mx-auto px-4 py-4 flex items-center gap-3">
|
||||||
|
<a href="./" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
|
||||||
|
<h1 class="font-semibold text-lg">Administration</h1>
|
||||||
|
<div class="ms-auto flex gap-2 items-center">
|
||||||
|
<div class="relative" id="userMenu">
|
||||||
|
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span id="userAvatar">U</span>
|
||||||
|
</button>
|
||||||
|
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||||
|
<a href="account.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||||
|
<a href="dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||||
|
<a href="admin.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||||
|
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="max-w-5xl mx-auto p-4 md:p-6 space-y-6">
|
||||||
|
<section class="section-card" data-role="owner">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h4>Team</h4>
|
||||||
|
<button type="button" id="btn-user-add" class="btn">+ Nutzer</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<table class="team-table" id="teamTable">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th class="text-right">Aktionen</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<form id="userForm" class="space-y-3 mt-4 hidden">
|
||||||
|
<input type="hidden" name="user_id">
|
||||||
|
<label class="block text-sm text-slate-600">Name
|
||||||
|
<input type="text" name="name" class="input mt-1" required>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">E-Mail
|
||||||
|
<input type="email" name="email" class="input mt-1" required>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Rolle
|
||||||
|
<select name="role" class="input mt-1">
|
||||||
|
<option value="owner">Owner</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="editor">Editor</option>
|
||||||
|
<option value="viewer">Viewer</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2 text-sm text-slate-600">
|
||||||
|
<input type="checkbox" name="is_active" checked> Aktiv
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2 text-sm text-slate-600 reset-only hidden">
|
||||||
|
<input type="checkbox" name="reset_password"> Passwort zurücksetzen
|
||||||
|
</label>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="button" id="userFormCancel" class="btn">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-card" data-role="admin">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h4>Absender für Testmails</h4>
|
||||||
|
<button type="button" id="btn-sender-add" class="btn">+ Absender</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<table class="team-table" id="senderTable">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Bezeichnung</th><th>From-Name</th><th>E-Mail</th><th>Reply-To</th><th class="text-right">Aktionen</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<form id="senderForm" class="space-y-3 mt-4 hidden">
|
||||||
|
<input type="hidden" name="sender_id">
|
||||||
|
<label class="block text-sm text-slate-600">Bezeichnung
|
||||||
|
<input type="text" name="label" class="input mt-1" placeholder="Interner Name (optional)">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Absender-Name
|
||||||
|
<input type="text" name="from_name" class="input mt-1" placeholder="z.B. Newsletter Team">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Absender-E-Mail
|
||||||
|
<input type="email" name="from_email" class="input mt-1" required placeholder="news@example.com">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Reply-To (optional)
|
||||||
|
<input type="email" name="reply_to" class="input mt-1" placeholder="support@example.com">
|
||||||
|
</label>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="button" id="senderFormCancel" class="btn">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-card" data-role="admin">
|
||||||
|
<h4>Integrationen, Downloads & Tokens</h4>
|
||||||
|
<p class="text-sm text-slate-600 mb-4">Die Dateien enthalten automatisch deine aktuellen Tokens. Nach dem Speichern neuer Tokens bitte die Dateien erneut herunterladen.</p>
|
||||||
|
<form id="settingsForm" class="space-y-3">
|
||||||
|
<label class="block text-sm text-slate-600">Bridge-URL
|
||||||
|
<input type="url" name="bridge_url" class="input mt-1" placeholder="https://domain.tld/emailtemplate_bridge.php">
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-600">Bridge Token</label>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<input type="text" name="bridge_token" class="input" readonly>
|
||||||
|
<button type="button" class="btn" data-rotate="bridge">Neu erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-600">Sender Token</label>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<input type="text" name="sender_token" class="input" readonly>
|
||||||
|
<button type="button" class="btn" data-rotate="sender">Neu erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-600">Externer API-Token</label>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<input type="text" name="external_api_token" class="input" readonly>
|
||||||
|
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-600">Verfügbare Tabellen (kommagetrennt oder Zeilen)</label>
|
||||||
|
<textarea name="bridge_tables" class="input mt-1" rows="3" placeholder="z.B. customers, orders"></textarea>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">Nur diese Tabellen werden als Platzhalter angeboten. Leer = alle.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<button type="button" id="btn-validate-bridge" class="btn w-max" data-role="admin">Verbindung prüfen & Tabellen laden</button>
|
||||||
|
<div id="bridgeTablesPreview" class="flex flex-wrap gap-2 text-sm text-slate-600">Noch nicht geprüft.</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-2 flex-wrap pt-2">
|
||||||
|
<div class="flex gap-2" data-role="admin">
|
||||||
|
<button type="button" class="btn" data-download="bridge">Bridge-Datei</button>
|
||||||
|
<button type="button" class="btn" data-download="sender">Sender-Datei</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn ms-auto">Einstellungen speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div id="toast-root"></div>
|
||||||
|
|
||||||
|
<script src="assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||||
|
<script type="module" src="assets/js/account.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,6 +2,8 @@ import { apiAction } from './api.js';
|
|||||||
import { initUserPanel, initAccountPage } from './ui-user.js';
|
import { initUserPanel, initAccountPage } from './ui-user.js';
|
||||||
import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
|
import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
|
||||||
|
|
||||||
|
const pageType = document.body?.dataset?.page || 'account';
|
||||||
|
|
||||||
async function ensureAuthenticated() {
|
async function ensureAuthenticated() {
|
||||||
try {
|
try {
|
||||||
const me = await apiAction('auth.me', { method: 'GET' });
|
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 () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const ok = await ensureAuthenticated();
|
const ok = await ensureAuthenticated();
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
if (!ensureAccess()) return;
|
||||||
initUserPanel();
|
initUserPanel();
|
||||||
initAccountPage();
|
initAccountPage();
|
||||||
mountLogoutButton('#btn-logout', { redirect: '/login.php' });
|
mountLogoutButton('#btn-logout', { redirect: '/login.php' });
|
||||||
|
|||||||
140
public/assets/js/dashboard.js
Normal file
140
public/assets/js/dashboard.js
Normal 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
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' });
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ const state = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let avatarBtn;
|
let avatarBtn;
|
||||||
|
let userMenuPanel;
|
||||||
let profileForm;
|
let profileForm;
|
||||||
let passwordForm;
|
let passwordForm;
|
||||||
let settingsForm;
|
let settingsForm;
|
||||||
@@ -22,10 +23,21 @@ let senderTable;
|
|||||||
let senderForm;
|
let senderForm;
|
||||||
let bridgePreview;
|
let bridgePreview;
|
||||||
let validateBridgeBtn;
|
let validateBridgeBtn;
|
||||||
|
let menuInitialized = false;
|
||||||
|
let menuOpen = false;
|
||||||
|
|
||||||
export function initUserPanel() {
|
export function initUserPanel() {
|
||||||
avatarBtn = document.getElementById('btn-user');
|
avatarBtn = document.getElementById('btn-user');
|
||||||
|
userMenuPanel = document.getElementById('userMenuPanel');
|
||||||
updateAvatar();
|
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() {
|
export function initAccountPage() {
|
||||||
@@ -58,22 +70,24 @@ export function initAccountPage() {
|
|||||||
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
|
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsForm?.querySelectorAll('button[data-rotate]').forEach(btn => {
|
if (settingsForm) {
|
||||||
btn.addEventListener('click', () => {
|
settingsForm.querySelectorAll('button[data-rotate]').forEach(btn => {
|
||||||
const type = btn.getAttribute('data-rotate');
|
btn.addEventListener('click', () => {
|
||||||
if (type && state.rotate[type] !== undefined) {
|
const type = btn.getAttribute('data-rotate');
|
||||||
state.rotate[type] = true;
|
if (type && state.rotate[type] !== undefined) {
|
||||||
toast('Token wird nach dem Speichern erneuert.', true, { duration: 2000 });
|
state.rotate[type] = true;
|
||||||
}
|
toast('Token wird nach dem Speichern erneuert.', true, { duration: 2000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
settingsForm?.querySelectorAll('button[data-download]').forEach(btn => {
|
settingsForm.querySelectorAll('button[data-download]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const type = btn.getAttribute('data-download');
|
const type = btn.getAttribute('data-download');
|
||||||
if (type) downloadFile(type);
|
if (type) downloadFile(type);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
switchTab(state.currentTab);
|
switchTab(state.currentTab);
|
||||||
loadAccountData();
|
loadAccountData();
|
||||||
@@ -96,6 +110,40 @@ function updateAvatar() {
|
|||||||
target.textContent = name ? name.trim().charAt(0).toUpperCase() : 'U';
|
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() {
|
function updateRoleVisibility() {
|
||||||
const role = (window.__currentUser?.role || '').toLowerCase();
|
const role = (window.__currentUser?.role || '').toLowerCase();
|
||||||
document.querySelectorAll('[data-role]').forEach(el => {
|
document.querySelectorAll('[data-role]').forEach(el => {
|
||||||
@@ -139,15 +187,17 @@ async function loadAccountData() {
|
|||||||
}
|
}
|
||||||
fillProfileForm(res.user);
|
fillProfileForm(res.user);
|
||||||
fillSettingsForm(res.settings || {});
|
fillSettingsForm(res.settings || {});
|
||||||
if (isOwner()) {
|
if (teamTable && isOwner()) {
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
}
|
}
|
||||||
if (isAdmin()) {
|
if (senderTable) {
|
||||||
await loadSenders();
|
if (isAdmin()) {
|
||||||
} else {
|
await loadSenders();
|
||||||
state.senders = [];
|
} else {
|
||||||
state.senderMap = new Map();
|
state.senders = [];
|
||||||
renderSenderList();
|
state.senderMap = new Map();
|
||||||
|
renderSenderList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
109
public/dashboard.php
Normal file
109
public/dashboard.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>Email Template System – Dashboard</title>
|
||||||
|
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||||
|
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||||
|
<link rel="stylesheet" href="assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light; }
|
||||||
|
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .8rem;border-radius:.8rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||||
|
.btn:hover{background:#f8fafc}
|
||||||
|
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||||
|
.btn-avatar:hover{background:#0284c7}
|
||||||
|
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:1.5rem}
|
||||||
|
.stat-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem}
|
||||||
|
.stat-card h4{margin:0;font-size:.95rem;color:#475569}
|
||||||
|
.stat-card strong{display:block;font-size:1.75rem;color:#0f172a}
|
||||||
|
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||||
|
.usage-table{width:100%;border-collapse:collapse;font-size:.9rem}
|
||||||
|
.usage-table th,.usage-table td{padding:.5rem;border-bottom:1px solid #e2e8f0;text-align:left}
|
||||||
|
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:180px;background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.35rem;z-index:40}
|
||||||
|
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.6rem;font-size:.9rem;color:#0f172a}
|
||||||
|
.user-menu-item:hover{background:#f1f5f9}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-50 text-slate-800" data-page="dashboard">
|
||||||
|
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
|
||||||
|
<div class="max-w-5xl mx-auto px-4 py-4 flex items-center gap-3">
|
||||||
|
<a href="./" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
|
||||||
|
<h1 class="font-semibold text-lg">Dashboard</h1>
|
||||||
|
<div class="ms-auto flex gap-2 items-center">
|
||||||
|
<div class="relative" id="userMenu">
|
||||||
|
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span id="userAvatar">U</span>
|
||||||
|
</button>
|
||||||
|
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||||
|
<a href="account.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||||
|
<a href="dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||||
|
<a href="admin.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||||
|
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="max-w-5xl mx-auto p-4 md:p-6">
|
||||||
|
<section class="stat-grid" id="dashboardCounts">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>Templates</h4>
|
||||||
|
<strong id="count-templates">–</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>Sections</h4>
|
||||||
|
<strong id="count-sections">–</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>Blocks</h4>
|
||||||
|
<strong id="count-blocks">–</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>Snippets</h4>
|
||||||
|
<strong id="count-snippets">–</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>Aufrufe gesamt</h4>
|
||||||
|
<strong id="count-usage">–</strong>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-card" data-role="admin">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h4>Template-Nutzung</h4>
|
||||||
|
<p class="text-sm text-slate-600">Wie oft wurden Templates über die API geladen? Setze einzelne Zähler bei Bedarf zurück.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn" id="btn-refresh-dashboard">Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<table class="usage-table" id="usageTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Template</th>
|
||||||
|
<th>Aufrufe</th>
|
||||||
|
<th>Zuletzt verwendet</th>
|
||||||
|
<th class="text-right">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td colspan="4" class="text-sm text-slate-500">Lade Daten…</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div id="toast-root"></div>
|
||||||
|
|
||||||
|
<script src="assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||||
|
<script type="module" src="assets/js/dashboard.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -40,6 +40,9 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
|||||||
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1rem;margin-bottom:1.25rem}
|
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1rem;margin-bottom:1.25rem}
|
||||||
.section-card h4{margin:0 0 .75rem;font-size:1rem;font-weight:600;color:#0f172a}
|
.section-card h4{margin:0 0 .75rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||||
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||||
|
.user-menu{position:absolute;top:calc(100% + .5rem);right:0;min-width:180px;background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;box-shadow:0 20px 35px rgba(15,23,42,.15);padding:.35rem;z-index:50}
|
||||||
|
.user-menu-item{display:block;width:100%;text-align:left;padding:.45rem .75rem;border-radius:.6rem;font-size:.9rem;color:#0f172a}
|
||||||
|
.user-menu-item:hover{background:#f1f5f9}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="page-admin bg-slate-50 text-slate-800">
|
<body class="page-admin bg-slate-50 text-slate-800">
|
||||||
@@ -52,11 +55,19 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
|||||||
<button type="button" data-tab="blocks" class="px-4 py-2 text-sm border-e">Blocks</button>
|
<button type="button" data-tab="blocks" class="px-4 py-2 text-sm border-e">Blocks</button>
|
||||||
<button type="button" data-tab="snippets" class="px-4 py-2 text-sm">Snippets</button>
|
<button type="button" data-tab="snippets" class="px-4 py-2 text-sm">Snippets</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="ms-auto flex gap-2 items-center">
|
<div class="ms-auto flex gap-3 items-center">
|
||||||
<button id="btn-new" type="button" class="btn">Neu …</button>
|
<button id="btn-new" type="button" class="btn">Neu …</button>
|
||||||
<a href="account.php" id="btn-user" class="btn-avatar" title="Mein Konto">
|
<div class="relative" id="userMenu">
|
||||||
<span id="userAvatar">U</span>
|
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||||
</a>
|
<span id="userAvatar">U</span>
|
||||||
|
</button>
|
||||||
|
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||||
|
<a href="account.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||||
|
<a href="dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||||
|
<a href="admin.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||||
|
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ApiKernel
|
|||||||
private string $action;
|
private string $action;
|
||||||
private array $tableMap;
|
private array $tableMap;
|
||||||
private AuthService $authService;
|
private AuthService $authService;
|
||||||
|
private array $tableExistsCache = [];
|
||||||
|
|
||||||
// --- Initialisierung & Konstruktor (Optimiert) ---
|
// --- Initialisierung & Konstruktor (Optimiert) ---
|
||||||
|
|
||||||
@@ -184,6 +185,21 @@ class ApiKernel
|
|||||||
foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field'];
|
foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field'];
|
||||||
return $cols;
|
return $cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function tableExists(string $table): bool
|
||||||
|
{
|
||||||
|
if ($table === '') return false;
|
||||||
|
if (array_key_exists($table, $this->tableExistsCache)) {
|
||||||
|
return $this->tableExistsCache[$table];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$this->pdo->query("SELECT 1 FROM `$table` LIMIT 1");
|
||||||
|
$this->tableExistsCache[$table] = true;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->tableExistsCache[$table] = false;
|
||||||
|
}
|
||||||
|
return $this->tableExistsCache[$table];
|
||||||
|
}
|
||||||
private function primaryKey(string $table): ?string { /* ... Logik bleibt unverändert ... */
|
private function primaryKey(string $table): ?string { /* ... Logik bleibt unverändert ... */
|
||||||
$stmt = $this->pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
|
$stmt = $this->pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
@@ -907,6 +923,114 @@ class ApiKernel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function fetchResourceCounts(int $customerId): array
|
||||||
|
{
|
||||||
|
$counts = [
|
||||||
|
'templates' => 0,
|
||||||
|
'sections' => 0,
|
||||||
|
'blocks' => 0,
|
||||||
|
'snippets' => 0,
|
||||||
|
'renders_total' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$map = $this->tableMap ?? [];
|
||||||
|
foreach (['templates', 'sections', 'blocks', 'snippets'] as $kind) {
|
||||||
|
$table = $map[$kind] ?? null;
|
||||||
|
if (!$table || !$this->tableExists($table)) continue;
|
||||||
|
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM `$table` WHERE `customer_id` = :cid");
|
||||||
|
$stmt->execute([':cid' => $customerId]);
|
||||||
|
$counts[$kind] = (int)($stmt->fetchColumn() ?: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$usageTable = $this->lookupTableName('template_usage', 'emailtemplate_template_usage');
|
||||||
|
if ($this->tableExists($usageTable)) {
|
||||||
|
$stmt = $this->pdo->prepare("SELECT SUM(`render_count`) FROM `$usageTable` WHERE `customer_id` = :cid");
|
||||||
|
$stmt->execute([':cid' => $customerId]);
|
||||||
|
$counts['renders_total'] = (int)($stmt->fetchColumn() ?: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function listTemplateUsage(int $customerId): array
|
||||||
|
{
|
||||||
|
$table = $this->tableMap['templates'] ?? null;
|
||||||
|
if (!$table || !$this->tableExists($table)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$usageTable = $this->lookupTableName('template_usage', 'emailtemplate_template_usage');
|
||||||
|
if ($this->tableExists($usageTable)) {
|
||||||
|
$sql = "SELECT t.id, t.name, t.updated_at, COALESCE(u.render_count, 0) AS render_count, u.last_rendered_at
|
||||||
|
FROM `$table` t
|
||||||
|
LEFT JOIN `$usageTable` u ON u.template_id = t.id
|
||||||
|
WHERE t.customer_id = :cid
|
||||||
|
ORDER BY render_count DESC, t.updated_at DESC";
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute([':cid' => $customerId]);
|
||||||
|
$rows = $stmt->fetchAll() ?: [];
|
||||||
|
} else {
|
||||||
|
$sql = "SELECT t.id, t.name, t.updated_at FROM `$table` t WHERE t.customer_id = :cid ORDER BY t.updated_at DESC";
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute([':cid' => $customerId]);
|
||||||
|
$rows = $stmt->fetchAll() ?: [];
|
||||||
|
foreach ($rows as &$row) {
|
||||||
|
$row['render_count'] = 0;
|
||||||
|
$row['last_rendered_at'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(static function ($row) {
|
||||||
|
return [
|
||||||
|
'template_id' => (int)($row['id'] ?? 0),
|
||||||
|
'name' => $row['name'] ?? '',
|
||||||
|
'render_count' => (int)($row['render_count'] ?? 0),
|
||||||
|
'last_rendered_at' => $row['last_rendered_at'] ?? null,
|
||||||
|
'updated_at' => $row['updated_at'] ?? null,
|
||||||
|
];
|
||||||
|
}, $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetTemplateUsage(int $customerId, array $templateIds): void
|
||||||
|
{
|
||||||
|
$usageTable = $this->lookupTableName('template_usage', 'emailtemplate_template_usage');
|
||||||
|
if (!$templateIds || !$this->tableExists($usageTable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$templateIds = array_values(array_unique(array_filter(array_map('intval', $templateIds), static fn ($v) => $v > 0)));
|
||||||
|
if (!$templateIds) return;
|
||||||
|
|
||||||
|
$placeholders = implode(',', array_fill(0, count($templateIds), '?'));
|
||||||
|
$sql = "DELETE FROM `$usageTable` WHERE `customer_id` = ? AND `template_id` IN ($placeholders)";
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute(array_merge([$customerId], $templateIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractIdList($raw): array
|
||||||
|
{
|
||||||
|
if ($raw === null) return [];
|
||||||
|
if (is_numeric($raw)) {
|
||||||
|
$raw = [(int)$raw];
|
||||||
|
} elseif (is_string($raw)) {
|
||||||
|
$raw = preg_split('/[\s,]+/', $raw);
|
||||||
|
} elseif (!is_array($raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = [];
|
||||||
|
foreach ($raw as $value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
$ids = array_merge($ids, $this->extractIdList($value));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($value === '' || $value === null) continue;
|
||||||
|
$ids[] = (int)$value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_values(array_unique(array_filter($ids, static fn ($v) => $v > 0)));
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
|
||||||
private function calculateUsage(string $kind, int $id, array $auth): array
|
private function calculateUsage(string $kind, int $id, array $auth): array
|
||||||
{
|
{
|
||||||
if ($id <= 0) return ['total' => 0];
|
if ($id <= 0) return ['total' => 0];
|
||||||
|
|||||||
Reference in New Issue
Block a user