Files
shooting-event/backend/stage_sessions.go

173 lines
3.9 KiB
Go
Raw Permalink Normal View History

2026-05-18 13:22:11 +04:00
package main
import (
"database/sql"
"fmt"
"strconv"
"strings"
"time"
)
const (
stagePhasePreliminary = "preliminary"
stagePhasePrelimTie = "prelim_tiebreak"
stagePhaseFinal = "final"
stagePhaseFinalTie = "final_tiebreak"
)
var allowedStagePhases = map[string]bool{
stagePhasePreliminary: true,
stagePhasePrelimTie: true,
stagePhaseFinal: true,
stagePhaseFinalTie: true,
}
func normalizeStagePhase(raw string) (string, bool) {
phase := strings.ToLower(strings.TrimSpace(raw))
if !allowedStagePhases[phase] {
return "", false
}
return phase, true
}
func normalizeStageGroup(phase string, raw string) (string, bool) {
group := strings.TrimSpace(raw)
switch phase {
case stagePhasePreliminary:
if strings.EqualFold(group, positionBoardUnassigned) {
return positionBoardUnassigned, true
}
group = normalizePreliminaryGroupKey(group)
if group == "" {
return "", false
}
return group, true
case stagePhaseFinal:
parsed, err := strconv.Atoi(group)
if err != nil || (parsed != 1 && parsed != 2) {
return "", false
}
return strconv.Itoa(parsed), true
case stagePhasePrelimTie, stagePhaseFinalTie:
parsed, err := strconv.Atoi(group)
if err != nil || parsed <= 0 {
return "", false
}
return strconv.Itoa(parsed), true
default:
return "", false
}
}
func normalizeStageRound(phase string, round int) int {
switch phase {
case stagePhasePreliminary:
if round < 1 {
return 1
}
if round > 3 {
return 3
}
return round
case stagePhaseFinal:
if round < 1 {
return 1
}
if round > 2 {
return 2
}
return round
default:
return 1
}
}
func normalizeTimestamp(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
if parsed, err := time.Parse(time.RFC3339Nano, trimmed); err == nil {
return parsed.UTC().Format(time.RFC3339)
}
if parsed, err := time.Parse("2006-01-02 15:04:05", trimmed); err == nil {
return parsed.UTC().Format(time.RFC3339)
}
return trimmed
}
func scanStageSession(scanner interface{ Scan(dest ...any) error }) (*StageSessionState, error) {
var item StageSessionState
var ended sql.NullString
if err := scanner.Scan(&item.ID, &item.Phase, &item.GroupKey, &item.Round, &item.StartedAt, &ended); err != nil {
return nil, err
}
item.Phase = strings.ToLower(strings.TrimSpace(item.Phase))
item.GroupKey = strings.TrimSpace(item.GroupKey)
item.StartedAt = normalizeTimestamp(item.StartedAt)
if ended.Valid {
item.EndedAt = normalizeTimestamp(ended.String)
}
return &item, nil
}
func (a *App) readCurrentStage() (*StageSessionState, error) {
row := a.db.QueryRow(`
SELECT id, phase, group_key, round, started_at, ended_at
FROM stage_sessions
WHERE ended_at IS NULL
ORDER BY started_at DESC, id DESC
LIMIT 1
`)
item, err := scanStageSession(row)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("read current stage: %w", err)
}
return item, nil
}
func (a *App) readLastStage() (*StageSessionState, error) {
row := a.db.QueryRow(`
SELECT id, phase, group_key, round, started_at, ended_at
FROM stage_sessions
ORDER BY started_at DESC, id DESC
LIMIT 1
`)
item, err := scanStageSession(row)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("read last stage: %w", err)
}
return item, nil
}
func (a *App) readStageHistory() ([]StageSessionState, error) {
rows, err := a.db.Query(`
SELECT id, phase, group_key, round, started_at, ended_at
FROM stage_sessions
ORDER BY started_at DESC, id DESC
`)
if err != nil {
return nil, fmt.Errorf("query stage history: %w", err)
}
defer rows.Close()
items := make([]StageSessionState, 0)
for rows.Next() {
item, scanErr := scanStageSession(rows)
if scanErr != nil {
return nil, fmt.Errorf("scan stage history: %w", scanErr)
}
items = append(items, *item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate stage history: %w", err)
}
return items, nil
}