Laurent 2 years ago
commit faaeb416c9
  1. 91
      PadelClub.xcodeproj/project.pbxproj
  2. 171
      PadelClub/Data/GroupStage.swift
  3. 306
      PadelClub/Data/Match.swift
  4. 3
      PadelClub/Data/PlayerRegistration.swift
  5. 274
      PadelClub/Data/Round.swift
  6. 20
      PadelClub/Data/TeamRegistration.swift
  7. 9
      PadelClub/Data/TeamScore.swift
  8. 121
      PadelClub/Data/Tournament.swift
  9. 17
      PadelClub/Extensions/Sequence+Extensions.swift
  10. 34
      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. 9
      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. 36
      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. 551
      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. 109
      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. 74
      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. 26
      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 */; };
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.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 */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.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 */; };
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.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 */; };
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; };
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.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 */; };
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 */; };
FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.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>"; };
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>"; };
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>"; };
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>"; };
@ -403,12 +419,25 @@
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -428,6 +457,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FFCFBFFE2BBBE86600B82851 /* Algorithms in Frameworks */,
FF2BE4872B85E27400592328 /* LeStorage.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -564,6 +594,7 @@
FFC83D4B2BB807C200750834 /* Round */,
FF967CF92BAEE11500A9A3BD /* GroupStage */,
FF967CFE2BAEEF5A00A9A3BD /* Match */,
FFCFC00B2BBC39A600B82851 /* Score */,
FF967D072BAF3D3000A9A3BD /* Team */,
FF089EB92BB011EE00F0AEC7 /* Player */,
FF3F74F72B919F96004CFE0E /* Tournament */,
@ -615,6 +646,7 @@
C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */,
FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */,
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */,
FFBF065D2BBD8040009D6715 /* MatchListView.swift */,
FF967CF72BAEDF0000A9A3BD /* Labels.swift */,
);
path = Components;
@ -786,6 +818,9 @@
FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */,
FFB9C8702BBADDE200A0EF4F /* Selectable.swift */,
FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */,
FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */,
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -798,6 +833,7 @@
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */,
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */,
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */,
FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -861,6 +897,7 @@
FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */,
FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */,
FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */,
FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */,
);
path = GroupStage;
sourceTree = "<group>";
@ -894,10 +931,24 @@
FFC83D4E2BB807D100750834 /* RoundsView.swift */,
FFC83D502BB8087E00750834 /* RoundView.swift */,
FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */,
FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
);
path = Round;
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 */ = {
isa = PBXGroup;
children = (
@ -915,6 +966,7 @@
children = (
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */,
FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */,
FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */,
);
path = ViewModifiers;
sourceTree = "<group>";
@ -970,6 +1022,9 @@
dependencies = (
);
name = PadelClub;
packageProductDependencies = (
FFCFBFFD2BBBE86600B82851 /* Algorithms */,
);
productName = PadelClub;
productReference = C425D3FD2B6D249D002A7B48 /* PadelClub.app */;
productType = "com.apple.product-type.application";
@ -1042,6 +1097,9 @@
Base,
);
mainGroup = C425D3F42B6D249D002A7B48;
packageReferences = (
FF4C7F052BBBE6B90031B6A3 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
);
productRefGroup = C425D3FE2B6D249D002A7B48 /* Products */;
projectDirPath = "";
projectReferences = (
@ -1163,6 +1221,7 @@
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */,
C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */,
@ -1185,7 +1244,9 @@
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */,
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */,
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */,
FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */,
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */,
@ -1205,6 +1266,7 @@
FF8F264B2BAE0B4100650388 /* TournamentLevelPickerView.swift in Sources */,
FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */,
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */,
FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */,
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */,
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */,
C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */,
@ -1216,15 +1278,18 @@
FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */,
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */,
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */,
FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
@ -1234,15 +1299,20 @@
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */,
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */,
C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */,
FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */,
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */,
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */,
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
@ -1267,11 +1337,13 @@
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */,
C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */,
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */,
FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */,
FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */,
FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */,
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */,
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */,
FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */,
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */,
C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */,
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
@ -1615,6 +1687,25 @@
};
/* 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 */
FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */ = {
isa = XCVersionGroup;

@ -7,6 +7,7 @@
import Foundation
import LeStorage
import Algorithms
@Observable
class GroupStage: ModelObject, Storable {
@ -36,8 +37,8 @@ class GroupStage: ModelObject, Storable {
self.startDate = startDate
}
func teamsAt(_ index: Int) -> TeamRegistration? {
teams().first(where: { $0.groupStagePosition == index })
func teamAt(groupStagePosition: Int) -> TeamRegistration? {
teams().first(where: { $0.groupStagePosition == groupStagePosition })
}
func tournamentObject() -> Tournament? {
@ -58,23 +59,25 @@ class GroupStage: ModelObject, Storable {
}
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
matches.filter { $0.hasEnded() }.isEmpty == false
_matches().filter { $0.hasEnded() }.isEmpty == false
}
func hasEnded() -> Bool {
if matches.isEmpty { return false }
return matches.allSatisfy { $0.hasEnded() }
guard teams().count == size else { return false }
let _matches = _matches()
if _matches.isEmpty { return false }
return _matches.allSatisfy { $0.hasEnded() }
}
func buildMatches() {
removeMatches()
_removeMatches()
var _matches = [Match]()
for i in 0..<numberOfMatchesToBuild {
for i in 0..<_numberOfMatchesToBuild() {
let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat)
_matches.append(newMatch)
}
@ -82,24 +85,162 @@ class GroupStage: ModelObject, Storable {
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()
}
var numberOfMatchesToBuild: Int {
private func _numberOfMatchesToBuild() -> Int {
(size * (size - 1)) / 2
}
var matches: [Match] {
private func _matches() -> [Match] {
Store.main.filter { $0.groupStage == self.id }
}
func teams() -> [TeamRegistration] {
Store.main.filter { $0.groupStage == self.id }
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
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 {
try Store.main.deleteDependencies(items: self.matches)
try Store.main.deleteDependencies(items: self._matches())
}
}
@ -120,6 +261,6 @@ extension GroupStage: Selectable {
}
func badgeValue() -> Int? {
nil
runningMatches().count
}
}

@ -26,7 +26,7 @@ class Match: ModelObject, Storable {
var broadcasted: Bool
var name: String?
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) {
self.round = round
@ -47,13 +47,17 @@ class Match: ModelObject, Storable {
func indexInRound() -> Int {
if groupStage != nil {
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 RoundRule.matchIndexWithinRound(fromMatchIndex: index)
}
func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let groupStageObject {
return groupStageObject.localizedMatchUpLabel(for: index)
}
switch displayStyle {
case .wide:
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() {
_toggleMatchDisableState(true)
}
@ -70,8 +126,24 @@ class Match: ModelObject, Storable {
_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) {
disabled = state
_toggleLoserMatchDisableState(state)
topPreviousRoundMatch()?._toggleMatchDisableState(state)
bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
try? DataStore.shared.matches.addOrUpdate(instance: self)
@ -99,8 +171,16 @@ class Match: ModelObject, Storable {
}.sorted(by: \.index).first
}
func previousMatch(_ teamPosition: Int) -> Match? {
if teamPosition == 0 {
func upperBracketMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == .one {
return roundObject?.upperBracketTopMatch(ofMatchIndex: index)
} else {
return roundObject?.upperBracketBottomMatch(ofMatchIndex: index)
}
}
func previousMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == .one {
return topPreviousRoundMatch()
} else {
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 {
teams().count == 2
}
@ -133,7 +317,7 @@ class Match: ModelObject, Storable {
}
func hasEnded() -> Bool {
endDate != nil
endDate != nil || hasWalkoutTeam() || winningTeamId != nil
}
func isGroupStage() -> Bool {
@ -160,75 +344,62 @@ class Match: ModelObject, Storable {
groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject()
}
func tournamentId() -> String? {
groupStageObject?.tournament ?? roundObject?.tournament
}
func scores() -> [TeamScore] {
Store.main.filter(isIncluded: { $0.match == id })
}
func teams() -> [TeamRegistration] {
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 }
}
func groupStageProjectedTeam(_ team: TeamData) -> TeamRegistration? {
guard groupStage != nil else { return nil }
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)
}
func scoreDifference(_ teamPosition: Int) -> (set: Int, game: Int)? {
guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil }
var reverseValue = 1
if teamPosition == team(.two)?.groupStagePosition {
reverseValue = -1
}
let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut())
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 }
return Store.main.filter(isIncluded: {
$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
return roundObject.roundProjectedTeam(team, inMatch: self)
}
func teamWon(_ team: TeamData) -> Bool {
true
func teamWon(_ team: TeamRegistration?) -> Bool {
guard let winningTeamId else { return false }
return winningTeamId == team?.id
}
func team(_ team: TeamData) -> TeamRegistration? {
func team(_ team: TeamPosition) -> TeamRegistration? {
if groupStage != nil {
switch team {
case .one:
return teams().first
return groupStageProjectedTeam(.one)
case .two:
return teams().last
return groupStageProjectedTeam(.two)
}
} else {
switch team {
@ -240,16 +411,20 @@ class Match: ModelObject, Storable {
}
}
func teamNames(_ team: TeamData) -> [String]? {
self.team(team)?.players().map { $0.playerLabel() }
func teamNames(_ team: TeamRegistration?) -> [String]? {
team?.players().map { $0.playerLabel() }
}
func teamWalkOut(_ team: TeamData) -> Bool {
false
func teamWalkOut(_ team: TeamRegistration?) -> Bool {
teamScore(ofTeam: team)?.isWalkOut() == true
}
func teamScore(_ team: TeamPosition) -> TeamScore? {
teamScore(ofTeam: self.team(team))
}
func teamScore(_ team: TeamData) -> TeamScore? {
scores().first(where: { $0.teamRegistration == self.team(team)?.id })
func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? {
scores().first(where: { $0.teamRegistration == team?.id })
}
func isRunning() -> Bool { // at least a match has started
@ -313,3 +488,20 @@ class Match: ModelObject, Storable {
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 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) {
self.teamRegistration = teamRegistration
self.firstName = firstName
@ -251,6 +253,7 @@ class PlayerRegistration: ModelObject, Storable {
case _email = "email"
case _weight = "weight"
case _source = "source"
case _hasArrived = "hasArrived"
}

@ -36,31 +36,221 @@ class Round: ModelObject, Storable {
func hasStarted() -> Bool {
matches.anySatisfy({ $0.hasStarted() })
playedMatches().anySatisfy({ $0.hasStarted() })
}
func hasEnded() -> Bool {
matches.allSatisfy({ $0.hasEnded() })
playedMatches().allSatisfy({ $0.hasEnded() })
}
func tournamentObject() -> Tournament? {
Store.main.findById(tournament)
}
var matches: [Match] {
Store.main.filter { $0.round == self.id && $0.disabled == false }
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 }
} else {
Store.main.filter { $0.round == self.id }
}
}
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? {
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 {
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 {
@ -71,18 +261,62 @@ class Round: ModelObject, Storable {
}
}
var loserRound: Round? {
guard let loser else { return nil }
return Store.main.findById(loser)
func loserRounds() -> [Round] {
return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed()
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.matches)
if let loserRound {
try Store.main.deleteDependencies(items: [loserRound])
func loserRoundsAndChildren() -> [Round] {
let loserRounds = loserRounds()
return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() })
}
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 {
case _id = "id"
case _tournament = "tournament"
@ -94,10 +328,18 @@ class Round: ModelObject, Storable {
extension Round: Selectable {
func selectionLabel() -> String {
roundTitle()
if let parentRound {
return "Tour #\(parentRound.loserRounds().count - index)"
} else {
return roundTitle()
}
}
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 callDate: Date?
var bracketPosition: Int?
var groupStagePosition: Int?
var groupStagePosition: Int? //todo devrait être non nil ?
var comment: String?
var source: String?
var sourceValue: String?
@ -32,6 +32,7 @@ class TeamRegistration: ModelObject, Storable {
var weight: Int = 0
var lockWeight: Int?
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) {
self.tournament = tournament
@ -52,17 +53,17 @@ class TeamRegistration: ModelObject, Storable {
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 seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = upperBranch ?? (isUpper ? 0 : 1)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = upperBranch ?? (isUpper ? 1 : 0)
teamPosition = slot ?? (isUpper ? .two : .one)
}
match.previousMatch(teamPosition)?.disableMatch()
bracketPosition = matchIndex * 2 + teamPosition
bracketPosition = matchIndex * 2 + teamPosition.rawValue
}
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
}
@ -203,7 +208,7 @@ class TeamRegistration: ModelObject, Storable {
}
}
func qualified() -> Bool {
func qualifiedFromGroupStage() -> Bool {
groupStagePosition != nil && bracketPosition != nil
}
@ -304,6 +309,7 @@ class TeamRegistration: ModelObject, Storable {
case _walkOut = "walkOut"
case _lockWeight = "lockWeight"
case _confirmationDate = "confirmationDate"
case _qualified = "qualified"
}
}

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

@ -86,6 +86,11 @@ class Tournament : ModelObject, Storable {
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 {
startDate <= Date()
}
@ -138,11 +143,11 @@ class Tournament : ModelObject, Storable {
return seeds().filter { $0.isSeedable() }
}
func lastSeedRound() -> Int? {
func lastSeedRound() -> Int {
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
} else {
return nil
return 0
}
}
@ -151,11 +156,11 @@ class Tournament : ModelObject, Storable {
}
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] {
getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 1 } ?? []
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? []
}
func availableSeedGroups() -> [SeedInterval] {
@ -178,13 +183,6 @@ class Tournament : ModelObject, Storable {
case 4...7:
return SeedInterval(first: 5, last: 8)
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)
case 16...23:
return SeedInterval(first: 17, last: 24)
@ -215,24 +213,67 @@ class Tournament : ModelObject, Storable {
return availableSeeds
}
func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup)
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 }
if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: false)
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)
}
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) {
}
let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: true)
return nil
}
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 availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup)
if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
}
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
}
} else if let chunk = seedGroup.chunk() {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
}
} else if let chunk = seedGroup.chunk() {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
}
}
@ -255,8 +296,12 @@ class Tournament : ModelObject, Storable {
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] {
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] {
@ -461,11 +506,11 @@ class Tournament : ModelObject, Storable {
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 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 {
@ -504,8 +549,12 @@ class Tournament : ModelObject, Storable {
}
func availableQualifiedTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil })
}
func qualifiedTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.qualified() })
unsortedTeams().filter({ $0.qualifiedFromGroupStage() })
}
func moreQualifiedToDraw() -> Int {
@ -517,16 +566,18 @@ class Tournament : ModelObject, Storable {
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
groupStage.teams()[qualifiedPerGroupStage]
}
.filter({ $0.qualified() == false })
.filter({ $0.qualifiedFromGroupStage() == false })
} else {
return []
}
}
func groupStagesAreOver() -> Bool {
guard groupStages().isEmpty == false else {
let groupStages = groupStages()
guard groupStages.isEmpty == false else {
return true
}
return groupStages.allSatisfy({ $0.hasEnded() })
return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
}
@ -546,11 +597,11 @@ class Tournament : ModelObject, Storable {
let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == 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
} 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)
self.rounds().forEach { round in
round.buildLoserBracket()
}
}
func deleteStructure() {

@ -7,6 +7,13 @@
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 {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
@ -23,16 +30,18 @@ extension Sequence {
extension Sequence {
func concurrentForEach(
_ operation: @escaping (Element) async -> Void
) async {
_ operation: @escaping (Element) async throws -> Void
) async throws {
// A task group automatically waits for all of its
// sub-tasks to complete, while also performing those
// tasks in parallel:
await withTaskGroup(of: Void.self) { group in
try await withThrowingTaskGroup(of: Void.self) { group in
for element in self {
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 {
do {
@ -128,24 +128,28 @@ recherche_type=club&club[autocomplete][value_container][value_field]=\(codeClub.
request.httpMethod = "POST"
request.httpBody = postData
do {
let commands : [HttpCommand] = try await runTenupTask(request: request)
let resultCommand = commands.first(where: { $0.results != nil })
if let gatheredTournaments = resultCommand?.results?.items {
var finalTournaments = tournaments + gatheredTournaments
if let count = resultCommand?.results?.nb_results {
if finalTournaments.count < count {
let newTournaments = await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub)
finalTournaments = finalTournaments + newTournaments
}
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 })
if let gatheredTournaments = resultCommand?.results?.items {
var finalTournaments = tournaments + gatheredTournaments
if let count = resultCommand?.results?.nb_results {
if finalTournaments.count < count {
let newTournaments = try await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub)
finalTournaments = finalTournaments + newTournaments
}
return finalTournaments
}
} catch {
print("getClubFederalTournaments", error)
return finalTournaments
}
// do {
// } catch {
// print("getClubFederalTournaments", error)
// }
//
return []
}

@ -14,4 +14,13 @@ enum NetworkManagerError: LocalizedError {
case mailNotSent //no network no error
case messageFailed
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 two
var otherTeam: TeamData {
var otherTeam: TeamPosition {
switch self {
case .one:
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
}
@ -972,6 +972,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
self.init(rawValue: value)
}
func defaultWalkOutScore(_ asWalkOutTeam: Bool) -> [Int] {
Array(repeating: asWalkOutTeam ? 0 : setFormat.scoreToWin, count: setsToWin)
}
var weight: Int {
switch self {
case .twoSets, .twoSetsDecisivePoint:
@ -1025,7 +1029,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
[.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
}

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

@ -12,10 +12,12 @@ import TipKit
@main
struct PadelClubApp: App {
let persistenceController = PersistenceController.shared
@State private var navigationViewModel = NavigationViewModel()
var body: some Scene {
WindowGroup {
MainView()
.environment(navigationViewModel)
.accentColor(.launchScreenBackground)
.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,11 +16,12 @@ struct SeedInterval: Hashable, Comparable {
}
func chunk() -> SeedInterval? {
if last - (last - first) / 2 > first {
return SeedInterval(first: first, last: last - (last - first) / 2)
} else {
return nil
if (last - first) / 2 > 0 {
if last - (last - first) / 2 > first {
return SeedInterval(first: first, last: last - (last - first) / 2)
}
}
return nil
}
}

@ -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: {
Text("Une erreur est survenue lors de la récupération de votre localisation.")
} actions: {
RowButtonView(title: "D'accord") {
RowButtonView("D'accord") {
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.")
} actions: {
if locationManager.manager.authorizationStatus != .restricted {
RowButtonView(title: "Chercher autour de moi") {
RowButtonView("Chercher autour de moi") {
if locationManager.manager.authorizationStatus == .notDetermined {
locationManager.manager.requestWhenInUseAuthorization()
} 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
}
}

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

@ -26,7 +26,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
.background {
Circle()
.fill(Color.white)
.opacity(selectedDestination == nil ? 1.0 : 0.5)
.opacity(selectedDestination == nil ? 1.0 : 0.4)
}
.buttonStyle(.plain)
}
@ -41,7 +41,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
.background {
Capsule()
.fill(Color.white)
.opacity(selectedDestination?.id == destination.id ? 1.0 : 0.5)
.opacity(selectedDestination?.id == destination.id ? 1.0 : 0.4)
}
.buttonStyle(.plain)
.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
fileprivate let defaultConfirmationMessage = "Êtes-vous sûr de vouloir faire cela ?"
struct RowButtonView: View {
var role: ButtonRole? = nil
let title: String
var systemImage: String? = nil
var image: String? = nil
var animatedProgress: Bool = false
let confirmationMessage: String
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 {
Button {
action()
Button(role: role) {
if role == .destructive {
askConfirmation = true
} else {
action()
}
} label: {
HStack {
if animatedProgress {
@ -47,8 +66,19 @@ struct RowButtonView: View {
.disabled(animatedProgress)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.tint(.launchScreenBackground)
.tint(role == .destructive ? Color.red : Color.launchScreenBackground)
.listRowBackground(Color.clear)
.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 {
RowButtonView(title:"Valider") {
RowButtonView("Valider") {
if tournaments.count > 1 || eventName.trimmed.isEmpty == false || selectedClub != nil {
let event = Event(name: eventName)
event.club = selectedClub?.id
@ -143,7 +143,7 @@ struct EventCreationView: View {
}
Section {
RowButtonView(title: "Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") {
RowButtonView("Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") {
let tournament = Tournament.newEmptyInstance()
self.tournaments.append(tournament)
}

@ -38,7 +38,7 @@ struct GroupStageSettingsView: View {
// if (tournament.groupStagesAreWrong || (tournament.emptySlotInGroupStages > 0 && tournament.entriesCount >= tournament.teamsFromGroupStages)) {
// Section {
// RowButtonView(title: "Reconstruire les poules") {
// RowButtonView("Reconstruire les poules") {
// confirmGroupStageRebuild = true
// }
// .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) {
// Section {
// RowButtonView(title: "Générer les matchs de poules") {
// RowButtonView("Générer les matchs de poules") {
// startAllGroupStageConfirmation = true
// }
// .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,209 +8,250 @@
import SwiftUI
struct GroupStageView: View {
@EnvironmentObject var dataStore: DataStore
@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 sortingMode: GroupStageSortingMode = .auto
@State private var confirmRemoveAll: Bool = false
@State private var confirmResetMatch: Bool = false
enum MenuLink: Int, Identifiable, Hashable {
var id: Int { self.rawValue }
case prepare
private enum GroupStageSortingMode {
case auto
case score
case weight
}
var groupStageView: some View {
ForEach(0..<(groupStage.size), id: \.self) { index in
// let entrant : Entrant? = runningGroupStageOrderedByScore ? groupStage.orderedByScore[Int(index)] : groupStage.entrantAtIndex(Int(index))
if let team = groupStage.teamsAt(index) {
Text(team.teamLabel())
// GroupStageEntrantMenuView(entrant: entrant, groupStage: groupStage, index: index.intValue)
} else {
Menu {
// 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: {
HStack {
Text("#\(index+1)")
Text("Aucune équipe")
}
}
}
}
var sortByScore: Bool {
sortingMode == .auto ? groupStage.hasEnded() : sortingMode == .score
}
func teamAt(atIndex index: Int) -> TeamRegistration? {
sortByScore ? groupStage.teams(sortByScore)[safe: index] : groupStage.teamAt(groupStagePosition: index)
}
var body: some View {
List {
Section {
groupStageView
// .disabled(canUpdateTournament == false)
// .sheet(item: $selectedMenuLink) { selectedMenuLink in
// switch selectedMenuLink {
// case .prepare:
// PrepareGroupStageView(groupStage: groupStage)
// }
// }
_groupStageView()
} header: {
HStack {
if groupStage.isBroadcasted() {
Label(groupStage.groupStageTitle(), systemImage: "airplayvideo")
} else {
Text(groupStage.groupStageTitle())
}
Spacer()
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 {
sortingMode = .weight
}
} label: {
Label(sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly)
}
}
} footer: {
HStack {
if groupStage.matches.isEmpty {
Button {
//groupStage.startGroupStage()
//save()
} label: {
Text("Créer les matchs")
.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()
}
}
}
private func _groupStageView() -> some View {
ForEach(0..<(groupStage.size), id: \.self) { index in
if let team = teamAt(atIndex: index), let groupStagePosition = team.groupStagePosition {
NavigationLink {
GroupStageTeamView(groupStage: groupStage, team: team)
} label: {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text("#\(groupStagePosition + 1)")
Text("Poids \(team.weight)")
}
.font(.caption)
HStack {
if let teamName = team.name {
Text(teamName)
} else {
VStack(alignment: .leading) {
ForEach(team.players()) { player in
Text(player.playerLabel())
}
}
}
if team.qualified {
Image(systemName: "checkmark.seal")
}
}
}
Spacer()
if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition) {
Text(score)
}
.buttonStyle(.borderless)
}
Spacer()
Menu {
// Button {
// selectedMenuLink = .prepare
// } label: {
// Label("Préparer", systemImage: "calendar")
// }
//
// Menu {
// MenuWarnView(warningSender: groupStage)
// } label: {
// Label("Prévenir", systemImage: "person.crop.circle")
// }
//
// if groupStage.isBroadcasted() {
// Button {
// groupStage.refreshBroadcast()
// } label: {
// Label("Rafraîchir", systemImage: "arrow.up.circle.fill")
// }
// Button {
// groupStage.stopBroadcast()
// save()
// } label: {
// Label("Arrêter la diffusion", systemImage: "stop.circle.fill")
// }
// } else if groupStage.tournament?.canBroadcast() == true {
// Button {
// Task {
// try? await groupStage.broadcastGroupStage()
// save()
// }
// } label: {
// Label("Diffuser", systemImage: "airplayvideo")
// }
// }
//
// Divider()
// if groupStage.tournament?.canBroadcast() == true {
// Menu {
// Button {
// Task {
// try? await groupStage.broadcastGroupStageMatches()
// save()
// }
// } label: {
// Label("Diffuser", systemImage: "airplayvideo")
// }
//
// Button {
// groupStage.refreshBroadcastMatches()
// } label: {
// Label("Rafraîchir", systemImage: "arrow.up.circle.fill")
// }
// Button {
// groupStage.stopBroadcastMatches()
// save()
// } label: {
// Label("Arrêter la diffusion", systemImage: "stop.circle.fill")
// }
// } label: {
// Text("Diffusion des matchs")
// }
// }
//
// Divider()
// Menu {
// if groupStage.orderedMatches.isEmpty == false {
// Button(role: .destructive) {
// groupStage.startGroupStage()
// save()
// } label: {
// Label("Re-démarrer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash")
// }
// }
//
// if groupStage.orderedMatches.isEmpty == false {
// Button(role: .destructive) {
// groupStage.removeMatches()
// save()
// } label: {
// Label("Supprimer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash")
// }
// }
//
// Button(role: .destructive) {
// groupStage.tournament?.completeEntries.filter { $0.groupStagePosition == groupStage.index }.forEach { $0.resetGroupStagePosition() }
// groupStage.tournament?.removeFromGroupStages(groupStage)
// groupStage.tournament?.numberOfGroupStages -= 1
// save()
// } label: {
// Label("Supprimer la \(groupStage.titleLabel.lowercased())", systemImage: "trash")
// }
// } label: {
// Text("Éditer")
// }
} label: {
}
} else {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 0) {
HStack {
Spacer()
Label("Options", systemImage: "ellipsis.circle").labelStyle(.titleOnly)
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)
})
}
.buttonStyle(.borderless)
}
}
}
}
if groupStage.matches.isEmpty == false {
Section {
ForEach(groupStage.matches) { match in
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
}
} header: {
Text("Matchs de la " + groupStage.groupStageTitle())
private func _groupStageMenuView() -> some View {
Menu {
if groupStage.matches().isEmpty {
Button {
//groupStage.startGroupStage()
//save()
} label: {
Text("Créer les matchs")
}
.buttonStyle(.borderless)
}
Button("Retirer tout le monde", role: .destructive) {
confirmRemoveAll = true
}
Button("Recommencer tous les matchs", role: .destructive) {
confirmResetMatch = true
}
// Button {
// selectedMenuLink = .prepare
// } label: {
// Label("Préparer", systemImage: "calendar")
// }
//
// Menu {
// MenuWarnView(warningSender: groupStage)
// } label: {
// Label("Prévenir", systemImage: "person.crop.circle")
// }
//
// if groupStage.isBroadcasted() {
// Button {
// groupStage.refreshBroadcast()
// } label: {
// Label("Rafraîchir", systemImage: "arrow.up.circle.fill")
// }
// Button {
// groupStage.stopBroadcast()
// save()
// } label: {
// Label("Arrêter la diffusion", systemImage: "stop.circle.fill")
// }
// } else if groupStage.tournament?.canBroadcast() == true {
// Button {
// Task {
// try? await groupStage.broadcastGroupStage()
// save()
// }
// } label: {
// Label("Diffuser", systemImage: "airplayvideo")
// }
// }
//
// Divider()
// if groupStage.tournament?.canBroadcast() == true {
// Menu {
// Button {
// Task {
// try? await groupStage.broadcastGroupStageMatches()
// save()
// }
// } label: {
// Label("Diffuser", systemImage: "airplayvideo")
// }
//
// Button {
// groupStage.refreshBroadcastMatches()
// } label: {
// Label("Rafraîchir", systemImage: "arrow.up.circle.fill")
// }
// Button {
// groupStage.stopBroadcastMatches()
// save()
// } label: {
// Label("Arrêter la diffusion", systemImage: "stop.circle.fill")
// }
// } label: {
// Text("Diffusion des matchs")
// }
// }
//
// Divider()
// Menu {
// if groupStage.orderedMatches.isEmpty == false {
// Button(role: .destructive) {
// groupStage.startGroupStage()
// save()
// } label: {
// Label("Re-démarrer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash")
// }
// }
//
// if groupStage.orderedMatches.isEmpty == false {
// Button(role: .destructive) {
// groupStage.removeMatches()
// save()
// } label: {
// Label("Supprimer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash")
// }
// }
//
// Button(role: .destructive) {
// groupStage.tournament?.completeEntries.filter { $0.groupStagePosition == groupStage.index }.forEach { $0.resetGroupStagePosition() }
// groupStage.tournament?.removeFromGroupStages(groupStage)
// groupStage.tournament?.numberOfGroupStages -= 1
// save()
// } label: {
// Label("Supprimer la \(groupStage.titleLabel.lowercased())", systemImage: "trash")
// }
// } label: {
// Text("Éditer")
// }
} label: {
LabelOptions()
}
// .onAppear {
// if let tournament = groupStage.tournament {
// canUpdateTournament = PersistenceController.shared.container.canUpdateRecord(forManagedObjectWith: tournament.objectID)
// } else {
// canUpdateTournament = true
// }
// }
.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)
}
}
.confirmationDialog("Êtes-vous sûr de vouloir faire cela ?", isPresented: $confirmResetMatch, titleVisibility: .visible) {
Button("Oui") {
groupStage.buildMatches()
}
}
}
// func save() {
@ -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 {
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) {
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 {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedGroupStage, destinations: tournament.groupStages(), nilDestinationIsValid: true)
switch selectedGroupStage {
case .none:
GroupStageSettingsView()
.navigationTitle("Réglages")
case .some(let groupStage):
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedDestination {
case .all:
List {
let allGroupStages = tournament.groupStages()
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)
.navigationTitle(groupStage.groupStageTitle())
case nil:
GroupStageSettingsView()
.navigationTitle("Réglages")
}
}
.navigationBarTitleDisplayMode(.inline)

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

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

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

@ -14,53 +14,90 @@ struct MatchSetupView: View {
@ViewBuilder
var body: some View {
_teamView(match.team(.one), teamPosition: 0)
_teamView(match.team(.two), teamPosition: 1)
_teamView(inTeamPosition: .one)
_teamView(inTeamPosition: .two)
}
@ViewBuilder
func _teamView(_ team: TeamRegistration?, teamPosition: Int) -> some View {
if let team {
TeamRowView(team: team, teamPosition: teamPosition)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .cancel) {
team.bracketPosition = nil
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
} label: {
Label("retirer", systemImage: "xmark")
}
func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View {
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)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .cancel) {
if match.isSeededBy(team: team, inTeamPosition: teamPosition) {
team.bracketPosition = nil
match.enableMatch()
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
} else {
match.teamWillBeWalkOut(team)
try? dataStore.matches.addOrUpdate(instance: match)
}
} label: {
Label("retirer", systemImage: "xmark")
}
}
}
} else {
HStack {
TeamPickerView(teamPicked: { team in
print(team.pasteData())
team.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false)
try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})
if let tournament = match.currentTournament() {
let availableSeedGroups = tournament.availableSeedGroups()
Menu {
ForEach(availableSeedGroups, id: \.self) { seedGroup in
Button {
if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) {
randomTeam.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false)
try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam)
VStack(alignment: .leading) {
if let team {
TeamRowView(team: team, teamPosition: teamPosition)
.strikethrough()
}
HStack {
let walkOutSpot = match.isWalkOutSpot(teamPosition)
let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData())
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.teamRegistrations.addOrUpdate(instance: team)
}
})
if let tournament = match.currentTournament() {
let availableSeedGroups = tournament.availableSeedGroups()
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")
}
} label: {
Label(seedGroup.localizedLabel(), systemImage: "dice")
}
ForEach(availableSeedGroups, id: \.self) { seedGroup in
Button {
if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) {
randomTeam.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false)
try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam)
}
} label: {
Label(seedGroup.localizedLabel(), systemImage: "dice")
}
}
} label: {
Text("Tirage").tag(nil as SeedInterval?)
}
} label: {
Text("Tirage").tag(nil as SeedInterval?)
.disabled(availableSeedGroups.isEmpty && walkOutSpot == false)
}
.disabled(availableSeedGroups.isEmpty)
}
.fixedSize(horizontal: false, vertical: true)
.buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent)
}
.fixedSize(horizontal: false, vertical: true)
.buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent)
}
}
}

@ -61,9 +61,7 @@ struct MatchSummaryView: View {
if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle {
Text(groupStage.groupStageTitle())
}
// if let index = match.entrantOne()?.bracketPositions?.first, let index2 = match.entrantTwo()?.bracketPositions?.first {
// Text("#\(index) contre #\(index2)")
// }
Text(match.matchTitle())
} else if let currentTournament = match.currentTournament() {
if matchViewStyle == .feedStyle {
//tournamentHeaderView(currentTournament)
@ -76,7 +74,7 @@ struct MatchSummaryView: View {
Spacer()
if let court = match.court, match.hasEnded() == false {
Spacer()
Text("Terrain \(court)")
Text("Terrain #\(court)")
}
}
}
@ -86,14 +84,14 @@ struct MatchSummaryView: View {
if matchViewStyle != .feedStyle {
HStack(spacing: 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)
if width == 1 {
Divider()
} else {
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)
}
}

@ -9,11 +9,19 @@ import SwiftUI
struct PlayerBlockView: View {
var match: Match
let team: TeamData
let teamPosition: TeamPosition
let team: TeamRegistration?
let color: Color
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]? {
match.teamNames(team)
}
@ -31,17 +39,24 @@ struct PlayerBlockView: View {
}
var scores: [String] {
match.teamScore(team)?.score?.components(separatedBy: ",") ?? []
match.teamScore(ofTeam: team)?.score?.components(separatedBy: ",") ?? []
}
private func _defaultLabel() -> String {
team.localizedLabel()
if match.upperBracketMatch(teamPosition)?.disabled == true {
return "Bye"
}
return teamPosition.localizedLabel()
}
var body: some View {
HStack {
VStack(alignment: .leading) {
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
Text(name).lineLimit(1)
}

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

@ -16,13 +16,13 @@ struct EmptyActivityView: View {
WelcomeView()
Section {
RowButtonView(title: "Créer votre premier événement", action: {
RowButtonView("Créer votre premier événement", action: {
newTournament = Tournament.newEmptyInstance()
})
}
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 {
MainView()
}

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

@ -166,7 +166,7 @@ struct PlayerPopoverView: View {
.multilineTextAlignment(.trailing)
Section {
RowButtonView(title: "Valider et ajouter un autre") {
RowButtonView("Valider et ajouter un autre") {
createManualPlayer()
lastName = ""
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 {
@EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@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 {
List {
Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed)
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 })
}
}
Section {
if let lastRound = tournament.rounds().first { // first is final, last round
RowButtonView(title: "Supprimer " + lastRound.roundTitle()) {
try? dataStore.rounds.delete(instance: lastRound)
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
tournament.allRounds().forEach({ round in
round.enableRound()
})
editMode?.wrappedValue = .active
}
}
Section {
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 matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
@ -47,50 +36,15 @@ struct RoundSettingsView: View {
}
try? dataStore.rounds.addOrUpdate(instance: round)
try? dataStore.matches.addOrUpdate(contentOfs: matches)
round.buildLoserBracket()
}
}
if let availableSeedGroup = tournament.availableSeedGroup() {
Section {
Picker(selection: $roundIndex) {
Text("choisir de la manche").tag(nil as Int?)
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())
}
}
Section {
if let lastRound = tournament.rounds().first { // first is final, last round
RowButtonView("Supprimer " + lastRound.roundTitle(), role: .destructive) {
try? dataStore.rounds.delete(instance: lastRound)
}
} header: {
Text("Placement des têtes de série")
}
}
}
@ -98,7 +52,7 @@ struct RoundSettingsView: View {
}
#Preview {
RoundSettingsView(isEditingTournamentSeed: .constant(true))
RoundSettingsView()
.environment(Tournament.mock())
.environmentObject(DataStore.shared)
}

@ -8,11 +8,41 @@
import SwiftUI
struct RoundView: View {
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var round: Round
var body: some View {
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 {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
} header: {
@ -21,9 +51,15 @@ struct RoundView: View {
}
}
.headerProminence(.increased)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
EditButton()
}
}
}
}
#Preview {
RoundView(round: Round.mock())
.environment(Tournament.mock())
}

@ -10,13 +10,13 @@ import SwiftUI
struct RoundsView: View {
var tournament: Tournament
@State private var selectedRound: Round?
@State private var isEditingTournamentSeed = false
@State var editMode: EditMode = .inactive
init(tournament: Tournament) {
self.tournament = tournament
_selectedRound = State(wrappedValue: tournament.getActiveRound())
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)
switch selectedRound {
case .none:
RoundSettingsView(isEditingTournamentSeed: $isEditingTournamentSeed)
RoundSettingsView()
.navigationTitle("Réglages")
case .some(let selectedRound):
RoundView(round: selectedRound)
.navigationTitle(selectedRound.roundTitle())
.editTournamentSeed(isEditingTournamentSeed)
}
}
.environment(\.editMode, $editMode)
.navigationBarTitleDisplayMode(.inline)
.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,18 +13,36 @@ struct TeamPickerView: View {
@Environment(\.dismiss) private var dismiss
@State private var presentTeamPickerView: Bool = false
@State private var searchField: String = ""
var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void))
var body: some View {
Button("Choisir") {
presentTeamPickerView = true
}
Button("Choisir") {
presentTeamPickerView = true
}
.sheet(isPresented: $presentTeamPickerView) {
NavigationStack {
List {
let teams = tournament.sortedTeams()
if luckyLosers.isEmpty == false {
Section {
_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.available() }).sorted(by: \.weight).reversed())
_teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed())
} header: {
Text("Disponible")
}

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

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

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

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

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