sync3
Laurent 1 month ago
commit ed161df9ba
  1. 35
      PadelClub.xcodeproj/project.pbxproj
  2. 3
      PadelClub/AppDelegate.swift
  3. 104
      PadelClub/Data/Federal/FederalTournament.swift
  4. 34
      PadelClub/Extensions/Tournament+Extensions.swift
  5. 3
      PadelClub/PadelClubApp.swift
  6. 69
      PadelClub/SyncedProducts.storekit
  7. 74
      PadelClub/Utils/Network/FederalDataService.swift
  8. 2
      PadelClub/Utils/Network/NetworkFederalService.swift
  9. 13
      PadelClub/ViewModel/FederalDataViewModel.swift
  10. 10
      PadelClub/ViewModel/SearchViewModel.swift
  11. 32
      PadelClub/Views/Calling/CallView.swift
  12. 23
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  13. 27
      PadelClub/Views/Calling/SendToAllView.swift
  14. 15
      PadelClub/Views/Club/Shared/ClubCourtSetupView.swift
  15. 14
      PadelClub/Views/Components/ButtonValidateView.swift
  16. 12
      PadelClub/Views/Components/Labels.swift
  17. 20
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  18. 70
      PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift
  19. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  20. 8
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  21. 2
      PadelClub/Views/GroupStage/GroupStagesView.swift
  22. 36
      PadelClub/Views/Match/MatchDetailView.swift
  23. 237
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  24. 11
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  25. 24
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  26. 131
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  27. 90
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  28. 36
      PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift
  29. 114
      PadelClub/Views/Navigation/MainView.swift
  30. 239
      PadelClub/Views/Navigation/OnboardingView.swift
  31. 1
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  32. 48
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  33. 21
      PadelClub/Views/Score/EditScoreView.swift
  34. 6
      PadelClub/Views/Score/FollowUpMatchView.swift
  35. 95
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  36. 2
      PadelClub/Views/Shared/TournamentFilterView.swift
  37. 75
      PadelClub/Views/Team/EditingTeamView.swift
  38. 2
      PadelClub/Views/Team/TeamRestingView.swift
  39. 10
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  40. 2
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  41. 2
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  42. 14
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  43. 8
      PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift
  44. 2
      PadelClub/Views/Tournament/Subscription/PurchaseListView.swift
  45. 11
      PadelClub/Views/Tournament/Subscription/SubscriptionView.swift
  46. 2
      PadelClub/Views/Tournament/TournamentRunningView.swift
  47. 26
      PadelClub/Views/Tournament/TournamentView.swift

@ -711,6 +711,12 @@
FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.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 */; }; FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
@ -1112,6 +1118,8 @@
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = "<group>"; }; FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = "<group>"; };
FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.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>"; };
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdaySelectionView.swift; 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>"; }; FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.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>"; };
@ -1569,6 +1577,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */, FF59FFB62B90EFBF0061EFF9 /* MainView.swift */,
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */,
FFD783FB2B91B919000F62A6 /* Agenda */, FFD783FB2B91B919000F62A6 /* Agenda */,
FF3F74FA2B91A04B004CFE0E /* Organizer */, FF3F74FA2B91A04B004CFE0E /* Organizer */,
FF3F74FB2B91A060004CFE0E /* Toolbox */, FF3F74FB2B91A060004CFE0E /* Toolbox */,
@ -1897,6 +1906,7 @@
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */, FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */,
FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */, FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */,
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */, FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */,
FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */,
FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */, FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */,
); );
path = Agenda; path = Agenda;
@ -2385,7 +2395,9 @@
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723A2DC28A92005CD239 /* ComposeViews.swift in Sources */, C497723A2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
@ -2650,7 +2662,9 @@
FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */, FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */,
FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C49772392DC28A92005CD239 /* ComposeViews.swift in Sources */, C49772392DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */, FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -2893,7 +2907,9 @@
FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */, FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */,
FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723B2DC28A92005CD239 /* ComposeViews.swift in Sources */, C497723B2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */, FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -3139,12 +3155,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.48; MARKETING_VERSION = 1.2.52;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3169,6 +3185,7 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
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";
@ -3185,12 +3202,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.48; MARKETING_VERSION = 1.2.52;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3288,6 +3305,7 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3304,7 +3322,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -3333,6 +3351,7 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3349,7 +3368,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -3393,7 +3412,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -3436,7 +3455,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

@ -67,9 +67,10 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel
StoreCenter.main.forceNoSynchronization = !synchronized StoreCenter.main.forceNoSynchronization = !synchronized
} }
func applicationWillEnterForeground(_ application: UIApplication) { func applicationWillEnterForeground(_ application: UIApplication) {
Task { Task {
try await Guard.main.refreshPurchasedAppleProducts() await Guard.main.refreshPurchases()
} }
} }

