diff --git a/controller/AdminController.go b/controller/AdminController.go index 30c8809..d0eaaad 100644 --- a/controller/AdminController.go +++ b/controller/AdminController.go @@ -15,9 +15,7 @@ import ( // GetTimeStamp func GetCurrentTimestamp(w http.ResponseWriter, r *http.Request) { - timestamp := managers.GetTimestamp() - json.NewEncoder(w).Encode(timestamp) } @@ -29,23 +27,18 @@ func SyncTimestamp(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - newTimestamp := managers.UpdateTimestamp(updateTimestampDto) - json.NewEncoder(w).Encode(newTimestamp) } func SyncRecruiting(w http.ResponseWriter, r *http.Request) { ts := managers.GetTimestamp() - managers.SyncRecruiting(ts) - json.NewEncoder(w).Encode("Sync Complete") } func SyncWeek(w http.ResponseWriter, r *http.Request) { newTimestamp := managers.MoveUpWeek() - json.NewEncoder(w).Encode(newTimestamp) } @@ -55,16 +48,13 @@ func SyncTimeslot(w http.ResponseWriter, r *http.Request) { if len(timeslot) == 0 { log.Panicln("Missing timeslot!") } - managers.SyncTimeslot(timeslot) - json.NewEncoder(w).Encode("Timeslot updated") } func SyncFreeAgencyRound(w http.ResponseWriter, r *http.Request) { managers.SyncFreeAgencyOffers() managers.MoveUpInOffseasonFreeAgency() - // managers.AttemptToDecreaseMinimumValues() json.NewEncoder(w).Encode("Moved to next free agency round") } @@ -76,39 +66,15 @@ func GetWeeksInSeason(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) seasonID := vars["seasonID"] weekID := vars["weekID"] - weeks := managers.GetWeeksInASeason(seasonID, weekID) - json.NewEncoder(w).Encode(weeks) } -// CreateCollegeRecruit? - -// CreateNFLPlayer -- Create NFL Player from template, and then synthetically progress them based on the year of input - -// UpdateTeamRecruitingProfile - -// ApproveCoachForTeam - -// RemoveCoachFromTeam - -// UpdateTeam - -// RunProgressionsForCollege -func RunProgressionsForCollege(w http.ResponseWriter, r *http.Request) { - -} - -// GenerateWalkons func GenerateWalkOns(w http.ResponseWriter, r *http.Request) { managers.GenerateWalkOns() fmt.Println(w, "Walk ons successfully generated.") } -// RunProgressionsForNFL - -// RunProgressionsForJuco? - func SyncTeamRecruitingRanks(w http.ResponseWriter, r *http.Request) { managers.SyncTeamRankings() fmt.Println(w, "Team Ranks successfully generated.") diff --git a/controller/TSController.go b/controller/TSController.go index 2675019..52e60b1 100644 --- a/controller/TSController.go +++ b/controller/TSController.go @@ -90,6 +90,7 @@ func CreateTSModelsFile(w http.ResponseWriter, r *http.Request) { Add(structs.CollegeGame{}). Add(structs.NFLCapsheet{}). Add(structs.NFLContract{}). + Add(structs.NFLUDFABoard{}). Add(structs.FreeAgencyOffer{}). Add(structs.FreeAgencyOfferDTO{}). Add(structs.NFLWaiverOffDTO{}). diff --git a/controller/UDFAController.go b/controller/UDFAController.go new file mode 100644 index 0000000..d9b809e --- /dev/null +++ b/controller/UDFAController.go @@ -0,0 +1,86 @@ +package controller + +import ( + "encoding/json" + "net/http" + + "github.com/CalebRose/SimFBA/managers" + "github.com/CalebRose/SimFBA/structs" + "github.com/gorilla/mux" +) + +// GetUDFABoardByTeamID returns the user's specific UDFA bidding board +func GetUDFABoardByTeamID(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + teamID := vars["teamID"] + if len(teamID) == 0 { + panic("User did not provide TeamID") + } + + board := managers.GetUDFABoardByTeamID(teamID) + json.NewEncoder(w).Encode(board) +} + +// AddPlayerToUDFABoard adds a single player to the user's board +func AddPlayerToUDFABoard(w http.ResponseWriter, r *http.Request) { + var profile structs.NFLUDFAProfile + err := json.NewDecoder(r.Body).Decode(&profile) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Call the manager to execute the DB logic + managers.AddPlayerToUDFABoard(profile) + + // Return success to the frontend + json.NewEncoder(w).Encode(true) +} + +// SaveUDFABoard updates the points assigned to all players on the board +func SaveUDFABoard(w http.ResponseWriter, r *http.Request) { + var board structs.NFLUDFABoard + err := json.NewDecoder(r.Body).Decode(&board) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Call the manager to execute the DB logic + managers.SaveUDFABoard(board) + + // Return success to the frontend + json.NewEncoder(w).Encode(true) +} + +// RemovePlayerFromUDFABoard deletes a player from the user's board +func RemovePlayerFromUDFABoard(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + profileID := vars["profileID"] + if len(profileID) == 0 { + panic("User did not provide profileID") + } + + // Call the manager to execute the DB logic + managers.RemovePlayerFromUDFABoard(profileID) + + // Return success to the frontend + json.NewEncoder(w).Encode(true) +} + +// ProcessUDFAs triggers the batch signing process (Used by Admin) +func ProcessUDFAs(w http.ResponseWriter, r *http.Request) { + dryRunParam := r.URL.Query().Get("dryRun") + isDryRun := dryRunParam == "true" + + // Trigger the batch signing logic + managers.ProcessUDFAs(isDryRun) + + // Return a clean success message to display in the Admin Panel + msg := "LIVE UDFA Signings Executed Successfully!" + if isDryRun { + msg = "Dry Run Complete. Check backend server console for simulation results." + } + + json.NewEncoder(w).Encode(msg) +} diff --git a/dbprovider/dbprovider.go b/dbprovider/dbprovider.go index 7356ce4..330ebfc 100644 --- a/dbprovider/dbprovider.go +++ b/dbprovider/dbprovider.go @@ -146,6 +146,8 @@ func (p *Provider) InitDatabase() bool { // db.AutoMigrate(&structs.NFLStandings{}) // db.AutoMigrate(&structs.NFLRequest{}) // db.AutoMigrate(&structs.NFLWaiverOffer{}) + // db.AutoMigrate(&structs.NFLUDFABoard{}) + // db.AutoMigrate(&structs.NFLUDFAProfile{}) // All // db.AutoMigrate(&structs.AdminRecruitModifier{}) diff --git a/main.go b/main.go index 4f65881..eaa162c 100644 --- a/main.go +++ b/main.go @@ -122,6 +122,7 @@ func handleRequests() http.Handler { apiRouter.HandleFunc("/admin/trades/accept/sync/{proposalID}", controller.SyncAcceptedTrade).Methods("GET") apiRouter.HandleFunc("/admin/trades/veto/sync/{proposalID}", controller.VetoAcceptedTrade).Methods("GET") apiRouter.HandleFunc("/admin/trades/cleanup", controller.CleanUpRejectedTrades).Methods("GET") + apiRouter.HandleFunc("/admin/process-udfas", controller.ProcessUDFAs).Methods("GET") // Bootstrap apiRouter.HandleFunc("/bootstrap/teams", controller.BootstrapTeamData).Methods("GET") @@ -425,6 +426,13 @@ func handleRequests() http.Handler { apiRouter.HandleFunc("/portal/player/scout/{id}", controller.GetScoutingDataByTransfer).Methods("GET") apiRouter.HandleFunc("/portal/export/players/", controller.ExportPortalPlayersToCSV).Methods("GET") + // UDFA Controls + + apiRouter.HandleFunc("/nfl/udfa/board/{teamID}", controller.GetUDFABoardByTeamID).Methods("GET") + apiRouter.HandleFunc("/nfl/udfa/board/add", controller.AddPlayerToUDFABoard).Methods("POST") + apiRouter.HandleFunc("/nfl/udfa/board/save", controller.SaveUDFABoard).Methods("POST") + apiRouter.HandleFunc("/nfl/udfa/board/remove/{profileID}", controller.RemovePlayerFromUDFABoard).Methods("GET") + // Discord Controls apiRouter.HandleFunc("/ds/cfb/team/{teamID}/", controller.GetTeamByTeamIDForDiscord).Methods("GET") apiRouter.HandleFunc("/ds/college/player/indstats/{id}/{week}/", controller.GetCollegePlayerStatsByNameTeamAndWeek).Methods("GET") diff --git a/managers/ForumManager.go b/managers/ForumManager.go index 1744ad3..5107d2d 100644 --- a/managers/ForumManager.go +++ b/managers/ForumManager.go @@ -1051,3 +1051,70 @@ func buildRecruitingSyncParagraphs(season, week int, signings []string) []string return paragraphs } + +// ───────────────────────────────────────────── +// NFL UDFA Sync Thread +// ───────────────────────────────────────────── + +// CreateNFLUDFASyncForumThread creates a system-generated forum thread in +// the "media-simnfl" subforum summarising the signings from the UDFA period. +func CreateNFLUDFASyncForumThread(season int, signings []string) { + ctx := context.Background() + + title := fmt.Sprintf("SimNFL: Season %d UDFA Signings", season) + eventKey := fmt.Sprintf("udfa_sync:nfl:season%d", season) + + paragraphs := buildNFLUDFASyncParagraphs(season, signings) + bodyText := strings.Join(paragraphs, "\n\n") + richBody := buildRichPostBody(paragraphs) + + input := fbsvc.CreateForumThreadInput{ + ForumID: "media-simnfl", + ForumPath: []string{"media", "simnfl"}, + Title: title, + AuthorUID: "system", + AuthorUsername: "SimSN", + AuthorDisplayName: "SimSN System", + CreatedByType: fbsvc.CreatedBySystem, + ThreadType: fbsvc.ThreadTypeStandard, + FirstPostBodyText: bodyText, + FirstPostBody: richBody, + ReferencedLeague: "nfl", + ExternalEventKey: eventKey, + } + + thread, err := fbsvc.CreateThread(ctx, input) + if err != nil { + log.Printf("ForumManager: failed to create NFL UDFA sync thread for season %d: %v", season, err) + return + } + + log.Printf("ForumManager: created NFL UDFA sync thread %s for season %d", thread.ID, season) +} + +func buildNFLUDFASyncParagraphs(season int, signings []string) []string { + var paragraphs []string + + if len(signings) == 0 { + paragraphs = append(paragraphs, + fmt.Sprintf( + "The Undrafted Free Agency period has concluded for Season %d. No players were signed.", + season, + ), + ) + } else { + paragraphs = append(paragraphs, + fmt.Sprintf( + "The Undrafted Free Agency period has concluded for Season %d! The following players have signed with new teams:", + season, + ), + ) + for _, label := range signings { + paragraphs = append(paragraphs, label) + } + } + + paragraphs = append(paragraphs, "Discuss the latest UDFA additions and how they fit into their new rosters below!") + + return paragraphs +} diff --git a/managers/UDFAManager.go b/managers/UDFAManager.go new file mode 100644 index 0000000..5eae44f --- /dev/null +++ b/managers/UDFAManager.go @@ -0,0 +1,217 @@ +package managers + +import ( + "fmt" + "math/rand" + "strconv" + "time" + + "github.com/CalebRose/SimFBA/dbprovider" + "github.com/CalebRose/SimFBA/models" + "github.com/CalebRose/SimFBA/structs" +) + +// ------------------------------------------------------------------------ +// USER BOARD MANAGEMENT (The functions your Controller was looking for) +// ------------------------------------------------------------------------ + +func GetUDFABoardByTeamID(teamID string) structs.NFLUDFABoard { + db := dbprovider.GetInstance().GetDB() + var board structs.NFLUDFABoard + + // Find the board and load the players currently on it + db.Preload("Profiles").Where("team_id = ?", teamID).Find(&board) + + // If the team doesn't have a board yet, create an empty one + if board.ID == 0 { + var team structs.NFLTeam + db.Where("id = ?", teamID).Find(&team) + + board = structs.NFLUDFABoard{ + TeamID: team.ID, + TeamAbbr: team.TeamAbbr, + } + db.Create(&board) + } + + return board +} + +func AddPlayerToUDFABoard(dto structs.NFLUDFAProfile) { + db := dbprovider.GetInstance().GetDB() + + // Get the user's board + board := GetUDFABoardByTeamID(strconv.Itoa(int(dto.TeamID))) + + // Check if this player is already on the board to prevent duplicates + var existing structs.NFLUDFAProfile + db.Where("nfl_udfa_board_id = ? AND player_id = ?", board.ID, dto.PlayerID).Find(&existing) + + if existing.ID == 0 { + dto.NFLUDFABoardID = board.ID + db.Create(&dto) + } +} + +func SaveUDFABoard(dto structs.NFLUDFABoard) { + db := dbprovider.GetInstance().GetDB() + + // Loop through the submitted board and update the points for each player + for _, profile := range dto.Profiles { + var existing structs.NFLUDFAProfile + db.Where("id = ?", profile.ID).Find(&existing) + + if existing.ID > 0 { + existing.Points = profile.Points + db.Save(&existing) + } + } +} + +func RemovePlayerFromUDFABoard(profileID string) { + db := dbprovider.GetInstance().GetDB() + db.Where("id = ?", profileID).Delete(&structs.NFLUDFAProfile{}) +} + +// ------------------------------------------------------------------------ +// ADMIN BATCH PROCESSING (The logic to actually sign the players) +// ------------------------------------------------------------------------ + +func ProcessUDFAs(isDryRun bool) { + db := dbprovider.GetInstance().GetDB() + + // 1. Get all Undrafted Players + var undraftedPlayers []models.NFLDraftee + db.Where("draft_pick_id = 0 AND drafted_team_id = 0").Find(&undraftedPlayers) + + // 2. Get all Bids + var allBids []structs.NFLUDFAProfile + db.Find(&allBids) + + // Group bids by PlayerID + bidsByPlayer := make(map[uint][]structs.NFLUDFAProfile) + for _, bid := range allBids { + bidsByPlayer[bid.PlayerID] = append(bidsByPlayer[bid.PlayerID], bid) + } + + // NEW: Create a map to group signings by Team for the Forum Post + teamSignings := make(map[string][]string) + + for _, player := range undraftedPlayers { + bids := bidsByPlayer[player.ID] + if len(bids) == 0 { + continue // No one bid on this player + } + + // Find winning bid (Highest points) + var winningBid structs.NFLUDFAProfile + maxPoints := 0 + var tiedBids []structs.NFLUDFAProfile + + for _, bid := range bids { + if bid.Points > maxPoints { + maxPoints = bid.Points + winningBid = bid + tiedBids = []structs.NFLUDFAProfile{bid} + } else if bid.Points == maxPoints { + tiedBids = append(tiedBids, bid) + } + } + + // Tie-breaker: Random Roll + if len(tiedBids) > 1 { + rand.Seed(time.Now().UnixNano()) + winningBid = tiedBids[rand.Intn(len(tiedBids))] + } + + // Execute + if !isDryRun && winningBid.Points > 0 { + SignUDFA(player, winningBid) + + // NEW: Record the signing for the forum post + playerString := fmt.Sprintf("%s %s %s", player.Position, player.FirstName, player.LastName) + teamSignings[winningBid.TeamAbbr] = append(teamSignings[winningBid.TeamAbbr], playerString) + + } else if isDryRun { + fmt.Printf("DRY RUN: %s %s would sign with %s for %d points\n", player.FirstName, player.LastName, winningBid.TeamAbbr, winningBid.Points) + } + } + + // NEW: Generate the Forum Post if it was a live run and players were signed! + if !isDryRun && len(teamSignings) > 0 { + // Get the current NFL Season ID from the Timestamp + // ts := GetTimestamp() + + var forumSignings []string + + for teamAbbr, players := range teamSignings { + // Add the Team abbreviation as a bolded paragraph line + forumSignings = append(forumSignings, fmt.Sprintf("**%s**", teamAbbr)) + + // Add each player as a bullet point below the team + for _, p := range players { + forumSignings = append(forumSignings, fmt.Sprintf("• %s", p)) + } + } + + // Fire off the automated post to Firebase! + // CreateNFLUDFASyncForumThread(ts.NFLSeasonID, forumSignings) - COMMENTED OUT FOR LOCALTEST + } +} + +// SendToFreeAgency officially moves an unsigned UDFA into the Free Agency pool +// and sets their minimum contract demands. +func SendToFreeAgency(draftee models.NFLDraftee) { + db := dbprovider.GetInstance().GetDB() + + // 1. Fetch the actual NFLPlayer record associated with this Draftee + var proPlayer structs.NFLPlayer + db.Where("player_id = ?", draftee.PlayerID).Find(&proPlayer) + + // If we somehow can't find the pro player record, abort to prevent a crash + if proPlayer.ID == 0 { + return + } + + // 2. Set the minimum contract demands on the PRO player record + proPlayer.MinimumValue = 0.5 + proPlayer.AAV = 0.5 + proPlayer.IsFreeAgent = true + proPlayer.IsAcceptingOffers = true + + // 3. Save the updated PRO player to the database + // db.Save(&proPlayer) - COMMENTED OUT FOR LOCAL TEST +} + +func SignUDFA(draftee models.NFLDraftee, bid structs.NFLUDFAProfile) { + db := dbprovider.GetInstance().GetDB() + + // Convert Draftee to an active NFLPlayer + nflPlayer := structs.NFLPlayer{ + BasePlayer: draftee.BasePlayer, + TeamID: int(bid.TeamID), + TeamAbbr: bid.TeamAbbr, + Experience: 1, + IsActive: true, + IsFreeAgent: false, + } + nflPlayer.ID = draftee.ID + db.Create(&nflPlayer) + + // Create 3-year contract: 0.5M Salary, 0 Bonus + contract := structs.NFLContract{ + NFLPlayerID: int(nflPlayer.ID), + TeamID: bid.TeamID, + Team: bid.TeamAbbr, + ContractLength: 3, + Y1BaseSalary: 0.5, + Y2BaseSalary: 0.5, + Y3BaseSalary: 0.5, + IsActive: true, + } + contract.CalculateContract() // Updates the AAV and total values + db.Create(&contract) + + // Delete from draftee table so they don't show up in the draft pool anymore + db.Delete(&draftee) +} diff --git a/repository/UDFARepository.go b/repository/UDFARepository.go new file mode 100644 index 0000000..88c5426 --- /dev/null +++ b/repository/UDFARepository.go @@ -0,0 +1,26 @@ +package repository + +import ( + "github.com/CalebRose/SimFBA/dbprovider" + "github.com/CalebRose/SimFBA/structs" +) + +// GetUDFABoardByTeamID fetches the board and all associated bid profiles +func GetUDFABoardByTeamID(teamID string) structs.NFLUDFABoard { + db := dbprovider.GetInstance().GetDB() + var board structs.NFLUDFABoard + db.Preload("Profiles").Where("team_id = ?", teamID).First(&board) + return board +} + +// SaveUDFAProfile saves or updates a specific bid +func SaveUDFAProfile(profile structs.NFLUDFAProfile) error { + db := dbprovider.GetInstance().GetDB() + return db.Save(&profile).Error +} + +// DeleteUDFAProfile removes a bid from the board +func DeleteUDFAProfile(profileID string) error { + db := dbprovider.GetInstance().GetDB() + return db.Where("id = ?", profileID).Delete(&structs.NFLUDFAProfile{}).Error +} \ No newline at end of file diff --git a/structs/NFLUDFA.go b/structs/NFLUDFA.go new file mode 100644 index 0000000..1452917 --- /dev/null +++ b/structs/NFLUDFA.go @@ -0,0 +1,22 @@ +package structs + +import "github.com/jinzhu/gorm" + +type NFLUDFABoard struct { + gorm.Model + TeamID uint + TeamAbbr string + Profiles []NFLUDFAProfile `gorm:"foreignKey:NFLUDFABoardID"` +} + +type NFLUDFAProfile struct { + gorm.Model + NFLUDFABoardID uint + PlayerID uint + PlayerName string + Position string + TeamID uint + TeamAbbr string + Points int // The 1-20 points allocated + IsSigned bool +} \ No newline at end of file