Laurent 1 year ago
commit 28b2d79d2a
  1. 44
      PadelClub.xcodeproj/project.pbxproj
  2. 103
      PadelClub/Data/DrawLog.swift
  3. 25
      PadelClub/Data/Match.swift
  4. 19
      PadelClub/Data/MatchScheduler.swift
  5. 56
      PadelClub/Data/TeamRegistration.swift
  6. 148
      PadelClub/Data/Tournament.swift
  7. 2
      PadelClub/Data/TournamentStore.swift
  8. 9
      PadelClub/Utils/PadelRule.swift
  9. 56
      PadelClub/Views/Components/FortuneWheelView.swift
  10. 12
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  11. 130
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  12. 69
      PadelClub/Views/Round/DrawLogsView.swift
  13. 107
      PadelClub/Views/Round/PreviewBracketPositionView.swift
  14. 90
      PadelClub/Views/Round/RoundSettingsView.swift
  15. 108
      PadelClub/Views/Round/RoundView.swift
  16. 85
      PadelClub/Views/Team/TeamRestingView.swift
  17. 90
      PadelClub/Views/Team/TeamRowView.swift
  18. 12
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  19. 18
      PadelClubTests/ServerDataTests.swift

@ -424,6 +424,15 @@
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; };
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; }; FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; };
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; };
FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; };
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; };
@ -1066,6 +1075,9 @@
FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; }; FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; };
FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; }; FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; };
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; };
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = "<group>"; };
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = "<group>"; };
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
@ -1356,6 +1368,7 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */, FF6EC9012B94799200EA7F5A /* Coredata */,
FF6EC9022B9479B900EA7F5A /* Federal */, FF6EC9022B9479B900EA7F5A /* Federal */,
); );
@ -1871,6 +1884,8 @@
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */,
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */,
); );
path = Round; path = Round;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2331,6 +2346,7 @@
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */,
FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */,
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */,
FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */,
C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */,
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
@ -2391,6 +2407,7 @@
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2442,6 +2459,7 @@
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */,
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */,
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */,
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
@ -2605,6 +2623,7 @@
FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */, FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */,
FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */, FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */,
FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */, FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */,
FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */, FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */,
FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */, FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */,
FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */, FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */,
@ -2665,6 +2684,7 @@
FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */,
FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */,
FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */,
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */,
FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */, FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */,
FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */, FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2716,6 +2736,7 @@
FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */,
FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */,
FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */,
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */,
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */,
@ -2858,6 +2879,7 @@
FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */, FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */,
FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */, FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */,
FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */, FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */,
FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */, FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */,
FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */, FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */,
FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */, FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */,
@ -2918,6 +2940,7 @@
FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */,
FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */,
FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */,
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */,
FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */, FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */,
FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */, FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2969,6 +2992,7 @@
FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */,
FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */,
FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */,
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */,
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */,
@ -3174,7 +3198,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3198,7 +3222,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.23; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3219,7 +3243,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3242,7 +3266,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.23; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3358,7 +3382,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.21; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3401,7 +3425,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.21; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3423,7 +3447,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3445,7 +3469,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.21; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3465,7 +3489,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3486,7 +3510,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.21; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -0,0 +1,103 @@
//
// DrawLog.swift
// PadelClub
//
// Created by razmig on 22/10/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
final class DrawLog: ModelObject, Storable {
static func resourceName() -> String { return "draw-logs" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
static var relationshipNames: [String] = []
var id: String = Store.randomId()
var tournament: String
var drawDate: Date = Date()
var drawSeed: Int
var drawMatchIndex: Int
var drawTeamPosition: TeamPosition
internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int, drawMatchIndex: Int, drawTeamPosition: TeamPosition) {
self.id = id
self.tournament = tournament
self.drawDate = drawDate
self.drawSeed = drawSeed
self.drawMatchIndex = drawMatchIndex
self.drawTeamPosition = drawTeamPosition
}
func tournamentObject() -> Tournament? {
Store.main.findById(self.tournament)
}
func computedBracketPosition() -> Int {
drawMatchIndex * 2 + drawTeamPosition.rawValue
}
func updateTeamBracketPosition(_ team: TeamRegistration) {
guard let match = drawMatch() else { return }
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition)
team.bracketPosition = seedPosition
tournamentObject()?.updateTeamScores(in: seedPosition)
}
func exportedDrawLog() -> String {
[drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ")
}
func localizedDrawSeedLabel() -> String {
return "Tête de série #\(drawSeed + 1)"
}
func localizedDrawLogLabel() -> String {
return [localizedDrawSeedLabel(), positionLabel()].joined(separator: " -> ")
}
func localizedDrawBranch() -> String {
drawTeamPosition.localizedBranchLabel()
}
func drawMatch() -> Match? {
let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex)
return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex })
}
func positionLabel() -> String {
return drawMatch()?.roundAndMatchTitle() ?? ""
}
func roundLabel() -> String {
return drawMatch()?.roundTitle() ?? ""
}
func matchLabel() -> String {
return drawMatch()?.matchTitle() ?? ""
}
var tournamentStore: TournamentStore {
return TournamentStore.instance(tournamentId: self.tournament)
}
override func deleteDependencies() throws {
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _tournament = "tournament"
case _drawDate = "drawDate"
case _drawSeed = "drawSeed"
case _drawMatchIndex = "drawMatchIndex"
case _drawTeamPosition = "drawTeamPosition"
}
func insertOnServer() throws {
self.tournamentStore.drawLogs.writeChangeAndInsertOnServer(instance: self)
}
}

