refactor: Offline-Meldung in die Zeitnahme-Leiste, ganze Zeile klickbar
Die Offline-Meldung stand als eigener Streifen unter der Kopfleiste — eine zweite Statusflaeche neben der, die es dafuer schon gibt. Sie sitzt jetzt in der Leiste unten rechts, ganz oben und mit fallender Dringlichkeit darunter: fehlende Verbindung, nicht uebertragene Zeiten, laufende Uhren. Das Symbol in der Kopfleiste wird offline weiss auf rot. In der Zeitentabelle ist die ganze Zeile anklickbar statt nur die Spalte "Zeit" — und nur, solange die Uhr laeuft. Eine gewertete Zeit hat im Zeit-Dialog nichts zu holen; was an ihr noch zu aendern waere, aendert die Korrektur nebenan. Name und Korrektur halten den Klick auf: Der Name fuehrt zum Fahrer, nicht in den Dialog. Die Zeit bleibt bei laufender Uhr ein echter Knopf, denn ein <tr> nimmt keinen Fokus — ohne ihn waere die Zeile mit der Tastatur nicht erreichbar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
This commit is contained in:
parent
660e72717a
commit
76e2eeff0e
4 changed files with 105 additions and 90 deletions
|
|
@ -1,61 +0,0 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Die Meldung, die stehen bleibt, solange der Server nicht antwortet.
|
||||
*
|
||||
* Kein Toast, der nach fünf Sekunden geht: Wer an der Strecke steht und
|
||||
* nicht weiss, dass seine Zeiten nirgends ankommen, erfährt es sonst erst
|
||||
* am Abend. Sie verschwindet genau dann, wenn die Verbindung zurück ist.
|
||||
*
|
||||
* Die Nachricht ist bewusst zweiteilig — was nicht geht, und was trotzdem
|
||||
* geht. Die Zeitnahme läuft offline weiter, und das muss dastehen, sonst
|
||||
* hört jemand aus Vorsicht damit auf.
|
||||
*/
|
||||
import { connection } from '$lib/stores/connection.svelte'
|
||||
import { timeOutbox } from '$lib/stores/timeOutbox.svelte'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RefreshCw, WifiOff } from 'lucide-svelte'
|
||||
|
||||
function since(ms: number): string {
|
||||
const min = Math.floor(ms / 60000)
|
||||
if (min < 1) return 'gerade eben'
|
||||
if (min < 60) return `seit ${min} min`
|
||||
return `seit ${Math.floor(min / 60)} h`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !connection.online}
|
||||
<div class="sticky top-0 z-30 border-b border-amber-500/40 bg-amber-500/15">
|
||||
<div class="mx-auto flex max-w-7xl flex-wrap items-center gap-x-3 gap-y-1 px-4 py-2 sm:px-6 lg:px-8">
|
||||
<WifiOff class="size-4 shrink-0 text-amber-700 dark:text-amber-500" />
|
||||
|
||||
<span class="text-sm font-medium">
|
||||
Keine Verbindung zum Server
|
||||
{#if connection.offlineFor !== null}
|
||||
<span class="font-normal text-muted-foreground">
|
||||
({since(connection.offlineFor)})
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="text-sm text-muted-foreground">
|
||||
Zeitnahme läuft weiter — gestoppt wird auf diesem Gerät und übertragen,
|
||||
sobald der Server wieder antwortet.
|
||||
{#if timeOutbox.pending > 0}
|
||||
{timeOutbox.pending}
|
||||
{timeOutbox.pending === 1 ? 'Zeit wartet' : 'Zeiten warten'}.
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="ml-auto shrink-0"
|
||||
disabled={connection.checking}
|
||||
onclick={() => connection.check()}
|
||||
>
|
||||
<RefreshCw class="size-4 mr-2 {connection.checking ? 'animate-spin' : ''}" />
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Dauerhaft sichtbare Leiste über allen Dashboard-Seiten, solange
|
||||
* irgendwo eine Zeit läuft. Gedacht für die Strecke: Man legt zwischen
|
||||
* zwei Startern die Trails oder die Fahrerliste an, und die laufende Uhr
|
||||
* bleibt trotzdem im Blick — samt Stopp ohne Umweg über die Stage-Seite.
|
||||
* Die Leiste über allen Dashboard-Seiten: alles, was den Zeitnehmer
|
||||
* betrifft, an einer Stelle. Von oben nach unten mit fallender
|
||||
* Dringlichkeit — fehlende Verbindung, nicht übertragene Zeiten, laufende
|
||||
* Uhren.
|
||||
*
|
||||
* Gedacht für die Strecke: Man legt zwischen zwei Startern die Trails
|
||||
* oder die Fahrerliste an, und die laufende Uhr bleibt trotzdem im Blick —
|
||||
* samt Stopp ohne Umweg über die Stage-Seite.
|
||||
*
|
||||
* Aufgeklappt ist der Normalfall: Wer an der Strecke steht, will die
|
||||
* laufenden Zeiten sehen und nicht erst danach fragen. Einklappen geht,
|
||||
|
|
@ -18,9 +22,10 @@
|
|||
import { getEventParticipantContext } from '$lib/stores/eventParticipants.svelte'
|
||||
import { ticker } from '$lib/stores/ticker.svelte'
|
||||
import { timeOutbox } from '$lib/stores/timeOutbox.svelte'
|
||||
import { connection } from '$lib/stores/connection.svelte'
|
||||
import TimeDialog from './TimeDialog.svelte'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ChevronDown, ChevronUp, CloudOff, RefreshCw, Timer } from 'lucide-svelte'
|
||||
import { ChevronDown, ChevronUp, CloudOff, RefreshCw, Timer, WifiOff } from 'lucide-svelte'
|
||||
|
||||
const times = getTimeContext()
|
||||
const riders = getRiderContext()
|
||||
|
|
@ -68,9 +73,17 @@
|
|||
const stage = stageId ? stages.getById(stageId) : undefined
|
||||
return stage?.description || 'Stage'
|
||||
}
|
||||
|
||||
/** „seit 3 min" — wie lange die Verbindung schon fehlt. */
|
||||
function since(ms: number): string {
|
||||
const min = Math.floor(ms / 60000)
|
||||
if (min < 1) return 'gerade eben'
|
||||
if (min < 60) return `seit ${min} min`
|
||||
return `seit ${Math.floor(min / 60)} h`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if running.length > 0 || timeOutbox.pending > 0}
|
||||
{#if running.length > 0 || timeOutbox.pending > 0 || !connection.online}
|
||||
<!-- fixed statt sticky: Die Leiste soll auch beim Scrollen stehen bleiben. -->
|
||||
<div class="fixed bottom-4 right-4 z-50 w-[min(24rem,calc(100vw-2rem))]">
|
||||
<div class="rounded-lg border bg-card text-card-foreground shadow-lg overflow-hidden">
|
||||
|
|
@ -116,9 +129,44 @@
|
|||
</div>
|
||||
|
||||
<!--
|
||||
Nicht uebertragene Zeiten stehen ganz oben und nicht im
|
||||
Aufgeklappten: Eine Zeit, die nur auf diesem Geraet existiert,
|
||||
ist das Dringendste, was diese Leiste zu sagen hat.
|
||||
Die fehlende Verbindung steht ueber allem: Sie ist der Grund,
|
||||
aus dem die Zeiten darunter liegenbleiben. Die Meldung geht
|
||||
erst, wenn der Server wieder antwortet — an der Strecke waeren
|
||||
fuenf Sekunden Einblendung dieselbe Katastrophe wie gar keine.
|
||||
-->
|
||||
{#if !connection.online}
|
||||
<div class="flex items-start gap-2 border-b bg-amber-500/15 px-3 py-2">
|
||||
<WifiOff class="mt-0.5 size-4 shrink-0 text-amber-600" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-xs font-medium">
|
||||
Keine Verbindung
|
||||
{#if connection.offlineFor !== null}
|
||||
<span class="font-normal text-muted-foreground">
|
||||
({since(connection.offlineFor)})
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Die Zeitnahme läuft weiter. Gestoppt wird auf diesem Gerät,
|
||||
übertragen, sobald der Server wieder antwortet.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="size-7 shrink-0"
|
||||
tooltip="Verbindung prüfen"
|
||||
disabled={connection.checking}
|
||||
onclick={() => connection.check()}
|
||||
>
|
||||
<RefreshCw class="size-4 {connection.checking ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
Nicht uebertragene Zeiten darunter: Eine Zeit, die nur auf
|
||||
diesem Geraet existiert, ist das Naechstdringende.
|
||||
-->
|
||||
{#if timeOutbox.pending > 0}
|
||||
<div class="flex items-center gap-2 border-b bg-amber-500/10 px-3 py-2">
|
||||
|
|
@ -129,8 +177,6 @@
|
|||
Gerät.
|
||||
{#if timeOutbox.error}
|
||||
<span class="block text-destructive">{timeOutbox.error}</span>
|
||||
{:else if !timeOutbox.isOnline}
|
||||
<span class="block text-muted-foreground">Kein Netz.</span>
|
||||
{/if}
|
||||
</span>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
import Logo from '$lib/components/Logo.svelte'
|
||||
import TeamMenu from '$lib/components/TeamMenu.svelte'
|
||||
import CreateTeamDialog from '$lib/components/CreateTeamDialog.svelte'
|
||||
import OfflineBanner from '$lib/components/OfflineBanner.svelte'
|
||||
import { connection } from '$lib/stores/connection.svelte'
|
||||
import * as NavigationMenu from '@/components/ui/navigation-menu'
|
||||
import Avatar from '$lib/components/Avatar.svelte'
|
||||
|
|
@ -259,7 +258,9 @@
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="size-7"
|
||||
class="size-7 {connection.online
|
||||
? ''
|
||||
: 'bg-destructive text-white hover:bg-destructive/90 hover:text-white'}"
|
||||
onclick={() => connection.check()}
|
||||
tooltip={connection.online
|
||||
? 'Verbindung steht'
|
||||
|
|
@ -268,7 +269,7 @@
|
|||
{#if connection.online}
|
||||
<Wifi class="h-4 w-4 text-muted-foreground" />
|
||||
{:else}
|
||||
<WifiOff class="h-4 w-4 text-amber-600" />
|
||||
<WifiOff class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
|
|
@ -396,8 +397,6 @@
|
|||
{/if}
|
||||
</header>
|
||||
|
||||
<OfflineBanner />
|
||||
|
||||
<main class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -282,7 +282,20 @@
|
|||
{@const t = row.time}
|
||||
{@const r = rider(t.rider)}
|
||||
{@const live = row.place === null}
|
||||
<Table.Row class={live ? 'bg-primary/5' : undefined}>
|
||||
<!--
|
||||
Anklickbar ist die ganze Zeile, nicht nur
|
||||
die Zeit — und nur, solange die Uhr laeuft.
|
||||
Eine gewertete Zeit hat im Zeit-Dialog
|
||||
nichts zu holen: Sie ist gestoppt, und was
|
||||
an ihr noch zu aendern waere, aendert die
|
||||
Korrektur nebenan.
|
||||
-->
|
||||
<Table.Row
|
||||
class="{live ? 'bg-primary/5' : ''} {live
|
||||
? 'cursor-pointer hover:bg-primary/10'
|
||||
: ''}"
|
||||
onclick={live ? () => openTime(t.rider) : undefined}
|
||||
>
|
||||
<Table.Cell>
|
||||
{#if live}
|
||||
<Badge variant="outline" class="text-primary border-primary">läuft</Badge>
|
||||
|
|
@ -299,7 +312,15 @@
|
|||
</Table.Cell>
|
||||
<Table.Cell class="font-medium">
|
||||
{#if r}
|
||||
<a href={riderPath(r.id)} class="hover:underline">{riderName(r) || '—'}</a>
|
||||
<!-- stopPropagation: Der Name fuehrt zum Fahrer,
|
||||
nicht in den Zeit-Dialog. -->
|
||||
<a
|
||||
href={riderPath(r.id)}
|
||||
class="hover:underline"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{riderName(r) || '—'}
|
||||
</a>
|
||||
{:else}
|
||||
—
|
||||
{/if}
|
||||
|
|
@ -333,16 +354,23 @@
|
|||
Stelle, nur laenger.
|
||||
-->
|
||||
<Table.Cell class="p-0 text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-auto w-full justify-end px-4 py-3 tabular-nums {live
|
||||
? 'font-semibold'
|
||||
: 'font-normal'}"
|
||||
tooltip={live ? 'Zeit stoppen' : 'Zeit erfassen'}
|
||||
onclick={() => openTime(t.rider)}
|
||||
>
|
||||
{t.formattedTime}
|
||||
</Button>
|
||||
{#if live}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-auto w-full justify-end px-4 py-3 font-semibold tabular-nums"
|
||||
tooltip="Zeit stoppen"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
openTime(t.rider)
|
||||
}}
|
||||
>
|
||||
{t.formattedTime}
|
||||
</Button>
|
||||
{:else}
|
||||
<span class="block px-4 py-3 tabular-nums">
|
||||
{t.formattedTime}
|
||||
</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<Button
|
||||
|
|
@ -352,7 +380,10 @@
|
|||
? ''
|
||||
: 'text-muted-foreground'}"
|
||||
tooltip="Zeitkorrektur"
|
||||
onclick={() => openPenalty(t.id, t.correction ?? 0)}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
openPenalty(t.id, t.correction ?? 0)
|
||||
}}
|
||||
>
|
||||
{t.correction
|
||||
? `${t.correction > 0 ? '+' : ''}${t.correction.toFixed(1)} s`
|
||||
|
|
|
|||
Loading…
Reference in a new issue