From fc219a044cd9f8d0a3e782da9d521f1a90d4e70e Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Fri, 6 Mar 2026 19:06:29 +1100 Subject: [PATCH] updated team stats --- internal/embedfs/web/css/output.css | 13 +- .../handlers/season_league_team_detail.go | 44 ++- .../seasonsview/fixture_review_result.templ | 17 ++ .../season_league_team_detail.templ | 262 ++++++++++-------- internal/view/teamsview/detail_seasons.templ | 19 +- 5 files changed, 230 insertions(+), 125 deletions(-) diff --git a/internal/embedfs/web/css/output.css b/internal/embedfs/web/css/output.css index e04cc37..b856653 100644 --- a/internal/embedfs/web/css/output.css +++ b/internal/embedfs/web/css/output.css @@ -1152,6 +1152,9 @@ .py-4 { padding-block: calc(var(--spacing) * 4); } + .py-5 { + padding-block: calc(var(--spacing) * 5); + } .py-6 { padding-block: calc(var(--spacing) * 6); } @@ -2041,11 +2044,6 @@ scale: var(--tw-scale-x) var(--tw-scale-y); } } - .sm\:grid-cols-4 { - @media (width >= 40rem) { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - } .sm\:flex-row { @media (width >= 40rem) { flex-direction: row; @@ -2177,11 +2175,6 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); } } - .lg\:grid-cols-8 { - @media (width >= 64rem) { - grid-template-columns: repeat(8, minmax(0, 1fr)); - } - } .lg\:items-end { @media (width >= 64rem) { align-items: flex-end; diff --git a/internal/handlers/season_league_team_detail.go b/internal/handlers/season_league_team_detail.go index 5d55c03..48e0bb3 100644 --- a/internal/handlers/season_league_team_detail.go +++ b/internal/handlers/season_league_team_detail.go @@ -35,6 +35,7 @@ func SeasonLeagueTeamDetailPage( var scheduleMap map[int]*db.FixtureSchedule var resultMap map[int]*db.FixtureResult var playerStats []*db.AggregatedPlayerStats + var leaderboard []*db.LeaderboardEntry if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { var err error @@ -72,12 +73,51 @@ func SeasonLeagueTeamDetailPage( return false, errors.Wrap(err, "db.GetPlayersNotOnTeam") } + // Get all teams and all fixtures for the league to compute leaderboard + var allTeams []*db.Team + err = tx.NewSelect(). + Model(&allTeams). + Join("INNER JOIN team_participations AS tp ON tp.team_id = t.id"). + Where("tp.season_id = ? AND tp.league_id = ?", twr.Season.ID, twr.League.ID). + Scan(ctx) + if err != nil { + return false, errors.Wrap(err, "tx.NewSelect allTeams") + } + + allFixtures, err := db.GetAllocatedFixtures(ctx, tx, twr.Season.ID, twr.League.ID) + if err != nil { + return false, errors.Wrap(err, "db.GetAllocatedFixtures") + } + allFixtureIDs := make([]int, len(allFixtures)) + for i, f := range allFixtures { + allFixtureIDs[i] = f.ID + } + allResultMap, err := db.GetFinalizedResultsForFixtures(ctx, tx, allFixtureIDs) + if err != nil { + return false, errors.Wrap(err, "db.GetFinalizedResultsForFixtures allFixtures") + } + + leaderboard = db.ComputeLeaderboard(allTeams, allFixtures, allResultMap) + return true, nil }); !ok { return } - record := db.ComputeTeamRecord(teamID, fixtures, resultMap) - renderSafely(seasonsview.SeasonLeagueTeamDetailPage(twr, fixtures, available, scheduleMap, resultMap, record, playerStats), s, r, w) + // Find this team's position and record from the leaderboard + var position int + var record *db.TeamRecord + for _, entry := range leaderboard { + if entry.Team.ID == teamID { + position = entry.Position + record = entry.Record + break + } + } + if record == nil { + record = &db.TeamRecord{} + } + + renderSafely(seasonsview.SeasonLeagueTeamDetailPage(twr, fixtures, available, scheduleMap, resultMap, record, playerStats, position, len(leaderboard)), s, r, w) }) } diff --git a/internal/view/seasonsview/fixture_review_result.templ b/internal/view/seasonsview/fixture_review_result.templ index e01796b..c41e931 100644 --- a/internal/view/seasonsview/fixture_review_result.templ +++ b/internal/view/seasonsview/fixture_review_result.templ @@ -260,3 +260,20 @@ func intPtrStr(v *int) string { } return fmt.Sprint(*v) } + +func ordinal(n int) string { + suffix := "th" + if n%100 >= 11 && n%100 <= 13 { + // 11th, 12th, 13th + } else { + switch n % 10 { + case 1: + suffix = "st" + case 2: + suffix = "nd" + case 3: + suffix = "rd" + } + } + return fmt.Sprintf("%d%s", n, suffix) +} diff --git a/internal/view/seasonsview/season_league_team_detail.templ b/internal/view/seasonsview/season_league_team_detail.templ index c4420d0..3019d01 100644 --- a/internal/view/seasonsview/season_league_team_detail.templ +++ b/internal/view/seasonsview/season_league_team_detail.templ @@ -8,7 +8,7 @@ import "fmt" import "sort" import "time" -templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture, available []*db.Player, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult, record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats) { +templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture, available []*db.Player, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult, record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats, position int, totalTeams int) { {{ team := twr.Team season := twr.Season @@ -62,14 +62,48 @@ templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture,
- + {{ + // Split fixtures into upcoming and completed + var upcoming []*db.Fixture + var completed []*db.Fixture + for _, f := range fixtures { + if _, hasResult := resultMap[f.ID]; hasResult { + completed = append(completed, f) + } else { + upcoming = append(upcoming, f) + } + } + // Sort completed by scheduled time descending (most recent first) + sort.Slice(completed, func(i, j int) bool { + ti := time.Time{} + tj := time.Time{} + if si, ok := scheduleMap[completed[i].ID]; ok && si.ScheduledTime != nil { + ti = *si.ScheduledTime + } + if sj, ok := scheduleMap[completed[j].ID]; ok && sj.ScheduledTime != nil { + tj = *sj.ScheduledTime + } + return ti.After(tj) + }) + // Limit to 5 most recent results + recentResults := completed + if len(recentResults) > 5 { + recentResults = recentResults[:5] + } + }}
+ + @teamRecordCard(record, position, totalTeams) + + @teamResultsSection(twr.Team, recentResults, resultMap) + @TeamRosterSection(twr, available) - @teamFixturesPane(twr.Team, fixtures, scheduleMap, resultMap) + + @teamUpcomingSection(twr.Team, upcoming, scheduleMap)
- +
- @teamStatsSection(record, playerStats) + @playerStatsSection(playerStats)
@@ -405,68 +439,45 @@ templ manageRosterModal(twr *db.TeamWithRoster, available []*db.Player, rosterPl } -templ teamFixturesPane(team *db.Team, fixtures []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult) { - {{ - // Split fixtures into upcoming and completed - var upcoming []*db.Fixture - var completed []*db.Fixture - for _, f := range fixtures { - if _, hasResult := resultMap[f.ID]; hasResult { - completed = append(completed, f) - } else { - upcoming = append(upcoming, f) - } - } - // Sort completed by scheduled time descending (most recent first) - sort.Slice(completed, func(i, j int) bool { - ti := time.Time{} - tj := time.Time{} - if si, ok := scheduleMap[completed[i].ID]; ok && si.ScheduledTime != nil { - ti = *si.ScheduledTime - } - if sj, ok := scheduleMap[completed[j].ID]; ok && sj.ScheduledTime != nil { - tj = *sj.ScheduledTime - } - return ti.After(tj) - }) - // Limit to 5 most recent results - recentResults := completed - if len(recentResults) > 5 { - recentResults = recentResults[:5] - } - }} -
- -
-

Results

- if len(recentResults) == 0 { -
-

No results yet.

-

Match results will appear here once games are played.

-
- } else { -
- for _, fixture := range recentResults { - @teamResultRow(team, fixture, resultMap) - } -
- } +templ teamResultsSection(team *db.Team, recentResults []*db.Fixture, resultMap map[int]*db.FixtureResult) { +
+
+

+ Results + (last 5) +

- -
-

Upcoming

- if len(upcoming) == 0 { -
-

No upcoming fixtures.

-
- } else { -
- for _, fixture := range upcoming { - @teamFixtureRow(team, fixture, scheduleMap) - } -
- } + if len(recentResults) == 0 { +
+

No results yet.

+

Match results will appear here once games are played.

+
+ } else { +
+ for _, fixture := range recentResults { + @teamResultRow(team, fixture, resultMap) + } +
+ } +
+} + +templ teamUpcomingSection(team *db.Team, upcoming []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule) { +
+
+

Upcoming

+ if len(upcoming) == 0 { +
+

No upcoming fixtures.

+
+ } else { +
+ for _, fixture := range upcoming { + @teamFixtureRow(team, fixture, scheduleMap) + } +
+ }
} @@ -595,69 +606,96 @@ templ teamResultRow(team *db.Team, fixture *db.Fixture, resultMap map[int]*db.Fi } -templ teamStatsSection(record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats) { +templ teamRecordCard(record *db.TeamRecord, position int, totalTeams int) {
-
-

Stats

+
+

Standing

if record.Played == 0 {
-

No stats yet.

-

Team statistics will appear here once games are played.

+

No games played yet.

} else { - -
-
- @statCell("Played", fmt.Sprint(record.Played), "") +
+ +
+
+ { ordinal(position) } +
+

Position

+

of { fmt.Sprint(totalTeams) } teams

+
+
+
+

Points

+

{ fmt.Sprint(record.Points) }

+
+
+ +
@statCell("W", fmt.Sprint(record.Wins), "text-green") @statCell("OTW", fmt.Sprint(record.OvertimeWins), "text-teal") @statCell("OTL", fmt.Sprint(record.OvertimeLosses), "text-peach") @statCell("L", fmt.Sprint(record.Losses), "text-red") +
+ +
+ @statCell("Played", fmt.Sprint(record.Played), "") @statCell("GF", fmt.Sprint(record.GoalsFor), "") @statCell("GA", fmt.Sprint(record.GoalsAgainst), "") - @statCell("PTS", fmt.Sprint(record.Points), "text-blue")
- - if len(playerStats) > 0 { -
-
- - - - - - - - - - - - - + } + +} + +templ playerStatsSection(playerStats []*db.AggregatedPlayerStats) { +
+
+

Player Stats

+
+ if len(playerStats) == 0 { +
+

No player stats yet.

+

Player statistics will appear here once games are played.

+
+ } else { +
+
+
PlayerGPPPSCGASVSHBLPA
+ + + + + + + + + + + + + + + + for _, ps := range playerStats { + + + + + + + + + + + - - - for _, ps := range playerStats { - - - - - - - - - - - - - } - -
PlayerGPPPSCGASVSHBLPA
{ ps.PlayerName }{ fmt.Sprint(ps.GamesPlayed) }{ fmt.Sprint(ps.PeriodsPlayed) }{ fmt.Sprint(ps.Score) }{ fmt.Sprint(ps.Goals) }{ fmt.Sprint(ps.Assists) }{ fmt.Sprint(ps.Saves) }{ fmt.Sprint(ps.Shots) }{ fmt.Sprint(ps.Blocks) }{ fmt.Sprint(ps.Passes) }
{ ps.PlayerName }{ fmt.Sprint(ps.GamesPlayed) }{ fmt.Sprint(ps.PeriodsPlayed) }{ fmt.Sprint(ps.Score) }{ fmt.Sprint(ps.Goals) }{ fmt.Sprint(ps.Assists) }{ fmt.Sprint(ps.Saves) }{ fmt.Sprint(ps.Shots) }{ fmt.Sprint(ps.Blocks) }{ fmt.Sprint(ps.Passes) }
-
+ } + +
- } +
}
} diff --git a/internal/view/teamsview/detail_seasons.templ b/internal/view/teamsview/detail_seasons.templ index 0bcbe3c..cfb5eeb 100644 --- a/internal/view/teamsview/detail_seasons.templ +++ b/internal/view/teamsview/detail_seasons.templ @@ -49,7 +49,7 @@ templ teamSeasonCard(team *db.Team, info *db.TeamSeasonInfo) {
Position - { fmt.Sprint(info.Position) } + { ordinal(info.Position) } / { fmt.Sprint(info.TotalTeams) } @@ -84,3 +84,20 @@ templ teamSeasonCard(team *db.Team, info *db.TeamSeasonInfo) {
} + +func ordinal(n int) string { + suffix := "th" + if n%100 >= 11 && n%100 <= 13 { + // 11th, 12th, 13th + } else { + switch n % 10 { + case 1: + suffix = "st" + case 2: + suffix = "nd" + case 3: + suffix = "rd" + } + } + return fmt.Sprintf("%d%s", n, suffix) +}