diff --git a/db/db.go b/db/db.go index fbd18c5..b3744e8 100644 --- a/db/db.go +++ b/db/db.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path/filepath" + "sort" "track-gopher/models" _ "github.com/mattn/go-sqlite3" @@ -599,3 +600,125 @@ func (db *DB) getLaneData(lane int, racerID int64, heatNum int, groupID int64) ( 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 +} diff --git a/models/models.go b/models/models.go index ecbe318..60f5feb 100644 --- a/models/models.go +++ b/models/models.go @@ -102,3 +102,12 @@ type AdminEvent struct { Type AdminEventType Event any } + +// FinalResult represents a racer's final result in a group +type FinalResult struct { + Racer Racer `json:"racer"` + Times []float64 `json:"times"` + AverageTime float64 `json:"average_time"` + Place int `json:"place"` + DNF bool `json:"dnf"` +} diff --git a/web/server.go b/web/server.go index 7928e47..35235d3 100644 --- a/web/server.go +++ b/web/server.go @@ -225,9 +225,11 @@ func (s *Server) routes() { s.router.Get("/race", s.handleRacePublic()) s.router.Get("/race/manage", s.handleRaceManage()) + // Add final results route + s.router.Get("/results", s.handleFinalResults()) + // Main page s.router.Get("/", s.handleIndex()) - } // Start starts the web server with HTTP/2 support @@ -1821,3 +1823,53 @@ func (s *Server) handleSetRacingGroup() http.HandlerFunc { json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } } + +// handleFinalResults renders the final results page +func (s *Server) handleFinalResults() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get groups from database + groups, err := s.db.GetGroups() + if err != nil { + s.logger.Error("Failed to get groups", "error", err) + http.Error(w, "Failed to get groups", http.StatusInternalServerError) + return + } + + // Get selected group ID from query parameter + selectedGroupID := int64(0) + selectedGroupName := "" + groupIDStr := r.URL.Query().Get("group_id") + if groupIDStr != "" { + groupID, err := strconv.ParseInt(groupIDStr, 10, 64) + if err == nil { + selectedGroupID = groupID + + // Find the group name + for _, group := range groups { + if group.ID == selectedGroupID { + selectedGroupName = group.Name + break + } + } + } + } + + var results []models.FinalResult + if selectedGroupID > 0 { + // Get final results for the selected group + results, err = s.db.GetFinalResults(selectedGroupID) + if err != nil { + s.logger.Error("Failed to get final results", "error", err) + http.Error(w, "Failed to get final results", http.StatusInternalServerError) + return + } + } + + // Render template + component := templates.FinalResultsPage(groups, selectedGroupID, results, selectedGroupName) + if err := component.Render(r.Context(), w); err != nil { + s.logger.Error("Failed to render final results template", "error", err) + http.Error(w, "Failed to render page", http.StatusInternalServerError) + } + } +} diff --git a/web/templates/final_results.templ b/web/templates/final_results.templ new file mode 100644 index 0000000..e43e79f --- /dev/null +++ b/web/templates/final_results.templ @@ -0,0 +1,64 @@ +package templates + +import "track-gopher/models" +import "fmt" + +templ FinalResults(results []models.FinalResult, groupName string) { +