Laurent 2 years ago
commit faaeb416c9
  1. 91
      PadelClub.xcodeproj/project.pbxproj
  2. 171
      PadelClub/Data/GroupStage.swift
  3. 302
      PadelClub/Data/Match.swift
  4. 3
      PadelClub/Data/PlayerRegistration.swift
  5. 272
      PadelClub/Data/Round.swift
  6. 20
      PadelClub/Data/TeamRegistration.swift
  7. 9
      PadelClub/Data/TeamScore.swift
  8. 99
      PadelClub/Data/Tournament.swift
  9. 17
      PadelClub/Extensions/Sequence+Extensions.swift
  10. 16
      PadelClub/Manager/Network/NetworkFederalService.swift
  11. 9
      PadelClub/Manager/Network/NetworkManagerError.swift
  12. 12
      PadelClub/Manager/PadelRule.swift
  13. 2
      PadelClub/Manager/SourceFileManager.swift
  14. 2
      PadelClub/PadelClubApp.swift
  15. 101
      PadelClub/ViewModel/MatchDescriptor.swift
  16. 14
      PadelClub/ViewModel/NavigationViewModel.swift
  17. 5
      PadelClub/ViewModel/SeedInterval.swift
  18. 33
      PadelClub/ViewModel/SetDescriptor.swift
  19. 6
      PadelClub/Views/Club/ClubSearchView.swift
  20. 4
      PadelClub/Views/Club/ClubsView.swift
  21. 4
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  22. 45
      PadelClub/Views/Components/MatchListView.swift
  23. 34
      PadelClub/Views/Components/RowButtonView.swift
  24. 4
      PadelClub/Views/Event/EventCreationView.swift
  25. 4
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  26. 107
      PadelClub/Views/GroupStage/GroupStageTeamView.swift
  27. 347
      PadelClub/Views/GroupStage/GroupStageView.swift
  28. 71
      PadelClub/Views/GroupStage/GroupStagesView.swift
  29. 2
      PadelClub/Views/Match/MatchDateView.swift
  30. 99
      PadelClub/Views/Match/MatchDetailView.swift
  31. 4
      PadelClub/Views/Match/MatchRowView.swift
  32. 53
      PadelClub/Views/Match/MatchSetupView.swift
  33. 10
      PadelClub/Views/Match/MatchSummaryView.swift
  34. 23
      PadelClub/Views/Match/PlayerBlockView.swift
  35. 56
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  36. 4
      PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift
  37. 18
      PadelClub/Views/Navigation/MainView.swift
  38. 2
      PadelClub/Views/Navigation/PadelClubView.swift
  39. 2
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  40. 53
      PadelClub/Views/Round/LoserBracketView.swift
  41. 96
      PadelClub/Views/Round/LoserRoundsView.swift
  42. 72
      PadelClub/Views/Round/RoundSettingsView.swift
  43. 38
      PadelClub/Views/Round/RoundView.swift
  44. 8
      PadelClub/Views/Round/RoundsView.swift
  45. 104
      PadelClub/Views/Score/EditScoreView.swift
  46. 50
      PadelClub/Views/Score/PointSelectionView.swift
  47. 26
      PadelClub/Views/Score/PointView.swift
  48. 215
      PadelClub/Views/Score/SetInputView.swift
  49. 57
      PadelClub/Views/Score/SetLabelView.swift
  50. 23
      PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift
  51. 20
      PadelClub/Views/Team/TeamPickerView.swift
  52. 6
      PadelClub/Views/Team/TeamRowView.swift
  53. 2
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  54. 16
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  55. 17
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  56. 2
      PadelClub/Views/Tournament/TournamentView.swift
  57. 26
      PadelClub/Views/ViewModifiers/TabItemModifier.swift

@ -103,6 +103,7 @@
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */; }; FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */; };
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; };
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; }; FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; };
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */; };
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; };
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; };
@ -171,12 +172,26 @@
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; };
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; };
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; };
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; };
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; };
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; };
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; };
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; };
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; };
FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; };
FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */; };
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; };
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; };
FFC83D512BB8087E00750834 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D502BB8087E00750834 /* RoundView.swift */; }; FFC83D512BB8087E00750834 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D502BB8087E00750834 /* RoundView.swift */; };
FFCFBFFE2BBBE86600B82851 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = FFCFBFFD2BBBE86600B82851 /* Algorithms */; };
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0012BBC39A600B82851 /* EditScoreView.swift */; };
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */; };
FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0112BBC3E1A00B82851 /* PointView.swift */; };
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */; };
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */; };
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; };
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; };
FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */; };
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; }; FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; };
FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; }; FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; };
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.swift */; }; FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.swift */; };
@ -334,6 +349,7 @@
FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; }; FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = "<group>"; }; FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = "<group>"; };
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = "<group>"; }; FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = "<group>"; };
FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemModifier.swift; sourceTree = "<group>"; };
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; }; FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; }; FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; };
@ -403,12 +419,25 @@
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = "<group>"; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = "<group>"; };
FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = "<group>"; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = "<group>"; };
FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamView.swift; sourceTree = "<group>"; };
FFBF065D2BBD8040009D6715 /* MatchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchListView.swift; sourceTree = "<group>"; };
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = "<group>"; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = "<group>"; };
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = "<group>"; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = "<group>"; };
FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = "<group>"; }; FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = "<group>"; };
FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketView.swift; sourceTree = "<group>"; };
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = "<group>"; };
FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = "<group>"; }; FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = "<group>"; };
FFC83D502BB8087E00750834 /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = "<group>"; }; FFC83D502BB8087E00750834 /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = "<group>"; };
FFCFC0012BBC39A600B82851 /* EditScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScoreView.swift; sourceTree = "<group>"; };
FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointSelectionView.swift; sourceTree = "<group>"; };
FFCFC0112BBC3E1A00B82851 /* PointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointView.swift; sourceTree = "<group>"; };
FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDescriptor.swift; sourceTree = "<group>"; };
FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetInputView.swift; sourceTree = "<group>"; };
FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetLabelView.swift; sourceTree = "<group>"; };
FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSmallSelectionView.swift; sourceTree = "<group>"; };
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDescriptor.swift; sourceTree = "<group>"; };
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = "<group>"; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = "<group>"; };
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; }; FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
FFD784012B91C1B4000F62A6 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; }; FFD784012B91C1B4000F62A6 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
@ -428,6 +457,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FFCFBFFE2BBBE86600B82851 /* Algorithms in Frameworks */,
FF2BE4872B85E27400592328 /* LeStorage.framework in Frameworks */, FF2BE4872B85E27400592328 /* LeStorage.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -564,6 +594,7 @@
FFC83D4B2BB807C200750834 /* Round */, FFC83D4B2BB807C200750834 /* Round */,
FF967CF92BAEE11500A9A3BD /* GroupStage */, FF967CF92BAEE11500A9A3BD /* GroupStage */,
FF967CFE2BAEEF5A00A9A3BD /* Match */, FF967CFE2BAEEF5A00A9A3BD /* Match */,
FFCFC00B2BBC39A600B82851 /* Score */,
FF967D072BAF3D3000A9A3BD /* Team */, FF967D072BAF3D3000A9A3BD /* Team */,
FF089EB92BB011EE00F0AEC7 /* Player */, FF089EB92BB011EE00F0AEC7 /* Player */,
FF3F74F72B919F96004CFE0E /* Tournament */, FF3F74F72B919F96004CFE0E /* Tournament */,
@ -615,6 +646,7 @@
C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */, C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */,
FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */, FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */,
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */, FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */,
FFBF065D2BBD8040009D6715 /* MatchListView.swift */,
FF967CF72BAEDF0000A9A3BD /* Labels.swift */, FF967CF72BAEDF0000A9A3BD /* Labels.swift */,
); );
path = Components; path = Components;
@ -786,6 +818,9 @@
FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */, FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */,
FFB9C8702BBADDE200A0EF4F /* Selectable.swift */, FFB9C8702BBADDE200A0EF4F /* Selectable.swift */,
FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */, FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */,
FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */,
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
); );
path = ViewModel; path = ViewModel;
sourceTree = "<group>"; sourceTree = "<group>";
@ -798,6 +833,7 @@
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */,
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */,
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */, FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */,
FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */,
); );
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
@ -861,6 +897,7 @@
FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */, FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */,
FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */,
FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */, FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */,
FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */,
); );
path = GroupStage; path = GroupStage;
sourceTree = "<group>"; sourceTree = "<group>";
@ -894,10 +931,24 @@
FFC83D4E2BB807D100750834 /* RoundsView.swift */, FFC83D4E2BB807D100750834 /* RoundsView.swift */,
FFC83D502BB8087E00750834 /* RoundView.swift */, FFC83D502BB8087E00750834 /* RoundView.swift */,
FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */, FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */,
FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
); );
path = Round; path = Round;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FFCFC00B2BBC39A600B82851 /* Score */ = {
isa = PBXGroup;
children = (
FFCFC0012BBC39A600B82851 /* EditScoreView.swift */,
FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */,
FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */,
FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */,
FFCFC0112BBC3E1A00B82851 /* PointView.swift */,
);
path = Score;
sourceTree = "<group>";
};
FFD783FB2B91B919000F62A6 /* Agenda */ = { FFD783FB2B91B919000F62A6 /* Agenda */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -915,6 +966,7 @@
children = ( children = (
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */, FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */,
FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */, FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */,
FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */,
); );
path = ViewModifiers; path = ViewModifiers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -970,6 +1022,9 @@
dependencies = ( dependencies = (
); );
name = PadelClub; name = PadelClub;
packageProductDependencies = (
FFCFBFFD2BBBE86600B82851 /* Algorithms */,
);
productName = PadelClub; productName = PadelClub;
productReference = C425D3FD2B6D249D002A7B48 /* PadelClub.app */; productReference = C425D3FD2B6D249D002A7B48 /* PadelClub.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@ -1042,6 +1097,9 @@
Base, Base,
); );
mainGroup = C425D3F42B6D249D002A7B48; mainGroup = C425D3F42B6D249D002A7B48;
packageReferences = (
FF4C7F052BBBE6B90031B6A3 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
);
productRefGroup = C425D3FE2B6D249D002A7B48 /* Products */; productRefGroup = C425D3FE2B6D249D002A7B48 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectReferences = ( projectReferences = (
@ -1163,6 +1221,7 @@
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */, FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */, FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */,
C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */, C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */,
@ -1185,7 +1244,9 @@
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */,
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */,
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */, FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */,
FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */, FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */,
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */, FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */,
@ -1205,6 +1266,7 @@
FF8F264B2BAE0B4100650388 /* TournamentLevelPickerView.swift in Sources */, FF8F264B2BAE0B4100650388 /* TournamentLevelPickerView.swift in Sources */,
FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */, FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */,
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */, C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */,
FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */,
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */, FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */,
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */, FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */,
C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */, C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */,
@ -1216,15 +1278,18 @@
FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */, FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */, FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */, FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */, C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */,
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */,
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */,
FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
@ -1234,15 +1299,20 @@
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */,
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */,
C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */,
FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */, FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */,
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */,
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */, FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */,
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */, FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
@ -1267,11 +1337,13 @@
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */,
C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */, C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */,
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */, FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */,
FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */,
FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */, FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */,
FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */, FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */,
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */,
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */,
FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */, FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */,
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */,
C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */,
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
@ -1615,6 +1687,25 @@
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
FF4C7F052BBBE6B90031B6A3 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-algorithms.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
FFCFBFFD2BBBE86600B82851 /* Algorithms */ = {
isa = XCSwiftPackageProductDependency;
package = FF4C7F052BBBE6B90031B6A3 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
productName = Algorithms;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */
FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */ = { FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;

@ -7,6 +7,7 @@
import Foundation import Foundation
import LeStorage import LeStorage
import Algorithms
@Observable @Observable
class GroupStage: ModelObject, Storable { class GroupStage: ModelObject, Storable {
@ -36,8 +37,8 @@ class GroupStage: ModelObject, Storable {
self.startDate = startDate self.startDate = startDate
} }
func teamsAt(_ index: Int) -> TeamRegistration? { func teamAt(groupStagePosition: Int) -> TeamRegistration? {
teams().first(where: { $0.groupStagePosition == index }) teams().first(where: { $0.groupStagePosition == groupStagePosition })
} }
func tournamentObject() -> Tournament? { func tournamentObject() -> Tournament? {
@ -58,23 +59,25 @@ class GroupStage: ModelObject, Storable {
} }
func isRunning() -> Bool { // at least a match has started func isRunning() -> Bool { // at least a match has started
matches.anySatisfy({ $0.isRunning() }) _matches().anySatisfy({ $0.isRunning() })
} }
func hasStarted() -> Bool { // meaning at least one match is over func hasStarted() -> Bool { // meaning at least one match is over
matches.filter { $0.hasEnded() }.isEmpty == false _matches().filter { $0.hasEnded() }.isEmpty == false
} }
func hasEnded() -> Bool { func hasEnded() -> Bool {
if matches.isEmpty { return false } guard teams().count == size else { return false }
return matches.allSatisfy { $0.hasEnded() } let _matches = _matches()
if _matches.isEmpty { return false }
return _matches.allSatisfy { $0.hasEnded() }
} }
func buildMatches() { func buildMatches() {
removeMatches() _removeMatches()
var _matches = [Match]() var _matches = [Match]()
for i in 0..<numberOfMatchesToBuild { for i in 0..<_numberOfMatchesToBuild() {
let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat) let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat)
_matches.append(newMatch) _matches.append(newMatch)
} }
@ -82,24 +85,162 @@ class GroupStage: ModelObject, Storable {
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
} }
func removeMatches() { func matches() -> [Match] {
let ordered = _matches()
if ordered.isEmpty == false && ordered.count == _matchOrder().count {
return _matchOrder().map {
ordered[$0]
}
} else {
return ordered
}
}
func scoreLabel(forGroupStagePosition groupStagePosition: Int) -> String? {
if let scoreData = _score(forGroupStagePosition: groupStagePosition) {
return "\(scoreData.wins)/\(scoreData.loses) " + scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
} else {
return nil
}
}
fileprivate func _score(forGroupStagePosition groupStagePosition: Int) -> TeamGroupStageScore? {
guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
let wins = matches.filter { $0.winningTeamId == team.id }.count
let loses = matches.filter { $0.losingTeamId == team.id }.count
let differences = matches.compactMap { $0.scoreDifference(groupStagePosition) }
let setDifference = differences.map { $0.set }.reduce(0,+)
let gameDifference = differences.map { $0.game }.reduce(0,+)
return (team, wins, loses, setDifference, gameDifference)
}
func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] {
let combos = Array((0..<size).combinations(ofCount: 2))
var matchIndexes = [Int]()
for (index, combo) in combos.enumerated() {
if combo.contains(groupStagePosition) { //team is playing
matchIndexes.append(index)
}
}
return _matches().filter { matchIndexes.contains($0.index) }
}
func availableToStart() -> [Match] {
matches().filter({ $0.canBeStarted() && $0.isRunning() == false })
}
func runningMatches() -> [Match] {
matches().filter({ $0.isRunning() })
}
func readyMatches() -> [Match] {
matches().filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
}
func finishedMatches() -> [Match] {
matches().filter({ $0.hasEnded() })
}
private func _matchOrder() -> [Int] {
switch size {
case 3:
return [1, 2, 0]
case 4:
return [2, 3, 1, 4, 5, 0]
case 5:
return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9]
// return [3, 5, 8, 2, 6, 7, 1, 9, 4, 0]
case 6:
return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0]
//return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
default:
return []
}
}
private func _matchUp(for matchIndex: Int) -> [Int] {
Array((0..<size).combinations(ofCount: 2))[matchIndex]
}
func localizedMatchUpLabel(for matchIndex: Int) -> String {
let matchUp = _matchUp(for: matchIndex)
if let index = matchUp.first, let index2 = matchUp.last {
return "#\(index + 1) contre #\(index2 + 1)"
} else {
return "--"
}
}
func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
let _teams = _teams(for: matchIndex)
switch team {
case .one:
return _teams.first!
case .two:
return _teams.last!
}
}
private func _teams(for matchIndex: Int) -> [TeamRegistration?] {
let combinations = Array(0..<size).combinations(ofCount: 2).map {$0}
return combinations[safe: matchIndex]?.map { teamAt(groupStagePosition: $0) } ?? []
}
private func _removeMatches() {
try? deleteDependencies() try? deleteDependencies()
} }
var numberOfMatchesToBuild: Int { private func _numberOfMatchesToBuild() -> Int {
(size * (size - 1)) / 2 (size * (size - 1)) / 2
} }
var matches: [Match] { private func _matches() -> [Match] {
Store.main.filter { $0.groupStage == self.id } Store.main.filter { $0.groupStage == self.id }
} }
func teams() -> [TeamRegistration] { fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
Store.main.filter { $0.groupStage == self.id } fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
let combos = Array((0..<size).combinations(ofCount: 2))
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
return teamPosition.id == match.losingTeamId
} else {
return false
}
}
func teams(_ sortedByScore: Bool = false) -> [TeamRegistration] {
let teams: [TeamRegistration] = Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
if sortedByScore {
return teams.compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference },
{ $0.gameDifference < $1.gameDifference},
{ self._headToHead($0.team, $1.team) },
{ $0.team.groupStagePosition! > $1.team.groupStagePosition! }
]
for predicate in predicates {
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
continue
}
return predicate(lhs, rhs)
}
return false
}.map({ $0.team }).reversed()
} else {
return teams.sorted(by: \TeamRegistration.groupStagePosition!)
}
} }
override func deleteDependencies() throws { override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.matches) try Store.main.deleteDependencies(items: self._matches())
} }
} }
@ -120,6 +261,6 @@ extension GroupStage: Selectable {
} }
func badgeValue() -> Int? { func badgeValue() -> Int? {
nil runningMatches().count
} }
} }

