Start und Stopp laufen ausschliesslich ueber TimeDialog. Der fuehrt immer beide Angaben - Fahrer und Run -, auch wenn eine davon aus dem Kontext vorbelegt ist: Beim Stoppen an der Strecke soll kein Zweifel bleiben, wen man gerade erwischt. DNF fragt nach, bevor es schreibt. Eine Zeit ohne Wertung laesst sich nicht zurueckholen, ein Fehlgriff waere also endgueltig. Tabelle und Toast-Leiste zeigen damit nur noch an; ihre Knoepfe oeffnen den Dialog mit passender Vorbelegung. Der alte Stopp-Dialog und der Fahrerlisten-Start entfallen - beides waren Ein-Tipp-Aktionen und damit genau das, was hier nicht mehr sein soll. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
149 lines
6 KiB
Svelte
149 lines
6 KiB
Svelte
<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 Run-Seite.
|
|
*
|
|
* Bei mehreren Zeiten zeigt die Leiste eingeklappt nur die Zahl und die
|
|
* längste Zeit; ausgeklappt alle. Bei genau einer entfällt das Klappen.
|
|
*
|
|
* Gestoppt wird auch von hier aus nicht direkt: Der Knopf öffnet den
|
|
* Zeit-Dialog mit Fahrer und Run vorbelegt.
|
|
*/
|
|
import { getTimeContext } from '$lib/stores/times.svelte'
|
|
import { fullName, getRiderContext } from '$lib/stores/riders.svelte'
|
|
import { getRunContext } from '$lib/stores/runs.svelte'
|
|
import { ticker } from '$lib/stores/ticker.svelte'
|
|
import TimeDialog from './TimeDialog.svelte'
|
|
import { Button } from '@/components/ui/button'
|
|
import { ChevronDown, ChevronUp, Timer } from 'lucide-svelte'
|
|
|
|
const times = getTimeContext()
|
|
const riders = getRiderContext()
|
|
const runs = getRunContext()
|
|
const clock = ticker()
|
|
|
|
const running = $derived(times.allRunning(clock.now))
|
|
const single = $derived(running.length === 1)
|
|
|
|
let expanded = $state(false)
|
|
|
|
// Vorbelegung für den Zeit-Dialog
|
|
let timeDialog = $state(false)
|
|
let dialogEvent = $state('')
|
|
let dialogRider = $state<string | null>(null)
|
|
let dialogRun = $state<string | null>(null)
|
|
|
|
function openTimeDialog(riderId: string | undefined, runId: string | undefined) {
|
|
const run = runId ? runs.getById(runId) : undefined
|
|
if (!run?.event) return
|
|
|
|
dialogEvent = run.event
|
|
dialogRider = riderId ?? null
|
|
dialogRun = run.id
|
|
timeDialog = true
|
|
}
|
|
|
|
// Eine einzelne Zeit steht immer offen da; bei mehreren entscheidet der
|
|
// Nutzer. Fällt die Anzahl wieder auf eine, ist der Zustand egal.
|
|
const open = $derived(single || expanded)
|
|
|
|
function riderLabel(riderId: string | undefined) {
|
|
const r = riderId ? riders.getById(riderId) : undefined
|
|
if (!r) return 'Unbekannter Fahrer'
|
|
return `#${r.number || '—'} ${fullName(r)}`.trim()
|
|
}
|
|
|
|
function runPath(runId: string | undefined) {
|
|
const run = runId ? runs.getById(runId) : undefined
|
|
if (!run?.event) return null
|
|
return `/dashboard/events/${run.event}/runs/${run.id}`
|
|
}
|
|
|
|
function runLabel(runId: string | undefined) {
|
|
const run = runId ? runs.getById(runId) : undefined
|
|
return run?.description || 'Run'
|
|
}
|
|
</script>
|
|
|
|
{#if running.length > 0}
|
|
<!-- 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">
|
|
<div class="flex items-center gap-2 px-3 py-2 border-b bg-primary/5">
|
|
<span class="relative flex size-2 shrink-0">
|
|
<span class="absolute inline-flex h-full w-full rounded-full bg-primary opacity-60 animate-ping"></span>
|
|
<span class="relative inline-flex size-2 rounded-full bg-primary"></span>
|
|
</span>
|
|
|
|
<span class="text-sm font-medium">
|
|
{running.length === 1 ? 'Zeit läuft' : `${running.length} Zeiten laufen`}
|
|
</span>
|
|
|
|
{#if !open}
|
|
<span class="text-sm tabular-nums font-semibold ml-auto">
|
|
{running[0].formattedTime}
|
|
</span>
|
|
{/if}
|
|
|
|
{#if !single}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="ml-auto size-7 shrink-0"
|
|
title={expanded ? 'Einklappen' : 'Alle anzeigen'}
|
|
onclick={() => (expanded = !expanded)}
|
|
>
|
|
{#if expanded}
|
|
<ChevronDown class="size-4" />
|
|
{:else}
|
|
<ChevronUp class="size-4" />
|
|
{/if}
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if open}
|
|
<ul class="divide-y max-h-[50vh] overflow-y-auto">
|
|
{#each running as t (t.id)}
|
|
{@const path = runPath(t.run)}
|
|
<li class="flex items-center gap-2 px-3 py-2">
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium truncate">{riderLabel(t.rider)}</p>
|
|
<p class="text-xs text-muted-foreground truncate">
|
|
{#if path}
|
|
<a href={path} class="hover:underline">{runLabel(t.run)}</a>
|
|
{:else}
|
|
{runLabel(t.run)}
|
|
{/if}
|
|
</p>
|
|
</div>
|
|
|
|
<span class="text-lg font-semibold tabular-nums shrink-0">
|
|
{t.formattedTime}
|
|
</span>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="shrink-0"
|
|
title="Zeit erfassen"
|
|
onclick={() => openTimeDialog(t.rider, t.run)}
|
|
>
|
|
<Timer class="size-4" />
|
|
</Button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<TimeDialog
|
|
bind:open={timeDialog}
|
|
eventId={dialogEvent}
|
|
riderId={dialogRider}
|
|
runId={dialogRun}
|
|
/>
|
|
{/if}
|