@ -164,22 +164,8 @@ defer {
} }
@discardableResult @discardableResult
func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int { func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int {
let matchIndex = index let matchIndex = index
var teamPosition : TeamPosition {
if let slot {
return slot
} else {
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
return teamPosition
}
}
previousMatch(teamPosition)?.disableMatch() previousMatch(teamPosition)?.disableMatch()
return matchIndex * 2 + teamPosition.rawValue return matchIndex * 2 + teamPosition.rawValue
} }
@ -247,6 +233,7 @@ defer {
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState() currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
} }
func resetScores() { func resetScores() {
@ -548,7 +535,7 @@ defer {
if endDate == nil { if endDate == nil {
endDate = Date() endDate = Date()
} }
teams().forEach({ $0.resetRestingTime() })
winningTeamId = teamScoreWinning.teamRegistration winningTeamId = teamScoreWinning.teamRegistration
losingTeamId = teamScoreWalkout.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
@ -571,6 +558,8 @@ defer {
teamOne?.hasArrived() teamOne?.hasArrived()
teamTwo?.hasArrived() teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
winningTeamId = teamOne?.id winningTeamId = teamOne?.id
losingTeamId = teamTwo?.id losingTeamId = teamTwo?.id
@ -937,6 +926,10 @@ defer {
(teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow
} }
func isValidSpot() -> Bool {
previousMatches().allSatisfy({ $0.isSeeded() == false })
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _round = "round" case _round = "round"

@ -493,8 +493,16 @@ final class MatchScheduler : ModelObject, Storable {
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty {
print("Handling break time conflicts or waiting for free courts") print("Handling break time conflicts or waiting for free courts")
let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }
let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
if let courtsUnavailability, previousEndDate != nil {
previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
if let courtsUnavailability, previousEndDateNoBreak != nil {
previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak }
@ -651,7 +659,12 @@ final class MatchScheduler : ModelObject, Storable {
} }
if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count {
print("All courts in rotation \(rotationIndex) are free") print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)")
}
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability)
return computedStartDateAndCourts.earliestFreeDate
} }
return minimumTargetedEndDate return minimumTargetedEndDate

@ -116,13 +116,39 @@ final class TeamRegistration: ModelObject, Storable {
} }
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding) var teamPosition : TeamPosition {
if let slot {
return slot
} else {
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 = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
return teamPosition
}
}
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: teamPosition)
tournamentObject()?.resetTeamScores(in: bracketPosition) tournamentObject()?.resetTeamScores(in: bracketPosition)
self.bracketPosition = seedPosition self.bracketPosition = seedPosition
if groupStagePosition != nil && qualified == false { if groupStagePosition != nil && qualified == false {
qualified = true qualified = true
} }
tournamentObject()?.updateTeamScores(in: bracketPosition) if let tournament = tournamentObject() {
if let index = index(in: tournament.selectedSortedTeams()) {
let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition)
do {
try tournamentStore.drawLogs.addOrUpdate(instance: drawLog)
} catch {
Logger.error(error)
}
}
tournament.updateTeamScores(in: bracketPosition)
}
} }
func expectedSummonDate() -> Date? { func expectedSummonDate() -> Date? {
@ -532,8 +558,21 @@ final class TeamRegistration: ModelObject, Storable {
} }
} }
var _cachedRestingTime: (Bool, Date?)?
func restingTime() -> Date? { func restingTime() -> Date? {
matches().sorted(by: \.computedEndDateForSorting).last?.endDate if let _cachedRestingTime { return _cachedRestingTime.1 }
let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate
_cachedRestingTime = (true, restingTime)
return restingTime
}
func resetRestingTime() {
_cachedRestingTime = nil
}
var restingTimeForSorting: Date {
restingTime()!
} }
func teamNameLabel() -> String { func teamNameLabel() -> String {
@ -544,6 +583,17 @@ final class TeamRegistration: ModelObject, Storable {
} }
} }
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition
} else if let bracketPosition {
return true
} else if let drawMatchIndex {
return true
}
return false
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _tournament = "tournament" case _tournament = "tournament"

