You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
725 lines
20 KiB
725 lines
20 KiB
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"track-gopher/models"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// ErrNotFound is returned when a requested record is not found
|
|
var ErrNotFound = errors.New("record not found")
|
|
|
|
// DB represents the database connection
|
|
type DB struct {
|
|
*sql.DB
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewDB creates a new database connection
|
|
func New(dbPath string) (*DB, error) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
}))
|
|
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create database directory: %w", err)
|
|
}
|
|
|
|
// Open database connection
|
|
sqlDB, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
// Create database instance
|
|
db := &DB{
|
|
DB: sqlDB,
|
|
logger: logger,
|
|
}
|
|
|
|
// Initialize schema
|
|
if err := db.initSchema(); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("failed to initialize schema: %w", err)
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// initSchema creates the database tables if they don't exist
|
|
func (db *DB) initSchema() error {
|
|
// Create groups table
|
|
_, err := db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create groups table: %w", err)
|
|
}
|
|
|
|
// Create racers table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS racers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
first_name TEXT NOT NULL,
|
|
last_name TEXT NOT NULL,
|
|
car_number TEXT NOT NULL UNIQUE,
|
|
car_weight REAL NOT NULL,
|
|
group_id INTEGER NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (group_id) REFERENCES groups(id)
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create racers table: %w", err)
|
|
}
|
|
|
|
// Create races table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS races (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create races table: %w", err)
|
|
}
|
|
|
|
// Create race_results table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS race_results (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
race_id INTEGER NOT NULL,
|
|
racer_id INTEGER NOT NULL,
|
|
lane INTEGER NOT NULL,
|
|
time REAL,
|
|
place INTEGER,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (race_id) REFERENCES races(id),
|
|
FOREIGN KEY (racer_id) REFERENCES racers(id),
|
|
UNIQUE(race_id, lane)
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create race_results table: %w", err)
|
|
}
|
|
|
|
// Create heats table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS heats (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
group_id INTEGER NOT NULL,
|
|
heat_num INTEGER NOT NULL,
|
|
lane1_id INTEGER,
|
|
lane2_id INTEGER,
|
|
lane3_id INTEGER,
|
|
lane4_id INTEGER,
|
|
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (lane1_id) REFERENCES racers(id) ON DELETE SET NULL,
|
|
FOREIGN KEY (lane2_id) REFERENCES racers(id) ON DELETE SET NULL,
|
|
FOREIGN KEY (lane3_id) REFERENCES racers(id) ON DELETE SET NULL,
|
|
FOREIGN KEY (lane4_id) REFERENCES racers(id) ON DELETE SET NULL
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create heats table: %w", err)
|
|
}
|
|
|
|
// Create settings table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create settings table: %w", err)
|
|
}
|
|
|
|
// Create heat_results table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS heat_results (
|
|
group_id INTEGER NOT NULL,
|
|
heat_number INTEGER NOT NULL,
|
|
lane1_time REAL NOT NULL DEFAULT 0,
|
|
lane1_position INTEGER NOT NULL DEFAULT 0,
|
|
lane2_time REAL NOT NULL DEFAULT 0,
|
|
lane2_position INTEGER NOT NULL DEFAULT 0,
|
|
lane3_time REAL NOT NULL DEFAULT 0,
|
|
lane3_position INTEGER NOT NULL DEFAULT 0,
|
|
lane4_time REAL NOT NULL DEFAULT 0,
|
|
lane4_position INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (group_id, heat_number),
|
|
FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE
|
|
);
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create heat_results table: %w", err)
|
|
}
|
|
|
|
// Add default settings if they don't exist
|
|
_, err = db.Exec(`
|
|
INSERT OR IGNORE INTO settings (key, value) VALUES ('current_racing_group', '1');
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add default settings: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (db *DB) Close() error {
|
|
return db.DB.Close()
|
|
}
|
|
|
|
// SaveHeats saves the heats for a group, replacing any existing heats
|
|
func (db *DB) SaveHeats(groupID int64, heats []models.Heat) error {
|
|
// Start a transaction
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete existing heats for this group
|
|
_, err = tx.Exec("DELETE FROM heats WHERE group_id = ?", groupID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete existing heats: %w", err)
|
|
}
|
|
|
|
// Insert new heats
|
|
stmt, err := tx.Prepare("INSERT INTO heats (group_id, heat_num, lane1_id, lane2_id, lane3_id, lane4_id) VALUES (?, ?, ?, ?, ?, ?)")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prepare statement: %w", err)
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for i, heat := range heats {
|
|
_, err = stmt.Exec(groupID, i+1, heat.Lane1ID, heat.Lane2ID, heat.Lane3ID, heat.Lane4ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert heat: %w", err)
|
|
}
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetHeats retrieves the saved heats for a group
|
|
func (db *DB) GetHeats(groupID int64) ([]models.Heat, error) {
|
|
rows, err := db.Query(`
|
|
SELECT heat_num, lane1_id, lane2_id, lane3_id, lane4_id
|
|
FROM heats
|
|
WHERE group_id = ?
|
|
ORDER BY heat_num
|
|
`, groupID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query heats: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var heats []models.Heat
|
|
for rows.Next() {
|
|
var heat models.Heat
|
|
err := rows.Scan(&heat.HeatNum, &heat.Lane1ID, &heat.Lane2ID, &heat.Lane3ID, &heat.Lane4ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan heat: %w", err)
|
|
}
|
|
heats = append(heats, heat)
|
|
}
|
|
|
|
return heats, nil
|
|
}
|
|
|
|
// HasSavedHeats checks if a group has saved heats
|
|
func (db *DB) HasSavedHeats(groupID int64) (bool, error) {
|
|
var count int
|
|
err := db.QueryRow("SELECT COUNT(*) FROM heats WHERE group_id = ?", groupID).Scan(&count)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check for saved heats: %w", err)
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// DeleteHeats deletes all heats for a group
|
|
func (db *DB) DeleteHeats(groupID int64) error {
|
|
_, err := db.Exec("DELETE FROM heats WHERE group_id = ?", groupID)
|
|
return err
|
|
}
|
|
|
|
// GetCurrentRacingGroup gets the currently racing group
|
|
func (db *DB) GetCurrentRacingGroup() (models.Group, error) {
|
|
var group models.Group
|
|
|
|
// Get the current racing group ID from settings
|
|
var groupID int64
|
|
err := db.QueryRow("SELECT value FROM settings WHERE key = 'current_racing_group'").Scan(&groupID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
// If no group is set, get the first group
|
|
err = db.QueryRow("SELECT id, name, description FROM groups ORDER BY id LIMIT 1").Scan(
|
|
&group.ID, &group.Name, &group.Description)
|
|
if err != nil {
|
|
return group, err
|
|
}
|
|
|
|
// Set this as the current racing group
|
|
_, err = db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('current_racing_group', ?)", group.ID)
|
|
return group, err
|
|
}
|
|
return group, err
|
|
}
|
|
|
|
// Get the group details
|
|
err = db.QueryRow("SELECT id, name, description FROM groups WHERE id = ?", groupID).Scan(
|
|
&group.ID, &group.Name, &group.Description)
|
|
return group, err
|
|
}
|
|
|
|
// SetCurrentRacingGroup sets the currently racing group
|
|
func (db *DB) SetCurrentRacingGroup(groupID int64) error {
|
|
_, err := db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('current_racing_group', ?)", groupID)
|
|
return err
|
|
}
|
|
|
|
// GetCurrentHeatNumber gets the current heat number for a group
|
|
func (db *DB) GetCurrentHeatNumber(groupID int64) (int, error) {
|
|
var heatNum int
|
|
key := fmt.Sprintf("current_heat_%d", groupID)
|
|
|
|
err := db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&heatNum)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
// If no heat is set, default to 1
|
|
_, err = db.Exec("INSERT INTO settings (key, value) VALUES (?, 1)", key)
|
|
return 1, err
|
|
}
|
|
return 0, err
|
|
}
|
|
|
|
return heatNum, nil
|
|
}
|
|
|
|
// SetCurrentHeatNumber sets the current heat number for a group
|
|
func (db *DB) SetCurrentHeatNumber(groupID int64, heatNum int) error {
|
|
key := fmt.Sprintf("current_heat_%d", groupID)
|
|
_, err := db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", key, heatNum)
|
|
return err
|
|
}
|
|
|
|
func (db *DB) SaveLaneResult(groupID int64, heatNum int, lane int, time float64, position int) error {
|
|
db.logger.Info("Saving lane result", "groupID", groupID, "heatNum", heatNum, "lane", lane, "time", time, "position", position)
|
|
var count int
|
|
err := db.QueryRow("SELECT COUNT(*) FROM heat_results WHERE group_id = ? AND heat_number = ?",
|
|
groupID, heatNum).Scan(&count)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if count > 0 {
|
|
switch lane {
|
|
case 1:
|
|
_, err = db.Exec("UPDATE heat_results SET lane1_time = ?, lane1_position = ? WHERE group_id = ? AND heat_number = ?", time, position, groupID, heatNum)
|
|
case 2:
|
|
_, err = db.Exec("UPDATE heat_results SET lane2_time = ?, lane2_position = ? WHERE group_id = ? AND heat_number = ?", time, position, groupID, heatNum)
|
|
case 3:
|
|
_, err = db.Exec("UPDATE heat_results SET lane3_time = ?, lane3_position = ? WHERE group_id = ? AND heat_number = ?", time, position, groupID, heatNum)
|
|
case 4:
|
|
_, err = db.Exec("UPDATE heat_results SET lane4_time = ?, lane4_position = ? WHERE group_id = ? AND heat_number = ?", time, position, groupID, heatNum)
|
|
}
|
|
} else {
|
|
switch lane {
|
|
case 1:
|
|
_, err = db.Exec("INSERT INTO heat_results (group_id, heat_number, lane1_time, lane1_position) VALUES (?, ?, ?, ?)", groupID, heatNum, time, position)
|
|
case 2:
|
|
_, err = db.Exec("INSERT INTO heat_results (group_id, heat_number, lane2_time, lane2_position) VALUES (?, ?, ?, ?)", groupID, heatNum, time, position)
|
|
case 3:
|
|
_, err = db.Exec("INSERT INTO heat_results (group_id, heat_number, lane3_time, lane3_position) VALUES (?, ?, ?, ?)", groupID, heatNum, time, position)
|
|
case 4:
|
|
_, err = db.Exec("INSERT INTO heat_results (group_id, heat_number, lane4_time, lane4_position) VALUES (?, ?, ?, ?)", groupID, heatNum, time, position)
|
|
}
|
|
}
|
|
if err != nil {
|
|
db.logger.Error("Error saving lane result", "error", err)
|
|
} else {
|
|
db.logger.Info("Saved lane result", "groupID", groupID, "heatNum", heatNum, "lane", lane, "time", time, "position", position)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// SaveHeatResult saves the result of a heat
|
|
func (db *DB) SaveHeatResult(result models.HeatResult) error {
|
|
db.logger.Info("Saving heat result", "result", result)
|
|
// Check if result already exists
|
|
var count int
|
|
err := db.QueryRow("SELECT COUNT(*) FROM heat_results WHERE group_id = ? AND heat_number = ?",
|
|
result.GroupID, result.HeatNumber).Scan(&count)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if count > 0 {
|
|
// Update existing result
|
|
_, err = db.Exec(`
|
|
UPDATE heat_results SET
|
|
lane1_time = ?, lane1_position = ?,
|
|
lane2_time = ?, lane2_position = ?,
|
|
lane3_time = ?, lane3_position = ?,
|
|
lane4_time = ?, lane4_position = ?
|
|
WHERE group_id = ? AND heat_number = ?`,
|
|
result.Lane1Time, result.Lane1Position,
|
|
result.Lane2Time, result.Lane2Position,
|
|
result.Lane3Time, result.Lane3Position,
|
|
result.Lane4Time, result.Lane4Position,
|
|
result.GroupID, result.HeatNumber)
|
|
} else {
|
|
// Insert new result
|
|
_, err = db.Exec(`
|
|
INSERT INTO heat_results (
|
|
group_id, heat_number,
|
|
lane1_time, lane1_position,
|
|
lane2_time, lane2_position,
|
|
lane3_time, lane3_position,
|
|
lane4_time, lane4_position
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
result.GroupID, result.HeatNumber,
|
|
result.Lane1Time, result.Lane1Position,
|
|
result.Lane2Time, result.Lane2Position,
|
|
result.Lane3Time, result.Lane3Position,
|
|
result.Lane4Time, result.Lane4Position)
|
|
}
|
|
|
|
db.logger.Info("Saved heat result", "result", result)
|
|
return err
|
|
}
|
|
|
|
// GetHeatResult gets the result of a specific heat
|
|
func (db *DB) GetHeatResult(groupID int64, heatNum int) (*models.HeatResult, error) {
|
|
result := &models.HeatResult{}
|
|
|
|
err := db.QueryRow(`
|
|
SELECT
|
|
group_id, heat_number,
|
|
lane1_time, lane1_position,
|
|
lane2_time, lane2_position,
|
|
lane3_time, lane3_position,
|
|
lane4_time, lane4_position
|
|
FROM heat_results
|
|
WHERE group_id = ? AND heat_number = ?`,
|
|
groupID, heatNum).Scan(
|
|
&result.GroupID, &result.HeatNumber,
|
|
&result.Lane1Time, &result.Lane1Position,
|
|
&result.Lane2Time, &result.Lane2Position,
|
|
&result.Lane3Time, &result.Lane3Position,
|
|
&result.Lane4Time, &result.Lane4Position)
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetHeatResults gets all heat results for a group
|
|
func (db *DB) GetHeatResults(groupID int64) ([]models.HeatResult, error) {
|
|
db.logger.Info("Getting heat results for group", "groupID", groupID)
|
|
rows, err := db.Query(`
|
|
SELECT
|
|
group_id, heat_number,
|
|
lane1_time, lane1_position,
|
|
lane2_time, lane2_position,
|
|
lane3_time, lane3_position,
|
|
lane4_time, lane4_position
|
|
FROM heat_results
|
|
WHERE group_id = ?
|
|
ORDER BY heat_number`,
|
|
groupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []models.HeatResult
|
|
for rows.Next() {
|
|
var result models.HeatResult
|
|
err := rows.Scan(
|
|
&result.GroupID, &result.HeatNumber,
|
|
&result.Lane1Time, &result.Lane1Position,
|
|
&result.Lane2Time, &result.Lane2Position,
|
|
&result.Lane3Time, &result.Lane3Position,
|
|
&result.Lane4Time, &result.Lane4Position)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
|
|
db.logger.Info("Heat results", "results", results)
|
|
return results, nil
|
|
}
|
|
|
|
// DeleteHeatResult deletes a heat result
|
|
func (db *DB) DeleteHeatResult(groupID int64, heatNum int) error {
|
|
_, err := db.Exec("DELETE FROM heat_results WHERE group_id = ? AND heat_number = ?",
|
|
groupID, heatNum)
|
|
return err
|
|
}
|
|
|
|
func (db *DB) GetHeatData(groupID int64, heatNum int) (*models.HeatData, error) {
|
|
db.logger.Info("Getting heat data for group", "groupID", groupID, "heatNum", heatNum)
|
|
heatData := &models.HeatData{}
|
|
lane1 := int64(0)
|
|
lane2 := int64(0)
|
|
lane3 := int64(0)
|
|
lane4 := int64(0)
|
|
|
|
err := db.QueryRow(`
|
|
SELECT
|
|
group_id, heat_num,
|
|
g.name as group_name,
|
|
(SELECT COUNT(*) FROM heats WHERE group_id = ?) as total_heats,
|
|
h.lane1_id, h.lane2_id, h.lane3_id, h.lane4_id
|
|
FROM heats h
|
|
JOIN groups g ON g.id = h.group_id
|
|
WHERE h.group_id = ? AND h.heat_num = ?`,
|
|
groupID, groupID, heatNum).Scan(
|
|
&heatData.Group.ID, &heatData.HeatNumber,
|
|
&heatData.Group.Name, &heatData.TotalHeats,
|
|
&lane1, &lane2, &lane3, &lane4)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting heat data: %v", err)
|
|
}
|
|
|
|
// Get racer data for each lane
|
|
if lane1 != 0 {
|
|
lane1Data, err := db.getLaneData(1, lane1, heatNum, groupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
heatData.Lane1 = lane1Data
|
|
}
|
|
|
|
if lane2 != 0 {
|
|
lane2Data, err := db.getLaneData(2, lane2, heatNum, groupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
heatData.Lane2 = lane2Data
|
|
}
|
|
|
|
if lane3 != 0 {
|
|
lane3Data, err := db.getLaneData(3, lane3, heatNum, groupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
heatData.Lane3 = lane3Data
|
|
}
|
|
|
|
if lane4 != 0 {
|
|
lane4Data, err := db.getLaneData(4, lane4, heatNum, groupID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
heatData.Lane4 = lane4Data
|
|
}
|
|
|
|
db.logger.Info("Heat data", "heatData", heatData)
|
|
return heatData, nil
|
|
}
|
|
|
|
// getLaneData gets the racer data for a specific lane
|
|
func (db *DB) getLaneData(lane int, racerID int64, heatNum int, groupID int64) (*models.LaneData, error) {
|
|
// Get racer data
|
|
var racer models.Racer
|
|
err := db.QueryRow(`
|
|
SELECT id, first_name, last_name, car_number, car_weight, group_id
|
|
FROM racers
|
|
WHERE id = ?`, racerID).Scan(
|
|
&racer.ID, &racer.FirstName, &racer.LastName, &racer.CarNumber, &racer.CarWeight, &racer.GroupID)
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Create lane data with racer info
|
|
laneData := &models.LaneData{
|
|
Lane: lane,
|
|
RacerID: racerID,
|
|
Name: racer.FirstName + " " + racer.LastName,
|
|
CarNum: racer.CarNumber,
|
|
CarWeight: racer.CarWeight,
|
|
Time: 0,
|
|
Place: 0,
|
|
}
|
|
|
|
heatResult, err := db.GetHeatResult(groupID, heatNum)
|
|
if err != nil {
|
|
return laneData, nil
|
|
}
|
|
|
|
switch lane {
|
|
case 1:
|
|
laneData.Time = heatResult.Lane1Time
|
|
laneData.Place = heatResult.Lane1Position
|
|
case 2:
|
|
laneData.Time = heatResult.Lane2Time
|
|
laneData.Place = heatResult.Lane2Position
|
|
case 3:
|
|
laneData.Time = heatResult.Lane3Time
|
|
laneData.Place = heatResult.Lane3Position
|
|
case 4:
|
|
laneData.Time = heatResult.Lane4Time
|
|
laneData.Place = heatResult.Lane4Position
|
|
}
|
|
|
|
return laneData, nil
|
|
}
|
|
|
|
// GetFinalResults calculates the final results for a group
|
|
// The final time is the average of the fastest 3 times, discarding the slowest time
|
|
// Racers with a time of 9.999 are marked as DNF and placed at the end
|
|
func (db *DB) GetFinalResults(groupID int64) ([]models.FinalResult, error) {
|
|
// Get all racers in the group
|
|
racers, err := db.GetRacersByGroup(groupID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get racers: %w", err)
|
|
}
|
|
|
|
// Get all heat results for the group
|
|
rows, err := db.Query(`
|
|
SELECT heat_number, lane1_id, lane1_time, lane2_id, lane2_time,
|
|
lane3_id, lane3_time, lane4_id, lane4_time
|
|
FROM heat_results
|
|
WHERE group_id = ?
|
|
ORDER BY heat_number
|
|
`, groupID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query heat results: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
// Map to store times for each racer
|
|
racerTimes := make(map[int64][]float64)
|
|
|
|
// Initialize the map for all racers
|
|
for _, racer := range racers {
|
|
racerTimes[racer.ID] = []float64{}
|
|
}
|
|
|
|
// Process each heat result
|
|
for rows.Next() {
|
|
var heatNum int
|
|
var lane1ID, lane2ID, lane3ID, lane4ID sql.NullInt64
|
|
var lane1Time, lane2Time, lane3Time, lane4Time sql.NullFloat64
|
|
|
|
err := rows.Scan(&heatNum, &lane1ID, &lane1Time, &lane2ID, &lane2Time,
|
|
&lane3ID, &lane3Time, &lane4ID, &lane4Time)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan heat result: %w", err)
|
|
}
|
|
|
|
// Add times for each lane if a racer was assigned
|
|
if lane1ID.Valid && lane1Time.Valid {
|
|
racerTimes[lane1ID.Int64] = append(racerTimes[lane1ID.Int64], lane1Time.Float64)
|
|
}
|
|
if lane2ID.Valid && lane2Time.Valid {
|
|
racerTimes[lane2ID.Int64] = append(racerTimes[lane2ID.Int64], lane2Time.Float64)
|
|
}
|
|
if lane3ID.Valid && lane3Time.Valid {
|
|
racerTimes[lane3ID.Int64] = append(racerTimes[lane3ID.Int64], lane3Time.Float64)
|
|
}
|
|
if lane4ID.Valid && lane4Time.Valid {
|
|
racerTimes[lane4ID.Int64] = append(racerTimes[lane4ID.Int64], lane4Time.Float64)
|
|
}
|
|
}
|
|
|
|
// Calculate final results
|
|
results := make([]models.FinalResult, 0, len(racers))
|
|
|
|
for _, racer := range racers {
|
|
times := racerTimes[racer.ID]
|
|
|
|
// Skip racers with no times
|
|
if len(times) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Check if racer has DNF (time of 9.999)
|
|
dnf := false
|
|
for _, time := range times {
|
|
if time >= 9.999 {
|
|
dnf = true
|
|
break
|
|
}
|
|
}
|
|
|
|
var avgTime float64
|
|
|
|
if dnf {
|
|
// If DNF, set average time to a high value for sorting
|
|
avgTime = 999.999
|
|
} else {
|
|
// Sort times to find fastest
|
|
sort.Float64s(times)
|
|
|
|
// Calculate average of fastest 3 times (or fewer if not enough runs)
|
|
numTimes := len(times)
|
|
if numTimes >= 4 {
|
|
// Discard the slowest time
|
|
avgTime = (times[0] + times[1] + times[2]) / 3.0
|
|
} else if numTimes == 3 {
|
|
avgTime = (times[0] + times[1] + times[2]) / 3.0
|
|
} else if numTimes == 2 {
|
|
avgTime = (times[0] + times[1]) / 2.0
|
|
} else {
|
|
avgTime = times[0]
|
|
}
|
|
}
|
|
|
|
results = append(results, models.FinalResult{
|
|
Racer: racer,
|
|
Times: times,
|
|
AverageTime: avgTime,
|
|
DNF: dnf,
|
|
})
|
|
}
|
|
|
|
// Sort results by average time (DNF racers will be at the end)
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return results[i].AverageTime < results[j].AverageTime
|
|
})
|
|
|
|
// Assign places
|
|
for i := range results {
|
|
results[i].Place = i + 1
|
|
}
|
|
|
|
return results, nil
|
|
}
|