import { useEffect, useState } from 'react' import { api } from '../api.js' function fmtTs(ts) { if (!ts) return '—' return new Date(ts).toLocaleString('no-NO', { timeZone: 'UTC', hour12: false }) } function imgSrc(path) { if (!path) return null return `/api/images/${path.replace(/^\/processed\//, '')}` } function ImageSlider({ passageId, currentImage }) { const [images, setImages] = useState([]) const [index, setIndex] = useState(0) useEffect(() => { api.getPassageImages(passageId).then(imgs => { setImages(imgs) // Start på siste bilde (passeringstidspunktet) setIndex(imgs.length > 0 ? imgs.length - 1 : 0) }) }, [passageId]) if (images.length === 0) { const src = imgSrc(currentImage) return src ? Passeringsbilde :
Bilde ikke tilgjengelig
} const current = images[index] return (
{`Bilde {images.length > 1 && (
setIndex(Number(e.target.value))} style={{ width: '100%' }} />
{fmtTs(images[0].timestamp_utc)} {index + 1} / {images.length} {index === images.length - 1 && ' — passeringstid'} {fmtTs(images[images.length - 1].timestamp_utc)}
)}
) } function ReviewCard({ passage, athletes, onResolved, onDeleted }) { const [bib, setBib] = useState(passage.bib_number || '') const [note, setNote] = useState('') const [saving, setSaving] = useState(false) const matchedAthlete = athletes.find(a => a.bib_number === bib) const handleResolve = async () => { setSaving(true) try { await api.resolvePassage(passage.passage_id, { bib_number: bib || null, profile_id: matchedAthlete?.profile_id || null, review_note: note || null, }) onResolved() } finally { setSaving(false) } } const handleDelete = async () => { if (!confirm('Slett passering?')) return await api.deletePassage(passage.passage_id) onDeleted() } return (
Stasjon{passage.station}
Passeringstid{fmtTs(passage.timestamp_utc)}
OCR-resultat{passage.bib_number || 'ingen'}
Konfidens{passage.confidence != null ? (passage.confidence * 100).toFixed(0) + '%' : '—'}
Merknad{passage.review_note || '—'}
setBib(e.target.value)} placeholder="Skriv inn riktig startnummer" /> {matchedAthlete && ( {matchedAthlete.name}{matchedAthlete.club ? ` (${matchedAthlete.club})` : ''} )} {bib && !matchedAthlete && ( Startnummer ikke i startliste )}
setNote(e.target.value)} placeholder="f.eks. startnummer skjult av arm" />
) } export default function ReviewPage({ activeRace }) { const [queue, setQueue] = useState([]) const [athletes, setAthletes] = useState([]) const [loading, setLoading] = useState(true) const load = async () => { setLoading(true) const [q, a] = await Promise.all([ api.getReviewQueue(activeRace?.race_id), api.getAthletes(), ]) setQueue(q) setAthletes(a) setLoading(false) } useEffect(() => { load() }, []) const removeFromQueue = (passageId) => setQueue(q => q.filter(p => p.passage_id !== passageId)) return ( <>

Manuell gjennomgang

{loading ? (

Laster...

) : queue.length === 0 ? (

Ingen passeringer venter på gjennomgang.

) : ( <>
{queue.length} passering(er) venter på manuell behandling.
{queue.map(p => ( removeFromQueue(p.passage_id)} onDeleted={() => removeFromQueue(p.passage_id)} /> ))} )} ) }