@ -1,5 +1,5 @@
// //
// Tournament.swift // swift
// PadelClub // PadelClub
// //
// Created by Laurent Morvillier on 02/02/2024. // Created by Laurent Morvillier on 02/02/2024.
@ -120,7 +120,11 @@ final class Tournament : ModelObject, Storable {
self.startDate = startDate self.startDate = startDate
self.endDate = endDate self.endDate = endDate
self.creationDate = creationDate self.creationDate = creationDate
#if DEBUG
self.isPrivate = false
#else
self.isPrivate = Guard.main.purchasedTransactions.isEmpty self.isPrivate = Guard.main.purchasedTransactions.isEmpty
#endif
self.groupStageFormat = groupStageFormat self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat self.roundFormat = roundFormat
self.loserRoundFormat = loserRoundFormat self.loserRoundFormat = loserRoundFormat
@ -142,16 +146,24 @@ final class Tournament : ModelObject, Storable {
self.entryFee = entryFee self.entryFee = entryFee
self.additionalEstimationDuration = additionalEstimationDuration self.additionalEstimationDuration = additionalEstimationDuration
self.isDeleted = isDeleted self.isDeleted = isDeleted
#if DEBUG
self.publishTeams = true
self.publishSummons = true
self.publishBrackets = true
self.publishGroupStages = true
self.publishRankings = true
#else
self.publishTeams = publishTeams self.publishTeams = publishTeams
self.publishSummons = publishSummons self.publishSummons = publishSummons
self.publishBrackets = publishBrackets self.publishBrackets = publishBrackets
self.publishGroupStages = publishGroupStages self.publishGroupStages = publishGroupStages
self.publishRankings = publishRankings
#endif
self.shouldVerifyBracket = shouldVerifyBracket self.shouldVerifyBracket = shouldVerifyBracket
self.shouldVerifyGroupStage = shouldVerifyGroupStage self.shouldVerifyGroupStage = shouldVerifyGroupStage
self.hideTeamsWeight = hideTeamsWeight self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
self.loserBracketMode = loserBracketMode self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount self.initialSeedCount = initialSeedCount
@ -335,6 +347,12 @@ final class Tournament : ModelObject, Storable {
override func deleteDependencies() throws { override func deleteDependencies() throws {
let store = self.tournamentStore let store = self.tournamentStore
let drawLogs = self.tournamentStore.drawLogs
for drawLog in drawLogs {
try drawLog.deleteDependencies()
}
store.drawLogs.deleteDependencies(drawLogs)
let teams = self.tournamentStore.teamRegistrations let teams = self.tournamentStore.teamRegistrations
for team in teams { for team in teams {
try team.deleteDependencies() try team.deleteDependencies()
@ -552,7 +570,7 @@ defer {
return endDate != nil return endDate != nil
} }
func state() -> Tournament.State { func state() -> State {
if self.isCanceled == true { if self.isCanceled == true {
return .canceled return .canceled
} }
@ -1948,7 +1966,7 @@ defer {
func labelIndexOf(team: TeamRegistration) -> String? { func labelIndexOf(team: TeamRegistration) -> String? {
if let teamIndex = indexOf(team: team) { if let teamIndex = indexOf(team: team) {
return "#" + (teamIndex + 1).formatted() return "Tête de série #" + (teamIndex + 1).formatted()
} else { } else {
return nil return nil
} }
@ -2257,6 +2275,128 @@ defer {
rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) }
} }
func seedsCount() -> Int {
selectedSortedTeams().count - groupStageSpots()
}
func lastDrawnDate() -> Date? {
drawLogs().last?.drawDate
}
func drawLogs() -> [DrawLog] {
self.tournamentStore.drawLogs.sorted(by: \.drawDate)
}
func seedSpotsLeft() -> Bool {
let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false })
if alreadySeededRounds.isEmpty { return true }
let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() }
return spotsLeft.isEmpty == false
}
func isRoundValidForSeeding(roundIndex: Int) -> Bool {
if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) {
return roundIndex >= lastRoundWithSeeds.index
} else {
return true
}
}
func updateSeedsBracketPosition() async {
await removeAllSeeds()
let drawLogs = drawLogs().reversed()
let seeds = seeds()
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
} catch {
Logger.error(error)
}
}
func removeAllSeeds() async {
unsortedTeams().forEach({ team in
team.bracketPosition = nil
})
let ts = allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
allRounds().forEach({ round in
round.enableRound()
})
}
func addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
func exportedDrawLogs() -> String {
var logs : [String] = ["Journal des tirages\n\n"]
logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n"))
return logs.joined()
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {

@ -26,6 +26,7 @@ class TournamentStore: Store, ObservableObject {
fileprivate(set) var teamScores: StoredCollection<TeamScore> = StoredCollection.placeholder() fileprivate(set) var teamScores: StoredCollection<TeamScore> = StoredCollection.placeholder()
fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder() fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder()
fileprivate(set) var drawLogs: StoredCollection<DrawLog> = StoredCollection.placeholder()
convenience init(tournament: Tournament) { convenience init(tournament: Tournament) {
self.init(identifier: tournament.id, parameter: "tournament") self.init(identifier: tournament.id, parameter: "tournament")
@ -51,6 +52,7 @@ class TournamentStore: Store, ObservableObject {
self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed) self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed) self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed) self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed)
self.drawLogs = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.loadCollectionsFromServerIfNoFile() self.loadCollectionsFromServerIfNoFile()

@ -996,6 +996,15 @@ enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
return shortName return shortName
} }
} }
func localizedBranchLabel() -> String {
switch self {
case .one:
return "Branche du haut"
case .two:
return "Branche du bas"
}
}
} }
enum SetFormat: Int, Hashable, Codable { enum SetFormat: Int, Hashable, Codable {

@ -8,20 +8,20 @@
import SwiftUI import SwiftUI
protocol SpinDrawable { protocol SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String]
} }
extension String: SpinDrawable { extension String: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
[self] [self]
} }
} }
extension Match: SpinDrawable { extension Match: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
let teams = teams() let teams = teams()
if teams.count == 1 { if teams.count == 1, hideNames == false {
return teams.first!.segmentLabel(displayStyle) return teams.first!.segmentLabel(displayStyle, hideNames: hideNames)
} else { } else {
return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 } return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 }
} }
@ -29,12 +29,16 @@ extension Match: SpinDrawable {
} }
extension TeamRegistration: SpinDrawable { extension TeamRegistration: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
var strings: [String] = [] var strings: [String] = []
let indexLabel = tournamentObject()?.labelIndexOf(team: self) let indexLabel = tournamentObject()?.labelIndexOf(team: self)
if let indexLabel { if let indexLabel {
strings.append(indexLabel) strings.append(indexLabel)
if hideNames {
return strings
}
} }
strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) }) strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) })
return strings return strings
} }
@ -51,8 +55,8 @@ struct DrawOption: Identifiable, SpinDrawable {
let initialIndex: Int let initialIndex: Int
let option: SpinDrawable let option: SpinDrawable
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
option.segmentLabel(displayStyle) option.segmentLabel(displayStyle, hideNames: hideNames)
} }
} }
@ -62,6 +66,7 @@ struct SpinDrawView: View {
let drawees: [any SpinDrawable] let drawees: [any SpinDrawable]
@State var segments: [any SpinDrawable] @State var segments: [any SpinDrawable]
var autoMode: Bool = false var autoMode: Bool = false
var hideNames: Bool = false
let completion: ([DrawResult]) async -> Void // Completion closure let completion: ([DrawResult]) async -> Void // Completion closure
@State private var drawCount: Int = 0 @State private var drawCount: Int = 0
@ -89,12 +94,12 @@ struct SpinDrawView: View {
} }
} else if drawCount < drawees.count { } else if drawCount < drawees.count {
Section { Section {
_segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center) _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center)
} }
Section { Section {
ZStack { ZStack {
FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode, hideNames: hideNames) { index in
self.selectedIndex = index self.selectedIndex = index
self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex)) self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex))
self.drawOptions.remove(at: index) self.drawOptions.remove(at: index)
@ -209,8 +214,8 @@ struct SpinDrawView: View {
private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View { private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View {
VStack(alignment: horizontalAlignment, spacing: 0.0) { VStack(alignment: horizontalAlignment, spacing: 0.0) {
ForEach(segment, id: \.self) { string in ForEach(segment.indices, id: \.self) { lineIndex in
Text(string).font(.title3) Text(segment[lineIndex]).font(.title3)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.lineLimit(1) .lineLimit(1)
} }
@ -221,13 +226,13 @@ struct SpinDrawView: View {
private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View { private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View {
VStack(spacing: 0.0) { VStack(spacing: 0.0) {
let draw = drawees[drawee] let draw = drawees[drawee]
_segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .center) _segmentLabelView(segment: draw.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center)
if result as? TeamRegistration != nil { if result as? TeamRegistration != nil {
Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed) Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed)
} else { } else {
Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed) Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed)
} }
_segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .center) _segmentLabelView(segment: result.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center)
} }
} }
} }
@ -236,10 +241,11 @@ struct FortuneWheelContainerView: View {
@State private var rotation: Double = 0 @State private var rotation: Double = 0
let segments: [any SpinDrawable] let segments: [any SpinDrawable]
let autoMode: Bool let autoMode: Bool
let hideNames: Bool
let completion: (Int) -> Void // Completion closure let completion: (Int) -> Void // Completion closure
var body: some View { var body: some View {
FortuneWheelView(segments: segments) FortuneWheelView(segments: segments, hideNames: hideNames)
.rotationEffect(.degrees(rotation)) .rotationEffect(.degrees(rotation))
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.padding(.top, 5) .padding(.top, 5)
@ -303,6 +309,7 @@ struct FortuneWheelContainerView: View {
struct FortuneWheelView: View { struct FortuneWheelView: View {
let segments: [any SpinDrawable] let segments: [any SpinDrawable]
let hideNames: Bool
let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown] let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown]
func getColor(forIndex index: Int) -> Color { func getColor(forIndex index: Int) -> Color {
@ -333,9 +340,9 @@ struct FortuneWheelView: View {
.fill(getColor(forIndex: index)) .fill(getColor(forIndex: index))
VStack(alignment: .trailing, spacing: 0.0) { VStack(alignment: .trailing, spacing: 0.0) {
let strings = segments[index].segmentLabel(.short) let strings = labels(forIndex: index)
ForEach(strings, id: \.self) { string in ForEach(strings.indices, id: \.self) { lineIndex in
Text(string).bold() Text(strings[lineIndex]).bold()
.font(.subheadline) .font(.subheadline)
} }
} }
@ -349,6 +356,19 @@ struct FortuneWheelView: View {
} }
} }
private func labels(forIndex index: Int) -> [String] {
if segments.count < 5 {
return segments[index].segmentLabel(.short, hideNames: hideNames)
} else {
let values = segments[index].segmentLabel(.short, hideNames: hideNames)
if values.count < 3 {
return values
} else {
return Array(segments[index].segmentLabel(.short, hideNames: hideNames).prefix(1))
}
}
}
// Calculate the position for the text in the middle of the arc segment // Calculate the position for the text in the middle of the arc segment
private func arcPosition(index: Int, radius: Double) -> CGPoint { private func arcPosition(index: Int, radius: Double) -> CGPoint {
let segmentAngle = 360.0 / Double(segments.count) let segmentAngle = 360.0 / Double(segments.count)

@ -78,16 +78,8 @@ struct PlayerBlockView: View {
} }
} }
if displayRestingTime, let restingTime = team?.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) { if displayRestingTime, let team {
if restingTime > -300 { TeamRowView.TeamRestingView(team: team)
Text("vient de finir")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("en repos depuis " + value)
.font(.footnote)
.foregroundStyle(.secondary)
}
} }
} }
.bold(hasWon) .bold(hasWon)

