Setzt die Rechte aus dem vorigen Commit in den Oberflaechen um und
ergaenzt die fehlenden Verwaltungsseiten.
Neu:
- /admin/users - Konten anlegen, Rolle und Sperre setzen, Passwort
zuruecksetzen, Gruppenzuordnung je Konto
- /admin/groups - Gruppen anlegen und umbenennen, Mitglieder samt
Rolle pflegen, zugeordnete Domains einsehen
- /admin/profile - eigenes Konto: Gruppenzugehoerigkeit ansehen und
das Passwort aendern. Das fehlte bisher vollstaendig, auch fuer
den einen Administrator.
Domains sind jetzt Administratorsache, weil dort die Gruppe zugewiesen
wird. Kurzlinks, Statistik und QR-Codes richten sich nach der Gruppe:
ein fremder Kurzlink antwortet mit 404 (seine Existenz geht den
Aufrufer nichts an), ein sichtbarer ohne Schreibrecht mit 403.
Geschlossene Luecke: /admin/uploads/{file} pruefte bisher nur, ob
ueberhaupt jemand angemeldet ist. Da alle Logos in einem gemeinsamen
Verzeichnis liegen, war der Dateiname die einzige Huerde. Ausgeliefert
wird jetzt nur noch, was laut Datenbank zu einem Kurzlink aus einer
Gruppe des Aufrufers gehoert.
Die Navigation blendet aus, wofuer die Rechte fehlen - das ist
Bedienkomfort, kein Zugriffsschutz: die Routen pruefen unabhaengig
davon noch einmal.
Der Setup-Assistent legt das erste Konto als Administrator an und
traegt es gleich als Editor der Gruppe "Standard" ein. Ohne das
koennte es zwar verwalten, aber selbst keine Kurzlinks anlegen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
7.1 KiB
PHP
173 lines
7.1 KiB
PHP
<?php
|
||
|
||
/**
|
||
* @var array{links:int,clicks:int,active:int} $summary
|
||
* @var list<array<string,mixed>> $recentLinks
|
||
* @var list<array<string,mixed>> $topLinks
|
||
* @var list<array{date:string,clicks:int}> $series
|
||
* @var int $domainCount
|
||
* @var bool $isAdmin
|
||
* @var bool $canCreate
|
||
* @var list<array<string,mixed>> $myGroups
|
||
* @var int|null $groupCount nur fuer Administratoren
|
||
* @var int|null $userCount nur fuer Administratoren
|
||
* @var App\Config $config
|
||
*/
|
||
|
||
use App\Repository\LinkRepository;
|
||
use App\Service\SystemCheck;
|
||
use App\Support\ShortDomain;
|
||
|
||
$fallbackBase = rtrim($config->baseUrl(), '/');
|
||
?>
|
||
<?php if (($systemProblems ?? []) !== []):
|
||
$hatFehler = false;
|
||
foreach ($systemProblems as $problem) {
|
||
if ($problem['status'] === SystemCheck::ERROR) {
|
||
$hatFehler = true;
|
||
}
|
||
}
|
||
?>
|
||
<section class="card check-result check-result--<?= $hatFehler ? 'error' : 'warn' ?>">
|
||
<div class="check-result__head">
|
||
<span class="check-result__icon" aria-hidden="true"><?= $hatFehler ? '×' : '!' ?></span>
|
||
<div>
|
||
<h2>Hinweise zum Server</h2>
|
||
<p class="check-result__summary">
|
||
<?= $hatFehler
|
||
? 'Es fehlt etwas, das die Anwendung braucht.'
|
||
: 'Alles läuft – einzelne Zusatzfunktionen stehen aber nicht zur Verfügung.' ?>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<ul class="check-list">
|
||
<?php foreach ($systemProblems as $problem): ?>
|
||
<li class="check-list__item check-list__item--<?= $problem['status'] === SystemCheck::ERROR ? 'error' : 'warn' ?>">
|
||
<span class="check-list__icon" aria-hidden="true"><?= $problem['status'] === SystemCheck::ERROR ? '×' : '!' ?></span>
|
||
<span class="check-list__label"><?= $e($problem['label']) ?></span>
|
||
<span class="check-list__text"><?= $e($problem['text']) ?></span>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</section>
|
||
<?php endif; ?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>Übersicht</h1>
|
||
<p class="muted">
|
||
<?php if ($isAdmin): ?>
|
||
Alle Kurzlinks und Klicks auf einen Blick.
|
||
<?php elseif ($myGroups === []): ?>
|
||
Sie sind noch in keiner Gruppe – deshalb ist hier noch nichts zu sehen.
|
||
<?php else: ?>
|
||
Kurzlinks und Klicks Ihrer Gruppen:
|
||
<?= $e(implode(', ', array_column($myGroups, 'name'))) ?>.
|
||
<?php endif; ?>
|
||
</p>
|
||
</div>
|
||
<?php if ($canCreate): ?>
|
||
<a href="<?= $e($url('/admin/links/new')) ?>" class="btn btn--primary">Neuer Kurzlink</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat">
|
||
<span class="stat__label">Kurzlinks</span>
|
||
<span class="stat__value"><?= (int) $summary['links'] ?></span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat__label">davon aktiv</span>
|
||
<span class="stat__value"><?= (int) $summary['active'] ?></span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat__label">Klicks gesamt</span>
|
||
<span class="stat__value"><?= number_format((int) $summary['clicks'], 0, ',', '.') ?></span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat__label">Klicks (30 Tage)</span>
|
||
<span class="stat__value"><?= number_format(array_sum(array_column($series, 'clicks')), 0, ',', '.') ?></span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat__label">Domains</span>
|
||
<span class="stat__value">
|
||
<?php if ($isAdmin): ?>
|
||
<a href="<?= $e($url('/admin/domains')) ?>"><?= (int) $domainCount ?></a>
|
||
<?php else: ?>
|
||
<?= (int) $domainCount ?>
|
||
<?php endif; ?>
|
||
</span>
|
||
</div>
|
||
<?php if ($isAdmin): ?>
|
||
<div class="stat">
|
||
<span class="stat__label">Gruppen</span>
|
||
<span class="stat__value">
|
||
<a href="<?= $e($url('/admin/groups')) ?>"><?= (int) $groupCount ?></a>
|
||
</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat__label">Benutzer</span>
|
||
<span class="stat__value">
|
||
<a href="<?= $e($url('/admin/users')) ?>"><?= (int) $userCount ?></a>
|
||
</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<section class="card">
|
||
<h2>Klicks der letzten 30 Tage</h2>
|
||
<div class="chart" data-chart="line" data-series="<?= $e(json_encode($series, JSON_UNESCAPED_SLASHES)) ?>">
|
||
<canvas height="120" aria-label="Liniendiagramm der Klicks pro Tag"></canvas>
|
||
<noscript><p class="muted">Für das Diagramm wird JavaScript benötigt.</p></noscript>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="grid grid--2">
|
||
<section class="card">
|
||
<h2>Zuletzt angelegt</h2>
|
||
<?php if ($recentLinks === []): ?>
|
||
<p class="muted">Noch keine Kurzlinks vorhanden.
|
||
<?php if ($canCreate): ?>
|
||
<a href="<?= $e($url('/admin/links/new')) ?>">Jetzt den ersten anlegen.</a>
|
||
<?php endif; ?>
|
||
</p>
|
||
<?php else: ?>
|
||
<ul class="list">
|
||
<?php foreach ($recentLinks as $link): ?>
|
||
<li class="list__item">
|
||
<div class="list__main">
|
||
<a class="mono" href="<?= $e($url('/admin/links/' . $link['id'] . '/stats')) ?>"><?php
|
||
if ($domainCount > 1 && $link['domain_base_url'] !== null): ?><span class="linkcopy__domain"><?=
|
||
$e(ShortDomain::displayName((string) $link['domain_base_url'])) ?></span><?php
|
||
endif; ?>/<?= $e($link['slug']) ?></a>
|
||
<span class="muted small truncate"><?= $e($link['target_url']) ?></span>
|
||
</div>
|
||
<span class="badge"><?= (int) $link['click_count'] ?> Klicks</span>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2>Meistgeklickt</h2>
|
||
<?php if ($topLinks === [] || (int) $summary['clicks'] === 0): ?>
|
||
<p class="muted">Noch keine Klicks erfasst.</p>
|
||
<?php else: ?>
|
||
<ul class="list">
|
||
<?php foreach ($topLinks as $link): ?>
|
||
<li class="list__item">
|
||
<div class="list__main">
|
||
<a class="mono" href="<?= $e($url('/admin/links/' . $link['id'] . '/stats')) ?>"><?php
|
||
if ($domainCount > 1 && $link['domain_base_url'] !== null): ?><span class="linkcopy__domain"><?=
|
||
$e(ShortDomain::displayName((string) $link['domain_base_url'])) ?></span><?php
|
||
endif; ?>/<?= $e($link['slug']) ?></a>
|
||
<span class="muted small truncate"><?= $e($link['target_url']) ?></span>
|
||
</div>
|
||
<span class="badge"><?= (int) $link['click_count'] ?> Klicks</span>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
</section>
|
||
</div>
|