From c70b9c554e474b370959757c7ef4e74c6ffe78e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20Gol=C3=A1=C5=88=20jr?= Date: Wed, 6 May 2026 13:28:10 +0200 Subject: [PATCH] Tournament management --- .../Database/TournamentContext.cs | 13 + ...260506083354_AddTournamentTeam.Designer.cs | 392 +++++++++ .../20260506083354_AddTournamentTeam.cs | 51 ++ .../TournamentContextModelSnapshot.cs | 44 + TournamentOrganizer/Models/Event.cs | 2 +- TournamentOrganizer/Models/Game.cs | 2 +- TournamentOrganizer/Models/Match.cs | 6 +- TournamentOrganizer/Models/Participant.cs | 20 +- TournamentOrganizer/Models/Player.cs | 2 +- TournamentOrganizer/Models/Round.cs | 4 +- TournamentOrganizer/Models/Team.cs | 5 +- TournamentOrganizer/Models/Tournament.cs | 6 +- .../ViewModels/MainViewModel.cs | 9 + .../ViewModels/TeamsViewModel.cs | 12 +- .../ViewModels/TournamentsViewModel.cs | 807 ++++++++++++++++++ TournamentOrganizer/Views/MainView.axaml | 1 + TournamentOrganizer/Views/TeamsView.axaml | 29 +- TournamentOrganizer/Views/TeamsView.axaml.cs | 8 +- .../Views/TournamentsView.axaml | 237 +++++ .../Views/TournamentsView.axaml.cs | 53 ++ 20 files changed, 1667 insertions(+), 36 deletions(-) create mode 100644 TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.Designer.cs create mode 100644 TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.cs create mode 100644 TournamentOrganizer/ViewModels/TournamentsViewModel.cs create mode 100644 TournamentOrganizer/Views/TournamentsView.axaml create mode 100644 TournamentOrganizer/Views/TournamentsView.axaml.cs diff --git a/TournamentOrganizer/Database/TournamentContext.cs b/TournamentOrganizer/Database/TournamentContext.cs index c1e108d..c3eea3e 100644 --- a/TournamentOrganizer/Database/TournamentContext.cs +++ b/TournamentOrganizer/Database/TournamentContext.cs @@ -17,6 +17,7 @@ public class TournamentContext : DbContext public DbSet Rounds { get; set; } public DbSet Teams { get; set; } public DbSet Tournaments { get; set; } + public DbSet TournamentTeams { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -41,6 +42,18 @@ public class TournamentContext : DbContext .WithMany() .HasForeignKey(t => t.LeaderId) .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasOne(tt => tt.Tournament) + .WithMany(t => t.TournamentTeams) + .HasForeignKey(tt => tt.TournamentId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(tt => tt.Team) + .WithMany(t => t.TournamentTeams) + .HasForeignKey(tt => tt.TeamId) + .OnDelete(DeleteBehavior.Cascade); } } diff --git a/TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.Designer.cs b/TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.Designer.cs new file mode 100644 index 0000000..77575a3 --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.Designer.cs @@ -0,0 +1,392 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TournamentOrganizer; + +#nullable disable + +namespace TournamentOrganizer.Migrations +{ + [DbContext(typeof(TournamentContext))] + [Migration("20260506083354_AddTournamentTeam")] + partial class AddTournamentTeam + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("TournamentOrganizer.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("End") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("S1GroupAdvances") + .HasColumnType("INTEGER"); + + b.Property("S1Groups") + .HasColumnType("INTEGER"); + + b.Property("S1RuleSet") + .HasColumnType("INTEGER"); + + b.Property("S2RuleSet") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Match", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TournamentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TournamentId"); + + b.ToTable("Matches"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Contact") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TeamId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TeamId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.PlayerParticipant", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("RoundId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PlayerParticipants"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MatchId"); + + b.ToTable("Rounds"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Team", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LeaderId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaderId"); + + b.ToTable("Teams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.TeamParticipant", b => + { + b.Property("MatchId") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("Seed") + .HasColumnType("INTEGER"); + + b.HasKey("MatchId", "TeamId"); + + b.HasIndex("TeamId"); + + b.ToTable("TeamParticipants"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Tournament", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("End") + .HasColumnType("TEXT"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("S1GroupAdvances") + .HasColumnType("INTEGER"); + + b.Property("S1Groups") + .HasColumnType("INTEGER"); + + b.Property("S1RuleSet") + .HasColumnType("INTEGER"); + + b.Property("S2RuleSet") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.HasIndex("GameId"); + + b.ToTable("Tournaments"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.TournamentTeam", b => + { + b.Property("TournamentId") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("INTEGER"); + + b.Property("Seed") + .HasColumnType("INTEGER"); + + b.HasKey("TournamentId", "TeamId"); + + b.HasIndex("TeamId"); + + b.ToTable("TournamentTeams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Match", b => + { + b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") + .WithMany() + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Player", b => + { + b.HasOne("TournamentOrganizer.Models.Team", "Team") + .WithMany("Players") + .HasForeignKey("TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.PlayerParticipant", b => + { + b.HasOne("TournamentOrganizer.Models.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Round", b => + { + b.HasOne("TournamentOrganizer.Models.Match", "Match") + .WithMany() + .HasForeignKey("MatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Match"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Team", b => + { + b.HasOne("TournamentOrganizer.Models.Player", "Leader") + .WithMany() + .HasForeignKey("LeaderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Leader"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.TeamParticipant", b => + { + b.HasOne("TournamentOrganizer.Models.Match", "Round") + .WithMany() + .HasForeignKey("MatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Team", "Team") + .WithMany("Matches") + .HasForeignKey("TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Round"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Tournament", b => + { + b.HasOne("TournamentOrganizer.Models.Event", "Event") + .WithMany("Tournaments") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Game", "Game") + .WithMany("Tournaments") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.TournamentTeam", b => + { + b.HasOne("TournamentOrganizer.Models.Team", "Team") + .WithMany("TournamentTeams") + .HasForeignKey("TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") + .WithMany("TournamentTeams") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Team"); + + b.Navigation("Tournament"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Event", b => + { + b.Navigation("Tournaments"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Game", b => + { + b.Navigation("Tournaments"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Team", b => + { + b.Navigation("Matches"); + + b.Navigation("Players"); + + b.Navigation("TournamentTeams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Tournament", b => + { + b.Navigation("TournamentTeams"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.cs b/TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.cs new file mode 100644 index 0000000..55b964e --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506083354_AddTournamentTeam.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TournamentOrganizer.Migrations +{ + /// + public partial class AddTournamentTeam : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TournamentTeams", + columns: table => new + { + TournamentId = table.Column(type: "INTEGER", nullable: false), + TeamId = table.Column(type: "INTEGER", nullable: false), + Seed = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TournamentTeams", x => new { x.TournamentId, x.TeamId }); + table.ForeignKey( + name: "FK_TournamentTeams_Teams_TeamId", + column: x => x.TeamId, + principalTable: "Teams", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TournamentTeams_Tournaments_TournamentId", + column: x => x.TournamentId, + principalTable: "Tournaments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TournamentTeams_TeamId", + table: "TournamentTeams", + column: "TeamId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TournamentTeams"); + } + } +} diff --git a/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs b/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs index c22b73e..191f5ac 100644 --- a/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs +++ b/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs @@ -223,6 +223,24 @@ namespace TournamentOrganizer.Migrations b.ToTable("Tournaments"); }); + modelBuilder.Entity("TournamentOrganizer.Models.TournamentTeam", b => + { + b.Property("TournamentId") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("INTEGER"); + + b.Property("Seed") + .HasColumnType("INTEGER"); + + b.HasKey("TournamentId", "TeamId"); + + b.HasIndex("TeamId"); + + b.ToTable("TournamentTeams"); + }); + modelBuilder.Entity("TournamentOrganizer.Models.Match", b => { b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") @@ -323,6 +341,25 @@ namespace TournamentOrganizer.Migrations b.Navigation("Game"); }); + modelBuilder.Entity("TournamentOrganizer.Models.TournamentTeam", b => + { + b.HasOne("TournamentOrganizer.Models.Team", "Team") + .WithMany("TournamentTeams") + .HasForeignKey("TeamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") + .WithMany("TournamentTeams") + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Team"); + + b.Navigation("Tournament"); + }); + modelBuilder.Entity("TournamentOrganizer.Models.Event", b => { b.Navigation("Tournaments"); @@ -338,6 +375,13 @@ namespace TournamentOrganizer.Migrations b.Navigation("Matches"); b.Navigation("Players"); + + b.Navigation("TournamentTeams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Tournament", b => + { + b.Navigation("TournamentTeams"); }); #pragma warning restore 612, 618 } diff --git a/TournamentOrganizer/Models/Event.cs b/TournamentOrganizer/Models/Event.cs index 4104c39..620943e 100644 --- a/TournamentOrganizer/Models/Event.cs +++ b/TournamentOrganizer/Models/Event.cs @@ -14,6 +14,6 @@ public class Event public DateTime Start { get; set; } public DateTime End { get; set; } - public required List Tournaments { get; set; } + public List Tournaments { get; set; } = []; } \ No newline at end of file diff --git a/TournamentOrganizer/Models/Game.cs b/TournamentOrganizer/Models/Game.cs index 6f95d5c..4bd4fe8 100644 --- a/TournamentOrganizer/Models/Game.cs +++ b/TournamentOrganizer/Models/Game.cs @@ -47,6 +47,6 @@ public class Game public RuleSet? S2RuleSet { get; set; } - public required List Tournaments { get; set; } + public List Tournaments { get; set; } = []; } \ No newline at end of file diff --git a/TournamentOrganizer/Models/Match.cs b/TournamentOrganizer/Models/Match.cs index b2be3cc..b412cb8 100644 --- a/TournamentOrganizer/Models/Match.cs +++ b/TournamentOrganizer/Models/Match.cs @@ -11,8 +11,8 @@ public class Match public int Id { get; set; } public int TournamentId { get; set; } - public required List Teams; - public required List Rounds; - public required Tournament Tournament { get; set; } + public List Teams { get; set; } = []; + public List Rounds { get; set; } = []; + public Tournament Tournament { get; set; } = null!; } \ No newline at end of file diff --git a/TournamentOrganizer/Models/Participant.cs b/TournamentOrganizer/Models/Participant.cs index efc3ab0..b28ef95 100644 --- a/TournamentOrganizer/Models/Participant.cs +++ b/TournamentOrganizer/Models/Participant.cs @@ -7,9 +7,8 @@ public class PlayerParticipant { public int RoundId { get; set; } public int PlayerId { get; set; } - - public required Player Player { get; set; } - public required Round Round { get; set; } + public Player Player { get; set; } = null!; + public Round Round { get; set; } = null!; } [PrimaryKey(nameof(MatchId), nameof(TeamId))] @@ -19,7 +18,16 @@ public class TeamParticipant public int TeamId { get; set; } public int Seed { get; set; } public int Score { get; set; } + public Team Team { get; set; } = null!; + public Match Round { get; set; } = null!; +} - public required Team Team { get; set; } - public required Match Round { get; set; } -} \ No newline at end of file +[PrimaryKey(nameof(TournamentId), nameof(TeamId))] +public class TournamentTeam +{ + public int TournamentId { get; set; } + public int TeamId { get; set; } + public int Seed { get; set; } + public Tournament Tournament { get; set; } = null!; + public Team Team { get; set; } = null!; +} diff --git a/TournamentOrganizer/Models/Player.cs b/TournamentOrganizer/Models/Player.cs index 0dfa4e5..46e8b18 100644 --- a/TournamentOrganizer/Models/Player.cs +++ b/TournamentOrganizer/Models/Player.cs @@ -14,6 +14,6 @@ public class Player public string Contact { get; set; } = "discordname"; public int TeamId { get; set; } - public required Team Team { get; set; } + public Team Team { get; set; } = null!; } \ No newline at end of file diff --git a/TournamentOrganizer/Models/Round.cs b/TournamentOrganizer/Models/Round.cs index 801fba7..6601454 100644 --- a/TournamentOrganizer/Models/Round.cs +++ b/TournamentOrganizer/Models/Round.cs @@ -19,7 +19,7 @@ public class Round public int MatchId { get; set; } public RoundState State { get; set; } = RoundState.Waiting; - public required List Players; - public required Match Match { get; set; } + public List Players { get; set; } = []; + public Match Match { get; set; } = null!; } \ No newline at end of file diff --git a/TournamentOrganizer/Models/Team.cs b/TournamentOrganizer/Models/Team.cs index 474c951..b9cd10e 100644 --- a/TournamentOrganizer/Models/Team.cs +++ b/TournamentOrganizer/Models/Team.cs @@ -14,7 +14,8 @@ public class Team public int? LeaderId { get; set; } public Player? Leader { get; set; } - public required List Players { get; set; } - public required List Matches { get; set; } + public List Players { get; set; } = []; + public List Matches { get; set; } = []; + public List TournamentTeams { get; set; } = []; } \ No newline at end of file diff --git a/TournamentOrganizer/Models/Tournament.cs b/TournamentOrganizer/Models/Tournament.cs index 9af875d..6c5f422 100644 --- a/TournamentOrganizer/Models/Tournament.cs +++ b/TournamentOrganizer/Models/Tournament.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -21,8 +22,9 @@ public class Tournament public int? S1GroupAdvances { get; set; } public RuleSet? S2RuleSet { get; set; } - public required Game Game { get; set; } - public required Event Event { get; set; } + public Game Game { get; set; } = null!; + public Event Event { get; set; } = null!; + public List TournamentTeams { get; set; } = []; public static ValidationResult ValidateDates(DateTime date, ValidationContext context) { diff --git a/TournamentOrganizer/ViewModels/MainViewModel.cs b/TournamentOrganizer/ViewModels/MainViewModel.cs index baf8ee9..07f32e1 100644 --- a/TournamentOrganizer/ViewModels/MainViewModel.cs +++ b/TournamentOrganizer/ViewModels/MainViewModel.cs @@ -50,6 +50,15 @@ public partial class MainViewModel : ViewModelBase Title = "Events Management"; await eventsVm.LoadEvents(); } + + [RelayCommand] + private async Task NavigateToTournaments() + { + var tournamentsVm = new TournamentsViewModel(); + CurrentView = tournamentsVm; + Title = "Tournament Management"; + await tournamentsVm.LoadTournaments(); + } } public partial class HomeViewModel : ViewModelBase diff --git a/TournamentOrganizer/ViewModels/TeamsViewModel.cs b/TournamentOrganizer/ViewModels/TeamsViewModel.cs index 7420e6f..4aadf5b 100644 --- a/TournamentOrganizer/ViewModels/TeamsViewModel.cs +++ b/TournamentOrganizer/ViewModels/TeamsViewModel.cs @@ -137,14 +137,14 @@ public partial class TeamsViewModel : ViewModelBase } UpdateFilteredEvents(); - var games = await _context.Games.ToListAsync(); + var tournaments = await _context.Tournaments + .Include(t => t.Game) + .Include(t => t.Event) + .ToListAsync(); AvailableTournaments.Clear(); - foreach (var g in games) + foreach (var t in tournaments) { - foreach (var e in events) - { - AvailableTournaments.Add($"{g.Name} @ {e.Name}"); - } + AvailableTournaments.Add(GetTournamentDisplayName(t)); } UpdateFilteredTournaments(); diff --git a/TournamentOrganizer/ViewModels/TournamentsViewModel.cs b/TournamentOrganizer/ViewModels/TournamentsViewModel.cs new file mode 100644 index 0000000..99b9391 --- /dev/null +++ b/TournamentOrganizer/ViewModels/TournamentsViewModel.cs @@ -0,0 +1,807 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.EntityFrameworkCore; +using TournamentOrganizer.Models; + +namespace TournamentOrganizer.ViewModels; + +public partial class TournamentsViewModel : ViewModelBase +{ + private readonly TournamentContext _context; + + [ObservableProperty] + private ObservableCollection _availableEvents = []; + + [ObservableProperty] + private EventOption? _selectedEvent; + + [ObservableProperty] + private ObservableCollection _availableTournaments = []; + + [ObservableProperty] + private TournamentOption? _selectedTournament; + + [ObservableProperty] + private DateTime _tournamentStart; + + [ObservableProperty] + private DateTime _tournamentEnd; + + [ObservableProperty] + private RuleSetOption? _selectedS1RuleSet; + + [ObservableProperty] + private int? _s1Groups; + + [ObservableProperty] + private int? _s1GroupAdvances; + + [ObservableProperty] + private RuleSetOption? _selectedS2RuleSet; + + [ObservableProperty] + private ObservableCollection _availableTeams = []; + + [ObservableProperty] + private ObservableCollection _participatingTeams = []; + + [ObservableProperty] + private TeamOption? _selectedTeamToAdd; + + [ObservableProperty] + private ParticipatingTeam? _selectedParticipatingTeam; + + [ObservableProperty] + private ObservableCollection _matches = []; + + [ObservableProperty] + private MatchDisplay? _selectedMatch; + + [ObservableProperty] + private string _eventFilterSearch = string.Empty; + + [ObservableProperty] + private string _tournamentFilterSearch = string.Empty; + + [ObservableProperty] + private bool _isEventDropdownOpen; + + [ObservableProperty] + private bool _isTournamentDropdownOpen; + + [ObservableProperty] + private bool _isEditing; + + [ObservableProperty] + private string _statusMessage = string.Empty; + + private readonly ObservableCollection _filteredEvents = []; + public ObservableCollection FilteredEvents => _filteredEvents; + + private readonly ObservableCollection _filteredTournaments = []; + public ObservableCollection FilteredTournaments => _filteredTournaments; + + public ObservableCollection S1RuleSetOptions { get; } = []; + public ObservableCollection S2RuleSetOptions { get; } = [new RuleSetOption(null, "(None)")]; + + public bool IsTournamentFilterEnabled => SelectedEvent != null; + + public TournamentsViewModel() + { + _context = new TournamentContext(); + foreach (var rs in Enum.GetValues()) + { + S1RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName())); + S2RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName())); + } + } + + partial void OnEventFilterSearchChanged(string value) => UpdateFilteredEvents(); + partial void OnSelectedEventChanged(EventOption? value) + { + OnPropertyChanged(nameof(IsTournamentFilterEnabled)); + UpdateAvailableTournaments(); + UpdateFilteredTournaments(); + SelectedTournament = null; + Matches.Clear(); + ParticipatingTeams.Clear(); + IsEditing = false; + } + + partial void OnSelectedTournamentChanged(TournamentOption? value) + { + if (value == null) + { + IsEditing = false; + Matches.Clear(); + ParticipatingTeams.Clear(); + return; + } + + TournamentStart = value.Tournament.Start; + TournamentEnd = value.Tournament.End; + SelectedS1RuleSet = S1RuleSetOptions.FirstOrDefault(o => o.Value == value.Tournament.S1RuleSet); + S1Groups = value.Tournament.S1Groups; + S1GroupAdvances = value.Tournament.S1GroupAdvances; + SelectedS2RuleSet = S2RuleSetOptions.FirstOrDefault(o => o.Value == value.Tournament.S2RuleSet); + IsEditing = true; + + LoadTournamentDetails(); + _ = LoadMatches(); + StatusMessage = $"Editing tournament: {value.Tournament.Game.Name}"; + } + + partial void OnSelectedS1RuleSetChanged(RuleSetOption? value) + { + _ = RegenerateMatches(); + } + + partial void OnSelectedS2RuleSetChanged(RuleSetOption? value) + { + if (value?.Value == null) + { + S1Groups = null; + S1GroupAdvances = null; + } + _ = RegenerateMatches(); + } + + partial void OnTournamentFilterSearchChanged(string value) => UpdateFilteredTournaments(); + + private void UpdateFilteredEvents() + { + _filteredEvents.Clear(); + foreach (var e in AvailableEvents.Where(e => string.IsNullOrWhiteSpace(EventFilterSearch) || e.DisplayName.ToLower().Contains(EventFilterSearch.ToLower()))) + { + _filteredEvents.Add(e); + } + } + + private void UpdateFilteredTournaments() + { + _filteredTournaments.Clear(); + foreach (var t in AvailableTournaments.Where(t => string.IsNullOrWhiteSpace(TournamentFilterSearch) || t.DisplayName.ToLower().Contains(TournamentFilterSearch.ToLower()))) + { + _filteredTournaments.Add(t); + } + } + + public void SelectEventFilter(EventOption option) + { + SelectedEvent = option; + IsEventDropdownOpen = false; + EventFilterSearch = string.Empty; + } + + public void ClearEventFilter() + { + SelectedEvent = null; + EventFilterSearch = string.Empty; + } + + public void SelectTournamentFilter(TournamentOption option) + { + SelectedTournament = option; + IsTournamentDropdownOpen = false; + TournamentFilterSearch = string.Empty; + } + + public void ClearTournamentFilter() + { + SelectedTournament = null; + TournamentFilterSearch = string.Empty; + } + + public async Task LoadTournaments() + { + var events = await _context.Events + .Include(e => e.Tournaments) + .ThenInclude(t => t.Game) + .ToListAsync(); + + AvailableEvents.Clear(); + _filteredEvents.Clear(); + foreach (var e in events) + { + var option = new EventOption(e); + AvailableEvents.Add(option); + _filteredEvents.Add(option); + } + + UpdateAvailableTournaments(); + } + + private void UpdateAvailableTournaments() + { + AvailableTournaments.Clear(); + if (SelectedEvent == null) + { + return; + } + + foreach (var t in SelectedEvent.Event.Tournaments) + { + AvailableTournaments.Add(new TournamentOption(t)); + } + } + + private async Task LoadTournamentDetails() + { + if (SelectedTournament == null) + { + return; + } + + var tournamentTeams = await _context.TournamentTeams + .Include(tt => tt.Team) + .ThenInclude(t => t.Players) + .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) + .ToListAsync(); + + ParticipatingTeams.Clear(); + foreach (var tt in tournamentTeams) + { + ParticipatingTeams.Add(new ParticipatingTeam(tt)); + } + + SelectedParticipatingTeam = null; + + var allTeams = await _context.Teams + .Include(t => t.Players) + .ToListAsync(); + + var participatingIds = ParticipatingTeams.Select(pt => pt.TeamId).ToHashSet(); + AvailableTeams.Clear(); + foreach (var team in allTeams) + { + if (!participatingIds.Contains(team.Id)) + { + AvailableTeams.Add(new TeamOption(team)); + } + } + + SelectedTeamToAdd = null; + } + + private async Task LoadMatches() + { + if (SelectedTournament == null) + { + return; + } + + var matches = await _context.Matches + .Where(m => m.TournamentId == SelectedTournament.Tournament.Id) + .Include(m => m.Teams) + .ThenInclude(tp => tp.Team) + .Include(m => m.Rounds) + .ThenInclude(r => r.Players) + .ThenInclude(pp => pp.Player) + .ToListAsync(); + + Matches.Clear(); + foreach (var match in matches) + { + Matches.Add(new MatchDisplay(match)); + } + } + + [RelayCommand] + private async Task SaveTournament() + { + if (SelectedTournament == null) + { + StatusMessage = "Select a tournament to edit"; + return; + } + + var tournament = await _context.Tournaments.FirstOrDefaultAsync(t => t.Id == SelectedTournament.Tournament.Id); + if (tournament == null) + { + StatusMessage = "Tournament not found"; + return; + } + + tournament.Start = TournamentStart; + tournament.End = TournamentEnd; + tournament.S1RuleSet = SelectedS1RuleSet?.Value ?? tournament.S1RuleSet; + tournament.S1Groups = S1Groups; + tournament.S1GroupAdvances = S1GroupAdvances; + tournament.S2RuleSet = SelectedS2RuleSet?.Value; + + await _context.SaveChangesAsync(); + StatusMessage = "Tournament updated"; + await LoadTournaments(); + + SelectedTournament = AvailableTournaments.FirstOrDefault(t => t.Tournament.Id == tournament.Id); + } + + [RelayCommand] + private async Task AddTeamToTournament() + { + if (SelectedTeamToAdd == null) + { + StatusMessage = "Select a team to add"; + return; + } + + if (SelectedTournament == null) + { + StatusMessage = "Select a tournament first"; + return; + } + + var tournamentTeams = await _context.TournamentTeams + .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) + .ToListAsync(); + + if (tournamentTeams.Any(tt => tt.TeamId == SelectedTeamToAdd.Team.Id)) + { + StatusMessage = "Team is already in this tournament"; + return; + } + + var seed = tournamentTeams.Count + 1; + var tournamentTeam = new TournamentTeam + { + TournamentId = SelectedTournament.Tournament.Id, + TeamId = SelectedTeamToAdd.Team.Id, + Seed = seed, + Tournament = SelectedTournament.Tournament, + Team = SelectedTeamToAdd.Team + }; + + _context.TournamentTeams.Add(tournamentTeam); + await _context.SaveChangesAsync(); + StatusMessage = $"Added '{SelectedTeamToAdd.Team.Name}' to tournament"; + + await LoadTournamentDetails(); + await RegenerateMatches(); + } + + [RelayCommand] + private async Task RemoveTeamFromTournament() + { + if (SelectedParticipatingTeam == null) + { + StatusMessage = "Select a team to remove"; + return; + } + + if (SelectedTournament == null) + { + StatusMessage = "Select a tournament first"; + return; + } + + var tournamentTeam = await _context.TournamentTeams + .FirstOrDefaultAsync(tt => tt.TournamentId == SelectedTournament.Tournament.Id && tt.TeamId == SelectedParticipatingTeam.TeamId); + + if (tournamentTeam != null) + { + _context.TournamentTeams.Remove(tournamentTeam); + await _context.SaveChangesAsync(); + StatusMessage = $"Removed '{SelectedParticipatingTeam.Name}' from tournament"; + } + + await LoadTournamentDetails(); + await RegenerateMatches(); + } + + [RelayCommand] + private async Task ReseedTeams() + { + if (SelectedTournament == null) + { + StatusMessage = "Select a tournament first"; + return; + } + + var tournamentTeams = await _context.TournamentTeams + .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) + .ToListAsync(); + + if (tournamentTeams.Count < 2) + { + StatusMessage = "Need at least 2 teams to reseed"; + return; + } + + var rng = new Random(); + var shuffled = tournamentTeams.OrderBy(_ => rng.Next()).ToList(); + + for (int i = 0; i < shuffled.Count; i++) + { + shuffled[i].Seed = i + 1; + } + + await _context.SaveChangesAsync(); + StatusMessage = "Teams reseeded"; + await LoadTournamentDetails(); + await RegenerateMatches(); + } + + private async Task RegenerateMatches() + { + if (SelectedTournament == null) + { + return; + } + + var tournamentTeams = await _context.TournamentTeams + .Include(tt => tt.Team) + .Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id) + .OrderBy(tt => tt.Seed) + .ToListAsync(); + + var existingMatches = await _context.Matches + .Where(m => m.TournamentId == SelectedTournament.Tournament.Id) + .ToListAsync(); + + if (existingMatches.Count > 0) + { + _context.Matches.RemoveRange(existingMatches); + await _context.SaveChangesAsync(); + } + + if (tournamentTeams.Count < 2) + { + Matches.Clear(); + return; + } + + var tournament = await _context.Tournaments.FindAsync(SelectedTournament.Tournament.Id); + if (tournament == null) + { + return; + } + + var participants = tournamentTeams.Select(tt => new TeamParticipant + { + TeamId = tt.TeamId, + Team = tt.Team, + Seed = tt.Seed, + Score = 0, + MatchId = 0, + Round = null! + }).ToList(); + + var ruleSet = tournament.S2RuleSet ?? tournament.S1RuleSet; + + switch (ruleSet) + { + case RuleSet.SingleElimination: + await GenerateSingleElimination(tournament, participants); + break; + case RuleSet.DoubleElimination: + await GenerateDoubleElimination(tournament, participants); + break; + case RuleSet.RoundRobin: + await GenerateRoundRobin(tournament, participants); + break; + case RuleSet.Swiss: + await GenerateSwiss(tournament, participants); + break; + } + + await LoadMatches(); + } + + private async Task GenerateSingleElimination(Tournament tournament, List teams) + { + int numTeams = teams.Count; + int nextPowerOf2 = 1; + while (nextPowerOf2 < numTeams) + { + nextPowerOf2 *= 2; + } + + int totalRounds = (int)Math.Log2(nextPowerOf2); + var allMatches = new List(); + + for (int round = 0; round < totalRounds; round++) + { + int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, round + 1); + + for (int i = 0; i < matchesInRound; i++) + { + var match = CreateMatch(tournament); + + if (round == 0) + { + int highSeed = i + 1; + int lowSeed = nextPowerOf2 - i; + + if (highSeed <= numTeams) + AddTeamToMatch(match, teams[highSeed - 1], highSeed); + if (lowSeed <= numTeams) + AddTeamToMatch(match, teams[lowSeed - 1], lowSeed); + } + + allMatches.Add(match); + } + } + + _context.Matches.AddRange(allMatches); + await _context.SaveChangesAsync(); + } + + private async Task GenerateDoubleElimination(Tournament tournament, List teams) + { + int numTeams = teams.Count; + int nextPowerOf2 = 1; + while (nextPowerOf2 < numTeams) + { + nextPowerOf2 *= 2; + } + + int wbRounds = (int)Math.Log2(nextPowerOf2); + var allMatches = new List(); + + var wbMatchesByRound = new List>(); + for (int r = 0; r < wbRounds; r++) + { + int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, r + 1); + var roundMatches = new List(); + for (int i = 0; i < matchesInRound; i++) + { + roundMatches.Add(CreateMatch(tournament)); + } + allMatches.AddRange(roundMatches); + wbMatchesByRound.Add(roundMatches); + } + + if (numTeams > 2) + { + var lbMatchesByRound = new List>(); + for (int r = 0; r < wbRounds - 1; r++) + { + int matchesInRound = nextPowerOf2 / 4; + var roundMatches = new List(); + for (int i = 0; i < matchesInRound; i++) + { + roundMatches.Add(CreateMatch(tournament)); + } + allMatches.AddRange(roundMatches); + lbMatchesByRound.Add(roundMatches); + } + + var lbFinal = CreateMatch(tournament); + allMatches.Add(lbFinal); + + var grandFinals = CreateMatch(tournament); + allMatches.Add(grandFinals); + } + + var firstRoundMatches = wbMatchesByRound[0]; + for (int i = 0; i < nextPowerOf2 / 2; i++) + { + int highSeed = i + 1; + int lowSeed = nextPowerOf2 - i; + var match = firstRoundMatches[i]; + + if (highSeed <= numTeams) + AddTeamToMatch(match, teams[highSeed - 1], highSeed); + if (lowSeed <= numTeams) + AddTeamToMatch(match, teams[lowSeed - 1], lowSeed); + } + + _context.Matches.AddRange(allMatches); + await _context.SaveChangesAsync(); + } + + private static Match CreateMatch(Tournament tournament) + { + var match = new Match + { + TournamentId = tournament.Id, + Tournament = tournament, + Teams = [], + Rounds = [] + }; + var r = new Round + { + MatchId = match.Id, + Match = match, + Players = [] + }; + match.Rounds.Add(r); + return match; + } + + private static void AddTeamToMatch(Match match, TeamParticipant team, int seed) + { + match.Teams.Add(new TeamParticipant + { + TeamId = team.TeamId, + Seed = seed, + Score = 0, + Team = team.Team, + MatchId = 0, + Round = match + }); + } + + private async Task GenerateRoundRobin(Tournament tournament, List teams) + { + var matches = new List(); + + for (int i = 0; i < teams.Count; i++) + { + for (int j = i + 1; j < teams.Count; j++) + { + var match = new Match + { + TournamentId = tournament.Id, + Tournament = tournament, + Teams = + [ + new TeamParticipant { TeamId = teams[i].TeamId, Seed = teams[i].Seed, Score = 0, Team = teams[i].Team, MatchId = 0, Round = null! }, + new TeamParticipant { TeamId = teams[j].TeamId, Seed = teams[j].Seed, Score = 0, Team = teams[j].Team, MatchId = 0, Round = null! } + ], + Rounds = [] + }; + + var round = new Round + { + MatchId = match.Id, + Match = match, + Players = [] + }; + match.Rounds.Add(round); + + matches.Add(match); + } + } + + _context.Matches.AddRange(matches); + await _context.SaveChangesAsync(); + + foreach (var match in matches) + { + foreach (var tp in match.Teams) + { + tp.Round = match; + } + } + + await _context.SaveChangesAsync(); + } + + private async Task GenerateSwiss(Tournament tournament, List teams) + { + var matches = new List(); + + int numRounds = (int)Math.Ceiling(Math.Log2(teams.Count)); + var shuffledTeams = teams.OrderBy(_ => Guid.NewGuid()).ToList(); + + for (int round = 0; round < numRounds; round++) + { + for (int i = 0; i < shuffledTeams.Count / 2; i++) + { + var team1 = shuffledTeams[i * 2]; + var team2 = shuffledTeams[i * 2 + 1]; + + var match = new Match + { + TournamentId = tournament.Id, + Tournament = tournament, + Teams = + [ + new TeamParticipant { TeamId = team1.TeamId, Seed = team1.Seed, Score = 0, Team = team1.Team, MatchId = 0, Round = null! }, + new TeamParticipant { TeamId = team2.TeamId, Seed = team2.Seed, Score = 0, Team = team2.Team, MatchId = 0, Round = null! } + ], + Rounds = [] + }; + + var r = new Round + { + MatchId = match.Id, + Match = match, + Players = [] + }; + match.Rounds.Add(r); + + matches.Add(match); + } + } + + _context.Matches.AddRange(matches); + await _context.SaveChangesAsync(); + } +} + +public class EventOption +{ + public Event Event { get; set; } = null!; + public string DisplayName { get; set; } = string.Empty; + + public EventOption() { } + + public EventOption(Event evt) + { + Event = evt; + DisplayName = evt.Name; + } + + public override string ToString() => DisplayName; +} + +public class TournamentOption +{ + public Tournament Tournament { get; set; } = null!; + public string DisplayName { get; set; } = string.Empty; + + public TournamentOption() { } + + public TournamentOption(Tournament tournament) + { + Tournament = tournament; + DisplayName = $"{tournament.Game.Name}"; + } + + public override string ToString() => DisplayName; +} + +public class TeamOption +{ + public Team Team { get; set; } = null!; + public string DisplayName { get; set; } = string.Empty; + + public TeamOption() { } + + public TeamOption(Team team) + { + Team = team; + DisplayName = team.Name; + } + + public override string ToString() => DisplayName; +} + +public class ParticipatingTeam +{ + public int TeamId { get; set; } + public string Name { get; set; } = string.Empty; + public int Seed { get; set; } + public int Score { get; set; } + + public ParticipatingTeam() { } + + public ParticipatingTeam(TournamentTeam tt) + { + TeamId = tt.TeamId; + Name = tt.Team.Name; + Seed = tt.Seed; + Score = 0; + } +} + +public class MatchDisplay +{ + public int Id { get; set; } + public string TeamsText { get; set; } = string.Empty; + public RoundState State { get; set; } + + public MatchDisplay() { } + + public MatchDisplay(Match match) + { + Id = match.Id; + if (match.Teams != null && match.Teams.Count >= 2) + { + TeamsText = $"{match.Teams[0].Team.Name} vs {match.Teams[1].Team.Name}"; + } + else if (match.Teams != null && match.Teams.Count == 1) + { + TeamsText = $"{match.Teams[0].Team.Name} vs TBD"; + } + + State = match.Rounds?.LastOrDefault()?.State ?? RoundState.Waiting; + } +} diff --git a/TournamentOrganizer/Views/MainView.axaml b/TournamentOrganizer/Views/MainView.axaml index 958b2f0..42ce3ab 100644 --- a/TournamentOrganizer/Views/MainView.axaml +++ b/TournamentOrganizer/Views/MainView.axaml @@ -17,6 +17,7 @@