From 5a51a8265deedc45edd66b11a187de88008f8f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20Gol=C3=A1=C5=88=20jr?= Date: Wed, 6 May 2026 16:21:50 +0200 Subject: [PATCH] Match management view --- .../Database/TournamentContext.cs | 12 + ...2131_AddMatchBracketNavigation.Designer.cs | 434 +++++++++++++++ ...0260506122131_AddMatchBracketNavigation.cs | 102 ++++ ...6131153_AddTeamParticipantSlot.Designer.cs | 437 +++++++++++++++ .../20260506131153_AddTeamParticipantSlot.cs | 29 + ...6134330_RemoveUnnecessarySlots.Designer.cs | 428 ++++++++++++++ .../20260506134330_RemoveUnnecessarySlots.cs | 51 ++ .../TournamentContextModelSnapshot.cs | 42 +- TournamentOrganizer/Models/Match.cs | 5 + .../ViewModels/MainViewModel.cs | 9 + .../ViewModels/MatchesViewModel.cs | 526 ++++++++++++++++++ .../ViewModels/TournamentsViewModel.cs | 221 +++++++- TournamentOrganizer/Views/MainView.axaml | 1 + TournamentOrganizer/Views/MatchesView.axaml | 211 +++++++ .../Views/MatchesView.axaml.cs | 53 ++ 15 files changed, 2554 insertions(+), 7 deletions(-) create mode 100644 TournamentOrganizer/Migrations/20260506122131_AddMatchBracketNavigation.Designer.cs create mode 100644 TournamentOrganizer/Migrations/20260506122131_AddMatchBracketNavigation.cs create mode 100644 TournamentOrganizer/Migrations/20260506131153_AddTeamParticipantSlot.Designer.cs create mode 100644 TournamentOrganizer/Migrations/20260506131153_AddTeamParticipantSlot.cs create mode 100644 TournamentOrganizer/Migrations/20260506134330_RemoveUnnecessarySlots.Designer.cs create mode 100644 TournamentOrganizer/Migrations/20260506134330_RemoveUnnecessarySlots.cs create mode 100644 TournamentOrganizer/ViewModels/MatchesViewModel.cs create mode 100644 TournamentOrganizer/Views/MatchesView.axaml create mode 100644 TournamentOrganizer/Views/MatchesView.axaml.cs diff --git a/TournamentOrganizer/Database/TournamentContext.cs b/TournamentOrganizer/Database/TournamentContext.cs index c3eea3e..11be6b5 100644 --- a/TournamentOrganizer/Database/TournamentContext.cs +++ b/TournamentOrganizer/Database/TournamentContext.cs @@ -54,6 +54,18 @@ public class TournamentContext : DbContext .WithMany(t => t.TournamentTeams) .HasForeignKey(tt => tt.TeamId) .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(m => m.WinnerMatch) + .WithMany() + .HasForeignKey(m => m.WinnerMatchId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(m => m.LoserMatch) + .WithMany() + .HasForeignKey(m => m.LoserMatchId) + .OnDelete(DeleteBehavior.Restrict); } } diff --git a/TournamentOrganizer/Migrations/20260506122131_AddMatchBracketNavigation.Designer.cs b/TournamentOrganizer/Migrations/20260506122131_AddMatchBracketNavigation.Designer.cs new file mode 100644 index 0000000..ac900e1 --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506122131_AddMatchBracketNavigation.Designer.cs @@ -0,0 +1,434 @@ +// +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("20260506122131_AddMatchBracketNavigation")] + partial class AddMatchBracketNavigation + { + /// + 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("LoserMatchId") + .HasColumnType("INTEGER"); + + b.Property("LoserSlot") + .HasColumnType("INTEGER"); + + b.Property("TournamentId") + .HasColumnType("INTEGER"); + + b.Property("WinnerMatchId") + .HasColumnType("INTEGER"); + + b.Property("WinnerSlot") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LoserMatchId"); + + b.HasIndex("TournamentId"); + + b.HasIndex("WinnerMatchId"); + + 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.Match", "LoserMatch") + .WithMany() + .HasForeignKey("LoserMatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") + .WithMany() + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Match", "WinnerMatch") + .WithMany() + .HasForeignKey("WinnerMatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("LoserMatch"); + + b.Navigation("Tournament"); + + b.Navigation("WinnerMatch"); + }); + + 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("Players") + .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("Rounds") + .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("Teams") + .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.Match", b => + { + b.Navigation("Rounds"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Round", b => + { + b.Navigation("Players"); + }); + + 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/20260506122131_AddMatchBracketNavigation.cs b/TournamentOrganizer/Migrations/20260506122131_AddMatchBracketNavigation.cs new file mode 100644 index 0000000..bc28f44 --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506122131_AddMatchBracketNavigation.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TournamentOrganizer.Migrations +{ + /// + public partial class AddMatchBracketNavigation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LoserMatchId", + table: "Matches", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "LoserSlot", + table: "Matches", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WinnerMatchId", + table: "Matches", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "WinnerSlot", + table: "Matches", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_Matches_LoserMatchId", + table: "Matches", + column: "LoserMatchId"); + + migrationBuilder.CreateIndex( + name: "IX_Matches_WinnerMatchId", + table: "Matches", + column: "WinnerMatchId"); + + migrationBuilder.AddForeignKey( + name: "FK_Matches_Matches_LoserMatchId", + table: "Matches", + column: "LoserMatchId", + principalTable: "Matches", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Matches_Matches_WinnerMatchId", + table: "Matches", + column: "WinnerMatchId", + principalTable: "Matches", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Matches_Matches_LoserMatchId", + table: "Matches"); + + migrationBuilder.DropForeignKey( + name: "FK_Matches_Matches_WinnerMatchId", + table: "Matches"); + + migrationBuilder.DropIndex( + name: "IX_Matches_LoserMatchId", + table: "Matches"); + + migrationBuilder.DropIndex( + name: "IX_Matches_WinnerMatchId", + table: "Matches"); + + migrationBuilder.DropColumn( + name: "LoserMatchId", + table: "Matches"); + + migrationBuilder.DropColumn( + name: "LoserSlot", + table: "Matches"); + + migrationBuilder.DropColumn( + name: "WinnerMatchId", + table: "Matches"); + + migrationBuilder.DropColumn( + name: "WinnerSlot", + table: "Matches"); + } + } +} diff --git a/TournamentOrganizer/Migrations/20260506131153_AddTeamParticipantSlot.Designer.cs b/TournamentOrganizer/Migrations/20260506131153_AddTeamParticipantSlot.Designer.cs new file mode 100644 index 0000000..a69e151 --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506131153_AddTeamParticipantSlot.Designer.cs @@ -0,0 +1,437 @@ +// +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("20260506131153_AddTeamParticipantSlot")] + partial class AddTeamParticipantSlot + { + /// + 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("LoserMatchId") + .HasColumnType("INTEGER"); + + b.Property("LoserSlot") + .HasColumnType("INTEGER"); + + b.Property("TournamentId") + .HasColumnType("INTEGER"); + + b.Property("WinnerMatchId") + .HasColumnType("INTEGER"); + + b.Property("WinnerSlot") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LoserMatchId"); + + b.HasIndex("TournamentId"); + + b.HasIndex("WinnerMatchId"); + + 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.Property("Slot") + .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.Match", "LoserMatch") + .WithMany() + .HasForeignKey("LoserMatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") + .WithMany() + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Match", "WinnerMatch") + .WithMany() + .HasForeignKey("WinnerMatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("LoserMatch"); + + b.Navigation("Tournament"); + + b.Navigation("WinnerMatch"); + }); + + 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("Players") + .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("Rounds") + .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("Teams") + .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.Match", b => + { + b.Navigation("Rounds"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Round", b => + { + b.Navigation("Players"); + }); + + 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/20260506131153_AddTeamParticipantSlot.cs b/TournamentOrganizer/Migrations/20260506131153_AddTeamParticipantSlot.cs new file mode 100644 index 0000000..1545398 --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506131153_AddTeamParticipantSlot.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TournamentOrganizer.Migrations +{ + /// + public partial class AddTeamParticipantSlot : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Slot", + table: "TeamParticipants", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Slot", + table: "TeamParticipants"); + } + } +} diff --git a/TournamentOrganizer/Migrations/20260506134330_RemoveUnnecessarySlots.Designer.cs b/TournamentOrganizer/Migrations/20260506134330_RemoveUnnecessarySlots.Designer.cs new file mode 100644 index 0000000..5ed0c99 --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506134330_RemoveUnnecessarySlots.Designer.cs @@ -0,0 +1,428 @@ +// +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("20260506134330_RemoveUnnecessarySlots")] + partial class RemoveUnnecessarySlots + { + /// + 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("LoserMatchId") + .HasColumnType("INTEGER"); + + b.Property("TournamentId") + .HasColumnType("INTEGER"); + + b.Property("WinnerMatchId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LoserMatchId"); + + b.HasIndex("TournamentId"); + + b.HasIndex("WinnerMatchId"); + + 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.Match", "LoserMatch") + .WithMany() + .HasForeignKey("LoserMatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") + .WithMany() + .HasForeignKey("TournamentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TournamentOrganizer.Models.Match", "WinnerMatch") + .WithMany() + .HasForeignKey("WinnerMatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("LoserMatch"); + + b.Navigation("Tournament"); + + b.Navigation("WinnerMatch"); + }); + + 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("Players") + .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("Rounds") + .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("Teams") + .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.Match", b => + { + b.Navigation("Rounds"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Round", b => + { + b.Navigation("Players"); + }); + + 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/20260506134330_RemoveUnnecessarySlots.cs b/TournamentOrganizer/Migrations/20260506134330_RemoveUnnecessarySlots.cs new file mode 100644 index 0000000..7da43c3 --- /dev/null +++ b/TournamentOrganizer/Migrations/20260506134330_RemoveUnnecessarySlots.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TournamentOrganizer.Migrations +{ + /// + public partial class RemoveUnnecessarySlots : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Slot", + table: "TeamParticipants"); + + migrationBuilder.DropColumn( + name: "LoserSlot", + table: "Matches"); + + migrationBuilder.DropColumn( + name: "WinnerSlot", + table: "Matches"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Slot", + table: "TeamParticipants", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "LoserSlot", + table: "Matches", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "WinnerSlot", + table: "Matches", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs b/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs index 191f5ac..93222c7 100644 --- a/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs +++ b/TournamentOrganizer/Migrations/TournamentContextModelSnapshot.cs @@ -75,13 +75,23 @@ namespace TournamentOrganizer.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("LoserMatchId") + .HasColumnType("INTEGER"); + b.Property("TournamentId") .HasColumnType("INTEGER"); + b.Property("WinnerMatchId") + .HasColumnType("INTEGER"); + b.HasKey("Id"); + b.HasIndex("LoserMatchId"); + b.HasIndex("TournamentId"); + b.HasIndex("WinnerMatchId"); + b.ToTable("Matches"); }); @@ -243,13 +253,27 @@ namespace TournamentOrganizer.Migrations modelBuilder.Entity("TournamentOrganizer.Models.Match", b => { + b.HasOne("TournamentOrganizer.Models.Match", "LoserMatch") + .WithMany() + .HasForeignKey("LoserMatchId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("TournamentOrganizer.Models.Tournament", "Tournament") .WithMany() .HasForeignKey("TournamentId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("TournamentOrganizer.Models.Match", "WinnerMatch") + .WithMany() + .HasForeignKey("WinnerMatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("LoserMatch"); + b.Navigation("Tournament"); + + b.Navigation("WinnerMatch"); }); modelBuilder.Entity("TournamentOrganizer.Models.Player", b => @@ -272,7 +296,7 @@ namespace TournamentOrganizer.Migrations .IsRequired(); b.HasOne("TournamentOrganizer.Models.Round", "Round") - .WithMany() + .WithMany("Players") .HasForeignKey("RoundId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -285,7 +309,7 @@ namespace TournamentOrganizer.Migrations modelBuilder.Entity("TournamentOrganizer.Models.Round", b => { b.HasOne("TournamentOrganizer.Models.Match", "Match") - .WithMany() + .WithMany("Rounds") .HasForeignKey("MatchId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -306,7 +330,7 @@ namespace TournamentOrganizer.Migrations modelBuilder.Entity("TournamentOrganizer.Models.TeamParticipant", b => { b.HasOne("TournamentOrganizer.Models.Match", "Round") - .WithMany() + .WithMany("Teams") .HasForeignKey("MatchId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -370,6 +394,18 @@ namespace TournamentOrganizer.Migrations b.Navigation("Tournaments"); }); + modelBuilder.Entity("TournamentOrganizer.Models.Match", b => + { + b.Navigation("Rounds"); + + b.Navigation("Teams"); + }); + + modelBuilder.Entity("TournamentOrganizer.Models.Round", b => + { + b.Navigation("Players"); + }); + modelBuilder.Entity("TournamentOrganizer.Models.Team", b => { b.Navigation("Matches"); diff --git a/TournamentOrganizer/Models/Match.cs b/TournamentOrganizer/Models/Match.cs index b412cb8..b4b6631 100644 --- a/TournamentOrganizer/Models/Match.cs +++ b/TournamentOrganizer/Models/Match.cs @@ -11,8 +11,13 @@ public class Match public int Id { get; set; } public int TournamentId { get; set; } + public int? WinnerMatchId { get; set; } + public int? LoserMatchId { get; set; } + public List Teams { get; set; } = []; public List Rounds { get; set; } = []; public Tournament Tournament { get; set; } = null!; + public Match? WinnerMatch { get; set; } + public Match? LoserMatch { get; set; } } \ No newline at end of file diff --git a/TournamentOrganizer/ViewModels/MainViewModel.cs b/TournamentOrganizer/ViewModels/MainViewModel.cs index 07f32e1..5cfdf03 100644 --- a/TournamentOrganizer/ViewModels/MainViewModel.cs +++ b/TournamentOrganizer/ViewModels/MainViewModel.cs @@ -59,6 +59,15 @@ public partial class MainViewModel : ViewModelBase Title = "Tournament Management"; await tournamentsVm.LoadTournaments(); } + + [RelayCommand] + private async Task NavigateToMatches() + { + var matchesVm = new MatchesViewModel(); + CurrentView = matchesVm; + Title = "Match Management"; + await matchesVm.LoadEvents(); + } } public partial class HomeViewModel : ViewModelBase diff --git a/TournamentOrganizer/ViewModels/MatchesViewModel.cs b/TournamentOrganizer/ViewModels/MatchesViewModel.cs new file mode 100644 index 0000000..a78f40d --- /dev/null +++ b/TournamentOrganizer/ViewModels/MatchesViewModel.cs @@ -0,0 +1,526 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.EntityFrameworkCore; +using TournamentOrganizer; +using TournamentOrganizer.Models; + +namespace TournamentOrganizer.ViewModels; + +public partial class MatchesViewModel : ViewModelBase +{ + private readonly TournamentContext _context; + + [ObservableProperty] + private ObservableCollection _availableEvents = []; + + [ObservableProperty] + private EventOption? _selectedEvent; + + [ObservableProperty] + private ObservableCollection _availableTournaments = []; + + [ObservableProperty] + private TournamentOption? _selectedTournament; + + [ObservableProperty] + private ObservableCollection _matches = []; + + [ObservableProperty] + private MatchDisplay? _selectedMatch; + + [ObservableProperty] + private ObservableCollection _matchTeams = []; + + [ObservableProperty] + private ObservableCollection _rounds = []; + + [ObservableProperty] + private RoundDisplay? _selectedRound; + + [ObservableProperty] + private string _eventFilterSearch = string.Empty; + + [ObservableProperty] + private string _tournamentFilterSearch = string.Empty; + + [ObservableProperty] + private bool _isEventDropdownOpen; + + [ObservableProperty] + private bool _isTournamentDropdownOpen; + + [ObservableProperty] + private string _statusMessage = string.Empty; + + private readonly ObservableCollection _filteredEvents = []; + public ObservableCollection FilteredEvents => _filteredEvents; + + private readonly ObservableCollection _filteredTournaments = []; + public ObservableCollection FilteredTournaments => _filteredTournaments; + + public bool IsTournamentFilterEnabled => SelectedEvent != null; + + public bool CanCreateRound => Rounds.Count == 0 || Rounds.All(r => r.State == RoundState.Finished); + + public bool CanStartRound => SelectedRound != null && SelectedRound.State == RoundState.Waiting; + + public bool CanAwardVictory => SelectedRound != null && SelectedRound.State == RoundState.Started; + + public MatchesViewModel() + { + _context = new TournamentContext(); + } + + public async Task LoadEvents() + { + 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(); + UpdateFilteredTournaments(); + } + + private void UpdateAvailableTournaments() + { + AvailableTournaments.Clear(); + if (SelectedEvent == null) + { + return; + } + + foreach (var t in SelectedEvent.Event.Tournaments) + { + AvailableTournaments.Add(new TournamentOption(t)); + } + } + + private async Task LoadMatches() + { + if (SelectedTournament == null) + { + Matches.Clear(); + return; + } + + int? selectedMatchId = SelectedMatch?.Id; + + var matches = await _context.Matches + .Where(m => m.TournamentId == SelectedTournament.Tournament.Id) + .Include(m => m.Teams) + .ThenInclude(tp => tp.Team) + .Include(m => m.Rounds) + .ToListAsync(); + + Matches.Clear(); + MatchDisplay? newSelectedMatch = null; + foreach (var match in matches) + { + var matchDisplay = new MatchDisplay(match); + Matches.Add(matchDisplay); + if (match.Id == selectedMatchId) + { + newSelectedMatch = matchDisplay; + } + } + + if (newSelectedMatch != null) + { + SelectedMatch = newSelectedMatch; + } + } + + private async Task LoadMatchDetails() + { + if (SelectedMatch == null) + { + MatchTeams.Clear(); + Rounds.Clear(); + _lastLoadedMatchId = null; + return; + } + + _lastLoadedMatchId = SelectedMatch.Id; + + var match = await _context.Matches + .Where(m => m.Id == SelectedMatch.Id) + .Include(m => m.Teams) + .ThenInclude(tp => tp.Team) + .Include(m => m.Rounds) + .FirstOrDefaultAsync(); + + if (match == null) + { + MatchTeams.Clear(); + Rounds.Clear(); + return; + } + + MatchTeams.Clear(); + foreach (var team in match.Teams) + { + MatchTeams.Add(new TeamParticipantDisplay(team)); + } + + Rounds.Clear(); + foreach (var round in match.Rounds) + { + Rounds.Add(new RoundDisplay(round)); + } + + OnPropertyChanged(nameof(CanCreateRound)); + OnPropertyChanged(nameof(CanStartRound)); + OnPropertyChanged(nameof(CanAwardVictory)); + } + + partial void OnSelectedEventChanged(EventOption? value) + { + OnPropertyChanged(nameof(IsTournamentFilterEnabled)); + UpdateAvailableTournaments(); + UpdateFilteredTournaments(); + SelectedTournament = null; + Matches.Clear(); + MatchTeams.Clear(); + Rounds.Clear(); + } + + partial void OnSelectedTournamentChanged(TournamentOption? value) + { + _ = LoadMatches(); + MatchTeams.Clear(); + Rounds.Clear(); + } + + private int? _lastLoadedMatchId; + + partial void OnSelectedMatchChanged(MatchDisplay? value) + { + if (value?.Id == _lastLoadedMatchId) + return; + _ = LoadMatchDetails(); + } + + partial void OnSelectedRoundChanged(RoundDisplay? value) + { + OnPropertyChanged(nameof(CanStartRound)); + OnPropertyChanged(nameof(CanAwardVictory)); + } + + partial void OnEventFilterSearchChanged(string value) => UpdateFilteredEvents(); + + 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(); + if (SelectedEvent == null) + { + return; + } + + 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; + } + + [RelayCommand] + private async Task CreateRound() + { + if (SelectedMatch == null) + { + StatusMessage = "Select a match first"; + return; + } + + var match = await _context.Matches + .Where(m => m.Id == SelectedMatch.Id) + .Include(m => m.Rounds) + .FirstOrDefaultAsync(); + + if (match == null) + { + StatusMessage = "Match not found"; + return; + } + + if (match.Rounds.Any(r => r.State != RoundState.Finished)) + { + StatusMessage = "All rounds must be finished before creating a new round"; + return; + } + + var round = new Round + { + MatchId = match.Id, + Match = match, + State = RoundState.Waiting, + Players = [] + }; + + match.Rounds.Add(round); + await _context.SaveChangesAsync(); + + StatusMessage = "Round created"; + await LoadMatches(); + await LoadMatchDetails(); + } + + [RelayCommand] + private async Task StartRound() + { + if (SelectedRound == null) + { + StatusMessage = "Select a round to start"; + return; + } + + var round = await _context.Rounds.FindAsync(SelectedRound.Id); + if (round == null) + { + StatusMessage = "Round not found"; + return; + } + + if (round.State != RoundState.Waiting) + { + StatusMessage = "Round must be in Waiting state to start"; + return; + } + + round.State = RoundState.Started; + await _context.SaveChangesAsync(); + + StatusMessage = "Round started"; + await LoadMatches(); + await LoadMatchDetails(); + } + + [RelayCommand] + private async Task AwardVictory(object? parameter) + { + if (SelectedRound == null) + { + StatusMessage = "Select a round first"; + return; + } + + if (SelectedMatch == null) + { + StatusMessage = "No match selected"; + return; + } + + if (SelectedRound.State != RoundState.Started) + { + StatusMessage = "Round must be in Started state to award victory"; + return; + } + + if (!int.TryParse(parameter?.ToString(), out int teamIndex)) + { + StatusMessage = "Invalid team index"; + return; + } + + var round = await _context.Rounds.FindAsync(SelectedRound.Id); + if (round == null) + { + StatusMessage = "Round not found"; + return; + } + + round.State = RoundState.Finished; + + var match = await _context.Matches + .Where(m => m.Id == SelectedMatch.Id) + .Include(m => m.Teams) + .FirstOrDefaultAsync(); + + if (match != null && teamIndex >= 0 && teamIndex < match.Teams.Count) + { + match.Teams[teamIndex].Score++; + } + + await _context.SaveChangesAsync(); + + // Promote winner and loser to next matches + await PromoteTeams(match); + + await _context.SaveChangesAsync(); + + var teamName = teamIndex < MatchTeams.Count ? MatchTeams[teamIndex].TeamName : "Unknown"; + StatusMessage = $"Victory awarded to {teamName}"; + await LoadMatches(); + await LoadMatchDetails(); + } + + private async Task PromoteTeams(Match? match) + { + if (match == null || match.Teams.Count < 2) + return; + + // Determine winner (higher score) and loser + var sortedTeams = match.Teams.OrderByDescending(t => t.Score).ToList(); + var winner = sortedTeams[0]; + var loser = sortedTeams[1]; + + // Promote winner to WinnerMatch + if (match.WinnerMatchId.HasValue) + { + var winnerMatch = await _context.Matches.FindAsync(match.WinnerMatchId.Value); + if (winnerMatch != null && !winnerMatch.Teams.Any(t => t.TeamId == winner.TeamId)) + { + winnerMatch.Teams.Add(new TeamParticipant + { + TeamId = winner.TeamId, + Seed = winner.Seed, + Score = 0, + Team = winner.Team, + MatchId = winnerMatch.Id, + Round = null! + }); + + // Check if winnerMatch now has a bye (only 1 team) and needs recursive promotion + if (winnerMatch.Teams.Count == 1 && winnerMatch.WinnerMatchId.HasValue) + { + await PromoteRecursive(winnerMatch); + } + } + } + + // Promote loser to LoserMatch (for double elimination) + // Only if this match has a LoserMatchId (meaning it's a winner bracket match) + if (match.LoserMatchId.HasValue) + { + var loserMatch = await _context.Matches.FindAsync(match.LoserMatchId.Value); + if (loserMatch != null && !loserMatch.Teams.Any(t => t.TeamId == loser.TeamId)) + { + loserMatch.Teams.Add(new TeamParticipant + { + TeamId = loser.TeamId, + Seed = loser.Seed, + Score = 0, + Team = loser.Team, + MatchId = loserMatch.Id, + Round = null! + }); + + // Check if loserMatch now has a bye and needs recursive promotion + if (loserMatch.Teams.Count == 1 && loserMatch.WinnerMatchId.HasValue) + { + await PromoteRecursive(loserMatch); + } + } + } + } + + private async Task PromoteRecursive(Match match) + { + if (match.Teams.Count != 1 || !match.WinnerMatchId.HasValue) + return; + + var team = match.Teams.First(); + var nextMatch = await _context.Matches.FindAsync(match.WinnerMatchId.Value); + + if (nextMatch != null && !nextMatch.Teams.Any(t => t.TeamId == team.TeamId)) + { + nextMatch.Teams.Add(new TeamParticipant + { + TeamId = team.TeamId, + Seed = team.Seed, + Score = 0, + Team = team.Team, + MatchId = nextMatch.Id, + Round = null! + }); + + // Continue recursive promotion if needed + if (nextMatch.Teams.Count == 1 && nextMatch.WinnerMatchId.HasValue) + { + await PromoteRecursive(nextMatch); + } + } + } +} + +public class TeamParticipantDisplay +{ + public int TeamId { get; set; } + public string TeamName { get; set; } = string.Empty; + public int Score { get; set; } + public int Seed { get; set; } + + public TeamParticipantDisplay() { } + + public TeamParticipantDisplay(TeamParticipant tp) + { + TeamId = tp.TeamId; + TeamName = tp.Team.Name; + Score = tp.Score; + Seed = tp.Seed; + } +} + +public class RoundDisplay +{ + public int Id { get; set; } + public RoundState State { get; set; } + + public RoundDisplay() { } + + public RoundDisplay(Round round) + { + Id = round.Id; + State = round.State; + } +} diff --git a/TournamentOrganizer/ViewModels/TournamentsViewModel.cs b/TournamentOrganizer/ViewModels/TournamentsViewModel.cs index 99b9391..c3c6b00 100644 --- a/TournamentOrganizer/ViewModels/TournamentsViewModel.cs +++ b/TournamentOrganizer/ViewModels/TournamentsViewModel.cs @@ -489,9 +489,111 @@ public partial class TournamentsViewModel : ViewModelBase break; } + // Auto-promote teams with byes (TBD opponents with no parent match) + await AutoPromoteByeTeams(tournament.Id); + await LoadMatches(); } + private async Task AutoPromoteByeTeams(int tournamentId) + { + // Find matches where a team has a TBD opponent (only 1 team in match) and no parent match + var matchesToPromote = await _context.Matches + .Where(m => m.TournamentId == tournamentId && m.Teams.Count == 1) + .Include(m => m.Teams) + .Include(m => m.WinnerMatch) + .ToListAsync(); + + foreach (var match in matchesToPromote) + { + // Check if this match has no incoming teams (no parent matches pointing to it) + var hasParent = await _context.Matches + .AnyAsync(m => m.WinnerMatchId == match.Id || m.LoserMatchId == match.Id); + + if (!hasParent && match.WinnerMatchId.HasValue) + { + // This is a bye match - promote the team to the next match + var team = match.Teams.First(); + var winnerMatch = match.WinnerMatch; + if (winnerMatch != null) + { + // Add team to the winner match (position doesn't matter) + if (!winnerMatch.Teams.Any(t => t.TeamId == team.TeamId)) + { + winnerMatch.Teams.Add(new TeamParticipant + { + TeamId = team.TeamId, + Seed = team.Seed, + Score = 0, + Team = team.Team, + MatchId = winnerMatch.Id, + Round = null! + }); + } + + // Mark the current match's round as finished since it was a bye + if (match.Rounds.Any()) + { + match.Rounds.Last().State = RoundState.Finished; + } + + // Recursively check if the destination match now has all teams and is also a bye + if (winnerMatch.Teams.Count == 1) + { + var winnerHasParent = await _context.Matches + .AnyAsync(m => m.WinnerMatchId == winnerMatch.Id || m.LoserMatchId == winnerMatch.Id); + + if (!winnerHasParent && winnerMatch.WinnerMatchId.HasValue) + { + // Recursively promote + await PromoteRecursive(winnerMatch, tournamentId); + } + } + } + } + } + + await _context.SaveChangesAsync(); + } + + private async Task PromoteRecursive(Match match, int tournamentId) + { + if (match.Teams.Count != 1 || !match.WinnerMatchId.HasValue) + return; + + var team = match.Teams.First(); + var winnerMatch = await _context.Matches + .FindAsync(match.WinnerMatchId.Value); + + if (winnerMatch != null) + { + // Add team to the winner match (position doesn't matter) + if (!winnerMatch.Teams.Any(t => t.TeamId == team.TeamId)) + { + winnerMatch.Teams.Add(new TeamParticipant + { + TeamId = team.TeamId, + Seed = team.Seed, + Score = 0, + Team = team.Team, + MatchId = winnerMatch.Id, + Round = null! + }); + } + + if (match.Rounds.Any()) + { + match.Rounds.Last().State = RoundState.Finished; + } + + // Check if we need to continue promoting + if (winnerMatch.Teams.Count == 1 && winnerMatch.WinnerMatchId.HasValue) + { + await PromoteRecursive(winnerMatch, tournamentId); + } + } + } + private async Task GenerateSingleElimination(Tournament tournament, List teams) { int numTeams = teams.Count; @@ -504,6 +606,7 @@ public partial class TournamentsViewModel : ViewModelBase int totalRounds = (int)Math.Log2(nextPowerOf2); var allMatches = new List(); + // Create all matches first for (int round = 0; round < totalRounds; round++) { int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, round + 1); @@ -511,7 +614,24 @@ public partial class TournamentsViewModel : ViewModelBase for (int i = 0; i < matchesInRound; i++) { var match = CreateMatch(tournament); + allMatches.Add(match); + } + } + _context.Matches.AddRange(allMatches); + await _context.SaveChangesAsync(); + + // Now set up bracket navigation + int matchIndex = 0; + 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 = allMatches[matchIndex]; + + // Set up teams for first round if (round == 0) { int highSeed = i + 1; @@ -523,11 +643,25 @@ public partial class TournamentsViewModel : ViewModelBase AddTeamToMatch(match, teams[lowSeed - 1], lowSeed); } - allMatches.Add(match); + // Set winner goes to next round match + if (round < totalRounds - 1) + { + int nextRoundMatches = nextPowerOf2 / (int)Math.Pow(2, round + 2); + int nextMatchIndex = 0; + for (int r = 0; r <= round; r++) + { + nextMatchIndex += nextPowerOf2 / (int)Math.Pow(2, r + 1); + } + int nextMatchInRound = i / 2; + int targetMatchIndex = nextMatchIndex + nextMatchInRound; + + match.WinnerMatchId = allMatches[targetMatchIndex].Id; + } + + matchIndex++; } } - _context.Matches.AddRange(allMatches); await _context.SaveChangesAsync(); } @@ -543,6 +677,7 @@ public partial class TournamentsViewModel : ViewModelBase int wbRounds = (int)Math.Log2(nextPowerOf2); var allMatches = new List(); + // Create winners bracket matches var wbMatchesByRound = new List>(); for (int r = 0; r < wbRounds; r++) { @@ -556,9 +691,10 @@ public partial class TournamentsViewModel : ViewModelBase wbMatchesByRound.Add(roundMatches); } + // Create losers bracket matches + var lbMatchesByRound = new List>(); if (numTeams > 2) { - var lbMatchesByRound = new List>(); for (int r = 0; r < wbRounds - 1; r++) { int matchesInRound = nextPowerOf2 / 4; @@ -573,11 +709,89 @@ public partial class TournamentsViewModel : ViewModelBase var lbFinal = CreateMatch(tournament); allMatches.Add(lbFinal); + lbMatchesByRound.Add(new List { lbFinal }); var grandFinals = CreateMatch(tournament); allMatches.Add(grandFinals); } + // Save all matches first to get IDs + _context.Matches.AddRange(allMatches); + await _context.SaveChangesAsync(); + + // Set up winners bracket navigation + int wbMatchIndex = 0; + for (int r = 0; r < wbRounds; r++) + { + int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, r + 1); + for (int i = 0; i < matchesInRound; i++) + { + var match = wbMatchesByRound[r][i]; + + // Winner goes to next WB round + if (r < wbRounds - 1) + { + var nextMatch = wbMatchesByRound[r + 1][i / 2]; + match.WinnerMatchId = nextMatch.Id; + } + else + { + // WB final winner goes to grand finals + var grandFinals = allMatches.Last(); + match.WinnerMatchId = grandFinals.Id; + } + + // Loser drops to losers bracket + if (r > 0 && lbMatchesByRound.Count > 0) + { + int lbRound = r - 1; + if (lbRound < lbMatchesByRound.Count - 1) // Not LB final + { + int lbMatchIndex = i / 2; + if (lbMatchIndex < lbMatchesByRound[lbRound].Count) + { + var lbMatch = lbMatchesByRound[lbRound][lbMatchIndex]; + match.LoserMatchId = lbMatch.Id; + } + } + } + else if (r == 0 && lbMatchesByRound.Count > 0) + { + // First round losers go to first LB round + int lbMatchIndex = i / 2; + if (lbMatchIndex < lbMatchesByRound[0].Count) + { + var lbMatch = lbMatchesByRound[0][lbMatchIndex]; + match.LoserMatchId = lbMatch.Id; + } + } + } + } + + // Set up losers bracket navigation + for (int r = 0; r < lbMatchesByRound.Count; r++) + { + for (int i = 0; i < lbMatchesByRound[r].Count; i++) + { + var match = lbMatchesByRound[r][i]; + + if (r < lbMatchesByRound.Count - 2) + { + // Winner goes to next LB round + var nextMatch = lbMatchesByRound[r + 1][i / 2]; + match.WinnerMatchId = nextMatch.Id; + } + else if (r == lbMatchesByRound.Count - 2) + { + // LB final winner goes to grand finals + var grandFinals = allMatches.Last(); + match.WinnerMatchId = grandFinals.Id; + } + // LB losers are eliminated - no LoserMatchId + } + } + + // Set up teams for first round var firstRoundMatches = wbMatchesByRound[0]; for (int i = 0; i < nextPowerOf2 / 2; i++) { @@ -591,7 +805,6 @@ public partial class TournamentsViewModel : ViewModelBase AddTeamToMatch(match, teams[lowSeed - 1], lowSeed); } - _context.Matches.AddRange(allMatches); await _context.SaveChangesAsync(); } diff --git a/TournamentOrganizer/Views/MainView.axaml b/TournamentOrganizer/Views/MainView.axaml index 42ce3ab..1c5c2d9 100644 --- a/TournamentOrganizer/Views/MainView.axaml +++ b/TournamentOrganizer/Views/MainView.axaml @@ -18,6 +18,7 @@