feat: comprehensive UI/UX and feature improvements

- Dark mode: CSS custom properties (global.css) + toggle button, persisted in localStorage
- i18n: German/English language switcher (lang/de.php, lang/en.php, includes/I18n.php)
- Mobile-responsive admin layout: hamburger menu, sidebar overlay (global.js + global.css)
- Shared admin navigation include (includes/admin_nav.php) used across all admin pages
- Toast notifications system globally available via global.js
- Voucher templates/profiles: CRUD UI at admin/templates.php with voucher_templates DB table
- Bulk voucher creation: create 1-20 vouchers at once with multi-print layout on index.php
- Configurable voucher defaults: expire time, device limit, max limit in admin settings
- Template quick-select on voucher form: auto-fills max_uses and expire_minutes
- Password reset flow: forgot_password.php + reset_password.php with token-based reset
- Audit log UI: admin/audit_log.php with filter, pagination, audit_log DB table
- Audit logging on login, user create/edit/delete, site create/edit/delete
- Admin pages updated: index, vouchers, users, sites all use admin_nav.php + dark mode + i18n
- Voucher admin: live search input added alongside existing status filter + pagination
- Users admin: password-reset-link button per user row (when SMTP enabled)
- Login page: i18n, dark mode, language switcher, forgot password link

https://claude.ai/code/session_01YN6Bcm1VSi8mpDeyKpyrdJ
This commit is contained in:
Claude 2026-05-08 17:59:17 +00:00
parent bf3e55a967
commit a1021a0f84
No known key found for this signature in database
20 changed files with 5281 additions and 5471 deletions

226
admin/audit_log.php Normal file
View file

