sync2
Laurent 9 months ago
commit f46890c445
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 1
      PadelClub/Data/Federal/FederalTournamentHolder.swift
  3. 227
      PadelClub/Data/TeamRegistration.swift
  4. 69
      PadelClub/Data/Tournament.swift
  5. 5
      PadelClub/HTML Templates/bracket-template.html
  6. 1
      PadelClub/HTML Templates/groupstage-template.html
  7. 1
      PadelClub/HTML Templates/player-template.html
  8. 7
      PadelClub/HTML Templates/tournament-template.html
  9. 70
      PadelClub/PadelClubApp.swift
  10. 7
      PadelClub/Utils/HtmlGenerator.swift
  11. 62
      PadelClub/Utils/HtmlService.swift
  12. 16
      PadelClub/Utils/PadelRule.swift
  13. 5
      PadelClub/Utils/URLs.swift
  14. 33
      PadelClub/Utils/VersionComparator.swift
  15. 86
      PadelClub/ViewModel/SearchViewModel.swift
  16. 2
      PadelClub/Views/Calling/CallSettingsView.swift
  17. 17
      PadelClub/Views/Calling/GroupStageCallingView.swift
  18. 18
      PadelClub/Views/Calling/SeedsCallingView.swift
  19. 7
      PadelClub/Views/Calling/SendToAllView.swift
  20. 18
      PadelClub/Views/Calling/TeamsCallingView.swift
  21. 99
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  22. 2
      PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift
  23. 72
      PadelClub/Views/Planning/MatchFormatGuideView.swift
  24. 23
      PadelClub/Views/Planning/PlanningSettingsView.swift
  25. 2
      PadelClub/Views/Round/RoundView.swift
  26. 48
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  27. 7
      PadelClub/Views/Team/Components/TeamWeightView.swift
  28. 3
      PadelClub/Views/Team/EditingTeamView.swift
  29. 3
      PadelClub/Views/Team/TeamRowView.swift
  30. 2
      PadelClub/Views/Tournament/FileImportView.swift
  31. 3
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  32. 3
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  33. 2
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  34. 99
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  35. 58
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  36. 17
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  37. 2
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  38. 2
      PadelClub/Views/Tournament/TournamentBuildView.swift
  39. 4
      PadelClub/Views/Tournament/TournamentInscriptionView.swift
  40. 2
      PadelClub/Views/Tournament/TournamentView.swift

@ -130,6 +130,9 @@
C49C73142D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; }; C49C73142D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C73152D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; }; C49C73152D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C73162D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; }; C49C73162D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; };
C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; };
C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; };
C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; }; C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; };
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; }; C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; };
C49EF0262BD80AE80077B5AA /* SubscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */; }; C49EF0262BD80AE80077B5AA /* SubscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */; };
@ -906,6 +909,9 @@
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; };
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; };
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; };
FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; };
@ -1089,6 +1095,7 @@
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusView.swift; sourceTree = "<group>"; }; C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusView.swift; sourceTree = "<group>"; };
C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = "<group>"; }; C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = "<group>"; };
C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPaymentType.swift; sourceTree = "<group>"; }; C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPaymentType.swift; sourceTree = "<group>"; };
C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionComparator.swift; sourceTree = "<group>"; };
C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; }; C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; };
C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; }; C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = "<group>"; }; C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = "<group>"; };
@ -1346,6 +1353,7 @@
FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; }; FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; };
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; };
FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.swift; sourceTree = "<group>"; };
FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = "<group>"; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = "<group>"; };
FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = "<group>"; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = "<group>"; };
FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; }; FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; };
@ -2225,6 +2233,7 @@
FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */,
FF1DC5582BAB767000FD8220 /* Tips.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */,
C49EF01A2BD6A1E80077B5AA /* URLs.swift */, C49EF01A2BD6A1E80077B5AA /* URLs.swift */,
C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */,
FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */, FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */,
); );
path = Utils; path = Utils;
@ -2263,6 +2272,7 @@
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */, FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */,
FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */,
FF1162882BD0523B000C4809 /* Components */, FF1162882BD0523B000C4809 /* Components */,
); );
path = Planning; path = Planning;
@ -2651,6 +2661,7 @@
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */, C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */,
FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */, FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */,
C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */,
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */,
@ -2748,6 +2759,7 @@
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */, FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */,
@ -2962,6 +2974,7 @@
FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */, FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */,
FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */, FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */,
FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */, FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */,
C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */,
FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */, FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */,
FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */, FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */,
FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */, FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */,
@ -3061,6 +3074,7 @@
FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */, FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */,
@ -3252,6 +3266,7 @@
FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */, FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */,
FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */, FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */,
FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */, FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */,
C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */,
FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */, FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */,
FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */, FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */,
FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */, FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */,
@ -3351,6 +3366,7 @@
FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */, FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */,
@ -3613,7 +3629,6 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3635,14 +3650,13 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.8; MARKETING_VERSION = 1.1.13;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -3660,7 +3674,6 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3682,14 +3695,13 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.8; MARKETING_VERSION = 1.1.13;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };

