Quelltext des aktuellen Standes
<?php
/**
* Globale Stoppuhr-Pill — wird in jedem Layout (main, admin) eingebunden.
*
* Sichtbar nur, wenn der eingeloggte User einen aktiven Timer hat.
* Tickt sekündlich per JS; Klick führt zum Ticket; "Stop"-Button
* submittet an /tickets/{id}/timer/stop.
*
* Erwartet aus dem View-Scope:
* $_global_running_timer Array oder null
* $csrf_token aus Application::render()
*/
$tim = $_global_running_timer ?? null;
if (!$tim) return;
// Server-autoritative verstrichene Sekunden (zeitzonensicher, via DB-Uhr).
$elapsedSec = (int)($tim['elapsed_seconds'] ?? 0);
$isPaused = !empty($tim['paused_at']);
$ticketId = (int)$tim['ticket_id'];
$subj = (string)($tim['ticket_subject'] ?? '');
// Sehr lange Betreffs kappen, damit die Pill nicht halb durchs Layout
// schießt.
$subjShort = mb_strimwidth($subj, 0, 32, '…');
?>
<style>
/* Inline-CSS, weil die Pill modulübergreifend sichtbar ist und
tickets.css nicht global geladen wird. Selektoren sind eindeutig
genug, um nicht in andere Module zu lecken. */
.global-timer-pill {
display: inline-flex; align-items: center; gap: 0.5rem;
background: var(--emerald-50); border: 1px solid var(--emerald-300); color: var(--emerald-700);
padding: 0.3rem 0.7rem; border-radius: 9999px;
font-size: 0.82rem; font-weight: 600; text-decoration: none;
transition: background-color .12s, transform .12s;
}
.global-timer-pill:hover { background: var(--emerald-100); transform: translateY(-1px); color: var(--emerald-700); }
.global-timer-pill.is-paused { background: var(--amber-100); border-color: var(--amber-300); color: var(--amber-800); }
.global-timer-pill.is-paused:hover { background: var(--amber-200); color: var(--amber-800); }
.global-timer-pill .dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--emerald-500); flex-shrink: 0;
animation: gtpPulse 1.4s ease-in-out infinite;
}
.global-timer-pill.is-paused .dot { background: var(--amber-500); animation: none; }
.global-timer-pill .clock {
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
font-weight: 700; letter-spacing: 0.02em;
}
.global-timer-pill .stop-btn {
border: none; background: rgba(220, 38, 38, 0.1); color: var(--red-700);
border-radius: 9999px; padding: 0.15rem 0.55rem;
font-size: 0.72rem; cursor: pointer; font-weight: 700;
margin-left: 0.2rem;
}
.global-timer-pill .stop-btn:hover { background: rgba(220, 38, 38, 0.2); }
@keyframes gtpPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
</style>
<a class="global-timer-pill <?= $isPaused ? 'is-paused' : '' ?>"
href="/tickets/<?= $ticketId ?>"
title="<?= htmlspecialchars('Timer läuft auf #' . $ticketId . ' · ' . $subj, ENT_QUOTES) ?>"
id="global-timer-pill"
data-elapsed-seconds="<?= $elapsedSec ?>"
data-paused="<?= $isPaused ? '1' : '0' ?>">
<span class="dot" aria-hidden="true"></span>
<span>#<?= $ticketId ?></span>
<span class="clock" id="global-timer-clock">00:00:00</span>
<form method="POST" action="/tickets/<?= $ticketId ?>/timer/stop"
style="display:inline;margin:0;padding:0;" data-dialog-keep>
<input type="hidden" name="_csrf" value="<?= htmlspecialchars($csrf_token ?? '', ENT_QUOTES) ?>">
<button type="submit" class="stop-btn"
data-dialog-keep data-confirm-stop="Timer wirklich stoppen?"
title="<?= $this->te('Timer stoppen') ?>"><?= $this->te('⏹ Stop') ?></button>
</form>
</a>
<script<?= $this->scriptNonce() ?>>
(function () {
var pill = document.getElementById('global-timer-pill');
if (!pill) return;
// Server liefert verstrichene Sekunden; clientseitig relativ zum Seitenladen
// weiterzählen (zeitzonenunabhängig).
var baseElapsed = parseInt(pill.dataset.elapsedSeconds, 10) || 0;
var isPaused = pill.dataset.paused === '1';
var pageLoadMs = Date.now();
var clockEl = document.getElementById('global-timer-clock');
function format(sec) {
if (sec < 0) sec = 0;
var h = Math.floor(sec / 3600);
var m = Math.floor((sec % 3600) / 60);
var s = sec % 60;
return (h < 10 ? '0' : '') + h + ':' +
(m < 10 ? '0' : '') + m + ':' +
(s < 10 ? '0' : '') + s;
}
function tick() {
var elapsed = isPaused
? baseElapsed
: baseElapsed + Math.floor((Date.now() - pageLoadMs) / 1000);
if (clockEl) clockEl.textContent = format(Math.max(0, elapsed));
}
tick();
setInterval(tick, 1000);
})();
</script>