@ -15,9 +15,6 @@ struct CourtAvailabilitySettingsView: View {
let event: Event let event: Event
@State private var showingPopover: Bool = false @State private var showingPopover: Bool = false
@State private var courtIndex: Int = 0
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
@State private var editingSlot: DateInterval? @State private var editingSlot: DateInterval?
var courtsUnavailability: [Int: [DateInterval]] { var courtsUnavailability: [Int: [DateInterval]] {
@ -45,10 +42,6 @@ struct CourtAvailabilitySettingsView: View {
} }
Button("éditer") { Button("éditer") {
editingSlot = dateInterval editingSlot = dateInterval
courtIndex = dateInterval.courtIndex
startDate = dateInterval.startDate
endDate = dateInterval.endDate
showingPopover = true
} }
Button("effacer", role: .destructive) { Button("effacer", role: .destructive) {
do { do {
@ -110,8 +103,6 @@ struct CourtAvailabilitySettingsView: View {
Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.")
} actions: { } actions: {
RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
} }
@ -120,8 +111,6 @@ struct CourtAvailabilitySettingsView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
} }
@ -130,6 +119,58 @@ struct CourtAvailabilitySettingsView: View {
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible") .navigationTitle("Créneau indisponible")
.sheet(isPresented: $showingPopover) { .sheet(isPresented: $showingPopover) {
CourtAvailabilityEditorView(event: event)
}
.sheet(item: $editingSlot) { editingSlot in
CourtAvailabilityEditorView(editingSlot: editingSlot, event: event)
}
}
}
struct CourtPicker: View {
@Environment(Tournament.self) var tournament: Tournament
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text(tournament.courtName(atIndex: $0))
}
}
}
}
struct CourtAvailabilityEditorView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
var editingSlot: DateInterval?
let event: Event
@State private var courtIndex: Int
@State private var startDate: Date
@State private var endDate: Date
init(editingSlot: DateInterval, event: Event) {
self.editingSlot = editingSlot
self.event = event
_courtIndex = .init(wrappedValue: editingSlot.courtIndex)
_startDate = .init(wrappedValue: editingSlot.startDate)
_endDate = .init(wrappedValue: editingSlot.endDate)
}
init(event: Event) {
self.event = event
_courtIndex = .init(wrappedValue: 0)
let startDate = event.eventStartDate()
_startDate = .init(wrappedValue: event.eventStartDate())
_endDate = .init(wrappedValue: startDate.addingTimeInterval(5400))
}
var body: some View {
NavigationStack { NavigationStack {
Form { Form {
Section { Section {
@ -153,11 +194,24 @@ struct CourtAvailabilitySettingsView: View {
} footer: { } footer: {
FooterButtonView("jour entier") { FooterButtonView("jour entier") {
startDate = startDate.startOfDay startDate = startDate.startOfDay
endDate = startDate.endOfDay() endDate = startDate.tomorrowAtNine.startOfDay
}
}
Section {
DateAdjusterView(date: $startDate)
} header: {
Text("Modifier rapidement l'horaire de début")
} }
Section {
DateAdjusterView(date: $endDate)
} header: {
Text("Modifier rapidement l'horaire de fin")
} }
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView { ButtonValidateView {
if editingSlot == nil { if editingSlot == nil {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
@ -176,40 +230,50 @@ struct CourtAvailabilitySettingsView: View {
Logger.error(error) Logger.error(error)
} }
} }
showingPopover = false
dismiss()
}
}
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
} }
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau") .navigationTitle(_navigationTitle())
.tint(.master) .tint(.master)
} }
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1
}
} }
private func _navigationTitle() -> String {
editingSlot == nil ? "Nouveau créneau" : "Édition du créneau"
} }
} }
struct CourtPicker: View { struct DateAdjusterView: View {
@Environment(Tournament.self) var tournament: Tournament @Binding var date: Date
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View { var body: some View {
Picker(title, selection: $selection) { HStack {
ForEach(0..<maxCourt, id: \.self) { _createButton(label: "-1h", timeOffset: -1, component: .hour)
Text(tournament.courtName(atIndex: $0)) _createButton(label: "-30m", timeOffset: -30, component: .minute)
_createButton(label: "+30m", timeOffset: 30, component: .minute)
_createButton(label: "+1h", timeOffset: 1, component: .hour)
} }
.font(.headline)
} }
private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View {
Button(action: {
date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date
}) {
Text(label)
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderedProminent)
.tint(.master)
} }
} }
//#Preview {
// CourtAvailabilitySettingsView(event: Event.mock())
//}