@ -10,7 +10,7 @@ import PadelClubData
// MARK: - FederalTournament // MARK: - FederalTournament
struct FederalTournament: Identifiable, Codable { struct FederalTournament: Identifiable, Codable, Hashable {
func getEvent() -> Event { func getEvent() -> Event {
let club = DataStore.shared.user.clubsObjects().first(where: { $0.code == codeClub }) let club = DataStore.shared.user.clubsObjects().first(where: { $0.code == codeClub })
@ -313,7 +313,7 @@ extension FederalTournament: FederalTournamentHolder {
} }
// MARK: - CategorieAge // MARK: - CategorieAge
struct CategorieAge: Codable { struct CategorieAge: Codable, Hashable {
var ageJoueurMin, ageMin, ageJoueurMax, ageRechercheMax: Int? var ageJoueurMin, ageMin, ageJoueurMax, ageRechercheMax: Int?
var categoriesAgeTypePratique: [CategoriesAgeTypePratique]? var categoriesAgeTypePratique: [CategoriesAgeTypePratique]?
var ageMax: Int? var ageMax: Int?
@ -335,18 +335,18 @@ struct CategorieAge: Codable {
} }
// MARK: - CategoriesAgeTypePratique // MARK: - CategoriesAgeTypePratique
struct CategoriesAgeTypePratique: Codable { struct CategoriesAgeTypePratique: Codable, Hashable {
var id: ID? var id: ID?
} }
// MARK: - ID // MARK: - ID
struct ID: Codable { struct ID: Codable, Hashable {
var typePratique: String? var typePratique: String?
var idCategorieAge: Int? var idCategorieAge: Int?
} }
// MARK: - CategorieTournoi // MARK: - CategorieTournoi
struct CategorieTournoi: Codable { struct CategorieTournoi: Codable, Hashable {
var code, codeTaxe: String? var code, codeTaxe: String?
var compteurGda: CompteurGda? var compteurGda: CompteurGda?
var libelle, niveauHierarchique: String? var libelle, niveauHierarchique: String?
@ -354,14 +354,14 @@ struct CategorieTournoi: Codable {
} }
// MARK: - CompteurGda // MARK: - CompteurGda
struct CompteurGda: Codable { struct CompteurGda: Codable, Hashable {
var classementMax: Classement? var classementMax: Classement?
var libelle: String? var libelle: String?
var classementMin: Classement? var classementMin: Classement?
} }
// MARK: - Classement // MARK: - Classement
struct Classement: Codable { struct Classement: Codable, Hashable {
var nature, libelle: String? var nature, libelle: String?
var serie: Serie? var serie: Serie?
var sexe: String? var sexe: String?
@ -371,7 +371,7 @@ struct Classement: Codable {
} }
// MARK: - Serie // MARK: - Serie
struct Serie: Codable { struct Serie: Codable, Hashable {
var code, libelle: String? var code, libelle: String?
var valide: Bool? var valide: Bool?
var sexe: String? var sexe: String?
@ -382,7 +382,7 @@ struct Serie: Codable {
} }
// MARK: - Epreuve // MARK: - Epreuve
struct Epreuve: Codable { struct Epreuve: Codable, Hashable {
var inscriptionEnLigneEnCours: Bool? var inscriptionEnLigneEnCours: Bool?
var categorieAge: CategorieAge? var categorieAge: CategorieAge?
var typeEpreuve: TypeEpreuve? var typeEpreuve: TypeEpreuve?
@ -419,7 +419,7 @@ struct Epreuve: Codable {
} }
// MARK: - TypeEpreuve // MARK: - TypeEpreuve
struct TypeEpreuve: Codable { struct TypeEpreuve: Codable, Hashable {
let code: String? let code: String?
let delai: Int? let delai: Int?
let libelle: String? let libelle: String?
@ -437,12 +437,12 @@ struct TypeEpreuve: Codable {
} }
// MARK: - BorneAnneesNaissance // MARK: - BorneAnneesNaissance
struct BorneAnneesNaissance: Codable { struct BorneAnneesNaissance: Codable, Hashable {
var min, max: Int? var min, max: Int?
} }
// MARK: - Installation // MARK: - Installation
struct Installation: Codable { struct Installation: Codable, Hashable {
var ville: String? var ville: String?
var lng: Double? var lng: Double?
var surfaces: [JSONAny]? var surfaces: [JSONAny]?
@ -457,7 +457,7 @@ struct Installation: Codable {
} }
// MARK: - JugeArbitre // MARK: - JugeArbitre
struct JugeArbitre: Codable { struct JugeArbitre: Codable, Hashable {
var idCRM, id: Int? var idCRM, id: Int?
var nom, prenom: String? var nom, prenom: String?
@ -468,7 +468,7 @@ struct JugeArbitre: Codable {
} }
// MARK: - ModeleDeBalle // MARK: - ModeleDeBalle
struct ModeleDeBalle: Codable { struct ModeleDeBalle: Codable, Hashable {
var libelle: String? var libelle: String?
var marqueDeBalle: MarqueDeBalle? var marqueDeBalle: MarqueDeBalle?
var id: Int? var id: Int?
@ -476,7 +476,7 @@ struct ModeleDeBalle: Codable {
} }
// MARK: - MarqueDeBalle // MARK: - MarqueDeBalle
struct MarqueDeBalle: Codable { struct MarqueDeBalle: Codable, Hashable {
var id: Int? var id: Int?
var valide: Bool? var valide: Bool?
var marque: String? var marque: String?
@ -529,9 +529,13 @@ class JSONCodingKey: CodingKey {
} }
} }
class JSONAny: Codable { class JSONAny: Codable, Hashable, Equatable {
let value: Any var value: Any
init() {
self.value = ()
}
static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny")
@ -722,4 +726,70 @@ class JSONAny: Codable {
try JSONAny.encode(to: &container, value: self.value) try JSONAny.encode(to: &container, value: self.value)
} }
} }
public static func == (lhs: JSONAny, rhs: JSONAny) -> Bool {
switch (lhs.value, rhs.value) {
case (let l as Bool, let r as Bool): return l == r
case (let l as Int64, let r as Int64): return l == r
case (let l as Double, let r as Double): return l == r
case (let l as String, let r as String): return l == r
case (let l as JSONNull, let r as JSONNull): return true
case (let l as [Any], let r as [Any]):
guard l.count == r.count else { return false }
return zip(l, r).allSatisfy { (a, b) in
// Recursively wrap in JSONAny for comparison
JSONAny(value: a) == JSONAny(value: b)
}
case (let l as [String: Any], let r as [String: Any]):
guard l.count == r.count else { return false }
for (key, lVal) in l {
guard let rVal = r[key], JSONAny(value: lVal) == JSONAny(value: rVal) else { return false }
}
return true
default:
return false
}
}
public func hash(into hasher: inout Hasher) {
switch value {
case let v as Bool:
hasher.combine(0)
hasher.combine(v)
case let v as Int64:
hasher.combine(1)
hasher.combine(v)
case let v as Double:
hasher.combine(2)
hasher.combine(v)
case let v as String:
hasher.combine(3)
hasher.combine(v)
case is JSONNull:
hasher.combine(4)
case let v as [Any]:
hasher.combine(5)
for elem in v {
JSONAny(value: elem).hash(into: &hasher)
}
case let v as [String: Any]:
hasher.combine(6)
// Order of hashing dictionary keys shouldn't matter
for key in v.keys.sorted() {
hasher.combine(key)
if let val = v[key] {
JSONAny(value: val).hash(into: &hasher)
}
}
default:
hasher.combine(-1)
}
}
// Helper init for internal use
convenience init(value: Any) {
self.init()
self.value = value
}
} }

@ -12,36 +12,6 @@ import LeStorage
extension Tournament { extension Tournament {
func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
matchFormat = roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee
registrationDateLimit = deadline(for: .inscription)
if enableOnlineRegistration, isAnimation() == false {
accountIsRequired = true
licenseIsRequired = true
}
}
func customizeUsingPreferences() {
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
tournament.tournamentLevel == self.tournamentLevel
&& tournament.tournamentCategory == self.tournamentCategory
&& tournament.federalTournamentAge == self.federalTournamentAge
&& tournament.hasEnded() == true
&& tournament.isCanceled == false
&& tournament.isDeleted == false
}).sorted(by: \.endDate!, order: .descending).first else {
return
}
self.dayDuration = lastTournamentWithSameBuild.dayDuration
self.teamCount = (lastTournamentWithSameBuild.teamCount / 2) * 2
self.enableOnlineRegistration = lastTournamentWithSameBuild.enableOnlineRegistration
}
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date(), name: name) let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date(), name: name)
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
@ -137,9 +107,9 @@ extension Tournament {
players.filter({ $0.hasHomonym() }) players.filter({ $0.hasHomonym() })
} }
func payIfNecessary() throws { func payIfNecessary() async throws {
if self.payment != nil { return } if self.payment != nil { return }
if let payment = Guard.main.paymentForNewTournament() { if let payment = await Guard.main.paymentForNewTournament() {
self.payment = payment self.payment = payment
DataStore.shared.tournaments.addOrUpdate(instance: self) DataStore.shared.tournaments.addOrUpdate(instance: self)
return return

@ -248,7 +248,8 @@ struct DownloadNewVersionView: View {
}.padding().background(.logoYellow) }.padding().background(.logoYellow)
.clipShape(.buttonBorder) .clipShape(.buttonBorder)
}.frame(maxWidth: .infinity) }
.frame(maxWidth: .infinity)
.foregroundStyle(.logoBackground) .foregroundStyle(.logoBackground)
.fontWeight(.medium) .fontWeight(.medium)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

@ -1,11 +1,36 @@
{ {
"appPolicies" : {
"eula" : "",
"policies" : [
{
"locale" : "en_US",
"policyText" : "",
"policyURL" : ""
}
]
},
"identifier" : "2055C391", "identifier" : "2055C391",
"nonRenewingSubscriptions" : [ "nonRenewingSubscriptions" : [
], ],
"products" : [ "products" : [
{ {
"displayPrice" : "14.0", "displayPrice" : "129.0",
"familyShareable" : false,
"internalID" : "6751947241",
"localizations" : [
{
"description" : "Achetez 10 tournois",
"displayName" : "Pack de 10 tournois",
"locale" : "fr"
}
],
"productID" : "app.padelclub.tournament.unit.10",
"referenceName" : "Pack de 10 tournois",
"type" : "Consumable"
},
{
"displayPrice" : "17.0",
"familyShareable" : false, "familyShareable" : false,
"internalID" : "6484163993", "internalID" : "6484163993",
"localizations" : [ "localizations" : [
@ -22,57 +47,53 @@
], ],
"settings" : { "settings" : {
"_applicationInternalID" : "6484163558", "_applicationInternalID" : "6484163558",
"_askToBuyEnabled" : false,
"_billingGracePeriodEnabled" : false,
"_billingIssuesEnabled" : false,
"_compatibilityTimeRate" : { "_compatibilityTimeRate" : {
"3" : 6 "3" : 6
}, },
"_developerTeamID" : "BQ3Y44M3Q6", "_developerTeamID" : "BQ3Y44M3Q6",
"_disableDialogs" : false,
"_failTransactionsEnabled" : false, "_failTransactionsEnabled" : false,
"_lastSynchronizedDate" : 735034894.72550702, "_lastSynchronizedDate" : 779705033.96878397,
"_locale" : "en_US", "_locale" : "fr",
"_storefront" : "USA", "_renewalBillingIssuesEnabled" : false,
"_storefront" : "FRA",
"_storeKitErrors" : [ "_storeKitErrors" : [
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "Load Products" "name" : "Load Products"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "Purchase" "name" : "Purchase"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "Verification" "name" : "Verification"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "App Store Sync" "name" : "App Store Sync"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "Subscription Status" "name" : "Subscription Status"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "App Transaction" "name" : "App Transaction"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "Manage Subscriptions Sheet" "name" : "Manage Subscriptions Sheet"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "Refund Request Sheet" "name" : "Refund Request Sheet"
}, },
{ {
"current" : null,
"enabled" : false, "enabled" : false,
"name" : "Offer Code Redeem Sheet" "name" : "Offer Code Redeem Sheet"
} }
@ -89,7 +110,15 @@
"subscriptions" : [ "subscriptions" : [
{ {
"adHocOffers" : [ "adHocOffers" : [
{
"displayPrice" : "45.0",
"internalID" : "1A02CDB5",
"numberOfPeriods" : 12,
"offerID" : "PRICE50",
"paymentMode" : "payAsYouGo",
"referenceName" : "ancien prix 50",
"subscriptionPeriod" : "P1M"
}
], ],
"codeOffers" : [ "codeOffers" : [
@ -110,7 +139,10 @@
"recurringSubscriptionPeriod" : "P1M", "recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Monthly Five", "referenceName" : "Monthly Five",
"subscriptionGroupID" : "21474782", "subscriptionGroupID" : "21474782",
"type" : "RecurringSubscription" "type" : "RecurringSubscription",
"winbackOffers" : [
]
}, },
{ {
"adHocOffers" : [ "adHocOffers" : [
@ -135,13 +167,16 @@
"recurringSubscriptionPeriod" : "P1M", "recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Monthly Unlimited", "referenceName" : "Monthly Unlimited",
"subscriptionGroupID" : "21474782", "subscriptionGroupID" : "21474782",
"type" : "RecurringSubscription" "type" : "RecurringSubscription",
"winbackOffers" : [
]
} }
] ]
} }
], ],
"version" : { "version" : {
"major" : 3, "major" : 4,
"minor" : 0 "minor" : 0
} }
} }

@ -240,7 +240,8 @@ class FederalDataService {
let queryString = urlComponents.query ?? "" let queryString = urlComponents.query ?? ""
// The servicePath now points to your backend's endpoint for all tournaments: 'fft/all-tournaments/' // The servicePath now points to your backend's endpoint for all tournaments: 'fft/all-tournaments/'
let urlRequest = try service._baseRequest(servicePath: "fft/all-tournaments?\(queryString)", method: .get, requiresToken: true) var urlRequest = try service._baseRequest(servicePath: "fft/all-tournaments?\(queryString)", method: .get, requiresToken: true)
urlRequest.timeoutInterval = 180
let (data, response) = try await URLSession.shared.data(for: urlRequest) let (data, response) = try await URLSession.shared.data(for: urlRequest)
@ -275,7 +276,8 @@ class FederalDataService {
// The servicePath now points to your backend's endpoint for umpire data: 'fft/umpire/{tournament_id}/' // The servicePath now points to your backend's endpoint for umpire data: 'fft/umpire/{tournament_id}/'
let servicePath = "fft/umpire/\(idTournament)/" let servicePath = "fft/umpire/\(idTournament)/"
let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false) var urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false)
urlRequest.timeoutInterval = 120.0
let (data, response) = try await URLSession.shared.data(for: urlRequest) let (data, response) = try await URLSession.shared.data(for: urlRequest)
@ -297,72 +299,4 @@ class FederalDataService {
throw NetworkManagerError.apiError("Failed to decode UmpireContactInfo: \(error.localizedDescription)") throw NetworkManagerError.apiError("Failed to decode UmpireContactInfo: \(error.localizedDescription)")
} }
} }
/// Fetches umpire contact data for multiple tournament IDs.
/// This function calls your backend endpoint that handles multiple tournament IDs via query parameters.
/// - Parameter tournamentIds: An array of tournament ID strings.
/// - Returns: A dictionary mapping tournament IDs to tuples `(name: String?, email: String?, phone: String?)` containing the umpire's contact info.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getUmpiresData(tournamentIds: [String]) async throws -> [String: (name: String?, email: String?, phone: String?)] {
let service = try StoreCenter.main.service()
// Validate input
guard !tournamentIds.isEmpty else {
throw NetworkManagerError.apiError("Tournament IDs array cannot be empty")
}
// Create the base service path
let basePath = "fft/umpires/"
// Build query parameters - join tournament IDs with commas
let tournamentIdsParam = tournamentIds.joined(separator: ",")
let queryItems = [URLQueryItem(name: "tournament_ids", value: tournamentIdsParam)]
// Create the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let servicePath = basePath + (urlComponents.url?.query.map { "?\($0)" } ?? "")
let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
// Check for HTTP errors
guard httpResponse.statusCode == 200 else {
if let errorData = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let message = errorData["message"] as? String {
throw NetworkManagerError.apiError("Server error: \(message)")
}
throw NetworkManagerError.apiError("HTTP error: \(httpResponse.statusCode)")
}
do {
let umpireResponse = try JSONDecoder().decode(UmpireDataResponse.self, from: data)
// Convert the results to the expected return format
var resultDict: [String: (name: String?, email: String?, phone: String?)] = [:]
for (tournamentId, umpireInfo) in umpireResponse.results {
resultDict[tournamentId] = (name: umpireInfo.name, email: umpireInfo.email, phone: umpireInfo.phone)
}
print("Umpire data fetched for \(resultDict.count) tournaments")
return resultDict
} catch {
print("Decoding error for UmpireDataResponse: \(error)")
throw NetworkManagerError.apiError("Failed to decode UmpireDataResponse: \(error.localizedDescription)")
}
}
} }

@ -93,7 +93,7 @@ class NetworkFederalService {
//"geocoding%5Bcountry%5D=fr&geocoding%5Bville%5D=13%20Avenue%20Emile%20Bodin%2013260%20Cassis&geocoding%5Brayon%5D=15&geocoding%5BuserPosition%5D%5Blat%5D=43.22278594081477&geocoding%5BuserPosition%5D%5Blng%5D=5.556953900769194&geocoding%5BuserPosition%5D%5BshowDistance%5D=true&nombreResultat=0&diplomeEtatOption=false&galaxieOption=false&fauteuilOption=false&tennisSanteOption=false" //"geocoding%5Bcountry%5D=fr&geocoding%5Bville%5D=13%20Avenue%20Emile%20Bodin%2013260%20Cassis&geocoding%5Brayon%5D=15&geocoding%5BuserPosition%5D%5Blat%5D=43.22278594081477&geocoding%5BuserPosition%5D%5Blng%5D=5.556953900769194&geocoding%5BuserPosition%5D%5BshowDistance%5D=true&nombreResultat=0&diplomeEtatOption=false&galaxieOption=false&fauteuilOption=false&tennisSanteOption=false"
let postData = parameters.data(using: .utf8) let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/clubs/ajax")!,timeoutInterval: Double.infinity) var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/clubs/ajax")!)
request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language") request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding") request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")

@ -23,6 +23,7 @@ class FederalDataViewModel {
var searchAttemptCount: Int = 0 var searchAttemptCount: Int = 0
var dayDuration: Int? var dayDuration: Int?
var dayPeriod: DayPeriod = .all var dayPeriod: DayPeriod = .all
var weekdays: Set<Int> = Set()
var lastError: NetworkManagerError? var lastError: NetworkManagerError?
func filterStatus() -> String { func filterStatus() -> String {
@ -36,6 +37,7 @@ class FederalDataViewModel {
} }
labels.append(contentsOf: clubNames.formatList()) labels.append(contentsOf: clubNames.formatList())
labels.append(contentsOf: weekdays.map { Date.weekdays[$0 - 1] }.formatList())
if dayPeriod != .all { if dayPeriod != .all {
labels.append(dayPeriod.localizedDayPeriodLabel()) labels.append(dayPeriod.localizedDayPeriodLabel())
} }
@ -68,11 +70,12 @@ class FederalDataViewModel {
selectedClubs.removeAll() selectedClubs.removeAll()
dayPeriod = .all dayPeriod = .all
dayDuration = nil dayDuration = nil
weekdays.removeAll()
id = UUID() id = UUID()
} }
func areFiltersEnabled() -> Bool { func areFiltersEnabled() -> Bool {
(levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false (weekdays.isEmpty && levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false
} }
var filteredFederalTournaments: [FederalTournamentHolder] { var filteredFederalTournaments: [FederalTournamentHolder] {
@ -96,6 +99,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&& &&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
}) })
} }
@ -106,6 +111,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&& &&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
}) })
.flatMap { $0.tournaments } .flatMap { $0.tournaments }
.filter { .filter {
@ -137,6 +144,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&& &&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
if let codeClub = tournament.club()?.code { if let codeClub = tournament.club()?.code {
return firstPart && (selectedClubs.isEmpty || selectedClubs.contains(codeClub)) return firstPart && (selectedClubs.isEmpty || selectedClubs.contains(codeClub))
@ -157,6 +166,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&& &&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
} }
func gatherTournaments(clubs: [Club], startDate: Date, endDate: Date? = nil) async throws { func gatherTournaments(clubs: [Club], startDate: Date, endDate: Date? = nil) async throws {

@ -75,6 +75,16 @@ class SearchViewModel: ObservableObject, Identifiable {
return message.joined(separator: "\n") return message.joined(separator: "\n")
} }
func sortTitle() -> String {
var base = [sortOption.localizedLabel()]
base.append((ascending ? "croissant" : "décroissant"))
if selectedAgeCategory != .unlisted {
base.append(selectedAgeCategory.localizedFederalAgeLabel())
}
return base.joined(separator: " ")
}
func codeClubs() -> [String] { func codeClubs() -> [String] {
let clubs: [Club] = DataStore.shared.user.clubsObjects() let clubs: [Club] = DataStore.shared.user.clubsObjects()
return clubs.compactMap { $0.code } return clubs.compactMap { $0.code }

@ -262,10 +262,8 @@ struct CallView: View {
NavigationStack { NavigationStack {
LoginView(reason: LoginReason.loginRequiredForFeature) { _ in LoginView(reason: LoginReason.loginRequiredForFeature) { _ in
self.showUserCreationView = false self.showUserCreationView = false
self._payTournamentAndExecute { self._summon(byMessage: self.summonParamByMessage,
self._summon(byMessage: self.summonParamByMessage, reSummon: self.summonParamByMessage)
reSummon: self.summonParamByMessage)
}
} }
} }
}) })
@ -353,12 +351,10 @@ struct CallView: View {
self.summonParamByMessage = byMessage self.summonParamByMessage = byMessage
self.summonParamReSummon = reSummon self.summonParamReSummon = reSummon
self._verifyUser { self._verifyUser {
self._payTournamentAndExecute { if byMessage {
if byMessage { self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) } else {
} else { self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
}
} }
} }
} }
@ -371,14 +367,14 @@ struct CallView: View {
} }
} }
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { // fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
do { // do {
try self.tournament.payIfNecessary() // try self.tournament.payIfNecessary()
handler() // handler()
} catch { // } catch {
self.showSubscriptionView = true // self.showSubscriptionView = true
} // }
} // }
fileprivate func _contactByMessage(reSummon: Bool, forcedEmptyMessage: Bool) { fileprivate func _contactByMessage(reSummon: Bool, forcedEmptyMessage: Bool) {
self.contactType = .message(date: callDate, self.contactType = .message(date: callDate,

@ -44,7 +44,6 @@ struct MenuWarningView: View {
} }
} label: { } label: {
Text("Prévenir") Text("Prévenir")
.underline()
} }
.sheet(isPresented: self.$showSubscriptionView, content: { .sheet(isPresented: self.$showSubscriptionView, content: {
NavigationStack { NavigationStack {
@ -147,9 +146,7 @@ struct MenuWarningView: View {
fileprivate func _tryToContact() { fileprivate func _tryToContact() {
self._verifyUser { self._verifyUser {
self._payTournamentAndExecute { self.contactType = self.savedContactType
self.contactType = self.savedContactType
}
} }
} }
@ -161,14 +158,16 @@ struct MenuWarningView: View {
} }
} }
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { // fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
do { // Task {
try tournament.payIfNecessary() // do {
handler() // try await tournament.payIfNecessary()
} catch { // handler()
self.showSubscriptionView = true // } catch {
} // self.showSubscriptionView = true
} // }
// }
// }
} }

@ -272,13 +272,10 @@ struct SendToAllView: View {
fileprivate func _contact() { fileprivate func _contact() {
self._verifyUser { self._verifyUser {
self._payTournamentAndExecute { if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil)
if contactMethod == 0 { } else {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil) contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil)
} else {
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil)
}
} }
} }
@ -292,14 +289,14 @@ struct SendToAllView: View {
} }
} }
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { // fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
do { // do {
try tournament.payIfNecessary() // try tournament.payIfNecessary()
handler() // handler()
} catch { // } catch {
self.showSubscriptionView = true // self.showSubscriptionView = true
} // }
} // }
private var _networkErrorMessage: String { private var _networkErrorMessage: String {
ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected) ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected)