@ -0,0 +1,226 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/I18n.php';
$auth = new Auth();
$auth->requireAdmin();
I18n::init();
$db = Database::getInstance();
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$filterAction = trim($_GET['action'] ?? '');
$filterUser = trim($_GET['user_id'] ?? '');
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 50;
$offset = ($page - 1) * $perPage;
$where = [];
$params = [];
if ($filterAction !== '') { $where[] = 'a.action = ?'; $params[] = $filterAction; }
if ($filterUser !== '') { $where[] = 'a.user_id = ?'; $params[] = (int)$filterUser; }
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$total = (int)$db->fetchOne("SELECT COUNT(*) as c FROM audit_log a $whereStr", $params)['c'];
$pages = max(1, (int)ceil($total / $perPage));
$logs = $db->fetchAll(
"SELECT a.*, u.name as user_name, u.email as user_email
FROM audit_log a LEFT JOIN users u ON a.user_id = u.id
$whereStr ORDER BY a.created_at DESC LIMIT ? OFFSET ?",
array_merge($params, [$perPage, $offset])
);
// Distinct actions for filter
$actions = $db->fetchAll("SELECT DISTINCT action FROM audit_log ORDER BY action");
// User list for filter
$users = $db->fetchAll("SELECT id, name FROM users WHERE is_active = 1 ORDER BY name");
$currentPage = 'audit_log';
$adminBase = '';
$actionLabels = [
'voucher_created' => '🎫 Voucher erstellt',
'voucher_bulk' => '🎫 Bulk Voucher',
'user_login' => '🔐 Login',
'user_logout' => '🚪 Logout',
'user_created' => '👤 Benutzer erstellt',
'user_updated' => '👤 Benutzer geändert',
'user_deleted' => '👤 Benutzer gelöscht',
'site_added' => '🌐 Site hinzugefügt',
'site_updated' => '🌐 Site geändert',
'site_deleted' => '🌐 Site gelöscht',
'settings_saved' => '⚙️ Einstellungen gespeichert',
'password_reset' => '🔑 Passwort-Reset',
'template_created' => '📋 Profil erstellt',
'template_updated' => '📋 Profil geändert',
'template_deleted' => '📋 Profil gelöscht',
];
?>
<!DOCTYPE html>
<html lang="<?= I18n::getLanguage() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('audit_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { margin-bottom: 25px; }
.page-title { font-size: 28px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
.card { background: var(--bg-card); border-radius: 15px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; margin-bottom: 20px; }
.card-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
.card-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 11px 15px; background: var(--bg-table-head); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.table td { padding: 12px 15px; border-bottom: 1px solid var(--border-color); font-size: 13px; color: var(--text-primary); }
.table tr:last-child td { border-bottom: none; }
.table tr:hover td { background: var(--bg-hover); }
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; }
.filter-bar { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
.filter-bar select { padding: 9px 12px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 13px; background: var(--bg-input); color: var(--text-primary); }
.filter-bar select:focus { outline: none; border-color: var(--accent); }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-small { padding: 6px 12px; font-size: 12px; }
.ip-cell { font-family: monospace; font-size: 12px; color: var(--text-muted); }
.details-cell { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
.pagination { display: flex; align-items: center; justify-content: space-between; padding: 14px 25px; border-top: 1px solid var(--border-color); flex-wrap: wrap; gap: 10px; }
.page-info { font-size: 13px; color: var(--text-muted); }
.page-btns { display: flex; gap: 5px; flex-wrap: wrap; }
.page-btn { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-card); color: var(--text-secondary); cursor: pointer; font-size: 13px; text-decoration: none; transition: all 0.2s; }
.page-btn:hover { border-color: var(--accent); color: var(--accent); }
.page-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state i { font-size: 42px; margin-bottom: 15px; display: block; opacity: 0.3; }
.action-chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; background: var(--bg-hover); color: var(--text-secondary); }
</style>
</head>
<div class="page-header">
<h1 class="page-title"><?= __('audit_title') ?></h1>
<p style="color: var(--text-muted); font-size: 14px;"><?= __('audit_subtitle') ?></p>
</div>
<!-- Filter -->
<div class="card" style="margin-bottom: 20px;">
<div class="card-header"><span class="card-title"><i class="fas fa-filter"></i> <?= __('audit_filter') ?></span></div>
<div style="padding: 20px 25px;">
<form method="get" class="filter-bar">
<div>
<label style="display:block;font-size:12px;color:var(--text-muted);margin-bottom:5px;"><?= __('audit_action') ?></label>
<select name="action">
<option value=""><?= __('audit_filter_all') ?></option>
<?php foreach ($actions as $a): ?>
<option value="<?= htmlspecialchars($a['action']) ?>" <?= $filterAction === $a['action'] ? 'selected' : '' ?>>
<?= htmlspecialchars($actionLabels[$a['action']] ?? $a['action']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<label style="display:block;font-size:12px;color:var(--text-muted);margin-bottom:5px;"><?= __('audit_user') ?></label>
<select name="user_id">
<option value="">Alle Benutzer</option>
<?php foreach ($users as $u): ?>
<option value="<?= $u['id'] ?>" <?= (string)$filterUser === (string)$u['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($u['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary btn-small"><i class="fas fa-search"></i> Filtern</button>
<a href="audit_log.php" class="btn btn-secondary btn-small"><i class="fas fa-times"></i> Zurücksetzen</a>
</form>
</div>
</div>
<!-- Log-Tabelle -->
<div class="card">
<div class="card-header">
<span class="card-title"><i class="fas fa-history"></i> <?= __('audit_title') ?></span>
<span style="font-size:13px;color:var(--text-muted);"><?= number_format($total) ?> Einträge</span>
</div>
<?php if (empty($logs)): ?>
<div class="empty-state"><i class="fas fa-history"></i><p><?= __('audit_none') ?></p></div>
<?php else: ?>
<div style="overflow-x:auto;">
<table class="table">
<thead>
<tr>
<th><?= __('audit_time') ?></th>
<th><?= __('audit_action') ?></th>
<th><?= __('audit_user') ?></th>
<th><?= __('audit_entity') ?></th>
<th><?= __('audit_details') ?></th>
<th><?= __('audit_ip') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<tr>
<td style="white-space:nowrap;color:var(--text-muted);">
<?= date('d.m.Y', strtotime($log['created_at'])) ?><br>
<small><?= date('H:i:s', strtotime($log['created_at'])) ?></small>
</td>
<td>
<span class="action-chip">
<?= htmlspecialchars($actionLabels[$log['action']] ?? $log['action']) ?>
</span>
</td>
<td>
<?php if ($log['user_name']): ?>
<strong style="font-size:13px;"><?= htmlspecialchars($log['user_name']) ?></strong><br>
<small style="color:var(--text-muted);"><?= htmlspecialchars($log['user_email'] ?? '') ?></small>
<?php else: ?>
<em style="color:var(--text-muted);">System/Anonym</em>
<?php endif; ?>
</td>
<td style="color:var(--text-secondary);">
<?php if ($log['entity_type']): ?>
<code style="font-size:11px;"><?= htmlspecialchars($log['entity_type']) ?>:<?= htmlspecialchars($log['entity_id'] ?? '') ?></code>
<?php else: ?>
<span style="color:var(--text-muted);">-</span>
<?php endif; ?>
</td>
<td class="details-cell" title="<?= htmlspecialchars($log['details'] ?? '') ?>">
<?= htmlspecialchars(mb_strimwidth($log['details'] ?? '-', 0, 80, '…')) ?>
</td>
<td class="ip-cell"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($pages > 1): ?>
<div class="pagination">
<span class="page-info">Seite <?= $page ?> von <?= $pages ?> (<?= $total ?> Einträge)</span>
<div class="page-btns">
<?php
$baseUrl = '?' . http_build_query(array_filter(['action' => $filterAction, 'user_id' => $filterUser]));
if ($page > 1): ?>
<a href="<?= $baseUrl ?>&page=<?= $page - 1 ?>" class="page-btn"><i class="fas fa-chevron-left"></i></a>
<?php endif;
for ($p = max(1, $page - 2); $p <= min($pages, $page + 2); $p++): ?>
<a href="<?= $baseUrl ?>&page=<?= $p ?>" class="page-btn <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor;
if ($page < $pages): ?>
<a href="<?= $baseUrl ?>&page=<?= $page + 1 ?>" class="page-btn"><i class="fas fa-chevron-right"></i></a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div><!-- main-content -->
<script src="../assets/global.js"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

311
admin/templates.php Normal file
View file

@ -0,0 +1,311 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../includes/Database.php';
require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/I18n.php';
$auth = new Auth();
$auth->requireAdmin();
I18n::init();
$db = Database::getInstance();
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$error = '';
$success = '';
// Profil hinzufügen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_template'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('error_csrf');
} else {
try {
$name = trim($_POST['name'] ?? '');
$maxUses = (int)($_POST['max_uses'] ?? 1);
$expireMin = (int)($_POST['expire_minutes'] ?? 480);
$description = trim($_POST['description'] ?? '');
if (empty($name)) throw new Exception(__('error_name_req'));
if ($maxUses < 1) $maxUses = 1;
if ($expireMin < 1) $expireMin = 60;
$db->execute(
"INSERT INTO voucher_templates (name, max_uses, expire_minutes, description, created_by) VALUES (?, ?, ?, ?, ?)",
[$name, $maxUses, $expireMin, $description, $_SESSION['user_id']]
);
$success = __('templates_added');
} catch (Exception $e) {
$error = $e->getMessage();
}
}
}
// Profil bearbeiten
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_template'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('error_csrf');
} else {
try {
$id = (int)$_POST['template_id'];
$name = trim($_POST['name'] ?? '');
$maxUses = (int)($_POST['max_uses'] ?? 1);
$expireMin = (int)($_POST['expire_minutes'] ?? 480);
$description = trim($_POST['description'] ?? '');
$isActive = isset($_POST['is_active']) ? 1 : 0;
if (empty($name)) throw new Exception(__('error_name_req'));
$db->execute(
"UPDATE voucher_templates SET name=?, max_uses=?, expire_minutes=?, description=?, is_active=? WHERE id=?",
[$name, $maxUses, $expireMin, $description, $isActive, $id]
);
$success = __('templates_updated');
} catch (Exception $e) {
$error = $e->getMessage();
}
}
}
// Profil löschen
if (isset($_GET['delete']) && isset($_GET['token'])) {
if ($auth->validateCsrfToken($_GET['token'])) {
$db->execute("DELETE FROM voucher_templates WHERE id = ?", [(int)$_GET['delete']]);
$success = __('templates_deleted');
} else {
$error = __('error_csrf');
}
}
$templates = $db->fetchAll("SELECT t.*, u.name as creator FROM voucher_templates t LEFT JOIN users u ON t.created_by = u.id ORDER BY t.is_active DESC, t.name");
$currentPage = 'templates';
$adminBase = '';
?>
<!DOCTYPE html>
<html lang="<?= I18n::getLanguage() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('templates_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; flex-wrap: wrap; gap: 15px; }
.page-title { font-size: 28px; font-weight: 600; color: var(--text-primary); }
.alert { padding: 14px 20px; border-radius: 10px; margin-bottom: 25px; font-size: 14px; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.card { background: var(--bg-card); border-radius: 15px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; margin-bottom: 25px; }
.card-header { padding: 20px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.card-title { font-size: 18px; font-weight: 600; color: var(--text-primary); }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 12px 15px; background: var(--bg-table-head); color: var(--text-secondary); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.table td { padding: 14px 15px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); font-size: 14px; }
.table tr:last-child td { border-bottom: none; }
.table tr:hover td { background: var(--bg-hover); }
.badge { display: inline-block; padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 500; }
.badge-success { background: #d4edda; color: #155724; }
.badge-secondary { background: var(--bg-hover); color: var(--text-muted); }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-danger { background: var(--danger); color: white; }
.btn-small { padding: 6px 12px; font-size: 12px; }
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--modal-overlay); z-index: 500; align-items: center; justify-content: center; }
.modal.active { display: flex; }
.modal-content { background: var(--bg-card); border-radius: 15px; max-width: 520px; width: 90%; max-height: 90vh; overflow-y: auto; }
.modal-header { padding: 22px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.modal-title { font-size: 18px; font-weight: 600; color: var(--text-primary); }
.modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: var(--text-muted); }
.modal-body { padding: 25px; }
.form-group { margin-bottom: 18px; }
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 11px 14px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; background: var(--bg-input); color: var(--text-primary); transition: border-color 0.2s; font-family: inherit; }
input:focus, textarea:focus { outline: none; border-color: var(--accent); }
.checkbox-group { display: flex; align-items: center; gap: 10px; }
.checkbox-group input { width: auto; accent-color: var(--accent); }
.help-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state i { font-size: 48px; margin-bottom: 20px; opacity: 0.3; display: block; }
.duration-badge { display: inline-flex; align-items: center; gap: 5px; background: var(--bg-hover); padding: 3px 10px; border-radius: 20px; font-size: 12px; color: var(--text-secondary); }
</style>
</head>
<div class="page-header">
<div>
<h1 class="page-title"><?= __('templates_title') ?></h1>
<p style="color: var(--text-muted); font-size: 14px; margin-top: 5px;"><?= __('templates_subtitle') ?></p>
</div>
<button onclick="openAddModal()" class="btn btn-primary">
<i class="fas fa-plus"></i> <?= __('templates_add') ?>
</button>
</div>
<?php if ($error): ?>
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<div class="card">
<div class="card-header">
<h2 class="card-title"><?= __('templates_title') ?></h2>
<span style="font-size: 13px; color: var(--text-muted);"><?= count($templates) ?> Profile</span>
</div>
<?php if (empty($templates)): ?>
<div class="empty-state">
<i class="fas fa-layer-group"></i>
<p style="font-size: 15px; margin-bottom: 8px;"><?= __('templates_none') ?></p>
<p style="font-size: 13px;"><?= __('templates_add_hint') ?></p>
<button onclick="openAddModal()" class="btn btn-primary" style="margin-top: 20px;"><i class="fas fa-plus"></i> <?= __('templates_add') ?></button>
</div>
<?php else: ?>
<div style="overflow-x: auto;">
<table class="table">
<thead>
<tr>
<th><?= __('templates_name') ?></th>
<th><?= __('templates_devices') ?></th>
<th><?= __('templates_duration') ?></th>
<th><?= __('templates_desc') ?></th>
<th><?= __('label_status') ?></th>
<th><?= __('label_actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($templates as $t): ?>
<tr>
<td><strong><?= htmlspecialchars($t['name']) ?></strong></td>
<td>
<span class="duration-badge"><i class="fas fa-mobile-alt"></i> <?= (int)$t['max_uses'] ?></span>
</td>
<td>
<?php
$m = (int)$t['expire_minutes'];
if ($m >= 1440 && $m % 1440 === 0) {
$durLabel = ($m / 1440) . ' Tag' . ($m / 1440 > 1 ? 'e' : '');
} elseif ($m >= 60 && $m % 60 === 0) {
$durLabel = ($m / 60) . ' Std.';
} else {
$durLabel = $m . ' Min.';
}
?>
<span class="duration-badge"><i class="fas fa-clock"></i> <?= $durLabel ?></span>
</td>
<td style="color: var(--text-secondary);"><?= htmlspecialchars($t['description'] ?? '-') ?></td>
<td>
<?php if ($t['is_active']): ?>
<span class="badge badge-success"><?= __('status_active') ?></span>
<?php else: ?>
<span class="badge badge-secondary"><?= __('status_inactive') ?></span>
<?php endif; ?>
</td>
<td>
<button onclick="openEditModal(<?= $t['id'] ?>, '<?= htmlspecialchars($t['name'], ENT_QUOTES) ?>', <?= (int)$t['max_uses'] ?>, <?= (int)$t['expire_minutes'] ?>, '<?= htmlspecialchars($t['description'] ?? '', ENT_QUOTES) ?>', <?= (int)$t['is_active'] ?>)"
class="btn btn-secondary btn-small"><i class="fas fa-edit"></i></button>
<a href="?delete=<?= $t['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
onclick="return confirm('Profil wirklich löschen?')"
class="btn btn-danger btn-small"><i class="fas fa-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div><!-- main-content -->
<!-- Modal: Hinzufügen -->
<div id="addModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title"><i class="fas fa-plus-circle" style="color: var(--accent);"></i> <?= __('templates_add') ?></h2>
<button class="modal-close" onclick="closeModal('addModal')">&times;</button>
</div>
<div class="modal-body">
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<div class="form-group"><label><?= __('templates_name') ?> *</label><input type="text" name="name" required placeholder="z.B. Tagespass, Event 4h"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label><?= __('templates_devices') ?></label>
<input type="number" name="max_uses" value="1" min="1" max="100">
<div class="help-text">Max. gleichzeitige Geräte</div>
</div>
<div class="form-group">
<label><?= __('templates_duration') ?> *</label>
<input type="number" name="expire_minutes" value="480" min="1" max="525600">
<div class="help-text">480 = 8 Stunden</div>
</div>
</div>
<div class="form-group"><label><?= __('templates_desc') ?></label><textarea name="description" rows="2" placeholder="Kurze Beschreibung für Ihr Team"></textarea></div>
<div style="display:flex;gap:10px;margin-top:20px;">
<button type="submit" name="add_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
<button type="button" onclick="closeModal('addModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal: Bearbeiten -->
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title"><i class="fas fa-edit" style="color: var(--accent);"></i> <?= __('templates_edit') ?></h2>
<button class="modal-close" onclick="closeModal('editModal')">&times;</button>
</div>
<div class="modal-body">
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="template_id" id="editId">
<div class="form-group"><label><?= __('templates_name') ?> *</label><input type="text" name="name" id="editName" required></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label><?= __('templates_devices') ?></label>
<input type="number" name="max_uses" id="editMaxUses" min="1" max="100">
</div>
<div class="form-group">
<label><?= __('templates_duration') ?></label>
<input type="number" name="expire_minutes" id="editExpireMin" min="1">
</div>
</div>
<div class="form-group"><label><?= __('templates_desc') ?></label><textarea name="description" id="editDesc" rows="2"></textarea></div>
<div class="checkbox-group" style="margin-bottom:20px;">
<input type="checkbox" name="is_active" id="editActive">
<label for="editActive" style="margin:0;"><?= __('status_active') ?></label>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" name="edit_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
<button type="button" onclick="closeModal('editModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
</div>
</form>
</div>
</div>
</div>
<script src="../assets/global.js"></script>
<script>
function openAddModal() { document.getElementById('addModal').classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function openEditModal(id, name, maxUses, expMin, desc, isActive) {
document.getElementById('editId').value = id;
document.getElementById('editName').value = name;
document.getElementById('editMaxUses').value = maxUses;
document.getElementById('editExpireMin').value = expMin;
document.getElementById('editDesc').value = desc;
document.getElementById('editActive').checked = isActive == 1;
document.getElementById('editModal').classList.add('active');
}
['addModal','editModal'].forEach(id => {
document.getElementById(id).addEventListener('click', function(e) {
if (e.target === this) closeModal(id);
});
});
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff