package seasonsview import "git.haelnorr.com/h/oslstats/internal/db" import "git.haelnorr.com/h/oslstats/internal/view/component/links" import "fmt" import "sort" import "strings" // teamAggStats holds aggregated stats for a single team in a fixture. type teamAggStats struct { Goals int Assists int PrimaryAssists int SecondaryAssists int Saves int Shots int Blocks int Passes int Turnovers int Takeaways int FaceoffsWon int FaceoffsLost int PostHits int PossessionSec int PlayersUsed int } func aggregateTeamStats(players []*db.PlayerWithPlayStatus) *teamAggStats { agg := &teamAggStats{} for _, p := range players { if !p.Played || p.Stats == nil { continue } agg.PlayersUsed++ if p.Stats.Goals != nil { agg.Goals += *p.Stats.Goals } if p.Stats.Assists != nil { agg.Assists += *p.Stats.Assists } if p.Stats.PrimaryAssists != nil { agg.PrimaryAssists += *p.Stats.PrimaryAssists } if p.Stats.SecondaryAssists != nil { agg.SecondaryAssists += *p.Stats.SecondaryAssists } if p.Stats.Saves != nil { agg.Saves += *p.Stats.Saves } if p.Stats.Shots != nil { agg.Shots += *p.Stats.Shots } if p.Stats.Blocks != nil { agg.Blocks += *p.Stats.Blocks } if p.Stats.Passes != nil { agg.Passes += *p.Stats.Passes } if p.Stats.Turnovers != nil { agg.Turnovers += *p.Stats.Turnovers } if p.Stats.Takeaways != nil { agg.Takeaways += *p.Stats.Takeaways } if p.Stats.FaceoffsWon != nil { agg.FaceoffsWon += *p.Stats.FaceoffsWon } if p.Stats.FaceoffsLost != nil { agg.FaceoffsLost += *p.Stats.FaceoffsLost } if p.Stats.PostHits != nil { agg.PostHits += *p.Stats.PostHits } if p.Stats.PossessionTimeSec != nil { agg.PossessionSec += *p.Stats.PossessionTimeSec } } return agg } func formatPossession(seconds int) string { m := seconds / 60 s := seconds % 60 return fmt.Sprintf("%d:%02d", m, s) } func faceoffPct(won, lost int) string { total := won + lost if total == 0 { return "0%" } pct := float64(won) / float64(total) * 100 return fmt.Sprintf("%.0f%%", pct) } // fixtureMatchAnalysisTab renders the full Match Analysis tab for completed fixtures. // Shows score, team stats comparison, match details, and top performers. templ fixtureMatchAnalysisTab( fixture *db.Fixture, result *db.FixtureResult, rosters map[string][]*db.PlayerWithPlayStatus, preview *db.MatchPreviewData, ) {
@analysisScoreHeader(fixture, result) @analysisTeamStatsComparison(fixture, rosters) @analysisTopPerformers(fixture, rosters) if preview != nil { @analysisStandingsContext(fixture, preview) }
} // analysisScoreHeader renders the final score in a prominent broadcast-style display. templ analysisScoreHeader(fixture *db.Fixture, result *db.FixtureResult) { {{ isOT := strings.EqualFold(result.EndReason, "Overtime") homeWon := result.Winner == "home" awayWon := result.Winner == "away" isForfeit := result.IsForfeit }}

Final Score

if isForfeit { @analysisForfeitDisplay(fixture, result) } else {
if fixture.HomeTeam.Color != "" {
}

@links.TeamNameLinkInSeason(fixture.HomeTeam, fixture.Season, fixture.League)

{ fmt.Sprint(result.HomeScore) } if homeWon { Winner }
if isOT { OT }
if fixture.AwayTeam.Color != "" {
}

@links.TeamNameLinkInSeason(fixture.AwayTeam, fixture.Season, fixture.League)

{ fmt.Sprint(result.AwayScore) } if awayWon { Winner }
}
} // analysisForfeitDisplay renders a forfeit result in the analysis header. templ analysisForfeitDisplay(fixture *db.Fixture, result *db.FixtureResult) { {{ isMutualForfeit := result.ForfeitType != nil && *result.ForfeitType == "mutual" isOutrightForfeit := result.ForfeitType != nil && *result.ForfeitType == "outright" forfeitTeamName := "" winnerTeamName := "" if isOutrightForfeit && result.ForfeitTeam != nil { if *result.ForfeitTeam == "home" { forfeitTeamName = fixture.HomeTeam.Name winnerTeamName = fixture.AwayTeam.Name } else { forfeitTeamName = fixture.AwayTeam.Name winnerTeamName = fixture.HomeTeam.Name } } }}
if isMutualForfeit { MUTUAL FORFEIT

Both teams receive an overtime loss

} else if isOutrightForfeit { FORFEIT

{ forfeitTeamName } forfeited — { winnerTeamName } wins

} if result.ForfeitReason != nil && *result.ForfeitReason != "" {

Reason

{ *result.ForfeitReason }

}
} // analysisTeamStatsComparison renders aggregated team stats in the broadcast comparison layout. templ analysisTeamStatsComparison(fixture *db.Fixture, rosters map[string][]*db.PlayerWithPlayStatus) { {{ homeAgg := aggregateTeamStats(rosters["home"]) awayAgg := aggregateTeamStats(rosters["away"]) }}

Team Statistics

if fixture.HomeTeam.Color != "" { } { fixture.HomeTeam.ShortName }
{ fixture.AwayTeam.ShortName } if fixture.AwayTeam.Color != "" { }
@previewStatRow( fmt.Sprint(homeAgg.Goals), "Goals", fmt.Sprint(awayAgg.Goals), homeAgg.Goals > awayAgg.Goals, awayAgg.Goals > homeAgg.Goals, ) @previewStatRow( fmt.Sprint(homeAgg.Assists), "Assists", fmt.Sprint(awayAgg.Assists), homeAgg.Assists > awayAgg.Assists, awayAgg.Assists > homeAgg.Assists, ) @previewStatRow( fmt.Sprint(homeAgg.Shots), "Shots", fmt.Sprint(awayAgg.Shots), homeAgg.Shots > awayAgg.Shots, awayAgg.Shots > homeAgg.Shots, ) @previewStatRow( fmt.Sprint(homeAgg.Saves), "Saves", fmt.Sprint(awayAgg.Saves), homeAgg.Saves > awayAgg.Saves, awayAgg.Saves > homeAgg.Saves, ) @previewStatRow( fmt.Sprint(homeAgg.Blocks), "Blocks", fmt.Sprint(awayAgg.Blocks), homeAgg.Blocks > awayAgg.Blocks, awayAgg.Blocks > homeAgg.Blocks, ) @previewStatRow( fmt.Sprint(homeAgg.Passes), "Passes", fmt.Sprint(awayAgg.Passes), homeAgg.Passes > awayAgg.Passes, awayAgg.Passes > homeAgg.Passes, ) @previewStatRow( fmt.Sprint(homeAgg.Takeaways), "Takeaways", fmt.Sprint(awayAgg.Takeaways), homeAgg.Takeaways > awayAgg.Takeaways, awayAgg.Takeaways > homeAgg.Takeaways, ) @previewStatRow( fmt.Sprint(homeAgg.Turnovers), "Turnovers", fmt.Sprint(awayAgg.Turnovers), homeAgg.Turnovers < awayAgg.Turnovers, awayAgg.Turnovers < homeAgg.Turnovers, ) {{ homeFO := homeAgg.FaceoffsWon + homeAgg.FaceoffsLost awayFO := awayAgg.FaceoffsWon + awayAgg.FaceoffsLost homeFOStr := fmt.Sprintf("%d/%d", homeAgg.FaceoffsWon, homeFO) awayFOStr := fmt.Sprintf("%d/%d", awayAgg.FaceoffsWon, awayFO) }} @previewStatRow( homeFOStr, "Faceoffs Won", awayFOStr, homeAgg.FaceoffsWon > awayAgg.FaceoffsWon, awayAgg.FaceoffsWon > homeAgg.FaceoffsWon, ) @previewStatRow( faceoffPct(homeAgg.FaceoffsWon, homeAgg.FaceoffsLost), "Faceoff %", faceoffPct(awayAgg.FaceoffsWon, awayAgg.FaceoffsLost), homeAgg.FaceoffsWon * (awayAgg.FaceoffsWon + awayAgg.FaceoffsLost) > awayAgg.FaceoffsWon * (homeAgg.FaceoffsWon + homeAgg.FaceoffsLost), awayAgg.FaceoffsWon * (homeAgg.FaceoffsWon + homeAgg.FaceoffsLost) > homeAgg.FaceoffsWon * (awayAgg.FaceoffsWon + awayAgg.FaceoffsLost), ) @previewStatRow( fmt.Sprint(homeAgg.PostHits), "Post Hits", fmt.Sprint(awayAgg.PostHits), homeAgg.PostHits > awayAgg.PostHits, awayAgg.PostHits > homeAgg.PostHits, ) @previewStatRow( formatPossession(homeAgg.PossessionSec), "Possession", formatPossession(awayAgg.PossessionSec), homeAgg.PossessionSec > awayAgg.PossessionSec, awayAgg.PossessionSec > homeAgg.PossessionSec, ) @previewStatRow( fmt.Sprint(homeAgg.PlayersUsed), "Players Used", fmt.Sprint(awayAgg.PlayersUsed), false, false, )
} // analysisTopPerformers shows the top players from each team based on score. templ analysisTopPerformers(fixture *db.Fixture, rosters map[string][]*db.PlayerWithPlayStatus) { {{ // Collect players who played and have stats, sorted by score descending type scoredPlayer struct { Player *db.Player Stats *db.FixtureResultPlayerStats IsManager bool IsFreeAgent bool } collectTop := func(players []*db.PlayerWithPlayStatus, limit int) []*scoredPlayer { var scored []*scoredPlayer for _, p := range players { if !p.Played || p.Stats == nil || p.Player == nil { continue } scored = append(scored, &scoredPlayer{ Player: p.Player, Stats: p.Stats, IsManager: p.IsManager, IsFreeAgent: p.IsFreeAgent, }) } sort.Slice(scored, func(i, j int) bool { si, sj := 0, 0 if scored[i].Stats.Score != nil { si = *scored[i].Stats.Score } if scored[j].Stats.Score != nil { sj = *scored[j].Stats.Score } return si > sj }) if len(scored) > limit { scored = scored[:limit] } return scored } homeTop := collectTop(rosters["home"], 3) awayTop := collectTop(rosters["away"], 3) }} if len(homeTop) > 0 || len(awayTop) > 0 {

Top Performers

if fixture.HomeTeam.Color != "" { }

{ fixture.HomeTeam.Name }

for i, p := range homeTop { @topPerformerCard(p.Player, p.Stats, p.IsManager, p.IsFreeAgent, i+1) }
if fixture.AwayTeam.Color != "" { }

{ fixture.AwayTeam.Name }

for i, p := range awayTop { @topPerformerCard(p.Player, p.Stats, p.IsManager, p.IsFreeAgent, i+1) }
} } // topPerformerCard renders a single top performer card with key stats. templ topPerformerCard(player *db.Player, stats *db.FixtureResultPlayerStats, isManager bool, isFreeAgent bool, rank int) { {{ rankLabels := map[int]string{1: "🥇", 2: "🥈", 3: "🥉"} rankLabel := rankLabels[rank] }}
{ rankLabel }
@links.PlayerLink(player) if isManager { } if isFreeAgent { FA }
if stats.Score != nil { { fmt.Sprint(*stats.Score) } SC } if stats.Goals != nil { { fmt.Sprint(*stats.Goals) } G } if stats.Assists != nil { { fmt.Sprint(*stats.Assists) } A } if stats.Saves != nil { { fmt.Sprint(*stats.Saves) } SV } if stats.Shots != nil { { fmt.Sprint(*stats.Shots) } SH }
} // analysisStandingsContext shows how this result fits into the league standings. templ analysisStandingsContext(fixture *db.Fixture, preview *db.MatchPreviewData) {

League Context

if fixture.HomeTeam.Color != "" { } { fixture.HomeTeam.ShortName }
{ fixture.AwayTeam.ShortName } if fixture.AwayTeam.Color != "" { }
{{ homePos := ordinal(preview.HomePosition) awayPos := ordinal(preview.AwayPosition) if preview.HomePosition == 0 { homePos = "N/A" } if preview.AwayPosition == 0 { awayPos = "N/A" } }} @previewStatRow( homePos, "Position", awayPos, preview.HomePosition > 0 && preview.HomePosition < preview.AwayPosition, preview.AwayPosition > 0 && preview.AwayPosition < preview.HomePosition, ) @previewStatRow( fmt.Sprint(preview.HomeRecord.Points), "Points", fmt.Sprint(preview.AwayRecord.Points), preview.HomeRecord.Points > preview.AwayRecord.Points, preview.AwayRecord.Points > preview.HomeRecord.Points, ) @previewStatRow( fmt.Sprintf("%d-%d-%d-%d", preview.HomeRecord.Wins, preview.HomeRecord.OvertimeWins, preview.HomeRecord.OvertimeLosses, preview.HomeRecord.Losses, ), "Record", fmt.Sprintf("%d-%d-%d-%d", preview.AwayRecord.Wins, preview.AwayRecord.OvertimeWins, preview.AwayRecord.OvertimeLosses, preview.AwayRecord.Losses, ), false, false, ) {{ homeDiff := preview.HomeRecord.GoalsFor - preview.HomeRecord.GoalsAgainst awayDiff := preview.AwayRecord.GoalsFor - preview.AwayRecord.GoalsAgainst }} @previewStatRow( fmt.Sprintf("%+d", homeDiff), "Goal Diff", fmt.Sprintf("%+d", awayDiff), homeDiff > awayDiff, awayDiff > homeDiff, ) if len(preview.HomeRecentGames) > 0 || len(preview.AwayRecentGames) > 0 {
for _, g := range preview.HomeRecentGames { @gameOutcomeIcon(g) }
Form
for _, g := range preview.AwayRecentGames { @gameOutcomeIcon(g) }
}
}