@ -23,10 +23,17 @@ struct ClubCourtSetupView: View {
.disabled(displayContext == .lockedForEditing) .disabled(displayContext == .lockedForEditing)
.onChange(of: club.courtCount) { .onChange(of: club.courtCount) {
if displayContext != .addition { if displayContext != .addition {
do { dataStore.clubs.addOrUpdate(instance: club)
try dataStore.clubs.addOrUpdate(instance: club) dataStore.events.filter { event in
} catch { event.club?.id == club.id
Logger.error(error) }.forEach { event in
let tournaments = event.tournaments.filter({ tournament in
tournament.startDate > Date()
})
tournaments.forEach { tournament in
tournament.courtCount = club.courtCount
}
dataStore.tournaments.addOrUpdate(contentOfs: tournaments)
} }
} }
} }

@ -13,10 +13,16 @@ struct ButtonValidateView: View {
let action: () -> () let action: () -> ()
var body: some View { var body: some View {
Button(title, role: role) { if #available(iOS 26.0, *) {
action()
Button(title, systemImage: "checkmark", role: role) {
action()
}
.buttonStyle(.borderedProminent)
} else {
Button(title, role: role) {
action()
}
} }
.clipShape(Capsule())
.buttonStyle(.bordered)
} }
} }

@ -9,7 +9,11 @@ import SwiftUI
struct LabelOptions: View { struct LabelOptions: View {
var body: some View { var body: some View {
Label("Options", systemImage: "ellipsis.circle") if #available(iOS 26.0, *) {
Label("Options", systemImage: "ellipsis")
} else {
Label("Options", systemImage: "ellipsis.circle")
}
} }
} }
@ -39,6 +43,10 @@ struct ShareLabel: View {
struct LabelFilter: View { struct LabelFilter: View {
var body: some View { var body: some View {
Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") if #available(iOS 26.0, *) {
Label("Filtrer", systemImage: "line.3.horizontal.decrease")
} else {
Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle")
}
} }
} }

