Files
oslstats/internal/handlers/free_agents.go
2026-02-22 22:44:17 +11:00

357 lines
11 KiB
Go

package handlers
import (
"context"
"net/http"
"strconv"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/oslstats/internal/contexts"
"git.haelnorr.com/h/oslstats/internal/db"
"git.haelnorr.com/h/oslstats/internal/notify"
"git.haelnorr.com/h/oslstats/internal/permissions"
"git.haelnorr.com/h/oslstats/internal/respond"
"git.haelnorr.com/h/oslstats/internal/throw"
"git.haelnorr.com/h/oslstats/internal/validation"
"git.haelnorr.com/h/oslstats/internal/view/seasonsview"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
// FreeAgentsListPage renders the free agents tab of a season league page
func FreeAgentsListPage(
s *hws.Server,
conn *db.DB,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seasonStr := r.PathValue("season_short_name")
leagueStr := r.PathValue("league_short_name")
var season *db.Season
var league *db.League
var freeAgents []*db.SeasonLeagueFreeAgent
var availablePlayers []*db.Player
if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error
sl, err := db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr)
if err != nil {
if db.IsBadRequest(err) {
throw.NotFound(s, w, r, r.URL.Path)
return false, nil
}
return false, errors.Wrap(err, "db.GetSeasonLeague")
}
season = sl.Season
league = sl.League
freeAgents, err = db.GetFreeAgentsForSeasonLeague(ctx, tx, season.ID, league.ID)
if err != nil {
return false, errors.Wrap(err, "db.GetFreeAgentsForSeasonLeague")
}
availablePlayers, err = db.GetPlayersNotOnTeam(ctx, tx, season.ID, league.ID)
if err != nil {
return false, errors.Wrap(err, "db.GetPlayersNotOnTeam")
}
// Filter out players already registered as free agents
faMap := make(map[int]bool, len(freeAgents))
for _, fa := range freeAgents {
faMap[fa.PlayerID] = true
}
filtered := make([]*db.Player, 0, len(availablePlayers))
for _, p := range availablePlayers {
if !faMap[p.ID] {
filtered = append(filtered, p)
}
}
availablePlayers = filtered
return true, nil
}); !ok {
return
}
if r.Method == "GET" {
renderSafely(seasonsview.SeasonLeagueFreeAgentsPage(season, league, freeAgents, availablePlayers), s, r, w)
} else {
renderSafely(seasonsview.SeasonLeagueFreeAgents(season, league, freeAgents, availablePlayers), s, r, w)
}
})
}
// RegisterFreeAgent handles POST to register a player as a free agent
func RegisterFreeAgent(
s *hws.Server,
conn *db.DB,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seasonStr := r.PathValue("season_short_name")
leagueStr := r.PathValue("league_short_name")
getter, ok := validation.ParseFormOrNotify(s, w, r)
if !ok {
respond.BadRequest(w, errors.New("failed to parse form"))
return
}
playerID := getter.Int("player_id").Required().Value
if !getter.ValidateAndNotify(s, w, r) {
respond.BadRequest(w, errors.New("invalid form data"))
return
}
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
sl, err := db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr)
if err != nil {
if db.IsBadRequest(err) {
throw.NotFound(s, w, r, r.URL.Path)
return false, nil
}
return false, errors.Wrap(err, "db.GetSeasonLeague")
}
// Verify player is not on a team in this season_league
players, err := db.GetPlayersNotOnTeam(ctx, tx, sl.Season.ID, sl.League.ID)
if err != nil {
return false, errors.Wrap(err, "db.GetPlayersNotOnTeam")
}
playerFound := false
for _, p := range players {
if p.ID == playerID {
playerFound = true
break
}
}
if !playerFound {
notify.Warn(s, w, r, "Cannot Register", "Player is already on a team in this league.", nil)
return false, nil
}
// Check if already registered
isRegistered, err := db.IsFreeAgentRegistered(ctx, tx, sl.Season.ID, sl.League.ID, playerID)
if err != nil {
return false, errors.Wrap(err, "db.IsFreeAgentRegistered")
}
if isRegistered {
notify.Warn(s, w, r, "Already Registered", "Player is already registered as a free agent.", nil)
return false, nil
}
err = db.RegisterFreeAgent(ctx, tx, sl.Season.ID, sl.League.ID, playerID, db.NewAuditFromRequest(r))
if err != nil {
return false, errors.Wrap(err, "db.RegisterFreeAgent")
}
return true, nil
}); !ok {
return
}
notify.Success(s, w, r, "Free Agent Registered", "Player has been registered as a free agent.", nil)
respond.HXRedirect(w, "/seasons/%s/leagues/%s/free-agents", seasonStr, leagueStr)
})
}
// UnregisterFreeAgent handles POST to unregister a player as a free agent
func UnregisterFreeAgent(
s *hws.Server,
conn *db.DB,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seasonStr := r.PathValue("season_short_name")
leagueStr := r.PathValue("league_short_name")
getter, ok := validation.ParseFormOrNotify(s, w, r)
if !ok {
respond.BadRequest(w, errors.New("failed to parse form"))
return
}
playerID := getter.Int("player_id").Required().Value
if !getter.ValidateAndNotify(s, w, r) {
respond.BadRequest(w, errors.New("invalid form data"))
return
}
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
sl, err := db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr)
if err != nil {
if db.IsBadRequest(err) {
throw.NotFound(s, w, r, r.URL.Path)
return false, nil
}
return false, errors.Wrap(err, "db.GetSeasonLeague")
}
err = db.UnregisterFreeAgent(ctx, tx, sl.Season.ID, sl.League.ID, playerID, db.NewAuditFromRequest(r))
if err != nil {
return false, errors.Wrap(err, "db.UnregisterFreeAgent")
}
return true, nil
}); !ok {
return
}
notify.Success(s, w, r, "Free Agent Removed", "Player has been unregistered as a free agent.", nil)
respond.HXRedirect(w, "/seasons/%s/leagues/%s/free-agents", seasonStr, leagueStr)
})
}
// NominateFreeAgentHandler handles POST to nominate a free agent for a fixture
func NominateFreeAgentHandler(
s *hws.Server,
conn *db.DB,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fixtureID, err := strconv.Atoi(r.PathValue("fixture_id"))
if err != nil {
throw.BadRequest(s, w, r, "Invalid fixture ID", err)
return
}
getter, ok := validation.ParseFormOrNotify(s, w, r)
if !ok {
return
}
playerID := getter.Int("player_id").Required().Value
teamID := getter.Int("team_id").Required().Value
if !getter.ValidateAndNotify(s, w, r) {
return
}
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
// Verify fixture exists and user is a manager
fixture, err := db.GetFixture(ctx, tx, fixtureID)
if err != nil {
if db.IsBadRequest(err) {
respond.NotFound(w, errors.Wrap(err, "db.GetFixture"))
return false, nil
}
return false, errors.Wrap(err, "db.GetFixture")
}
// Check if user can nominate: either a manager of the nominating team,
// or has fixtures.manage permission (can nominate for either team)
user := db.CurrentUser(ctx)
canManage := contexts.Permissions(ctx).HasPermission(permissions.FixturesManage)
if !canManage {
canSchedule, userTeamID, err := fixture.CanSchedule(ctx, tx, user)
if err != nil {
return false, errors.Wrap(err, "fixture.CanSchedule")
}
if !canSchedule || userTeamID != teamID {
throw.Forbidden(s, w, r, "You must be a manager of the nominating team", nil)
return false, nil
}
}
// Verify the team_id is actually one of the fixture's teams
if teamID != fixture.HomeTeamID && teamID != fixture.AwayTeamID {
throw.BadRequest(s, w, r, "Invalid team for this fixture", nil)
return false, nil
}
// Verify player is a registered free agent in this season_league
isRegistered, err := db.IsFreeAgentRegistered(ctx, tx, fixture.SeasonID, fixture.LeagueID, playerID)
if err != nil {
return false, errors.Wrap(err, "db.IsFreeAgentRegistered")
}
if !isRegistered {
notify.Warn(s, w, r, "Not Registered", "Player is not a registered free agent in this league.", nil)
return false, nil
}
err = db.NominateFreeAgent(ctx, tx, fixtureID, playerID, teamID, db.NewAuditFromRequest(r))
if err != nil {
if db.IsBadRequest(err) {
notify.Warn(s, w, r, "Cannot Nominate", err.Error(), nil)
return false, nil
}
return false, errors.Wrap(err, "db.NominateFreeAgent")
}
return true, nil
}); !ok {
return
}
notify.Success(s, w, r, "Free Agent Nominated", "Free agent has been nominated for this fixture.", nil)
respond.HXRedirect(w, "/fixtures/%d", fixtureID)
})
}
// RemoveFreeAgentNominationHandler handles POST to remove a free agent nomination
func RemoveFreeAgentNominationHandler(
s *hws.Server,
conn *db.DB,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fixtureID, err := strconv.Atoi(r.PathValue("fixture_id"))
if err != nil {
throw.BadRequest(s, w, r, "Invalid fixture ID", err)
return
}
playerID, err := strconv.Atoi(r.PathValue("player_id"))
if err != nil {
throw.BadRequest(s, w, r, "Invalid player ID", err)
return
}
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
// Check if user can remove: either has fixtures.manage permission,
// or is a manager of the team that nominated the free agent
canManage := contexts.Permissions(ctx).HasPermission(permissions.FixturesManage)
if !canManage {
fixture, err := db.GetFixture(ctx, tx, fixtureID)
if err != nil {
if db.IsBadRequest(err) {
respond.NotFound(w, errors.Wrap(err, "db.GetFixture"))
return false, nil
}
return false, errors.Wrap(err, "db.GetFixture")
}
user := db.CurrentUser(ctx)
canSchedule, userTeamID, err := fixture.CanSchedule(ctx, tx, user)
if err != nil {
return false, errors.Wrap(err, "fixture.CanSchedule")
}
if !canSchedule {
throw.Forbidden(s, w, r, "You must be a team manager to remove nominations", nil)
return false, nil
}
// Verify the nomination belongs to the user's team
nominations, err := db.GetNominatedFreeAgentsByTeam(ctx, tx, fixtureID, userTeamID)
if err != nil {
return false, errors.Wrap(err, "db.GetNominatedFreeAgentsByTeam")
}
found := false
for _, n := range nominations {
if n.PlayerID == playerID {
found = true
break
}
}
if !found {
throw.Forbidden(s, w, r, "You can only remove nominations made by your team", nil)
return false, nil
}
}
err := db.RemoveFreeAgentNomination(ctx, tx, fixtureID, playerID, db.NewAuditFromRequest(r))
if err != nil {
return false, errors.Wrap(err, "db.RemoveFreeAgentNomination")
}
return true, nil
}); !ok {
return
}
notify.Success(s, w, r, "Nomination Removed", "Free agent nomination has been removed.", nil)
respond.HXRedirect(w, "/fixtures/%d", fixtureID)
})
}