sync2
Laurent 9 months ago
commit f46890c445
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 1
      PadelClub/Data/Federal/FederalTournamentHolder.swift
  3. 395
      PadelClub/Data/TeamRegistration.swift
  4. 571
      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. 132
      PadelClub/PadelClubApp.swift
  10. 7
      PadelClub/Utils/HtmlGenerator.swift
  11. 62
      PadelClub/Utils/HtmlService.swift
  12. 34
      PadelClub/Utils/PadelRule.swift
  13. 7
      PadelClub/Utils/URLs.swift
  14. 33
      PadelClub/Utils/VersionComparator.swift
  15. 190
      PadelClub/ViewModel/SearchViewModel.swift
  16. 2
      PadelClub/Views/Calling/CallSettingsView.swift
  17. 19
      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. 133
      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. 9
      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. 121
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  35. 60
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  36. 21
      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:

@ -11,43 +11,50 @@ 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()
@ -67,83 +74,85 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
self.confirmationDate = confirmationDate self.confirmationDate = confirmationDate
self.qualified = qualified self.qualified = qualified
} }
func hasRegisteredOnline() -> Bool { func hasRegisteredOnline() -> Bool {
players().anySatisfy({ $0.registeredOnline }) players().anySatisfy({ $0.registeredOnline })
} }
func unrankedOrUnknown() -> Bool { func unrankedOrUnknown() -> Bool {
players().anySatisfy({ $0.source == nil }) players().anySatisfy({ $0.source == nil })
} }
func isOutOfTournament() -> Bool { func isOutOfTournament() -> Bool {
walkOut walkOut
} }
required init(from decoder: any Decoder) throws { required init(from decoder: any Decoder) throws {
try super.init(from: decoder) try super.init(from: decoder)
} }
var tournamentStore: TournamentStore? { var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament) return TournamentLibrary.shared.store(tournamentId: self.tournament)
} }
// MARK: - Computed dependencies // MARK: - Computed dependencies
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: -
func deleteTeamScores() { func deleteTeamScores() {
guard let tournamentStore = self.tournamentStore else { return } guard let tournamentStore = self.tournamentStore else { return }
let ts = tournamentStore.teamScores.filter({ $0.teamRegistration == id }) let ts = tournamentStore.teamScores.filter({ $0.teamRegistration == id })
tournamentStore.teamScores.delete(contentOfs: ts) tournamentStore.teamScores.delete(contentOfs: ts)
} }
override func deleteDependencies() { override func deleteDependencies() {
let unsortedPlayers = unsortedPlayers() let unsortedPlayers = unsortedPlayers()
for player in unsortedPlayers { for player in unsortedPlayers {
player.deleteDependencies() player.deleteDependencies()
} }
self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers) self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers)
let teamScores = teamScores() let teamScores = teamScores()
for teamScore in teamScores { for teamScore in teamScores {
teamScore.deleteDependencies() teamScore.deleteDependencies()
} }
self.tournamentStore?.teamScores.deleteDependencies(teamScores) self.tournamentStore?.teamScores.deleteDependencies(teamScores)
} }
func hasArrived(isHere: Bool = false) { func hasArrived(isHere: Bool = false) {
let unsortedPlayers = unsortedPlayers() let unsortedPlayers = unsortedPlayers()
unsortedPlayers.forEach({ $0.hasArrived = !isHere }) unsortedPlayers.forEach({ $0.hasArrived = !isHere })
self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
} }
func isHere() -> Bool { func isHere() -> Bool {
let unsortedPlayers = unsortedPlayers() let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false } if unsortedPlayers.isEmpty { return false }
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 {
@ -170,7 +181,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
tournament.updateTeamScores(in: bracketPosition) tournament.updateTeamScores(in: bracketPosition)
} }
} }
func expectedSummonDate() -> Date? { func expectedSummonDate() -> Date? {
if let groupStageStartDate = groupStageObject()?.startDate { if let groupStageStartDate = groupStageObject()?.startDate {
return groupStageStartDate return groupStageStartDate
@ -179,11 +190,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
return nil return nil
} }
var initialWeight: Int { var initialWeight: Int {
return lockedWeight ?? weight return lockedWeight ?? weight
} }
func called() -> Bool { func called() -> Bool {
return callDate != nil return callDate != nil
} }
@ -195,36 +206,36 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func getPhoneNumbers() -> [String] { func getPhoneNumbers() -> [String] {
return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false }) return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false })
} }
func getMail() -> [String] { func getMail() -> [String] {
let mails = players().compactMap({ $0.email }) let mails = players().compactMap({ $0.email })
return mails return mails
} }
func isImported() -> Bool { func isImported() -> Bool {
let unsortedPlayers = unsortedPlayers() let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false } if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.isImported() }) return unsortedPlayers.allSatisfy({ $0.isImported() })
} }
func isWildCard() -> Bool { func isWildCard() -> Bool {
return wildCardBracket || wildCardGroupStage return wildCardBracket || wildCardGroupStage
} }
func isPlaying() -> Bool { func isPlaying() -> Bool {
return currentMatch() != nil return currentMatch() != nil
} }
func currentMatch() -> Match? { func currentMatch() -> Match? {
return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() }) return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
} }
func teamScores() -> [TeamScore] { func teamScores() -> [TeamScore] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamScores.filter({ $0.teamRegistration == id }) return tournamentStore.teamScores.filter({ $0.teamRegistration == id })
} }
func wins() -> [Match] { func wins() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.winningTeamId == id }) return tournamentStore.matches.filter({ $0.winningTeamId == id })
@ -234,7 +245,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id }) return tournamentStore.matches.filter({ $0.losingTeamId == id })
} }
func matches() -> [Match] { func matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id }) return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id })
@ -243,62 +254,74 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
var tournamentCategory: TournamentCategory { var tournamentCategory: TournamentCategory {
tournamentObject()?.tournamentCategory ?? .men tournamentObject()?.tournamentCategory ?? .men
} }
@objc @objc
var canonicalName: String { var canonicalName: String {
players().map { $0.canonicalName }.joined(separator: " ") players().map { $0.canonicalName }.joined(separator: " ")
} }
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
}) })
} }
func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) { func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) {
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? {
guard let tournament = tournamentObject() else { return nil } guard let tournament = tournamentObject() else { return nil }
guard let index = index(in: tournament.selectedSortedTeams()) else { return nil } guard let index = index(in: tournament.selectedSortedTeams()) else { return nil }
return "(\(index + 1))" return "(\(index + 1))"
} }
func index(in teams: [TeamRegistration]) -> Int? { func index(in teams: [TeamRegistration]) -> Int? {
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 "###"
} }
} }
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
} }
func includes(players: [PlayerRegistration]) -> Bool { func includes(players: [PlayerRegistration]) -> Bool {
let unsortedPlayers = unsortedPlayers() let unsortedPlayers = unsortedPlayers()
guard players.count == unsortedPlayers.count else { return false } guard players.count == unsortedPlayers.count else { return false }
@ -308,32 +331,33 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
} }
} }
func includes(player: PlayerRegistration) -> Bool { func includes(player: PlayerRegistration) -> Bool {
return unsortedPlayers().anySatisfy { _player in return unsortedPlayers().anySatisfy { _player in
_player.isSameAs(player) _player.isSameAs(player)
} }
} }
func canPlay() -> Bool { func canPlay() -> Bool {
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 {
return groupStage == nil && bracketPosition == nil return groupStage == nil && bracketPosition == nil
} }
func inGroupStage() -> Bool { func inGroupStage() -> Bool {
return groupStagePosition != nil return groupStagePosition != nil
} }
func inRound() -> Bool { func inRound() -> Bool {
return bracketPosition != nil return bracketPosition != nil
} }
func positionLabel() -> String? { func positionLabel() -> String? {
if groupStagePosition != nil { return "Poule" } if groupStagePosition != nil { return "Poule" }
if let initialRound = initialRound() { if let initialRound = initialRound() {
@ -342,62 +366,75 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return nil return nil
} }
} }
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
} }
} }
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() })
groupStage = nil groupStage = nil
groupStagePosition = nil groupStagePosition = nil
} }
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
} }
func resetPositions() { func resetPositions() {
resetGroupeStagePosition() resetGroupeStagePosition()
resetBracketPosition() resetBracketPosition()
} }
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())
} }
} }
var computedRegistrationDate: Date { var computedRegistrationDate: Date {
return registrationDate ?? .distantFuture return registrationDate ?? .distantFuture
} }
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 {
case .rawText: case .rawText:
return "Inscrit\(onlineSuffix) le \(formattedDate)" return "Inscrit\(onlineSuffix) le \(formattedDate)"
@ -405,13 +442,14 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return formattedDate return formattedDate
} }
} }
func formattedSummonDate(_ exportFormat: ExportFormat = .rawText) -> String? { func formattedSummonDate(_ exportFormat: ExportFormat = .rawText) -> String? {
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
} }
@ -423,22 +461,31 @@ 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
@ -449,8 +496,7 @@ 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)
@ -458,16 +504,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
players.forEach { player in players.forEach { player in
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?)
func replacementRange() -> TeamRange? { func replacementRange() -> TeamRange? {
guard let tournamentObject = tournamentObject() else { return nil } guard let tournamentObject = tournamentObject() else { return nil }
guard let index = tournamentObject.indexOf(team: self) else { return nil } guard let index = tournamentObject.indexOf(team: self) else { return nil }
@ -476,7 +522,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
let right = selectedSortedTeams[safe: index + 1] let right = selectedSortedTeams[safe: index + 1]
return (left: left, right: right) return (left: left, right: right)
} }
func replacementRangeExtended() -> TeamRange? { func replacementRangeExtended() -> TeamRange? {
guard let tournamentObject = tournamentObject() else { return nil } guard let tournamentObject = tournamentObject() else { return nil }
guard let groupStagePosition else { return nil } guard let groupStagePosition else { return nil }
@ -485,19 +531,23 @@ 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)
} }
typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool
func players() -> [PlayerRegistration] { func players() -> [PlayerRegistration] {
@ -506,36 +556,43 @@ 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 {
if !predicate(lhs, rhs) && !predicate(rhs, lhs) { if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
continue continue
} }
return predicate(lhs, rhs) return predicate(lhs, rhs)
} }
return false return false
} }
} }
func coaches() -> [PlayerRegistration] { func coaches() -> [PlayerRegistration] {
guard let store = self.tournamentStore else { return [] } guard let store = self.tournamentStore else { return [] }
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 {
return tournamentObject()?.significantPlayerCount() ?? 2 return tournamentObject()?.significantPlayerCount() ?? 2
} }
func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] { func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] {
let players = unsortedPlayers() let players = unsortedPlayers()
if players.count >= 2 { return [] } if players.count >= 2 { return [] }
@ -548,16 +605,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
return missing return missing
} }
func unrankValue(for malePlayer: Bool) -> Int { func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000 return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000
} }
func groupStageObject() -> GroupStage? { func groupStageObject() -> GroupStage? {
guard let groupStage else { return nil } guard let groupStage else { return nil }
return self.tournamentStore?.groupStages.findById(groupStage) return self.tournamentStore?.groupStages.findById(groupStage)
} }
func initialRound() -> Round? { func initialRound() -> Round? {
guard let bracketPosition else { return nil } guard let bracketPosition else { return nil }
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2) let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
@ -567,22 +624,23 @@ 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 {
confirmationDate != nil confirmationDate != nil
} }
func tournamentObject() -> Tournament? { func tournamentObject() -> Tournament? {
return Store.main.findById(tournament) return Store.main.findById(tournament)
} }
func groupStagePositionAtStep(_ step: Int) -> Int? { func groupStagePositionAtStep(_ step: Int) -> Int? {
guard let groupStagePosition else { return nil } guard let groupStagePosition else { return nil }
if step == 0 { if step == 0 {
@ -592,33 +650,36 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
return nil return nil
} }
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
} }
} }
var _cachedRestingTime: (Bool, Date?)? var _cachedRestingTime: (Bool, Date?)?
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
} }
func resetRestingTime() { func resetRestingTime() {
_cachedRestingTime = nil _cachedRestingTime = nil
} }
var restingTimeForSorting: Date { var restingTimeForSorting: Date {
restingTime()! restingTime()!
} }
func teamNameLabel() -> String { func teamNameLabel() -> String {
if let name, name.isEmpty == false { if let name, name.isEmpty == false {
return name return name
@ -626,25 +687,29 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return "Toute l'équipe" return "Toute l'équipe"
} }
} }
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() {
playerRegistration.insertOnServer() playerRegistration.insertOnServer()
} }
} }
} }
enum TeamDataSource: Int, Codable { enum TeamDataSource: Int, Codable {

File diff suppressed because it is too large Load Diff

@ -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,54 +64,86 @@ struct PadelClubApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MainView()
.environment(\.horizontalSizeClass, .compact) if self.blockApp {
.alert(isPresented: presentError, error: registrationError) { DownloadNewVersionView()
Button("Contactez-nous") { } else {
_openMail() MainView()
} .environment(\.horizontalSizeClass, .compact)
Button("Annuler", role: .cancel) { .alert(isPresented: presentError, error: registrationError) {
registrationError = nil Button("Contactez-nous") {
_openMail()
}
Button("Annuler", role: .cancel) {
registrationError = nil
}
} }
} .onOpenURL { url in
.onOpenURL { url in
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
#else #else
_handleIncomingURL(url) _handleIncomingURL(url)
#endif #endif
} }
.environmentObject(networkMonitor) .environmentObject(networkMonitor)
.environmentObject(dataStore) .environmentObject(dataStore)
.environment(importObserverViewModel) .environment(importObserverViewModel)
.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()
print(PersistenceController.getModelVersion()) print(PersistenceController.getModelVersion())
} }
.task { .task {
try? Tips.resetDatastore() try? Tips.resetDatastore()
try? Tips.configure([ try? Tips.configure([
.displayFrequency(.immediate), .displayFrequency(.immediate),
.datastoreLocation(.applicationDefault) .datastoreLocation(.applicationDefault)
]) ])
}
.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
}
} }
.environment(\.managedObjectContext, persistenceController.localContainer.viewContext) }
} }
} }
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
let pathComponents = url.pathComponents let pathComponents = url.pathComponents
@ -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,16 +1996,30 @@ 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 {
switch self { if level == .p500 {
case .inscription: switch self {
return -13 case .inscription:
case .broadcastList: return -6
return -12 case .broadcastList:
case .wildcardRequest: return -6
return -9 case .wildcardRequest:
case .wildcardLicensePurchase, .definitiveBroadcastList: return -4
return -8 case .wildcardLicensePurchase, .definitiveBroadcastList:
return -4
}
} else {
switch self {
case .inscription:
return -13
case .broadcastList:
return -12
case .wildcardRequest:
return -9
case .wildcardLicensePurchase, .definitiveBroadcastList:
return -8
}
} }
} }