@ -126,17 +126,7 @@ struct GroupStageSettingsView: View {
Section { Section {
RowButtonView("Retirer tout le monde", role: .destructive) { RowButtonView("Retirer tout le monde", role: .destructive) {
let teams = groupStage.teams() groupStage.removeAllTeams()
teams.forEach { team in
team.groupStagePosition = nil
team.groupStage = nil
groupStage._matches().forEach({ $0.updateTeamScores() })
}
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
} }
} footer: { } footer: {
Text("Toutes les équipes seront retirées et les scores des matchs seront perdus.") Text("Toutes les équipes seront retirées et les scores des matchs seront perdus.")
@ -188,6 +178,14 @@ struct GroupStageSettingsView: View {
} footer: { } footer: {
Text("Mets à jour les équipes de la poule si jamais une erreur est persistante.") Text("Mets à jour les équipes de la poule si jamais une erreur est persistante.")
} }
if tournament.lastStep() == 0 {
RowButtonView("Effacer la poule", role: .destructive) {
tournament.deleteGroupStage(groupStage)
dismiss()
dataStore.tournaments.addOrUpdate(instance: self.tournament)
}
}
} }
.onChange(of: size) { .onChange(of: size) {
if size != groupStage.size { if size != groupStage.size {

@ -147,55 +147,45 @@ struct GroupStageTeamView: View {
Group { Group {
switch contactType { switch contactType {
case .message(_, let recipients, let body, _): case .message(_, let recipients, let body, _):
if Guard.main.paymentForNewTournament() != nil { MessageComposeView(recipients: recipients, body: body) { result in
MessageComposeView(recipients: recipients, body: body) { result in switch result {
switch result { case .cancelled:
case .cancelled: break
break case .failed:
case .failed: self.sentError = .messageFailed
self.sentError = .messageFailed case .sent:
case .sent: if networkMonitor.connected == false {
if networkMonitor.connected == false { self.contactType = nil
self.contactType = nil if team.getPhoneNumbers().isEmpty == false {
if team.getPhoneNumbers().isEmpty == false { self.sentError = .uncalledTeams([team])
self.sentError = .uncalledTeams([team]) } else {
} else { self.sentError = .messageNotSent
self.sentError = .messageNotSent
}
} }
@unknown default:
break
} }
@unknown default:
break
} }
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
} }
case .mail(_, let recipients, let bccRecipients, let body, let subject, _): case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
if Guard.main.paymentForNewTournament() != nil { MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in switch result {
switch result { case .cancelled, .saved:
case .cancelled, .saved: self.contactType = nil
self.contactType = nil case .failed:
case .failed: self.contactType = nil
self.sentError = .mailFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
self.sentError = .mailFailed if team.getMail().isEmpty == false {
case .sent: self.sentError = .uncalledTeams([team])
if networkMonitor.connected == false { } else {
self.contactType = nil self.sentError = .mailNotSent
if team.getMail().isEmpty == false {
self.sentError = .uncalledTeams([team])
} else {
self.sentError = .mailNotSent
}
} }
@unknown default:
break
} }
@unknown default:
break
} }
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
} }
} }
} }

@ -70,7 +70,7 @@ struct GroupStageView: View {
} }
Section { Section {
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches, runningMatches: runningMatches), hideWhenEmpty: true)
} }
Section { Section {

@ -99,6 +99,14 @@ struct GroupStagesSettingsView: View {
} }
if tournament.lastStep() == 0, step == 0 { if tournament.lastStep() == 0, step == 0 {
Section {
RowButtonView("Ajouter une poule", role: .destructive) {
self.tournament.addGroupStage()
dataStore.tournaments.addOrUpdate(instance: self.tournament)
}
}
Section { Section {
RowButtonView("Ajouter une phase de poule", role: .destructive) { RowButtonView("Ajouter une phase de poule", role: .destructive) {
tournament.addNewGroupStageStep() tournament.addNewGroupStageStep()

@ -234,7 +234,7 @@ struct GroupStagesView: View {
Section { Section {
MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), isExpanded: false) MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches, runningMatches: runningMatches), isExpanded: false)
} }
Section { Section {

@ -325,6 +325,17 @@ struct MatchDetailView: View {
Text(match.confirmed ? "Confirmé" : "Non confirmé") Text(match.confirmed ? "Confirmé" : "Non confirmé")
} }
if match.hasWalkoutTeam() == true {
Divider()
Button(role: .destructive) {
match.removeWalkOut()
save()
} label: {
Text("Annuler le forfait")
}
}
Divider() Divider()
if match.courtIndex != nil { if match.courtIndex != nil {
@ -615,9 +626,20 @@ struct MatchDetailView: View {
} }
self._verifyUser { self._verifyUser {
self._payTournamentAndExecute {
self.scoreType = .edition Task {
do {
try await self._payTournamentAndExecute()
self.scoreType = .edition
} catch {
self.showSubscriptionView = true
}
} }
// self._payTournamentAndExecute {
// self.scoreType = .edition
// }
} }
} }
@ -629,15 +651,9 @@ struct MatchDetailView: View {
} }
} }
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { fileprivate func _payTournamentAndExecute() async throws {
guard let tournament = match.currentTournament() else { fatalError("missing tournament") } guard let tournament = match.currentTournament() else { fatalError("missing tournament") }
try await tournament.payIfNecessary()
do {
try tournament.payIfNecessary()
handler()
} catch {
self.showSubscriptionView = true
}
} }
private func save() { private func save() {

@ -25,6 +25,7 @@ struct ActivityView: View {
@State private var quickAccessScreen: QuickAccessScreen? = nil @State private var quickAccessScreen: QuickAccessScreen? = nil
@State private var displaySearchView: Bool = false @State private var displaySearchView: Bool = false
@State private var pasteString: String? = nil @State private var pasteString: String? = nil
@State private var presentOnboarding: Bool = false
enum QuickAccessScreen : Identifiable, Hashable { enum QuickAccessScreen : Identifiable, Hashable {
case inscription case inscription
@ -77,15 +78,21 @@ struct ActivityView: View {
@ViewBuilder @ViewBuilder
private func _pasteView() -> some View { private func _pasteView() -> some View {
Button { if #available(iOS 26.0, *) {
quickAccessScreen = .inscription Button("Ajouter une équipe", systemImage: "person.badge.plus") {
} label: { quickAccessScreen = .inscription
Image(systemName: "person.crop.circle.badge.plus") }
.resizable() } else {
.scaledToFit() Button {
.frame(minHeight: 32) quickAccessScreen = .inscription
} label: {
Image(systemName: "person.crop.circle.badge.plus")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.accessibilityLabel("Ajouter une équipe")
} }
.accessibilityLabel("Ajouter une équipe")
// if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true { // if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true {
// PasteButton(payloadType: String.self) { strings in // PasteButton(payloadType: String.self) { strings in
@ -217,85 +224,124 @@ struct ActivityView: View {
.navigationDestination(for: Tournament.self) { tournament in .navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament) TournamentView(tournament: tournament)
} }
.navigationDestination(for: SubScreen.self) { build in
switch build {
case .subscription(let federalTournament, let build):
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user)
}
}
// .onDisappear(perform: { // .onDisappear(perform: {
// pasteButtonIsDisplayed = nil // pasteButtonIsDisplayed = nil
// print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed) // print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed)
// }) // })
.toolbar { .toolbar {
ToolbarItemGroup(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button { if #available(iOS 26.0, *) {
switch viewStyle { if viewStyle == .calendar {
case .list: Button("Vue calendrier", systemImage: "calendar") {
viewStyle = .calendar switch viewStyle {
case .calendar: case .list:
viewStyle = .list viewStyle = .calendar
case .calendar:
viewStyle = .list
}
}
.buttonStyle(.borderedProminent)
} else {
Button("Vue calendrier", systemImage: "calendar") {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
}
} }
} label: { } else {
Image(systemName: "calendar.circle") Button {
.resizable() switch viewStyle {
.scaledToFit() case .list:
.frame(minHeight: 32) viewStyle = .calendar
} case .calendar:
.symbolVariant(viewStyle == .calendar ? .fill : .none) viewStyle = .list
}
Button { } label: {
presentFilterView.toggle() Image(systemName: "calendar.circle")
} label: { .resizable()
Image(systemName: "line.3.horizontal.decrease.circle") .scaledToFit()
.resizable() .frame(minHeight: 32)
.scaledToFit() }
.frame(minHeight: 32) .symbolVariant(viewStyle == .calendar ? .fill : .none)
} }
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
_pasteView()
} }
ToolbarItem(placement: .topBarTrailing) { if #available(iOS 26.0, *) {
Button { ToolbarSpacer(placement: .topBarLeading)
newTournament = Tournament.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
} }
if tournaments.isEmpty == false, federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around { ToolbarItem(placement: .topBarLeading) {
ToolbarItemGroup(placement: .bottomBar) {
VStack(spacing: 0) { if #available(iOS 26.0, *) {
let searchStatus = _searchStatus() if federalDataViewModel.areFiltersEnabled() {
if searchStatus.isEmpty == false { Button("Filtre", systemImage: "line.3.horizontal.decrease") {
Text(_searchStatus()) presentFilterView.toggle()
.font(.footnote) }
.foregroundStyle(.secondary) .buttonStyle(.borderedProminent)
} else {
Button("Filtre", systemImage: "line.3.horizontal.decrease") {
presentFilterView.toggle()
} }
}
} else {
Button {
presentFilterView.toggle()
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
}
}
HStack { if #available(iOS 26.0, *) {
if navigation.agendaDestination == .around { ToolbarSpacer(placement: .topBarLeading)
FooterButtonView("modifier votre recherche") { }
displaySearchView = true
}
if federalDataViewModel.areFiltersEnabled() { ToolbarItem(placement: .topBarLeading) {
Text("ou") _pasteView()
} }
}
if federalDataViewModel.areFiltersEnabled() { ToolbarItem(placement: .topBarTrailing) {
FooterButtonView(_filterButtonTitle()) { if #available(iOS 26.0, *) {
presentFilterView = true Button("Ajouter", systemImage: "plus") {
} newTournament = Tournament.newEmptyInstance()
}
} else {
Button {
newTournament = Tournament.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
}
}
} if #unavailable(iOS 26.0) {
} if _shouldDisplaySearchStatus() {
.padding(.bottom, 8) ToolbarItemGroup(placement: .bottomBar) {
_searchBoxView()
} }
} }
} }
} }
.sheet(isPresented: $presentOnboarding, content: {
OnboardingView()
.environmentObject(dataStore)
})
.sheet(isPresented: $presentFilterView) { .sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel) TournamentFilterView(federalDataViewModel: federalDataViewModel)
.environment(navigation) .environment(navigation)
@ -397,6 +443,41 @@ struct ActivityView: View {
} }
} }
private func _shouldDisplaySearchStatus() -> Bool {
tournaments.isEmpty == false && (federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around)
}
private func _searchBoxView() -> some View {
VStack(spacing: 0) {
let searchStatus = _searchStatus()
if searchStatus.isEmpty == false {
Text(_searchStatus())
.font(.footnote)
.foregroundStyle(.secondary)
}
HStack {
if navigation.agendaDestination == .around {
FooterButtonView("modifier votre recherche") {
displaySearchView = true
}
if federalDataViewModel.areFiltersEnabled() {
Text("ou")
}
}
if federalDataViewModel.areFiltersEnabled() {
FooterButtonView(_filterButtonTitle()) {
presentFilterView = true
}
}
}
.padding(.bottom, 8)
}
}
private func _searchStatus() -> String { private func _searchStatus() -> String {
var searchStatus : [String] = [] var searchStatus : [String] = []
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
@ -474,6 +555,11 @@ struct ActivityView: View {
navigation.agendaDestination = .tenup navigation.agendaDestination = .tenup
} }
SupportButtonView(contentIsUnavailable: true) SupportButtonView(contentIsUnavailable: true)
FooterButtonView("Vous n'êtes pas un juge-arbitre ou un organisateur de tournoi ? En savoir plus") {
presentOnboarding = true
}
.tint(.logoBackground)
} }
} }
@ -485,6 +571,7 @@ struct ActivityView: View {
} }
} }
@ViewBuilder
private func _tenupEmptyView() -> some View { private func _tenupEmptyView() -> some View {
if dataStore.user.hasTenupClubs() == false { if dataStore.user.hasTenupClubs() == false {
ContentUnavailableView { ContentUnavailableView {
@ -496,6 +583,10 @@ struct ActivityView: View {
presentClubSearchView = true presentClubSearchView = true
} }
.padding() .padding()
FooterButtonView("Cette app est dédié aux juge-arbitres et organisateurs de tournoi. Vous êtes un joueur à la recherche d'un tournoi homologué ? Utilisez notre outil de recherche") {
navigation.agendaDestination = .around
}
.tint(.logoBackground)
} }
} else { } else {
ContentUnavailableView { ContentUnavailableView {
@ -518,13 +609,16 @@ struct ActivityView: View {
ContentUnavailableView { ContentUnavailableView {
Label("Recherche de tournoi", systemImage: "magnifyingglass") Label("Recherche de tournoi", systemImage: "magnifyingglass")
} description: { } description: {
Text("Chercher les tournois autour de vous pour mieux décider les tournois à proposer dans votre club. Padel Club vous facilite même l'inscription !") Text("Chercher les tournois homologués autour de vous. Padel Club vous facilite même l'inscription !")
} actions: { } actions: {
RowButtonView("Lancer la recherche") { RowButtonView("Chercher un tournoi") {
displaySearchView = true displaySearchView = true
} }
.padding() .padding()
} }
.onAppear {
displaySearchView = true
}
} else { } else {
if federalDataViewModel.lastError == nil { if federalDataViewModel.lastError == nil {
ContentUnavailableView { ContentUnavailableView {
@ -561,3 +655,8 @@ struct ActivityView: View {
//#Preview { //#Preview {
// ActivityView() // ActivityView()
//} //}
enum SubScreen: Hashable {
case subscription(FederalTournament, TournamentBuild)
}

@ -95,10 +95,15 @@ struct CalendarView: View {
if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) { if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) {
if navigation.agendaDestination == .around { if navigation.agendaDestination == .around {
NavigationLink(build.buildHolderTitle(.wide)) {
TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user) if #available(iOS 26.0, *) {
NavigationLink(build.buildHolderTitle(.wide), value: SubScreen.subscription(tournament, build as! TournamentBuild))
} else {
NavigationLink(build.buildHolderTitle(.wide)) {
TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user)
}
} }
} else { } else {
Button(build.buildHolderTitle(.wide)) { Button(build.buildHolderTitle(.wide)) {
_createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build) _createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build)
} }

@ -158,6 +158,29 @@ struct EventListView: View {
} }
Divider() Divider()
} }
Menu {
Picker("Choix du montant", selection: Binding<Double>(get: {
// If all tournaments share the same price, show it; otherwise default to 0
let prices = Set(pcTournaments.compactMap { $0.entryFee })
return prices.count == 1 ? prices.first ?? 0.0 : 0.0
}, set: { (newValue: Double) in
// Apply the chosen price to every tournament
pcTournaments.forEach { tournament in
tournament.entryFee = newValue
}
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
})) {
ForEach([Double](stride(from: 0.0, through: 50.0, by: 5.0)), id: \.self) { (price: Double) in
Text(price.formatted(.currency(code: Locale.current.currency?.identifier ?? "EUR"))).tag(price as Double)
}
}
} label: {
Text("Montant de l'inscription")
}
Divider()
Menu { Menu {
Button { Button {
pcTournaments.forEach { tournament in pcTournaments.forEach { tournament in
@ -536,3 +559,4 @@ struct EventListView: View {
//#Preview { //#Preview {
// EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true) // EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true)
//} //}

@ -30,6 +30,18 @@ struct TournamentLookUpView: View {
@State private var confirmSearch: Bool = false @State private var confirmSearch: Bool = false
@State private var locationRequested = false @State private var locationRequested = false
@State private var apiError: StoreError? @State private var apiError: StoreError?
@State private var quickOption: QuickDateOption? = nil
enum QuickDateOption: String, Identifiable, Hashable {
case thisMonth
case thisWeek
case nextWeek
case nextMonth
case twoWeeks
case nextThreeMonth
var id: String { self.rawValue }
}
var tournaments: [FederalTournament] { var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments federalDataViewModel.searchedFederalTournaments
@ -140,23 +152,28 @@ struct TournamentLookUpView: View {
} }
.toolbarTitleDisplayMode(.large) .toolbarTitleDisplayMode(.large)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", systemImage: "xmark", role: .cancel) {
dismiss()
}
}
ToolbarItem(placement: .bottomBar) { ToolbarItem(placement: .bottomBar) {
if revealSearchParameters { if revealSearchParameters {
FooterButtonView("Lancer la recherche") { Button("Lancer la recherche") {
if dataStore.appSettings.city.isEmpty { if dataStore.appSettings.city.isEmpty {
confirmSearch = true confirmSearch = true
} else { } else {
runSearch() runSearch()
} }
} }
.buttonStyle(.borderedProminent)
.disabled(searching) .disabled(searching)
} else if searching { } else if searching {
HStack(spacing: 20) { HStack(spacing: 20) {
Spacer() Spacer()
ProgressView() ProgressView()
if total > 0 { if total > 0 {
let percent = Double(tournaments.count) / Double(total) Text("\(total) tournois en cours de récupération")
Text(percent.formatted(.percent.precision(.significantDigits(1...3))) + " en récupération de Tenup")
.font(.caption) .font(.caption)
} }
Spacer() Spacer()
@ -193,11 +210,12 @@ struct TournamentLookUpView: View {
revealSearchParameters = true revealSearchParameters = true
federalDataViewModel.searchedFederalTournaments = [] federalDataViewModel.searchedFederalTournaments = []
federalDataViewModel.searchAttemptCount = 0 federalDataViewModel.searchAttemptCount = 0
federalDataViewModel.removeFilters()
} label: { } label: {
Text("Ré-initialiser la recherche") Text("Ré-initialiser la recherche")
} }
} label: { } label: {
Label("Options", systemImage: "ellipsis.circle") LabelOptions()
} }
} }
} }
@ -221,26 +239,46 @@ struct TournamentLookUpView: View {
private func _gatherNumbers() { private func _gatherNumbers() {
Task { Task {
print("Doing.....") print("Doing.....")
let tournamentsToFetch = tournaments.enumerated().filter { (idx, tournament) in
tournament.japPhoneNumber == nil || tournament.japPhoneNumber?.isEmpty == true
}
let idIndexPairs: [(Int, String)] = tournamentsToFetch.map { ($0.offset, $0.element.id) }
let tournamentIDs: [String] = idIndexPairs.map { $0.1 }
guard !tournamentIDs.isEmpty else {
print("All numbers already gathered.")
return
}
// Split into batches of 100
let batchSize = 100
let batches = idIndexPairs.chunked(into: batchSize)
await withTaskGroup(of: (Int, String?).self) { group in print("Processing \(idIndexPairs.count) tournaments in \(batches.count) batches of \(batchSize)")
for i in 0..<tournaments.count {
let tournamentID = tournaments[i].id
let index = i // Capture index for use in the child task
group.addTask { // Process each batch sequentially
print("Starting task for tournament \(index) / \(self.tournaments.count)") for (batchIndex, batch) in batches.enumerated() {
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournamentID).phone print("Starting batch \(batchIndex + 1) of \(batches.count) (\(batch.count) tournaments)")
return (index, phone) // Return the index along with the phone number
await withTaskGroup(of: (Int, String?).self) { group in
for (index, tournamentID) in batch {
group.addTask {
print("Starting task for tournament \(index) / \(self.tournaments.count)")
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournamentID).phone
return (index, phone) // Return the index along with the phone number
}
} }
}
// Process results as they complete // Process results as they complete
for await (index, phone) in group { for await (index, phone) in group {
var tournamentData = federalDataViewModel.searchedFederalTournaments[index] // Get a mutable copy var tournamentData = federalDataViewModel.searchedFederalTournaments[index] // Get a mutable copy
tournamentData.updateJapPhoneNumber(phone: phone) // Mutate the copy tournamentData.updateJapPhoneNumber(phone: phone) // Mutate the copy
federalDataViewModel.searchedFederalTournaments[index] = tournamentData // Assign back federalDataViewModel.searchedFederalTournaments[index] = tournamentData // Assign back
}
} }
print("Completed batch \(batchIndex + 1) of \(batches.count)")
} }
print(".....Done") print(".....Done")
} }
} }
@ -320,7 +358,7 @@ struct TournamentLookUpView: View {
print("count", count, total, tournaments.count, page) print("count", count, total, tournaments.count, page)
total = count total = count
if tournaments.count < count && page < total / 30 { if total - tournaments.count > count / 50 && page < total / 30 {
if total < 200 || requestedToGetAllPages { if total < 200 || requestedToGetAllPages {
page += 1 page += 1
await getNewPage() await getNewPage()
@ -340,8 +378,54 @@ struct TournamentLookUpView: View {
var searchParametersView: some View { var searchParametersView: some View {
@Bindable var appSettings = dataStore.appSettings @Bindable var appSettings = dataStore.appSettings
Section { Section {
DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date) Picker(selection: $quickOption) {
DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date) Text("Libre").tag(nil as QuickDateOption?)
Text("Cette semaine").tag(QuickDateOption.thisWeek as QuickDateOption?)
Text("2 prochaines semaines").tag(QuickDateOption.twoWeeks as QuickDateOption?)
Text("La semaine prochaine").tag(QuickDateOption.nextWeek as QuickDateOption?)
Text("Ce mois-ci").tag(QuickDateOption.thisMonth as QuickDateOption?)
Text("2 prochains mois").tag(QuickDateOption.nextMonth as QuickDateOption?)
Text("3 prochains mois").tag(QuickDateOption.nextThreeMonth as QuickDateOption?)
} label: {
Text("Choix de dates")
}
.pickerStyle(.menu)
.onChange(of: quickOption) { oldValue, newValue in
switch newValue {
case nil:
break
case .twoWeeks:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfWeek.addingTimeInterval(14 * 24 * 60 * 60)
case .nextWeek:
appSettings.startDate = Date().endOfWeek.nextDay.startOfDay
appSettings.endDate = Date().endOfWeek.addingTimeInterval(7 * 24 * 60 * 60)
case .thisMonth:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfMonth.endOfDay()
case .thisWeek:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfWeek
case .nextMonth:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth
case .nextThreeMonth:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth.nextDay.endOfMonth
}
}
DatePicker(selection: $appSettings.startDate, displayedComponents: .date) {
Text("Début")
.onTapGesture(count: 2) {
appSettings.startDate = appSettings.startDate.startOfCurrentMonth
}
}
DatePicker(selection: $appSettings.endDate, displayedComponents: .date) {
Text("Fin")
.onTapGesture(count: 2) {
appSettings.endDate = appSettings.endDate.nextDay.endOfMonth
}
}
Picker(selection: $appSettings.dayDuration) { Picker(selection: $appSettings.dayDuration) {
Text("Aucune").tag(nil as Int?) Text("Aucune").tag(nil as Int?)
Text(1.formatted()).tag(1 as Int?) Text(1.formatted()).tag(1 as Int?)
@ -351,6 +435,9 @@ struct TournamentLookUpView: View {
Text("Durée souhaitée (en jours)") Text("Durée souhaitée (en jours)")
} }
@Bindable var federalDataViewModel = federalDataViewModel
WeekdayselectionView(weekdays: $federalDataViewModel.weekdays)
Picker(selection: $appSettings.dayPeriod) { Picker(selection: $appSettings.dayPeriod) {
ForEach(DayPeriod.allCases) { ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel().capitalized).tag($0) Text($0.localizedDayPeriodLabel().capitalized).tag($0)
@ -392,12 +479,12 @@ struct TournamentLookUpView: View {
} }
.symbolVariant(.fill) .symbolVariant(.fill)
.foregroundColor (Color.white) .foregroundColor (Color.white)
.cornerRadius (20)
.font(.system(size: 12)) .font(.system(size: 12))
} }
} }
Picker(selection: $appSettings.distance) { Picker(selection: $appSettings.distance) {
Text(distanceLimit(distance:15).formatted()).tag(15.0)
Text(distanceLimit(distance:30).formatted()).tag(30.0) Text(distanceLimit(distance:30).formatted()).tag(30.0)
Text(distanceLimit(distance:50).formatted()).tag(50.0) Text(distanceLimit(distance:50).formatted()).tag(50.0)
Text(distanceLimit(distance:60).formatted()).tag(60.0) Text(distanceLimit(distance:60).formatted()).tag(60.0)

@ -22,6 +22,7 @@ struct TournamentSubscriptionView: View {
@State private var didSendMessage: Bool = false @State private var didSendMessage: Bool = false
@State private var didSaveInCalendar: Bool = false @State private var didSaveInCalendar: Bool = false
@State private var phoneNumber: String? = nil @State private var phoneNumber: String? = nil
@State private var errorWhenGatheringPhone: Bool = false
init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: CustomUser) { init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: CustomUser) {
self.federalTournament = federalTournament self.federalTournament = federalTournament
@ -111,9 +112,13 @@ struct TournamentSubscriptionView: View {
Text(federalTournament.phoneLabel()) Text(federalTournament.phoneLabel())
} }
if let phoneNumber { LabeledContent("Téléphone JAP") {
LabeledContent("Téléphone JAP") { if let phoneNumber {
Text(phoneNumber) Text(phoneNumber)
} else if errorWhenGatheringPhone == false {
ProgressView()
} else {
Image(systemName: "exclamationmark.triangle")
} }
} }
} header: { } header: {
@ -163,8 +168,15 @@ struct TournamentSubscriptionView: View {
CopyPasteButtonView(pasteValue: messageBody) CopyPasteButtonView(pasteValue: messageBody)
} }
} }
.ifAvailableiOS26 { view in
view.toolbar(.hidden, for: .tabBar)
}
.task { .task {
self.phoneNumber = try? await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id).phone do {
self.phoneNumber = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id).phone
} catch {
self.errorWhenGatheringPhone = true
}
} }
.toolbarBackground(.visible, for: .bottomBar) .toolbarBackground(.visible, for: .bottomBar)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
@ -176,51 +188,61 @@ struct TournamentSubscriptionView: View {
} }
} }
.toolbar(content: { .toolbar(content: {
ToolbarItem(placement: .status) { if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .bottomBar)
}
ToolbarItem(placement: .bottomBar) {
Menu { Menu {
if let courrielEngagement = federalTournament.courrielEngagement { Menu {
Section { if let courrielEngagement = federalTournament.courrielEngagement {
RowButtonView("S'inscrire par email", systemImage: "envelope") { Button("Email", systemImage: "envelope") {
contactType = .mail(date: nil, recipients: [courrielEngagement], bccRecipients: nil, body: messageBody, subject: messageSubject, tournamentBuild: build as? TournamentBuild) contactType = .mail(date: nil, recipients: [courrielEngagement], bccRecipients: nil, body: messageBody, subject: messageSubject, tournamentBuild: build as? TournamentBuild)
} }
} }
}
if let telephone = phoneNumber { if let telephone = phoneNumber {
if telephone.isMobileNumber() { if telephone.isMobileNumber() {
Section { Button("Message", systemImage: "message") {
RowButtonView("S'inscrire par message", systemImage: "message") {
contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild) contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild)
} }
} }
} let number = telephone.replacingOccurrences(of: " ", with: "")
let number = telephone.replacingOccurrences(of: " ", with: "") if let url = URL(string: "tel:\(number)") {
if let url = URL(string: "tel:\(number)") { Link(destination: url) {
Link(destination: url) { Label("Appeler le JAP", systemImage: "phone")
Label("Appeler le JAP", systemImage: "phone") }
} }
} }
} label: {
Label("Inscription", systemImage: "pencil.and.list.clipboard")
} }
if let installation = federalTournament.installation, let telephone = installation.telephone { Menu {
Section { if let installation = federalTournament.installation, let telephone = installation.telephone {
RowButtonView("Contacter le club", systemImage: "house.and.flag") { Button("Email", systemImage: "envelope") {
contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild) contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild)
} }
} let number = telephone.replacingOccurrences(of: " ", with: "")
let number = telephone.replacingOccurrences(of: " ", with: "") if let url = URL(string: "tel:\(number)") {
if let url = URL(string: "tel:\(number)") { Link(destination: url) {
Link(destination: url) { Label("Appeler", systemImage: "phone")
Label("Appeler le club", systemImage: "phone") }
} }
} }
} label: {
Label("Contacter le club", systemImage: "house.and.flag")
} }
} label: { } label: {
Text("Contact et inscription") Text("S'inscrire")
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
} }
.menuStyle(.button) .menuStyle(.button)
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.offset(y:-2) }
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .bottomBar)
} }
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
@ -361,3 +383,17 @@ struct TournamentSubscriptionView: View {
} }
} }
extension View {
/// Runs a transform only on iOS 26+, otherwise returns self
@ViewBuilder
func ifAvailableiOS26<Content: View>(
@ViewBuilder transform: (Self) -> Content
) -> some View {
if #available(iOS 26.0, *) {
transform(self)
} else {
self
}
}
}

@ -0,0 +1,36 @@
//
// WeekdayselectionView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/09/2025.
//
import SwiftUI
import PadelClubData
import LeStorage
struct WeekdayselectionView: View {
@Binding var weekdays: Set<Int>
var body: some View {
NavigationLink {
List((1...7), selection: $weekdays) { type in
Text(Date.weekdays[type - 1]).tag(type as Int)
}
.navigationTitle("Jour de la semaine")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Jour de la semaine")
Spacer()
if weekdays.isEmpty || weekdays.count == 7 {
Text("N'importe")
.foregroundStyle(.secondary)
} else {
Text(weekdays.sorted().map({ Date.weekdays[$0 - 1] }).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
}
}

@ -17,8 +17,15 @@ struct MainView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@Environment(ImportObserver.self) private var importObserver: ImportObserver @Environment(ImportObserver.self) private var importObserver: ImportObserver
@State private var federalDataViewModel: FederalDataViewModel = FederalDataViewModel.shared
@State private var mainViewId: UUID = UUID() @State private var mainViewId: UUID = UUID()
@State private var presentOnboarding: Bool = false
@State private var canPresentOnboarding: Bool = false
@State private var presentFilterView: Bool = false
@State private var displaySearchView: Bool = false
@AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false
var lastDataSource: String? { var lastDataSource: String? {
dataStore.appSettings.lastDataSource dataStore.appSettings.lastDataSource
@ -90,6 +97,34 @@ struct MainView: View {
// PadelClubView() // PadelClubView()
// .tabItem(for: .padelClub) // .tabItem(for: .padelClub)
} }
.applyTabViewBottomAccessory(content: {
if (navigation.selectedTab == .activity || navigation.selectedTab == nil) && _shouldDisplaySearchStatus() {
_searchBoxView()
}
})
.sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel)
.environment(navigation)
.tint(.master)
}
.sheet(isPresented: $displaySearchView) {
NavigationStack {
TournamentLookUpView()
.environment(federalDataViewModel)
.environment(navigation)
}
}
.onAppear {
if canPresentOnboarding || StoreCenter.main.userId != nil {
if didSeeOnboarding == false {
presentOnboarding = true
}
}
}
.sheet(isPresented: $presentOnboarding, content: {
OnboardingView()
.environmentObject(dataStore)
})
.id(mainViewId) .id(mainViewId)
.onChange(of: dataStore.user.id) { .onChange(of: dataStore.user.id) {
print("dataStore.user.id = ", dataStore.user.id) print("dataStore.user.id = ", dataStore.user.id)
@ -98,6 +133,8 @@ struct MainView: View {
navigation.path.removeLast(navigation.path.count) navigation.path.removeLast(navigation.path.count)
mainViewId = UUID() mainViewId = UUID()
} }
canPresentOnboarding = true
} }
.environmentObject(dataStore) .environmentObject(dataStore)
.task { .task {
@ -247,8 +284,85 @@ struct MainView: View {
} }
} }
} }
private func _searchStatus() -> String {
var searchStatus : [String] = []
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
let filteredSearchedFederalTournaments = federalDataViewModel.filteredSearchedFederalTournaments
let status : String = filteredSearchedFederalTournaments.count.formatted() + " tournoi" + filteredSearchedFederalTournaments.count.pluralSuffix
searchStatus.append(status)
}
if federalDataViewModel.areFiltersEnabled() {
searchStatus.append(federalDataViewModel.filterStatus())
}
return searchStatus.joined(separator: " ")
}
private func _shouldDisplaySearchStatus() -> Bool {
guard navigation.path.count == 0 else { return false }
return federalDataViewModel.areFiltersEnabled() || (navigation.agendaDestination == .around && federalDataViewModel.searchedFederalTournaments.isEmpty == false)
}
private func _searchBoxView() -> some View {
VStack(spacing: 0) {
let searchStatus = _searchStatus()
if searchStatus.isEmpty == false {
Text(_searchStatus())
.font(.footnote)
.foregroundStyle(.secondary)
}
HStack {
if navigation.agendaDestination == .around {
FooterButtonView("modifier votre recherche") {
displaySearchView = true
}
if federalDataViewModel.areFiltersEnabled() {
Text("ou")
}
}
if federalDataViewModel.areFiltersEnabled() {
FooterButtonView(_filterButtonTitle()) {
presentFilterView = true
}
}
}
}
}
private func _filterButtonTitle() -> String {
var prefix = "modifier "
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
prefix = ""
}
return prefix + "vos filtres"
}
} }
//#Preview { //#Preview {
// MainView() // MainView()
//} //}
fileprivate extension View {
@ViewBuilder
func applyTabViewBottomAccessory<Content: View>(
@ViewBuilder content: () -> Content
) -> some View {
if #available(iOS 26.0, *) {
self.tabViewBottomAccessory {
content()
}
} else {
self
}
}
}