@ -0,0 +1,69 @@
//
// DrawLogsView.swift
// PadelClub
//
// Created by razmig on 22/10/2024.
//
import SwiftUI
import LeStorage
struct DrawLogsView: View {
@Environment(Tournament.self) var tournament
var drawLogs: [DrawLog] {
tournament.drawLogs().reversed()
}
var body: some View {
List {
ForEach(drawLogs) { drawLog in
HStack {
VStack(alignment: .leading) {
Text(drawLog.localizedDrawSeedLabel())
Text(drawLog.drawDate.localizedDate())
.font(.footnote)
.foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text(drawLog.positionLabel()).lineLimit(1).truncationMode(.middle)
Text(drawLog.localizedDrawBranch())
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
}
.overlay(content: {
if drawLogs.isEmpty {
ContentUnavailableView("Aucun tirage", systemImage: "dice", description: Text("Aucun tirage au sort n'a été effectué."))
}
})
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) {
Menu {
ShareLink(item: tournament.exportedDrawLogs()) {
Label("Partager les tirages", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
Divider()
Button("Tout effacer", role: .destructive) {
do {
try tournament.tournamentStore.drawLogs.deleteAll()
} catch {
Logger.error(error)
}
}
} label: {
LabelOptions()
}
}
})
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Journal des tirages")
}
}

@ -0,0 +1,107 @@
//
// PreviewBracketPositionView.swift
// PadelClub
//
// Created by razmig on 23/10/2024.
//
import SwiftUI
struct PreviewBracketPositionView: View {
let seeds: [TeamRegistration]
let drawLogs: [DrawLog]
@State private var filterOption: PreviewBracketPositionFilterOption = .difference
enum PreviewBracketPositionFilterOption: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case all
case difference
case summon
func isDisplayable(_ team: TeamRegistration, drawLog: DrawLog?) -> Bool {
switch self {
case .all:
true
case .difference:
team.isDifferentPosition(drawLog?.computedBracketPosition()) == true
case .summon:
team.callDate != drawLog?.drawMatch()?.startDate
}
}
}
var body: some View {
List {
Section {
ForEach(seeds.indices, id: \.self) { seedIndex in
let seed = seeds[seedIndex]
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
if filterOption.isDisplayable(seed, drawLog: drawLog) {
HStack {
VStack(alignment: .leading) {
Text("Tête de série #\(seedIndex + 1)").font(.caption)
TeamRowView.TeamView(team: seed)
TeamRowView.TeamCallDateView(team: seed)
}
Spacer()
if let drawLog {
VStack(alignment: .trailing) {
Text(drawLog.roundLabel()).lineLimit(1).truncationMode(.middle).font(.caption)
Text(drawLog.matchLabel()).lineLimit(1).truncationMode(.middle)
Text(drawLog.localizedDrawBranch())
if let expectedDate = drawLog.drawMatch()?.startDate {
Text(expectedDate.localizedDate())
.font(.caption)
} else {
Text("Aucun horaire")
.font(.caption)
}
}
}
}
.listRowView(isActive: true, color: seed.isDifferentPosition(drawLog?.computedBracketPosition()) ? .logoRed : .green, hideColorVariation: true)
}
}
} header: {
Picker(selection: $filterOption) {
Text("Tous").tag(PreviewBracketPositionFilterOption.all)
Text("Changements").tag(PreviewBracketPositionFilterOption.difference)
Text("Convoc.").tag(PreviewBracketPositionFilterOption.summon)
} label: {
Text("Filter")
}
.labelsHidden()
.pickerStyle(.segmented)
.textCase(nil)
}
}
.overlay(content: {
if seeds.isEmpty {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Aucun tête de série dans le tournoi."))
} else if filterOption == .difference, noSeedHasDifferentPlace() {
ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau."))
} else if filterOption == .summon, noSeedHasDifferentSummon() {
ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau."))
}
})
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Aperçu du tableau tiré")
}
func noSeedHasDifferentPlace() -> Bool {
seeds.enumerated().allSatisfy({ seedIndex, seed in
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
return seed.isDifferentPosition(drawLog?.computedBracketPosition()) == false
})
}
func noSeedHasDifferentSummon() -> Bool {
seeds.enumerated().allSatisfy({ seedIndex, seed in
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
return seed.callDate == drawLog?.drawMatch()?.startDate
})
}
}

@ -74,6 +74,32 @@ struct RoundSettingsView: View {
} }
} }
let previewAvailable = tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount() && tournament.lastDrawnDate() != nil && tournament.seedSpotsLeft()
Section {
NavigationLink {
DrawLogsView()
.environment(tournament)
} label: {
Text("Gestionnaire des tirages au sort")
}
if previewAvailable {
NavigationLink {
PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs())
} label: {
Text("Aperçu du repositionnement")
}
RowButtonView("Replacer toutes les têtes de série", role: .destructive) {
await tournament.updateSeedsBracketPosition()
}
}
} footer: {
if previewAvailable {
Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.")
}
}
// Section { // Section {
// RowButtonView("Enabled", role: .destructive) { // RowButtonView("Enabled", role: .destructive) {
// let allMatches = tournament._allMatchesIncludingDisabled() // let allMatches = tournament._allMatchesIncludingDisabled()
@ -127,72 +153,12 @@ struct RoundSettingsView: View {
} }
private func _removeAllSeeds() async { private func _removeAllSeeds() async {
tournament.unsortedTeams().forEach({ team in await tournament.removeAllSeeds()
team.bracketPosition = nil
})
let ts = tournament.allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch {
Logger.error(error)
}
tournament.allRounds().forEach({ round in
round.enableRound()
})
self.isEditingTournamentSeed.wrappedValue = true self.isEditingTournamentSeed.wrappedValue = true
} }
private func _addNewRound(_ roundIndex: Int) async { private func _addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) await tournament.addNewRound(roundIndex)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournament.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
} }
private func _removeRound(_ lastRound: Round) async { private func _removeRound(_ lastRound: Round) async {

@ -18,6 +18,7 @@ struct RoundView: View {
@State private var selectedSeedGroup: SeedInterval? @State private var selectedSeedGroup: SeedInterval?
@State private var showPrintScreen: Bool = false @State private var showPrintScreen: Bool = false
@State private var hideNames: Bool = true
var upperRound: UpperRound var upperRound: UpperRound
@ -37,14 +38,14 @@ struct RoundView: View {
let displayableMatches: [Match] = self.upperRound.round.playedMatches() let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in return displayableMatches.filter { match in
match.teamScores.count == 1 match.teamScores.count == 1
} }.filter({ $0.isValidSpot() })
} }
private var seedSpaceLeft: [Match] { private var seedSpaceLeft: [Match] {
let displayableMatches: [Match] = self.upperRound.round.playedMatches() let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in return displayableMatches.filter { match in
match.teamScores.count == 0 match.teamScores.count == 0
} }.filter({ $0.isValidSpot() })
} }
private var availableSeedGroup: SeedInterval? { private var availableSeedGroup: SeedInterval? {
@ -121,6 +122,7 @@ struct RoundView: View {
} }
} }
} else { } else {
let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index)
let availableSeeds = tournament.availableSeeds() let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableQualifiedTeams = tournament.availableQualifiedTeams()
@ -140,6 +142,12 @@ struct RoundView: View {
if (availableSeedGroup.isFixed() == false) { if (availableSeedGroup.isFixed() == false) {
Section { Section {
Toggle(isOn: $hideNames) {
Text("Masquer les noms")
if hideNames {
Text("Réalise un tirage des positions.")
}
}
RowButtonView("Tirage au sort \(availableSeedGroup.localizedInterval()) visuel") { RowButtonView("Tirage au sort \(availableSeedGroup.localizedInterval()) visuel") {
self.selectedSeedGroup = availableSeedGroup self.selectedSeedGroup = availableSeedGroup
} }
@ -152,7 +160,6 @@ struct RoundView: View {
if availableQualifiedTeams.isEmpty == false { if availableQualifiedTeams.isEmpty == false {
let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false
let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft
if availableSeedSpot.isEmpty == false {
Section { Section {
DisclosureGroup { DisclosureGroup {
ForEach(availableQualifiedTeams) { team in ForEach(availableQualifiedTeams) { team in
@ -174,65 +181,25 @@ struct RoundView: View {
} label: { } label: {
TeamRowView(team: team, displayCallDate: false) TeamRowView(team: team, displayCallDate: false)
} }
.disabled(availableSeedSpot.isEmpty || isRoundValidForSeeding == false)
} }
} label: { } label: {
Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count)
} }
} header: { } header: {
Text("Tirage au sort visuel d'un qualifié").font(.subheadline) Text("Tirage au sort visuel d'un qualifié").font(.subheadline)
} footer: {
if availableSeedSpot.isEmpty || isRoundValidForSeeding == false {
Text("Aucune place disponible !")
.foregroundStyle(.red)
} }
} }
} }
if availableSeeds.isEmpty == false { if availableSeeds.isEmpty == false {
if seedSpaceLeft.isEmpty == false { let spots = (seedSpaceLeft.isEmpty == false) ? seedSpaceLeft : spaceLeft
Section { let opposingSeeding = (seedSpaceLeft.isEmpty == false) ? false : true
DisclosureGroup { _drawSection(availableSeeds: availableSeeds, spots: spots, opposingSeeding: opposingSeeding, isRoundValidForSeeding: isRoundValidForSeeding)
ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false)
}
_save(seeds: [team])
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
}
} else if spaceLeft.isEmpty == false {
Section {
DisclosureGroup {
ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: spaceLeft) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
}
_save(seeds: [team])
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
}
}
} }
} }
@ -306,7 +273,7 @@ struct RoundView: View {
let seeds = _seeds(availableSeedGroup: availableSeedGroup) let seeds = _seeds(availableSeedGroup: availableSeedGroup)
let availableSeedSpot = _availableSeedSpot(availableSeedGroup: availableSeedGroup) let availableSeedSpot = _availableSeedSpot(availableSeedGroup: availableSeedGroup)
NavigationStack { NavigationStack {
SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true) { draws in SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true, hideNames: hideNames) { draws in
Task { Task {
draws.forEach { drawResult in draws.forEach { drawResult in
seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding)
@ -399,18 +366,45 @@ struct RoundView: View {
} }
} }
} }
private func _drawSection(availableSeeds: [TeamRegistration], spots: [Match], opposingSeeding: Bool, isRoundValidForSeeding: Bool) -> some View {
Section {
DisclosureGroup {
ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: spots) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: spots[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding)
} }
//#Preview { _save(seeds: [team])
// RoundView(round: Round.mock()) }
// .environment(Tournament.mock()) }
//} } label: {
TeamRowView(team: team, displayCallDate: false)
}
.disabled(spots.isEmpty || isRoundValidForSeeding == false)
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
} footer: {
if spots.isEmpty || isRoundValidForSeeding == false {
Text("Aucune place disponible ! Ajouter une manche via les réglages du tableau.")
.foregroundStyle(.red)
}
}
}
}
struct MatchSpot: SpinDrawable { struct MatchSpot: SpinDrawable {
let match: Match let match: Match
let teamPosition: TeamPosition let teamPosition: TeamPosition
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
[match.roundTitle(), matchTitle(displayStyle: displayStyle)].compactMap { $0 } [match.roundTitle(), matchTitle(displayStyle: displayStyle)].compactMap { $0 }
} }

