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) }) }