@ -0,0 +1,239 @@
import SwiftUI
struct OnboardingView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State private var selection = 0
@Environment(\.openURL) var openURL
@Environment(\.dismiss) private var dismiss
@AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false
var steps: [OnboardingStep] {
[
// Écran 1 Bienvenue
.single(
title: "Bienvenue sur Padel Club",
description: "L’outil idéal des juges-arbitres et organisateurs pour gérer leurs tournois de A à Z.",
image: .padelClubLogoFondclairTransparent,
imageSystem: nil,
buttonTitle: "Suivant",
action: { selection += 1 }
),
// Écran 2 Juges arbitres
.single(
title: "Pour les Juges-Arbitres",
description: "Planification, convocations, tirages, résultats… Tout ce qu’il faut pour organiser un tournoi de padel.",
image: nil,
imageSystem: "calendar.badge.clock",
buttonTitle: "Suivant",
action: { selection += 1 }
),
// Écran 3 Joueurs (Multi boutons)
.multi(
title: "Vous êtes joueur ?",
description: "Cette app a été pensée faite pour les organisateurs.\nPour suivre vos tournois et convocations, rendez-vous sur https://padelclub.app",
image: nil,
imageSystem: "person.fill.questionmark",
tools: [
("Aller sur le site joueur", {
if let url = URL(string: "https://padelclub.app") {
openURL(url)
}
})
],
finalButtonTitle: "Continuer",
finalAction: {
selection += 1
}
),
// Écran 4 Outils utiles aux joueurs
.multi(
title: "Quelques outils utiles",
description: "Même si pensée pour les organisateurs, vous trouverez aussi quelques fonctions pratiques en tant que joueur.",
image: nil,
imageSystem: "wrench.and.screwdriver",
tools: [
("Chercher un tournoi Ten'Up", {
dismiss()
navigation.agendaDestination = .around
}),
("Accès au classement mensuel", {
dismiss()
navigation.selectedTab = .toolbox
}),
("Calculateur de points", {
dismiss()
navigation.selectedTab = .toolbox
}),
("Consulter les règles du jeu", {
dismiss()
navigation.selectedTab = .toolbox
}),
("Créer vos animations amicales", {
dismiss()
navigation.agendaDestination = .activity
})
],
finalButtonTitle: "J'ai compris",
finalAction: {
UserDefaults.standard.set(true, forKey: "didSeeOnboarding")
dismiss()
}
)
]
}
var body: some View {
NavigationStack {
TabView(selection: $selection) {
ForEach(Array(steps.enumerated()), id: \.offset) { index, step in
switch step {
case let .single(title, description, image, imageSystem, buttonTitle, action):
OnboardingPage(
title: title,
description: description,
image: image,
imageSystem: imageSystem,
buttonTitle: buttonTitle,
action: action
)
.tag(index)
case let .multi(title, description, image, imageSystem, tools, finalButtonTitle, finalAction):
OnboardingMultiButtonPage(
title: title,
description: description,
image: image,
imageSystem: imageSystem,
tools: tools,
finalButtonTitle: finalButtonTitle,
finalAction: finalAction
)
.tag(index)
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always)) // <- ensures background
.tint(.black) // <- sets the indicator color
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
didSeeOnboarding = true
dismiss()
} label: {
Text("Plus tard")
}
}
}
}
.tint(.master)
}
}
// MARK: - Enum de configuration
enum OnboardingStep {
case single(title: String, description: String, image: ImageResource?, imageSystem: String?, buttonTitle: String, action: () -> Void)
case multi(title: String, description: String, image: ImageResource?, imageSystem: String?, tools: [(String, () -> Void)], finalButtonTitle: String?, finalAction: () -> Void)
}
// MARK: - Vue de base commune
struct OnboardingBasePage<Content: View>: View {
var title: String
var description: String
var image: ImageResource?
var imageSystem: String?
@ViewBuilder var content: () -> Content
var body: some View {
VStack(spacing: 20) {
Spacer()
if let imageSystem {
Image(systemName: imageSystem)
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
} else if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
}
Text(title)
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
Text(description)
.font(.body)
.multilineTextAlignment(.center)
.padding(.horizontal, 30)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
Spacer()
content()
Spacer(minLength: 40)
}
}
}
// MARK: - Page avec un bouton
struct OnboardingPage: View {
var title: String
var description: String
var image: ImageResource?
var imageSystem: String?
var buttonTitle: String
var action: () -> Void
var body: some View {
OnboardingBasePage(title: title, description: description, image: image, imageSystem: imageSystem) {
RowButtonView(buttonTitle) {
action()
}
.padding()
}
}
}
// MARK: - Page avec plusieurs boutons
struct OnboardingMultiButtonPage: View {
var title: String
var description: String
var image: ImageResource?
var imageSystem: String?
var tools: [(String, () -> Void)]
var finalButtonTitle: String?
var finalAction: () -> Void
var body: some View {
OnboardingBasePage(title: title, description: description, image: image, imageSystem: imageSystem) {
VStack(spacing: 12) {
ForEach(Array(tools.enumerated()), id: \.offset) { _, tool in
FooterButtonView(tool.0) {
tool.1()
}
.tint(.master)
}
}
if let finalButtonTitle = finalButtonTitle {
RowButtonView(finalButtonTitle) {
finalAction()
}
.padding()
}
}
}
}
#Preview {
OnboardingView()
}