@ -9,87 +9,57 @@ import SwiftUI
struct TeamRestingView: View { struct TeamRestingView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var sortingMode: SortingMode = .restingTime @State private var displayMode: DisplayMode = .teams
@State private var selectedCourt: Int? @State private var selectedCourt: Int?
@State private var readyMatches: [Match] = [] @State private var readyMatches: [Match] = []
@State private var matchesLeft: [Match] = [] @State private var matchesLeft: [Match] = []
@State private var teams: [TeamRegistration] = []
enum SortingMode: Int, Identifiable, CaseIterable { enum DisplayMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
case index case teams
case restingTime case restingTime
case court
func localizedSortingModeLabel() -> String { func localizedSortingModeLabel() -> String {
switch self { switch self {
case .index: case .teams:
return "Ordre" return "Équipes"
case .court:
return "Terrain"
case .restingTime: case .restingTime:
return "Repos" return "Matchs"
} }
} }
} }
var sortingModeCases: [SortingMode] {
var sortingModes = [SortingMode]()
sortingModes.append(.index)
sortingModes.append(.restingTime)
sortingModes.append(.court)
return sortingModes
}
func contentUnavailableDescriptionLabel() -> String { func contentUnavailableDescriptionLabel() -> String {
switch sortingMode { switch displayMode {
case .index:
return "Ce tournoi n'a aucun match prêt à démarrer"
case .restingTime: case .restingTime:
return "Ce tournoi n'a aucun match prêt à démarrer" return "Ce tournoi n'a aucun match prêt à démarrer"
case .court: case .teams:
return "Ce tournoi n'a aucun match prêt à démarrer" return "Ce tournoi n'a aucune équipe ayant déjà terminé un match."
} }
} }
var sortedMatches: [Match] { var sortedMatches: [Match] {
switch sortingMode {
case .index:
return readyMatches
case .restingTime:
return readyMatches.sorted(by: \.restingTimeForSorting) return readyMatches.sorted(by: \.restingTimeForSorting)
case .court:
return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending)
} }
var sortedTeams: [TeamRegistration] {
return teams
} }
var body: some View { var body: some View {
List { List {
Section { Section {
Picker(selection: $selectedCourt) { switch displayMode {
Text("Aucun").tag(nil as Int?) case .teams:
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in if sortedTeams.isEmpty == false {
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex as Int?) ForEach(sortedTeams) { team in
} TeamRowView(team: team, displayRestingTime: true)
} label: {
Text("Sur le terrain")
} }
// } else {
// Toggle(isOn: $checkCanPlay) { ContentUnavailableView("Aucune équipe en repos", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
// if isFree {
// Text("Vérifier le paiement ou la présence")
// } else {
// Text("Vérifier la présence")
// }
// }
// } footer: {
// if isFree {
// Text("Masque les matchs où un ou plusieurs joueurs qui ne sont pas encore arrivé")
// } else {
// Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé")
// }
} }
case .restingTime:
Section {
if sortedMatches.isEmpty == false { if sortedMatches.isEmpty == false {
ForEach(sortedMatches) { match in ForEach(sortedMatches) { match in
MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt)
@ -97,23 +67,23 @@ struct TeamRestingView: View {
} else { } else {
ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
} }
}
} header: { } header: {
Picker(selection: $sortingMode) { Picker(selection: $displayMode) {
ForEach(sortingModeCases) { sortingMode in ForEach(DisplayMode.allCases) { sortingMode in
Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode) Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode)
} }
} label: { } label: {
Text("Méthode de tri") Text("Affichage")
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.segmented) .pickerStyle(.segmented)
} }
.headerProminence(.increased)
.textCase(nil) .textCase(nil)
} }
.toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Temps de repos")
.navigationTitle("Match à suivre")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.onAppear { .onAppear {
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
let matchesLeft = tournament.matchesLeft(allMatches) let matchesLeft = tournament.matchesLeft(allMatches)
@ -121,6 +91,7 @@ struct TeamRestingView: View {
let readyMatches = tournament.readyMatches(allMatches) let readyMatches = tournament.readyMatches(allMatches)
self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.matchesLeft = matchesLeft self.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)
} }
} }
} }