@ -22,7 +22,6 @@ protocol FederalTournamentHolder {
} }
extension FederalTournamentHolder { extension FederalTournamentHolder {
func durationLabel() -> String { func durationLabel() -> String {
switch dayDuration { switch dayDuration {
case 1: case 1:

@ -12,42 +12,49 @@ import SwiftUI
@Observable @Observable
final class TeamRegistration: BaseTeamRegistration, SideStorable { final class TeamRegistration: BaseTeamRegistration, SideStorable {
// static func resourceName() -> String { "team-registrations" } // static func resourceName() -> String { "team-registrations" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } // static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true } // static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = [] // static var relationshipNames: [String] = []
// //
// var id: String = Store.randomId() // var id: String = Store.randomId()
// var lastUpdate: Date // var lastUpdate: Date
// var tournament: String // var tournament: String
// var groupStage: String? // var groupStage: String?
// var registrationDate: Date? // var registrationDate: Date?
// var callDate: Date? // var callDate: Date?
// var bracketPosition: Int? // var bracketPosition: Int?
// var groupStagePosition: Int? // var groupStagePosition: Int?
// var comment: String? // var comment: String?
// var source: String? // var source: String?
// var sourceValue: String? // var sourceValue: String?
// var logo: String? // var logo: String?
// var name: String? // var name: String?
// //
// var walkOut: Bool = false // var walkOut: Bool = false
// var wildCardBracket: Bool = false // var wildCardBracket: Bool = false
// var wildCardGroupStage: Bool = false // var wildCardGroupStage: Bool = false
// var weight: Int = 0 // var weight: Int = 0
// var lockedWeight: Int? // var lockedWeight: Int?
// var confirmationDate: Date? // var confirmationDate: Date?
// var qualified: Bool = false // var qualified: Bool = false
// var finalRanking: Int? // var finalRanking: Int?
// var pointsEarned: Int? // var pointsEarned: Int?
// //
// var storeId: String? = nil // var storeId: String? = nil
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, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) { 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, walkOut: Bool = false,
wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0,
lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false
) {
super.init() super.init()
// self.storeId = tournament // self.storeId = tournament
self.tournament = tournament self.tournament = tournament
self.groupStage = groupStage self.groupStage = groupStage
self.registrationDate = registrationDate ?? Date() self.registrationDate = registrationDate ?? Date()
@ -76,7 +83,6 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
players().anySatisfy({ $0.source == nil }) players().anySatisfy({ $0.source == nil })
} }
func isOutOfTournament() -> Bool { func isOutOfTournament() -> Bool {
walkOut walkOut
} }
@ -93,7 +99,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func unsortedPlayers() -> [PlayerRegistration] { func unsortedPlayers() -> [PlayerRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false } return tournamentStore.playerRegistrations.filter {
$0.teamRegistration == self.id && $0.coach == false
}
} }
// MARK: - // MARK: -
@ -130,20 +138,21 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return unsortedPlayers.allSatisfy({ $0.hasArrived }) return unsortedPlayers.allSatisfy({ $0.hasArrived })
} }
func isSeedable() -> Bool { func isSeedable() -> Bool {
bracketPosition == nil && groupStage == nil bracketPosition == nil && groupStage == nil
} }
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
var teamPosition : TeamPosition { var teamPosition: TeamPosition {
if let slot { if let slot {
return slot return slot
} else { } else {
let matchIndex = match.index let matchIndex = match.index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) let isUpper =
RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
< (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two) var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding { if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one) teamPosition = slot ?? (isUpper ? .two : .one)
@ -160,7 +169,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
if let tournament = tournamentObject() { if let tournament = tournamentObject() {
if let index = index(in: tournament.selectedSortedTeams()) { if let index = index(in: tournament.selectedSortedTeams()) {
let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition, drawType: .seed) let drawLog = DrawLog(
tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index,
drawTeamPosition: teamPosition, drawType: .seed)
do { do {
try tournamentStore?.drawLogs.addOrUpdate(instance: drawLog) try tournamentStore?.drawLogs.addOrUpdate(instance: drawLog)
} catch { } catch {
@ -252,7 +263,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool { func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
guard let codeClubOrClubName else { return true } guard let codeClubOrClubName else { return true }
return unsortedPlayers().anySatisfy({ return unsortedPlayers().anySatisfy({
$0.clubName?.contains(codeClubOrClubName) == true || $0.clubName?.contains(codeClubOrClubName) == true $0.clubName?.contains(codeClubOrClubName) == true
|| $0.clubName?.contains(codeClubOrClubName) == true
}) })
} }
@ -260,13 +272,19 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory) self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory)
} }
func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&") -> String { func teamLabel(
_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&"
) -> String {
if let name { return name } if let name { return name }
return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " \(separator) ") return players().map { $0.playerLabel(displayStyle) }.joined(
separator: twoLines ? "\n" : " \(separator) ")
} }
func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String { func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String {
[displayTeamName ? name : nil, displayRank ? seedIndex() : nil, displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel()].compactMap({ $0 }).joined(separator: " ") [
displayTeamName ? name : nil, displayRank ? seedIndex() : nil,
displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel(),
].compactMap({ $0 }).joined(separator: " ")
} }
func seedIndex() -> String? { func seedIndex() -> String? {
@ -279,8 +297,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return teams.firstIndex(where: { $0.id == id }) return teams.firstIndex(where: { $0.id == id })
} }
func formattedSeed(in teams: [TeamRegistration]) -> String { func formattedSeed(in teams: [TeamRegistration]? = nil) -> String {
if let index = index(in: teams) { let selectedSortedTeams = teams ?? tournamentObject()?.selectedSortedTeams() ?? []
if let index = index(in: selectedSortedTeams) {
return "#\(index + 1)" return "#\(index + 1)"
} else { } else {
return "###" return "###"
@ -288,13 +307,17 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
func contains(_ searchField: String) -> Bool { func contains(_ searchField: String) -> Bool {
return unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true return unsortedPlayers().anySatisfy({ $0.contains(searchField) })
|| self.name?.localizedCaseInsensitiveContains(searchField) == true
} }
func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool { func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool {
let arrayOfIds : [String] = unsortedPlayers().compactMap({ $0.licenceId?.strippedLicense?.canonicalVersion }) let arrayOfIds: [String] = unsortedPlayers().compactMap({
let ids : Set<String> = Set<String>(arrayOfIds.sorted()) $0.licenceId?.strippedLicense?.canonicalVersion
let searchedIds = Set<String>(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted()) })
let ids: Set<String> = Set<String>(arrayOfIds.sorted())
let searchedIds = Set<String>(
playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted())
if ids.isEmpty || searchedIds.isEmpty { return false } if ids.isEmpty || searchedIds.isEmpty { return false }
return ids.hashValue == searchedIds.hashValue return ids.hashValue == searchedIds.hashValue
} }
@ -319,7 +342,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
let unsortedPlayers = unsortedPlayers() let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false } if unsortedPlayers.isEmpty { return false }
return matches().isEmpty == false || unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived }) return matches().isEmpty == false
|| unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived })
} }
func availableForSeedPick() -> Bool { func availableForSeedPick() -> Bool {
@ -346,7 +370,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func initialRoundColor() -> Color? { func initialRoundColor() -> Color? {
if walkOut { return Color.logoRed } if walkOut { return Color.logoRed }
if groupStagePosition != nil { return Color.blue } if groupStagePosition != nil { return Color.blue }
if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] { if let initialRound = initialRound(),
let colorHex = RoundRule.colors[safe: initialRound.index]
{
return Color(uiColor: .init(fromHex: colorHex)) return Color(uiColor: .init(fromHex: colorHex))
} else { } else {
return nil return nil
@ -356,8 +382,12 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func resetGroupeStagePosition() { func resetGroupeStagePosition() {
guard let tournamentStore = self.tournamentStore else { return } guard let tournamentStore = self.tournamentStore else { return }
if let groupStage { if let groupStage {
let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map { $0.id } let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map {
let teamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) }) $0.id
}
let teamScores = tournamentStore.teamScores.filter({
$0.teamRegistration == id && matches.contains($0.match)
})
tournamentStore.teamScores.delete(contentOfs: teamScores) tournamentStore.teamScores.delete(contentOfs: teamScores)
} }
//groupStageObject()?._matches().forEach({ $0.updateTeamScores() }) //groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
@ -368,7 +398,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func resetBracketPosition() { func resetBracketPosition() {
guard let tournamentStore = self.tournamentStore else { return } guard let tournamentStore = self.tournamentStore else { return }
let matches = tournamentStore.matches.filter({ $0.groupStage == nil }).map { $0.id } let matches = tournamentStore.matches.filter({ $0.groupStage == nil }).map { $0.id }
let teamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) }) let teamScores = tournamentStore.teamScores.filter({
$0.teamRegistration == id && matches.contains($0.match)
})
tournamentStore.teamScores.delete(contentOfs: teamScores) tournamentStore.teamScores.delete(contentOfs: teamScores)
self.bracketPosition = nil self.bracketPosition = nil
@ -382,9 +414,13 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String { func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String {
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name].compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator()) return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .csv: case .csv:
return [index.formatted(), playersPasteData(exportFormat), isWildCard() ? "WC" : weight.formatted()].joined(separator: exportFormat.separator()) return [
index.formatted(), playersPasteData(exportFormat),
isWildCard() ? "WC" : weight.formatted(),
].joined(separator: exportFormat.separator())
} }
} }
@ -395,7 +431,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? { func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? {
guard let registrationDate else { return nil } guard let registrationDate else { return nil }
let formattedDate = registrationDate.formatted(.dateTime.weekday().day().month().hour().minute()) let formattedDate = registrationDate.formatted(
.dateTime.weekday().day().month().hour().minute())
let onlineSuffix = hasRegisteredOnline() ? " en ligne" : "" let onlineSuffix = hasRegisteredOnline() ? " en ligne" : ""
switch exportFormat { switch exportFormat {
@ -411,7 +448,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
if let callDate { if let callDate {
return "Convoqué le " + callDate.formatted(.dateTime.weekday().day().month().hour().minute()) return "Convoqué le "
+ callDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else { } else {
return nil return nil
} }
@ -427,18 +465,27 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String { func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.newLineSeparator()) return players().map { $0.pasteData(exportFormat) }.joined(
separator: exportFormat.newLineSeparator())
case .csv: case .csv:
return players().map { [$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted() ].joined(separator: exportFormat.separator()) }.joined(separator: exportFormat.separator()) return players().map {
[$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()]
.joined(separator: exportFormat.separator())
}.joined(separator: exportFormat.separator())
} }
} }
func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) { func updatePlayers(
_ players: Set<PlayerRegistration>,
inTournamentCategory tournamentCategory: TournamentCategory
) {
let previousPlayers = Set(unsortedPlayers()) let previousPlayers = Set(unsortedPlayers())
players.forEach { player in players.forEach { player in
previousPlayers.forEach { oldPlayer in previousPlayers.forEach { oldPlayer in
if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense, player.licenceId?.strippedLicense != nil { if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense,
player.licenceId?.strippedLicense != nil
{
player.registeredOnline = oldPlayer.registeredOnline player.registeredOnline = oldPlayer.registeredOnline
player.coach = oldPlayer.coach player.coach = oldPlayer.coach
player.tournamentPlayed = oldPlayer.tournamentPlayed player.tournamentPlayed = oldPlayer.tournamentPlayed
@ -450,7 +497,6 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
} }
let playersToRemove = previousPlayers.subtracting(players) let playersToRemove = previousPlayers.subtracting(players)
self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove) self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove)
setWeight(from: Array(players), inTournamentCategory: tournamentCategory) setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
@ -459,11 +505,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
player.teamRegistration = id player.teamRegistration = id
} }
// do { // do {
// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) // try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// } catch { // } catch {
// Logger.error(error) // Logger.error(error)
// } // }
} }
typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?) typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)
@ -485,14 +531,18 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
if groupStagePosition == 0 { if groupStagePosition == 0 {
left = tournamentObject.seeds().last left = tournamentObject.seeds().last
} else { } else {
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight) let previousHat = selectedSortedTeams.filter({
$0.groupStagePosition == groupStagePosition - 1
}).sorted(by: \.weight)
left = previousHat.last left = previousHat.last
} }
var right: TeamRegistration? = nil var right: TeamRegistration? = nil
if groupStagePosition == tournamentObject.teamsPerGroupStage - 1 { if groupStagePosition == tournamentObject.teamsPerGroupStage - 1 {
right = nil right = nil
} else { } else {
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition + 1 }).sorted(by: \.weight) let previousHat = selectedSortedTeams.filter({
$0.groupStagePosition == groupStagePosition + 1
}).sorted(by: \.weight)
right = previousHat.first right = previousHat.first
} }
return (left: left, right: right) return (left: left, right: right)
@ -506,8 +556,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
let predicates: [AreInIncreasingOrder] = [ let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $0.rank ?? Int.max < $1.rank ?? Int.max }, { $0.rank ?? Int.max < $1.rank ?? Int.max },
{ $0.lastName < $1.lastName}, { $0.lastName < $1.lastName },
{ $0.firstName < $1.firstName } { $0.firstName < $1.firstName },
] ]
for predicate in predicates { for predicate in predicates {
@ -527,9 +577,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return store.playerRegistrations.filter { $0.coach } return store.playerRegistrations.filter { $0.coach }
} }
func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) { func setWeight(
from players: [PlayerRegistration],
inTournamentCategory tournamentCategory: TournamentCategory
) {
let significantPlayerCount = significantPlayerCount() let significantPlayerCount = significantPlayerCount()
weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+) weight =
(players.prefix(significantPlayerCount).map { $0.computedRank }
+ missingPlayerType(inTournamentCategory: tournamentCategory).map {
unrankValue(for: $0 == 1 ? true : false)
}).prefix(significantPlayerCount).reduce(0, +)
} }
func significantPlayerCount() -> Int { func significantPlayerCount() -> Int {
@ -567,12 +624,13 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func initialMatch() -> Match? { func initialMatch() -> Match? {
guard let bracketPosition else { return nil } guard let bracketPosition else { return nil }
guard let initialRoundObject = initialRound() else { return nil } guard let initialRoundObject = initialRound() else { return nil }
return self.tournamentStore?.matches.first(where: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }) return self.tournamentStore?.matches.first(where: {
$0.round == initialRoundObject.id && $0.index == bracketPosition / 2
})
} }
func toggleSummonConfirmation() { func toggleSummonConfirmation() {
if confirmationDate == nil { confirmationDate = Date() } if confirmationDate == nil { confirmationDate = Date() } else { confirmationDate = nil }
else { confirmationDate = nil }
} }
func didConfirmSummon() -> Bool { func didConfirmSummon() -> Bool {
@ -595,7 +653,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func wildcardLabel() -> String? { func wildcardLabel() -> String? {
if isWildCard() { if isWildCard() {
let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")].joined(separator: " ") let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")]
.joined(separator: " ")
return wildcardLabel return wildcardLabel
} else { } else {
return nil return nil
@ -606,7 +665,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func restingTime() -> Date? { func restingTime() -> Date? {
if let _cachedRestingTime { return _cachedRestingTime.1 } if let _cachedRestingTime { return _cachedRestingTime.1 }
let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate let restingTime = matches().filter({ $0.hasEnded() }).sorted(
by: \.computedEndDateForSorting
).last?.endDate
_cachedRestingTime = (true, restingTime) _cachedRestingTime = (true, restingTime)
return restingTime return restingTime
} }
@ -630,14 +691,18 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex { if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition return drawMatchIndex != bracketPosition
} else if let bracketPosition { } else if bracketPosition != nil {
return true return true
} else if let drawMatchIndex { } else if drawMatchIndex != nil {
return true return true
} }
return false return false
} }
func shouldDisplayRankAndWeight() -> Bool {
unsortedPlayers().count > 0
}
func insertOnServer() { func insertOnServer() {
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self) self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
for playerRegistration in self.unsortedPlayers() { for playerRegistration in self.unsortedPlayers() {

@ -19,7 +19,8 @@ final class Tournament: BaseTournament {
// super.init() // super.init()
// } // }
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) {
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) {
super.init() super.init()
self.event = event self.event = event
self.name = name self.name = name
@ -29,11 +30,7 @@ final class Tournament: BaseTournament {
#if DEBUG #if DEBUG
self.isPrivate = false self.isPrivate = false
#else #else
if Guard.main.currentPlan == .monthlyUnlimited { self.isPrivate = isPrivate
self.isPrivate = true
} else {
self.isPrivate = Guard.main.purchasedTransactions.isEmpty
}
#endif #endif
self.groupStageFormat = groupStageFormat self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat self.roundFormat = roundFormat
@ -136,6 +133,10 @@ final class Tournament: BaseTournament {
return Array(tournamentStore.teamRegistrations) return Array(tournamentStore.teamRegistrations)
} }
func unsortedTeamsCount() -> Int {
return self.tournamentStore?.teamRegistrations.count ?? 0
}
func groupStages(atStep step: Int = 0) -> [GroupStage] { func groupStages(atStep step: Int = 0) -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
@ -277,7 +278,8 @@ defer {
} }
func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String {
let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams() let _selectedSortedTeams = selectedSortedTeams()
let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams)
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2)) return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2))
@ -577,13 +579,13 @@ defer {
return rounds.sorted(by: \.index).reversed() return rounds.sorted(by: \.index).reversed()
} }
func sortedTeams() -> [TeamRegistration] { func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams() let teams = selectedSortedTeams
return teams + waitingListTeams(in: teams, includingWalkOuts: true) return teams + waitingListTeams(in: teams, includingWalkOuts: true)
} }
func waitingListSortedTeams() -> [TeamRegistration] { func waitingListSortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams() let teams = selectedSortedTeams
return waitingListTeams(in: teams, includingWalkOuts: false) return waitingListTeams(in: teams, includingWalkOuts: false)
} }
@ -880,9 +882,8 @@ defer {
} }
} }
func registrationIssues() -> Int { func registrationIssues(selectedTeams: [TeamRegistration]) -> Int {
let players : [PlayerRegistration] = unsortedPlayers() let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players) let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
@ -911,7 +912,7 @@ defer {
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
} }
static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)] static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.computedOrder)]
static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
@ -1535,6 +1536,7 @@ defer {
team.wildCardGroupStage = true team.wildCardGroupStage = true
} }
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team return team
} }
@ -1551,6 +1553,7 @@ defer {
let teams = (0..<count).map { _ in let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id) let team = TeamRegistration(tournament: id)
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team return team
} }
@ -1836,6 +1839,25 @@ defer {
//enableOnlineRegistration = true //enableOnlineRegistration = true
registrationDateLimit = deadline(for: .inscription) registrationDateLimit = deadline(for: .inscription)
} }
//self.customizeUsingPreferences()
}
func customizeUsingPreferences() {
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
tournament.tournamentLevel == self.tournamentLevel
&& tournament.tournamentCategory == self.tournamentCategory
&& tournament.federalTournamentAge == self.federalTournamentAge
&& tournament.hasEnded() == true
&& tournament.isCanceled == false
&& tournament.isDeleted == false
}).sorted(by: \.endDate!, order: .descending).first else {
return
}
self.dayDuration = lastTournamentWithSameBuild.dayDuration
self.teamCount = (lastTournamentWithSameBuild.teamCount / 2) * 2
self.enableOnlineRegistration = lastTournamentWithSameBuild.enableOnlineRegistration
} }
func onlineRegistrationCanBeEnabled() -> Bool { func onlineRegistrationCanBeEnabled() -> Bool {
@ -2333,7 +2355,6 @@ extension Bool {
//} //}
extension Tournament: FederalTournamentHolder { extension Tournament: FederalTournamentHolder {
func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() { if isAnimation() {
if let name { if let name {
@ -2428,12 +2449,21 @@ extension Tournament {
} }
let rankSourceDate = _mostRecentDateAvailable let rankSourceDate = _mostRecentDateAvailable
let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false } let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false }.sorted(by: \.endDate!, order: .descending)
var shouldBePrivate = tournaments.first?.isPrivate ?? true
if Guard.main.currentPlan == .monthlyUnlimited {
shouldBePrivate = false
} else if Guard.main.purchasedTransactions.isEmpty == false {
shouldBePrivate = false
}
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
//creator: DataStore.shared.user?.id //creator: DataStore.shared.user?.id
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode) return Tournament(isPrivate: shouldBePrivate, groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode)
} }
static func fake() -> Tournament { static func fake() -> Tournament {
@ -2446,10 +2476,7 @@ extension Tournament {
func deadline(for type: TournamentDeadlineType) -> Date? { func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
var daysOffset = type.daysOffset var daysOffset = type.daysOffset(level: tournamentLevel)
if tournamentLevel == .p500 {
daysOffset += 7
}
if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) { if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date) let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)

@ -1,4 +1,7 @@
<ul class="round"> <ul class="round">
<li class="spacer">&nbsp;{{roundLabel}}</li> <li class="spacer">
&nbsp;{{roundLabel}}
<div>{{formatLabel}}</div>
</li>
{{match-template}} {{match-template}}
</ul> </ul>

@ -82,6 +82,7 @@ body{
<caption> <caption>
<h2>{{bracketTitle}}</h2> <h2>{{bracketTitle}}</h2>
<h3>{{bracketStartDate}}</h3> <h3>{{bracketStartDate}}</h3>
<h3>{{formatLabel}}</h3>
</caption> </caption>
<tr> <tr>
<th scope="col" style="visibility:hidden"></th> <th scope="col" style="visibility:hidden"></th>

@ -1,3 +1,4 @@
<div class="player">{{teamIndex}}</div>
<div class="player">{{playerOne}}<span>{{weightOne}}</span></div> <div class="player">{{playerOne}}<span>{{weightOne}}</span></div>
<div class="player">{{playerTwo}}<span>{{weightTwo}}</span></div> <div class="player">{{playerTwo}}<span>{{weightTwo}}</span></div>

@ -9,6 +9,7 @@
flex-direction:row; flex-direction:row;
padding: 1%; padding: 1%;
} }
.round{ .round{
display:flex; display:flex;
flex-direction:column; flex-direction:column;
@ -27,7 +28,7 @@
.round .spacer{ flex-grow:1; .round .spacer{ flex-grow:1;
font-size:24px; font-size:24px;
text-align: center; text-align: center;
color: #bbb; color: #000000;
font-style:italic; font-style:italic;
} }
.round .spacer:first-child, .round .spacer:first-child,
@ -65,7 +66,7 @@
li.game-spacer{ li.game-spacer{
border-right:2px solid #4f7a38; border-right:2px solid #4f7a38;
min-height:156px; min-height:{{minHeight}}px;
text-align: right; text-align: right;
display : flex; display : flex;
justify-content: center; justify-content: center;
@ -95,7 +96,7 @@
</head> </head>
<body> <body>
<h1>{{tournamentTitle}}</h1> <h3>{{tournamentTitle}} - {{tournamentStartDate}}</h3>
<main id="tournament"> <main id="tournament">
{{brackets}} {{brackets}}
</main> </main>

@ -19,6 +19,8 @@ struct PadelClubApp: App {
@State private var importObserverViewModel = ImportObserver() @State private var importObserverViewModel = ImportObserver()
@Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var blockApp = false
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var presentError: Binding<Bool> { var presentError: Binding<Bool> {
@ -62,6 +64,10 @@ struct PadelClubApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
if self.blockApp {
DownloadNewVersionView()
} else {
MainView() MainView()
.environment(\.horizontalSizeClass, .compact) .environment(\.horizontalSizeClass, .compact)
.alert(isPresented: presentError, error: registrationError) { .alert(isPresented: presentError, error: registrationError) {
@ -84,14 +90,15 @@ struct PadelClubApp: App {
.environment(navigationViewModel) .environment(navigationViewModel)
.accentColor(.master) .accentColor(.master)
.onAppear { .onAppear {
self._checkVersion()
#if DEBUG #if DEBUG
print("Running in Debug mode") print("Running in Debug mode")
#elseif TESTFLIGHT #elseif TESTFLIGHT
print("Running in TestFlight mode") print("Running in TestFlight mode")
#elseif PRODTEST #elseif PRODTEST
print("Running in ProdTest mode") print("Running in ProdTest mode")
#else #else
print("Running in Release mode") print("Running in Release mode")
#endif #endif
networkMonitor.checkConnection() networkMonitor.checkConnection()
self._onAppear() self._onAppear()
@ -109,6 +116,33 @@ print("Running in Release mode")
.environment(\.managedObjectContext, persistenceController.localContainer.viewContext) .environment(\.managedObjectContext, persistenceController.localContainer.viewContext)
} }
} }
}
fileprivate func _checkVersion() {
Task.detached(priority: .high) {
if let requiredVersion = await self._retrieveRequiredVersion() {
let cleanedRequired = requiredVersion.replacingOccurrences(of: "\n", with: "")
Logger.log(">>> REQUIRED VERSION = \(requiredVersion)")
if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
await MainActor.run {
self.blockApp = VersionComparator.compare(cleanedRequired, currentVersion) == 1
}
}
}
}
}
fileprivate func _retrieveRequiredVersion() async -> String? {
let requiredVersionURL = URLs.main.extend(path: "static/misc/required-version.txt")
do {
let (data, _) = try await URLSession.shared.data(from: requiredVersionURL)
return String(data: data, encoding: .utf8)
} catch {
Logger.log("Error fetching required version: \(error)")
return nil
}
}
private func _handleIncomingURL(_ url: URL) { private func _handleIncomingURL(_ url: URL) {
// Parse the URL // Parse the URL
@ -173,3 +207,31 @@ print("Running in Release mode")
} }
} }
} }
struct DownloadNewVersionView: View {
var body: some View {
VStack {
// AngledStripesBackground()
Spacer()
Text("Veuillez télécharger la nouvelle version de Padel Club pour continuer à vous servir de l'app !")
.padding(32.0)
.background(.logoYellow)
.clipShape(.buttonBorder)
.foregroundStyle(.logoBackground)
.fontWeight(.medium)
.multilineTextAlignment(.center)
.padding(.horizontal, 64.0)
Image("logo").padding(.vertical, 50.0)
Spacer()
}.background(.logoBackground)
.onTapGesture {
UIApplication.shared.open(URLs.appStore.url)
}
}
}

@ -24,6 +24,9 @@ class HtmlGenerator: ObservableObject {
@Published var displayHeads: Bool = false @Published var displayHeads: Bool = false
@Published var groupStageIsReady: Bool = false @Published var groupStageIsReady: Bool = false
@Published var displayRank: Bool = false @Published var displayRank: Bool = false
@Published var displayTeamIndex: Bool = false
@Published var displayScore: Bool = false
private var pdfDocument: PDFDocument = PDFDocument() private var pdfDocument: PDFDocument = PDFDocument()
private var rects: [CGRect] = [] private var rects: [CGRect] = []
private var completionHandler: ((Result<Bool, Error>) -> ())? private var completionHandler: ((Result<Bool, Error>) -> ())?
@ -167,12 +170,12 @@ class HtmlGenerator: ObservableObject {
func generateHtml() -> String { func generateHtml() -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false) HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore)
} }
func generateLoserBracketHtml(upperRound: Round) -> String { func generateLoserBracketHtml(upperRound: Round) -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withScore: false) HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore)
} }
var pdfURL: URL? { var pdfURL: URL? {

@ -50,7 +50,7 @@ enum HtmlService {
} }
} }
func html(headName: Bool, withRank: Bool, withScore: Bool) -> String { func html(headName: Bool, withRank: Bool, withTeamIndex: Bool, withScore: Bool) -> String {
guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else { guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else {
fatalError() fatalError()
} }
@ -69,12 +69,12 @@ enum HtmlService {
} }
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle(.short)) template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle(.short))
template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle()) template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle())
template = template.replacingOccurrences(of: "{{formatLabel}}", with: bracket.matchFormat.formatTitle())
var col = "" var col = ""
var row = "" var row = ""
bracket.teams().forEach { entrant in bracket.teams().forEach { entrant in
col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withScore: withScore)) col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withScore: withScore)) row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
template = template.replacingOccurrences(of: "{{teamsCol}}", with: col) template = template.replacingOccurrences(of: "{{teamsCol}}", with: col)
template = template.replacingOccurrences(of: "{{teamsRow}}", with: row) template = template.replacingOccurrences(of: "{{teamsRow}}", with: row)
@ -82,6 +82,12 @@ enum HtmlService {
return template return template
case .groupstageEntrant(let entrant): case .groupstageEntrant(let entrant):
var template = html var template = html
if withTeamIndex == false {
template = template.replacingOccurrences(of: #"<div class="player">{{teamIndex}}</div>"#, with: "")
} else {
template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.seedIndex() ?? "")
}
if let playerOne = entrant.players()[safe: 0] { if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel())
if withRank { if withRank {
@ -108,7 +114,7 @@ enum HtmlService {
return template return template
case .groupstageRow(let entrant, let teamsPerBracket): case .groupstageRow(let entrant, let teamsPerBracket):
var template = html var template = html
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withScore: withScore)) template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
var scores = "" var scores = ""
(0..<teamsPerBracket).forEach { index in (0..<teamsPerBracket).forEach { index in
@ -117,28 +123,35 @@ enum HtmlService {
if shouldHide == false { if shouldHide == false {
match = entrant.groupStageObject()?.matchPlayed(by: entrant.groupStagePosition!, againstPosition: index) match = entrant.groupStageObject()?.matchPlayed(by: entrant.groupStagePosition!, againstPosition: index)
} }
scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(headName: headName, withRank: withRank, withScore: withScore)) scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
template = template.replacingOccurrences(of: "{{scores}}", with: scores) template = template.replacingOccurrences(of: "{{scores}}", with: scores)
return template return template
case .groupstageColumn(let entrant, let position): case .groupstageColumn(let entrant, let position):
var template = html var template = html
template = template.replacingOccurrences(of: "{{tablePosition}}", with: position) template = template.replacingOccurrences(of: "{{tablePosition}}", with: position)
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(headName: headName, withRank: withRank, withScore: withScore)) template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
return template return template
case .groupstageScore(let match, let shouldHide): case .groupstageScore(let match, let shouldHide):
var template = html var template = html
if match == nil || withScore == false { if match == nil || withScore == false {
template = template.replacingOccurrences(of: "{{winner}}", with: "") template = template.replacingOccurrences(of: "{{winner}}", with: "")
template = template.replacingOccurrences(of: "{{score}}", with: "") template = template.replacingOccurrences(of: "{{score}}", with: "")
} else { } else if let match, let winner = match.winner() {
template = template.replacingOccurrences(of: "{{winner}}", with: match!.winner()!.teamLabel()) template = template.replacingOccurrences(of: "{{winner}}", with: winner.teamLabel())
template = template.replacingOccurrences(of: "{{score}}", with: match!.scoreLabel()) template = template.replacingOccurrences(of: "{{score}}", with: match.scoreLabel())
} }
template = template.replacingOccurrences(of: "{{hide}}", with: shouldHide ? "hide" : "") template = template.replacingOccurrences(of: "{{hide}}", with: shouldHide ? "hide" : "")
return template return template
case .player(let entrant): case .player(let entrant):
var template = html var template = html
if withTeamIndex == false {
template = template.replacingOccurrences(of: #"<div class="player">{{teamIndex}}</div>"#, with: "")
} else {
template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.formattedSeed())
}
if let playerOne = entrant.players()[safe: 0] { if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel())
if withRank { if withRank {
@ -164,18 +177,22 @@ enum HtmlService {
} }
return template return template
case .hiddenPlayer: case .hiddenPlayer:
return html + html var template = html + html
if withTeamIndex {
template += html
}
return template
case .match(let match): case .match(let match):
var template = html var template = html
if let entrantOne = match.team(.one) { if let entrantOne = match.team(.one) {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} else { } else {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
if let entrantTwo = match.team(.two) { if let entrantTwo = match.team(.two) {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} else { } else {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
if match.disabled { if match.disabled {
template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden") template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden")
@ -196,19 +213,20 @@ enum HtmlService {
var template = "" var template = ""
var bracket = "" var bracket = ""
for (_, match) in round._matches().enumerated() { for (_, match) in round._matches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore)) template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle()) bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
bracket = bracket.replacingOccurrences(of: "{{formatLabel}}", with: round.matchFormat.formatTitle())
return bracket return bracket
case .loserBracket(let upperRound): case .loserBracket(let upperRound):
var template = html var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle()) template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle())
var brackets = "" var brackets = ""
for round in upperRound.loserRounds() { for round in upperRound.loserRounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore)) brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
var winnerName = "" let winnerName = ""
let winner = """ let winner = """
<ul class="round" scope="last"> <ul class="round" scope="last">
<li class="spacer">&nbsp;</li> <li class="spacer">&nbsp;</li>
@ -224,15 +242,17 @@ enum HtmlService {
return template return template
case .template(let tournament): case .template(let tournament):
var template = html var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short)) template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156")
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.title))
template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: tournament.formattedDate())
var brackets = "" var brackets = ""
for round in tournament.rounds() { for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore)) brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
var winnerName = "" var winnerName = ""
if let tournamentWinner = tournament.tournamentWinner() { if let tournamentWinner = tournament.tournamentWinner() {
winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withScore: withScore) winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)
} }
let winner = """ let winner = """
<ul class="round" scope="last"> <ul class="round" scope="last">