@ -65,6 +65,7 @@ struct ToolboxView: View {
Section { Section {
NavigationLink { NavigationLink {
SelectablePlayerListView(isPresented: false, lastDataSource: true) SelectablePlayerListView(isPresented: false, lastDataSource: true)
.toolbar(.hidden, for: .tabBar)
} label: { } label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
} }

@ -49,27 +49,25 @@ struct UmpireView: View {
PurchaseListView() PurchaseListView()
if Guard.main.currentPlan != .monthlyUnlimited { Section {
Section { Button {
Button { self.showSubscriptions = true
self.showSubscriptions = true } label: {
} label: { Label("Les offres", systemImage: "bookmark.fill")
Label("Les offres", systemImage: "bookmark.fill") }.simultaneousGesture(
}.simultaneousGesture( LongPressGesture()
LongPressGesture() .onEnded { _ in
.onEnded { _ in self.showProductIds = true
self.showProductIds = true }
} )
)
.highPriorityGesture( .highPriorityGesture(
TapGesture() TapGesture()
.onEnded { _ in .onEnded { _ in
self.showSubscriptions = true self.showSubscriptions = true
} }
) )
}
} }
if StoreCenter.main.isAuthenticated { if StoreCenter.main.isAuthenticated {
@ -317,18 +315,6 @@ struct UmpireView: View {
licenseMessage = nil licenseMessage = nil
} }
.navigationTitle("Juge-Arbitre") .navigationTitle("Juge-Arbitre")
.toolbar {
#if DEBUG
ToolbarItem(placement: .topBarTrailing) {
NetworkStatusView()
// if StoreCenter.main.collectionsCanSynchronize {
// Image(systemName: "checkmark.icloud")
// } else {
// Image(systemName: "icloud.slash")
// }
}
#endif
}
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .toolbar(content: {
if focusedField != nil { if focusedField != nil {

@ -139,13 +139,15 @@ struct EditScoreView: View {
Text(matchDescriptor.teamLabelTwo) Text(matchDescriptor.teamLabelTwo)
} }
Divider() if self.matchDescriptor.match?.hasWalkoutTeam() == true {
Divider()
Button { Button {
self.matchDescriptor.match?.removeWalkOut() self.matchDescriptor.match?.removeWalkOut()
save() save()
} label: { } label: {
Text("Annuler un forfait") Text("Annuler un forfait")
}
} }
} label: { } label: {
Text("Forfait d'une équipe ?") Text("Forfait d'une équipe ?")
@ -174,6 +176,13 @@ struct EditScoreView: View {
} }
if matchDescriptor.hasEnded { if matchDescriptor.hasEnded {
if self.matchDescriptor.match?.hasWalkoutTeam() == true {
RowButtonView("Annuler le forfait", role: .destructive) {
self.matchDescriptor.match?.removeWalkOut()
save()
}
}
Section { Section {
HStack { HStack {
Spacer() Spacer()

@ -88,7 +88,7 @@ struct FollowUpMatchView: View {
let allMatches = currentTournament?.allMatches() ?? [] let allMatches = currentTournament?.allMatches() ?? []
self.matchesLeft = Tournament.matchesLeft(allMatches) self.matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = Tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches) let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.isFree = currentTournament?.isFree() ?? true self.isFree = currentTournament?.isFree() ?? true
} }
@ -100,7 +100,7 @@ struct FollowUpMatchView: View {
self.autoDismiss = autoDismiss self.autoDismiss = autoDismiss
self.matchesLeft = Tournament.matchesLeft(allMatches) self.matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = Tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches) let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.isFree = false self.isFree = false
} }
@ -156,7 +156,7 @@ struct FollowUpMatchView: View {
case .index: case .index:
return matches return matches
case .restingTime: case .restingTime:
return matches.sorted(by: \.restingTimeForSorting) return readyMatches.sorted(by: \.restingTimeForSorting)
case .court: case .court:
return matchesLeft.filter({ $0.courtIndex == selectedCourt }) return matchesLeft.filter({ $0.courtIndex == selectedCourt })
case .winner: case .winner:

@ -96,16 +96,27 @@ struct SelectablePlayerListView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
if importObserver.isImportingFile() == false { if importObserver.isImportingFile() == false {
if searchViewModel.filterSelectionEnabled == false { VStack {
VStack { HStack {
HStack { Picker(selection: $searchViewModel.filterOption) {
Picker(selection: $searchViewModel.filterOption) { ForEach(PlayerFilterOption.allCases, id: \.self) { scope in
ForEach(PlayerFilterOption.allCases, id: \.self) { scope in Text(scope.icon().capitalized)
Text(scope.icon().capitalized) }
} } label: {
} label: { }
.pickerStyle(.segmented)
Picker(selection: $searchViewModel.dataSet) {
ForEach(DataSet.allCases) { dataSet in
Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet)
} }
.pickerStyle(.segmented) } label: {
}
}
if searchViewModel.isPresented == false {
HStack {
Menu { Menu {
if let lastDataSource = dataStore.appSettings.localizedLastDataSource() { if let lastDataSource = dataStore.appSettings.localizedLastDataSource() {
Section { Section {
@ -132,7 +143,7 @@ struct SelectablePlayerListView: View {
} }
Divider() Divider()
Section { Menu {
Picker(selection: $searchViewModel.selectedAgeCategory) { Picker(selection: $searchViewModel.selectedAgeCategory) {
ForEach(FederalTournamentAge.allCases) { ageCategory in ForEach(FederalTournamentAge.allCases) { ageCategory in
Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory) Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory)
@ -141,7 +152,7 @@ struct SelectablePlayerListView: View {
Text("Catégorie d'âge") Text("Catégorie d'âge")
} }
} header: { } label: {
Text("Catégorie d'âge") Text("Catégorie d'âge")
} }
Divider() Divider()
@ -165,23 +176,36 @@ struct SelectablePlayerListView: View {
Text("Assimilés") Text("Assimilés")
} }
} label: { } label: {
VStack(alignment: .trailing) { Text("tri par " + searchViewModel.sortTitle().lowercased())
Label(searchViewModel.sortOption.localizedLabel(), systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down") .underline()
if searchViewModel.selectedAgeCategory != .unlisted { .font(.caption)
Text(searchViewModel.selectedAgeCategory.localizedFederalAgeLabel()).font(.caption) // Label("Filtre", systemImage: "line.3.horizontal.decrease")
} // .labelsHidden()
}
if searchViewModel.selectedPlayers.count > 0 {
Divider()
Button {
searchViewModel.filterSelectionEnabled.toggle()
} label: {
Text("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la sélection")
.underline()
.font(.caption)
} }
} }
} }
.fixedSize()
} }
.padding(.bottom)
.padding(.horizontal)
.background(Material.thick)
Divider()
} }
.padding(.bottom)
.padding(.horizontal)
.background(Material.thick)
Divider()
MySearchView(searchViewModel: searchViewModel, contentUnavailableAction: contentUnavailableAction) MySearchView(searchViewModel: searchViewModel, contentUnavailableAction: contentUnavailableAction)
.environment(\.editMode, searchViewModel.allowMultipleSelection ? .constant(.active) : .constant(.inactive)) .environment(\.editMode, searchViewModel.allowMultipleSelection ? .constant(.active) : .constant(.inactive))
.searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, isPresented: $searchViewModel.isPresented, placement: .navigationBarDrawer(displayMode: .always), prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in .searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, isPresented: $searchViewModel.isPresented, placement: .toolbar, prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in
Text(token.shortLocalizedLabel) Text(token.shortLocalizedLabel)
}) })
.keyboardType(.alphabet) .keyboardType(.alphabet)
@ -212,11 +236,10 @@ struct SelectablePlayerListView: View {
} }
.scrollDismissesKeyboard(.immediately) .scrollDismissesKeyboard(.immediately)
.navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection)
.toolbarBackground(searchViewModel.allowMultipleSelection ? .visible : .hidden, for: .bottomBar) .toolbarBackground(.hidden, for: .bottomBar)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
// .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) // .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor)
.interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false)
.navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} else { } else {
List { List {
@ -284,7 +307,7 @@ struct SelectablePlayerListView: View {
searchViewModel.selectedPlayers.removeAll() searchViewModel.selectedPlayers.removeAll()
dismiss() dismiss()
} label: { } label: {
Text("Annuler") Label("Annuler", systemImage: "xmark")
} }
} }
@ -297,28 +320,16 @@ struct SelectablePlayerListView: View {
} }
.disabled(searchViewModel.selectedPlayers.isEmpty) .disabled(searchViewModel.selectedPlayers.isEmpty)
} }
ToolbarItem(placement: .status) { }
let count = searchViewModel.selectedPlayers.count
VStack(spacing: 0) { if #available(iOS 26.0, *) {
Text(count.formatted() + " joueur" + count.pluralSuffix + " séléctionné" + count.pluralSuffix).font(.footnote).foregroundStyle(.secondary) DefaultToolbarItem(kind: .search, placement: .bottomBar)
FooterButtonView("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la liste") {
searchViewModel.filterSelectionEnabled.toggle()
}
}
}
} }
} }
.navigationTitle("Recherche")
.navigationBarTitleDisplayMode(.large)
// .modifierWithCondition(searchViewModel.user != nil) { thisView in // .modifierWithCondition(searchViewModel.user != nil) { thisView in
// thisView // thisView
.toolbarTitleMenu {
Picker(selection: $searchViewModel.dataSet) {
ForEach(DataSet.allCases) { dataSet in
Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet)
}
} label: {
}
}
// } // }
// .bottomBarAlternative(hide: searchViewModel.selectedPlayers.isEmpty) { // .bottomBarAlternative(hide: searchViewModel.selectedPlayers.isEmpty) {
// ZStack { // ZStack {