@ -12,50 +12,93 @@ struct TeamRowView: View {
var team: TeamRegistration var team: TeamRegistration
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false var displayCallDate: Bool = false
var displayRestingTime: Bool = false
var body: some View { var body: some View {
LabeledContent { LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { TeamHeadlineView(team: team)
if let groupStage = team.groupStageObject() { TeamView(team: team)
HStack { }
Text(groupStage.groupStageTitle(.title)) if displayCallDate {
if let finalPosition = groupStage.finalPosition(ofTeam: team) { TeamCallDateView(team: team)
Text((finalPosition + 1).ordinalFormatted()) }
if displayRestingTime {
TeamRestingView(team: team)
} }
} }
} else if let round = team.initialRound() {
Text(round.roundTitle(.wide))
} }
if let wildcardLabel = team.wildcardLabel() { struct TeamRestingView: View {
Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption) let team: TeamRegistration
@ViewBuilder
var body: some View {
if let restingTime = team.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) {
if restingTime > -300 {
Text("vient de finir")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("en repos depuis " + value)
.font(.footnote)
.foregroundStyle(.secondary)
}
} }
} }
}
struct TeamView: View {
let team: TeamRegistration
var body: some View {
if let name = team.name, name.isEmpty == false { if let name = team.name, name.isEmpty == false {
Text(name).foregroundStyle(.secondary).font(.footnote) Text(name).foregroundStyle(.secondary).font(.footnote)
if team.players().isEmpty { if team.players().isEmpty {
Text("Aucun joueur") Text("Aucun joueur")
} else { } else {
ForEach(team.players()) { player in CompactTeamView(team: team)
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
} }
} else { } else {
if team.players().isEmpty == false { if team.players().isEmpty == false {
ForEach(team.players()) { player in CompactTeamView(team: team)
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
} else { } else {
Text("Place réservée") Text("Place réservée")
Text("Place réservée") Text("Place réservée")
} }
} }
} }
if displayCallDate { }
struct TeamHeadlineView: View {
let team: TeamRegistration
var body: some View {
HStack {
if let groupStage = team.groupStageObject() {
HStack {
Text(groupStage.groupStageTitle(.title))
if let finalPosition = groupStage.finalPosition(ofTeam: team) {
Text((finalPosition + 1).ordinalFormatted())
}
}
} else if let round = team.initialRound() {
Text(round.roundTitle(.wide))
}
if let wildcardLabel = team.wildcardLabel() {
Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption)
}
}
}
}
struct TeamCallDateView: View {
let team: TeamRegistration
var body: some View {
if let callDate = team.callDate { if let callDate = team.callDate {
Text("Déjà convoquée \(callDate.localizedDate())") Text("Déjà convoquée \(callDate.localizedDate())")
.foregroundStyle(.logoRed) .foregroundStyle(.logoRed)
@ -69,9 +112,14 @@ struct TeamRowView: View {
} }
} }
} }
struct CompactTeamView: View {
let team: TeamRegistration
var body: some View {
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
}
} }
} }
//#Preview {
// TeamRowView(team: TeamRegistration.mock())
//}

@ -102,18 +102,14 @@ struct BroadcastView: View {
} }
Section { Section {
Toggle(isOn: $tournament.isPrivate) { Toggle("Visible sur Padel Club", isOn: Binding(
Text("Tournoi privé") get: { !tournament.isPrivate },
} set: { tournament.isPrivate = !$0 }
))
Toggle(isOn: $tournament.hideTeamsWeight) { Toggle(isOn: $tournament.hideTeamsWeight) {
Text("Masquer les poids des équipes") Text("Masquer les poids des équipes")
} }
} footer: {
let verb : String = tournament.isPrivate ? "est" : "sera"
let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))"
Text(.init(footerString))
} }
if tournament.isPrivate == false { if tournament.isPrivate == false {

@ -370,4 +370,22 @@ final class ServerDataTests: XCTestCase {
} }
func testDrawLog() async throws {
let tournament: [Tournament] = try await StoreCenter.main.service().get()
guard let tournamentId = tournament.first?.id else {
assertionFailure("missing tournament in database")
return
}
let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two)
let d: DrawLog = try await StoreCenter.main.service().post(drawLog)
assert(d.tournament == drawLog.tournament)
assert(d.drawDate.formatted() == drawLog.drawDate.formatted())
assert(d.drawSeed == drawLog.drawSeed)
assert(d.drawTeamPosition == drawLog.drawTeamPosition)
assert(d.drawMatchIndex == drawLog.drawMatchIndex)
}
} }

Loading…
Cancel
Save