@ -1996,7 +1996,19 @@ enum TournamentDeadlineType: String, CaseIterable {
case wildcardLicensePurchase = "Prise de licence des WC" case wildcardLicensePurchase = "Prise de licence des WC"
case definitiveBroadcastList = "Publication définitive" case definitiveBroadcastList = "Publication définitive"
var daysOffset: Int { func daysOffset(level: TournamentLevel) -> Int {
if level == .p500 {
switch self {
case .inscription:
return -6
case .broadcastList:
return -6
case .wildcardRequest:
return -4
case .wildcardLicensePurchase, .definitiveBroadcastList:
return -4
}
} else {
switch self { switch self {
case .inscription: case .inscription:
return -13 return -13
@ -2007,6 +2019,8 @@ enum TournamentDeadlineType: String, CaseIterable {
case .wildcardLicensePurchase, .definitiveBroadcastList: case .wildcardLicensePurchase, .definitiveBroadcastList:
return -8 return -8
} }
}
} }
var timeOffset: DateComponents { var timeOffset: DateComponents {

@ -47,6 +47,11 @@ enum URLs: String, Identifiable {
var url: URL { var url: URL {
return URL(string: self.rawValue)! return URL(string: self.rawValue)!
} }
func extend(path: String) -> URL {
return URL(string: self.rawValue + path)!
}
} }
enum PageLink: String, Identifiable, CaseIterable { enum PageLink: String, Identifiable, CaseIterable {

@ -0,0 +1,33 @@
//
// VersionComparator.swift
// PadelClub
//
// Created by Laurent Morvillier on 13/02/2025.
//
class VersionComparator {
static func compare(_ version1: String, _ version2: String) -> Int {
// Split versions into components
let v1Components = version1.split(separator: ".").map { Int($0) ?? 0 }
let v2Components = version2.split(separator: ".").map { Int($0) ?? 0 }
// Get the maximum length to compare
let maxLength = max(v1Components.count, v2Components.count)
// Compare each component
for i in 0..<maxLength {
let v1Num = i < v1Components.count ? v1Components[i] : 0
let v2Num = i < v2Components.count ? v2Components[i] : 0
if v1Num < v2Num {
return -1 // version1 is smaller
} else if v1Num > v2Num {
return 1 // version1 is larger
}
}
return 0 // versions are equal
}
}

@ -89,8 +89,31 @@ class SearchViewModel: ObservableObject, Identifiable {
return nil return nil
} }
func shouldIncludeSearchTextPredicate() -> Bool {
if allowMultipleSelection {
return true
}
if allowSingleSelection {
return true
}
if tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted {
return true
}
return dataSet == .national && searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted)
}
func showIndex() -> Bool { func showIndex() -> Bool {
if (dataSet == .national || dataSet == .ligue) { return isFiltering() } if dataSet == .national {
if searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) {
return false
} else {
return isFiltering()
}
}
if (dataSet == .ligue) { return isFiltering() }
if filterOption == .all { return isFiltering() } if filterOption == .all { return isFiltering() }
return true return true
} }
@ -149,13 +172,11 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
} }
func orPredicate() -> NSPredicate? { func searchTextPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = [] var predicates : [NSPredicate] = []
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed
let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed
switch tokens.first {
case .none:
if canonicalVersionWithoutPunctuation.isEmpty == false { if canonicalVersionWithoutPunctuation.isEmpty == false {
let wordsPredicates = wordsPredicates() let wordsPredicates = wordsPredicates()
if let wordsPredicates { if let wordsPredicates {
@ -169,6 +190,27 @@ class SearchViewModel: ObservableObject, Identifiable {
let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
predicates.append(predicate) predicates.append(predicate)
} }
if predicates.isEmpty {
return nil
}
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
}
func orPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = []
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed
let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed
if tokens.isEmpty {
if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false {
if let searchTextPredicate = searchTextPredicate() {
predicates.append(searchTextPredicate)
}
}
}
for token in tokens {
switch token {
case .ligue: case .ligue:
if canonicalVersionWithoutPunctuation.isEmpty { if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil")) predicates.append(NSPredicate(format: "ligueName == nil"))
@ -208,7 +250,7 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
} }
}
if predicates.isEmpty { if predicates.isEmpty {
return nil return nil
} }
@ -314,6 +356,17 @@ class SearchViewModel: ObservableObject, Identifiable {
static func pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { static func pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? {
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
let matches = pasteField.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = licensesPredicates
if matches.count == 2 {
return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)
}
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
// Remove all characters that are not in the allowedCharacterSet // Remove all characters that are not in the allowedCharacterSet
@ -327,14 +380,8 @@ class SearchViewModel: ObservableObject, Identifiable {
let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines) let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines)
let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 } let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 }
let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 }) let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 })
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
//self.wordsCount = nameComponents.count
if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) {
orPredicates.append(slashPredicate)
}
//self.wordsCount = nameComponents.count
if filterOption == .female { if filterOption == .female {
andPredicates.append(NSPredicate(format: "male == NO")) andPredicates.append(NSPredicate(format: "male == NO"))
} }
@ -343,12 +390,21 @@ class SearchViewModel: ObservableObject, Identifiable {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) {
orPredicates.append(slashPredicate)
}
print("nameComponents", nameComponents.count)
if nameComponents.count < 50 {
if nameComponents.count > 1 { if nameComponents.count > 1 {
orPredicates.append(contentsOf: nameComponents.pairs().map { orPredicates.append(contentsOf: nameComponents.pairs().map {
return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }) return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) })
} else { } else {
orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) })
} }
}
let components = text.split(separator: " ") let components = text.split(separator: " ")
let pattern = components.joined(separator: ".*") let pattern = components.joined(separator: ".*")
@ -356,10 +412,6 @@ class SearchViewModel: ObservableObject, Identifiable {
let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
orPredicates.append(canonicalFullNamePredicate) orPredicates.append(canonicalFullNamePredicate)
let matches = pasteField.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = orPredicates + licensesPredicates
var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
if orPredicates.isEmpty == false { if orPredicates.isEmpty == false {

@ -22,6 +22,8 @@ struct CallSettingsView: View {
var body: some View { var body: some View {
List { List {
Section { Section {
NavigationLink { NavigationLink {
CallMessageCustomizationView(tournament: tournament) CallMessageCustomizationView(tournament: tournament)

@ -6,14 +6,31 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct GroupStageCallingView: View { struct GroupStageCallingView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@State private var displayByTeam: Bool = false @State private var displayByTeam: Bool = false
var body: some View { var body: some View {
let groupStages = tournament.groupStages() let groupStages = tournament.groupStages()
List { List {
if tournament.isPrivate {
Section {
RowButtonView("Rendre visible sur Padel Club") {
tournament.isPrivate = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} footer: {
Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.")
.foregroundStyle(.logoRed)
}
}
let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }) let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil })
if uncalled.isEmpty == false { if uncalled.isEmpty == false {

@ -6,13 +6,31 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct SeedsCallingView: View { struct SeedsCallingView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var displayByMatch: Bool = true @State private var displayByMatch: Bool = true
var body: some View { var body: some View {
List { List {
if tournament.isPrivate {
Section {
RowButtonView("Rendre visible sur Padel Club") {
tournament.isPrivate = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} footer: {
Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.")
.foregroundStyle(.logoRed)
}
}
let tournamentRounds = tournament.rounds() let tournamentRounds = tournament.rounds()
let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil }) let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil })

@ -223,13 +223,14 @@ struct SendToAllView: View {
} }
func _teams() -> [TeamRegistration] { func _teams() -> [TeamRegistration] {
let selectedSortedTeams = tournament.selectedSortedTeams()
if onlyWaitingList { if onlyWaitingList {
return tournament.waitingListSortedTeams() return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams)
} }
if _roundTeams().isEmpty && _groupStagesTeams().isEmpty { if _roundTeams().isEmpty && _groupStagesTeams().isEmpty {
return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : []) return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : [])
} }
return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : []) return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : [])
} }
func _roundTeams() -> [TeamRegistration] { func _roundTeams() -> [TeamRegistration] {

@ -10,6 +10,7 @@ import LeStorage
struct TeamsCallingView: View { struct TeamsCallingView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
let teams : [TeamRegistration] let teams : [TeamRegistration]
@State private var hideConfirmed: Bool = false @State private var hideConfirmed: Bool = false
@ -31,6 +32,23 @@ struct TeamsCallingView: View {
var body: some View { var body: some View {
List { List {
if tournament.isPrivate {
Section {
RowButtonView("Rendre visible sur Padel Club") {
tournament.isPrivate = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} footer: {
Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.")
.foregroundStyle(.logoRed)
}
}
PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false } let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false }

@ -38,6 +38,10 @@ struct EventListView: View {
let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments) let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
Text("\(count.formatted()) tournoi" + count.pluralSuffix) Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} footer: {
if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] {
_menuOptions(pcTournaments)
}
} }
.headerProminence(.increased) .headerProminence(.increased)
} }
@ -56,6 +60,10 @@ struct EventListView: View {
let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments) let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
Text("\(count.formatted()) tournoi" + count.pluralSuffix) Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} footer: {
if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] {
_menuOptions(pcTournaments)
}
} }
.id(sectionIndex) .id(sectionIndex)
.headerProminence(.increased) .headerProminence(.increased)
@ -84,6 +92,88 @@ struct EventListView: View {
} }
} }
private func _menuOptions(_ pcTournaments: [Tournament]) -> some View {
Menu {
_options(pcTournaments)
} label: {
Text("Options rapides pour ce mois")
.underline()
}
}
@ViewBuilder
private func _options(_ pcTournaments: [Tournament]) -> some View {
Section {
if pcTournaments.anySatisfy({ $0.isPrivate == true }) {
Button {
pcTournaments.forEach { tournament in
tournament.isPrivate = false
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Afficher ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club")
}
}
if pcTournaments.anySatisfy({ $0.isPrivate == false }) {
Button {
pcTournaments.forEach { tournament in
tournament.isPrivate = true
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Masquer ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club")
}
}
} header: {
Text("Visibilité sur Padel Club")
}
Divider()
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) {
Section {
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) {
Button {
pcTournaments.forEach { tournament in
tournament.enableOnlineRegistration = true
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Activer l'inscription en ligne")
}
}
if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) {
Button {
pcTournaments.forEach { tournament in
tournament.enableOnlineRegistration = false
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Désactiver l'inscription en ligne")
}
}
} header: {
Text("Inscription en ligne")
}
}
}
private func _nextMonths() -> [Date] { private func _nextMonths() -> [Date] {
let currentDate = Date().startOfMonth let currentDate = Date().startOfMonth
let uniqueDates = tournaments.map { $0.startDate.startOfMonth }.uniqued().sorted() let uniqueDates = tournaments.map { $0.startDate.startOfMonth }.uniqued().sorted()
@ -124,13 +214,20 @@ struct EventListView: View {
ShareModelView(instance: tournament) ShareModelView(instance: tournament)
} }
} }
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)
.contextMenu { .contextMenu {
if tournament.hasEnded() == false { if tournament.hasEnded() == false {
Button { Button {
navigation.openTournamentInOrganizer(tournament) navigation.openTournamentInOrganizer(tournament)
} label: { } label: {
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") Label("Afficher dans le gestionnaire", systemImage: "line.diagonal.arrow")
} }
Divider()
_options([tournament])
} }
} }
#if DEBUG #if DEBUG

@ -22,7 +22,7 @@ struct MatchFormatStorageView: View {
var body: some View { var body: some View {
Section { Section {
LabeledContent { LabeledContent {
StepperView(title: "minutes", count: $estimatedDuration, step: 5) StepperView(title: "minute", count: $estimatedDuration, step: 5)
} label: { } label: {
MatchFormatRowView(matchFormat: matchFormat, hideDuration: true) MatchFormatRowView(matchFormat: matchFormat, hideDuration: true)
} }

@ -0,0 +1,72 @@
//
// MatchFormatGuideView.swift
// PadelClub
//
// Created by razmig on 20/02/2025.
//
import SwiftUI
struct MatchFormatGuideView: View {
let matchCounts = Array(2...7)
let formats: [MatchFormat] = [
.twoSets, .twoSetsDecisivePoint,
.twoSetsSuperTie, .twoSetsDecisivePointSuperTie,
.twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint,
.nineGames, .nineGamesDecisivePoint,
.superTie
]
func getFormatDescription(for matchCount: Int) -> String {
var description = ""
// Group formats by their behavior
let formatGroups = Dictionary(grouping: formats) { format in
format.maximumMatchPerDay(for: matchCount)
}
// Sort by maximum matches allowed (descending)
let sortedMaxMatches = formatGroups.keys.sorted(by: >)
for maxMatches in sortedMaxMatches {
if let formatsForMax = formatGroups[maxMatches] {
let formatStrings = formatsForMax.map { $0.format }.joined(separator: "/")
if maxMatches > 0 && maxMatches <= matchCount {
description += "Maximum \(maxMatches) matchs en format \(formatStrings)\n"
} else if maxMatches == 0 {
description += "Aucun match au format \(formatStrings)\n"
}
}
}
if matchCount >= 7 {
description += "Format \(MatchFormat.superTie.format) principalement"
}
return description.isEmpty ? "Aucun match possible" : description
}
var body: some View {
List {
Section {
ForEach(matchCounts, id: \.self) { count in
VStack {
Text("\(count) matchs par jour")
.font(.headline)
Text(getFormatDescription(for: count))
}
}
// Special case for 7+ matches
VStack {
Text("7+ matchs par jour")
.font(.headline)
Text("Tournois P 25 uniquement (soirée/demi-journée/journée)")
}
}
}
.navigationTitle("Guide des Formats de Match")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}

@ -23,6 +23,7 @@ struct PlanningSettingsView: View {
@State private var parallelType: Bool = false @State private var parallelType: Bool = false
@State private var deletingDateMatchesDone: Bool = false @State private var deletingDateMatchesDone: Bool = false
@State private var deletingDone: Bool = false @State private var deletingDone: Bool = false
@State private var presentFormatHelperView: Bool = false
var tournamentStore: TournamentStore? { var tournamentStore: TournamentStore? {
return self.tournament.tournamentStore return self.tournament.tournamentStore
@ -145,6 +146,28 @@ struct PlanningSettingsView: View {
_smartView() _smartView()
} }
.navigationTitle("Réglages")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
presentFormatHelperView = true
} label: {
Text("Aide-mémoire")
}
}
}
.sheet(isPresented: $presentFormatHelperView) {
NavigationStack {
MatchFormatGuideView()
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Retour", role: .cancel) {
presentFormatHelperView = false
}
}
}
}
}
.headerProminence(.increased) .headerProminence(.increased)
.onAppear { .onAppear {
do { do {

@ -262,7 +262,7 @@ struct RoundView: View {
.foregroundStyle(.green) .foregroundStyle(.green)
} }
} label: { } label: {
Text("Classement final des équipes") Text("Classement final")
if tournament.publishRankings == false { if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
} }

@ -338,6 +338,21 @@ struct MySearchView: View {
_players = FetchRequest<ImportedPlayer>(sortDescriptors: searchViewModel.sortDescriptors(), predicate: searchViewModel.predicate()) _players = FetchRequest<ImportedPlayer>(sortDescriptors: searchViewModel.sortDescriptors(), predicate: searchViewModel.predicate())
} }
func searchedPlayers() -> [ImportedPlayer] {
if searchViewModel.searchText.isEmpty {
return Array(players)
}
if let searchPredicate = searchViewModel.searchTextPredicate() {
let filteredPlayers = players.filter { player in
searchPredicate.evaluate(with: player)
}
return filteredPlayers
}
return Array(players)
}
var body: some View { var body: some View {
playersView playersView
.overlay { .overlay {
@ -371,8 +386,6 @@ struct MySearchView: View {
@ViewBuilder @ViewBuilder
var playersView: some View { var playersView: some View {
let showProgression = true
let showFemaleInMaleAssimilation = searchViewModel.showFemaleInMaleAssimilation
if searchViewModel.allowMultipleSelection { if searchViewModel.allowMultipleSelection {
List(selection: $searchViewModel.selectedPlayers) { List(selection: $searchViewModel.selectedPlayers) {
if searchViewModel.filterSelectionEnabled { if searchViewModel.filterSelectionEnabled {
@ -423,7 +436,7 @@ struct MySearchView: View {
} }
} }
.id(UUID()) .id(UUID())
} else { } else if searchViewModel.shouldIncludeSearchTextPredicate() {
Section { Section {
ForEach(players.indices, id: \.self) { index in ForEach(players.indices, id: \.self) { index in
let player = players[index] let player = players[index]
@ -435,26 +448,45 @@ struct MySearchView: View {
} }
} }
.id(UUID()) .id(UUID())
} else {
let filteredPlayers = searchedPlayers()
Section {
ForEach(filteredPlayers.indices, id: \.self) { index in
let player = filteredPlayers[index]
let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil
let computedIndex = realIndex != nil ? realIndex! + 1 : nil
ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
} header: {
if filteredPlayers.isEmpty == false {
headerView()
}
}
.id(UUID())
} }
} else { } else {
let filteredPlayers = searchedPlayers()
Section { Section {
ForEach(players.indices, id: \.self) { index in ForEach(filteredPlayers.indices, id: \.self) { index in
let player = players[index] let player = filteredPlayers[index]
let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil
let computedIndex = realIndex != nil ? realIndex! + 1 : nil
if searchViewModel.allowSingleSelection { if searchViewModel.allowSingleSelection {
Button { Button {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
} else { } else {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
} }
} }
} header: { } header: {
if players.isEmpty == false { if filteredPlayers.isEmpty == false {
headerView() headerView()
} }
} }

@ -12,16 +12,15 @@ struct TeamWeightView: View {
let team: TeamRegistration let team: TeamRegistration
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var teamIndex: Int? { var teamIndex: Int?
team.tournamentObject()?.indexOf(team: team)
}
var displayWeight: Bool { var displayWeight: Bool {
team.tournamentObject()?.hideWeight() == false team.shouldDisplayRankAndWeight() && team.tournamentObject()?.hideWeight() == false
} }
var body: some View { var body: some View {
VStack(alignment: .trailing, spacing: 0) { VStack(alignment: .trailing, spacing: 0) {
let displayWeight = self.displayWeight
if (teamPosition == .one || teamPosition == nil) && displayWeight { if (teamPosition == .one || teamPosition == nil) && displayWeight {
Text(team.weight.formatted()) Text(team.weight.formatted())
.monospacedDigit() .monospacedDigit()

@ -47,7 +47,8 @@ struct EditingTeamView: View {
} }
private func _resetTeam() { private func _resetTeam() {
self.currentWaitingList = tournament.waitingListSortedTeams().filter({ $0.hasRegisteredOnline() }).first let selectedSortedTeams = tournament.selectedSortedTeams()
self.currentWaitingList = tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams).filter({ $0.hasRegisteredOnline() }).first
team.resetPositions() team.resetPositions()
team.wildCardGroupStage = false team.wildCardGroupStage = false
team.walkOut = false team.walkOut = false

@ -13,10 +13,11 @@ struct TeamRowView: View {
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false var displayCallDate: Bool = false
var displayRestingTime: Bool = false var displayRestingTime: Bool = false
var teamIndex: Int?
var body: some View { var body: some View {
LabeledContent { LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition, teamIndex: teamIndex)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
TeamHeadlineView(team: team) TeamHeadlineView(team: team)

@ -312,7 +312,7 @@ struct FileImportView: View {
} }
} else if didImport { } else if didImport {
let _filteredTeams = filteredTeams let _filteredTeams = filteredTeams
let previousTeams = tournament.sortedTeams() let previousTeams = tournament.sortedTeams(selectedSortedTeams: tournament.selectedSortedTeams())
if previousTeams.isEmpty == false { if previousTeams.isEmpty == false {
Section { Section {

@ -595,15 +595,12 @@ struct AddTeamView: View {
return 1 return 1
} }
@MainActor
private func handlePasteString(_ first: String) { private func handlePasteString(_ first: String) {
if first.isEmpty == false { if first.isEmpty == false {
DispatchQueue.main.async {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
autoSelect = true autoSelect = true
} }
}
pasteString = first pasteString = first
editableTextField = first editableTextField = first
textHeight = Self._calculateHeight(text: first) textHeight = Self._calculateHeight(text: first)

@ -93,8 +93,9 @@ struct TournamentGeneralSettingsView: View {
} }
.frame(maxHeight: 200) .frame(maxHeight: 200)
.overlay { .overlay {
if tournamentInformation.isEmpty { if tournamentInformation.isEmpty, focusedField != ._information {
Text("Texte visible dans l'onglet informations sur Padel Club.").italic() Text("Texte visible dans l'onglet informations sur Padel Club.").italic()
.foregroundStyle(.secondary)
} }
} }
} header: { } header: {

@ -34,7 +34,7 @@ struct TournamentMatchFormatsSettingsView: View {
Section { Section {
LabeledContent { LabeledContent {
StepperView(title: "minutes", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10) StepperView(title: "minute", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10)
} label: { } label: {
Text("Modifier les durées moyennes") Text("Modifier les durées moyennes")
} }

@ -180,7 +180,7 @@ struct InscriptionManagerView: View {
return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) return _simpleHash(ids: ids1) != _simpleHash(ids: ids2)
} }
private func _setHash() { private func _setHash(currentSelectedSortedTeams: [TeamRegistration]? = nil) {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -188,18 +188,17 @@ struct InscriptionManagerView: View {
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
let selectedSortedTeams = tournament.selectedSortedTeams() let selectedSortedTeams = currentSelectedSortedTeams == nil ? tournament.selectedSortedTeams() : currentSelectedSortedTeams!
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
} }
self.registrationIssues = nil self.registrationIssues = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.registrationIssues = tournament.registrationIssues() self.registrationIssues = tournament.registrationIssues(selectedTeams: selectedSortedTeams)
} }
} }
private func _handleHashDiff() { private func _handleHashDiff(selectedSortedTeams: [TeamRegistration]) {
let selectedSortedTeams = tournament.selectedSortedTeams()
let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) { if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) {
self.teamsHash = newHash self.teamsHash = newHash
@ -225,9 +224,10 @@ struct InscriptionManagerView: View {
} }
var body: some View { var body: some View {
Group { let selectedSortedTeams = tournament.selectedSortedTeams()
if tournament.unsortedTeams().isEmpty == false { return Group {
_teamRegisteredView() if tournament.unsortedTeamsCount() > 0 {
_teamRegisteredView(selectedSortedTeams: selectedSortedTeams)
} else { } else {
List { List {
@ -263,10 +263,10 @@ struct InscriptionManagerView: View {
await _refreshList() await _refreshList()
} }
.onAppear { .onAppear {
_setHash() _setHash(currentSelectedSortedTeams: selectedSortedTeams)
} }
.onDisappear { .onDisappear {
_handleHashDiff() _handleHashDiff(selectedSortedTeams: selectedSortedTeams)
} }
.sheet(isPresented: $isLearningMore) { .sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament) LearnMoreSheetView(tournament: tournament)
@ -490,47 +490,43 @@ struct InscriptionManagerView: View {
tournament.unsortedPlayers() tournament.unsortedPlayers()
} }
var sortedTeams: [TeamRegistration] { func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
if filterMode == .waiting { if filterMode == .waiting {
return tournament.waitingListSortedTeams() return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams)
} else { } else {
return tournament.sortedTeams() return tournament.sortedTeams(selectedSortedTeams: selectedSortedTeams)
} }
} }
var filteredTeams: [TeamRegistration] { func filteredTeams(sortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let filtered = sortedTeams.lazy.filter { team in
var teams = sortedTeams
switch filterMode { switch filterMode {
case .wildcardBracket: case .wildcardBracket:
teams = teams.filter({ $0.wildCardBracket }) return team.wildCardBracket
case .wildcardGroupStage: case .wildcardGroupStage:
teams = teams.filter({ $0.wildCardGroupStage }) return team.wildCardGroupStage
case .walkOut: case .walkOut:
teams = teams.filter({ $0.walkOut }) return team.walkOut
case .bracket: case .bracket:
teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false }) return team.inRound() && !team.inGroupStage()
case .groupStage: case .groupStage:
teams = teams.filter({ $0.inGroupStage() }) return team.inGroupStage()
case .notImported: case .notImported:
teams = teams.filter({ $0.isImported() == false }) return !team.isImported()
case .registeredLocally: case .registeredLocally:
teams = teams.filter({ $0.hasRegisteredOnline() == false }) return !team.hasRegisteredOnline()
case .registeredOnline: case .registeredOnline:
teams = teams.filter({ $0.hasRegisteredOnline() == true }) return team.hasRegisteredOnline()
default: default:
break return true
} }
if sortingMode == .registrationDate {
teams = teams.sorted(by: \.computedRegistrationDate)
} }
if byDecreasingOrdering { let sorted = sortingMode == .registrationDate
return teams.reversed() ? filtered.sorted(by: { $0.computedRegistrationDate < $1.computedRegistrationDate })
} else { : Array(filtered)
return teams
} return byDecreasingOrdering ? sorted.reversed() : sorted
} }
// private func _fixModel() { // private func _fixModel() {
@ -572,12 +568,10 @@ struct InscriptionManagerView: View {
} }
} }
private func _teamRegisteredView() -> some View { private func _teamRegisteredView(selectedSortedTeams: [TeamRegistration]) -> some View {
List { List {
let selectedSortedTeams = tournament.selectedSortedTeams()
if presentSearch == false { if presentSearch == false {
_informationView() _informationView(for: selectedSortedTeams)
if tournament.isAnimation() == false { if tournament.isAnimation() == false {
_rankHandlerView() _rankHandlerView()
@ -585,7 +579,8 @@ struct InscriptionManagerView: View {
} }
} }
let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) let sortedTeams = sortedTeams(selectedSortedTeams: selectedSortedTeams)
let teams = searchField.isEmpty ? filteredTeams(sortedTeams: sortedTeams) : filteredTeams(sortedTeams: sortedTeams).filter({ $0.contains(searchField.canonicalVersion) })
if teams.isEmpty && searchField.isEmpty == false { if teams.isEmpty && searchField.isEmpty == false {
ContentUnavailableView { ContentUnavailableView {
@ -622,7 +617,7 @@ struct InscriptionManagerView: View {
EditingTeamView(team: team) EditingTeamView(team: team)
.environment(tournament) .environment(tournament)
} label: { } label: {
TeamRowView(team: team) TeamRowView(team: team, teamIndex: teamIndex)
} }
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
if tournament.enableOnlineRegistration == false { if tournament.enableOnlineRegistration == false {
@ -735,18 +730,18 @@ struct InscriptionManagerView: View {
} }
} }
private func _teamCountForFilterMode(filterMode: FilterMode) -> String { private func _teamCountForFilterMode(filterMode: FilterMode, in teams: [TeamRegistration]) -> String {
switch filterMode { switch filterMode {
case .wildcardBracket: case .wildcardBracket:
return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted() return teams.filter({ $0.wildCardBracket }).count.formatted()
case .wildcardGroupStage: case .wildcardGroupStage:
return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted() return teams.filter({ $0.wildCardGroupStage }).count.formatted()
case .all: case .all:
return unsortedTeamsWithoutWO.count.formatted() return unsortedTeamsWithoutWO.count.formatted()
case .bracket: case .bracket:
return tournament.selectedSortedTeams().filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted() return teams.filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted()
case .groupStage: case .groupStage:
return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted() return teams.filter({ $0.inGroupStage() }).count.formatted()
case .walkOut: case .walkOut:
let wo = walkoutTeams.count.formatted() let wo = walkoutTeams.count.formatted()
return wo return wo
@ -754,20 +749,20 @@ struct InscriptionManagerView: View {
let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
return waiting.formatted() return waiting.formatted()
case .notImported: case .notImported:
let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count) let notImported: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.isImported() == false }).count)
return notImported.formatted() return notImported.formatted()
case .registeredLocally: case .registeredLocally:
let registeredLocally: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() == false }).count) let registeredLocally: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() == false }).count)
return registeredLocally.formatted() return registeredLocally.formatted()
case .registeredOnline: case .registeredOnline:
let registeredOnline: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() }).count) let registeredOnline: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() }).count)
return registeredOnline.formatted() return registeredOnline.formatted()
} }
} }
@ViewBuilder @ViewBuilder
private func _informationView() -> some View { private func _informationView(for teams: [TeamRegistration]) -> some View {
Section { Section {
HStack { HStack {
// VStack(alignment: .leading, spacing: 0) { // VStack(alignment: .leading, spacing: 0) {
@ -781,7 +776,7 @@ struct InscriptionManagerView: View {
// } // }
// //
ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in
_filterModeView(filterMode: filterMode) _filterModeView(filterMode: filterMode, in: teams)
} }
Button { Button {
@ -809,7 +804,7 @@ struct InscriptionManagerView: View {
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
HStack { HStack {
ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in
_filterModeView(filterMode: filterMode) _filterModeView(filterMode: filterMode, in: teams)
} }
} }
.padding(.bottom, -4) .padding(.bottom, -4)
@ -883,7 +878,7 @@ struct InscriptionManagerView: View {
} }
} }
private func _filterModeView(filterMode: FilterMode) -> some View { private func _filterModeView(filterMode: FilterMode, in teams: [TeamRegistration]) -> some View {
Button { Button {
if self.filterMode == filterMode { if self.filterMode == filterMode {
@ -894,7 +889,7 @@ struct InscriptionManagerView: View {
} label: { } label: {
VStack(alignment: .center, spacing: -2) { VStack(alignment: .center, spacing: -2) {
Text(filterMode.localizedLabel(.short)).font(.caption).padding(.horizontal, -8) Text(filterMode.localizedLabel(.short)).font(.caption).padding(.horizontal, -8)
Text(_teamCountForFilterMode(filterMode: filterMode)).font(.largeTitle) Text(_teamCountForFilterMode(filterMode: filterMode, in: teams)).font(.largeTitle)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())

@ -28,10 +28,20 @@ struct PrintSettingsView: View {
// Toggle(isOn: $generator.displayHeads, label: { // Toggle(isOn: $generator.displayHeads, label: {
// Text("Afficher les têtes de séries") // Text("Afficher les têtes de séries")
// }) // })
Toggle(isOn: $generator.displayTeamIndex, label: {
Text("Afficher le poids et le rang de l'équipe")
})
Toggle(isOn: $generator.displayRank, label: { Toggle(isOn: $generator.displayRank, label: {
Text("Afficher le classement du joueur") Text("Afficher le classement du joueur")
}) })
Toggle(isOn: $generator.displayScore, label: {
Text("Afficher le score")
Text("Affiche le score des matchs terminés")
})
Toggle(isOn: $generator.includeBracket, label: { Toggle(isOn: $generator.includeBracket, label: {
Text("Tableau") Text("Tableau")
}) })
@ -152,32 +162,34 @@ struct PrintSettingsView: View {
.navigationTitle("Imprimer") .navigationTitle("Imprimer")
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
// .toolbar {
// ToolbarItem(placement: .topBarTrailing) {
// Menu {
// Section {
// ShareLink(item: generator.generateHtml()) {
// Text("Tableau")
// }
//
// if let groupStage = tournament.groupStages().first {
// ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) {
// Text("Poule")
// }
// }
// } header: {
// Text("Partager le code source HTML")
// }
// } label: {
// Label("Options", systemImage: "ellipsis.circle")
// }
// }
// }
.sheet(isPresented: $presentShareView) { .sheet(isPresented: $presentShareView) {
if let pdfURL = generator.pdfURL { if let pdfURL = generator.pdfURL {
ShareSheet(urls: [pdfURL]) ShareSheet(urls: [pdfURL])
} }
} }
#if DEBUG
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Section {
ShareLink(item: generator.generateHtml()) {
Text("Tableau")
}
if let groupStage = tournament.groupStages().first {
ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)) {
Text("Poule")
}
}
} header: {
Text("Partager le code source HTML")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
#endif
} }
@ViewBuilder @ViewBuilder
@ -199,7 +211,7 @@ struct PrintSettingsView: View {
Group { Group {
if prepareGroupStage { if prepareGroupStage {
ForEach(tournament.groupStages()) { groupStage in ForEach(tournament.groupStages()) { groupStage in
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore), loadStatusChanged: { loaded, error, webView in
if let error { if let error {
print("preparePDF", error) print("preparePDF", error)
} else if loaded == false { } else if loaded == false {
@ -301,7 +313,7 @@ struct WebViewPreview: View {
ProgressView() ProgressView()
.onAppear { .onAppear {
if let groupStage { if let groupStage {
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false) html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)
} else if let round { } else if let round {
html = generator.generateLoserBracketHtml(upperRound: round) html = generator.generateLoserBracketHtml(upperRound: round)
} else { } else {

@ -141,17 +141,18 @@ struct RegistrationSetupView: View {
} }
Section { Section {
Toggle(isOn: $targetTeamCountEnabled) { // Toggle(isOn: $targetTeamCountEnabled) {
Text("Activer une limite") // Text("Activer une limite")
} // }
//
if targetTeamCountEnabled { // if targetTeamCountEnabled {
// StepperView(count: $targetTeamCount, minimum: 4)
// }
StepperView(count: $targetTeamCount, minimum: 4) StepperView(count: $targetTeamCount, minimum: 4)
}
} header: { } header: {
Text("Paires admises") Text("Paires admises")
} footer: { } footer: {
Text("Si une limite de paire existe, les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.") Text("Les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.")
} }
Section { Section {
@ -160,7 +161,7 @@ struct RegistrationSetupView: View {
} }
if waitingListLimitEnabled { if waitingListLimitEnabled {
StepperView(count: $waitingListLimit, minimum: 1) StepperView(count: $waitingListLimit, minimum: 0)
} }
} header: { } header: {
Text("Liste d'attente") Text("Liste d'attente")

@ -71,7 +71,7 @@ struct TournamentRankView: View {
} footer: { } footer: {
if let url = tournament.shareURL(.rankings) { if let url = tournament.shareURL(.rankings) {
Link(destination: url) { Link(destination: url) {
Text("Voir la page des classements sur Padel Club") Text("Voir les classements sur Padel Club")
} }
} }
} }

@ -124,7 +124,7 @@ struct TournamentBuildView: View {
.foregroundStyle(.green) .foregroundStyle(.green)
} }
} label: { } label: {
Text("Classement final des équipes") Text("Classement final")
if tournament.publishRankings == false { if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
} }

@ -21,6 +21,10 @@ struct TournamentInscriptionView: View {
Text("Gestion des inscriptions") Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate { if let closedRegistrationDate = tournament.closedRegistrationDate {
Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened))
} else if tournament.enableOnlineRegistration {
Text("Inscription en ligne activée")
} else if tournament.onlineRegistrationCanBeEnabled() {
Text("Inscription en ligne désactivée")
} }
} }
} }

@ -249,7 +249,7 @@ struct TournamentView: View {
.foregroundStyle(.green) .foregroundStyle(.green)
} }
} label: { } label: {
Text("Classement final des équipes") Text("Classement final")
if tournament.publishRankings == false { if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
} }

Loading…
Cancel
Save