@ -26,7 +26,7 @@ class Match: ModelObject, Storable {
var broadcasted: Bool var broadcasted: Bool
var name: String? var name: String?
var order: Int var order: Int
private(set) var disabled: Bool = false var disabled: Bool = false
internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) { internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) {
self.round = round self.round = round
@ -47,13 +47,17 @@ class Match: ModelObject, Storable {
func indexInRound() -> Int { func indexInRound() -> Int {
if groupStage != nil { if groupStage != nil {
return index return index
} else if let index = roundObject?.matches.firstIndex(where: { $0.id == id }) { } else if let index = roundObject?.playedMatches().firstIndex(where: { $0.id == id }) {
return index return index
} }
return RoundRule.matchIndexWithinRound(fromMatchIndex: index) return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
} }
func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String { func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let groupStageObject {
return groupStageObject.localizedMatchUpLabel(for: index)
}
switch displayStyle { switch displayStyle {
case .wide: case .wide:
return "Match \(indexInRound() + 1)" return "Match \(indexInRound() + 1)"
@ -62,6 +66,58 @@ class Match: ModelObject, Storable {
} }
} }
func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> Bool {
guard let bracketPosition = team.bracketPosition else { return false }
return index * 2 + teamPosition.rawValue == bracketPosition
}
func resetMatch() {
losingTeamId = nil
winningTeamId = nil
endDate = nil
court = nil
servingTeamId = nil
}
func teamWillBeWalkOut(_ team: TeamRegistration) {
resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
try? DataStore.shared.teamScores.delete(contentOfs: previousScores)
if let existingTeamScore = teamScore(ofTeam: team) {
try? DataStore.shared.teamScores.delete(instance: existingTeamScore)
}
let teamScoreWalkout = TeamScore(match: id, teamRegistration: team.id)
teamScoreWalkout.walkOut = 1
try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreWalkout)
}
func luckyLosers() -> [TeamRegistration] {
roundObject?.previousRound()?.losers() ?? []
}
func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool {
teamScore(teamPosition)?.walkOut == 1
}
func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) {
resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
try? DataStore.shared.teamScores.delete(contentOfs: previousScores)
if let existingTeamScore = teamScore(ofTeam: team) {
try? DataStore.shared.teamScores.delete(instance: existingTeamScore)
}
let matchIndex = index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let position = matchIndex * 2 + teamPosition.rawValue
let teamScoreLuckyLoser = TeamScore(match: id, teamRegistration: team.id)
teamScoreLuckyLoser.luckyLoser = position
try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)
}
func disableMatch() { func disableMatch() {
_toggleMatchDisableState(true) _toggleMatchDisableState(true)
} }
@ -70,8 +126,24 @@ class Match: ModelObject, Storable {
_toggleMatchDisableState(false) _toggleMatchDisableState(false)
} }
private func _toggleLoserMatchDisableState(_ state: Bool) {
if isLoserBracket == false {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index)
if let loserMatch = roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) {
loserMatch.disabled = state
try? DataStore.shared.matches.addOrUpdate(instance: loserMatch)
loserMatch._toggleLoserMatchDisableState(state)
}
} else {
roundObject?.loserRounds().forEach({ round in
round.handleLoserRoundState()
})
}
}
fileprivate func _toggleMatchDisableState(_ state: Bool) { fileprivate func _toggleMatchDisableState(_ state: Bool) {
disabled = state disabled = state
_toggleLoserMatchDisableState(state)
topPreviousRoundMatch()?._toggleMatchDisableState(state) topPreviousRoundMatch()?._toggleMatchDisableState(state)
bottomPreviousRoundMatch()?._toggleMatchDisableState(state) bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
try? DataStore.shared.matches.addOrUpdate(instance: self) try? DataStore.shared.matches.addOrUpdate(instance: self)
@ -99,8 +171,16 @@ class Match: ModelObject, Storable {
}.sorted(by: \.index).first }.sorted(by: \.index).first
} }
func previousMatch(_ teamPosition: Int) -> Match? { func upperBracketMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == 0 { if teamPosition == .one {
return roundObject?.upperBracketTopMatch(ofMatchIndex: index)
} else {
return roundObject?.upperBracketBottomMatch(ofMatchIndex: index)
}
}
func previousMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == .one {
return topPreviousRoundMatch() return topPreviousRoundMatch()
} else { } else {
return bottomPreviousRoundMatch() return bottomPreviousRoundMatch()
@ -124,6 +204,110 @@ class Match: ModelObject, Storable {
} }
} }
func setWalkOut(_ teamPosition: TeamPosition) {
let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, teamRegistration: team(teamPosition)?.id)
teamScoreWalkout.walkOut = 0
let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, teamRegistration: team(teamPosition.otherTeam)?.id)
teamScoreWinning.walkOut = nil
try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
if endDate == nil {
endDate = Date()
}
winningTeamId = teamScoreWinning.teamRegistration
losingTeamId = teamScoreWalkout.teamRegistration
// matchDescriptor.match?.tournament?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
// matchDescriptor.match?.loserBracket?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
// matchDescriptor.match?.currentTournament?.removeField(matchDescriptor.match?.fieldIndex)
}
func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
updateScore(fromMatchDescriptor: matchDescriptor)
if endDate == nil {
endDate = Date()
}
// matchDescriptor.match?.tournament?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
// matchDescriptor.match?.loserBracket?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
// matchDescriptor.match?.currentTournament?.removeField(matchDescriptor.match?.fieldIndex)
winningTeamId = team(matchDescriptor.winner)?.id
losingTeamId = team(matchDescriptor.winner.otherTeam)?.id
}
func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, teamRegistration: team(.one)?.id)
teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",")
let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, teamRegistration: team(.two)?.id)
teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",")
try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo])
matchFormat = matchDescriptor.matchFormat
}
func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) {
if hasEnded() == false {
startDate = fromStartDate
switch fieldSetup {
case .random:
let courtName = availableCourts().randomElement()
court = courtName
case .field(let courtName):
court = courtName
}
} else {
startDate = fromStartDate
endDate = toEndDate
}
}
func courtCount() -> Int {
currentTournament()?.courtCount ?? 1
}
func courtIsAvailable(_ courtIndex: Int) -> Bool {
let courtUsed = currentTournament()?.courtUsed() ?? []
return courtUsed.contains(String(courtIndex)) == false
// return Set(availableCourts().map { String($0) }).subtracting(Set(courtUsed))
}
func courtIsPreferred(_ courtIndex: Int) -> Bool {
false
}
func availableCourts() -> [String] {
let courtUsed = currentTournament()?.courtUsed() ?? []
let availableCourts = Array(1...courtCount())
return Array(Set(availableCourts.map { String($0) }).subtracting(Set(courtUsed)))
}
func removeCourt() {
court = nil
}
func setCourt(_ courtIndex: Int) {
court = String(courtIndex)
}
func canBeStarted() -> Bool {
let teams = teams()
guard teams.count == 2 else { return false }
guard hasEnded() == false else { return false }
guard hasStarted() == false else { return false }
return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0) == false })
}
func isTeamPlaying(_ team: TeamRegistration) -> Bool {
if isGroupStage() {
let isPlaying = groupStageObject?.runningMatches().filter({ $0.teams().contains(team) }).isEmpty == false
return isPlaying
} else {
//todo
return false
}
}
func isReady() -> Bool { func isReady() -> Bool {
teams().count == 2 teams().count == 2
} }
@ -133,7 +317,7 @@ class Match: ModelObject, Storable {
} }
func hasEnded() -> Bool { func hasEnded() -> Bool {
endDate != nil endDate != nil || hasWalkoutTeam() || winningTeamId != nil
} }
func isGroupStage() -> Bool { func isGroupStage() -> Bool {
@ -160,75 +344,62 @@ class Match: ModelObject, Storable {
groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject() groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject()
} }
func tournamentId() -> String? {
groupStageObject?.tournament ?? roundObject?.tournament
}
func scores() -> [TeamScore] { func scores() -> [TeamScore] {
Store.main.filter(isIncluded: { $0.match == id }) Store.main.filter(isIncluded: { $0.match == id })
} }
func teams() -> [TeamRegistration] { func teams() -> [TeamRegistration] {
if groupStage != nil { if groupStage != nil {
return scores().compactMap({ $0.team }).sorted(by: \.groupStagePosition!) return [groupStageProjectedTeam(.one), groupStageProjectedTeam(.two)].compactMap { $0 }
} }
return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 }
} }
func groupStageProjectedTeam(_ team: TeamData) -> TeamRegistration? { func scoreDifference(_ teamPosition: Int) -> (set: Int, game: Int)? {
guard groupStage != nil else { return nil } guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil }
var reverseValue = 1
switch team { if teamPosition == team(.two)?.groupStagePosition {
case .one: reverseValue = -1
if let teamId = topPreviousRoundMatch()?.winningTeamId {
return Store.main.findById(teamId)
} }
case .two: let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
if let teamId = bottomPreviousRoundMatch()?.winningTeamId { let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut())
return Store.main.findById(teamId) var setDifference : Int = 0
if endedSetsOne.count == 1 {
setDifference = endedSetsOne[0] - endedSetsTwo[0]
} else {
setDifference = endedSetsOne.filter { $0 == matchFormat.setFormat.scoreToWin }.count - endedSetsTwo.filter { $0 == matchFormat.setFormat.scoreToWin }.count
} }
let zip = zip(endedSetsOne, endedSetsTwo)
let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+)
return (setDifference * reverseValue, gameDifference * reverseValue)
} }
return nil func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
guard let groupStageObject else { return nil }
return groupStageObject.team(teamPosition: team, inMatchIndex: index)
} }
func seed(_ team: TeamData) -> TeamRegistration? { func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
guard let roundObject else { return nil } guard let roundObject else { return nil }
return Store.main.filter(isIncluded: { return roundObject.roundProjectedTeam(team, inMatch: self)
$0.tournament == roundObject.tournament && $0.bracketPosition != nil
}).first(where: {
($0.bracketPosition! / 2) == self.index
&& ($0.bracketPosition! % 2) == team.rawValue
})
}
func roundProjectedTeam(_ team: TeamData) -> TeamRegistration? {
guard round != nil else { return nil }
if let seed = seed(team) {
return seed
}
switch team {
case .one:
if let teamId = topPreviousRoundMatch()?.winningTeamId {
return Store.main.findById(teamId)
}
case .two:
if let teamId = bottomPreviousRoundMatch()?.winningTeamId {
return Store.main.findById(teamId)
}
} }
return nil func teamWon(_ team: TeamRegistration?) -> Bool {
guard let winningTeamId else { return false }
return winningTeamId == team?.id
} }
func teamWon(_ team: TeamData) -> Bool { func team(_ team: TeamPosition) -> TeamRegistration? {
true
}
func team(_ team: TeamData) -> TeamRegistration? {
if groupStage != nil { if groupStage != nil {
switch team { switch team {
case .one: case .one:
return teams().first return groupStageProjectedTeam(.one)
case .two: case .two:
return teams().last return groupStageProjectedTeam(.two)
} }
} else { } else {
switch team { switch team {
@ -240,16 +411,20 @@ class Match: ModelObject, Storable {
} }
} }
func teamNames(_ team: TeamData) -> [String]? { func teamNames(_ team: TeamRegistration?) -> [String]? {
self.team(team)?.players().map { $0.playerLabel() } team?.players().map { $0.playerLabel() }
} }
func teamWalkOut(_ team: TeamData) -> Bool { func teamWalkOut(_ team: TeamRegistration?) -> Bool {
false teamScore(ofTeam: team)?.isWalkOut() == true
} }
func teamScore(_ team: TeamData) -> TeamScore? { func teamScore(_ team: TeamPosition) -> TeamScore? {
scores().first(where: { $0.teamRegistration == self.team(team)?.id }) teamScore(ofTeam: self.team(team))
}
func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? {
scores().first(where: { $0.teamRegistration == team?.id })
} }
func isRunning() -> Bool { // at least a match has started func isRunning() -> Bool { // at least a match has started
@ -313,3 +488,20 @@ class Match: ModelObject, Storable {
case _disabled = "disabled" case _disabled = "disabled"
} }
} }
enum MatchDateSetup: Hashable, Identifiable {
case inMinutes(Int)
case now
case customDate
var id: Int { hashValue }
}
enum MatchFieldSetup: Hashable, Identifiable {
case random
// case firstAvailable
case field(String)
var id: Int { hashValue }
}

@ -35,6 +35,8 @@ class PlayerRegistration: ModelObject, Storable {
var weight: Int = 0 var weight: Int = 0
var source: PlayerDataSource? var source: PlayerDataSource?
var hasArrived: Bool = false
internal init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int, source: PlayerDataSource? = nil) { internal init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int, source: PlayerDataSource? = nil) {
self.teamRegistration = teamRegistration self.teamRegistration = teamRegistration
self.firstName = firstName self.firstName = firstName
@ -251,6 +253,7 @@ class PlayerRegistration: ModelObject, Storable {
case _email = "email" case _email = "email"
case _weight = "weight" case _weight = "weight"
case _source = "source" case _source = "source"
case _hasArrived = "hasArrived"
} }

@ -36,31 +36,221 @@ class Round: ModelObject, Storable {
func hasStarted() -> Bool { func hasStarted() -> Bool {
matches.anySatisfy({ $0.hasStarted() }) playedMatches().anySatisfy({ $0.hasStarted() })
} }
func hasEnded() -> Bool { func hasEnded() -> Bool {
matches.allSatisfy({ $0.hasEnded() }) playedMatches().allSatisfy({ $0.hasEnded() })
} }
func tournamentObject() -> Tournament? { func tournamentObject() -> Tournament? {
Store.main.findById(tournament) Store.main.findById(tournament)
} }
var matches: [Match] { private func _matches() -> [Match] {
Store.main.filter { $0.round == self.id }
}
func team(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? {
switch team {
case .one:
return roundProjectedTeam(.one, inMatch: match)
case .two:
return roundProjectedTeam(.two, inMatch: match)
}
}
func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil
}).first(where: {
($0.bracketPosition! / 2) == matchIndex
&& ($0.bracketPosition! % 2) == team.rawValue
})
}
func losers() -> [TeamRegistration] {
_matches().compactMap { $0.losingTeamId }.compactMap { Store.main.findById($0) }
}
func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? {
if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) {
return seed
}
switch team {
case .one:
if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 }) {
return luckyLoser.team
} else if let loser = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId {
return Store.main.findById(loser)
} else if let previousMatch = topPreviousRoundMatch(ofMatch: match) {
if let teamId = previousMatch.winningTeamId {
return Store.main.findById(teamId)
} else if previousMatch.disabled {
return previousMatch.teams().first
}
}
case .two:
if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) {
return luckyLoser.team
} else if let loser = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId {
return Store.main.findById(loser)
} else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) {
if let teamId = previousMatch.winningTeamId {
return Store.main.findById(teamId)
} else if previousMatch.disabled {
return previousMatch.teams().first
}
}
}
return nil
}
func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) {
return upperBracketTopMatch
}
return nil
}
func upperBracketBottomMatch(ofMatchIndex matchIndex: Int) -> Match? {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) {
return upperBracketBottomMatch
}
return nil
}
func topPreviousRoundMatch(ofMatch match: Match) -> Match? {
guard let previousRound = previousRound() else { return nil }
return Store.main.filter {
$0.index == match.topPreviousRoundMatchIndex() && $0.round == previousRound.id
}.sorted(by: \.index).first
}
func bottomPreviousRoundMatch(ofMatch match: Match) -> Match? {
guard let previousRound = previousRound() else { return nil }
return Store.main.filter {
$0.index == match.bottomPreviousRoundMatchIndex() && $0.round == previousRound.id
}.sorted(by: \.index).first
}
func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? {
Store.main.filter(isIncluded: {
let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index)
return $0.round == id && index == matchIndexInRound
}).first
}
func playedMatches() -> [Match] {
if loser == nil {
Store.main.filter { $0.round == self.id && $0.disabled == false } Store.main.filter { $0.round == self.id && $0.disabled == false }
} else {
Store.main.filter { $0.round == self.id }
}
} }
func previousRound() -> Round? { func previousRound() -> Round? {
Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == index + 1 }).first Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index + 1 }).first
} }
func nextRound() -> Round? { func nextRound() -> Round? {
Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == index - 1 }).first Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index - 1 }).first
}
func loserRounds(forRoundIndex roundIndex: Int) -> [Round] {
return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount)
}
func isDisabled() -> Bool {
_matches().allSatisfy({ $0.disabled })
}
func getActiveLoserRound() -> Round? {
let rounds = loserRounds()
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false })
}
func enableRound() {
_toggleRound(disable: false)
}
func disableRound() {
_toggleRound(disable: true)
}
private func _toggleRound(disable: Bool) {
let _matches = _matches()
_matches.forEach { match in
match.disabled = disable
match.resetMatch()
try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores)
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
}
func handleLoserRoundState() {
let _matches = _matches()
_matches.forEach { match in
let previousRound = self.previousRound()
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index)
var parentMatches = [Match]()
if isLoserBracket(), previousRound == nil, let parentRound = parentRound {
let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2)
let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)
parentMatches = [upperBracketTopMatch, upperBracketBottomMatch].compactMap({ $0 })
} else if let previousRound {
let previousRoundTopMatch : Match? = Store.main.filter {
$0.round == previousRound.id && $0.index == match.topPreviousRoundMatchIndex()
}.first
let previousRoundBottomMatch : Match? = Store.main.filter {
$0.round == previousRound.id && $0.index == match.bottomPreviousRoundMatchIndex()
}.first
parentMatches = [previousRoundTopMatch, previousRoundBottomMatch].compactMap({ $0 })
}
if parentMatches.anySatisfy({ $0.disabled }) {
match.disabled = true
} else if parentMatches.allSatisfy({ $0.disabled == false }) {
match.disabled = false
}
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
loserRounds().forEach { round in
round.handleLoserRoundState()
}
}
var cumulativeMatchCount: Int {
var totalMatches = playedMatches().count
if let parent = parentRound {
totalMatches += parent.cumulativeMatchCount
}
return totalMatches
}
func initialRound() -> Round? {
if let parentRound {
return parentRound.initialRound()
} else {
return self
}
} }
func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
RoundRule.roundName(fromRoundIndex: index) if let parentRound, let initialRound = parentRound.initialRound() {
let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count
print("initialRound", initialRound.roundTitle())
if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() {
return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle)
}
}
return RoundRule.roundName(fromRoundIndex: index)
} }
func roundStatus() -> String { func roundStatus() -> String {
@ -71,16 +261,60 @@ class Round: ModelObject, Storable {
} }
} }
var loserRound: Round? { func loserRounds() -> [Round] {
guard let loser else { return nil } return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed()
return Store.main.findById(loser)
} }
override func deleteDependencies() throws { func loserRoundsAndChildren() -> [Round] {
try Store.main.deleteDependencies(items: self.matches) let loserRounds = loserRounds()
if let loserRound { return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() })
try Store.main.deleteDependencies(items: [loserRound]) }
func isLoserBracket() -> Bool {
loser != nil
}
func buildLoserBracket() {
guard loserRounds().isEmpty else { return }
let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index)
guard currentRoundMatchCount > 1 else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount)
let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat
let rounds = (0..<roundCount).map { //index 0 is the final
let round = Round(tournament: tournament, index: $0, matchFormat: loserBracketMatchFormat)
round.loser = id //parent
return round
}
try? DataStore.shared.rounds.addOrUpdate(contentOfs: rounds)
let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount)
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat)
}
print(matches.map {
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
})
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
loserRounds().forEach { round in
round.buildLoserBracket()
}
} }
var parentRound: Round? {
guard let parentRound = loser else { return nil }
return Store.main.findById(parentRound)
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: _matches())
try Store.main.deleteDependencies(items: loserRoundsAndChildren())
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -94,10 +328,18 @@ class Round: ModelObject, Storable {
extension Round: Selectable { extension Round: Selectable {
func selectionLabel() -> String { func selectionLabel() -> String {
roundTitle() if let parentRound {
return "Tour #\(parentRound.loserRounds().count - index)"
} else {
return roundTitle()
}
} }
func badgeValue() -> Int? { func badgeValue() -> Int? {
nil if let parentRound {
return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count
} else {
return playedMatches().filter({ $0.isRunning() }).count
}
} }
} }

@ -18,7 +18,7 @@ class TeamRegistration: ModelObject, Storable {
var registrationDate: Date? var registrationDate: Date?
var callDate: Date? var callDate: Date?
var bracketPosition: Int? var bracketPosition: Int?
var groupStagePosition: Int? var groupStagePosition: Int? //todo devrait être non nil ?
var comment: String? var comment: String?
var source: String? var source: String?
var sourceValue: String? var sourceValue: String?
@ -32,6 +32,7 @@ class TeamRegistration: ModelObject, Storable {
var weight: Int = 0 var weight: Int = 0
var lockWeight: Int? var lockWeight: Int?
var confirmationDate: Date? var confirmationDate: Date?
var qualified: Bool = false
internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, category: Int? = nil) { internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, category: Int? = nil) {
self.tournament = tournament self.tournament = tournament
@ -52,17 +53,17 @@ class TeamRegistration: ModelObject, Storable {
bracketPosition == nil && groupStage == nil bracketPosition == nil && groupStage == nil
} }
func setSeedPosition(inSpot match: Match, upperBranch: Int?, opposingSeeding: Bool) { func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
let matchIndex = match.index let matchIndex = match.index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = upperBranch ?? (isUpper ? 0 : 1) var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding { if opposingSeeding {
teamPosition = upperBranch ?? (isUpper ? 1 : 0) teamPosition = slot ?? (isUpper ? .two : .one)
} }
match.previousMatch(teamPosition)?.disableMatch() match.previousMatch(teamPosition)?.disableMatch()
bracketPosition = matchIndex * 2 + teamPosition bracketPosition = matchIndex * 2 + teamPosition.rawValue
} }
var initialWeight: Int { var initialWeight: Int {
@ -159,7 +160,11 @@ class TeamRegistration: ModelObject, Storable {
} }
} }
func available() -> Bool { func canPlay() -> Bool {
teamScores().isEmpty == false || players().allSatisfy({ $0.hasPaid() || $0.hasArrived })
}
func availableForSeedPick() -> Bool {
groupStage == nil && bracketPosition == nil groupStage == nil && bracketPosition == nil
} }
@ -203,7 +208,7 @@ class TeamRegistration: ModelObject, Storable {
} }
} }
func qualified() -> Bool { func qualifiedFromGroupStage() -> Bool {
groupStagePosition != nil && bracketPosition != nil groupStagePosition != nil && bracketPosition != nil
} }
@ -304,6 +309,7 @@ class TeamRegistration: ModelObject, Storable {
case _walkOut = "walkOut" case _walkOut = "walkOut"
case _lockWeight = "lockWeight" case _lockWeight = "lockWeight"
case _confirmationDate = "confirmationDate" case _confirmationDate = "confirmationDate"
case _qualified = "qualified"
} }
} }