@ -47,6 +47,8 @@ struct TournamentFilterView: View {
} label: { } label: {
Text("En semaine ou week-end") Text("En semaine ou week-end")
} }
WeekdayselectionView(weekdays: $federalDataViewModel.weekdays)
} }
Section { Section {

@ -209,11 +209,6 @@ struct EditingTeamView: View {
Text(registrationDateModified.localizedWeekDay().capitalized) Text(registrationDateModified.localizedWeekDay().capitalized)
} }
} }
#if DEBUG
.disabled(false)
#else
.disabled(team.hasPaidOnline() || team.hasRegisteredOnline())
#endif
Toggle(isOn: $wildCardBracket) { Toggle(isOn: $wildCardBracket) {
Text("Wildcard Tableau") Text("Wildcard Tableau")
@ -354,55 +349,45 @@ struct EditingTeamView: View {
Group { Group {
switch contactType { switch contactType {
case .message(_, let recipients, let body, _): case .message(_, let recipients, let body, _):
if Guard.main.paymentForNewTournament() != nil { MessageComposeView(recipients: recipients, body: body) { result in
MessageComposeView(recipients: recipients, body: body) { result in switch result {
switch result { case .cancelled:
case .cancelled: break
break case .failed:
case .failed: self.sentError = .messageFailed
self.sentError = .messageFailed case .sent:
case .sent: if networkMonitor.connected == false {
if networkMonitor.connected == false { self.contactType = nil
self.contactType = nil if team.getPhoneNumbers().isEmpty == false {
if team.getPhoneNumbers().isEmpty == false { self.sentError = .uncalledTeams([team])
self.sentError = .uncalledTeams([team]) } else {
} else { self.sentError = .messageNotSent
self.sentError = .messageNotSent
}
} }
@unknown default:
break
} }
@unknown default:
break
} }
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
} }
case .mail(_, let recipients, let bccRecipients, let body, let subject, _): case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
if Guard.main.paymentForNewTournament() != nil { MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in switch result {
switch result { case .cancelled, .saved:
case .cancelled, .saved: self.contactType = nil
case .failed:
self.contactType = nil
self.sentError = .mailFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
case .failed: if team.getMail().isEmpty == false {
self.contactType = nil self.sentError = .uncalledTeams([team])
self.sentError = .mailFailed } else {
case .sent: self.sentError = .mailNotSent
if networkMonitor.connected == false {
self.contactType = nil
if team.getMail().isEmpty == false {
self.sentError = .uncalledTeams([team])
} else {
self.sentError = .mailNotSent
}
} }
@unknown default:
break
} }
@unknown default:
break
} }
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
} }
} }
} }