@ -46,7 +46,12 @@ 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,66 +172,85 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
} }
func searchTextPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = []
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed
if canonicalVersionWithoutPunctuation.isEmpty == false {
let wordsPredicates = wordsPredicates()
if let wordsPredicates {
predicates.append(wordsPredicates)
} else {
predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation))
}
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation))
let components = canonicalVersionWithoutPunctuation.split(separator: " ")
let pattern = components.joined(separator: ".*")
let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
predicates.append(predicate)
}
if predicates.isEmpty {
return nil
}
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
}
func orPredicate() -> NSPredicate? { func orPredicate() -> 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 let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed
switch tokens.first {
case .none: if tokens.isEmpty {
if canonicalVersionWithoutPunctuation.isEmpty == false { if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false {
let wordsPredicates = wordsPredicates() if let searchTextPredicate = searchTextPredicate() {
if let wordsPredicates { predicates.append(searchTextPredicate)
predicates.append(wordsPredicates)
} else {
predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation))
} }
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation))
let components = canonicalVersionWithoutPunctuation.split(separator: " ")
let pattern = components.joined(separator: ".*")
let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
predicates.append(predicate)
}
case .ligue:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil"))
} else {
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .club:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "clubName == nil"))
} else {
predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .rankMoreThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation))
}
case .rankLessThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation))
}
case .rankBetween:
let values = canonicalVersionWithPunctuation.components(separatedBy: ",")
if canonicalVersionWithPunctuation.isEmpty || values.count != 2 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
} }
case .age: }
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { for token in tokens {
predicates.append(NSPredicate(format: "birthYear == 0")) switch token {
} else if let birthYear = Int(canonicalVersionWithoutPunctuation) { case .ligue:
predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil"))
} else {
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .club:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "clubName == nil"))
} else {
predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .rankMoreThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation))
}
case .rankLessThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation))
}
case .rankBetween:
let values = canonicalVersionWithPunctuation.components(separatedBy: ",")
if canonicalVersionWithPunctuation.isEmpty || values.count != 2 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
}
case .age:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "birthYear == 0"))
} else if let birthYear = Int(canonicalVersionWithoutPunctuation) {
predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString()))
}
} }
} }
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,11 +390,20 @@ class SearchViewModel: ObservableObject, Identifiable {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
if nameComponents.count > 1 {
orPredicates.append(contentsOf: nameComponents.pairs().map { if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) {
return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }) orPredicates.append(slashPredicate)
} else { }
orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) })
print("nameComponents", nameComponents.count)
if nameComponents.count < 50 {
if nameComponents.count > 1 {
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) })
} else {
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: " ")
@ -355,11 +411,7 @@ class SearchViewModel: ObservableObject, Identifiable {
print(text, pattern) print(text, pattern)
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,15 +6,32 @@
// //
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 {
NavigationLink { NavigationLink {

@ -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)
}
}

@ -14,7 +14,7 @@ struct PlanningSettingsView: View {
@Bindable var tournament: Tournament @Bindable var tournament: Tournament
@Bindable var matchScheduler: MatchScheduler @Bindable var matchScheduler: MatchScheduler
@State private var groupStageChunkCount: Int @State private var groupStageChunkCount: Int
@State private var isScheduling: Bool = false @State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false @State private var schedulingDone: Bool = false
@ -23,11 +23,12 @@ 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
} }
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
if let matchScheduler = tournament.matchScheduler() { if let matchScheduler = tournament.matchScheduler() {
@ -43,7 +44,7 @@ struct PlanningSettingsView: View {
self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue())
} }
} }
var body: some View { var body: some View {
List { List {
if tournament.payment == nil { if tournament.payment == nil {
@ -62,7 +63,7 @@ struct PlanningSettingsView: View {
} }
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
if let event = tournament.eventObject() { if let event = tournament.eventObject() {
NavigationLink { NavigationLink {
CourtAvailabilitySettingsView(event: event) CourtAvailabilitySettingsView(event: event)
@ -75,7 +76,7 @@ struct PlanningSettingsView: View {
} }
} }
} }
NavigationLink { NavigationLink {
MultiCourtPickerView(matchScheduler: matchScheduler) MultiCourtPickerView(matchScheduler: matchScheduler)
.environment(tournament) .environment(tournament)
@ -118,7 +119,7 @@ struct PlanningSettingsView: View {
} }
} }
} }
if issueFound { if issueFound {
Text("Padel Club n'a pas réussi à définir un horaire pour tous les matchs de ce tournoi, à cause de la programmation d'autres tournois ou de l'indisponibilité des terrains.") Text("Padel Club n'a pas réussi à définir un horaire pour tous les matchs de ce tournoi, à cause de la programmation d'autres tournois ou de l'indisponibilité des terrains.")
.foregroundStyle(.logoRed) .foregroundStyle(.logoRed)
@ -131,7 +132,7 @@ struct PlanningSettingsView: View {
} label: { } label: {
Text("Voir plus d'options intelligentes") Text("Voir plus d'options intelligentes")
} }
if let event, event.tournaments.count > 1 { if let event, event.tournaments.count > 1 {
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) {
Text("Ne pas tenir compte des autres tournois") Text("Ne pas tenir compte des autres tournois")
@ -142,9 +143,31 @@ struct PlanningSettingsView: View {
Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.") Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.")
} }
} }
_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 {
@ -165,13 +188,13 @@ struct PlanningSettingsView: View {
.deferredRendering(for: .seconds(2)) .deferredRendering(for: .seconds(2))
} }
} }
if deletingDone { if deletingDone {
Label("Tous les horaires ont été supprimés", systemImage: "clock.badge.xmark") Label("Tous les horaires ont été supprimés", systemImage: "clock.badge.xmark")
.toastFormatted() .toastFormatted()
.deferredRendering(for: .seconds(2)) .deferredRendering(for: .seconds(2))
} }
if deletingDateMatchesDone { if deletingDateMatchesDone {
Label("Horaires des matchs supprimés", systemImage: "clock.badge.xmark") Label("Horaires des matchs supprimés", systemImage: "clock.badge.xmark")
.toastFormatted() .toastFormatted()
@ -189,7 +212,7 @@ struct PlanningSettingsView: View {
_save() _save()
} }
} }
private func _localizedFooterMessage(groupStagesWithDateIsEmpty: Bool, roundsWithDateIsEmpty: Bool) -> String { private func _localizedFooterMessage(groupStagesWithDateIsEmpty: Bool, roundsWithDateIsEmpty: Bool) -> String {
let base = "Supprime les horaires des matchs restants non démarrés." let base = "Supprime les horaires des matchs restants non démarrés."
let extend = " Garde les horaires définis pour les " let extend = " Garde les horaires définis pour les "
@ -203,20 +226,20 @@ struct PlanningSettingsView: View {
return base + extend + "poules et les manches du tableau." return base + extend + "poules et les manches du tableau."
} }
} }
@ViewBuilder @ViewBuilder
private func _smartView() -> some View { private func _smartView() -> some View {
let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false })
let allGroupStages = tournament.allGroupStages() let allGroupStages = tournament.allGroupStages()
let allRounds = tournament.allRounds() let allRounds = tournament.allRounds()
let matchesWithDate = allMatches.filter({ $0.startDate != nil }) let matchesWithDate = allMatches.filter({ $0.startDate != nil })
let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate)
let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament)
_formatPerDayView(matchCountPerDay: countedSet) _formatPerDayView(matchCountPerDay: countedSet)
let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil })
let roundsWithDate = allRounds.filter({ $0.startDate != nil }) let roundsWithDate = allRounds.filter({ $0.startDate != nil })
if matchesWithDate.isEmpty == false { if matchesWithDate.isEmpty == false {
@ -238,7 +261,7 @@ struct PlanningSettingsView: View {
Text(_localizedFooterMessage(groupStagesWithDateIsEmpty: groupStagesWithDate.isEmpty, roundsWithDateIsEmpty: roundsWithDate.isEmpty)) Text(_localizedFooterMessage(groupStagesWithDateIsEmpty: groupStagesWithDate.isEmpty, roundsWithDateIsEmpty: roundsWithDate.isEmpty))
} }
} }
if groupStagesWithDate.isEmpty == false { if groupStagesWithDate.isEmpty == false {
Section { Section {
RowButtonView("Supprimer les horaires des poules", role: .destructive) { RowButtonView("Supprimer les horaires des poules", role: .destructive) {
@ -246,7 +269,7 @@ struct PlanningSettingsView: View {
deletingDone = false deletingDone = false
allGroupStages.forEach({ $0.startDate = nil }) allGroupStages.forEach({ $0.startDate = nil })
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages) try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
deletingDone = true deletingDone = true
} catch { } catch {
Logger.error(error) Logger.error(error)
@ -254,7 +277,7 @@ struct PlanningSettingsView: View {
} }
} }
} }
if roundsWithDate.isEmpty == false { if roundsWithDate.isEmpty == false {
Section { Section {
RowButtonView("Supprimer les horaires du tableau", role: .destructive) { RowButtonView("Supprimer les horaires du tableau", role: .destructive) {
@ -271,7 +294,7 @@ struct PlanningSettingsView: View {
Text("Supprime les horaires définis pour les manches du tableau.") Text("Supprime les horaires définis pour les manches du tableau.")
} }
} }
if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false { if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false {
Section { Section {
RowButtonView("Supprimer tous les horaires", role: .destructive) { RowButtonView("Supprimer tous les horaires", role: .destructive) {
@ -282,10 +305,10 @@ struct PlanningSettingsView: View {
$0.confirmed = false $0.confirmed = false
}) })
try self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches) try self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
allGroupStages.forEach({ $0.startDate = nil }) allGroupStages.forEach({ $0.startDate = nil })
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages) try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
allRounds.forEach({ $0.startDate = nil }) allRounds.forEach({ $0.startDate = nil })
try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds) try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
deletingDone = true deletingDone = true
@ -297,7 +320,7 @@ struct PlanningSettingsView: View {
Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.") Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.")
} }
} }
#if DEBUG #if DEBUG
Section { Section {
RowButtonView("Debug delete all dates", role: .destructive) { RowButtonView("Debug delete all dates", role: .destructive) {
@ -309,10 +332,10 @@ struct PlanningSettingsView: View {
$0.confirmed = false $0.confirmed = false
}) })
try self.tournamentStore?.matches.addOrUpdate(contentOfs: tournament.allMatches()) try self.tournamentStore?.matches.addOrUpdate(contentOfs: tournament.allMatches())
allGroupStages.forEach({ $0.startDate = nil }) allGroupStages.forEach({ $0.startDate = nil })
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages) try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
allRounds.forEach({ $0.startDate = nil }) allRounds.forEach({ $0.startDate = nil })
try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds) try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
deletingDone = true deletingDone = true
@ -325,7 +348,7 @@ struct PlanningSettingsView: View {
} }
#endif #endif
Section { Section {
if groupStagesWithDate.isEmpty == false { if groupStagesWithDate.isEmpty == false {
Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.")
@ -348,7 +371,7 @@ struct PlanningSettingsView: View {
Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline()
} }
} }
@ViewBuilder @ViewBuilder
private func _optionsView() -> some View { private func _optionsView() -> some View {
List { List {
@ -375,7 +398,7 @@ struct PlanningSettingsView: View {
matchScheduler.groupStageChunkCount = nil matchScheduler.groupStageChunkCount = nil
} }
} }
if parallelType { if parallelType {
TournamentFieldsManagerView(localizedStringKey: "Poule en parallèle", count: $groupStageChunkCount, max: tournament.groupStageCount) TournamentFieldsManagerView(localizedStringKey: "Poule en parallèle", count: $groupStageChunkCount, max: tournament.groupStageCount)
.onChange(of: groupStageChunkCount) { .onChange(of: groupStageChunkCount) {
@ -385,7 +408,7 @@ struct PlanningSettingsView: View {
} footer: { } footer: {
Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.") Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.")
} }
Section { Section {
Toggle(isOn: $matchScheduler.simultaneousStart) { Toggle(isOn: $matchScheduler.simultaneousStart) {
Text("Démarrage simultané") Text("Démarrage simultané")
@ -394,7 +417,7 @@ struct PlanningSettingsView: View {
Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.") Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.")
} }
} }
Section { Section {
Toggle(isOn: $matchScheduler.randomizeCourts) { Toggle(isOn: $matchScheduler.randomizeCourts) {
Text("Distribuer les terrains au hasard") Text("Distribuer les terrains au hasard")
@ -408,7 +431,7 @@ struct PlanningSettingsView: View {
} footer: { } footer: {
Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.")
} }
Section { Section {
Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) {
Text("Équilibrer les matchs d'une manche") Text("Équilibrer les matchs d'une manche")
@ -416,13 +439,13 @@ struct PlanningSettingsView: View {
} footer: { } footer: {
Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.")
} }
Section { Section {
Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) { Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) {
Text("Finir une manche, classement inclus avant de continuer") Text("Finir une manche, classement inclus avant de continuer")
} }
} }
Section { Section {
Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) { Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) {
Text("Tenir compte des temps de pause réglementaires") Text("Tenir compte des temps de pause réglementaires")
@ -430,7 +453,7 @@ struct PlanningSettingsView: View {
} header: { } header: {
Text("Tableau") Text("Tableau")
} }
Section { Section {
Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) { Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) {
Text("Tenir compte des temps de pause réglementaires") Text("Tenir compte des temps de pause réglementaires")
@ -438,19 +461,19 @@ struct PlanningSettingsView: View {
} header: { } header: {
Text("Classement") Text("Classement")
} }
Section { Section {
Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) { Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) {
Text("Forcer une rotation d'attente supplémentaire entre 2 phases") Text("Forcer une rotation d'attente supplémentaire entre 2 phases")
} }
LabeledContent { LabeledContent {
StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2) StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2)
} label: { } label: {
Text("Tableau") Text("Tableau")
} }
.disabled(matchScheduler.rotationDifferenceIsImportant == false) .disabled(matchScheduler.rotationDifferenceIsImportant == false)
LabeledContent { LabeledContent {
StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2) StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2)
} label: { } label: {
@ -460,7 +483,7 @@ struct PlanningSettingsView: View {
} footer: { } footer: {
Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.") Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.")
} }
Section { Section {
LabeledContent { LabeledContent {
StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5) StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5)
@ -478,11 +501,11 @@ struct PlanningSettingsView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }
private func _setupSchedule() async -> Bool { private func _setupSchedule() async -> Bool {
return matchScheduler.updateSchedule(tournament: tournament) return matchScheduler.updateSchedule(tournament: tournament)
} }
private func _save() { private func _save() {
do { do {
try self.tournamentStore?.matchSchedulers.addOrUpdate(instance: matchScheduler) try self.tournamentStore?.matchSchedulers.addOrUpdate(instance: matchScheduler)
@ -491,21 +514,21 @@ struct PlanningSettingsView: View {
Logger.error(error) Logger.error(error)
} }
} }
private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] { private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] {
var matchesByDay = [Date: [Match]]() var matchesByDay = [Date: [Match]]()
let calendar = Calendar.current let calendar = Calendar.current
for match in matches { for match in matches {
// Extract day/month/year and create a date with only these components // Extract day/month/year and create a date with only these components
let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting) let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting)
let strippedDate = calendar.date(from: components)! let strippedDate = calendar.date(from: components)!
// Group matches by the strippedDate (only day/month/year) // Group matches by the strippedDate (only day/month/year)
if matchesByDay[strippedDate] == nil { if matchesByDay[strippedDate] == nil {
matchesByDay[strippedDate] = [] matchesByDay[strippedDate] = []
} }
let shouldIncludeMatch: Bool let shouldIncludeMatch: Bool
switch match.matchType { switch match.matchType {
case .groupStage: case .groupStage:
@ -515,24 +538,24 @@ struct PlanningSettingsView: View {
case .loserBracket: case .loserBracket:
shouldIncludeMatch = true shouldIncludeMatch = true
} }
if shouldIncludeMatch { if shouldIncludeMatch {
matchesByDay[strippedDate]!.append(match) matchesByDay[strippedDate]!.append(match)
} }
} }
return matchesByDay return matchesByDay
} }
private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] { private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] {
let days = matchesByDay.keys let days = matchesByDay.keys
var matchCountPerDay = [Date: NSCountedSet]() var matchCountPerDay = [Date: NSCountedSet]()
for day in days { for day in days {
if let matches = matchesByDay[day] { if let matches = matchesByDay[day] {
var groupStageCount = 0 var groupStageCount = 0
let countedSet = NSCountedSet() let countedSet = NSCountedSet()
for match in matches { for match in matches {
switch match.matchType { switch match.matchType {
case .groupStage: case .groupStage:
@ -547,15 +570,15 @@ struct PlanningSettingsView: View {
break break
} }
} }
if groupStageCount > 0 { if groupStageCount > 0 {
for _ in 0..<groupStageCount { for _ in 0..<groupStageCount {
countedSet.add(tournament.groupStageMatchFormat) countedSet.add(tournament.groupStageMatchFormat)
} }
} }
if let loserRounds = matches.filter({ $0.round != nil }).filter({ $0.roundObject?.parent == nil }).sorted(by: \.computedStartDateForSorting).last?.roundObject?.loserRounds() { if let loserRounds = matches.filter({ $0.round != nil }).filter({ $0.roundObject?.parent == nil }).sorted(by: \.computedStartDateForSorting).last?.roundObject?.loserRounds() {
let ids = matches.map { $0.id } let ids = matches.map { $0.id }
for loserRound in loserRounds { for loserRound in loserRounds {
if let first = loserRound.playedMatches().first { if let first = loserRound.playedMatches().first {
@ -568,7 +591,7 @@ struct PlanningSettingsView: View {
matchCountPerDay[day] = countedSet matchCountPerDay[day] = countedSet
} }
} }
return matchCountPerDay return matchCountPerDay
} }
@ -578,7 +601,7 @@ struct PlanningSettingsView: View {
Section { Section {
let totalMatches = countedSet.totalCount() let totalMatches = countedSet.totalCount()
ForEach(Array(countedSet).compactMap { $0 as? MatchFormat }, id: \.self) { matchFormat in ForEach(Array(countedSet).compactMap { $0 as? MatchFormat }, id: \.self) { matchFormat in
let count = countedSet.count(for: matchFormat) let count = countedSet.count(for: matchFormat)
let totalForThisFormat = matchFormat.maximumMatchPerDay(for: totalMatches) let totalForThisFormat = matchFormat.maximumMatchPerDay(for: totalMatches)
let error = count > totalForThisFormat let error = count > totalForThisFormat
@ -605,7 +628,7 @@ struct PlanningSettingsView: View {
} }
} }
} }
// Helper function to format date to string (you can customize the format) // Helper function to format date to string (you can customize the format)
private func _formattedDate(_ date: Date) -> String { private func _formattedDate(_ date: Date) -> String {
let formatter = DateFormatter() let formatter = DateFormatter()

@ -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)
} }

@ -337,6 +337,21 @@ struct MySearchView: View {
_searchViewModel = ObservedObject(wrappedValue: searchViewModel) _searchViewModel = ObservedObject(wrappedValue: searchViewModel)
_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
@ -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,14 +595,11 @@ 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

@ -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: return team.wildCardBracket
teams = teams.filter({ $0.wildCardBracket }) case .wildcardGroupStage:
case .wildcardGroupStage: return team.wildCardGroupStage
teams = teams.filter({ $0.wildCardGroupStage }) case .walkOut:
case .walkOut: return team.walkOut
teams = teams.filter({ $0.walkOut }) case .bracket:
case .bracket: return team.inRound() && !team.inGroupStage()
teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false }) case .groupStage:
case .groupStage: return team.inGroupStage()
teams = teams.filter({ $0.inGroupStage() }) case .notImported:
case .notImported: return !team.isImported()
teams = teams.filter({ $0.isImported() == false }) case .registeredLocally:
case .registeredLocally: return !team.hasRegisteredOnline()
teams = teams.filter({ $0.hasRegisteredOnline() == false }) case .registeredOnline:
case .registeredOnline: return team.hasRegisteredOnline()
teams = teams.filter({ $0.hasRegisteredOnline() == true }) default:
default: return true
break }
}
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)
} // }
} header: { StepperView(count: $targetTeamCount, minimum: 4)
} 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