added team view to season_leagues

This commit is contained in:
2026-02-20 19:57:06 +11:00
parent c3d8e6c675
commit 7ea21c63e4
25 changed files with 1018 additions and 51 deletions

View File

@@ -33,7 +33,7 @@ type Fixture struct {
func NewFixture(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string,
homeTeamID, awayTeamID, round int, audit *AuditMeta,
) (*Fixture, error) {
season, league, teams, err := GetSeasonLeague(ctx, tx, seasonShortName, leagueShortName)
season, league, teams, err := GetSeasonLeagueWithTeams(ctx, tx, seasonShortName, leagueShortName)
if err != nil {
return nil, errors.Wrap(err, "GetSeasonLeague")
}
@@ -59,7 +59,7 @@ func NewFixture(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName
func NewRound(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string,
round int, audit *AuditMeta,
) ([]*Fixture, error) {
season, league, teams, err := GetSeasonLeague(ctx, tx, seasonShortName, leagueShortName)
season, league, teams, err := GetSeasonLeagueWithTeams(ctx, tx, seasonShortName, leagueShortName)
if err != nil {
return nil, errors.Wrap(err, "GetSeasonLeague")
}
@@ -71,22 +71,22 @@ func NewRound(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName s
return fixtures, nil
}
func GetFixtures(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string) (*Season, *League, []*Fixture, error) {
season, league, _, err := GetSeasonLeague(ctx, tx, seasonShortName, leagueShortName)
func GetFixtures(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string) (*SeasonLeague, []*Fixture, error) {
sl, err := GetSeasonLeague(ctx, tx, seasonShortName, leagueShortName)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "GetSeasonLeague")
return nil, nil, errors.Wrap(err, "GetSeasonLeague")
}
fixtures, err := GetList[Fixture](tx).
Where("season_id = ?", season.ID).
Where("league_id = ?", league.ID).
Where("season_id = ?", sl.SeasonID).
Where("league_id = ?", sl.LeagueID).
Order("game_week ASC NULLS FIRST", "round ASC", "id ASC").
Relation("HomeTeam").
Relation("AwayTeam").
GetAll(ctx)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "GetList")
return nil, nil, errors.Wrap(err, "GetList")
}
return season, league, fixtures, nil
return sl, fixtures, nil
}
func GetFixture(ctx context.Context, tx bun.Tx, id int) (*Fixture, error) {
@@ -98,6 +98,22 @@ func GetFixture(ctx context.Context, tx bun.Tx, id int) (*Fixture, error) {
Get(ctx)
}
func GetFixturesForTeam(ctx context.Context, tx bun.Tx, seasonID, leagueID, teamID int) ([]*Fixture, error) {
fixtures, err := GetList[Fixture](tx).
Where("season_id = ?", seasonID).
Where("league_id = ?", leagueID).
Where("game_week IS NOT NULL").
Where("(home_team_id = ? OR away_team_id = ?)", teamID, teamID).
Order("game_week ASC", "round ASC", "id ASC").
Relation("HomeTeam").
Relation("AwayTeam").
GetAll(ctx)
if err != nil {
return nil, errors.Wrap(err, "GetList")
}
return fixtures, nil
}
func GetFixturesByGameWeek(ctx context.Context, tx bun.Tx, seasonID, leagueID, gameweek int) ([]*Fixture, error) {
fixtures, err := GetList[Fixture](tx).
Where("season_id = ?", seasonID).
@@ -180,13 +196,13 @@ func UpdateFixtureGameWeeks(ctx context.Context, tx bun.Tx, fixtures []*Fixture,
}
func DeleteAllFixtures(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string, audit *AuditMeta) error {
season, league, _, err := GetSeasonLeague(ctx, tx, seasonShortName, leagueShortName)
sl, err := GetSeasonLeague(ctx, tx, seasonShortName, leagueShortName)
if err != nil {
return errors.Wrap(err, "GetSeasonLeague")
}
err = DeleteItem[Fixture](tx).
Where("season_id = ?", season.ID).
Where("league_id = ?", league.ID).
Where("season_id = ?", sl.SeasonID).
Where("league_id = ?", sl.LeagueID).
WithAudit(audit, nil).
Delete(ctx)
if err != nil {

View File

@@ -0,0 +1,30 @@
package migrations
import (
"context"
"git.haelnorr.com/h/oslstats/internal/db"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(
// UP migration
func(ctx context.Context, conn *bun.DB) error {
// Add your migration code here
_, err := conn.NewCreateTable().
IfNotExists().
Model((*db.TeamRoster)(nil)).
Exec(ctx)
if err != nil {
return err
}
return nil
},
// DOWN migration
func(ctx context.Context, conn *bun.DB) error {
// Add your rollback code here
return nil
},
)
}

View File

@@ -90,3 +90,16 @@ func UpdatePlayerSlapID(ctx context.Context, tx bun.Tx, playerID int, slapID uin
}
return nil
}
func GetPlayersNotOnTeam(ctx context.Context, tx bun.Tx, seasonID, leagueID int) ([]*Player, error) {
players, err := GetList[Player](tx).Relation("User").
Join("LEFT JOIN team_rosters tr ON tr.player_id = p.id").
Where("NOT (tr.season_id = ? and tr.league_id = ?) OR (tr.season_id IS NULL and tr.league_id IS NULL)",
seasonID, leagueID).
Order("p.name ASC").
GetAll(ctx)
if err != nil {
return nil, errors.Wrap(err, "GetList")
}
return players, nil
}

View File

@@ -2,6 +2,7 @@ package db
import (
"context"
"database/sql"
"git.haelnorr.com/h/oslstats/internal/permissions"
"github.com/pkg/errors"
@@ -15,8 +16,36 @@ type SeasonLeague struct {
League *League `bun:"rel:belongs-to,join:league_id=id"`
}
// GetSeasonLeague retrieves a specific season-league combination with teams
func GetSeasonLeague(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string) (*Season, *League, []*Team, error) {
// GetSeasonLeague retrieves a specific season-league combination
func GetSeasonLeague(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string) (*SeasonLeague, error) {
if seasonShortName == "" {
return nil, errors.New("season short_name cannot be empty")
}
if leagueShortName == "" {
return nil, errors.New("league short_name cannot be empty")
}
sl := new(SeasonLeague)
err := tx.NewSelect().
Model(sl).
Relation("Season", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("season.short_name = ?", seasonShortName)
}).
Relation("League", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("league.short_name = ?", leagueShortName)
}).Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, BadRequestNotFound("season_league", "season.short_name,league.short_name", seasonShortName+","+leagueShortName)
}
return nil, errors.Wrap(err, "tx.NewSelect")
}
return sl, nil
}
// GetSeasonLeagueWithTeams retrieves a specific season-league combination with teams
func GetSeasonLeagueWithTeams(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string) (*Season, *League, []*Team, error) {
if seasonShortName == "" {
return nil, nil, nil, errors.New("season short_name cannot be empty")
}
@@ -41,6 +70,9 @@ func GetSeasonLeague(ctx context.Context, tx bun.Tx, seasonShortName, leagueShor
Join("INNER JOIN team_participations AS tp ON tp.team_id = t.id").
Where("tp.season_id = ? AND tp.league_id = ?", season.ID, league.ID).
Order("t.name ASC").
Relation("Players", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("season_id = ? AND league_id = ?", season.ID, league.ID)
}).
Scan(ctx)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "tx.Select teams")

View File

@@ -24,6 +24,7 @@ func (db *DB) RegisterModels() []any {
(*UserRole)(nil),
(*SeasonLeague)(nil),
(*TeamParticipation)(nil),
(*TeamRoster)(nil),
(*User)(nil),
(*DiscordToken)(nil),
(*Season)(nil),

View File

@@ -17,6 +17,7 @@ type Team struct {
Seasons []Season `bun:"m2m:team_participations,join:Team=Season" json:"-"`
Leagues []League `bun:"m2m:team_participations,join:Team=League" json:"-"`
Players []Player `bun:"m2m:team_rosters,join:Team=Player" json:"-"`
}
func NewTeam(ctx context.Context, tx bun.Tx, name, shortName, altShortName, color string, audit *AuditMeta) (*Team, error) {

176
internal/db/teamroster.go Normal file
View File

@@ -0,0 +1,176 @@
package db
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
type TeamRoster struct {
bun.BaseModel `bun:"table:team_rosters,alias:tr"`
TeamID int `bun:",pk,notnull" json:"team_id"`
SeasonID int `bun:",pk,notnull,unique:player" json:"season_id"`
LeagueID int `bun:",pk,notnull,unique:player" json:"league_id"`
PlayerID int `bun:",pk,notnull,unique:player" json:"player_id"`
IsManager bool `bun:"is_manager,default:'false'" json:"is_manager"`
Team *Team `bun:"rel:belongs-to,join:team_id=id" json:"-"`
Player *Player `bun:"rel:belongs-to,join:player_id=id" json:"-"`
Season *Season `bun:"rel:belongs-to,join:season_id=id" json:"-"`
League *League `bun:"rel:belongs-to,join:league_id=id" json:"-"`
}
type TeamWithRoster struct {
Team *Team
Season *Season
League *League
Manager *Player
Players []*Player
}
func GetTeamRoster(ctx context.Context, tx bun.Tx, seasonShortName, leagueShortName string, teamID int) (*TeamWithRoster, error) {
tr := []*TeamRoster{}
err := tx.NewSelect().
Model(&tr).
Relation("Team", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("team.id = ?", teamID)
}).
Relation("Season", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("season.short_name = ?", seasonShortName)
}).
Relation("League", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("league.short_name = ?", leagueShortName)
}).
Relation("Player").Scan(ctx)
if err != nil {
return nil, errors.Wrap(err, "tx.NewSelect")
}
team, err := GetTeam(ctx, tx, teamID)
if err != nil {
return nil, errors.Wrap(err, "GetTeam")
}
sl, err := GetSeasonLeague(ctx, tx, seasonShortName, leagueShortName)
if err != nil {
return nil, errors.Wrap(err, "GetSeasonLeague")
}
var manager *Player
players := []*Player{}
for _, tp := range tr {
if tp.IsManager {
manager = tp.Player
} else {
players = append(players, tp.Player)
}
}
players = append([]*Player{manager}, players...)
twr := &TeamWithRoster{
team,
sl.Season,
sl.League,
manager,
players,
}
return twr, nil
}
func AddPlayerToTeam(ctx context.Context, tx bun.Tx, seasonID, leagueID, teamID, playerID int, manager bool, audit *AuditMeta) error {
season, err := GetByID[Season](tx, seasonID).Get(ctx)
if err != nil {
return errors.Wrap(err, "GetSeason")
}
league, err := GetByID[League](tx, leagueID).Get(ctx)
if err != nil {
return errors.Wrap(err, "GetLeague")
}
team, err := GetByID[Team](tx, teamID).Get(ctx)
if err != nil {
return errors.Wrap(err, "GetTeam")
}
player, err := GetByID[Player](tx, playerID).Get(ctx)
if err != nil {
return errors.Wrap(err, "GetPlayer")
}
tr := &TeamRoster{
SeasonID: season.ID,
LeagueID: league.ID,
TeamID: team.ID,
PlayerID: player.ID,
IsManager: manager,
}
err = Insert(tx, tr).WithAudit(audit, nil).Exec(ctx)
if err != nil {
return errors.Wrap(err, "Insert")
}
return nil
}
// ManageTeamRoster replaces the entire roster for a team in a season/league.
// It deletes all existing roster entries and inserts the new ones.
func ManageTeamRoster(ctx context.Context, tx bun.Tx, seasonID, leagueID, teamID, managerID int, playerIDs []int, audit *AuditMeta) error {
// Delete all existing roster entries for this team/season/league
_, err := tx.NewDelete().
Model((*TeamRoster)(nil)).
Where("season_id = ?", seasonID).
Where("league_id = ?", leagueID).
Where("team_id = ?", teamID).
Exec(ctx)
if err != nil {
return errors.Wrap(err, "delete existing roster")
}
// Insert manager if provided
if managerID > 0 {
tr := &TeamRoster{
SeasonID: seasonID,
LeagueID: leagueID,
TeamID: teamID,
PlayerID: managerID,
IsManager: true,
}
err = Insert(tx, tr).Exec(ctx)
if err != nil {
return errors.Wrap(err, "Insert manager")
}
}
// Insert players
for _, playerID := range playerIDs {
if playerID == managerID {
continue // Already inserted as manager
}
tr := &TeamRoster{
SeasonID: seasonID,
LeagueID: leagueID,
TeamID: teamID,
PlayerID: playerID,
IsManager: false,
}
err = Insert(tx, tr).Exec(ctx)
if err != nil {
return errors.Wrap(err, "Insert player")
}
}
// Log the roster change
details := map[string]any{
"season_id": seasonID,
"league_id": leagueID,
"team_id": teamID,
"manager_id": managerID,
"player_ids": playerIDs,
}
info := &AuditInfo{
"teams.manage_players",
"team_roster",
fmt.Sprintf("%d-%d-%d", seasonID, leagueID, teamID),
details,
}
err = LogSuccess(ctx, tx, audit, info)
if err != nil {
return errors.Wrap(err, "LogSuccess")
}
return nil
}