@ -90,7 +90,7 @@ struct TeamRestingView: View {
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
let matchesLeft = Tournament.matchesLeft(allMatches) let matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = Tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches) let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.matchesLeft = matchesLeft self.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting) self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)

@ -336,7 +336,7 @@ struct InscriptionManagerView: View {
.tint(.master) .tint(.master)
} }
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Menu { Menu {
Toggle(isOn: $compactMode) { Toggle(isOn: $compactMode) {
Text("Vue compact") Text("Vue compact")
@ -364,6 +364,14 @@ struct InscriptionManagerView: View {
LabelFilter() LabelFilter()
.symbolVariant(filterMode == .all ? .none : .fill) .symbolVariant(filterMode == .all ? .none : .fill)
} }
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .navigationBarTrailing)
}
ToolbarItem(placement: .navigationBarTrailing) {
Menu { Menu {
if tournament.inscriptionClosed() == false { if tournament.inscriptionClosed() == false {
Menu { Menu {

@ -201,7 +201,7 @@ struct PrintSettingsView: View {
Text("Partager le code source HTML") Text("Partager le code source HTML")
} }
} label: { } label: {
Label("Options", systemImage: "ellipsis.circle") LabelOptions()
} }
} }
} }

@ -42,7 +42,7 @@ struct TournamentRankView: View {
Section { Section {
let all = tournament.allMatches() let all = tournament.allMatches()
let runningMatches = Tournament.runningMatches(all) let runningMatches = Tournament.runningMatches(all)
let matchesLeft = Tournament.readyMatches(all) let matchesLeft = Tournament.readyMatches(all, runningMatches: runningMatches)
MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false)
MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false)

@ -28,10 +28,16 @@ struct TournamentCellView: View {
if let federalTournament = tournament as? FederalTournament { if let federalTournament = tournament as? FederalTournament {
if FederalDataViewModel.shared.isFederalTournamentValidForFilters(federalTournament, build: build) { if FederalDataViewModel.shared.isFederalTournamentValidForFilters(federalTournament, build: build) {
if navigation.agendaDestination == .around { if navigation.agendaDestination == .around {
NavigationLink { if #available(iOS 26.0, *) {
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) NavigationLink(value: SubScreen.subscription(federalTournament, build as! TournamentBuild)) {
} label: { _buildView(build, existingTournament: event?.existingBuild(build))
_buildView(build, existingTournament: event?.existingBuild(build)) }
} else {
NavigationLink {
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user)
} label: {
_buildView(build, existingTournament: event?.existingBuild(build))
}
} }
} else { } else {
_buildView(build, existingTournament: event?.existingBuild(build)) _buildView(build, existingTournament: event?.existingBuild(build))

@ -63,12 +63,16 @@ struct PaymentStatusView: View {
EmptyView() EmptyView()
} }
}.onAppear { }.onAppear {
// self.payment = nil self._loadPayment()
self.payment = Guard.main.paymentForNewTournament()
} }
} }
fileprivate func _loadPayment() {
Task {
self.payment = await Guard.main.paymentForNewTournament()
}
}
} }
struct FreeTournamentTip: Tip { struct FreeTournamentTip: Tip {

@ -110,7 +110,7 @@ struct PurchaseView: View {
var body: some View { var body: some View {
HStack { HStack {
Image(systemName: self.purchaseRow.item.systemImage) Image(systemName: self.purchaseRow.item.summarySystemImage)
.foregroundColor(.accentColor).font(.title2) .foregroundColor(.accentColor).font(.title2)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(self.purchaseRow.name) Text(self.purchaseRow.name)

@ -238,14 +238,9 @@ struct SubscriptionView: View {
fileprivate func _restore() { fileprivate func _restore() {
Task { Task {
do { self.isRestoring = true
self.isRestoring = true await Guard.main.refreshPurchases()
try await Guard.main.refreshPurchasedAppleProducts() self.isRestoring = false
self.isRestoring = false
} catch {
self.isRestoring = false
Logger.error(error)
}
} }
} }

@ -22,7 +22,7 @@ struct TournamentRunningView: View {
let runningMatches = Tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)
let matchesLeft = Tournament.matchesLeft(allMatches) let matchesLeft = Tournament.matchesLeft(allMatches)
let readyMatches = Tournament.readyMatches(allMatches) let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
let availableToStart = Tournament.availableToStart(allMatches, in: runningMatches, checkCanPlay: true) let availableToStart = Tournament.availableToStart(allMatches, in: runningMatches, checkCanPlay: true)
Section { Section {

@ -218,10 +218,12 @@ struct TournamentView: View {
#if DEBUG #if DEBUG
Button { Button {
do { Task {
try self.tournament.payIfNecessary() do {
} catch { try await self.tournament.payIfNecessary()
Logger.error(error) } catch {
Logger.error(error)
}
} }
} label: { } label: {
Label("Payer le tournoi", systemImage: "dollarsign.circle.fill") Label("Payer le tournoi", systemImage: "dollarsign.circle.fill")
@ -238,10 +240,10 @@ struct TournamentView: View {
} }
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
Text("Réglages de l'événement") Label("Événement", systemImage: "wrench.and.screwdriver")
} }
NavigationLink(value: Screen.settings) { NavigationLink(value: Screen.settings) {
LabelSettings() Label("Tournoi", systemImage: "wrench.and.screwdriver")
} }
NavigationLink(value: Screen.call) { NavigationLink(value: Screen.call) {
@ -290,10 +292,10 @@ struct TournamentView: View {
} }
} }
NavigationLink(value: Screen.broadcast) { // NavigationLink(value: Screen.broadcast) {
Label("Publication", systemImage: "airplayvideo") // Label("Publication", systemImage: "airplayvideo")
} // }
//
NavigationLink(value: Screen.print) { NavigationLink(value: Screen.print) {
Label("Imprimer", systemImage: "printer") Label("Imprimer", systemImage: "printer")
} }
@ -309,6 +311,10 @@ struct TournamentView: View {
Text("Gestion du tournoi") Text("Gestion du tournoi")
Text("Annuler, supprimer ou terminer le tournoi") Text("Annuler, supprimer ou terminer le tournoi")
} }
Divider()
NavigationLink(value: Screen.stateSettings) {
Label("Tournoi", systemImage: "trash")
} }
} label: { } label: {
LabelOptions() LabelOptions()

Loading…
Cancel
Save