Barrierefreiheit und mobile Darstellung
- Kontrast: gedämpfter Text war mit 3,1:1 unter WCAG AA, jetzt 4,9:1 (hell) bzw. 6,4:1 (dunkel) - Sprungmarke „Zum Inhalt springen" in Admin-Shell und Voucher-Seite - aria-label für alle reinen Icon-Schaltflächen (Theme, Menü, Abmelden, Zeilenaktionen), aria-current auf dem aktiven Navigationspunkt, role="group" für den Sprachumschalter - 194 dekorative Icons mit aria-hidden versehen, damit Screenreader sie nicht vorlesen - Toast-Container als aria-live-Bereich ausgezeichnet - prefers-reduced-motion schaltet Animationen und Übergänge ab Tabellen (Benutzer, Vouchers, Profile, Audit-Log, Dashboard, API-Keys) werden unter 720 px zu Karten: die Spaltenüberschrift steht per data-label vor dem Wert, statt horizontal zu scrollen. Datums- und Zeitformat in der Voucher-Liste folgen jetzt der gewählten Sprache statt fest de-DE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
850a04d628
commit
7f1d93debd
21 changed files with 716 additions and 632 deletions
|
|
@ -143,9 +143,9 @@ $currentPage = 'vouchers';
|
|||
|
||||
<?php if (empty($sites)): ?>
|
||||
<div class="no-sites-warning">
|
||||
<i class="fas fa-triangle-exclamation" style="font-size:26px;margin-bottom:12px;display:block;"></i>
|
||||
<i class="fas fa-triangle-exclamation" style="font-size:26px;margin-bottom:12px;display:block;" aria-hidden="true"></i>
|
||||
<h3><?= __('vouchers_no_sites') ?></h3>
|
||||
<a href="sites.php" class="btn btn-primary" style="margin-top:20px;"><i class="fas fa-plus"></i> <?= __('nav_sites') ?></a>
|
||||
<a href="sites.php" class="btn btn-primary" style="margin-top:20px;"><i class="fas fa-plus" aria-hidden="true"></i> <?= __('nav_sites') ?></a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ $currentPage = 'vouchers';
|
|||
</select>
|
||||
<input type="search" id="searchInput" class="search-bar" placeholder="<?= __('vouchers_search') ?>" oninput="filterAndRender()" style="display:none;">
|
||||
<button id="refreshBtn" class="btn btn-secondary" onclick="loadVouchers(true)" disabled>
|
||||
<i class="fas fa-sync-alt"></i> <?= __('btn_refresh') ?>
|
||||
<i class="fas fa-sync-alt" aria-hidden="true"></i> <?= __('btn_refresh') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -187,12 +187,12 @@ $currentPage = 'vouchers';
|
|||
<div class="card-header">
|
||||
<h2 class="card-title" id="voucherListTitle">Vouchers</h2>
|
||||
<a id="csvExportBtn" style="display:none;" class="btn btn-secondary btn-sm" href="#">
|
||||
<i class="fas fa-download"></i> <?= __('btn_export_csv') ?>
|
||||
<i class="fas fa-download" aria-hidden="true"></i> <?= __('btn_export_csv') ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<div id="voucherContent">
|
||||
<div class="empty-state"><i class="fas fa-ticket-alt"></i><p><?= __('vouchers_select_hint') ?></p></div>
|
||||
<div class="empty-state"><i class="fas fa-ticket-alt" aria-hidden="true"></i><p><?= __('vouchers_select_hint') ?></p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -200,10 +200,12 @@ $currentPage = 'vouchers';
|
|||
|
||||
</main>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="toast-container" role="status" aria-live="polite"></div>
|
||||
<script src="../assets/global.js"></script>
|
||||
<script>
|
||||
const csrfToken = '<?= $auth->getCsrfToken() ?>';
|
||||
// Datums- und Zeitformat folgen der gewaehlten Sprache
|
||||
const LOCALE = '<?= I18n::getLanguage() === 'en' ? 'en-GB' : 'de-DE' ?>';
|
||||
let currentSiteId = null;
|
||||
let allVouchers = [];
|
||||
let currentFilter = 'all';
|
||||
|
|
@ -218,7 +220,7 @@ async function loadVouchers(syncFirst=false) {
|
|||
const searchInput = document.getElementById('searchInput');
|
||||
|
||||
if (!siteId) {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt"></i><p><?= addslashes(__('vouchers_select_hint')) ?></p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt" aria-hidden="true"></i><p><?= addslashes(__('vouchers_select_hint')) ?></p></div>`;
|
||||
document.getElementById('statsContainer').style.display = 'none';
|
||||
searchInput.style.display = 'none';
|
||||
refreshBtn.disabled = true;
|
||||
|
|
@ -227,9 +229,9 @@ async function loadVouchers(syncFirst=false) {
|
|||
|
||||
currentSiteId = siteId;
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${syncFirst ? '<?= addslashes(__('btn_refresh')) ?>' : '<?= addslashes(__('btn_refresh')) ?>'}`;
|
||||
refreshBtn.innerHTML = `<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> ${syncFirst ? '<?= addslashes(__('btn_refresh')) ?>' : '<?= addslashes(__('btn_refresh')) ?>'}`;
|
||||
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><span>${syncFirst ? 'Synchronisiere...' : 'Lade...'}</span></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="loading"><i class="fas fa-spinner" aria-hidden="true"></i><span>${syncFirst ? 'Synchronisiere...' : 'Lade...'}</span></div>`;
|
||||
|
||||
try {
|
||||
const result = await fetch(`vouchers.php?ajax_get_vouchers=1&site_id=${siteId}${syncFirst?'&sync=1':''}`).then(r=>r.json());
|
||||
|
|
@ -247,15 +249,15 @@ async function loadVouchers(syncFirst=false) {
|
|||
csvBtn.href = `vouchers.php?export_csv=1&site_id=${siteId}&token=${csrfToken}`;
|
||||
if (syncFirst) showToast('success', '<?= addslashes(__('btn_refresh')) ?>', `${result.count} Vouchers geladen`);
|
||||
} else {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)"></i><p>${result.message}</p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)" aria-hidden="true"></i><p>${result.message}</p></div>`;
|
||||
document.getElementById('statsContainer').style.display = 'none';
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)"></i><p>Verbindungsfehler: ${e.message}</p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)" aria-hidden="true"></i><p>Verbindungsfehler: ${e.message}</p></div>`;
|
||||
document.getElementById('statsContainer').style.display = 'none';
|
||||
}
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> <?= addslashes(__('btn_refresh')) ?>';
|
||||
refreshBtn.innerHTML = '<i class="fas fa-sync-alt" aria-hidden="true"></i> <?= addslashes(__('btn_refresh')) ?>';
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
|
|
@ -293,7 +295,7 @@ function renderVouchers() {
|
|||
const pageVouchers = vouchers.slice((currentPage-1)*PAGE_SIZE, currentPage*PAGE_SIZE);
|
||||
|
||||
if (vouchers.length === 0) {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt"></i><p><?= addslashes(__('vouchers_none')) ?></p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt" aria-hidden="true"></i><p><?= addslashes(__('vouchers_none')) ?></p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +311,7 @@ function renderVouchers() {
|
|||
<button class="filter-btn ${currentFilter==='expired'?'active':''}" data-filter="expired" onclick="setFilter('expired')"><?= __('vouchers_filter_expired') ?> (${expiredCnt})</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container"><table class="table">
|
||||
<div class="table-container"><table class="table table-stack">
|
||||
<thead><tr>
|
||||
<th><?= __('label_created') ?></th>
|
||||
<th><?= __('label_code') ?></th>
|
||||
|
|
@ -330,21 +332,21 @@ function renderVouchers() {
|
|||
})() : '';
|
||||
const usagePct = v.quota > 0 ? Math.min(100, (v.used/v.quota)*100) : 0;
|
||||
const statusBadge = v.status==='valid'
|
||||
? `<span class="badge badge-success"><i class="fas fa-check"></i> <?= __('status_valid') ?></span>`
|
||||
? `<span class="badge badge-success"><i class="fas fa-check" aria-hidden="true"></i> <?= __('status_valid') ?></span>`
|
||||
: v.status==='used'
|
||||
? `<span class="badge badge-warning"><i class="fas fa-user-check"></i> <?= __('status_used') ?></span>`
|
||||
: `<span class="badge badge-danger"><i class="fas fa-times"></i> <?= __('status_expired') ?></span>`;
|
||||
? `<span class="badge badge-warning"><i class="fas fa-user-check" aria-hidden="true"></i> <?= __('status_used') ?></span>`
|
||||
: `<span class="badge badge-danger"><i class="fas fa-times" aria-hidden="true"></i> <?= __('status_expired') ?></span>`;
|
||||
|
||||
html += `<tr id="voucher-${v._id}">
|
||||
<td><strong>${createDate.toLocaleDateString('de-DE')}</strong><br><small style="color:var(--text-muted)">${createDate.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}</small></td>
|
||||
<td><code onclick="copyToClipboard('${escapeHtml(v.formatted_code||'')}','Kopiert!')" title="Kopieren" style="cursor:pointer">${escapeHtml(v.formatted_code||'')}</code></td>
|
||||
<td class="voucher-note" title="${escapeHtml(v.note||'-')}">${escapeHtml(v.note||'-')}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td><div class="usage-info"><span>${v.used}/${v.quota>0?v.quota:'∞'}</span>${v.quota>0?`<div class="usage-bar"><div class="usage-bar-fill" style="width:${usagePct}%"></div></div>`:''}</div></td>
|
||||
<td>${remaining?`<span style="color:var(--success)"><i class="fas fa-clock"></i> ${remaining}</span><br>`:''}<small style="color:var(--text-muted)">${v.duration} Min.</small></td>
|
||||
<td style="white-space:nowrap;">
|
||||
<button onclick="resendVoucher('${v._id}','${escapeHtml(v.formatted_code||'')}')" class="btn btn-secondary btn-sm" title="Per E-Mail senden"><i class="fas fa-envelope"></i></button>
|
||||
<button onclick="deleteVoucher('${v._id}')" class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>"><i class="fas fa-trash"></i></button>
|
||||
<td data-label="<?= __('label_created') ?>"><strong>${createDate.toLocaleDateString(LOCALE)}</strong><br><small style="color:var(--text-muted)">${createDate.toLocaleTimeString(LOCALE,{hour:'2-digit',minute:'2-digit'})}</small></td>
|
||||
<td data-label="<?= __('label_code') ?>"><code onclick="copyToClipboard('${escapeHtml(v.formatted_code||'')}','<?= __('js_copied') ?>')" title="<?= __('js_copy') ?>" style="cursor:pointer">${escapeHtml(v.formatted_code||'')}</code></td>
|
||||
<td class="voucher-note" data-label="<?= __('label_note') ?>" title="${escapeHtml(v.note||'-')}">${escapeHtml(v.note||'-')}</td>
|
||||
<td data-label="<?= __('label_status') ?>">${statusBadge}</td>
|
||||
<td data-label="<?= __('label_usage') ?>"><div class="usage-info"><span>${v.used}/${v.quota>0?v.quota:'∞'}</span>${v.quota>0?`<div class="usage-bar"><div class="usage-bar-fill" style="width:${usagePct}%"></div></div>`:''}</div></td>
|
||||
<td data-label="<?= __('label_expires') ?>">${remaining?`<span style="color:var(--success)"><i class="fas fa-clock" aria-hidden="true"></i> ${remaining}</span><br>`:''}<small style="color:var(--text-muted)">${v.duration} <?= __('label_minutes_short') ?></small></td>
|
||||
<td data-label="<?= __('label_actions') ?>" style="white-space:nowrap;">
|
||||
<button onclick="resendVoucher('${v._id}','${escapeHtml(v.formatted_code||'')}')" class="btn btn-secondary btn-sm" title="<?= __('vouchers_resend') ?>" aria-label="<?= __('vouchers_resend') ?>"><i class="fas fa-envelope" aria-hidden="true"></i></button>
|
||||
<button onclick="deleteVoucher('${v._id}')" class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>" aria-label="<?= __('btn_delete') ?>"><i class="fas fa-trash" aria-hidden="true"></i></button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
|
@ -356,11 +358,11 @@ function renderVouchers() {
|
|||
<span style="font-size:13px;color:var(--text-muted);">${<?= json_encode(__('vouchers_page_of')) ?>
|
||||
.replace('{current}',currentPage).replace('{total}',totalPages)} (${vouchers.length})</span>
|
||||
<div style="display:flex;gap:5px;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage-1})" ${currentPage<=1?'disabled':''}><i class="fas fa-chevron-left"></i></button>`;
|
||||
<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage-1})" ${currentPage<=1?'disabled':''}><i class="fas fa-chevron-left" aria-hidden="true"></i></button>`;
|
||||
for (let p=Math.max(1,currentPage-2); p<=Math.min(totalPages,currentPage+2); p++) {
|
||||
html += `<button class="btn btn-sm ${p===currentPage?'btn-primary':'btn-secondary'}" onclick="setPage(${p})">${p}</button>`;
|
||||
}
|
||||
html += `<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage+1})" ${currentPage>=totalPages?'disabled':''}><i class="fas fa-chevron-right"></i></button>
|
||||
html += `<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage+1})" ${currentPage>=totalPages?'disabled':''}><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue