diff --git a/frontend/src/lib/components/RunningTimesToast.svelte b/frontend/src/lib/components/RunningTimesToast.svelte new file mode 100644 index 0000000..43e6168 --- /dev/null +++ b/frontend/src/lib/components/RunningTimesToast.svelte @@ -0,0 +1,127 @@ + + +{#if running.length > 0} + +
+
+
+ + + + + + + {running.length === 1 ? 'Zeit läuft' : `${running.length} Zeiten laufen`} + + + {#if !open} + + {running[0].formattedTime} + + {/if} + + {#if !single} + + {/if} +
+ + {#if open} + + {/if} +
+
+ + +{/if} diff --git a/frontend/src/lib/components/StopTimeDialog.svelte b/frontend/src/lib/components/StopTimeDialog.svelte new file mode 100644 index 0000000..8483908 --- /dev/null +++ b/frontend/src/lib/components/StopTimeDialog.svelte @@ -0,0 +1,84 @@ + + + { + if (!open) close() + }} +> + + + Zeit stoppen? + + {#if time} + #{rider?.number ?? '—'} {rider ? fullName(rider) : 'Unbekannter Fahrer'} + {:else} + Diese Zeit läuft nicht mehr. + {/if} + + + + {#if time} +

+ {time.formattedTime} +

+ {/if} + + {#if error} +

{error}

+ {/if} + + + + + + +
+
diff --git a/frontend/src/lib/components/ui/table/table-header.svelte b/frontend/src/lib/components/ui/table/table-header.svelte index f47d259..72c02fc 100644 --- a/frontend/src/lib/components/ui/table/table-header.svelte +++ b/frontend/src/lib/components/ui/table/table-header.svelte @@ -13,7 +13,7 @@ {@render children?.()} diff --git a/frontend/src/lib/stores/times.svelte.ts b/frontend/src/lib/stores/times.svelte.ts index d87b30d..282abfa 100644 --- a/frontend/src/lib/stores/times.svelte.ts +++ b/frontend/src/lib/stores/times.svelte.ts @@ -166,8 +166,18 @@ export class TimeStore { } get running(): TimeWithDuration[] { - const now = Date.now() - return this.scoped.filter((r) => r.status === 'active').map((r) => enrich(r, now)) + return this.allRunning() + } + + /** + * Alle laufenden Zeiten des Teams, run-übergreifend — der am längsten + * laufende zuerst. `now` wie bei `getByRun`, damit die Anzeige mitzählt. + */ + allRunning(now = Date.now()): TimeWithDuration[] { + return this.scoped + .filter((r) => r.status === 'active') + .map((r) => enrich(r, now)) + .sort((a, b) => (a.start < b.start ? -1 : 1)) } get finished(): TimeWithDuration[] { diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index c0ccd0a..924fd2c 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -15,6 +15,7 @@ import { Separator } from '@/components/ui/separator' import Logo from '$lib/components/Logo.svelte' import TeamSwitcher from '$lib/components/TeamSwitcher.svelte' + import RunningTimesToast from '$lib/components/RunningTimesToast.svelte' import { Home, Calendar, LogOut, Menu, X, Moon, Sun, Route, Settings } from 'lucide-svelte' import { toggleMode, mode } from 'mode-watcher' import { onDestroy, onMount } from 'svelte' @@ -182,5 +183,8 @@
{@render children?.()}
+ + + {/if} diff --git a/frontend/src/routes/dashboard/events/[id]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/+page.svelte index 6293a1a..bea292a 100644 --- a/frontend/src/routes/dashboard/events/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/+page.svelte @@ -199,7 +199,7 @@ {@const done = times.getByRider(rider.id).filter((t) => t.status === 'finished').length} goto(riderPath(rider.id))}> - {rider.number || '—'} + {rider.number || '—'} diff --git a/frontend/src/routes/dashboard/events/[id]/riders/[riderId]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/riders/[riderId]/+page.svelte index 123d464..a3871da 100644 --- a/frontend/src/routes/dashboard/events/[id]/riders/[riderId]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/riders/[riderId]/+page.svelte @@ -97,7 +97,7 @@ {event.name || 'Event'}
- {rider.number || '—'} + {rider.number || '—'}

{fullName(rider) || 'Ohne Name'}

@@ -118,7 +118,7 @@ Bestzeit -
+
{stats.best != null ? formatDuration(stats.best) : '—'}
@@ -128,7 +128,7 @@ Gesamtzeit -
+
{stats.done > 0 ? formatDuration(stats.total) : '—'}
@@ -185,7 +185,7 @@ {/if} - + {#if row.active} {row.active.formattedTime} {:else if row.best} @@ -196,7 +196,7 @@ {#if row.placement} - + {row.placement} {:else} diff --git a/frontend/src/routes/dashboard/events/[id]/runs/[runId]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/runs/[runId]/+page.svelte index 2d157a3..a16545a 100644 --- a/frontend/src/routes/dashboard/events/[id]/runs/[runId]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/runs/[runId]/+page.svelte @@ -9,9 +9,10 @@ import { getEventContext } from '$lib/stores/events.svelte' import { getRunContext } from '$lib/stores/runs.svelte' import { fullName, getRiderContext } from '$lib/stores/riders.svelte' - import { getTimeContext, type TimeStatus } from '$lib/stores/times.svelte' + import { getTimeContext } from '$lib/stores/times.svelte' import { ticker } from '$lib/stores/ticker.svelte' import { TIME_STATUS_LABEL } from '$lib/time' + import StopTimeDialog from '$lib/components/StopTimeDialog.svelte' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' @@ -19,7 +20,7 @@ import * as Dialog from '@/components/ui/dialog' import * as Table from '@/components/ui/table' import { - ArrowLeft, Clock, Download, Flag, Play, Search, Square, Timer, Trophy, + ArrowLeft, Download, Flag, Play, Search, Square, Timer, Trophy, } from 'lucide-svelte' const events = getEventContext() @@ -41,6 +42,16 @@ const leaderboard = $derived(valid && run ? times.getLeaderboard(run.id) : []) const unranked = $derived(valid && run ? times.unrankedByRun(run.id) : []) + /** + * Laufende und gewertete Zeiten in einer Liste: Wer gerade unterwegs ist, + * steht oben ohne Platz, darunter die Rangliste. So sieht man beides auf + * einen Blick, statt zwischen zwei Karten zu springen. + */ + const rows = $derived([ + ...running.map((time) => ({ time, place: null as number | null })), + ...leaderboard.map((time, i) => ({ time, place: i + 1 })), + ]) + function rider(id: string | undefined) { return id ? riders.getById(id) : undefined } @@ -92,30 +103,12 @@ } // --- Zeitnahme stoppen ----------------------------------------------- - let stopDialog = $state(false) + // Dialog und Fehlerbehandlung stecken in StopTimeDialog; hier steht nur, + // welche Zeit gerade gestoppt werden soll. let stopTimeId = $state(null) - let stopError = $state(null) - - // Aus der laufenden Liste gelesen, damit die Anzeige im Dialog mitzählt. - const stopTime = $derived(running.find((t) => t.id === stopTimeId)) function openStop(timeId: string) { stopTimeId = timeId - stopError = null - stopDialog = true - } - - async function doStop(status: 'finished' | 'dnf') { - if (!stopTimeId) return - - stopError = null - try { - await times.stop(stopTimeId, status as TimeStatus) - stopDialog = false - stopTimeId = null - } catch (e: any) { - stopError = e.message ?? 'Die Zeit konnte nicht gestoppt werden.' - } } // --- Export ------------------------------------------------------------ @@ -202,62 +195,18 @@ {/if} - - - - - - Laufende Zeiten ({running.length}) - - - - {#if running.length === 0} -

- Keine laufende Zeit. Über „Zeitnahme starten" geht es los. -

- {:else} -
    - {#each running as t (t.id)} - {@const r = rider(t.rider)} -
  • - - -
  • - {/each} -
- {/if} -
-
- - + - Rangliste ({leaderboard.length}) + Zeiten ({leaderboard.length} gewertet{running.length ? `, ${running.length} laufend` : ''}) - {#if leaderboard.length === 0} + {#if rows.length === 0}

- Für diesen Run wurden noch keine Zeiten gewertet. + Noch keine Zeit. Über „Zeitnahme starten" geht es los.

{:else} @@ -268,19 +217,29 @@ Name Zeit Korrektur + - {#each leaderboard as t, i (t.id)} + {#each rows as row (row.time.id)} + {@const t = row.time} {@const r = rider(t.rider)} - + {@const live = row.place === null} + openStop(t.id) : undefined} + > - - {i + 1} - + {#if live} + läuft + {:else} + + {row.place} + + {/if} - {r?.number ?? '—'} + {r?.number ?? '—'} {#if r} @@ -289,10 +248,25 @@ — {/if} - {t.formattedTime} - + + {t.formattedTime} + + {t.correction ? `${t.correction > 0 ? '+' : ''}${t.correction.toFixed(1)}s` : '–'} + e.stopPropagation()}> + {#if live} + + + {/if} + {/each} @@ -315,7 +289,7 @@ {@const r = rider(t.rider)}
  • - {r?.number ?? '—'} + {r?.number ?? '—'} {#if r} {fullName(r) || '—'} @@ -369,7 +343,7 @@ onclick={() => startFor(c.rider.id)} >
    - {c.rider.number || '—'} + {c.rider.number || '—'} {fullName(c.rider) || '—'}
    {#if c.active} @@ -392,40 +366,4 @@ - - - - - Zeit stoppen? - - {#if stopTime} - {@const r = rider(stopTime.rider)} - #{r?.number ?? '—'} {r ? fullName(r) : 'Unbekannter Fahrer'} - {:else} - Diese Zeit läuft nicht mehr. - {/if} - - - - {#if stopTime} -

    - {stopTime.formattedTime} -

    - {/if} - - {#if stopError} -

    {stopError}

    - {/if} - - - - - - -
    -
    +