@ -19,9 +19,9 @@ class TeamScore: ModelObject, Storable {
var playerRegistrations: [String]? var playerRegistrations: [String]?
var score: String? var score: String?
var walkOut: Int? var walkOut: Int?
var luckyLoser: Bool var luckyLoser: Int?
internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Bool) { internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Int? = nil) {
self.match = match self.match = match
self.teamRegistration = teamRegistration self.teamRegistration = teamRegistration
self.playerRegistrations = playerRegistrations self.playerRegistrations = playerRegistrations
@ -30,6 +30,10 @@ class TeamScore: ModelObject, Storable {
self.luckyLoser = luckyLoser self.luckyLoser = luckyLoser
} }
func isWalkOut() -> Bool {
walkOut != nil
}
func matchObject() -> Match? { func matchObject() -> Match? {
Store.main.findById(match) Store.main.findById(match)
} }
@ -39,7 +43,6 @@ class TeamScore: ModelObject, Storable {
return nil return nil
} }
return DataStore.shared.teamRegistrations.findById(teamRegistration) return DataStore.shared.teamRegistrations.findById(teamRegistration)
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {

@ -86,6 +86,11 @@ class Tournament : ModelObject, Storable {
case build case build
} }
func courtUsed() -> [String] {
let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id })
return Set(runningMatches.compactMap { $0.court }).sorted()
}
func hasStarted() -> Bool { func hasStarted() -> Bool {
startDate <= Date() startDate <= Date()
} }
@ -138,11 +143,11 @@ class Tournament : ModelObject, Storable {
return seeds().filter { $0.isSeedable() } return seeds().filter { $0.isSeedable() }
} }
func lastSeedRound() -> Int? { func lastSeedRound() -> Int {
if let last = seeds().filter({ $0.bracketPosition != nil }).last { if let last = seeds().filter({ $0.bracketPosition != nil }).last {
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2) return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
} else { } else {
return nil return 0
} }
} }
@ -151,11 +156,11 @@ class Tournament : ModelObject, Storable {
} }
func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 0 } ?? [] getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 0 } ?? []
} }
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 1 } ?? [] getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? []
} }
func availableSeedGroups() -> [SeedInterval] { func availableSeedGroups() -> [SeedInterval] {
@ -178,13 +183,6 @@ class Tournament : ModelObject, Storable {
case 4...7: case 4...7:
return SeedInterval(first: 5, last: 8) return SeedInterval(first: 5, last: 8)
case 8...15: case 8...15:
// if 16 - 9 > availableSeeds().count {
// switch alreadySetupSeeds {
// case 8...15:
// return SeedInterval(first: 5, last: 8)
// case 8...15:
// return SeedInterval(first: 5, last: 8)
// }
return SeedInterval(first: 9, last: 16) return SeedInterval(first: 9, last: 16)
case 16...23: case 16...23:
return SeedInterval(first: 17, last: 24) return SeedInterval(first: 17, last: 24)
@ -215,7 +213,49 @@ class Tournament : ModelObject, Storable {
return availableSeeds return availableSeeds
} }
func seedGroupAvailable(atRoundIndex roundIndex: Int) -> SeedInterval? {
if let availableSeedGroup = availableSeedGroup() {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: availableSeedGroup)
} else {
return nil
}
}
func seedGroupAvailable(atRoundIndex roundIndex: Int, availableSeedGroup: SeedInterval) -> SeedInterval? {
if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() {
if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup }
let availableSeeds = seeds(inSeedGroup: availableSeedGroup)
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
if availableSeeds.count == availableSeedSpot.count {
return availableSeedGroup
} else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) {
return availableSeedGroup
} else if let chunk = availableSeedGroup.chunk() {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
}
}
return nil
}
func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) { func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) {
if seedGroup == SeedInterval(first: 1, last: 2) {
let seeds = seeds()
if let matches = getRound(atRoundIndex: roundIndex)?.playedMatches() {
if let lastMatch = matches.last {
seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false)
}
if let firstMatch = matches.first {
seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false)
}
}
} else {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup) let availableSeeds = seeds(inSeedGroup: seedGroup)
@ -223,18 +263,19 @@ class Tournament : ModelObject, Storable {
if availableSeeds.count <= availableSeedSpot.count { if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled() let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() { for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: false) seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
} }
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) { } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
let spots = availableSeedOpponentSpot.shuffled() let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() { for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: true) seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
} }
} else if let chunk = seedGroup.chunk() { } else if let chunk = seedGroup.chunk() {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
} }
} }
}
func inscriptionClosed() -> Bool { func inscriptionClosed() -> Bool {
@ -255,8 +296,12 @@ class Tournament : ModelObject, Storable {
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first
} }
func allRounds() -> [Round] {
Store.main.filter { $0.tournament == self.id }
}
func rounds() -> [Round] { func rounds() -> [Round] {
Store.main.filter { $0.tournament == self.id }.sorted(by: \.index).reversed() Store.main.filter { $0.tournament == self.id && $0.loser == nil }.sorted(by: \.index).reversed()
} }
func sortedTeams() -> [TeamRegistration] { func sortedTeams() -> [TeamRegistration] {
@ -461,11 +506,11 @@ class Tournament : ModelObject, Storable {
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
await unsortedPlayers().concurrentForEach { player in try await unsortedPlayers().concurrentForEach { player in
let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate }) let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate })
let sources = dataURLs.map { CSVParser(url: $0) } let sources = dataURLs.map { CSVParser(url: $0) }
try? await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0) try await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0)
} }
await MainActor.run { await MainActor.run {
@ -504,8 +549,12 @@ class Tournament : ModelObject, Storable {
} }
func availableQualifiedTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil })
}
func qualifiedTeams() -> [TeamRegistration] { func qualifiedTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.qualified() }) unsortedTeams().filter({ $0.qualifiedFromGroupStage() })
} }
func moreQualifiedToDraw() -> Int { func moreQualifiedToDraw() -> Int {
@ -517,16 +566,18 @@ class Tournament : ModelObject, Storable {
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
groupStage.teams()[qualifiedPerGroupStage] groupStage.teams()[qualifiedPerGroupStage]
} }
.filter({ $0.qualified() == false }) .filter({ $0.qualifiedFromGroupStage() == false })
} else { } else {
return [] return []
} }
} }
func groupStagesAreOver() -> Bool { func groupStagesAreOver() -> Bool {
guard groupStages().isEmpty == false else { let groupStages = groupStages()
guard groupStages.isEmpty == false else {
return true return true
} }
return groupStages.allSatisfy({ $0.hasEnded() })
return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
} }
@ -546,11 +597,11 @@ class Tournament : ModelObject, Storable {
let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }) let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false })
if ongoingGroupStages.isEmpty == false { if ongoingGroupStages.isEmpty == false {
return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours"
} }
return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix
} else { } else {
return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours"
} }
} }
@ -614,6 +665,10 @@ class Tournament : ModelObject, Storable {
}) })
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches) try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
self.rounds().forEach { round in
round.buildLoserBracket()
}
} }
func deleteStructure() { func deleteStructure() {

@ -7,6 +7,13 @@
import Foundation import Foundation
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
extension Sequence { extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in return sorted { a, b in
@ -23,16 +30,18 @@ extension Sequence {
extension Sequence { extension Sequence {
func concurrentForEach( func concurrentForEach(
_ operation: @escaping (Element) async -> Void _ operation: @escaping (Element) async throws -> Void
) async { ) async throws {
// A task group automatically waits for all of its // A task group automatically waits for all of its
// sub-tasks to complete, while also performing those // sub-tasks to complete, while also performing those
// tasks in parallel: // tasks in parallel:
await withTaskGroup(of: Void.self) { group in try await withThrowingTaskGroup(of: Void.self) { group in
for element in self { for element in self {
group.addTask { group.addTask {
await operation(element) try await operation(element)
} }
for try await _ in group {}
} }
} }
} }

@ -92,7 +92,7 @@ class NetworkFederalService {
} }
func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil) async -> [FederalTournament] { func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil) async throws -> [FederalTournament] {
if formId.isEmpty { if formId.isEmpty {
do { do {
@ -128,24 +128,28 @@ recherche_type=club&club[autocomplete][value_container][value_field]=\(codeClub.
request.httpMethod = "POST" request.httpMethod = "POST"
request.httpBody = postData request.httpBody = postData
do {
let commands : [HttpCommand] = try await runTenupTask(request: request) let commands : [HttpCommand] = try await runTenupTask(request: request)
if commands.anySatisfy({ $0.command == "alert" }) {
throw NetworkManagerError.maintenance
}
let resultCommand = commands.first(where: { $0.results != nil }) let resultCommand = commands.first(where: { $0.results != nil })
if let gatheredTournaments = resultCommand?.results?.items { if let gatheredTournaments = resultCommand?.results?.items {
var finalTournaments = tournaments + gatheredTournaments var finalTournaments = tournaments + gatheredTournaments
if let count = resultCommand?.results?.nb_results { if let count = resultCommand?.results?.nb_results {
if finalTournaments.count < count { if finalTournaments.count < count {
let newTournaments = await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub) let newTournaments = try await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub)
finalTournaments = finalTournaments + newTournaments finalTournaments = finalTournaments + newTournaments
} }
} }
return finalTournaments return finalTournaments
} }
} catch {
print("getClubFederalTournaments", error)
}
// do {
// } catch {
// print("getClubFederalTournaments", error)
// }
//
return [] return []
} }

@ -14,4 +14,13 @@ enum NetworkManagerError: LocalizedError {
case mailNotSent //no network no error case mailNotSent //no network no error
case messageFailed case messageFailed
case messageNotSent //no network no error case messageNotSent //no network no error
var errorDescription: String? {
switch self {
case .maintenance:
return "Le site de la FFT est en maintenance"
default:
return String(describing: self)
}
}
} }

@ -794,11 +794,11 @@ enum TournamentType: Int, Hashable, Codable, CaseIterable, Identifiable {
} }
} }
enum TeamData: Int, Hashable, Codable, CaseIterable { enum TeamPosition: Int, Hashable, Codable, CaseIterable {
case one case one
case two case two
var otherTeam: TeamData { var otherTeam: TeamPosition {
switch self { switch self {
case .one: case .one:
return .two return .two
@ -841,7 +841,7 @@ enum SetFormat: Int, Hashable, Codable {
} }
} }
func winner(teamOne: Int, teamTwo: Int) -> TeamData { func winner(teamOne: Int, teamTwo: Int) -> TeamPosition {
return teamOne >= teamTwo ? .one : .two return teamOne >= teamTwo ? .one : .two
} }
@ -972,6 +972,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
self.init(rawValue: value) self.init(rawValue: value)
} }
func defaultWalkOutScore(_ asWalkOutTeam: Bool) -> [Int] {
Array(repeating: asWalkOutTeam ? 0 : setFormat.scoreToWin, count: setsToWin)
}
var weight: Int { var weight: Int {
switch self { switch self {
case .twoSets, .twoSetsDecisivePoint: case .twoSets, .twoSetsDecisivePoint:
@ -1025,7 +1029,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
[.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie] [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie]
} }
func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamData { func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition {
scoreTeamOne >= scoreTeamTwo ? .one : .two scoreTeamOne >= scoreTeamTwo ? .one : .two
} }

@ -78,7 +78,7 @@ class SourceFileManager {
allFiles.contains(where: { $0.dateFromPath == date }) == false allFiles.contains(where: { $0.dateFromPath == date }) == false
} }
await dates.concurrentForEach { date in try? await dates.concurrentForEach { date in
await self.fetchData(fromDate: date) await self.fetchData(fromDate: date)
} }
} }

@ -12,10 +12,12 @@ import TipKit
@main @main
struct PadelClubApp: App { struct PadelClubApp: App {
let persistenceController = PersistenceController.shared let persistenceController = PersistenceController.shared
@State private var navigationViewModel = NavigationViewModel()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MainView() MainView()
.environment(navigationViewModel)
.accentColor(.launchScreenBackground) .accentColor(.launchScreenBackground)
.onAppear { .onAppear {
self._onAppear() self._onAppear()

@ -0,0 +1,101 @@
//
// MatchDescriptor.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import Foundation
class MatchDescriptor: ObservableObject {
@Published var matchFormat: MatchFormat
@Published var setDescriptors: [SetDescriptor]
var court: Int = 1
var title: String = "Titre du match"
var teamLabelOne: String = ""
var teamLabelTwo: String = ""
var startDate: Date = Date()
var match: Match?
init(match: Match? = nil) {
self.match = match
if let groupStage = match?.groupStageObject {
self.matchFormat = groupStage.matchFormat
self.setDescriptors = [SetDescriptor(setFormat: groupStage.matchFormat.setFormat)]
} else {
let format = match?.matchFormat ?? match?.currentTournament()?.matchFormat ?? .defaultFormatForMatchType(.groupStage)
self.matchFormat = format
self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)]
}
let teamOne = match?.team(.one)
let teamTwo = match?.team(.two)
self.teamLabelOne = teamOne?.teamLabel() ?? ""
self.teamLabelTwo = teamTwo?.teamLabel() ?? ""
if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score {
self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in
SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat)
})
}
}
var teamOneScores: [String] {
setDescriptors.compactMap { $0.valueTeamOne }.map { "\($0)" }
}
var teamTwoScores: [String] {
setDescriptors.compactMap { $0.valueTeamTwo }.map { "\($0)" }
}
var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count }
var scoreTeamTwo: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .two }.count }
var hasEnded: Bool {
return matchFormat.hasEnded(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo)
}
func addNewSet() {
if hasEnded == false {
setDescriptors.append(SetDescriptor(setFormat: matchFormat.newSetFormat(setCount: setDescriptors.count)))
}
}
var winner: TeamPosition {
matchFormat.winner(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo)
}
var winnerLabel: String {
if winner == .one {
return teamLabelOne
} else {
return teamLabelTwo
}
}
}
fileprivate func combineArraysIntoTuples(_ array1: [String], _ array2: [String]) -> [(String?, String?)] {
// Zip the two arrays together and map them to tuples of optional strings
let combined = zip(array1, array2).map { (element1, element2) in
return (element1, element2)
}
// If one array is longer than the other, append the remaining elements
let remainingElements: [(String?, String?)]
if array1.count > array2.count {
let remaining = Array(array1[array2.count...]).map { (element) in
return (element, nil as String?)
}
remainingElements = remaining
} else if array2.count > array1.count {
let remaining = Array(array2[array1.count...]).map { (element) in
return (nil as String?, element)
}
remainingElements = remaining
} else {
remainingElements = []
}
// Concatenate the two arrays
return combined + remainingElements
}

@ -0,0 +1,14 @@
//
// NavigationViewModel.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/04/2024.
//
import SwiftUI
@Observable
class NavigationViewModel {
var agendaDestination: AgendaDestination? = .activity
var tournament: Tournament?
}

@ -16,12 +16,13 @@ struct SeedInterval: Hashable, Comparable {
} }
func chunk() -> SeedInterval? { func chunk() -> SeedInterval? {
if (last - first) / 2 > 0 {
if last - (last - first) / 2 > first { if last - (last - first) / 2 > first {
return SeedInterval(first: first, last: last - (last - first) / 2) return SeedInterval(first: first, last: last - (last - first) / 2)
} else {
return nil
} }
} }
return nil
}
} }
extension SeedInterval { extension SeedInterval {

@ -0,0 +1,33 @@
//
// SetDescriptor.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import Foundation
struct SetDescriptor: Identifiable, Equatable {
let id: UUID = UUID()
var valueTeamOne: Int?
var valueTeamTwo: Int?
var tieBreakValueTeamOne: Int?
var tieBreakValueTeamTwo: Int?
var setFormat: SetFormat
var hasEnded: Bool {
if let valueTeamTwo, let valueTeamOne {
return setFormat.hasEnded(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else {
return false
}
}
var winner: TeamPosition? {
if let valueTeamTwo, let valueTeamOne {
return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else {
return nil
}
}
}

@ -130,7 +130,7 @@ struct ClubSearchView: View {
} description: { } description: {
Text("Une erreur est survenue lors de la récupération de votre localisation.") Text("Une erreur est survenue lors de la récupération de votre localisation.")
} actions: { } actions: {
RowButtonView(title: "D'accord") { RowButtonView("D'accord") {
locationManager.lastError = nil locationManager.lastError = nil
} }
} }
@ -147,7 +147,7 @@ struct ClubSearchView: View {
Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
} actions: { } actions: {
if locationManager.manager.authorizationStatus != .restricted { if locationManager.manager.authorizationStatus != .restricted {
RowButtonView(title: "Chercher autour de moi") { RowButtonView("Chercher autour de moi") {
if locationManager.manager.authorizationStatus == .notDetermined { if locationManager.manager.authorizationStatus == .notDetermined {
locationManager.manager.requestWhenInUseAuthorization() locationManager.manager.requestWhenInUseAuthorization()
} else if locationManager.manager.authorizationStatus == .denied { } else if locationManager.manager.authorizationStatus == .denied {
@ -157,7 +157,7 @@ struct ClubSearchView: View {
} }
} }
} }
RowButtonView(title: "Chercher une ville ou un code postal") { RowButtonView("Chercher une ville ou un code postal") {
searchPresented = true searchPresented = true
} }
} }

@ -60,10 +60,10 @@ struct ClubsView: View {
} description: { } description: {
Text("Texte décrivant l'utilité d'un club et les features que cela apporte") Text("Texte décrivant l'utilité d'un club et les features que cela apporte")
} actions: { } actions: {
RowButtonView(title: "Créer un nouveau club", systemImage: "plus.circle.fill") { RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") {
presentClubCreationView = true presentClubCreationView = true
} }
RowButtonView(title: "Chercher un club", systemImage: "magnifyingglass.circle.fill") { RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") {
presentClubSearchView = true presentClubSearchView = true
} }
} }

@ -26,7 +26,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
.background { .background {
Circle() Circle()
.fill(Color.white) .fill(Color.white)
.opacity(selectedDestination == nil ? 1.0 : 0.5) .opacity(selectedDestination == nil ? 1.0 : 0.4)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -41,7 +41,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
.background { .background {
Capsule() Capsule()
.fill(Color.white) .fill(Color.white)
.opacity(selectedDestination?.id == destination.id ? 1.0 : 0.5) .opacity(selectedDestination?.id == destination.id ? 1.0 : 0.4)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {

@ -0,0 +1,45 @@
//
// MatchListView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/04/2024.
//
import SwiftUI
struct MatchListView: View {
@EnvironmentObject var dataStore: DataStore
let section: String
let matches: [Match]
var matchViewStyle: MatchViewStyle = .sectionedStandardStyle
@State private var isExpanded: Bool = true
@ViewBuilder
var body: some View {
if matches.isEmpty == false {
Section {
if isExpanded {
ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: matchViewStyle)
}
}
} header: {
Button {
isExpanded.toggle()
} label: {
HStack {
Text(section.capitalized)
Spacer()
Text(matches.count.formatted())
Image(systemName: isExpanded ? "chevron.down.circle" : "chevron.right.circle")
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity)
}
.headerProminence(.increased)
}
}
}

@ -7,16 +7,35 @@
import SwiftUI import SwiftUI
fileprivate let defaultConfirmationMessage = "Êtes-vous sûr de vouloir faire cela ?"
struct RowButtonView: View { struct RowButtonView: View {
var role: ButtonRole? = nil
let title: String let title: String
var systemImage: String? = nil var systemImage: String? = nil
var image: String? = nil var image: String? = nil
var animatedProgress: Bool = false var animatedProgress: Bool = false
let confirmationMessage: String
let action: () -> () let action: () -> ()
@State private var askConfirmation: Bool = false
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, animatedProgress: Bool = false, confirmationMessage: String? = nil, action: @escaping () -> Void) {
self.role = role
self.title = title
self.systemImage = systemImage
self.image = image
self.animatedProgress = animatedProgress
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.action = action
}
var body: some View { var body: some View {
Button { Button(role: role) {
if role == .destructive {
askConfirmation = true
} else {
action() action()
}
} label: { } label: {
HStack { HStack {
if animatedProgress { if animatedProgress {
@ -47,8 +66,19 @@ struct RowButtonView: View {
.disabled(animatedProgress) .disabled(animatedProgress)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(.launchScreenBackground) .tint(role == .destructive ? Color.red : Color.launchScreenBackground)
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(.zero)) .listRowInsets(EdgeInsets(.zero))
.confirmationDialog("Confirmation",
isPresented: $askConfirmation,
titleVisibility: .visible) {
Button("OK") {
action()
}
Button("Annuler", role: .cancel) {}
} message: {
Text(confirmationMessage)
}
} }
} }

@ -88,7 +88,7 @@ struct EventCreationView: View {
} }
Section { Section {
RowButtonView(title:"Valider") { RowButtonView("Valider") {
if tournaments.count > 1 || eventName.trimmed.isEmpty == false || selectedClub != nil { if tournaments.count > 1 || eventName.trimmed.isEmpty == false || selectedClub != nil {
let event = Event(name: eventName) let event = Event(name: eventName)
event.club = selectedClub?.id event.club = selectedClub?.id
@ -143,7 +143,7 @@ struct EventCreationView: View {
} }
Section { Section {
RowButtonView(title: "Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") { RowButtonView("Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") {
let tournament = Tournament.newEmptyInstance() let tournament = Tournament.newEmptyInstance()
self.tournaments.append(tournament) self.tournaments.append(tournament)
} }

@ -38,7 +38,7 @@ struct GroupStageSettingsView: View {
// if (tournament.groupStagesAreWrong || (tournament.emptySlotInGroupStages > 0 && tournament.entriesCount >= tournament.teamsFromGroupStages)) { // if (tournament.groupStagesAreWrong || (tournament.emptySlotInGroupStages > 0 && tournament.entriesCount >= tournament.teamsFromGroupStages)) {
// Section { // Section {
// RowButtonView(title: "Reconstruire les poules") { // RowButtonView("Reconstruire les poules") {
// confirmGroupStageRebuild = true // confirmGroupStageRebuild = true
// } // }
// .modify { // .modify {
@ -83,7 +83,7 @@ struct GroupStageSettingsView: View {
// if tournament.isRoundSwissTournament() == false && (tournament.orderedGroupStages.allSatisfy({ $0.orderedMatches.count > 0 }) == false && tournament.groupStagesAreOver == false && tournament.groupStagesCount > 0) { // if tournament.isRoundSwissTournament() == false && (tournament.orderedGroupStages.allSatisfy({ $0.orderedMatches.count > 0 }) == false && tournament.groupStagesAreOver == false && tournament.groupStagesCount > 0) {
// Section { // Section {
// RowButtonView(title: "Générer les matchs de poules") { // RowButtonView("Générer les matchs de poules") {
// startAllGroupStageConfirmation = true // startAllGroupStageConfirmation = true
// } // }
// .modify { // .modify {

@ -0,0 +1,107 @@
//
// GroupStageTeamView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/04/2024.
//
import SwiftUI
struct GroupStageTeamView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
let groupStage: GroupStage
var team: TeamRegistration
var body: some View {
List {
ForEach(team.players()) { player in
Section {
ImportedPlayerView(player: player)
} footer: {
HStack {
Button("contacter") {
}
Spacer()
Button {
player.hasArrived.toggle()
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
} label: {
Label("présent", systemImage: player.hasArrived ? "checkmark.circle" : "circle")
}
}
}
}
if groupStage.tournamentObject()?.hasEnded() == false {
Section {
if team.qualified == false {
RowButtonView("Qualifier l'équipe") {
team.qualified = true
team.bracketPosition = nil
_save()
}
}
}
Section {
if team.qualified {
RowButtonView("Annuler la qualification", role: .destructive) {
team.qualified = false
team.bracketPosition = nil
_save()
}
}
}
Section {
// if let deltaLabel = bracket.tournament?.deltaLabel(index, bracketIndex: bracket.index.intValue) {
// Section {
// Button(role: .destructive) {
// entrant.resetBracketPosition()
// save()
// } label: {
// Text(deltaLabel)
// }
// Divider()
// } header: {
// Text("Toute l'équipe, poids: " + entrant.updatedRank.formatted())
// }
// }
//
// ForEach(entrant.orderedPlayers) { player in
// if let deltaLabel = bracket.tournament?.playerDeltaLabel(rank: entrant.otherPlayer(player)?.currentRank, positionInBracket: index, bracketIndex: bracket.index.intValue) {
// Section {
// Button(role: .destructive) {
// entrant.team?.removeFromPlayers(player)
// save()
// } label: {
// Text(deltaLabel)
// }
// Divider()
// } header: {
// Text(player.longLabel + ", rang: " + player.formattedRank)
// }
// }
// }
} header: {
Text("Remplacement")
}
Section {
RowButtonView("Retirer de la poule") {
team.groupStagePosition = nil
team.groupStage = nil
_save()
}
}
}
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Détail de l'équipe")
}
private func _save() {
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
dismiss()
}
}

@ -8,77 +8,121 @@
import SwiftUI import SwiftUI
struct GroupStageView: View { struct GroupStageView: View {
@EnvironmentObject var dataStore: DataStore
@Bindable var groupStage: GroupStage @Bindable var groupStage: GroupStage
// @State private var selectedMenuLink: MenuLink?
// @State private var canUpdateTournament: Bool = false
// @AppStorage("showLongLabel") private var showLongLabel: Bool = false
// @AppStorage("hideRank") private var hideRank: Bool = false
@State private var confirmGroupStageStart: Bool = false @State private var confirmGroupStageStart: Bool = false
@State private var sortingMode: GroupStageSortingMode = .auto
@State private var confirmRemoveAll: Bool = false
@State private var confirmResetMatch: Bool = false
enum MenuLink: Int, Identifiable, Hashable { private enum GroupStageSortingMode {
var id: Int { self.rawValue } case auto
case prepare case score
case weight
} }
var groupStageView: some View { var sortByScore: Bool {
ForEach(0..<(groupStage.size), id: \.self) { index in sortingMode == .auto ? groupStage.hasEnded() : sortingMode == .score
// let entrant : Entrant? = runningGroupStageOrderedByScore ? groupStage.orderedByScore[Int(index)] : groupStage.entrantAtIndex(Int(index)) }
if let team = groupStage.teamsAt(index) {
Text(team.teamLabel()) func teamAt(atIndex index: Int) -> TeamRegistration? {
// GroupStageEntrantMenuView(entrant: entrant, groupStage: groupStage, index: index.intValue) sortByScore ? groupStage.teams(sortByScore)[safe: index] : groupStage.teamAt(groupStagePosition: index)
}
var body: some View {
List {
Section {
_groupStageView()
} header: {
HStack {
if let startDate = groupStage.startDate {
Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute()))
}
Spacer()
Button {
if sortingMode == .weight {
sortingMode = .score
} else { } else {
Menu { sortingMode = .weight
// Section { }
// EntrantPickerView(groupStage: groupStage, index: Int(index))
// }
//
// if let tournament = groupStage.tournament, let deltaLabel = tournament.deltaLabel(index.intValue, groupStageIndex: groupStage.index.intValue) {
// let date = tournament.localizedDate ?? ""
// Divider()
// Section {
// ShareLink(item: "\(tournament.localizedTitle)\n\(date)\nCherche une équipe dont le poids d'équipe " + deltaLabel) {
// Text(deltaLabel)
// }
// } header: {
// Text("Remplacer avec un poids d'équipe")
// }
// }
} label: { } label: {
HStack { Label(sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly)
Text("#\(index+1)") }
Text("Aucune équipe")
} }
.buttonStyle(.plain)
} }
MatchListView(section: "disponible", matches: groupStage.availableToStart()).id(UUID())
MatchListView(section: "en cours", matches: groupStage.runningMatches()).id(UUID())
MatchListView(section: "à lancer", matches: groupStage.readyMatches()).id(UUID())
MatchListView(section: "terminés", matches: groupStage.finishedMatches()).id(UUID())
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
_groupStageMenuView()
} }
} }
} }
var body: some View { private func _groupStageView() -> some View {
List { ForEach(0..<(groupStage.size), id: \.self) { index in
Section { if let team = teamAt(atIndex: index), let groupStagePosition = team.groupStagePosition {
groupStageView NavigationLink {
// .disabled(canUpdateTournament == false) GroupStageTeamView(groupStage: groupStage, team: team)
// .sheet(item: $selectedMenuLink) { selectedMenuLink in } label: {
// switch selectedMenuLink { HStack(alignment: .center) {
// case .prepare: VStack(alignment: .leading, spacing: 0) {
// PrepareGroupStageView(groupStage: groupStage)
// }
// }
} header: {
HStack { HStack {
if groupStage.isBroadcasted() { Text("#\(groupStagePosition + 1)")
Label(groupStage.groupStageTitle(), systemImage: "airplayvideo") Text("Poids \(team.weight)")
}
.font(.caption)
HStack {
if let teamName = team.name {
Text(teamName)
} else { } else {
Text(groupStage.groupStageTitle()) VStack(alignment: .leading) {
ForEach(team.players()) { player in
Text(player.playerLabel())
}
}
}
if team.qualified {
Image(systemName: "checkmark.seal")
}
}
} }
Spacer() Spacer()
if let startDate = groupStage.startDate { if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition) {
Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) Text(score)
}
} }
} }
} footer: { } else {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 0) {
HStack { HStack {
if groupStage.matches.isEmpty { Text("#\(index + 1)")
}
.font(.caption)
TeamPickerView(teamPicked: { team in
print(team.pasteData())
team.groupStage = groupStage.id
team.groupStagePosition = index
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})
}
}
}
}
}
private func _groupStageMenuView() -> some View {
Menu {
if groupStage.matches().isEmpty {
Button { Button {
//groupStage.startGroupStage() //groupStage.startGroupStage()
//save() //save()
@ -87,8 +131,14 @@ struct GroupStageView: View {
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
Spacer()
Menu { Button("Retirer tout le monde", role: .destructive) {
confirmRemoveAll = true
}
Button("Recommencer tous les matchs", role: .destructive) {
confirmResetMatch = true
}
// Button { // Button {
// selectedMenuLink = .prepare // selectedMenuLink = .prepare
// } label: { // } label: {
@ -184,34 +234,25 @@ struct GroupStageView: View {
// Text("Éditer") // Text("Éditer")
// } // }
} label: { } label: {
HStack { LabelOptions()
Spacer()
Label("Options", systemImage: "ellipsis.circle").labelStyle(.titleOnly)
}
} }
.buttonStyle(.borderless) .confirmationDialog("Êtes-vous sûr de vouloir faire cela ?", isPresented: $confirmRemoveAll, titleVisibility: .visible) {
Button("Oui") {
let teams = groupStage.teams()
teams.forEach { team in
team.groupStagePosition = nil
team.groupStage = nil
} }
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} }
if groupStage.matches.isEmpty == false {
Section {
ForEach(groupStage.matches) { match in
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
} }
} header: { .confirmationDialog("Êtes-vous sûr de vouloir faire cela ?", isPresented: $confirmResetMatch, titleVisibility: .visible) {
Text("Matchs de la " + groupStage.groupStageTitle()) Button("Oui") {
groupStage.buildMatches()
} }
} }
} }
// .onAppear {
// if let tournament = groupStage.tournament {
// canUpdateTournament = PersistenceController.shared.container.canUpdateRecord(forManagedObjectWith: tournament.objectID)
// } else {
// canUpdateTournament = true
// }
// }
}
// func save() { // func save() {
// do { // do {
@ -228,163 +269,3 @@ struct GroupStageView: View {
// } // }
// } // }
} }
//struct GroupStageEntrantMenuView: View {
// @ObservedObject var entrant: Entrant
// @ObservedObject var groupStage: GroupStage
// @Environment(\.managedObjectContext) private var viewContext
// @AppStorage("showLongLabel") private var showLongLabel: Bool = false
// @AppStorage("hideRank") private var hideRank: Bool = false
//
// let index: Int
//
// var body: some View {
// Menu {
// ForEach(entrant.orderedPlayers) { player in
// Menu {
// Text(player.formattedRank)
// Text(player.localizedAge)
// if let computedClubName = player.computedClubName {
// Text(computedClubName)
// }
// } label: {
// Text(player.longLabel)
// }
// }
//
// if groupStage.tournament?.isOver == false {
// if entrant.qualified == false {
// Divider()
// Button {
// entrant.addToQualifiedGroup()
// entrant.objectWillChange.send()
// entrant.orderedGroupStages.forEach { $0.objectWillChange.send() }
// entrant.currentTournament?.objectWillChange.send()
// entrant.currentTournament?.orderedMatches.forEach { $0.objectWillChange.send() }
// save()
// } label: {
// Label("Qualifier l'équipe", systemImage: "checkmark")
// }
// }
//
// Divider()
// if entrant.qualified {
// Menu {
// Button(role: .destructive) {
// entrant.unqualified()
// entrant.objectWillChange.send()
// entrant.orderedGroupStages.forEach { $0.objectWillChange.send() }
// entrant.currentTournament?.objectWillChange.send()
// entrant.currentTournament?.orderedMatches.forEach { $0.objectWillChange.send() }
// save()
// } label: {
// Label("Annuler la qualification", systemImage: "xmark")
// }
// } label: {
// Text("Qualification")
// }
// }
//
// Menu {
// if let deltaLabel = groupStage.tournament?.deltaLabel(index, groupStageIndex: groupStage.index.intValue) {
// Section {
// Button(role: .destructive) {
// entrant.resetGroupStagePosition()
// save()
// } label: {
// Text(deltaLabel)
// }
// Divider()
// } header: {
// Text("Toute l'équipe, poids: " + entrant.updatedRank.formatted())
// }
// }
//
// ForEach(entrant.orderedPlayers) { player in
// if let deltaLabel = groupStage.tournament?.playerDeltaLabel(rank: entrant.otherPlayer(player)?.currentRank, positionInGroupStage: index, groupStageIndex: groupStage.index.intValue) {
// Section {
// Button(role: .destructive) {
// entrant.team?.removeFromPlayers(player)
// save()
// } label: {
// Text(deltaLabel)
// }
// Divider()
// } header: {
// Text(player.longLabel + ", rang: " + player.formattedRank)
// }
// }
// }
// } label: {
// Text("Remplacement")
// }
//
// Menu {
// Button(role: .destructive) {
// entrant.resetGroupStagePosition()
// save()
// } label: {
// Label("Retirer l'équipe", systemImage: "xmark")
// }
// } label: {
// Text("Retirer")
// }
//
// }
// } label: {
// HStack(alignment: .center) {
// if let tournament = groupStage.tournament, groupStage.hasEnded, groupStage.groupStageRound > 0 {
// Text("#\(index + Int((groupStage.index - tournament.numberOfGroupStages)*tournament.teamsPerGroupStage) + 1)")
// } else {
// Text("#\(index + 1)")
// }
// VStack(alignment: .leading, spacing: 0) {
// if hideRank == false {
// Text("Poids \(entrant.updatedRank)")
// .font(.caption)
// }
//
// HStack {
// if let brand = entrant.team?.brand?.title {
// Text(brand)
// } else {
//
// VStack(alignment: .leading) {
// Text(entrant.longLabelPlayerOne)
// Text(entrant.longLabelPlayerTwo)
// }
//
// }
//
// if groupStage.tournament?.isRoundSwissTournament() == true {
// if entrant.groupStagePosition == groupStage.index {
// Text("forcé")
// } else {
// Text("auto")
// }
// } else {
// if entrant.qualified {
// Image(systemName: "checkmark.seal")
// }
// }
// }
// }
// Spacer()
// Text(groupStage.scoreLabel(for: entrant.position(in: groupStage)))
// }
// }
// .buttonStyle(.plain)
// }
//
// func save() {
// do {
// try viewContext.save()
// } catch {
// // Replace this implementation with code to handle the error appropriately.
// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
// let nsError = error as NSError
// fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
// }
// }
//}

@ -9,23 +9,78 @@ import SwiftUI
struct GroupStagesView: View { struct GroupStagesView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedGroupStage: GroupStage? @State private var selectedDestination: GroupStageDestination?
enum GroupStageDestination: Selectable, Identifiable {
case all
case groupStage(GroupStage)
var id: String {
switch self {
case .all:
return "all-group-stage"
case .groupStage(let groupStage):
return groupStage.id
}
}
func selectionLabel() -> String {
switch self {
case .all:
return "Tout"
case .groupStage(let groupStage):
return groupStage.groupStageTitle()
}
}
func badgeValue() -> Int? {
switch self {
case .all:
return nil
case .groupStage(let groupStage):
return groupStage.badgeValue()
}
}
}
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_selectedGroupStage = State(wrappedValue: tournament.getActiveGroupStage()) let gs = tournament.getActiveGroupStage()
if let gs {
_selectedDestination = State(wrappedValue: .groupStage(gs))
}
}
func allDestinations() -> [GroupStageDestination] {
var allDestinations : [GroupStageDestination] = [.all]
let groupStageDestinations : [GroupStageDestination] = tournament.groupStages().map { GroupStageDestination.groupStage($0) }
allDestinations.append(contentsOf: groupStageDestinations)
return allDestinations
} }
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedGroupStage, destinations: tournament.groupStages(), nilDestinationIsValid: true) GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedGroupStage { switch selectedDestination {
case .none: case .all:
GroupStageSettingsView() List {
.navigationTitle("Réglages") let allGroupStages = tournament.groupStages()
case .some(let groupStage): let availableToStart = allGroupStages.flatMap({ $0.availableToStart() })
let runningMatches = allGroupStages.flatMap({ $0.runningMatches() })
let readyMatches = allGroupStages.flatMap({ $0.readyMatches() })
let finishedMatches = allGroupStages.flatMap({ $0.finishedMatches() })
MatchListView(section: "disponible", matches: availableToStart, matchViewStyle: .standardStyle)
MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle)
MatchListView(section: "à lancer", matches: readyMatches, matchViewStyle: .standardStyle)
MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle)
}
.navigationTitle("Toutes les poules")
case .groupStage(let groupStage):
GroupStageView(groupStage: groupStage) GroupStageView(groupStage: groupStage)
.navigationTitle(groupStage.groupStageTitle()) .navigationTitle(groupStage.groupStageTitle())
case nil:
GroupStageSettingsView()
.navigationTitle("Réglages")
} }
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)

@ -83,7 +83,7 @@ struct MatchDateView: View {
.monospacedDigit() .monospacedDigit()
} }
if match.startDate == nil { if match.startDate == nil && match.hasEnded() == false {
Text("démarrage").font(.footnote).foregroundStyle(.secondary) Text("démarrage").font(.footnote).foregroundStyle(.secondary)
Text("non défini") Text("non défini")
} }

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
struct MatchDetailView: View { struct MatchDetailView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
let matchViewStyle: MatchViewStyle let matchViewStyle: MatchViewStyle
@ -102,15 +103,15 @@ struct MatchDetailView: View {
if match.hasEnded() == false { if match.hasEnded() == false {
Menu { Menu {
Button("Non défini") { Button("Non défini") {
match.court = nil match.removeCourt()
save() save()
} }
// ForEach(1...match.numberOfField, id: \.self) { courtIndex in ForEach(1...match.courtCount(), id: \.self) { courtIndex in
// Button("Terrain #\(courtIndex.formatted())") { Button("Terrain #\(courtIndex.formatted())") {
// match.fieldIndex = Int64(courtIndex) match.setCourt(courtIndex)
// save() save()
// } }
// } }
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("terrain").font(.footnote).foregroundStyle(.secondary) Text("terrain").font(.footnote).foregroundStyle(.secondary)
@ -202,11 +203,14 @@ struct MatchDetailView: View {
// } // }
// .presentationDetents([.fraction(0.66)]) // .presentationDetents([.fraction(0.66)])
// } // }
// .sheet(item: $scoreType, onDismiss: { .sheet(item: $scoreType, onDismiss: {
// if match.hasEnded() && match.isTournamentMatch() { if match.hasEnded() && match.isTournamentMatch() {
// dismiss() dismiss()
// } }
// }) { scoreType in }) { scoreType in
let matchDescriptor = MatchDescriptor(match: match)
EditScoreView(matchDescriptor: matchDescriptor)
// switch scoreType { // switch scoreType {
// case .edition: // case .edition:
// let matchDescriptor = MatchDescriptor(match: match) // let matchDescriptor = MatchDescriptor(match: match)
@ -238,8 +242,8 @@ struct MatchDetailView: View {
// FeedbackView(feedbackData: feedbackData) // FeedbackView(feedbackData: feedbackData)
// } // }
// } // }
//
// } }
// .refreshable { // .refreshable {
// if match.isBroadcasted() { // if match.isBroadcasted() {
@ -308,24 +312,6 @@ struct MatchDetailView: View {
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
} }
enum MatchDateSetup: Hashable, Identifiable {
case inMinutes(Int)
case now
case customDate
var id: Int { hashValue }
}
enum MatchFieldSetup: Hashable, Identifiable {
case random
// case firstAvailable
case field(String)
var id: Int { hashValue }
}
enum ScoreType: Int, Identifiable, Hashable { enum ScoreType: Int, Identifiable, Hashable {
var id: Int { var id: Int {
self.rawValue self.rawValue
@ -338,14 +324,6 @@ struct MatchDetailView: View {
case health = 5 case health = 5
} }
var entrantLabelOne: String {
return "match.longLabelTeamOne"
}
var entrantLabelTwo: String {
return "match.longLabelTeamTwo"
}
@ViewBuilder @ViewBuilder
var menuView: some View { var menuView: some View {
if match.isReady() { if match.isReady() {
@ -374,7 +352,7 @@ struct MatchDetailView: View {
} }
var inputScoreView: some View { var inputScoreView: some View {
RowButtonView(title: "Saisir les résultats", systemImage: "list.clipboard") { RowButtonView("Saisir les résultats", systemImage: "list.clipboard") {
scoreType = .edition scoreType = .edition
} }
} }
@ -399,9 +377,10 @@ struct MatchDetailView: View {
if match.isReady() { if match.isReady() {
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15))
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration))
Text("Tout de suite").tag(MatchDateSetup.now) Text("Tout de suite").tag(MatchDateSetup.now)
} }
Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-match.matchFormat.estimatedDuration))
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration))
Text("À").tag(MatchDateSetup.customDate) Text("À").tag(MatchDateSetup.customDate)
} label: { } label: {
Text("Horaire") Text("Horaire")
@ -436,11 +415,10 @@ struct MatchDetailView: View {
Picker(selection: $fieldSetup) { Picker(selection: $fieldSetup) {
Text("Au hasard").tag(MatchFieldSetup.random) Text("Au hasard").tag(MatchFieldSetup.random)
//Text("Premier disponible").tag(MatchFieldSetup.firstAvailable) //Text("Premier disponible").tag(MatchFieldSetup.firstAvailable)
// ForEach(1...match.numberOfField, id: \.self) { courtIndex in ForEach(1...match.courtCount(), id: \.self) { courtIndex in
// let fieldIndex = Int64(courtIndex) Text("Terrain #\(courtIndex)")
// let fieldIsAvailable : Bool = match.currentTournament?.fieldIsAvailable(fieldIndex) ?? true .tag(MatchFieldSetup.field(String(courtIndex)))
// Label("Terrain #\(courtIndex)", systemImage: match.isFieldPreferred(fieldIndex) ? "heart" : "").tag(MatchFieldSetup.field(courtIndex)) }
// }
} label: { } label: {
Text("Choix du terrain") Text("Choix du terrain")
} }
@ -461,26 +439,8 @@ struct MatchDetailView: View {
// } // }
// } // }
RowButtonView(title: "Valider") { RowButtonView("Valider") {
if match.hasEnded() == false { match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup)
match.startDate = startDate
if match.isTournamentMatch() {
// switch fieldSetup {
// case .random:
// let field = match.freeFields.randomElement() ?? match.currentTournament?.freeFields.randomElement() ?? 1
// match.setupFieldAndStartDateIfPossible(field)
// case .field(let courtIndex):
// let fieldIndex = Int64(courtIndex)
// match.setupFieldAndStartDateIfPossible(fieldIndex)
// }
}
} else {
match.startDate = startDate
if match.endDate != nil {
match.endDate = endDate
}
}
if broadcasted { if broadcasted {
broadcastAndSave() broadcastAndSave()
@ -501,12 +461,12 @@ struct MatchDetailView: View {
var broadcastView: some View { var broadcastView: some View {
Section { Section {
// if match.isBroadcasted() { // if match.isBroadcasted() {
// RowButtonView(title: "Arrêter de diffuser") { // RowButtonView("Arrêter de diffuser") {
// match.stopBroadcast() // match.stopBroadcast()
// save() // save()
// } // }
// } else if match.canBroadcast() == true { // } else if match.canBroadcast() == true {
// RowButtonView(title: "Diffuser", systemImage: "airplayvideo") { // RowButtonView("Diffuser", systemImage: "airplayvideo") {
// broadcastAndSave() // broadcastAndSave()
// } // }
// } // }
@ -523,6 +483,7 @@ struct MatchDetailView: View {
private func save() { private func save() {
try? dataStore.matches.addOrUpdate(instance: match)
} }
private func broadcastAndSave() { private func broadcastAndSave() {

@ -10,11 +10,11 @@ import SwiftUI
struct MatchRowView: View { struct MatchRowView: View {
var match: Match var match: Match
let matchViewStyle: MatchViewStyle let matchViewStyle: MatchViewStyle
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(\.editMode) private var editMode
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if isEditingTournamentSeed && match.isGroupStage() == false { if editMode?.wrappedValue.isEditing == true && match.isGroupStage() == false && match.isLoserBracket == false {
MatchSetupView(match: match) MatchSetupView(match: match)
} else { } else {
NavigationLink { NavigationLink {

@ -14,37 +14,72 @@ struct MatchSetupView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
_teamView(match.team(.one), teamPosition: 0) _teamView(inTeamPosition: .one)
_teamView(match.team(.two), teamPosition: 1) _teamView(inTeamPosition: .two)
} }
@ViewBuilder @ViewBuilder
func _teamView(_ team: TeamRegistration?, teamPosition: Int) -> some View { func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View {
if let team { let team = match.team(teamPosition)
let teamScore = match.teamScore(ofTeam: team)
if let team, teamScore?.walkOut == nil {
VStack(alignment: .leading, spacing: 0) {
if let teamScore, teamScore.luckyLoser != nil {
Text("Repêchée").italic().font(.caption)
}
TeamRowView(team: team, teamPosition: teamPosition) TeamRowView(team: team, teamPosition: teamPosition)
.swipeActions(edge: .trailing, allowsFullSwipe: false) { .swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .cancel) { Button(role: .cancel) {
if match.isSeededBy(team: team, inTeamPosition: teamPosition) {
team.bracketPosition = nil team.bracketPosition = nil
match.enableMatch()
try? dataStore.teamRegistrations.addOrUpdate(instance: team) try? dataStore.teamRegistrations.addOrUpdate(instance: team)
} else {
match.teamWillBeWalkOut(team)
try? dataStore.matches.addOrUpdate(instance: match)
}
} label: { } label: {
Label("retirer", systemImage: "xmark") Label("retirer", systemImage: "xmark")
} }
} }
}
} else { } else {
VStack(alignment: .leading) {
if let team {
TeamRowView(team: team, teamPosition: teamPosition)
.strikethrough()
}
HStack { HStack {
TeamPickerView(teamPicked: { team in let walkOutSpot = match.isWalkOutSpot(teamPosition)
let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
team.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) if walkOutSpot {
match.setLuckyLoser(team: team, teamPosition: teamPosition)
try? dataStore.matches.addOrUpdate(instance: match)
} else {
team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false)
try? dataStore.matches.addOrUpdate(instance: match) try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: team) try? dataStore.teamRegistrations.addOrUpdate(instance: team)
}
}) })
if let tournament = match.currentTournament() { if let tournament = match.currentTournament() {
let availableSeedGroups = tournament.availableSeedGroups() let availableSeedGroups = tournament.availableSeedGroups()
Menu { Menu {
if walkOutSpot, luckyLosers.isEmpty == false {
Button {
if let randomTeam = luckyLosers.randomElement() {
match.setLuckyLoser(team: randomTeam, teamPosition: teamPosition)
try? dataStore.matches.addOrUpdate(instance: match)
}
} label: {
Label("Repêchage", systemImage: "dice")
}
}
ForEach(availableSeedGroups, id: \.self) { seedGroup in ForEach(availableSeedGroups, id: \.self) { seedGroup in
Button { Button {
if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) { if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) {
randomTeam.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) randomTeam.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false)
try? dataStore.matches.addOrUpdate(instance: match) try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam) try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam)
} }
@ -55,12 +90,14 @@ struct MatchSetupView: View {
} label: { } label: {
Text("Tirage").tag(nil as SeedInterval?) Text("Tirage").tag(nil as SeedInterval?)
} }
.disabled(availableSeedGroups.isEmpty) .disabled(availableSeedGroups.isEmpty && walkOutSpot == false)
} }
} }
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.buttonBorderShape(.capsule) .buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
}
} }
} }
} }

@ -61,9 +61,7 @@ struct MatchSummaryView: View {
if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle { if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle {
Text(groupStage.groupStageTitle()) Text(groupStage.groupStageTitle())
} }
// if let index = match.entrantOne()?.bracketPositions?.first, let index2 = match.entrantTwo()?.bracketPositions?.first { Text(match.matchTitle())
// Text("#\(index) contre #\(index2)")
// }
} else if let currentTournament = match.currentTournament() { } else if let currentTournament = match.currentTournament() {
if matchViewStyle == .feedStyle { if matchViewStyle == .feedStyle {
//tournamentHeaderView(currentTournament) //tournamentHeaderView(currentTournament)
@ -76,7 +74,7 @@ struct MatchSummaryView: View {
Spacer() Spacer()
if let court = match.court, match.hasEnded() == false { if let court = match.court, match.hasEnded() == false {
Spacer() Spacer()
Text("Terrain \(court)") Text("Terrain #\(court)")
} }
} }
} }
@ -86,14 +84,14 @@ struct MatchSummaryView: View {
if matchViewStyle != .feedStyle { if matchViewStyle != .feedStyle {
HStack(spacing: 0) { HStack(spacing: 0) {
VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 0) { VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 0) {
PlayerBlockView(match: match, team: .one, color: color, width: width) PlayerBlockView(match: match, teamPosition: .one, color: color, width: width)
.padding(matchViewStyle == .plainStyle ? 0 : 8) .padding(matchViewStyle == .plainStyle ? 0 : 8)
if width == 1 { if width == 1 {
Divider() Divider()
} else { } else {
Divider().frame(height: width).overlay(color) Divider().frame(height: width).overlay(color)
} }
PlayerBlockView(match: match, team: .two, color: color, width: width) PlayerBlockView(match: match, teamPosition: .two, color: color, width: width)
.padding(matchViewStyle == .plainStyle ? 0 : 8) .padding(matchViewStyle == .plainStyle ? 0 : 8)
} }
} }

@ -9,11 +9,19 @@ import SwiftUI
struct PlayerBlockView: View { struct PlayerBlockView: View {
var match: Match var match: Match
let teamPosition: TeamPosition
let team: TeamData let team: TeamRegistration?
let color: Color let color: Color
let width: CGFloat let width: CGFloat
init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat) {
self.match = match
self.teamPosition = teamPosition
self.team = match.team(teamPosition)
self.color = color
self.width = width
}
var names: [String]? { var names: [String]? {
match.teamNames(team) match.teamNames(team)
} }
@ -31,17 +39,24 @@ struct PlayerBlockView: View {
} }
var scores: [String] { var scores: [String] {
match.teamScore(team)?.score?.components(separatedBy: ",") ?? [] match.teamScore(ofTeam: team)?.score?.components(separatedBy: ",") ?? []
} }
private func _defaultLabel() -> String { private func _defaultLabel() -> String {
team.localizedLabel() if match.upperBracketMatch(teamPosition)?.disabled == true {
return "Bye"
}
return teamPosition.localizedLabel()
} }
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let names { if let names {
if let teamScore = match.teamScore(ofTeam: team), teamScore.luckyLoser != nil {
Text("Repêchée").italic().font(.caption)
}
ForEach(names, id: \.self) { name in ForEach(names, id: \.self) { name in
Text(name).lineLimit(1) Text(name).lineLimit(1)
} }

@ -9,9 +9,9 @@ import SwiftUI
struct ActivityView: View { struct ActivityView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation
@State private var searchText: String = "" @State private var searchText: String = ""
@State private var agendaDestination: AgendaDestination? = .activity
@State private var filterEnabled: Bool = false @State private var filterEnabled: Bool = false
@State private var presentToolbar: Bool = false @State private var presentToolbar: Bool = false
@ -20,6 +20,7 @@ struct ActivityView: View {
@State private var federalTournaments: [FederalTournament] = [] @State private var federalTournaments: [FederalTournament] = []
@State private var isGatheringFederalTournaments: Bool = false @State private var isGatheringFederalTournaments: Bool = false
@Binding var selectedTab: TabDestination? @Binding var selectedTab: TabDestination?
@State private var error: Error?
var runningTournaments: [FederalTournamentHolder] { var runningTournaments: [FederalTournamentHolder] {
dataStore.tournaments.filter({ $0.endDate == nil }) dataStore.tournaments.filter({ $0.endDate == nil })
@ -40,7 +41,7 @@ struct ActivityView: View {
} }
var tournaments: [FederalTournamentHolder] { var tournaments: [FederalTournamentHolder] {
switch agendaDestination! { switch navigation.agendaDestination! {
case .activity: case .activity:
runningTournaments runningTournaments
case .history: case .history:
@ -52,10 +53,11 @@ struct ActivityView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@Bindable var navigation = navigation
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false)
List { List {
switch agendaDestination! { switch navigation.agendaDestination! {
case .activity: case .activity:
EventListView(tournaments: runningTournaments, viewStyle: viewStyle) EventListView(tournaments: runningTournaments, viewStyle: viewStyle)
case .history: case .history:
@ -65,7 +67,17 @@ struct ActivityView: View {
} }
} }
.overlay { .overlay {
if isGatheringFederalTournaments { if let error, navigation.agendaDestination == .tenup {
ContentUnavailableView {
Label("Erreur", systemImage: "exclamationmark")
} description: {
Text(error.localizedDescription)
} actions: {
RowButtonView("D'accord.") {
self.error = nil
}
}
} else if isGatheringFederalTournaments {
ProgressView() ProgressView()
} else { } else {
if tournaments.isEmpty { if tournaments.isEmpty {
@ -77,7 +89,7 @@ struct ActivityView: View {
} description: { } description: {
Text("Description du filtre") Text("Description du filtre")
} actions: { } actions: {
RowButtonView(title: "supprimer le filtre") { RowButtonView("supprimer le filtre") {
filterEnabled.toggle() filterEnabled.toggle()
} }
} }
@ -94,20 +106,20 @@ struct ActivityView: View {
EventCreationView(tournaments: [tournament]) EventCreationView(tournaments: [tournament])
} }
.refreshable { .refreshable {
if agendaDestination == .tenup { if navigation.agendaDestination == .tenup {
federalTournaments.removeAll() federalTournaments.removeAll()
_gatherFederalTournaments() _gatherFederalTournaments()
} }
} }
.task { .task {
if agendaDestination == .tenup if navigation.agendaDestination == .tenup
&& dataStore.clubs.isEmpty == false && dataStore.clubs.isEmpty == false
&& federalTournaments.isEmpty { && federalTournaments.isEmpty {
_gatherFederalTournaments() _gatherFederalTournaments()
} }
} }
.onChange(of: agendaDestination) { .onChange(of: navigation.agendaDestination) {
if agendaDestination == .tenup if navigation.agendaDestination == .tenup
&& dataStore.clubs.isEmpty == false && dataStore.clubs.isEmpty == false
&& federalTournaments.isEmpty { && federalTournaments.isEmpty {
_gatherFederalTournaments() _gatherFederalTournaments()
@ -175,6 +187,10 @@ struct ActivityView: View {
TournamentView() TournamentView()
.environment(tournament) .environment(tournament)
} }
.navigationDestination(item: $navigation.tournament) { tournament in
TournamentView()
.environment(tournament)
}
} }
} }
} }
@ -182,8 +198,12 @@ struct ActivityView: View {
private func _gatherFederalTournaments() { private func _gatherFederalTournaments() {
isGatheringFederalTournaments = true isGatheringFederalTournaments = true
Task { Task {
await dataStore.clubs.filter { $0.code != nil }.concurrentForEach { club in do {
federalTournaments += await NetworkFederalService.shared.getClubFederalTournaments(page: 0, tournaments: [], club: club.name, codeClub: club.code!, startDate: .now.startOfMonth) try await dataStore.clubs.filter { $0.code != nil }.concurrentForEach { club in
federalTournaments += try await NetworkFederalService.shared.getClubFederalTournaments(page: 0, tournaments: [], club: club.name, codeClub: club.code!, startDate: .now.startOfMonth)
}
} catch {
self.error = error
} }
isGatheringFederalTournaments = false isGatheringFederalTournaments = false
} }
@ -191,7 +211,7 @@ struct ActivityView: View {
@ViewBuilder @ViewBuilder
private func _dataEmptyView() -> some View { private func _dataEmptyView() -> some View {
switch agendaDestination! { switch navigation.agendaDestination! {
case .activity: case .activity:
_runningEmptyView() _runningEmptyView()
case .history: case .history:
@ -215,11 +235,11 @@ struct ActivityView: View {
} description: { } description: {
Text("Aucun événement en cours ou à venir dans votre agenda.") Text("Aucun événement en cours ou à venir dans votre agenda.")
} actions: { } actions: {
RowButtonView(title: "Créer un nouvel événement") { RowButtonView("Créer un nouvel événement") {
newTournament = Tournament.newEmptyInstance() newTournament = Tournament.newEmptyInstance()
} }
RowButtonView(title: "Importer via Tenup") { RowButtonView("Importer via Tenup") {
agendaDestination = .tenup navigation.agendaDestination = .tenup
} }
} }
} }
@ -239,7 +259,7 @@ struct ActivityView: View {
} description: { } description: {
Text("Pour voir vos tournois tenup ici, indiquez vos clubs préférés.") Text("Pour voir vos tournois tenup ici, indiquez vos clubs préférés.")
} actions: { } actions: {
RowButtonView(title: "Choisir mes clubs préférés") { RowButtonView("Choisir mes clubs préférés") {
selectedTab = .umpire selectedTab = .umpire
} }
} }
@ -249,7 +269,7 @@ struct ActivityView: View {
} description: { } description: {
Text("Aucun tournoi n'a pu être récupéré via tenup.") Text("Aucun tournoi n'a pu être récupéré via tenup.")
} actions: { } actions: {
RowButtonView(title: "Rafraîchir") { RowButtonView("Rafraîchir") {
_gatherFederalTournaments() _gatherFederalTournaments()
} }
} }

@ -16,13 +16,13 @@ struct EmptyActivityView: View {
WelcomeView() WelcomeView()
Section { Section {
RowButtonView(title: "Créer votre premier événement", action: { RowButtonView("Créer votre premier événement", action: {
newTournament = Tournament.newEmptyInstance() newTournament = Tournament.newEmptyInstance()
}) })
} }
Section { Section {
RowButtonView(title: "Importer vos tournois Tenup", action: { RowButtonView("Importer vos tournois Tenup", action: {
}) })
} }

@ -132,24 +132,6 @@ struct MainView: View {
} }
} }
fileprivate extension View {
func tabItem(for tabDestination: TabDestination) -> some View {
modifier(TabItemModifier(tabDestination: tabDestination))
}
}
fileprivate struct TabItemModifier: ViewModifier {
let tabDestination: TabDestination
func body(content: Content) -> some View {
content
.tabItem {
Label(tabDestination.title, systemImage: tabDestination.image)
}
.tag(tabDestination as TabDestination?)
}
}
#Preview { #Preview {
MainView() MainView()
} }

@ -54,7 +54,7 @@ struct PadelClubView: View {
// } description: { // } description: {
// Text("Padel peut importer toutes les données publique de la FFT concernant tous les compétiteurs et compétitrices.") // Text("Padel peut importer toutes les données publique de la FFT concernant tous les compétiteurs et compétitrices.")
// } actions: { // } actions: {
// RowButtonView(title: "Démarrer l'importation") { // RowButtonView("Démarrer l'importation") {
// _startImporting() // _startImporting()
// } // }
// } // }

@ -166,7 +166,7 @@ struct PlayerPopoverView: View {
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
Section { Section {
RowButtonView(title: "Valider et ajouter un autre") { RowButtonView("Valider et ajouter un autre") {
createManualPlayer() createManualPlayer()
lastName = "" lastName = ""
firstName = "" firstName = ""

@ -0,0 +1,53 @@
//
// LoserBracketView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 04/04/2024.
//
import SwiftUI
struct LoserBracketView: View {
@EnvironmentObject var dataStore: DataStore
let loserRounds: [Round]
@ViewBuilder
var body: some View {
if let first = loserRounds.first {
List {
ForEach(loserRounds) { loserRound in
_loserRoundView(loserRound)
let childLoserRounds = loserRound.loserRounds()
if childLoserRounds.isEmpty == false {
let uniqueChildRound = childLoserRounds.first
if childLoserRounds.count == 1, let uniqueChildRound {
_loserRoundView(uniqueChildRound)
} else if let uniqueChildRound {
NavigationLink {
LoserBracketView(loserRounds: childLoserRounds)
} label: {
Text(uniqueChildRound.roundTitle())
}
}
}
}
}
.navigationTitle(first.roundTitle())
}
}
private func _loserRoundView(_ loserRound: Round) -> some View {
Section {
ForEach(loserRound.playedMatches()) { match in
MatchRowView(match: match, matchViewStyle: .standardStyle)
}
} header: {
Text(loserRound.roundTitle())
}
}
}
#Preview {
LoserBracketView(loserRounds: [Round.mock()])
.environmentObject(DataStore.shared)
}

@ -0,0 +1,96 @@
//
// LoserRoundsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 04/04/2024.
//
import SwiftUI
struct LoserRoundsView: View {
var upperBracketRound: Round
@State private var selectedRound: Round?
let loserRounds: [Round]
init(upperBracketRound: Round) {
self.upperBracketRound = upperBracketRound
self.loserRounds = upperBracketRound.loserRounds()
_selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound())
}
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: loserRounds, nilDestinationIsValid: true)
switch selectedRound {
case .none:
List {
RowButtonView("Effacer", role: .destructive) {
}
}
case .some(let selectedRound):
LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index))
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
struct LoserRoundView: View {
@EnvironmentObject var dataStore: DataStore
let loserRounds: [Round]
@Environment(\.editMode) private var editMode
private func _roundDisabled() -> Bool {
loserRounds.allSatisfy({ $0.isDisabled() })
}
var body: some View {
List {
if editMode?.wrappedValue.isEditing == true {
_editingView()
}
ForEach(loserRounds) { loserRound in
Section {
ForEach(loserRound.playedMatches()) { match in
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
.overlay {
if match.disabled {
Image(systemName: "xmark")
.resizable()
.scaledToFit()
.opacity(0.8)
}
}
.disabled(match.disabled)
}
} header: {
Text(loserRound.roundTitle(.wide))
}
}
}
.headerProminence(.increased)
.toolbar {
EditButton()
}
}
private func _editingView() -> some View {
if _roundDisabled() {
RowButtonView("Jouer ce tour", role: .destructive) {
loserRounds.forEach { round in
round.enableRound()
round.handleLoserRoundState()
}
}
} else {
RowButtonView("Ne pas jouer ce tour", role: .destructive) {
loserRounds.forEach { round in
round.disableRound()
}
}
}
}
}

@ -9,36 +9,25 @@ import SwiftUI
struct RoundSettingsView: View { struct RoundSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Binding var isEditingTournamentSeed: Bool
@State private var roundIndex: Int?
var round: Round? {
guard let roundIndex else { return nil }
return tournament.rounds()[roundIndex]
}
var body: some View { var body: some View {
List { List {
Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed)
Section { Section {
RowButtonView(title: "Retirer toutes les têtes de séries") { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) {
tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) tournament.unsortedTeams().forEach({ $0.bracketPosition = nil })
} try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} tournament.allRounds().forEach({ round in
round.enableRound()
Section { })
if let lastRound = tournament.rounds().first { // first is final, last round editMode?.wrappedValue = .active
RowButtonView(title: "Supprimer " + lastRound.roundTitle()) {
try? dataStore.rounds.delete(instance: lastRound)
}
} }
} }
Section { Section {
let roundIndex = tournament.rounds().count let roundIndex = tournament.rounds().count
RowButtonView(title: "Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex)) { RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex)) {
let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
@ -47,50 +36,15 @@ struct RoundSettingsView: View {
} }
try? dataStore.rounds.addOrUpdate(instance: round) try? dataStore.rounds.addOrUpdate(instance: round)
try? dataStore.matches.addOrUpdate(contentOfs: matches) try? dataStore.matches.addOrUpdate(contentOfs: matches)
round.buildLoserBracket()
} }
} }
if let availableSeedGroup = tournament.availableSeedGroup() {
Section { Section {
if let lastRound = tournament.rounds().first { // first is final, last round
Picker(selection: $roundIndex) { RowButtonView("Supprimer " + lastRound.roundTitle(), role: .destructive) {
Text("choisir de la manche").tag(nil as Int?) try? dataStore.rounds.delete(instance: lastRound)
ForEach(tournament.rounds()) { round in
Text(round.roundTitle()).tag(round.index as Int?)
}
} label: {
Text(availableSeedGroup.localizedLabel())
}
if let roundIndex {
RowButtonView(title: "Valider") {
if availableSeedGroup == SeedInterval(first: 1, last: 2) {
let seeds = tournament.seeds()
// let startIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
// let numberOfMatchInRound = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
// let lastIndex = startIndex + numberOfMatchInRound - 1
// seeds.prefix(1).first?.bracketPosition = lastIndex * 2 + 1 //TS 1 branche du bas du dernier match
// seeds.prefix(2).dropFirst().first?.bracketPosition = startIndex * 2 //TS 2 branche du haut du premier match
if let matches = tournament.getRound(atRoundIndex: roundIndex)?.matches {
if let lastMatch = matches.last {
seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, upperBranch: 1, opposingSeeding: false)
}
if let firstMatch = matches.first {
seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, upperBranch: 0, opposingSeeding: false)
}
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
} else {
tournament.setSeeds(inRoundIndex: roundIndex, inSeedGroup: availableSeedGroup)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds())
}
}
} }
} header: {
Text("Placement des têtes de série")
} }
} }
} }
@ -98,7 +52,7 @@ struct RoundSettingsView: View {
} }
#Preview { #Preview {
RoundSettingsView(isEditingTournamentSeed: .constant(true)) RoundSettingsView()
.environment(Tournament.mock()) .environment(Tournament.mock())
.environmentObject(DataStore.shared) .environmentObject(DataStore.shared)
} }

@ -8,11 +8,41 @@
import SwiftUI import SwiftUI
struct RoundView: View { struct RoundView: View {
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var round: Round var round: Round
var body: some View { var body: some View {
List { List {
ForEach(round.matches) { match in
if editMode?.wrappedValue.isEditing == false {
let loserRounds = round.loserRounds()
if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) {
Section {
NavigationLink {
LoserRoundsView(upperBracketRound: round)
.environment(tournament)
.navigationTitle(first.roundTitle())
} label: {
Text(first.roundTitle())
}
}
}
} else if let availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) {
RowButtonView("Placer \(availableSeedGroup.localizedLabel())") {
tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds())
if tournament.availableSeeds().isEmpty {
editMode?.wrappedValue = .inactive
}
}
}
ForEach(round.playedMatches()) { match in
Section { Section {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
} header: { } header: {
@ -21,9 +51,15 @@ struct RoundView: View {
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
EditButton()
}
}
} }
} }
#Preview { #Preview {
RoundView(round: Round.mock()) RoundView(round: Round.mock())
.environment(Tournament.mock())
} }

@ -10,13 +10,13 @@ import SwiftUI
struct RoundsView: View { struct RoundsView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedRound: Round? @State private var selectedRound: Round?
@State private var isEditingTournamentSeed = false @State var editMode: EditMode = .inactive
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_selectedRound = State(wrappedValue: tournament.getActiveRound()) _selectedRound = State(wrappedValue: tournament.getActiveRound())
if tournament.availableSeeds().isEmpty == false { if tournament.availableSeeds().isEmpty == false {
_isEditingTournamentSeed = State(wrappedValue: true) _editMode = .init(wrappedValue: .active)
} }
} }
@ -25,14 +25,14 @@ struct RoundsView: View {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: tournament.rounds(), nilDestinationIsValid: true) GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: tournament.rounds(), nilDestinationIsValid: true)
switch selectedRound { switch selectedRound {
case .none: case .none:
RoundSettingsView(isEditingTournamentSeed: $isEditingTournamentSeed) RoundSettingsView()
.navigationTitle("Réglages") .navigationTitle("Réglages")
case .some(let selectedRound): case .some(let selectedRound):
RoundView(round: selectedRound) RoundView(round: selectedRound)
.navigationTitle(selectedRound.roundTitle()) .navigationTitle(selectedRound.roundTitle())
.editTournamentSeed(isEditingTournamentSeed)
} }
} }
.environment(\.editMode, $editMode)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }

@ -0,0 +1,104 @@
//
// EditScoreView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 27/02/2023.
//
import SwiftUI
struct EditScoreView: View {
@EnvironmentObject var dataStore: DataStore
@ObservedObject var matchDescriptor: MatchDescriptor
@Environment(\.dismiss) private var dismiss
func walkout(_ team: TeamPosition) {
matchDescriptor.match?.setWalkOut(team)
save()
dismiss()
}
var body: some View {
Form {
Section {
Text(matchDescriptor.teamLabelOne)
Text(matchDescriptor.teamLabelTwo)
} footer: {
HStack {
Menu {
Button {
walkout(.one)
} label: {
Text(matchDescriptor.teamLabelOne)
}
Button {
walkout(.two)
} label: {
Text(matchDescriptor.teamLabelTwo)
}
} label: {
Text("Forfait")
}
Spacer()
MatchTypeSmallSelectionView(selectedFormat: $matchDescriptor.matchFormat, format: "Format")
.onChange(of: matchDescriptor.matchFormat) { newValue in
matchDescriptor.setDescriptors.removeAll()
matchDescriptor.addNewSet()
}
}
}
ForEach($matchDescriptor.setDescriptors) { $setDescriptor in
SetInputView(setDescriptor: $setDescriptor)
.onChange(of: setDescriptor.hasEnded) { hasEnded in
if hasEnded {
if matchDescriptor.hasEnded == false {
matchDescriptor.addNewSet()
}
} else {
let index = matchDescriptor.setDescriptors.firstIndex(where: { $0 == setDescriptor }) ?? 0
matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index])
}
}
}
if matchDescriptor.hasEnded {
Section {
HStack {
Spacer()
VStack {
Text(matchDescriptor.winnerLabel)
}
.multilineTextAlignment(.center)
Spacer()
}
RowButtonView("Victoire") {
matchDescriptor.match?.setScore(fromMatchDescriptor: matchDescriptor)
save()
dismiss()
}
} footer: {
Text("Termine la rencontre sur ce score")
}
}
if matchDescriptor.match?.hasEnded() == false {
Section {
RowButtonView("Mise à jour") {
matchDescriptor.match?.updateScore(fromMatchDescriptor: matchDescriptor)
save()
dismiss()
}
} footer: {
Text("Met à jour le score pour la diffusion, ne termine pas la rencontre")
}
}
}
}
func save() {
if let match = matchDescriptor.match {
try? dataStore.matches.addOrUpdate(instance: match)
}
}
}

@ -0,0 +1,50 @@
//
// PointSelectionView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 27/02/2023.
//
import SwiftUI
struct PointSelectionView: View {
@Binding var valueSelected: Int?
var values: [Int]
var possibleValues: [Int]
var disableValues: [Int] = []
var deleteAction: () -> ()
let gridItems: [GridItem] = [GridItem(.adaptive(minimum: 65), spacing: 20)]
init(valueSelected: Binding<Int?>, values: [Int], possibleValues: [Int], disableValues: [Int], deleteAction: @escaping () -> Void) {
_valueSelected = valueSelected
self.values = values
self.possibleValues = Set(values + possibleValues).sorted().reversed()
self.disableValues = disableValues
self.deleteAction = deleteAction
}
var body: some View {
LazyVGrid(columns: gridItems, alignment: .center, spacing: 20) {
ForEach(possibleValues, id: \.self) { value in
Button {
valueSelected = value
} label: {
PointView(value: "\(value).circle.fill")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.disabled(disableValues.contains(value) || values.contains(value) == false )
}
Button {
deleteAction()
} label: {
PointView(value: "delete.left.fill")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.padding()
}
}

@ -0,0 +1,26 @@
//
// PointView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 27/02/2023.
//
import SwiftUI
struct PointView: View {
let value: String
var body: some View {
Image(systemName: value)
.resizable()
.aspectRatio(contentMode: .fit)
.font(.largeTitle)
.frame(height: 40)
}
}
struct PointView_Previews: PreviewProvider {
static var previews: some View {
PointView(value:"delete.left.fill")
}
}

@ -0,0 +1,215 @@
//
// SetInputView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import SwiftUI
struct SetInputView: View {
@Binding var setDescriptor: SetDescriptor
@State private var showSetInputView: Bool = true
@State private var showTieBreakInputView: Bool = false
var setFormat: SetFormat { setDescriptor.setFormat }
private var showTieBreakView: Bool {
setFormat.shouldTiebreak(scoreTeamOne: setDescriptor.valueTeamOne ?? 0, scoreTeamTwo: setDescriptor.valueTeamTwo ?? 0)
}
private var isMainViewTieBreakView: Bool {
setFormat == .superTieBreak || setFormat == .megaTieBreak
}
private var currentValue: Binding<Int?> {
Binding {
if setDescriptor.valueTeamOne != nil {
return setDescriptor.valueTeamTwo
} else {
return setDescriptor.valueTeamOne
}
} set: { newValue, _ in
if setDescriptor.valueTeamOne != nil {
setDescriptor.valueTeamTwo = newValue
} else {
setDescriptor.valueTeamOne = newValue
}
}
}
private var currentTiebreakValue: Binding<Int?> {
Binding {
if setDescriptor.tieBreakValueTeamOne != nil {
return setDescriptor.tieBreakValueTeamTwo
} else {
return setDescriptor.tieBreakValueTeamOne
}
} set: { newValue, _ in
if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo {
if tieBreakValueTeamOne < tieBreakValueTeamTwo && tieBreakValueTeamTwo > 6 {
setDescriptor.tieBreakValueTeamOne = newValue
}
else if tieBreakValueTeamOne > tieBreakValueTeamTwo && tieBreakValueTeamOne > 6 {
setDescriptor.tieBreakValueTeamTwo = newValue
}
}
else if setDescriptor.tieBreakValueTeamOne != nil {
setDescriptor.tieBreakValueTeamTwo = newValue
} else {
setDescriptor.tieBreakValueTeamOne = newValue
}
}
}
private var disableValues: [Int] {
if let valueTeamOne = setDescriptor.valueTeamOne {
return setFormat.disableValuesForTeamTwo(with: valueTeamOne)
}
return []
}
private var disableTieBreakValues: [Int] {
if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne {
if tieBreakValueTeamOne == 7 {
return [7,6]
}
}
return []
}
func deleteLastValue() {
setDescriptor.valueTeamOne = nil
setDescriptor.valueTeamTwo = nil
}
func deleteLastTiebreakValue() {
setDescriptor.tieBreakValueTeamOne = nil
setDescriptor.tieBreakValueTeamTwo = nil
}
func possibleValues() -> [Int] {
if let valueTeamOne = setDescriptor.valueTeamOne {
if valueTeamOne == 7 && setFormat == .six {
return [6,5]
}
if valueTeamOne == 5 && setFormat == .four {
return [3,2]
}
}
return setFormat.possibleValues
}
func tieBreakPossibleValues() -> [Int] {
if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo {
if tieBreakValueTeamOne == 6 && tieBreakValueTeamTwo == 8 {
return []
}
if tieBreakValueTeamOne < 7 && tieBreakValueTeamTwo == 7 {
return [9, 5, 4, 3, 2, 1, 0]
}
if tieBreakValueTeamOne == 7 && tieBreakValueTeamTwo < 7 {
return [9, 5, 4, 3, 2, 1, 0]
}
return Array(((max(tieBreakValueTeamOne, tieBreakValueTeamTwo)+2)..<max(tieBreakValueTeamOne, tieBreakValueTeamTwo)+8)).reversed()
}
if setDescriptor.tieBreakValueTeamOne != nil {
return [9, 5, 4, 3, 2, 1, 0]
}
return SetFormat.six.possibleValues
}
var body: some View {
Section {
DisclosureGroup(isExpanded: $showSetInputView) {
PointSelectionView(valueSelected: currentValue, values: possibleValues(), possibleValues: setFormat.possibleValues, disableValues: disableValues, deleteAction: deleteLastValue)
} label: {
SetLabelView(initialValueLeft: $setDescriptor.valueTeamOne, initialValueRight: $setDescriptor.valueTeamTwo, shouldDisplaySteppers: isMainViewTieBreakView)
}
if showTieBreakView {
DisclosureGroup(isExpanded: $showTieBreakInputView) {
PointSelectionView(valueSelected: currentTiebreakValue, values: tieBreakPossibleValues(), possibleValues: SetFormat.six.possibleValues, disableValues: disableTieBreakValues, deleteAction: deleteLastTiebreakValue)
} label: {
SetLabelView(initialValueLeft: $setDescriptor.tieBreakValueTeamOne, initialValueRight: $setDescriptor.tieBreakValueTeamTwo, shouldDisplaySteppers: showTieBreakInputView, isTieBreak: true)
}
}
}
.onChange(of: setDescriptor.valueTeamOne, perform: { newValue in
if let newValue {
if newValue == setFormat.scoreToWin - 1 && setFormat.tieBreak == 8 {
setDescriptor.valueTeamTwo = setFormat.scoreToWin
} else if newValue == setFormat.scoreToWin - 2 && setFormat.tieBreak == 8 {
setDescriptor.valueTeamTwo = setFormat.scoreToWin
} else if newValue == setFormat.scoreToWin - 1 {
setDescriptor.valueTeamTwo = setFormat.scoreToWin + 1
} else if newValue <= setFormat.scoreToWin - 2 {
setDescriptor.valueTeamTwo = setFormat.scoreToWin
} else if newValue > 10 && setFormat == .superTieBreak {
setDescriptor.valueTeamTwo = newValue - 2
} else if newValue > 15 && setFormat == .megaTieBreak {
setDescriptor.valueTeamTwo = newValue - 2
}
}
})
.onChange(of: setDescriptor.valueTeamTwo, perform: { newValue in
if setDescriptor.valueTeamOne != nil && setDescriptor.valueTeamTwo != nil {
showSetInputView = false
}
})
.onChange(of: setDescriptor.tieBreakValueTeamOne, perform: { newValue in
if let newValue, setDescriptor.tieBreakValueTeamTwo == nil {
if newValue > 7 {
setDescriptor.tieBreakValueTeamTwo = newValue - 2
}
if newValue == 6 {
setDescriptor.tieBreakValueTeamTwo = newValue + 2
}
if newValue <= 5 {
setDescriptor.tieBreakValueTeamTwo = 7
}
}
else if let newValue, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo {
if newValue > 6 && tieBreakValueTeamTwo < newValue {
setDescriptor.tieBreakValueTeamTwo = newValue - 2
}
if newValue > 6 && tieBreakValueTeamTwo > newValue {
setDescriptor.tieBreakValueTeamTwo = newValue + 2
}
if newValue == 6 {
setDescriptor.tieBreakValueTeamTwo = newValue + 2
}
if newValue <= 5 {
setDescriptor.tieBreakValueTeamTwo = 7
showTieBreakInputView = false
}
}
})
.onChange(of: setDescriptor.tieBreakValueTeamTwo, perform: { newValue in
if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, tieBreakValueTeamOne <= 5 {
showTieBreakInputView = false
} else {
if let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo {
if let newValue {
if newValue > 6 && tieBreakValueTeamTwo > setDescriptor.tieBreakValueTeamOne ?? 0 {
setDescriptor.tieBreakValueTeamOne = newValue - 2
}
if newValue > 4 && tieBreakValueTeamTwo < setDescriptor.tieBreakValueTeamOne ?? 0 {
setDescriptor.tieBreakValueTeamOne = newValue + 2
}
}
}
if let newValue, let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne {
if newValue < 6 && tieBreakValueTeamOne == 7 {
showTieBreakInputView = false
}
}
}
})
.listRowSeparator(.hidden)
}
}

@ -0,0 +1,57 @@
//
// SetLabelView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import SwiftUI
struct SetLabelView: View {
@Binding var initialValueLeft: Int?
@Binding var initialValueRight: Int?
@State private var valueLeft: Int = 0
@State private var valueRight: Int = 0
var shouldDisplaySteppers: Bool = false
var isTieBreak: Bool = false
var body: some View {
HStack(spacing: 0) {
if shouldDisplaySteppers {
Stepper(value: $valueLeft, in: 0...Int.max) {
} onEditingChanged: { didChange in
initialValueLeft = valueLeft
}
.fixedSize()
.scaleEffect(0.7)
}
Spacer()
Text("\(valueLeft) / \(valueRight)")
.font(isTieBreak ? .headline : .largeTitle).monospacedDigit()
.scaledToFit()
.minimumScaleFactor(0.5)
.lineLimit(1)
Spacer()
if shouldDisplaySteppers {
Stepper(value: $valueRight, in: 0...Int.max) {
} onEditingChanged: { didChange in
initialValueRight = valueRight
}
.fixedSize()
.scaleEffect(0.7)
}
}
.onChange(of: initialValueLeft) { newValue in
valueLeft = initialValueLeft ?? 0
}
.onChange(of: initialValueRight) { newValue in
valueRight = initialValueRight ?? 0
}
.onAppear {
valueLeft = initialValueLeft ?? 0
valueRight = initialValueRight ?? 0
}
}
}

@ -0,0 +1,23 @@
//
// MatchTypeSmallSelectionView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import SwiftUI
struct MatchTypeSmallSelectionView: View {
@Binding var selectedFormat: MatchFormat
let format: String
var body: some View {
Picker(selection: $selectedFormat) {
ForEach(MatchFormat.allCases, id: \.rawValue) { matchFormat in
Text(format + " " + matchFormat.format)
.tag(matchFormat)
}
} label: {
}
}
}

@ -13,6 +13,7 @@ struct TeamPickerView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var presentTeamPickerView: Bool = false @State private var presentTeamPickerView: Bool = false
@State private var searchField: String = "" @State private var searchField: String = ""
var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
var body: some View { var body: some View {
@ -23,8 +24,25 @@ struct TeamPickerView: View {
NavigationStack { NavigationStack {
List { List {
let teams = tournament.sortedTeams() let teams = tournament.sortedTeams()
if luckyLosers.isEmpty == false {
Section { Section {
_teamListView(teams.filter({ $0.available() }).sorted(by: \.weight).reversed()) _teamListView(luckyLosers.sorted(by: \.weight))
} header: {
Text("Repêchage")
}
}
let qualified = tournament.availableQualifiedTeams()
if qualified.isEmpty == false {
Section {
_teamListView(qualified.sorted(by: \.weight))
} header: {
Text("Qualifiées entrants")
}
}
Section {
_teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed())
} header: { } header: {
Text("Disponible") Text("Disponible")
} }

@ -9,12 +9,12 @@ import SwiftUI
struct TeamRowView: View { struct TeamRowView: View {
var team: TeamRegistration var team: TeamRegistration
var teamPosition: Int? = nil var teamPosition: TeamPosition? = nil
var body: some View { var body: some View {
LabeledContent { LabeledContent {
VStack(alignment: .trailing, spacing: 0) { VStack(alignment: .trailing, spacing: 0) {
if teamPosition == 0 || teamPosition == nil { if teamPosition == .one || teamPosition == nil {
Text(team.weight.formatted()) Text(team.weight.formatted())
.font(.caption) .font(.caption)
} }
@ -22,7 +22,7 @@ struct TeamRowView: View {
Text("#" + (index + 1).formatted()) Text("#" + (index + 1).formatted())
.font(.title) .font(.title)
} }
if teamPosition == 1 { if teamPosition == .two {
Text(team.weight.formatted()) Text(team.weight.formatted())
.font(.caption) .font(.caption)

@ -32,7 +32,7 @@ struct UpdateSourceRankDateView: View {
} }
} }
RowButtonView(title: "Valider") { RowButtonView("Valider") {
updatingRank = true updatingRank = true
Task { Task {
do { do {

@ -208,12 +208,12 @@ struct InscriptionManagerView: View {
} description: { } description: {
Text("\(searchField) est introuvable dans les équipes inscrites.") Text("\(searchField) est introuvable dans les équipes inscrites.")
} actions: { } actions: {
RowButtonView(title: "Modifier la recherche") { RowButtonView("Modifier la recherche") {
searchField = "" searchField = ""
presentSearch = true presentSearch = true
} }
RowButtonView(title: "Créer une équipe") { RowButtonView("Créer une équipe") {
Task { Task {
await MainActor.run() { await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
@ -222,7 +222,7 @@ struct InscriptionManagerView: View {
} }
} }
RowButtonView(title: "D'accord") { RowButtonView("D'accord") {
searchField = "" searchField = ""
presentSearch = false presentSearch = false
} }
@ -564,16 +564,16 @@ struct InscriptionManagerView: View {
if editedTeam == nil { if editedTeam == nil {
if createdPlayerIds.isEmpty { if createdPlayerIds.isEmpty {
RowButtonView(title: "Bloquer une place") { RowButtonView("Bloquer une place") {
_createTeam() _createTeam()
} }
} else { } else {
RowButtonView(title: "Ajouter l'équipe") { RowButtonView("Ajouter l'équipe") {
_createTeam() _createTeam()
} }
} }
} else { } else {
RowButtonView(title: "Modifier l'équipe") { RowButtonView("Modifier l'équipe") {
_updateTeam() _updateTeam()
} }
} }
@ -601,11 +601,11 @@ struct InscriptionManagerView: View {
} description: { } description: {
Text("Aucun joueur classé n'a été trouvé dans ce message.") Text("Aucun joueur classé n'a été trouvé dans ce message.")
} actions: { } actions: {
RowButtonView(title: "Créer un joueur non classé") { RowButtonView("Créer un joueur non classé") {
presentPlayerCreation = true presentPlayerCreation = true
} }
RowButtonView(title: "Effacer cette recherche") { RowButtonView("Effacer cette recherche") {
self.pasteString = nil self.pasteString = nil
} }
} }

@ -9,6 +9,7 @@ import SwiftUI
struct TournamentCellView: View { struct TournamentCellView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation
let tournament: FederalTournamentHolder let tournament: FederalTournamentHolder
let color: Color = .black let color: Color = .black
@ -21,11 +22,11 @@ struct TournamentCellView: View {
var body: some View { var body: some View {
ForEach(tournament.tournaments, id: \.id) { build in ForEach(tournament.tournaments, id: \.id) { build in
_buildView(build, alreadyExist: event?.existingBuild(build) != nil) _buildView(build, existingTournament: event?.existingBuild(build))
} }
} }
private func _buildView(_ build: any TournamentBuildHolder, alreadyExist: Bool) -> some View { private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View {
HStack { HStack {
DateBoxView(date: tournament.startDate, displayStyle: displayStyle) DateBoxView(date: tournament.startDate, displayStyle: displayStyle)
Rectangle() Rectangle()
@ -53,7 +54,10 @@ struct TournamentCellView: View {
Text(tournament.sortedTeams().count.formatted()) Text(tournament.sortedTeams().count.formatted())
} else if let federalTournament = tournament as? FederalTournament { } else if let federalTournament = tournament as? FederalTournament {
Button { Button {
if alreadyExist == false { if let existingTournament {
navigation.agendaDestination = .activity
navigation.tournament = existingTournament
} else {
let event = federalTournament.getEvent() let event = federalTournament.getEvent()
let newTournament = Tournament.newEmptyInstance() let newTournament = Tournament.newEmptyInstance()
newTournament.event = event.id newTournament.event = event.id
@ -63,17 +67,14 @@ struct TournamentCellView: View {
newTournament.dayDuration = federalTournament.dayDuration newTournament.dayDuration = federalTournament.dayDuration
newTournament.startDate = federalTournament.startDate newTournament.startDate = federalTournament.startDate
try? dataStore.tournaments.addOrUpdate(instance: newTournament) try? dataStore.tournaments.addOrUpdate(instance: newTournament)
} else {
//event?.existingBuild(build)
} }
} label: { } label: {
Image(systemName: alreadyExist ? "checkmark.circle.fill" : "square.and.arrow.down") Image(systemName: existingTournament != nil ? "checkmark.circle.fill" : "square.and.arrow.down")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(height: 28) .frame(height: 28)
.tint(alreadyExist ? Color.green : nil) .tint(existingTournament != nil ? Color.green : nil)
} }
.buttonStyle(.borderless)
} }
} }
.font(displayStyle == .wide ? .title : .title3) .font(displayStyle == .wide ? .title : .title3)

@ -49,7 +49,7 @@ struct TournamentView: View {
} }
if endOfInscriptionDate < Date() { if endOfInscriptionDate < Date() {
RowButtonView(title: "Clôturer les inscriptions") { RowButtonView("Clôturer les inscriptions") {
tournament.lockRegistration() tournament.lockRegistration()
_save() _save()
} }

@ -0,0 +1,26 @@
//
// TabItemModifier.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import SwiftUI
struct TabItemModifier: ViewModifier {
let tabDestination: TabDestination
func body(content: Content) -> some View {
content
.tabItem {
Label(tabDestination.title, systemImage: tabDestination.image)
}
.tag(tabDestination as TabDestination?)
}
}
extension View {
func tabItem(for tabDestination: TabDestination) -> some View {
modifier(TabItemModifier(tabDestination: tabDestination))
}
}
Loading…
Cancel
Save