Laurent 3 weeks ago
commit 99cf9df1ef
  1. 155
      PadelClub.xcodeproj/project.pbxproj
  2. 22
      PadelClub/AppDelegate.swift
  3. 104
      PadelClub/Data/Federal/FederalTournament.swift
  4. 22
      PadelClub/Extensions/View+Extensions.swift
  5. 13
      PadelClub/PadelClubApp.swift
  6. 74
      PadelClub/Utils/Network/FederalDataService.swift
  7. 2
      PadelClub/Utils/Network/NetworkFederalService.swift
  8. 41
      PadelClub/Utils/Network/PaymentService.swift
  9. 15
      PadelClub/Utils/Tips.swift
  10. 6
      PadelClub/ViewModel/AgendaDestination.swift
  11. 13
      PadelClub/ViewModel/FederalDataViewModel.swift
  12. 2
      PadelClub/ViewModel/NavigationViewModel.swift
  13. 10
      PadelClub/ViewModel/SearchViewModel.swift
  14. 5
      PadelClub/ViewModel/TabDestination.swift
  15. 2
      PadelClub/Views/Calling/BracketCallingView.swift
  16. 11
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  17. 3
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  18. 4
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  19. 4
      PadelClub/Views/Calling/SendToAllView.swift
  20. 64
      PadelClub/Views/Cashier/CashierSettingsView.swift
  21. 2
      PadelClub/Views/Cashier/CashierView.swift
  22. 12
      PadelClub/Views/Cashier/Event/EventCreationView.swift
  23. 32
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  24. 31
      PadelClub/Views/Cashier/Event/EventTournamentsView.swift
  25. 91
      PadelClub/Views/Cashier/Event/TournamentPickerView.swift
  26. 11
      PadelClub/Views/Club/ClubDetailView.swift
  27. 77
      PadelClub/Views/Club/ClubsView.swift
  28. 25
      PadelClub/Views/Components/BarButtonView.swift
  29. 14
      PadelClub/Views/Components/ButtonValidateView.swift
  30. 1
      PadelClub/Views/Components/FortuneWheelView.swift
  31. 12
      PadelClub/Views/Components/Labels.swift
  32. 13
      PadelClub/Views/Components/StepperView.swift
  33. 20
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  34. 6
      PadelClub/Views/GroupStage/GroupStageView.swift
  35. 8
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  36. 7
      PadelClub/Views/GroupStage/GroupStagesView.swift
  37. 18
      PadelClub/Views/GroupStage/RankingGroupStageSetupView.swift
  38. 10
      PadelClub/Views/Match/Components/MatchDateView.swift
  39. 26
      PadelClub/Views/Match/MatchDetailView.swift
  40. 1
      PadelClub/Views/Match/MatchSetupView.swift
  41. 249
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  42. 11
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  43. 76
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  44. 137
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  45. 80
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  46. 36
      PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift
  47. 131
      PadelClub/Views/Navigation/MainView.swift
  48. 226
      PadelClub/Views/Navigation/MyAccount/MyAccountView.swift
  49. 239
      PadelClub/Views/Navigation/OnboardingView.swift
  50. 2
      PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift
  51. 25
      PadelClub/Views/Navigation/Toolbox/APICallsListView.swift
  52. 44
      PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift
  53. 188
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  54. 68
      PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift
  55. 85
      PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift
  56. 467
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  57. 10
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  58. 12
      PadelClub/Views/Planning/PlanningSettingsView.swift
  59. 55
      PadelClub/Views/Planning/PlanningView.swift
  60. 2
      PadelClub/Views/Planning/SchedulerView.swift
  61. 28
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  62. 11
      PadelClub/Views/Player/PlayerDetailView.swift
  63. 6
      PadelClub/Views/Round/DrawLogsView.swift
  64. 3
      PadelClub/Views/Round/LoserRoundView.swift
  65. 7
      PadelClub/Views/Round/LoserRoundsView.swift
  66. 9
      PadelClub/Views/Round/RoundSettingsView.swift
  67. 13
      PadelClub/Views/Round/RoundView.swift
  68. 6
      PadelClub/Views/Round/RoundsView.swift
  69. 30
      PadelClub/Views/Score/EditScoreView.swift
  70. 13
      PadelClub/Views/Score/FollowUpMatchView.swift
  71. 2
      PadelClub/Views/Shared/LearnMoreSheetView.swift
  72. 97
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  73. 72
      PadelClub/Views/Shared/SupportButtonView.swift
  74. 2
      PadelClub/Views/Shared/TournamentFilterView.swift
  75. 26
      PadelClub/Views/Team/EditingTeamView.swift
  76. 51
      PadelClub/Views/Team/PaymentRequestButton.swift
  77. 24
      PadelClub/Views/Team/PaymentService.swift
  78. 2
      PadelClub/Views/Team/TeamRestingView.swift
  79. 2
      PadelClub/Views/Tournament/ConsolationTournamentImportView.swift
  80. 4
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  81. 55
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  82. 236
      PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift
  83. 15
      PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift
  84. 162
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  85. 9
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  86. 51
      PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift
  87. 228
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  88. 7
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  89. 30
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  90. 427
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  91. 5
      PadelClub/Views/Tournament/Screen/TournamentCallView.swift
  92. 5
      PadelClub/Views/Tournament/Screen/TournamentCashierView.swift
  93. 7
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  94. 7
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift
  95. 6
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  96. 21
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  97. 9
      PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift
  98. 2
      PadelClub/Views/Tournament/TournamentRunningView.swift
  99. 114
      PadelClub/Views/Tournament/TournamentView.swift
  100. 1
      PadelClub/Views/User/AccountView.swift
  101. Some files were not shown because too many files have changed in this diff Show More

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD2F22C412681000DBD9A /* AppDelegate.swift */; };
C410F54E2DF340FE009713ED /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4D05D462DC10AE5009B053C /* WebKit.framework */; };
C411C9C32BEBA453003017AD /* ServerDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C22BEBA453003017AD /* ServerDataTests.swift */; };
C411C9C92BF219CB003017AD /* UserDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C82BF219CB003017AD /* UserDataTests.swift */; };
C411C9D02BF38F41003017AD /* TokenExemptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */; };
@ -154,6 +155,21 @@
FF2B51612C7E302C00FFF126 /* local.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = FF2B51602C7E302C00FFF126 /* local.sqlite */; };
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; };
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; };
FF30ACED2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; };
FF30ACEE2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; };
FF30ACEF2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; };
FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; };
FF39B6152DC8825E004E10CE /* PadelClubData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49770202DC25A23005CD239 /* PadelClubData.framework */; };
@ -615,7 +631,6 @@
FF70FBAF2C90584900129CC2 /* UpdateSourceRankDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */; };
FF70FBB02C90584900129CC2 /* GlobalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */; };
FF70FBB22C90584900129CC2 /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; };
FF70FBB42C90584900129CC2 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = FF70FABE2C90584900129CC2 /* Algorithms */; };
FF70FBB52C90584900129CC2 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF70FAC02C90584900129CC2 /* Zip */; };
FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; };
FF70FBB82C90584900129CC2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; };
@ -711,6 +726,21 @@
FFA252B62CDD2C6C0074E63F /* 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 */; };
FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; };
FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; };
FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; };
FFA97C9E2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; };
FFA97C9F2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; };
FFA97CA02E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; };
FFA97CA22E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; };
FFA97CA32E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; };
FFA97CA42E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.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 */; };
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
@ -734,6 +764,12 @@
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; };
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; };
FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; };
FFC2DB202E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; };
FFC2DB212E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; };
FFC2DB222E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; };
FFC2DB242E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; };
FFC2DB252E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; };
FFC2DB262E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; };
FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */; };
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; };
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; };
@ -1007,6 +1043,11 @@
FF2B51622C7F073100FFF126 /* Model_1_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_1_1.xcdatamodel; sourceTree = "<group>"; };
FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLinksView.swift; sourceTree = "<group>"; };
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = "<group>"; };
FF30ACEC2E8D700B008B6006 /* PaymentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentService.swift; sourceTree = "<group>"; };
FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRequestButton.swift; sourceTree = "<group>"; };
FF30AD2F2E92A994008B6006 /* MyAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountView.swift; sourceTree = "<group>"; };
FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireOptionsView.swift; sourceTree = "<group>"; };
FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireSettingsView.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
FF39B60F2DC87FEB004E10CE /* PadelClubData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PadelClubData.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1112,6 +1153,11 @@
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>"; };
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentPickerView.swift; sourceTree = "<group>"; };
FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingGroupStageSetupView.swift; 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>"; };
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>"; };
@ -1127,6 +1173,8 @@
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = "<group>"; };
FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = "<group>"; };
FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSelectorView.swift; sourceTree = "<group>"; };
FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadManagerView.swift; sourceTree = "<group>"; };
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundView.swift; sourceTree = "<group>"; };
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = "<group>"; };
FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = "<group>"; };
@ -1217,7 +1265,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FF70FBB42C90584900129CC2 /* Algorithms in Frameworks */,
C410F54E2DF340FE009713ED /* WebKit.framework in Frameworks */,
FF39B6172DC88267004E10CE /* PadelClubData.framework in Frameworks */,
FF70FBB52C90584900129CC2 /* Zip in Frameworks */,
FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */,
@ -1333,6 +1381,7 @@
C49771DE2DC25F04005CD239 /* Badge+Extensions.swift */,
C49771DF2DC25F04005CD239 /* SpinDrawable+Extensions.swift */,
C49772022DC260D3005CD239 /* Round+Extensions.swift */,
FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1565,11 +1614,21 @@
path = SeedData;
sourceTree = "<group>";
};
FF30AD2E2E92A936008B6006 /* MyAccount */ = {
isa = PBXGroup;
children = (
FF30AD2F2E92A994008B6006 /* MyAccountView.swift */,
);
path = MyAccount;
sourceTree = "<group>";
};
FF39719B2B8DE04B004C4E75 /* Navigation */ = {
isa = PBXGroup;
children = (
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */,
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */,
FFD783FB2B91B919000F62A6 /* Agenda */,
FF30AD2E2E92A936008B6006 /* MyAccount */,
FF3F74FA2B91A04B004CFE0E /* Organizer */,
FF3F74FB2B91A060004CFE0E /* Toolbox */,
FF3F74FC2B91A06B004CFE0E /* Umpire */,
@ -1653,6 +1712,8 @@
isa = PBXGroup;
children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */,
FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */,
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */,
@ -1744,6 +1805,7 @@
FF4AB6B42B9248200002987F /* NetworkManager.swift */,
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */,
FFE8B5BA2DA9896800BDE966 /* RefundService.swift */,
FF30ACEC2E8D700B008B6006 /* PaymentService.swift */,
FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */,
FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */,
FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */,
@ -1769,6 +1831,8 @@
FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */,
FFE8B5BE2DAA325400BDE966 /* RefundResultsView.swift */,
FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */,
FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */,
FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -1796,6 +1860,7 @@
FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */,
FF5DA18E2BB9268800A33061 /* GroupStagesSettingsView.swift */,
FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */,
FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */,
FF9AC3932BE3625D00C2E883 /* Components */,
FF9AC3922BE3625200C2E883 /* Shared */,
);
@ -1824,6 +1889,7 @@
FF1162862BD004AD000C4809 /* EditingTeamView.swift */,
FF17CA562CC02FEA003C7323 /* CoachListView.swift */,
FF7DCD382CC330260041110C /* TeamRestingView.swift */,
FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */,
);
path = Team;
@ -1857,6 +1923,7 @@
FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */,
FF8F263C2BAD627A00650388 /* TournamentConfiguratorView.swift */,
FF8E52332DF01D6100099B75 /* EventStatusView.swift */,
FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */,
);
name = Event;
path = Cashier/Event;
@ -1897,6 +1964,7 @@
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */,
FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */,
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */,
FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */,
FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */,
);
path = Agenda;
@ -2065,7 +2133,6 @@
);
name = "PadelClub TestFlight";
packageProductDependencies = (
FF70FABE2C90584900129CC2 /* Algorithms */,
FF70FAC02C90584900129CC2 /* Zip */,
);
productName = PadelClub;
@ -2236,11 +2303,13 @@
FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */,
FFE103122C366E5900684FC9 /* ImagePickerView.swift in Sources */,
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */,
FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */,
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */,
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */,
@ -2261,6 +2330,7 @@
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */,
FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FF30ACEF2E8D700B008B6006 /* PaymentService.swift in Sources */,
FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */,
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */,
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */,
@ -2295,6 +2365,7 @@
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */,
FFC91B032BD85E2400B29808 /* CourtView.swift in Sources */,
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */,
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */,
@ -2310,6 +2381,7 @@
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */,
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FFC2DB252E97DD0A00869317 /* HeadManagerView.swift in Sources */,
FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */,
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */,
@ -2336,6 +2408,7 @@
FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
FFA97CA02E8A7C080089EA22 /* View+Extensions.swift in Sources */,
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FFF03C942BD91D0C00B516FC /* ButtonValidateView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
@ -2371,6 +2444,7 @@
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFC2DB212E97D00300869317 /* TournamentSelectorView.swift in Sources */,
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
@ -2386,7 +2460,9 @@
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723A2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
@ -2401,6 +2477,7 @@
FF8E52342DF01D6100099B75 /* EventStatusView.swift in Sources */,
FFE8B63C2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */,
FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
@ -2422,11 +2499,13 @@
FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */,
FFC91AF92BD6A09100B29808 /* FortuneWheelView.swift in Sources */,
FFE8B5C82DAA390900BDE966 /* StripeValidationService.swift in Sources */,
FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */,
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */,
FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */,
FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */,
FFA97CA32E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */,
FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */,
FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */,
FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */,
@ -2501,11 +2580,13 @@
FF4CBF532C996C0600151637 /* ClubsView.swift in Sources */,
FF4CBF542C996C0600151637 /* ImagePickerView.swift in Sources */,
FF4CBF552C996C0600151637 /* MatchTypeSelectionView.swift in Sources */,
FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */,
FF4CBF562C996C0600151637 /* MatchSetupView.swift in Sources */,
FF4CBF572C996C0600151637 /* NetworkManager.swift in Sources */,
FF4CBF582C996C0600151637 /* EventLinksView.swift in Sources */,
FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */,
FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */,
FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */,
FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */,
@ -2526,6 +2607,7 @@
FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */,
FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */,
FF30ACEE2E8D700B008B6006 /* PaymentService.swift in Sources */,
FF4CBF722C996C0600151637 /* SwiftParser.swift in Sources */,
FF4CBF732C996C0600151637 /* ChangePasswordView.swift in Sources */,
FF4CBF742C996C0600151637 /* TournamentSubscriptionView.swift in Sources */,
@ -2560,6 +2642,7 @@
FF4CBF882C996C0600151637 /* TeamRowView.swift in Sources */,
FF4CBF8A2C996C0600151637 /* AppDelegate.swift in Sources */,
FF4CBF8C2C996C0600151637 /* EditScoreView.swift in Sources */,
FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */,
FF4CBF8D2C996C0600151637 /* TournamentOrganizerView.swift in Sources */,
FF4CBF8F2C996C0600151637 /* TournamentRunningView.swift in Sources */,
FF4CBF902C996C0600151637 /* TournamentDatePickerView.swift in Sources */,
@ -2575,6 +2658,7 @@
FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */,
FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF4CBFA12C996C0600151637 /* LoserRoundSettingsView.swift in Sources */,
FFC2DB262E97DD0A00869317 /* HeadManagerView.swift in Sources */,
FF4CBFA22C996C0600151637 /* Persistence.swift in Sources */,
FF4CBFA32C996C0600151637 /* CloseDatePicker.swift in Sources */,
FF4CBFA42C996C0600151637 /* BarButtonView.swift in Sources */,
@ -2601,6 +2685,7 @@
FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */,
FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */,
FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */,
FFA97C9E2E8A7C080089EA22 /* View+Extensions.swift in Sources */,
FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */,
FF4CBFC32C996C0600151637 /* GroupStageCallingView.swift in Sources */,
FF4CBFC52C996C0600151637 /* CashierSettingsView.swift in Sources */,
@ -2636,6 +2721,7 @@
FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */,
FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */,
FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FFC2DB202E97D00300869317 /* TournamentSelectorView.swift in Sources */,
FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */,
FFE8B63B2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */,
@ -2651,7 +2737,9 @@
FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */,
FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C49772392DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -2666,6 +2754,7 @@
FF8E52362DF01D6100099B75 /* EventStatusView.swift in Sources */,
FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */,
FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */,
FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */,
FFE8B5B52DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
@ -2687,11 +2776,13 @@
FF4CC0102C996C0600151637 /* LoadingViewModifier.swift in Sources */,
FF4CC0112C996C0600151637 /* PlayerView.swift in Sources */,
FF4CC0122C996C0600151637 /* MatchDetailView.swift in Sources */,
FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF4CC0142C996C0600151637 /* PlayerBlockView.swift in Sources */,
FF4CC0172C996C0600151637 /* PointView.swift in Sources */,
FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */,
FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */,
C49771E72DC25F04005CD239 /* Color+Extensions.swift in Sources */,
FFA97CA22E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */,
C49771E82DC25F04005CD239 /* Badge+Extensions.swift in Sources */,
C49771E92DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */,
FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */,
@ -2744,11 +2835,13 @@
FF70FAD22C90584900129CC2 /* ClubsView.swift in Sources */,
FF70FAD32C90584900129CC2 /* ImagePickerView.swift in Sources */,
FF70FAD42C90584900129CC2 /* MatchTypeSelectionView.swift in Sources */,
FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */,
FF70FAD52C90584900129CC2 /* MatchSetupView.swift in Sources */,
FF70FAD62C90584900129CC2 /* NetworkManager.swift in Sources */,
FF70FAD72C90584900129CC2 /* EventLinksView.swift in Sources */,
FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */,
FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */,
FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */,
FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */,
@ -2769,6 +2862,7 @@
FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */,
FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */,
FF30ACED2E8D700B008B6006 /* PaymentService.swift in Sources */,
FF70FAF12C90584900129CC2 /* SwiftParser.swift in Sources */,
FF70FAF22C90584900129CC2 /* ChangePasswordView.swift in Sources */,
FF70FAF32C90584900129CC2 /* TournamentSubscriptionView.swift in Sources */,
@ -2803,6 +2897,7 @@
FF70FB072C90584900129CC2 /* TeamRowView.swift in Sources */,
FF70FB092C90584900129CC2 /* AppDelegate.swift in Sources */,
FF70FB0B2C90584900129CC2 /* EditScoreView.swift in Sources */,
FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */,
FF70FB0C2C90584900129CC2 /* TournamentOrganizerView.swift in Sources */,
FF70FB0E2C90584900129CC2 /* TournamentRunningView.swift in Sources */,
FF70FB0F2C90584900129CC2 /* TournamentDatePickerView.swift in Sources */,
@ -2818,6 +2913,7 @@
FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */,
FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF70FB202C90584900129CC2 /* LoserRoundSettingsView.swift in Sources */,
FFC2DB242E97DD0A00869317 /* HeadManagerView.swift in Sources */,
FF70FB212C90584900129CC2 /* Persistence.swift in Sources */,
FF70FB222C90584900129CC2 /* CloseDatePicker.swift in Sources */,
FF70FB232C90584900129CC2 /* BarButtonView.swift in Sources */,
@ -2844,6 +2940,7 @@
FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */,
FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */,
FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */,
FFA97C9F2E8A7C080089EA22 /* View+Extensions.swift in Sources */,
FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */,
FF70FB422C90584900129CC2 /* GroupStageCallingView.swift in Sources */,
FF70FB442C90584900129CC2 /* CashierSettingsView.swift in Sources */,
@ -2879,6 +2976,7 @@
FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */,
FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */,
FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FFC2DB222E97D00300869317 /* TournamentSelectorView.swift in Sources */,
FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */,
FFE8B63A2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */,
@ -2894,7 +2992,9 @@
FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */,
FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723B2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -2909,6 +3009,7 @@
FF8E52352DF01D6100099B75 /* EventStatusView.swift in Sources */,
FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */,
FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */,
FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */,
FFE8B5B42DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
@ -2930,11 +3031,13 @@
FF70FB8F2C90584900129CC2 /* LoadingViewModifier.swift in Sources */,
FF70FB902C90584900129CC2 /* PlayerView.swift in Sources */,
FF70FB912C90584900129CC2 /* MatchDetailView.swift in Sources */,
FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF70FB932C90584900129CC2 /* PlayerBlockView.swift in Sources */,
FF70FB962C90584900129CC2 /* PointView.swift in Sources */,
FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */,
FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */,
C49771E42DC25F04005CD239 /* Color+Extensions.swift in Sources */,
FFA97CA42E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */,
C49771E52DC25F04005CD239 /* Badge+Extensions.swift in Sources */,
C49771E62DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */,
FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */,
@ -3118,7 +3221,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3140,12 +3243,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.51;
MARKETING_VERSION = 1.2.55;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3166,10 +3269,11 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3186,12 +3290,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.51;
MARKETING_VERSION = 1.2.55;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3289,6 +3393,7 @@
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3305,7 +3410,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -3334,6 +3439,7 @@
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3350,7 +3456,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -3376,7 +3482,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3394,18 +3500,19 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.37;
MARKETING_VERSION = 1.2.53;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -3419,7 +3526,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3436,12 +3543,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.37;
MARKETING_VERSION = 1.2.53;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3449,6 +3556,7 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = TESTFLIGHT;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -3530,14 +3638,6 @@
minimumVersion = 2.1.2;
};
};
FF70FABF2C90584900129CC2 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-algorithms.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/marmelroy/Zip";
@ -3567,11 +3667,6 @@
package = FF4CBF422C996C0600151637 /* XCRemoteSwiftPackageReference "Zip" */;
productName = Zip;
};
FF70FABE2C90584900129CC2 /* Algorithms */ = {
isa = XCSwiftPackageProductDependency;
package = FF70FABF2C90584900129CC2 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
productName = Algorithms;
};
FF70FAC02C90584900129CC2 /* Zip */ = {
isa = XCSwiftPackageProductDependency;
package = FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */;

@ -26,21 +26,33 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel
return true
}
fileprivate func _domain() -> String {
#if DEBUG
return "xlr.alwaysdata.net"
#elseif TESTFLIGHT
return "padelclub.app"
#elseif PRODTEST
return "padelclub.app"
#else
return "padelclub.app"
#endif
}
fileprivate func _configureLeStorage() {
StoreCenter.main.blackListUserName("apple-test")
StoreCenter.main.classProject = "PadelClubData"
// let secureScheme = true
let domain: String = URLs.activationHost.rawValue
let domain: String = self._domain()
#if DEBUG
if let secure = PListReader.readBool(plist: "local", key: "secure_server"),
let domain = PListReader.readString(plist: "local", key: "server_domain") {
StoreCenter.main.configureURLs(secureScheme: secure, domain: domain, webSockets: false)
StoreCenter.main.configureURLs(secureScheme: secure, domain: domain, webSockets: true, useSynchronization: true)
} else {
StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: false)
StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: true, useSynchronization: true)
}
#else
StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: false)
StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: true, useSynchronization: true)
#endif
StoreCenter.main.logsFailedAPICalls()

@ -10,7 +10,7 @@ import PadelClubData
// MARK: - FederalTournament
struct FederalTournament: Identifiable, Codable {
struct FederalTournament: Identifiable, Codable, Hashable {
func getEvent() -> Event {
let club = DataStore.shared.user.clubsObjects().first(where: { $0.code == codeClub })
@ -313,7 +313,7 @@ extension FederalTournament: FederalTournamentHolder {
}
// MARK: - CategorieAge
struct CategorieAge: Codable {
struct CategorieAge: Codable, Hashable {
var ageJoueurMin, ageMin, ageJoueurMax, ageRechercheMax: Int?
var categoriesAgeTypePratique: [CategoriesAgeTypePratique]?
var ageMax: Int?
@ -335,18 +335,18 @@ struct CategorieAge: Codable {
}
// MARK: - CategoriesAgeTypePratique
struct CategoriesAgeTypePratique: Codable {
struct CategoriesAgeTypePratique: Codable, Hashable {
var id: ID?
}
// MARK: - ID
struct ID: Codable {
struct ID: Codable, Hashable {
var typePratique: String?
var idCategorieAge: Int?
}
// MARK: - CategorieTournoi
struct CategorieTournoi: Codable {
struct CategorieTournoi: Codable, Hashable {
var code, codeTaxe: String?
var compteurGda: CompteurGda?
var libelle, niveauHierarchique: String?
@ -354,14 +354,14 @@ struct CategorieTournoi: Codable {
}
// MARK: - CompteurGda
struct CompteurGda: Codable {
struct CompteurGda: Codable, Hashable {
var classementMax: Classement?
var libelle: String?
var classementMin: Classement?
}
// MARK: - Classement
struct Classement: Codable {
struct Classement: Codable, Hashable {
var nature, libelle: String?
var serie: Serie?
var sexe: String?
@ -371,7 +371,7 @@ struct Classement: Codable {
}
// MARK: - Serie
struct Serie: Codable {
struct Serie: Codable, Hashable {
var code, libelle: String?
var valide: Bool?
var sexe: String?
@ -382,7 +382,7 @@ struct Serie: Codable {
}
// MARK: - Epreuve
struct Epreuve: Codable {
struct Epreuve: Codable, Hashable {
var inscriptionEnLigneEnCours: Bool?
var categorieAge: CategorieAge?
var typeEpreuve: TypeEpreuve?
@ -419,7 +419,7 @@ struct Epreuve: Codable {
}
// MARK: - TypeEpreuve
struct TypeEpreuve: Codable {
struct TypeEpreuve: Codable, Hashable {
let code: String?
let delai: Int?
let libelle: String?
@ -437,12 +437,12 @@ struct TypeEpreuve: Codable {
}
// MARK: - BorneAnneesNaissance
struct BorneAnneesNaissance: Codable {
struct BorneAnneesNaissance: Codable, Hashable {
var min, max: Int?
}
// MARK: - Installation
struct Installation: Codable {
struct Installation: Codable, Hashable {
var ville: String?
var lng: Double?
var surfaces: [JSONAny]?
@ -457,7 +457,7 @@ struct Installation: Codable {
}
// MARK: - JugeArbitre
struct JugeArbitre: Codable {
struct JugeArbitre: Codable, Hashable {
var idCRM, id: Int?
var nom, prenom: String?
@ -468,7 +468,7 @@ struct JugeArbitre: Codable {
}
// MARK: - ModeleDeBalle
struct ModeleDeBalle: Codable {
struct ModeleDeBalle: Codable, Hashable {
var libelle: String?
var marqueDeBalle: MarqueDeBalle?
var id: Int?
@ -476,7 +476,7 @@ struct ModeleDeBalle: Codable {
}
// MARK: - MarqueDeBalle
struct MarqueDeBalle: Codable {
struct MarqueDeBalle: Codable, Hashable {
var id: Int?
var valide: Bool?
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 {
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)
}
}
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
}
}

@ -0,0 +1,22 @@
//
// View+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/09/2025.
//
import SwiftUI
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
}
}
}

@ -193,11 +193,11 @@ struct PadelClubApp: App {
navigationViewModel.selectedTab = .umpire
}
if navigationViewModel.umpirePath.isEmpty {
navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login)
} else if navigationViewModel.umpirePath.last! != .login {
navigationViewModel.umpirePath.removeAll()
navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login)
if navigationViewModel.accountPath.isEmpty {
navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login)
} else if navigationViewModel.accountPath.last! != .login {
navigationViewModel.accountPath.removeAll()
navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login)
}
}
}.resume()
@ -248,7 +248,8 @@ struct DownloadNewVersionView: View {
}.padding().background(.logoYellow)
.clipShape(.buttonBorder)
}.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
.foregroundStyle(.logoBackground)
.fontWeight(.medium)
.multilineTextAlignment(.center)

@ -240,7 +240,8 @@ class FederalDataService {
let queryString = urlComponents.query ?? ""
// 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)
@ -275,7 +276,8 @@ class FederalDataService {
// The servicePath now points to your backend's endpoint for umpire data: 'fft/umpire/{tournament_id}/'
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)
@ -297,72 +299,4 @@ class FederalDataService {
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"
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("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")

@ -0,0 +1,41 @@
//
// PaymentService.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/10/2025.
//
import Foundation
import LeStorage
import PadelClubData
class PaymentService {
static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(
servicePath: "resend-payment-email/\(teamRegistrationId)/",
method: .post,
requiresToken: true
)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PaymentError.requestFailed
}
return try JSON.decoder.decode(SimpleResponse.self, from: data)
}
}
enum PaymentError: Error {
case requestFailed
case unauthorized
case unknown
}
struct SimpleResponse: Codable {
let success: Bool
let message: String
}

@ -660,6 +660,21 @@ struct UpdatePlannedDatesTip: Tip {
}
}
struct MergeTournamentTip: Tip {
var title: Text {
Text("Transfert de tournois")
}
var message: Text? {
Text("Vous pouvez transferer des tournois d'un autre événement dans celui-ci.")
}
var image: Image? {
Image(systemName: "square.and.arrow.down")
}
}
struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
var tint: Color?

@ -25,7 +25,7 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
var localizedTitleKey: String {
switch self {
case .activity:
return "En cours"
return "À venir"
case .history:
return "Terminé"
case .tenup:
@ -60,9 +60,9 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
func badgeValue() -> Int? {
switch self {
case .activity:
DataStore.shared.tournaments.filter { $0.endDate == nil && $0.isDeleted == false && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
DataStore.shared.tournaments.filter { $0.endDate == nil && $0.isDeleted == false && FederalDataViewModel.shared.isTournamentValidForFilters($0) && $0.sharing != .granted }.count
case .history:
DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) && $0.sharing != .granted }.count
case .tenup:
FederalDataViewModel.shared.filteredFederalTournaments.map { $0.tournaments.count }.reduce(0,+)
case .around:

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

@ -12,7 +12,7 @@ import PadelClubData
class NavigationViewModel {
var path = NavigationPath()
var toolboxPath = NavigationPath()
var umpirePath: [UmpireView.UmpireScreen] = []
var accountPath: [MyAccountView.AccountScreen] = []
var ongoingPath = NavigationPath()
var selectedTab: TabDestination?
var agendaDestination: AgendaDestination? = .activity

@ -74,6 +74,16 @@ class SearchViewModel: ObservableObject, Identifiable {
}
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] {
let clubs: [Club] = DataStore.shared.user.clubsObjects()

@ -17,6 +17,7 @@ enum TabDestination: CaseIterable, Identifiable {
case tournamentOrganizer
case umpire
case ongoing
case myAccount
var title: String {
switch self {
@ -30,6 +31,8 @@ enum TabDestination: CaseIterable, Identifiable {
return "Gestionnaire"
case .umpire:
return "Juge-Arbitre"
case .myAccount:
return "Compte"
}
}
@ -45,6 +48,8 @@ enum TabDestination: CaseIterable, Identifiable {
return "squares.below.rectangle"
case .umpire:
return "person.bust"
case .myAccount:
return "person.crop.circle"
}
}
}

@ -106,7 +106,7 @@ struct BracketCallingView: View {
ForEach(filteredRounds()) { round in
let seeds = seeds(forRoundIndex: round.index)
let startDate = round.startDate ?? round.playedMatches().first?.startDate
let startDate = ([round.startDate] + round.playedMatches().map { $0.startDate }).compacted().min()
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) == false })
if seeds.isEmpty == false {
Section {

@ -31,7 +31,7 @@ struct CallMessageCustomizationView: View {
self.tournament = tournament
_customCallMessageBody = State(wrappedValue: DataStore.shared.user.summonsMessageBody ?? (DataStore.shared.user.summonsUseFullCustomMessage ? "" : ContactType.defaultCustomMessage))
_customCallMessageSignature = State(wrappedValue: DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament))
_customClubName = State(wrappedValue: tournament.clubName ?? "Lieu du tournoi")
_customClubName = State(wrappedValue: tournament.customClubName ?? tournament.clubName ?? "Lieu du tournoi")
_summonsAvailablePaymentMethods = State(wrappedValue: DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods)
}
@ -126,7 +126,7 @@ struct CallMessageCustomizationView: View {
} label: {
Text("Valider")
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
}
}
}
@ -235,14 +235,13 @@ struct CallMessageCustomizationView: View {
if let eventClub = tournament.eventObject()?.clubObject() {
let hasBeenCreated: Bool = eventClub.hasBeenCreated(by: StoreCenter.main.userId)
Section {
TextField("Nom du club", text: $customClubName, axis: .vertical)
.lineLimit(2)
TextField("Nom du club", text: $customClubName)
.autocorrectionDisabled()
.focused($focusedField, equals: .clubName)
.onSubmit {
eventClub.name = customClubName
tournament.customClubName = customClubName.prefixTrimmed(100)
do {
try dataStore.clubs.addOrUpdate(instance: eventClub)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}

@ -44,8 +44,9 @@ struct MenuWarningView: View {
}
} label: {
Text("Prévenir")
.underline()
}
.menuStyle(.button)
.buttonStyle(.borderedProminent)
.sheet(isPresented: self.$showSubscriptionView, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)

@ -14,7 +14,7 @@ struct PlayersWithoutContactView: View {
var body: some View {
Section {
let withoutEmails = players.filter({ $0.email?.isEmpty == true || $0.email == nil })
let withoutEmails = players.filter({ $0.hasMail() == false })
DisclosureGroup {
ForEach(withoutEmails) { player in
NavigationLink {
@ -32,7 +32,7 @@ struct PlayersWithoutContactView: View {
}
}
let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false })
let withoutPhones = players.filter({ $0.hasMobilePhone() == false })
DisclosureGroup {
ForEach(withoutPhones) { player in
NavigationLink {

@ -273,9 +273,9 @@ struct SendToAllView: View {
self._verifyUser {
if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil)
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.phoneNumber, $0.contactPhoneNumber] }.compactMap({ $0 }), body: finalMessage(), 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)
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.email, $0.contactEmail] }.compactMap({ $0 }), body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil)
}
}

@ -127,49 +127,47 @@ struct CashierSettingsView: View {
}
}
ToolbarItem(placement: .keyboard) {
HStack {
if focusedField == ._entryFee {
if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()))) {
entryFee = priceTag
tournament.entryFee = priceTag
focusedField = nil
}
.buttonStyle(.bordered)
}
} else {
Button("Gratuit") {
entryFee = nil
tournament.entryFee = nil
ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._entryFee {
if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()))) {
entryFee = priceTag
tournament.entryFee = priceTag
focusedField = nil
}
.buttonStyle(.bordered)
}
} else if focusedField == ._clubMemberFeeDeduction {
ForEach(deductionTags, id: \.self) { deductionTag in
Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) {
clubMemberFeeDeduction = deductionTag
tournament.clubMemberFeeDeduction = deductionTag
focusedField = nil
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
}
} else {
Button("Gratuit") {
clubMemberFeeDeduction = entryFee
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
entryFee = nil
tournament.entryFee = nil
focusedField = nil
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
}
Spacer()
Button("Valider") {
} else if focusedField == ._clubMemberFeeDeduction {
ForEach(deductionTags, id: \.self) { deductionTag in
Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) {
clubMemberFeeDeduction = deductionTag
tournament.clubMemberFeeDeduction = deductionTag
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
Button("Gratuit") {
clubMemberFeeDeduction = entryFee
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
focusedField = nil
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
}
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
}
}

@ -18,7 +18,7 @@ struct ShareableObject {
func sharedData() async -> Data? {
let _players = players.filter({ cashierViewModel._shouldDisplayPlayer($0) })
.map {
[$0.pasteData()]
[$0.pasteData(type: .payment)]
.compacted()
.joined(separator: "\n")
}

@ -73,14 +73,12 @@ struct EventCreationView: View {
.focused($textFieldIsFocus)
.toolbar {
if textFieldIsFocus {
ToolbarItem(placement: .keyboard) {
HStack {
Spacer()
Button("Valider") {
textFieldIsFocus = false
}
.buttonStyle(.bordered)
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Valider") {
textFieldIsFocus = false
}
.buttonStyle(.borderedProminent)
}
}
}

@ -187,26 +187,24 @@ struct EventSettingsView: View {
}
if focusedField != nil {
ToolbarItem(placement: .keyboard) {
HStack {
if focusedField == ._name, eventName.isEmpty == false {
Button("Effacer") {
event.name = nil
eventName = ""
}
.buttonStyle(.borderless)
} else if focusedField == ._information, tournamentInformation.isEmpty == false {
Button("Effacer") {
tournamentInformation = ""
}
.buttonStyle(.borderless)
ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._name, eventName.isEmpty == false {
Button("Effacer") {
event.name = nil
eventName = ""
}
Spacer()
Button("Valider") {
focusedField = nil
.buttonStyle(.borderedProminent)
} else if focusedField == ._information, tournamentInformation.isEmpty == false {
Button("Effacer") {
tournamentInformation = ""
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
}
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
}
}

@ -15,7 +15,8 @@ struct EventTournamentsView: View {
let event: Event
@State private var newTournament: Tournament?
@State private var mainTournament: Tournament?
@State private var showTournamentPicker: Bool = false
var presentTournamentCreationView: Binding<Bool> { Binding(
get: { newTournament != nil },
set: { isPresented in
@ -122,13 +123,35 @@ struct EventTournamentsView: View {
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter un tournoi", icon: "plus.circle.fill") {
let tournament = Tournament.newEmptyInstance()
newTournament = tournament
BarButtonView("Importer un tournoi", icon: "square.and.arrow.down") {
showTournamentPicker = true
}
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarTrailing)
}
ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 26.0, *) {
BarButtonView("Ajouter une indisponibilité", icon: "plus") {
let tournament = Tournament.newEmptyInstance()
newTournament = tournament
}
} else {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
let tournament = Tournament.newEmptyInstance()
newTournament = tournament
}
}
}
}
.headerProminence(.increased)
.sheet(isPresented: $showTournamentPicker, content: {
NavigationStack {
TournamentPickerView(event: event)
.environmentObject(dataStore)
}
})
.sheet(isPresented: presentTournamentCreationView) {
if let newTournament {
NavigationStack {

@ -0,0 +1,91 @@
//
// TournamentPickerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/09/2025.
//
import SwiftUI
import LeStorage
import PadelClubData
import TipKit
struct TournamentPickerView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
let mergeTournamentTip = MergeTournamentTip()
let event: Event
@State private var selectedTournamentIds: Set<String> = Set()
@State private var shouldEraseEmptyEvents: Bool = false
var tournaments: [Tournament] {
dataStore.tournaments.filter({ $0.isDeleted == false && $0.event?.id != event.id }).sorted(by: \.startDate, order: .descending)
}
var body: some View {
List(selection: $selectedTournamentIds) {
Section {
TipView(mergeTournamentTip)
.tipStyle(tint: .green)
}
Section {
Toggle(isOn: $shouldEraseEmptyEvents) {
Text("Effacer les événements vides")
Text("Les événements qui n'ont plus de tournois seront effacés automatiquement.")
}
}
ForEach(tournaments) { tournament in
TournamentCellView(tournament: tournament).tag(tournament.id)
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler") {
dismiss()
}
}
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView {
_transferTournaments()
dismiss()
}
.disabled(selectedTournamentIds.isEmpty)
}
}
.navigationTitle("Transfert de tournois")
}
private func _transferTournaments() {
let tournaments = tournaments
var eventIdsToCheck = Set<String>()
var tournamentsToSave = [Tournament]()
selectedTournamentIds.forEach { id in
if let tournament = tournaments.first(where: { $0.id == id }) {
if let eventId = tournament.event{
eventIdsToCheck.insert(eventId)
}
tournament.event = event.id
tournamentsToSave.append(tournament)
}
}
dataStore.tournaments.addOrUpdate(contentOfs: tournamentsToSave)
if shouldEraseEmptyEvents {
var eventsToDelete = [Event]()
eventIdsToCheck.forEach { eventId in
if let eventToCheck = dataStore.events.first(where: { $0.id == eventId }) {
if eventToCheck.tournaments.isEmpty && shouldEraseEmptyEvents {
eventsToDelete.append(eventToCheck)
}
}
}
dataStore.events.delete(contentOfs: eventsToDelete)
}
}
}

@ -223,13 +223,12 @@ struct ClubDetailView: View {
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .keyboard) {
HStack {
Button("Fermer", role: .cancel) {
focusedField = nil
}
Spacer()
ToolbarItemGroup(placement: .keyboard) {
Button("Fermer", role: .cancel) {
focusedField = nil
}
.buttonStyle(.borderedProminent)
Spacer()
}
}
})

@ -28,19 +28,19 @@ struct ClubsView: View {
var body: some View {
List {
#if DEBUG
Section {
RowButtonView("Delete unexisted clubs", action: {
let ids = dataStore.user.clubs
ids.forEach { clubId in
if dataStore.clubs.findById(clubId) == nil {
dataStore.user.clubs.removeAll(where: { $0 == clubId })
}
}
dataStore.saveUser()
})
}
#endif
// #if DEBUG
// Section {
// RowButtonView("Delete unexisted clubs", action: {
// let ids = dataStore.user.clubs
// ids.forEach { clubId in
// if dataStore.clubs.findById(clubId) == nil {
// dataStore.user.clubs.removeAll(where: { $0 == clubId })
// }
// }
// dataStore.saveUser()
// })
// }
// #endif
let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: false)
let onlyCreatedClubs : [Club] = dataStore.user.createdClubsObjectsNotFavorite()
@ -106,7 +106,6 @@ struct ClubsView: View {
}
}
.navigationTitle(selection == nil ? "Clubs favoris" : "Choisir un club")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.sheet(isPresented: presentClubCreationView) {
if let newClub {
@ -129,23 +128,41 @@ struct ClubsView: View {
.tint(.master)
}
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
Button {
presentClubSearchView = true
} label: {
Image(systemName: "magnifyingglass.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 26.0, *) {
Button("Chercher", systemImage: "magnifyingglass") {
presentClubSearchView = true
}
} else {
Button {
presentClubSearchView = true
} label: {
Image(systemName: "magnifyingglass.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
}
Button {
newClub = Club.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarTrailing)
}
ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 26.0, *) {
Button("Ajouter", systemImage: "plus") {
newClub = Club.newEmptyInstance()
}
} else {
Button {
newClub = Club.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
}
}
}

@ -22,23 +22,14 @@ struct BarButtonView: View {
Button(action: {
action()
}) {
Image(systemName: icon)
.resizable()
.scaledToFit()
.frame(minHeight: 28)
/*
Label {
Text(accessibilityLabel)
} icon: {
Image(systemName: icon)
.resizable()
.scaledToFit()
.frame(minHeight: 36)
}
.labelStyle(.iconOnly)
//todo: resizing not working when label used
*/
if #available(iOS 26.0, *) {
Label(accessibilityLabel, systemImage: icon)
} else {
Image(systemName: icon)
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
}
}
}

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

@ -220,6 +220,7 @@ struct FortuneWheelContainerView: View {
.frame(width: 20, height: 20)
.rotationEffect(.degrees(180))
}
.frame(maxWidth: 600, maxHeight: 600)
.onAppear {
if autoMode {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

@ -9,7 +9,11 @@ import SwiftUI
struct LabelOptions: 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 {
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")
}
}
}

@ -67,15 +67,14 @@ struct StepperView: View {
}
.multilineTextAlignment(.trailing)
.toolbar {
ToolbarItem(placement: .keyboard) {
ToolbarItemGroup(placement: .keyboard) {
if amountIsFocused {
HStack {
Spacer()
Button("Confirmer") {
amountIsFocused = false
_validate()
}
Spacer()
Button("Confirmer") {
amountIsFocused = false
_validate()
}
.buttonStyle(.borderedProminent)
}
}
}

@ -126,17 +126,7 @@ struct GroupStageSettingsView: View {
Section {
RowButtonView("Retirer tout le monde", role: .destructive) {
let teams = groupStage.teams()
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)
}
groupStage.removeAllTeams()
}
} footer: {
Text("Toutes les équipes seront retirées et les scores des matchs seront perdus.")
@ -188,6 +178,14 @@ struct GroupStageSettingsView: View {
} footer: {
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) {
if size != groupStage.size {

@ -70,7 +70,7 @@ struct GroupStageView: View {
}
Section {
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true)
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches, runningMatches: runningMatches), hideWhenEmpty: true)
}
Section {
@ -96,6 +96,9 @@ struct GroupStageView: View {
}
}
.onAppear(perform: {
groupStage.clearScoreCache()
})
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
_groupStageMenuView()
@ -242,7 +245,6 @@ struct GroupStageView: View {
Text("#\(index + 1)")
.font(.caption)
TeamPickerView(groupStagePosition: index, pickTypeContext: .groupStage, teamPicked: { team in
print(team.pasteData())
team.groupStage = groupStage.id
team.groupStagePosition = index
groupStage._matches().forEach({ $0.updateTeamScores() })

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

@ -234,7 +234,7 @@ struct GroupStagesView: View {
Section {
MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), isExpanded: false)
MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches, runningMatches: runningMatches), isExpanded: false)
}
Section {
@ -256,5 +256,10 @@ struct GroupStagesView: View {
.environment(tournament)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
}

@ -0,0 +1,18 @@
//
// RankingGroupStageSetupView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 30/09/2025.
//
import SwiftUI
struct RankingGroupStageSetupView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
#Preview {
RankingGroupStageSetupView()
}

@ -42,6 +42,16 @@ struct MatchDateView: View {
let estimatedDuration = match.getDuration()
if isReady {
Section {
Button("Démarrer il y a 5 minutes") {
if let updatedField {
match.setCourt(updatedField)
}
match.updateStartDate(Calendar.current.date(byAdding: .minute, value: -5, to: currentDate), keepPlannedStartDate: true)
match.endDate = nil
match.confirmed = true
_save()
}
Button("Démarrer maintenant") {
if let updatedField {
match.setCourt(updatedField)

@ -309,6 +309,13 @@ struct MatchDetailView: View {
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
NavigationLink {
ShareModelView(instance: self.match)
} label: {
Label("Partager", systemImage: "square.and.arrow.up")
}
Toggle(isOn: .init(get: {
return match.confirmed
}, set: { value in
@ -317,7 +324,18 @@ struct MatchDetailView: View {
})) {
Text(match.confirmed ? "Confirmé" : "Non confirmé")
}
if match.hasWalkoutTeam() == true {
Divider()
Button(role: .destructive) {
match.removeWalkOut()
save()
} label: {
Text("Annuler le forfait")
}
}
Divider()
if match.courtIndex != nil {
@ -409,7 +427,13 @@ struct MatchDetailView: View {
.navigationTitle(match.matchTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
if let tournament = match.currentTournament() {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
}
var quickLookHeader: some View {

@ -65,7 +65,6 @@ struct MatchSetupView: View {
HStack {
let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, pickTypeContext: matchTypeContext == .bracket ? .bracket : .loserBracket, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData())
if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket {
match.setLuckyLoser(team: team, teamPosition: teamPosition)
do {

@ -25,7 +25,8 @@ struct ActivityView: View {
@State private var quickAccessScreen: QuickAccessScreen? = nil
@State private var displaySearchView: Bool = false
@State private var pasteString: String? = nil
@State private var presentOnboarding: Bool = false
enum QuickAccessScreen : Identifiable, Hashable {
case inscription
@ -38,17 +39,17 @@ struct ActivityView: View {
}
var runningTournaments: [FederalTournamentHolder] {
return dataStore.tournaments.filter({ $0.endDate == nil })
return dataStore.tournaments.filter({ $0.endDate == nil && $0.sharing != .granted })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) })
}
func getRunningTournaments() -> [Tournament] {
return dataStore.tournaments.filter({ $0.endDate == nil })
return dataStore.tournaments.filter({ $0.endDate == nil && $0.sharing != .granted })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) })
}
var endedTournaments: [Tournament] {
return dataStore.tournaments.filter({ $0.endDate != nil })
return dataStore.tournaments.filter({ $0.endDate != nil && $0.sharing != .granted })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) })
}
//
@ -77,15 +78,21 @@ struct ActivityView: View {
@ViewBuilder
private func _pasteView() -> some View {
Button {
quickAccessScreen = .inscription
} label: {
Image(systemName: "person.crop.circle.badge.plus")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
if #available(iOS 26.0, *) {
Button("Ajouter une équipe", systemImage: "person.badge.plus") {
quickAccessScreen = .inscription
}
} else {
Button {
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 {
// PasteButton(payloadType: String.self) { strings in
@ -217,85 +224,124 @@ struct ActivityView: View {
.navigationDestination(for: Tournament.self) { tournament in
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: {
// pasteButtonIsDisplayed = nil
// print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed)
// })
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
ToolbarItem(placement: .topBarLeading) {
if #available(iOS 26.0, *) {
if viewStyle == .calendar {
Button("Vue calendrier", systemImage: "calendar") {
switch viewStyle {
case .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
}
}
}
} else {
Button {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
} label: {
Image(systemName: "calendar.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
} label: {
Image(systemName: "calendar.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
.symbolVariant(viewStyle == .calendar ? .fill : .none)
}
.symbolVariant(viewStyle == .calendar ? .fill : .none)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarLeading)
}
ToolbarItem(placement: .topBarLeading) {
Button {
presentFilterView.toggle()
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
if #available(iOS 26.0, *) {
if federalDataViewModel.areFiltersEnabled() {
Button("Filtre", systemImage: "line.3.horizontal.decrease") {
presentFilterView.toggle()
}
.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)
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarLeading)
}
ToolbarItem(placement: .topBarLeading) {
_pasteView()
}
ToolbarItem(placement: .topBarTrailing) {
Button {
newTournament = Tournament.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
if #available(iOS 26.0, *) {
Button("Ajouter", systemImage: "plus") {
newTournament = Tournament.newEmptyInstance()
}
} else {
Button {
newTournament = Tournament.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
}
}
if tournaments.isEmpty == false, federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around {
ToolbarItemGroup(placement: .bottomBar) {
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)
if #unavailable(iOS 26.0) {
if _shouldDisplaySearchStatus() {
ToolbarItemGroup(placement: .bottomBar) {
_searchBoxView()
}
}
}
}
.sheet(isPresented: $presentOnboarding, content: {
OnboardingView()
.environmentObject(dataStore)
})
.sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel)
.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 {
var searchStatus : [String] = []
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
@ -465,7 +546,7 @@ struct ActivityView: View {
.frame(width: 100)
}
} description: {
Text("Aucun événement en cours ou à venir dans votre agenda.")
Text("Aucun événement dans votre agenda.")
} actions: {
RowButtonView("Créer un nouvel événement") {
newTournament = Tournament.newEmptyInstance()
@ -473,7 +554,12 @@ struct ActivityView: View {
RowButtonView("Importer via Tenup") {
navigation.agendaDestination = .tenup
}
SupportButtonView(contentIsUnavailable: true)
SupportButtonView(supportButtonType: .contentIsUnavailable)
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 {
if dataStore.user.hasTenupClubs() == false {
ContentUnavailableView {
@ -496,6 +583,10 @@ struct ActivityView: View {
presentClubSearchView = true
}
.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 {
ContentUnavailableView {
@ -518,13 +609,16 @@ struct ActivityView: View {
ContentUnavailableView {
Label("Recherche de tournoi", systemImage: "magnifyingglass")
} 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: {
RowButtonView("Lancer la recherche") {
RowButtonView("Chercher un tournoi") {
displaySearchView = true
}
.padding()
}
.onAppear {
displaySearchView = true
}
} else {
if federalDataViewModel.lastError == nil {
ContentUnavailableView {
@ -561,3 +655,8 @@ struct ActivityView: View {
//#Preview {
// ActivityView()
//}
enum SubScreen: Hashable {
case subscription(FederalTournament, TournamentBuild)
}

@ -95,10 +95,15 @@ struct CalendarView: View {
if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) {
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)) {
_createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build)
}

@ -204,15 +204,15 @@ struct EventListView: View {
}
Divider()
Menu {
Button {
Task {
await pcTournaments.concurrentForEach { tournament in
await tournament.refreshTeamList(forced: true)
}
}
} label: {
Text("M-à-j des inscriptions")
}
// Button {
// Task {
// await pcTournaments.concurrentForEach { tournament in
// await tournament.refreshTeamList(forced: true)
// }
// }
// } label: {
// Text("M-à-j des inscriptions")
// }
Button {
pcTournaments.forEach { tournament in
@ -413,27 +413,27 @@ struct EventListView: View {
private func _tournamentView(_ tournament: Tournament) -> some View {
NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament)
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name.CollectionDidLoad), perform: { notification in
if let store = notification.object as? SyncedCollection<TeamRegistration> {
if store.storeId == tournament.id {
if tournament.store?.fileCollectionsAllLoaded() == true {
tournament.lastTeamRefresh = nil
}
}
}
if let store = notification.object as? SyncedCollection<PlayerRegistration> {
if store.storeId == tournament.id {
if tournament.store?.fileCollectionsAllLoaded() == true {
tournament.lastTeamRefresh = nil
}
}
}
})
.id(tournament.lastTeamRefresh)
.task(priority: .background) {
await tournament.refreshTeamList(forced: false)
}
// .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name.CollectionDidLoad), perform: { notification in
//
// if let store = notification.object as? SyncedCollection<TeamRegistration> {
// if store.storeId == tournament.id {
// if tournament.store?.fileCollectionsAllLoaded() == true {
// tournament.lastTeamRefresh = nil
// }
// }
// }
// if let store = notification.object as? SyncedCollection<PlayerRegistration> {
// if store.storeId == tournament.id {
// if tournament.store?.fileCollectionsAllLoaded() == true {
// tournament.lastTeamRefresh = nil
// }
// }
// }
// })
// .id(tournament.lastTeamRefresh)
// .task(priority: .background) {
// await tournament.refreshTeamList(forced: false)
// }
}
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)
.onChange(of: tournament.isTemplate) {
@ -463,10 +463,12 @@ struct EventListView: View {
}
#if DEBUG
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
dataStore.deleteTournament(tournament)
} label: {
LabelDelete()
if tournament.sharing == nil {
Button(role: .destructive) {
dataStore.deleteTournament(tournament)
} label: {
LabelDelete()
}
}
Button() {
dataStore.deleteTournament(tournament, noSync: true)
@ -530,11 +532,15 @@ struct EventListView: View {
if federalTournament.umpireLabel().isEmpty == false {
newTournament.umpireCustomContact = federalTournament.umpireLabel()
} else {
newTournament.umpireCustomContact = DataStore.shared.user.fullName()
}
if federalTournament.mailLabel().isEmpty == false {
newTournament.umpireCustomMail = federalTournament.mailLabel()
} else {
newTournament.umpireCustomMail = DataStore.shared.user.email
}
newTournament.umpireCustomPhone = DataStore.shared.user.phone
do {
let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id)
if let email = umpireData.email {

@ -30,7 +30,19 @@ struct TournamentLookUpView: View {
@State private var confirmSearch: Bool = false
@State private var locationRequested = false
@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] {
federalDataViewModel.searchedFederalTournaments
}
@ -140,23 +152,28 @@ struct TournamentLookUpView: View {
}
.toolbarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", systemImage: "xmark", role: .cancel) {
dismiss()
}
}
ToolbarItem(placement: .bottomBar) {
if revealSearchParameters {
FooterButtonView("Lancer la recherche") {
Button("Lancer la recherche") {
if dataStore.appSettings.city.isEmpty {
confirmSearch = true
} else {
runSearch()
}
}
.buttonStyle(.borderedProminent)
.disabled(searching)
} else if searching {
HStack(spacing: 20) {
Spacer()
ProgressView()
if total > 0 {
let percent = Double(tournaments.count) / Double(total)
Text(percent.formatted(.percent.precision(.significantDigits(1...3))) + " en récupération de Tenup")
Text("\(total) tournois en cours de récupération")
.font(.caption)
}
Spacer()
@ -193,11 +210,12 @@ struct TournamentLookUpView: View {
revealSearchParameters = true
federalDataViewModel.searchedFederalTournaments = []
federalDataViewModel.searchAttemptCount = 0
federalDataViewModel.removeFilters()
} label: {
Text("Ré-initialiser la recherche")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
LabelOptions()
}
}
}
@ -219,28 +237,50 @@ struct TournamentLookUpView: View {
}
private func _gatherNumbers() {
searching = true
Task {
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
}
await withTaskGroup(of: (Int, String?).self) { group in
for i in 0..<tournaments.count {
let tournamentID = tournaments[i].id
let index = i // Capture index for use in the child task
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
// Split into batches of 100
let batchSize = 100
let batches = idIndexPairs.chunked(into: batchSize)
print("Processing \(idIndexPairs.count) tournaments in \(batches.count) batches of \(batchSize)")
// Process each batch sequentially
for (batchIndex, batch) in batches.enumerated() {
print("Starting batch \(batchIndex + 1) of \(batches.count) (\(batch.count) tournaments)")
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
for await (index, phone) in group {
var tournamentData = federalDataViewModel.searchedFederalTournaments[index] // Get a mutable copy
tournamentData.updateJapPhoneNumber(phone: phone) // Mutate the copy
federalDataViewModel.searchedFederalTournaments[index] = tournamentData // Assign back
// Process results as they complete
for await (index, phone) in group {
var tournamentData = federalDataViewModel.searchedFederalTournaments[index] // Get a mutable copy
tournamentData.updateJapPhoneNumber(phone: phone) // Mutate the copy
federalDataViewModel.searchedFederalTournaments[index] = tournamentData // Assign back
}
}
print("Completed batch \(batchIndex + 1) of \(batches.count)")
}
searching = false
print(".....Done")
}
}
@ -320,7 +360,7 @@ struct TournamentLookUpView: View {
print("count", count, total, tournaments.count, page)
total = count
if tournaments.count < count && page < total / 30 {
if total - tournaments.count > count / 50 && page < total / 30 {
if total < 200 || requestedToGetAllPages {
page += 1
await getNewPage()
@ -340,8 +380,54 @@ struct TournamentLookUpView: View {
var searchParametersView: some View {
@Bindable var appSettings = dataStore.appSettings
Section {
DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date)
DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date)
Picker(selection: $quickOption) {
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) {
Text("Aucune").tag(nil as Int?)
Text(1.formatted()).tag(1 as Int?)
@ -350,7 +436,10 @@ struct TournamentLookUpView: View {
} label: {
Text("Durée souhaitée (en jours)")
}
@Bindable var federalDataViewModel = federalDataViewModel
WeekdayselectionView(weekdays: $federalDataViewModel.weekdays)
Picker(selection: $appSettings.dayPeriod) {
ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel().capitalized).tag($0)
@ -392,12 +481,12 @@ struct TournamentLookUpView: View {
}
.symbolVariant(.fill)
.foregroundColor (Color.white)
.cornerRadius (20)
.font(.system(size: 12))
}
}
Picker(selection: $appSettings.distance) {
Text(distanceLimit(distance:15).formatted()).tag(15.0)
Text(distanceLimit(distance:30).formatted()).tag(30.0)
Text(distanceLimit(distance:50).formatted()).tag(50.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 didSaveInCalendar: Bool = false
@State private var phoneNumber: String? = nil
@State private var errorWhenGatheringPhone: Bool = false
init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: CustomUser) {
self.federalTournament = federalTournament
@ -111,9 +112,13 @@ struct TournamentSubscriptionView: View {
Text(federalTournament.phoneLabel())
}
if let phoneNumber {
LabeledContent("Téléphone JAP") {
LabeledContent("Téléphone JAP") {
if let phoneNumber {
Text(phoneNumber)
} else if errorWhenGatheringPhone == false {
ProgressView()
} else {
Image(systemName: "exclamationmark.triangle")
}
}
} header: {
@ -163,8 +168,15 @@ struct TournamentSubscriptionView: View {
CopyPasteButtonView(pasteValue: messageBody)
}
}
.ifAvailableiOS26 { view in
view.toolbar(.hidden, for: .tabBar)
}
.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: .navigationBar)
@ -176,51 +188,61 @@ struct TournamentSubscriptionView: View {
}
}
.toolbar(content: {
ToolbarItem(placement: .status) {
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .bottomBar)
}
ToolbarItem(placement: .bottomBar) {
Menu {
if let courrielEngagement = federalTournament.courrielEngagement {
Section {
RowButtonView("S'inscrire par email", systemImage: "envelope") {
Menu {
if let courrielEngagement = federalTournament.courrielEngagement {
Button("Email", systemImage: "envelope") {
contactType = .mail(date: nil, recipients: [courrielEngagement], bccRecipients: nil, body: messageBody, subject: messageSubject, tournamentBuild: build as? TournamentBuild)
}
}
}
if let telephone = phoneNumber {
if telephone.isMobileNumber() {
Section {
RowButtonView("S'inscrire par message", systemImage: "message") {
if let telephone = phoneNumber {
if telephone.isMobileNumber() {
Button("Message", systemImage: "message") {
contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild)
}
}
}
let number = telephone.replacingOccurrences(of: " ", with: "")
if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler le JAP", systemImage: "phone")
let number = telephone.replacingOccurrences(of: " ", with: "")
if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler le JAP", systemImage: "phone")
}
}
}
} label: {
Label("Inscription", systemImage: "pencil.and.list.clipboard")
}
if let installation = federalTournament.installation, let telephone = installation.telephone {
Section {
RowButtonView("Contacter le club", systemImage: "house.and.flag") {
Menu {
if let installation = federalTournament.installation, let telephone = installation.telephone {
Button("Email", systemImage: "envelope") {
contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild)
}
}
let number = telephone.replacingOccurrences(of: " ", with: "")
if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler le club", systemImage: "phone")
let number = telephone.replacingOccurrences(of: " ", with: "")
if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
}
}
}
} label: {
Label("Contacter le club", systemImage: "house.and.flag")
}
} label: {
Text("Contact et inscription")
Text("S'inscrire")
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
}
.menuStyle(.button)
.buttonStyle(.borderedProminent)
.offset(y:-2)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .bottomBar)
}
ToolbarItem(placement: .topBarTrailing) {

@ -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,9 +17,16 @@ struct MainView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@Environment(ImportObserver.self) private var importObserver: ImportObserver
@State private var federalDataViewModel: FederalDataViewModel = FederalDataViewModel.shared
@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? {
dataStore.appSettings.lastDataSource
}
@ -73,23 +80,54 @@ struct MainView: View {
}
.toolbarBackground(.visible, for: .tabBar)
TournamentOrganizerView()
.tabItem(for: .tournamentOrganizer)
.toolbarBackground(.visible, for: .tabBar)
OngoingContainerView()
.tabItem(for: .ongoing)
.badge(self.dataStore.runningMatches().count)
.toolbarBackground(.visible, for: .tabBar)
UmpireOptionsView()
.tabItem(for: .umpire)
.toolbarBackground(.visible, for: .tabBar)
// TournamentOrganizerView()
// .tabItem(for: .tournamentOrganizer)
// .toolbarBackground(.visible, for: .tabBar)
ToolboxView()
.tabItem(for: .toolbox)
.toolbarBackground(.visible, for: .tabBar)
UmpireView()
.tabItem(for: .umpire)
.badge(badgeText)
MyAccountView()
.tabItem(for: .myAccount)
.toolbarBackground(.visible, for: .tabBar)
.badge(badgeText)
// PadelClubView()
// .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)
.onChange(of: dataStore.user.id) {
print("dataStore.user.id = ", dataStore.user.id)
@ -98,6 +136,8 @@ struct MainView: View {
navigation.path.removeLast(navigation.path.count)
mainViewId = UUID()
}
canPresentOnboarding = true
}
.environmentObject(dataStore)
.task {
@ -247,8 +287,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 {
// 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,226 @@
//
// MyAccountView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import SwiftUI
import CoreLocation
import LeStorage
import StoreKit
import PadelClubData
struct MyAccountView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore
@State private var showSubscriptions: Bool = false
@State private var showProductIds: Bool = false
@FocusState private var focusedField: CustomUser.CodingKeys?
// @State var isConnected: Bool = false
enum AccountScreen {
case login
}
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.accountPath) {
List {
Section {
SupportButtonView(supportButtonType: .bugReport, showIcon: true)
}
PurchaseListView()
Section {
Button {
self.showSubscriptions = true
} label: {
Label("Les offres", systemImage: "bookmark.fill")
}.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
self.showProductIds = true
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
self.showSubscriptions = true
}
)
}
if StoreCenter.main.isAuthenticated {
NavigationLink {
AccountView(user: dataStore.user) { }
} label: {
AccountRowView(userName: dataStore.user.username)
}
} else {
NavigationLink(value: AccountScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
if StoreCenter.main.isAuthenticated {
let onlineRegPaymentMode = dataStore.user.registrationPaymentMode
Section {
LabeledContent {
switch onlineRegPaymentMode {
case .corporate:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .disabled:
Text("Désactivé")
.bold()
case .noFee:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .stripe:
Text("Activé")
.bold()
.foregroundStyle(.green)
}
} label: {
Text("Option 'Paiement en ligne'")
if onlineRegPaymentMode == .corporate {
Text("Mode Padel Club")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .noFee {
Text("Commission Stripe")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .stripe {
Text("Commission Stripe et Padel Club")
.foregroundStyle(.secondary)
}
}
} footer: {
if onlineRegPaymentMode == .disabled {
FooterButtonView("Contactez nous pour activer cette option.") {
let emailTo: String = "support@padelclub.app"
let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)"
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
.font(.callout)
.multilineTextAlignment(.leading)
} else {
Text("Permet de proposer le paiement de vos tournois en ligne.")
}
}
Section {
SupportButtonView(supportButtonType: .sharingRequest)
} header: {
Text("Partage et délégation de compte")
} footer: {
Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?")
}
}
Section {
Link(destination: URLs.appReview.url) {
Text("Partagez vos impressions !")
}
Link(destination: URLs.instagram.url) {
Text("Compte Instagram PadelClub.app")
}
Link(destination: URLs.appDescription.url) {
Text("Page de présentation de Padel Club")
}
}
Section {
Link(destination: URLs.privacy.url) {
Text("Politique de confidentialité")
}
Link(destination: URLs.eula.url) {
Text("Contrat d'utilisation")
}
}
}
.sheet(isPresented: self.$showSubscriptions, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptions)
.environment(\.colorScheme, .light)
}
})
.sheet(isPresented: self.$showProductIds, content: {
ProductIdsView()
})
.navigationDestination(for: AccountScreen.self) { screen in
switch screen {
case .login:
LoginView {_ in }
}
}
.navigationTitle("Mon compte")
}
}
}
struct AccountRowView: View {
@EnvironmentObject var dataStore: DataStore
var userName: String
var body: some View {
let isAuthenticated = StoreCenter.main.isAuthenticated
LabeledContent {
if isAuthenticated {
Text(self.userName)
} else if StoreCenter.main.userName != nil {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
}
} label: {
Label("Mon compte", systemImage: "person.fill")
if isAuthenticated && dataStore.user.email.isEmpty == false {
Text(dataStore.user.email)
}
}
}
}
struct ProductIdsView: View {
@State var transactions: [StoreKit.Transaction] = []
var body: some View {
VStack {
List {
LabeledContent("count", value: String(self.transactions.count))
ForEach(self.transactions) { transaction in
if #available(iOS 17.2, *) {
if let offer = transaction.offer {
LabeledContent(transaction.productID, value: "\(offer.type)")
} else {
LabeledContent(transaction.productID, value: "no offer")
}
} else {
Text("need ios 17.2")
}
}
}.onAppear {
Task {
self.transactions = Array(Guard.main.purchasedTransactions)
}
}
}
}
}

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

@ -31,7 +31,7 @@ struct TournamentOrganizerView: View {
.toolbarBackground(.visible, for: .navigationBar)
}
}
let tournaments = dataStore.tournaments.filter({ $0.hasEnded() == false && $0.isDeleted == false && $0.isCanceled == false }).sorted(by: \.startDate).reversed()
let tournaments = dataStore.tournaments.filter({ $0.hasEnded() == false && $0.isDeleted == false && $0.isCanceled == false && $0.sharing != .granted }).sorted(by: \.startDate).reversed()
if tournaments.isEmpty == false {
Divider()
HStack {

@ -12,20 +12,27 @@ import PadelClubData
struct APICallsListView: View {
@State var descriptors: [CollectionDescriptor] = []
@State var total = 0
var body: some View {
List {
ForEach(self.descriptors) { descriptor in
NavigationLink {
if let syncedType = descriptor.type as? any SyncedStorable.Type {
APICallsView(name: descriptor.name, type: syncedType)
Section {
LabeledContent("Total count", value: "\(total)")
}
Section {
ForEach(self.descriptors) { descriptor in
NavigationLink {
if let syncedType = descriptor.type as? any SyncedStorable.Type {
APICallsView(name: descriptor.name, type: syncedType)
}
} label: {
LabeledContent(descriptor.name, value: descriptor.count.string)
}
} label: {
LabeledContent(descriptor.name, value: descriptor.count.string)
}
}
}.onAppear {
self.load()
@ -49,6 +56,8 @@ struct APICallsListView: View {
func loadCount<T: SyncedStorable>(_ type: T.Type, _ descriptor: CollectionDescriptor) async {
let calls = await StoreCenter.main.apiCalls(type: type)
descriptor.count = calls.count
self.total = total + descriptor.count
// Logger.log("\(descriptor.name), count = \(calls.count)")
}

@ -10,6 +10,10 @@ import LeStorage
import PadelClubData
struct DebugSettingsView: View {
@State private var errorMessage: String?
@State private var showingError = false
@State private var isSynchronizing = false
var body: some View {
List {
@ -17,7 +21,19 @@ struct DebugSettingsView: View {
LabeledContent("Has Websocket Manager", value: self._hasWebSocketManager)
LabeledContent("Websocket ping", value: self._wsPingStatus)
LabeledContent("Websocket failure", value: self._wsFailure)
LabeledContent("Last sync date", value: self._lastSyncDate)
LabeledContent("Last synced object date", value: self._lastSyncDate)
if isSynchronizing {
HStack {
ProgressView()
.scaleEffect(0.8)
Text("Synchronizing...")
.foregroundColor(.secondary)
}
} else {
Button("Synchronize") {
self._synchronize()
}
}
}
Section("Settings") {
@ -40,6 +56,11 @@ struct DebugSettingsView: View {
}
}
.alert("Synchronization Error", isPresented: $showingError) {
Button("OK") { }
} message: {
Text(errorMessage ?? "An unknown error occurred")
}
}
fileprivate var _userId: String {
@ -79,6 +100,27 @@ struct DebugSettingsView: View {
fileprivate var _lastSyncDate: String {
return "\(StoreCenter.main.lastSyncDate)"
}
fileprivate func _synchronize() {
Logger.log("launch sync...")
Task {
await MainActor.run {
isSynchronizing = true
}
let error = await StoreCenter.main.synchronizeLastUpdates()
if let error {
await MainActor.run {
errorMessage = error.localizedDescription
showingError = true
}
}
await MainActor.run {
isSynchronizing = false
}
}
}
}
struct DebugPurchaseView: View {

@ -21,6 +21,7 @@ struct ToolboxView: View {
@State private var tapCount = 0
@State private var lastTapTime: Date? = nil
private let tapTimeThreshold: TimeInterval = 1.0
@State private var displaySearchPlayer: Bool = false
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
@ -39,43 +40,6 @@ struct ToolboxView: View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.toolboxPath) {
List {
Section {
Link(destination: URLs.main.url) {
Text("Accéder à padelclub.app")
}
.contextMenu {
ShareLink(item: URLs.main.url)
}
SupportButtonView(contentIsUnavailable: false)
Link(destination: URLs.appReview.url) {
Text("Partagez vos impressions !")
}
Link(destination: URLs.instagram.url) {
Text("Compte Instagram PadelClub.app")
}
}
if self.showDebugViews {
DebugView()
}
Section {
NavigationLink {
SelectablePlayerListView(isPresented: false, lastDataSource: true)
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
NavigationLink {
RankCalculatorView()
} label: {
Label("Calculateur de points", systemImage: "scalemass")
}
}
Section {
NavigationLink {
PadelClubView()
@ -85,8 +49,7 @@ struct ToolboxView: View {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} label: {
Text(_lastDataSourceDate.monthYearFormatted)
Text("Classement mensuel utilisé")
Label(_lastDataSourceDate.monthYearFormatted, systemImage: "calendar.badge.checkmark")
}
} else {
LabeledContent {
@ -94,16 +57,45 @@ struct ToolboxView: View {
.tint(.logoRed)
} label: {
if let _mostRecentDateAvailable {
Text(_mostRecentDateAvailable.monthYearFormatted)
Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "calendar.badge")
} else {
Text("Aucun")
Label("Aucun", systemImage: "calendar.badge.exclamationmark")
}
Text("Classement mensuel disponible")
}
}
}
} header: {
Text("Classement mensuel utilisé")
}
Section {
Button {
displaySearchPlayer = true
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
NavigationLink {
RankCalculatorView()
} label: {
Label("Calculateur de points", systemImage: "scalemass")
}
}
Section {
Link(destination: URLs.main.url) {
Label("Padel Club sur le Web", systemImage: "link")
}
.contextMenu {
ShareLink(item: URLs.main.url)
}
ShareLink(item: URLs.appStore.url) {
Label("Padel Club sur l'App Store", systemImage: "square.and.arrow.up")
}
}
Section {
Link("Guide de la compétition", destination: URLs.padelCompetitionGeneralGuide.url)
Link("CDC des tournois", destination: URLs.padelCompetitionSpecificGuide.url)
@ -118,21 +110,10 @@ struct ToolboxView: View {
}
}
Section {
Link(destination: URLs.appDescription.url) {
Text("Page de présentation de Padel Club")
}
}
Section {
Link(destination: URLs.privacy.url) {
Text("Politique de confidentialité")
}
Link(destination: URLs.eula.url) {
Text("Contrat d'utilisation")
}
if self.showDebugViews {
DebugView()
}
Section {
RowButtonView("Effacer les logs", role: .destructive) {
StoreCenter.main.resetLoggingCollections()
@ -140,6 +121,19 @@ struct ToolboxView: View {
}
}
}
.sheet(isPresented: $displaySearchPlayer, content: {
NavigationStack {
SelectablePlayerListView(isPresented: false, lastDataSource: true)
.toolbar(.hidden, for: .tabBar)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Fermer") {
displaySearchPlayer = false
}
}
}
}
})
.onAppear {
#if DEBUG
self.showDebugViews = true
@ -162,16 +156,9 @@ struct ToolboxView: View {
}
}
}
// .navigationBarTitleDisplayMode(.large)
// .navigationTitle(TabDestination.toolbox.title)
.navigationBarTitleDisplayMode(.large)
.navigationTitle(TabDestination.toolbox.title)
.toolbar {
ToolbarItem(placement: .principal) {
Text(TabDestination.toolbox.title)
.font(.headline)
.onTapGesture {
_handleTitleTap()
}
}
ToolbarItem(placement: .topBarLeading) {
Link(destination: URLs.appStore.url) {
Text("v\(PadelClubApp.appVersion)")
@ -179,15 +166,16 @@ struct ToolboxView: View {
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
ShareLink(item: URLs.appStore.url) {
Label("Lien AppStore", systemImage: "link")
}
ShareLink(item: ZipLog(), preview: .init("Mon archive")) {
Label("Mes données", systemImage: "server.rack")
Text("Archiver mes données")
}
Divider()
Toggle("Outils avancées", isOn: $showDebugViews)
} label: {
Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
LabelOptions()
}
}
}
@ -244,66 +232,6 @@ struct DebugView: View {
Logger.log("Api calls reset")
}
}
Section {
RowButtonView("Fix Names") {
for tournament in dataStore.tournaments {
if let store = tournament.tournamentStore {
let playerRegistrations = store.playerRegistrations
playerRegistrations.forEach { player in
player.firstName = player.firstName.trimmed.capitalized
player.lastName = player.lastName.trimmed.uppercased()
}
do {
try store.playerRegistrations.addOrUpdate(contentOfs: playerRegistrations)
} catch {
Logger.error(error)
}
}
}
}
}
Section {
RowButtonView("Delete teams") {
for tournament in DataStore.shared.tournaments {
if let store: TournamentStore = tournament.tournamentStore {
let teamRegistrations = store.teamRegistrations.filter({ $0.tournamentObject() == nil })
do {
try store.teamRegistrations.delete(contentOfs: teamRegistrations)
} catch {
Logger.error(error)
}
}
}
}
}
Section {
// TODO
RowButtonView("Delete players") {
for tournament in DataStore.shared.tournaments {
if let store: TournamentStore = tournament.tournamentStore {
let playersRegistrations = store.playerRegistrations.filter({ $0.team() == nil })
do {
try store.playerRegistrations.delete(contentOfs: playersRegistrations)
} catch {
Logger.error(error)
}
}
}
}
}
}
}

@ -0,0 +1,68 @@
//
// UmpireOptionsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 06/10/2025.
//
import SwiftUI
import PadelClubData
struct UmpireOptionsView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State private var umpireOption: UmpireOption? = .umpire
var body: some View {
@Bindable var navigation = navigation
NavigationStack {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $umpireOption, destinations: UmpireOption.allCases, nilDestinationIsValid: true)
switch umpireOption {
case .none:
UmpireSettingsView()
.navigationTitle("Préférences")
case .umpire:
UmpireView()
.navigationTitle("Juge-Arbitre")
case .clubs:
ClubsView()
}
}
.navigationBarTitleDisplayMode(.large)
.navigationTitle("Juge-Arbitre")
.toolbarBackground(.visible, for: .navigationBar)
}
}
}
enum UmpireOption: Int, CaseIterable, Identifiable, Selectable, Equatable {
func badgeValue() -> Int? {
nil
}
func badgeImage() -> PadelClubData.Badge? {
nil
}
func badgeValueColor() -> Color? {
nil
}
var id: Int { self.rawValue }
case umpire
case clubs
var localizedTitleKey: String {
switch self {
case .umpire:
return "Juge-Arbitre"
case .clubs:
return "Clubs Favoris"
}
}
func selectionLabel(index: Int) -> String {
localizedTitleKey
}
}

@ -0,0 +1,85 @@
//
// UmpireSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 06/10/2025.
//
import SwiftUI
import CoreLocation
import LeStorage
import StoreKit
import PadelClubData
struct UmpireSettingsView: View {
@EnvironmentObject var dataStore: DataStore
var body: some View {
List {
if dataStore.user.canEnableOnlinePayment() {
Section {
if let tournamentTemplate = Tournament.getTemplateTournament() {
NavigationLink {
RegistrationSetupView(tournament: tournamentTemplate)
} label: {
Text("Référence")
Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary)
}
} else {
Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité")
}
} header: {
Text("Inscription et paiement en ligne")
} footer: {
Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne")
}
}
Section {
@Bindable var user = dataStore.user
Toggle(isOn: $user.disableRankingFederalRuling) {
Text("Désactiver la règle fédérale")
}
.onChange(of: user.disableRankingFederalRuling) {
dataStore.saveUser()
}
} header: {
Text("Règle fédérale classement final")
} footer: {
Text("Dernier de poule ≠ dernier du tournoi")
}
Section {
@Bindable var user = dataStore.user
Picker(selection: $user.loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: user.loserBracketMode) {
dataStore.saveUser()
}
} header: {
Text("Matchs de classement")
}
Section {
NavigationLink {
GlobalSettingsView()
} label: {
Label("Formats de jeu par défaut", systemImage: "megaphone")
}
NavigationLink {
DurationSettingsView()
} label: {
Label("Définir les durées moyennes", systemImage: "deskclock")
}
} footer: {
Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.")
}
}
}
}

@ -17,8 +17,6 @@ struct UmpireView: View {
@EnvironmentObject var dataStore: DataStore
@State private var presentSearchView: Bool = false
@State private var showSubscriptions: Bool = false
@State private var showProductIds: Bool = false
@State private var umpireCustomMail: String
@State private var umpireCustomPhone: String
@State private var umpireCustomContact: String
@ -38,99 +36,9 @@ struct UmpireView: View {
_umpireCustomContact = State(wrappedValue: DataStore.shared.user.umpireCustomContact ?? "")
}
enum UmpireScreen {
case login
}
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.umpirePath) {
List {
PurchaseListView()
Section {
Button {
self.showSubscriptions = true
} label: {
Label("Les offres", systemImage: "bookmark.fill")
}.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
self.showProductIds = true
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
self.showSubscriptions = true
}
)
}
if StoreCenter.main.isAuthenticated {
NavigationLink {
AccountView(user: dataStore.user) { }
} label: {
AccountRowView(userName: dataStore.user.username)
}
let onlineRegPaymentMode = dataStore.user.registrationPaymentMode
Section {
LabeledContent {
switch onlineRegPaymentMode {
case .corporate:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .disabled:
Text("Désactivé")
.bold()
case .noFee:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .stripe:
Text("Activé")
.bold()
.foregroundStyle(.green)
}
} label: {
Text("Option 'Paiement en ligne'")
if onlineRegPaymentMode == .corporate {
Text("Mode Padel Club")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .noFee {
Text("Commission Stripe")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .stripe {
Text("Commission Stripe et Padel Club")
.foregroundStyle(.secondary)
}
}
} footer: {
if onlineRegPaymentMode == .disabled {
FooterButtonView("Contactez nous pour activer cette option.") {
let emailTo: String = "support@padelclub.app"
let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)"
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
.font(.callout)
.multilineTextAlignment(.leading)
} else {
Text("Permet de proposer le paiement de vos tournois en ligne.")
}
}
} else {
NavigationLink(value: UmpireScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
List {
if StoreCenter.main.isAuthenticated {
let currentPlayerData = dataStore.user.currentPlayerData()
Section {
if let reason = licenseMessage {
@ -139,11 +47,11 @@ struct UmpireView: View {
if let currentPlayerData {
//todo palmares
ImportedPlayerView(player: currentPlayerData, showProgression: true)
// NavigationLink {
//
// } label: {
// ImportedPlayerView(player: currentPlayerData)
// }
// NavigationLink {
//
// } label: {
// ImportedPlayerView(player: currentPlayerData)
// }
} else {
RowButtonView("Ma fiche joueur", systemImage: "person.bust") {
presentSearchView = true
@ -155,6 +63,8 @@ struct UmpireView: View {
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
}
} header: {
Text("Mes infos licencié")
} footer: {
if dataStore.user.licenceId == nil {
Text("Si vous avez participé à un tournoi dans les 12 derniers mois, Padel Club peut vous retrouver.")
@ -173,7 +83,7 @@ struct UmpireView: View {
self.licenseMessage = nil
self.dataStore.saveUser()
}
} label: {
Text("options")
.foregroundStyle(Color.master)
@ -181,233 +91,119 @@ struct UmpireView: View {
}
}
}
_customUmpireView()
Section {
NavigationLink {
ClubsView()
} label: {
LabeledContent {
Text(dataStore.user.clubs.count.formatted())
} label: {
Label("Clubs favoris", systemImage: "house.and.flag")
}
@Bindable var user = dataStore.user
if dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone {
Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed)
}
} footer: {
Text("Il s'agit des clubs qui sont utilisés pour récupérer les tournois tenup.")
}
// Section {
// NavigationLink {
// UmpireStatisticView()
// } label: {
// Text("Statistiques de participations")
// }
// }
//
if StoreCenter.main.isAuthenticated {
_customUmpireView()
Section {
@Bindable var user = dataStore.user
if dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone {
Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed)
}
Toggle(isOn: $user.hideUmpireMail) {
Text("Masquer l'email")
}
Toggle(isOn: $user.hideUmpirePhone) {
Text("Masquer le téléphone")
}
} footer: {
Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.")
}
}
if dataStore.user.canEnableOnlinePayment() {
Section {
if let tournamentTemplate = Tournament.getTemplateTournament() {
NavigationLink {
RegistrationSetupView(tournament: tournamentTemplate)
} label: {
Text("Référence")
Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary)
}
} else {
Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité")
}
} header: {
Text("Inscription et paiement en ligne")
} footer: {
Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne")
}
}
Section {
@Bindable var user = dataStore.user
Toggle(isOn: $user.disableRankingFederalRuling) {
Text("Désactiver la règle fédéral")
Toggle(isOn: $user.hideUmpireMail) {
Text("Masquer l'email")
}
.onChange(of: user.disableRankingFederalRuling) {
dataStore.saveUser()
Toggle(isOn: $user.hideUmpirePhone) {
Text("Masquer le téléphone")
}
} header: {
Text("Règle fédérale classement finale")
} footer: {
Text("Dernier de poule ≠ dernier du tournoi")
Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.")
}
Section {
@Bindable var user = dataStore.user
Picker(selection: $user.loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: user.loserBracketMode) {
dataStore.saveUser()
}
}
.overlay(content: {
if StoreCenter.main.isAuthenticated == false {
ContentUnavailableView {
Label("Aucun compte", systemImage: "person.crop.circle.badge.exclamationmark")
} description: {
Text("Créer un compte Padel Club pour personnaliser vos informations de Juge-Arbitre")
} actions: {
RowButtonView("Créer un compte") {
_openCreateAccountView()
}
} header: {
Text("Matchs de classement")
}
Section {
NavigationLink {
GlobalSettingsView()
} label: {
Label("Formats de jeu par défaut", systemImage: "megaphone")
}
NavigationLink {
DurationSettingsView()
} label: {
Label("Définir les durées moyennes", systemImage: "deskclock")
}
})
.onChange(of: StoreCenter.main.userId) {
license = dataStore.user.licenceId ?? ""
licenseMessage = nil
}
.navigationBarBackButtonHidden(focusedField != nil)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
} footer: {
Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.")
}
// Section {
// Text("Tenup ID")
// }
//
// Section {
// Text("Tournois")
// }
//
// Section {
// NavigationLink {
//
// } label: {
// Text("Favori")
// }
// NavigationLink {
//
// } label: {
// Text("Black list")
// }
// }
}
.onChange(of: StoreCenter.main.userId) {
license = dataStore.user.licenceId ?? ""
licenseMessage = nil
}
.navigationTitle("Juge-Arbitre")
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
})
.toolbar {
if focusedField != nil {
ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Effacer") {
_deleteUmpireMail()
}
}
}
})
.toolbar {
if focusedField != nil {
ToolbarItem(placement: .keyboard) {
HStack {
if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Effacer") {
_deleteUmpireMail()
}
.buttonStyle(.borderless)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") {
_deleteUmpirePhone()
}
.buttonStyle(.borderless)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") {
_deleteUmpireContact()
}
.buttonStyle(.borderless)
}
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") {
_deleteUmpirePhone()
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") {
_deleteUmpireContact()
}
.buttonStyle(.borderedProminent)
}
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
}
.onChange(of: [dataStore.user.umpireCustomMail, dataStore.user.umpireCustomPhone, dataStore.user.umpireCustomContact]) {
self.dataStore.saveUser()
}
.onChange(of: [dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone]) {
self.dataStore.saveUser()
}
.onChange(of: focusedField) { old, new in
if old == ._umpireCustomMail {
_confirmUmpireMail()
} else if old == ._umpireCustomPhone {
_confirmUmpirePhone()
} else if old == ._umpireCustomContact {
_confirmUmpireContact()
} else if old == ._licenceId {
_confirmlicense()
}
}
.onChange(of: [dataStore.user.umpireCustomMail, dataStore.user.umpireCustomPhone, dataStore.user.umpireCustomContact]) {
self.dataStore.saveUser()
}
.onChange(of: [dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone]) {
self.dataStore.saveUser()
}
.onChange(of: focusedField) { old, new in
if old == ._umpireCustomMail {
_confirmUmpireMail()
} else if old == ._umpireCustomPhone {
_confirmUmpirePhone()
} else if old == ._umpireCustomContact {
_confirmUmpireContact()
} else if old == ._licenceId {
_confirmlicense()
}
.sheet(isPresented: self.$showSubscriptions, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptions)
.environment(\.colorScheme, .light)
}
})
.sheet(isPresented: self.$showProductIds, content: {
ProductIdsView()
})
.sheet(isPresented: $presentSearchView) {
let user = dataStore.user
NavigationStack {
SelectablePlayerListView(allowSelection: 1, searchField: user.firstName + " " + user.lastName, playerSelectionAction: { players in
if let player = players.first {
if user.clubsObjects().contains(where: { $0.code == player.clubCode }) == false {
let userClub = Club.findOrCreate(name: player.clubName!, code: player.clubCode)
if userClub.hasBeenCreated(by: StoreCenter.main.userId) {
dataStore.clubs.addOrUpdate(instance: userClub)
}
user.setUserClub(userClub)
}
.sheet(isPresented: $presentSearchView) {
let user = dataStore.user
NavigationStack {
SelectablePlayerListView(allowSelection: 1, searchField: user.firstName + " " + user.lastName, playerSelectionAction: { players in
if let player = players.first {
if user.clubsObjects().contains(where: { $0.code == player.clubCode }) == false {
let userClub = Club.findOrCreate(name: player.clubName!, code: player.clubCode)
if userClub.hasBeenCreated(by: StoreCenter.main.userId) {
dataStore.clubs.addOrUpdate(instance: userClub)
}
self._updateUserLicense(license: player.license?.computedLicense)
user.setUserClub(userClub)
}
})
}
.task {
do {
try await dataStore.clubs.loadDataFromServerIfAllowed()
} catch {
Logger.error(error)
self._updateUserLicense(license: player.license?.computedLicense)
}
}
})
}
.navigationDestination(for: UmpireScreen.self) { screen in
switch screen {
case .login:
LoginView {_ in }
.task {
do {
try await dataStore.clubs.loadDataFromServerIfAllowed()
} catch {
Logger.error(error)
}
}
}
@ -425,6 +221,10 @@ struct UmpireView: View {
}
private func _openCreateAccountView() {
navigation.selectedTab = .myAccount
}
private func _updateUserLicense(license: String?) {
guard let license else { return }
@ -532,67 +332,10 @@ struct UmpireView: View {
} }
} header: {
Text("Juge-arbitre")
Text("Mes infos juge-arbitre")
} footer: {
Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.")
}
}
}
struct AccountRowView: View {
@EnvironmentObject var dataStore: DataStore
var userName: String
var body: some View {
let isAuthenticated = StoreCenter.main.isAuthenticated
LabeledContent {
if isAuthenticated {
Text(self.userName)
} else if StoreCenter.main.userName != nil {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
}
} label: {
Label("Mon compte", systemImage: "person.fill")
if isAuthenticated && dataStore.user.email.isEmpty == false {
Text(dataStore.user.email)
}
}
}
}
struct ProductIdsView: View {
@State var transactions: [StoreKit.Transaction] = []
var body: some View {
VStack {
List {
LabeledContent("count", value: String(self.transactions.count))
ForEach(self.transactions) { transaction in
if #available(iOS 17.2, *) {
if let offer = transaction.offer {
LabeledContent(transaction.productID, value: "\(offer.type)")
} else {
LabeledContent(transaction.productID, value: "no offer")
}
} else {
Text("need ios 17.2")
}
}
}.onAppear {
Task {
self.transactions = Array(Guard.main.purchasedTransactions)
}
}
}
}
}
//#Preview {
// UmpireView()
//}

@ -111,8 +111,14 @@ struct CourtAvailabilitySettingsView: View {
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
showingPopover = true
if #available(iOS 26.0, *) {
BarButtonView("Ajouter une indisponibilité", icon: "plus") {
showingPopover = true
}
} else {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
showingPopover = true
}
}
}
}

@ -56,6 +56,18 @@ struct PlanningSettingsView: View {
DatePicker(selection: $tournament.startDate) {
Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized).lineLimit(1)
}
NavigationLink {
TournamentMatchFormatsSettingsView()
.environment(tournament)
} label: {
VStack(alignment: .leading) {
Text(tournament.formatSummary())
Text("Formats par défaut").foregroundStyle(.secondary).font(.caption)
}
}
LabeledContent {
StepperView(count: $tournament.dayDuration, minimum: 1)
} label: {

@ -315,8 +315,9 @@ struct PlanningView: View {
matchesToUpdate = matches.filter({ selectedIds.contains($0.stringId) })
} label: {
Text("Modifier")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderless)
.buttonStyle(.borderedProminent)
.disabled(selectedIds.isEmpty)
}
}
@ -727,9 +728,6 @@ struct PlanningView: View {
}
}
private func _eventCourtCount() -> Int { timeSlots.first?.value.first?.currentTournament()?.eventObject()?.eventCourtCount() ?? 2
}
private func _save() {
let groupByTournaments = allMatches.grouped { match in
match.currentTournament()
@ -748,16 +746,27 @@ struct PlanningView: View {
Button("Tirer au sort") {
_removeCourts()
let eventCourtCount = _eventCourtCount()
for slot in timeSlots {
var courtsAvailable = Array(0...eventCourtCount)
let matches = slot.value
matches.forEach { match in
if let rand = courtsAvailable.randomElement() {
var courtsByTournament: [String: Set<Int>] = [:]
for match in matches {
if let tournament = match.currentTournament(),
let available = tournament.matchScheduler()?.courtsAvailable {
courtsByTournament[tournament.id, default: []].formUnion(available)
}
}
for match in matches {
guard let tournament = match.currentTournament() else { continue }
// Get current set of available courts for this tournament id
guard var courts = courtsByTournament[tournament.id], !courts.isEmpty else { continue }
// Pick a random court
if let rand = courts.randomElement() {
match.courtIndex = rand
courtsAvailable.remove(elements: [rand])
// Remove from local copy and assign back into the dictionary
courts.remove(rand)
courtsByTournament[tournament.id] = courts
}
}
}
@ -767,16 +776,27 @@ struct PlanningView: View {
Button("Fixer par ordre croissant") {
_removeCourts()
let eventCourtCount = _eventCourtCount()
for slot in timeSlots {
var courtsAvailable = Array(0..<eventCourtCount)
let matches = slot.value.sorted(by: \.computedOrder)
var courtsByTournament: [String: Set<Int>] = [:]
for match in matches {
if let tournament = match.currentTournament(),
let available = tournament.matchScheduler()?.courtsAvailable {
courtsByTournament[tournament.id, default: []].formUnion(available.sorted())
}
}
for i in 0..<matches.count {
if !courtsAvailable.isEmpty {
let court = courtsAvailable.removeFirst()
guard let tournament = matches[i].currentTournament() else { continue }
// Get current set of available courts for this tournament id
guard var courts = courtsByTournament[tournament.id]?.sorted(), !courts.isEmpty else { continue }
if courts.isEmpty == false {
let court = courts.removeFirst()
matches[i].courtIndex = court
// Remove from local copy and assign back into the dictionary
courtsByTournament[tournament.id] = Set(courts)
}
}
}
@ -983,3 +1003,4 @@ extension EnvironmentValues {
set { self[EnableMoveKey.self] = newValue }
}
}

@ -172,7 +172,7 @@ struct SchedulerView: View {
Text("Match de classement \(round.roundTitle(.short))")
} footer: {
if tournament.isAnimation() == false, round.index == 1, let semi = round.loserRounds().first {
let federalFormat = tournament.loserBracketSmartMatchFormat(1)
let federalFormat = tournament.loserBracketSmartMatchFormat()
if semi.matchFormat.weight > federalFormat.weight {
Button {
round.updateMatchFormatAndAllMatches(federalFormat)

@ -202,27 +202,25 @@ struct PlayerPopoverView: View {
}
if licenseIsFocused || amountIsFocused {
ToolbarItem(placement: .keyboard) {
HStack {
Spacer()
Button("Confirmer") {
if licenseIsFocused {
license = license.trimmedMultiline
if requiredField.contains(.license) {
if license.isLicenseNumber {
amountIsFocused = true
} else {
displayWrongLicenceError = true
}
} else {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Confirmer") {
if licenseIsFocused {
license = license.trimmedMultiline
if requiredField.contains(.license) {
if license.isLicenseNumber {
amountIsFocused = true
} else {
displayWrongLicenceError = true
}
} else {
amountIsFocused = false
amountIsFocused = true
}
} else {
amountIsFocused = false
}
.buttonStyle(.bordered)
}
.buttonStyle(.borderedProminent)
}
}
}

@ -286,7 +286,7 @@ struct PlayerDetailView: View {
}
LabeledContent {
TextField("Téléphone contact", text: $contactPhoneNumber)
TextField("Téléphone", text: $contactPhoneNumber)
.focused($focusedField, equals: ._contactPhoneNumber)
.keyboardType(.namePhonePad)
.textContentType(nil)
@ -315,12 +315,12 @@ struct PlayerDetailView: View {
CopyPasteButtonView(pasteValue: player.contactPhoneNumber)
PasteButtonView(text: $contactPhoneNumber)
} label: {
Text("Téléphone contact")
Text("Téléphone")
}
}
LabeledContent {
TextField("Email contact", text: $contactEmail)
TextField("Email", text: $contactEmail)
.focused($focusedField, equals: ._contactEmail)
.keyboardType(.emailAddress)
.textContentType(nil)
@ -342,7 +342,7 @@ struct PlayerDetailView: View {
CopyPasteButtonView(pasteValue: player.contactEmail)
PasteButtonView(text: $contactEmail)
} label: {
Text("Email contact")
Text("Email")
}
}
} header: {
@ -372,7 +372,7 @@ struct PlayerDetailView: View {
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) {
ShareLink(item: player.pasteData(type: .sharing)) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
@ -401,6 +401,7 @@ struct PlayerDetailView: View {
}
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
}
}

@ -52,11 +52,7 @@ struct DrawLogsView: View {
Divider()
Button("Tout effacer", role: .destructive) {
do {
try tournament.tournamentStore?.drawLogs.deleteAll()
} catch {
Logger.error(error)
}
tournament.tournamentStore?.drawLogs.reset()
}
} label: {
LabelOptions()

@ -103,6 +103,9 @@ struct LoserRoundView: View {
}
.onAppear(perform: {
updateDisplayedMatches()
self.loserBracket.rounds.forEach({ round in
round.invalidateCache()
})
})
.onChange(of: isEditingTournamentSeed.wrappedValue) {
updateDisplayedMatches()

@ -297,5 +297,12 @@ struct LoserRoundsView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle(upperBracketRound.correspondingLoserRoundTitle)
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
if let tournament = upperBracketRound.round.tournamentObject() {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
}
}

@ -160,14 +160,7 @@ struct RoundSettingsView: View {
}
private func _removeRound(_ lastRound: Round) async {
await MainActor.run {
let teams = lastRound.seeds()
teams.forEach { team in
team.resetBracketPosition()
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
tournamentStore?.rounds.delete(instance: lastRound)
}
await tournament.removeRound(lastRound)
}
}

@ -29,6 +29,7 @@ struct RoundView: View {
func _refreshRound() {
self.upperRound.playedMatches = self.upperRound.round.playedMatches()
self.upperRound.round.invalidateCache()
}
init(upperRound: UpperRound) {
@ -79,9 +80,11 @@ struct RoundView: View {
}
}
if let disabledMatchesCount, disabledMatchesCount > 0 {
let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle())
TipView(bracketTip).tipStyle(tint: .green, asSection: true)
if let disabledMatchesCount {
if disabledMatchesCount > 0 {
let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle())
TipView(bracketTip).tipStyle(tint: .green, asSection: true)
}
let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount)
@ -93,7 +96,9 @@ struct RoundView: View {
Text("Match\(leftToPlay.pluralSuffix) à jouer en \(upperRound.title)")
}
} footer: {
Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement")
if disabledMatchesCount > 0 {
Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement")
}
}
}
}

@ -48,11 +48,17 @@ struct RoundsView: View {
case .some(let selectedRound):
RoundView(upperRound: selectedRound).id(selectedRound.id)
.navigationTitle(selectedRound.round.roundTitle())
}
}
.environment(\.isEditingTournamentSeed, $isEditingTournamentSeed)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
}

@ -139,13 +139,15 @@ struct EditScoreView: View {
Text(matchDescriptor.teamLabelTwo)
}
Divider()
Button {
self.matchDescriptor.match?.removeWalkOut()
save()
} label: {
Text("Annuler un forfait")
if self.matchDescriptor.match?.hasWalkoutTeam() == true {
Divider()
Button {
self.matchDescriptor.match?.removeWalkOut()
save()
} label: {
Text("Annuler un forfait")
}
}
} label: {
Text("Forfait d'une équipe ?")
@ -174,6 +176,13 @@ struct EditScoreView: View {
}
if matchDescriptor.hasEnded {
if self.matchDescriptor.match?.hasWalkoutTeam() == true {
RowButtonView("Annuler le forfait", role: .destructive) {
self.matchDescriptor.match?.removeWalkOut()
save()
}
}
Section {
HStack {
Spacer()
@ -235,6 +244,13 @@ struct EditScoreView: View {
matchDescriptor.setDescriptors.removeAll()
matchDescriptor.addNewSet()
}
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
if let tournament = matchDescriptor.match?.currentTournament() {
$0.navigationBarTitle(tournament.tournamentTitle())
}
}
}
}
func save() {

@ -88,7 +88,7 @@ struct FollowUpMatchView: View {
let allMatches = currentTournament?.allMatches() ?? []
self.matchesLeft = Tournament.matchesLeft(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.isFree = currentTournament?.isFree() ?? true
}
@ -100,7 +100,7 @@ struct FollowUpMatchView: View {
self.autoDismiss = autoDismiss
self.matchesLeft = Tournament.matchesLeft(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.isFree = false
}
@ -156,7 +156,7 @@ struct FollowUpMatchView: View {
case .index:
return matches
case .restingTime:
return matches.sorted(by: \.restingTimeForSorting)
return readyMatches.sorted(by: \.restingTimeForSorting)
case .court:
return matchesLeft.filter({ $0.courtIndex == selectedCourt })
case .winner:
@ -292,6 +292,13 @@ struct FollowUpMatchView: View {
}
}
}
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
if let tournament = match?.currentTournament() {
$0.navigationBarTitle(tournament.tournamentTitle())
}
}
}
.onChange(of: readyMatches) {
dismissWhenPresentFollowUpMatchIsDismissed = true
if autoDismiss {

@ -28,7 +28,7 @@ struct LearnMoreSheetView: View {
""")
} actions: {
ShareLink(item: tournament.pasteDataForImporting().createFile(tournament.tournamentTitle(.short))) {
ShareLink(item: tournament.pasteDataForImporting(type: .sharing).createFile(tournament.tournamentTitle(.short))) {
Text("Exporter les inscriptions")
}

@ -96,16 +96,27 @@ struct SelectablePlayerListView: View {
var body: some View {
VStack(spacing: 0) {
if importObserver.isImportingFile() == false {
if searchViewModel.filterSelectionEnabled == false {
VStack {
HStack {
Picker(selection: $searchViewModel.filterOption) {
ForEach(PlayerFilterOption.allCases, id: \.self) { scope in
Text(scope.icon().capitalized)
}
} label: {
VStack {
HStack {
Picker(selection: $searchViewModel.filterOption) {
ForEach(PlayerFilterOption.allCases, id: \.self) { scope in
Text(scope.icon().capitalized)
}
} 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 {
if let lastDataSource = dataStore.appSettings.localizedLastDataSource() {
Section {
@ -132,7 +143,7 @@ struct SelectablePlayerListView: View {
}
Divider()
Section {
Menu {
Picker(selection: $searchViewModel.selectedAgeCategory) {
ForEach(FederalTournamentAge.allCases) { ageCategory in
Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory)
@ -141,11 +152,11 @@ struct SelectablePlayerListView: View {
Text("Catégorie d'âge")
}
} header: {
} label: {
Text("Catégorie d'âge")
}
Divider()
Section {
Toggle(isOn: .init(get: {
return searchViewModel.hideAssimilation == false
@ -165,23 +176,36 @@ struct SelectablePlayerListView: View {
Text("Assimilés")
}
} label: {
VStack(alignment: .trailing) {
Label(searchViewModel.sortOption.localizedLabel(), systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down")
if searchViewModel.selectedAgeCategory != .unlisted {
Text(searchViewModel.selectedAgeCategory.localizedFederalAgeLabel()).font(.caption)
}
Text("tri par " + searchViewModel.sortTitle().lowercased())
.underline()
.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)
.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)
})
.keyboardType(.alphabet)
@ -212,11 +236,10 @@ struct SelectablePlayerListView: View {
}
.scrollDismissesKeyboard(.immediately)
.navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection)
.toolbarBackground(searchViewModel.allowMultipleSelection ? .visible : .hidden, for: .bottomBar)
.toolbarBackground(.hidden, for: .bottomBar)
.toolbarBackground(.visible, for: .navigationBar)
// .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor)
.interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false)
.navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet))
.navigationBarTitleDisplayMode(.inline)
} else {
List {
@ -284,7 +307,7 @@ struct SelectablePlayerListView: View {
searchViewModel.selectedPlayers.removeAll()
dismiss()
} label: {
Text("Annuler")
Label("Annuler", systemImage: "xmark")
}
}
@ -297,28 +320,16 @@ struct SelectablePlayerListView: View {
}
.disabled(searchViewModel.selectedPlayers.isEmpty)
}
ToolbarItem(placement: .status) {
let count = searchViewModel.selectedPlayers.count
VStack(spacing: 0) {
Text(count.formatted() + " joueur" + count.pluralSuffix + " séléctionné" + count.pluralSuffix).font(.footnote).foregroundStyle(.secondary)
FooterButtonView("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la liste") {
searchViewModel.filterSelectionEnabled.toggle()
}
}
}
}
if #available(iOS 26.0, *) {
DefaultToolbarItem(kind: .search, placement: .bottomBar)
}
}
.navigationTitle("Recherche")
.navigationBarTitleDisplayMode(.large)
// .modifierWithCondition(searchViewModel.user != nil) { thisView in
// thisView
.toolbarTitleMenu {
Picker(selection: $searchViewModel.dataSet) {
ForEach(DataSet.allCases) { dataSet in
Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet)
}
} label: {
}
}
// }
// .bottomBarAlternative(hide: searchViewModel.selectedPlayers.isEmpty) {
// ZStack {

@ -15,8 +15,43 @@ extension URL: Identifiable {
return self.absoluteString
}
}
enum SupportButtonType {
case contentIsUnavailable
case supervisorRequest
case bugReport
case sharingRequest
var localizedPrefix: String {
switch self {
case .contentIsUnavailable:
return "Décrivez votre problème"
case .supervisorRequest:
return localizedTopic
case .bugReport:
return "Décrivez votre problème"
case .sharingRequest:
return localizedTopic
}
}
var localizedTopic: String {
switch self {
case .contentIsUnavailable:
return "Support Padel Club"
case .supervisorRequest:
return "Demande d'ajout de superviseur"
case .bugReport:
return "Support Padel Club"
case .sharingRequest:
return "Demande de partage"
}
}
}
struct SupportButtonView: View {
let contentIsUnavailable: Bool
let supportButtonType: SupportButtonType
var showIcon: Bool = false
@State private var sentError: ContactManagerError? = nil
@State private var zipFilePath: URL?
@ -34,14 +69,37 @@ struct SupportButtonView: View {
var body: some View {
Group {
if contentIsUnavailable {
FooterButtonView("Besoin d'aide ? Un problème ? Contactez-nous !") {
switch supportButtonType {
case .sharingRequest:
Button("Nous contacter") {
_zip()
}
} else {
Button("Signaler un problème") {
case .supervisorRequest:
if showIcon {
Button("Demande d'ajout de superviseur", systemImage: "person.badge.plus") {
_zip()
}
.labelStyle(.iconOnly)
} else {
Button("Demande d'ajout de superviseur") {
_zip()
}
}
case .contentIsUnavailable:
FooterButtonView("Besoin d'aide ? Un problème ? Contactez-nous !") {
_zip()
}
case .bugReport:
if showIcon {
Button("Signaler un problème", systemImage: "square.and.pencil") {
_zip()
}
.labelStyle(.titleAndIcon)
} else {
Button("Signaler un problème") {
_zip()
}
}
}
}
.alert("Un problème est survenu", isPresented: messageSentFailed) {
@ -74,14 +132,14 @@ struct SupportButtonView: View {
private func _getSubject() -> String {
let device = UIDevice.current
let iOSVersion = device.systemVersion
return "[\(PadelClubApp.appVersion), \(iOSVersion), \(_getDeviceIdentifier())] Support Padel Club"
return "[\(PadelClubApp.appVersion), \(iOSVersion), \(_getDeviceIdentifier())] \(supportButtonType.localizedTopic)"
}
private func _getBody() -> String {
let separator = "---------------------------------------------"
let token = try? StoreCenter.main.token()
return ["Décrivez votre problème", "\n\n\n", separator, "token", token ?? "", separator, "userId", StoreCenter.main.userId, separator, "dataStore userId", DataStore.shared.user.id].compacted().joined(separator: "\n")
return [supportButtonType.localizedPrefix, "\n\n\n", separator, "token", token ?? "", separator, "userId", StoreCenter.main.userId, separator, "dataStore userId", DataStore.shared.user.id].compacted().joined(separator: "\n")
}
private func _getDeviceIdentifier() -> String {

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

@ -88,6 +88,19 @@ struct EditingTeamView: View {
team.uniqueRandomIndex = 0
}
var hasRegisteredOnline: Binding<Bool> {
Binding {
team.hasRegisteredOnline()
} set: { hasRegisteredOnline in
let players = team.players()
players.forEach { player in
player.registeredOnline = hasRegisteredOnline
}
tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
}
}
var body: some View {
List {
Section {
@ -103,7 +116,7 @@ struct EditingTeamView: View {
}
} footer: {
HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData())
CopyPasteButtonView(pasteValue: team.playersPasteData(type: .sharing))
Spacer()
if team.isWildCard(), team.unsortedPlayers().isEmpty {
TeamPickerView(pickTypeContext: .wildcard) { teamregistration in
@ -126,11 +139,10 @@ struct EditingTeamView: View {
}
.headerProminence(.increased)
if team.hasRegisteredOnline() || team.hasPaidOnline() {
if team.hasRegisteredOnline() || team.hasPaidOnline() || tournament.enableOnlineRegistration {
Section {
LabeledContent {
Text(team.hasRegisteredOnline() ? "Oui" : "Non")
} label: {
Toggle(isOn: hasRegisteredOnline) {
Text("Inscrits en ligne")
}
@ -140,6 +152,10 @@ struct EditingTeamView: View {
} label: {
Text("Payé en ligne")
}
if team.hasPaidOnline() == false {
PaymentRequestButton(teamRegistration: team)
}
}
if let refundMessage, refundMessage.isEmpty == false {

@ -0,0 +1,51 @@
//
// PaymentRequestButton.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/10/2025.
//
import SwiftUI
import PadelClubData
struct PaymentRequestButton: View {
let teamRegistration: TeamRegistration
@State private var isLoading = false
@State private var showAlert = false
@State private var alertMessage = ""
var body: some View {
Button("Renvoyer email de paiement") {
resendEmail()
}
.disabled(isLoading)
.alert("Résultat", isPresented: $showAlert) {
Button("OK") { }
} message: {
Text(alertMessage)
}
}
private func resendEmail() {
isLoading = true
Task {
do {
let response = try await PaymentService.resendPaymentEmail(
teamRegistrationId: teamRegistration.id
)
await MainActor.run {
isLoading = false
alertMessage = response.message
showAlert = true
}
} catch {
await MainActor.run {
isLoading = false
alertMessage = "Erreur lors de l'envoi"
showAlert = true
}
}
}
}
}

@ -0,0 +1,24 @@
class PaymentService {
static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(
servicePath: "resend-payment-email/\(teamRegistrationId)/",
method: .post,
requiresToken: true
)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PaymentError.requestFailed
}
return try JSON.decoder.decode(SimpleResponse.self, from: data)
}
}
struct SimpleResponse: Codable {
let success: Bool
let message: String
}

@ -90,7 +90,7 @@ struct TeamRestingView: View {
let allMatches = tournament.allMatches()
let matchesLeft = Tournament.matchesLeft(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.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)

@ -101,7 +101,7 @@ struct ConsolationTournamentImportView: View {
Picker(selection: $selectedTournament) {
Text("Aucun tournoi").tag(nil as Tournament?)
ForEach(tournaments) { tournament in
TournamentCellView(tournament: tournament).tag(tournament)
TournamentCellView(tournament: tournament, displayContext: .selection).tag(tournament)
}
} label: {
if selectedTournament == nil {

@ -231,6 +231,7 @@ struct AddTeamView: View {
.disabled(_limitPlayerCount())
.foregroundStyle(.master)
.labelStyle(.titleAndIcon)
.frame(maxWidth: .infinity)
.buttonBorderShape(.capsule)
}
}
@ -461,6 +462,7 @@ struct AddTeamView: View {
self.editableTextField = pasteString
self.focusedField = nil
}
.buttonStyle(.borderedProminent)
Spacer()
Button("Chercher") {
if editableTextField.count > 1 {
@ -470,7 +472,7 @@ struct AddTeamView: View {
self.displayWarningNotEnoughCharacter = true
}
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
}
}
} header: {

@ -84,6 +84,24 @@ struct BroadcastView: View {
.tipStyle(tint: nil)
}
if let shareURL = tournament.shareURL(.info) {
Section {
Link(destination: shareURL) {
Text(shareURL.absoluteString)
}
} header: {
Text("Page d'information")
} footer: {
HStack {
CopyPasteButtonView(pasteValue: shareURL.absoluteString)
Spacer()
ShareLink(item: shareURL) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
}
}
if let url = tournament.shareURL(.clubBroadcast) {
Section {
Link(destination: url) {
@ -113,6 +131,21 @@ struct BroadcastView: View {
}
}
Section {
let links : [PageLink] = [.info, .teams, .summons, .groupStages, .matches, .rankings, .broadcast, .clubBroadcast]
Picker(selection: $pageLink) {
ForEach(links) { pageLink in
Text(pageLink.localizedLabel()).tag(pageLink)
}
} label: {
Text("Page à partager")
}
.pickerStyle(.menu)
actionForURL(title: "Partager la page '" + pageLink.localizedLabel() + "'", url: tournament.shareURL(pageLink))
} header: {
Text("Lien du tournoi à partager")
}
if tournament.isPrivate == false {
Section {
@ -300,24 +333,9 @@ struct BroadcastView: View {
}
}
.toolbar(content: {
if StoreCenter.main.userId != nil, tournament.isPrivate == false, tournament.club() != nil {
if StoreCenter.main.userId != nil, tournament.club() != nil {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Section {
let links : [PageLink] = [.info, .teams, .summons, .groupStages, .matches, .rankings, .broadcast, .clubBroadcast]
Picker(selection: $pageLink) {
ForEach(links) { pageLink in
Text(pageLink.localizedLabel()).tag(pageLink)
}
} label: {
Text("Choisir la page à partager")
}
.pickerStyle(.menu)
actionForURL(title: "Partager la page '" + pageLink.localizedLabel() + "'", url: tournament.shareURL(pageLink))
} header: {
Text("Lien du tournoi à partager")
}
#if DEBUG
Section {
actionForURL(title: "La Boutique", url: URLs.main.url.appending(path: "shop"))
@ -344,6 +362,11 @@ struct BroadcastView: View {
})
.headerProminence(.increased)
.navigationTitle("Publication")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.sheet(item: $urlToShow) { urlToShow in

@ -0,0 +1,236 @@
//
// HeadManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 09/10/2025.
//
import SwiftUI
import LeStorage
import PadelClubData
struct HeadManagerView: View {
@EnvironmentObject private var dataStore: DataStore
@Environment(\.dismiss) var dismiss
let teamsInBracket: Int
let heads: Int
let initialSeedRepartition: [Int]?
let result: ([Int]) -> Void
@State private var seedRepartition: [Int]
@State private var selectedSeedRound: Int? = nil
init(teamsInBracket: Int, heads: Int, initialSeedRepartition: [Int], result: @escaping ([Int]) -> Void) {
self.teamsInBracket = teamsInBracket
self.heads = heads
self.initialSeedRepartition = initialSeedRepartition
self.result = result
if initialSeedRepartition.isEmpty == false {
_seedRepartition = .init(wrappedValue: initialSeedRepartition)
_selectedSeedRound = .init(wrappedValue: initialSeedRepartition.firstIndex(where: { $0 > 0 }))
} else {
let seedRepartition = Self.place(heads: heads, teamsInBracket: teamsInBracket, initialSeedRound: nil)
_seedRepartition = .init(wrappedValue: seedRepartition)
_selectedSeedRound = .init(wrappedValue: seedRepartition.firstIndex(where: { $0 > 0 }))
}
}
static func leftToPlace(heads: Int, teamsPerRound: [Int]) -> Int {
let re = heads - teamsPerRound.reduce(0, +)
return re
}
static func place(heads: Int, teamsInBracket: Int, initialSeedRound: Int?) -> [Int] {
var teamsPerRound: [Int] = []
let dimension = RoundRule.teamsInFirstRound(forTeams: teamsInBracket)
/*
si 32 = 32, ok si N < 32 alors on mets le max en 16 - ce qui reste à mettre en 32
*/
var startingRound = RoundRule.numberOfRounds(forTeams: dimension) - 1
if let initialSeedRound, initialSeedRound > 0 {
teamsPerRound = Array(repeating: 0, count: initialSeedRound)
startingRound = initialSeedRound
} else {
if dimension != teamsInBracket {
startingRound -= 1
}
teamsPerRound = Array(repeating: 0, count: startingRound)
}
while leftToPlace(heads: heads, teamsPerRound: teamsPerRound) > 0 {
// maxAssignable: On retire toutes les équipes placées dans les tours précédents, pondérées par leur propagation (puissance du tour)
let alreadyPut = teamsPerRound.reduce(0, +)
let headsLeft = heads - alreadyPut
// Calculate how many teams from previous rounds propagate to this round
let currentRound = teamsPerRound.count
var previousTeams = 0
for (i, teams) in teamsPerRound.enumerated() {
previousTeams += teams * (1 << (currentRound - i))
}
let totalAvailable = RoundRule.numberOfMatches(forRoundIndex: currentRound) * 2
let maxAssignable = max(0, totalAvailable - previousTeams)
var valueToAppend = min(max(0, headsLeft), maxAssignable)
if headsLeft - maxAssignable > 0 {
let theory = valueToAppend - (headsLeft - maxAssignable)
if theory > 0 && maxAssignable - theory == 0 {
valueToAppend = theory
} else {
let lastValue = teamsPerRound.last ?? 0
var newValueToAppend = theory == 0 ? maxAssignable / 2 : theory
if theory > maxAssignable || theory < 0 {
newValueToAppend = valueToAppend / 2
}
valueToAppend = lastValue > 0 ? lastValue : newValueToAppend
}
}
teamsPerRound.append(valueToAppend)
}
return teamsPerRound
}
var leftToPlace: Int {
Self.leftToPlace(heads: heads, teamsPerRound: seedRepartition)
}
var body: some View {
List {
Section {
Picker(selection: $selectedSeedRound) {
Text("Choisir").tag(nil as Int?)
ForEach(seedRepartition.indices, id: \.self) { idx in
Text(RoundRule.roundName(fromRoundIndex: idx, displayStyle: .short)).tag(idx)
}
} label: {
Text("Tour de la tête de série n°1")
}
.onChange(of: selectedSeedRound) {
seedRepartition = Self.place(heads: heads, teamsInBracket: teamsInBracket, initialSeedRound: selectedSeedRound)
}
}
Section {
LabeledContent {
Text(heads.formatted())
} label: {
Text("Équipes à placer en tableau")
}
if (teamsInBracket - heads) > 0 {
LabeledContent {
Text((teamsInBracket - heads).formatted())
} label: {
Text("Qualifiés entrants")
}
}
LabeledContent {
Text(leftToPlace.formatted())
} label: {
Text("Restant à placer")
}
LabeledContent {
let matchCount = seedRepartition.enumerated().map { (index, value) in
var result = 0
var count = value
if count == 0, let selectedSeedRound, index < selectedSeedRound {
let t = RoundRule.numberOfMatches(forRoundIndex: index)
result = RoundRule.cumulatedNumberOfMatches(forTeams: t * 2)
} else {
if index == seedRepartition.count - 1 {
count += (teamsInBracket - heads)
} else if index == seedRepartition.count - 2 {
count += ((seedRepartition[index + 1] + (teamsInBracket - heads)) / 2)
} else {
count += (seedRepartition[index + 1])
}
result = RoundRule.cumulatedNumberOfMatches(forTeams: count)
}
// print(index, value, result, count)
return result
}
.reduce(0, +)
Text(matchCount.formatted())
} label: {
Text("Matchs estimés")
}
}
Section {
//
// LabeledContent {
// StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound))
// } label: {
// Text("Nombre de tête de série")
// }
//
ForEach(seedRepartition.sorted().indices, id: \.self) { index in
SeedStepperRowView(count: $seedRepartition[index], roundIndex: index, max: leftToPlace + seedRepartition[index])
}
}
if leftToPlace > 0 {
RowButtonView("Ajouter une manche") {
while leftToPlace > 0 {
let headsLeft = heads - seedRepartition.reduce(0, +)
let lastValue = seedRepartition.last ?? 0
let maxAssignable = RoundRule.numberOfMatches(forRoundIndex: seedRepartition.count - 1) * 2 - lastValue
var valueToAppend = min(max(0, headsLeft), maxAssignable * 2)
if headsLeft - maxAssignable > 0 {
valueToAppend = valueToAppend - (headsLeft - maxAssignable)
}
// print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)")
seedRepartition.append(valueToAppend)
}
}
}
}
.navigationTitle("Répartition")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView(title: "Valider") {
self.result(seedRepartition)
dismiss()
}
}
ToolbarItem(placement: .topBarLeading) {
Button("Annuler") {
dismiss()
}
}
}
// .onChange(of: seedRepartition) { old, new in
// if modifiyingSeedRound == false {
// let minCount = min(old.count, new.count)
// if let idx = (0..<minCount).first(where: { old[$0] > new[$0] }) {
// seedRepartition = Array(new.prefix(idx+1))
// }
// }
// }
}
}
private struct SeedStepperRowView: View {
@Binding var count: Int
var roundIndex: Int
var max: Int
var body: some View {
LabeledContent {
HStack {
StepperView(count: $count, minimum: 0, maximum: min(RoundRule.numberOfMatches(forRoundIndex: roundIndex) * 2, max))
}
} label: {
Text("Équipes en \(RoundRule.roundName(fromRoundIndex: roundIndex))")
}
}
}

@ -9,12 +9,25 @@ import SwiftUI
import PadelClubData
struct TournamentFormatSelectionView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) private var tournament: Tournament
@State private var globalFormat: MatchFormat = DataStore.shared.user.bracketMatchFormatPreference ?? .nineGamesDecisivePoint
@ViewBuilder
var body: some View {
@Bindable var tournament = tournament
Section {
MatchTypeSelectionView(selectedFormat: $globalFormat, format: "Tout")
.onChange(of: globalFormat) { oldValue, newValue in
tournament.matchFormat = newValue
tournament.loserBracketMatchFormat = newValue
tournament.groupStageMatchFormat = newValue
}
} footer: {
Text("Modifier le format de tous les types de matchs")
}
Section {
MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule", additionalEstimationDuration: tournament.additionalEstimationDuration)
MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau", additionalEstimationDuration: tournament.additionalEstimationDuration)

@ -217,78 +217,76 @@ struct TournamentGeneralSettingsView: View {
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
if focusedField != nil {
ToolbarItem(placement: .keyboard) {
HStack {
if focusedField == ._entryFee {
if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) {
entryFee = priceTag
tournament.entryFee = priceTag
focusedField = nil
}
.buttonStyle(.bordered)
}
} else {
Button("Gratuit") {
entryFee = nil
tournament.entryFee = nil
focusedField = nil
}
.buttonStyle(.bordered)
}
} else if focusedField == ._clubMemberFeeDeduction {
ForEach(deductionTags, id: \.self) { deductionTag in
Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) {
clubMemberFeeDeduction = deductionTag
tournament.clubMemberFeeDeduction = deductionTag
ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._entryFee {
if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) {
entryFee = priceTag
tournament.entryFee = priceTag
focusedField = nil
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
}
} else {
Button("Gratuit") {
clubMemberFeeDeduction = entryFee
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
entryFee = nil
tournament.entryFee = nil
focusedField = nil
}
.buttonStyle(.bordered)
} else {
if focusedField == ._name, tournamentName.isEmpty == false {
Button("Effacer") {
tournament.name = nil
tournamentName = ""
}
.buttonStyle(.borderless)
} else if focusedField == ._information, tournamentInformation.isEmpty == false {
Button("Effacer") {
tournament.information = nil
tournamentInformation = ""
}
.buttonStyle(.borderless)
} else if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Effacer") {
_deleteUmpireMail()
}
.buttonStyle(.borderless)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") {
_deleteUmpirePhone()
}
.buttonStyle(.borderless)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") {
_deleteUmpireContact()
}
.buttonStyle(.borderless)
.buttonStyle(.borderedProminent)
}
} else if focusedField == ._clubMemberFeeDeduction {
ForEach(deductionTags, id: \.self) { deductionTag in
Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) {
clubMemberFeeDeduction = deductionTag
tournament.clubMemberFeeDeduction = deductionTag
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
Spacer()
Button("Valider") {
Button("Gratuit") {
clubMemberFeeDeduction = entryFee
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
focusedField = nil
}
.buttonStyle(.bordered)
.buttonStyle(.borderedProminent)
} else {
if focusedField == ._name, tournamentName.isEmpty == false {
Button("Effacer") {
tournament.name = nil
tournamentName = ""
}
.buttonStyle(.borderless)
} else if focusedField == ._information, tournamentInformation.isEmpty == false {
Button("Effacer") {
tournament.information = nil
tournamentInformation = ""
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Effacer") {
_deleteUmpireMail()
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") {
_deleteUmpirePhone()
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") {
_deleteUmpireContact()
}
.buttonStyle(.borderedProminent)
}
}
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
}
}
@ -481,10 +479,34 @@ struct TournamentGeneralSettingsView: View {
dataStore.tournaments.addOrUpdate(instance: tournament)
}
private func _email() -> String {
if tournament.sharing == nil {
return dataStore.user.email
} else {
return "Mail"
}
}
private func _phone() -> String {
if tournament.sharing == nil {
return dataStore.user.phone ?? "Téléphone"
} else {
return "Téléphone"
}
}
private func _contact() -> String {
if tournament.sharing == nil {
return dataStore.user.fullName()
} else {
return "Contact"
}
}
private func _customUmpireView() -> some View {
Section {
VStack(alignment: .leading) {
TextField(dataStore.user.email, text: $umpireCustomMail)
TextField(_email(), text: $umpireCustomMail)
.frame(maxWidth: .infinity)
.keyboardType(.emailAddress)
.autocapitalization(.none)
@ -498,7 +520,7 @@ struct TournamentGeneralSettingsView: View {
}
VStack(alignment: .leading) {
TextField(dataStore.user.phone ?? "Téléphone", text: $umpireCustomPhone)
TextField(_phone(), text: $umpireCustomPhone)
.frame(maxWidth: .infinity)
.keyboardType(.phonePad)
.focused($focusedField, equals: ._umpireCustomPhone)
@ -512,26 +534,30 @@ struct TournamentGeneralSettingsView: View {
VStack(alignment: .leading) {
TextField(dataStore.user.fullName(), text: $umpireCustomContact)
TextField(_contact(), text: $umpireCustomContact)
.frame(maxWidth: .infinity)
.keyboardType(.default)
.focused($focusedField, equals: ._umpireCustomContact)
.onSubmit {
_confirmUmpireContact()
}
if dataStore.user.getSummonsMessageSignature() != nil, umpireCustomContact != dataStore.user.fullName() {
if tournament.sharing == nil, dataStore.user.getSummonsMessageSignature() != nil, umpireCustomContact != dataStore.user.fullName() {
Text("Attention vous avez une signature personnalisée contenant un contact différent.").foregroundStyle(.logoRed)
FooterButtonView("retirer la personnalisation ?") {
dataStore.user.summonsMessageSignature = nil
self.dataStore.saveUser()
}
} }
}
}
} header: {
Text("Juge-arbitre")
} footer: {
Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.")
if tournament.sharing == nil {
Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.")
} else {
Text("Ce tournoi vous avez été partagé par un autre utilisateur. Par défaut ses informations seront utilisés pour ces champs si jamais ils restent vides.")
}
}
}
}

@ -25,12 +25,10 @@ struct TournamentMatchFormatsSettingsView: View {
var body: some View {
@Bindable var tournament = tournament
List {
if confirmUpdate {
RowButtonView("Modifier les matchs existants", role: .destructive) {
_updateAllFormat()
}
RowButtonView("Modifier les matchs existants", role: .destructive) {
_updateAllFormat()
}
TournamentFormatSelectionView()
Section {
@ -73,6 +71,7 @@ struct TournamentMatchFormatsSettingsView: View {
.deferredRendering(for: .seconds(2))
}
}
.navigationTitle("Formats")
}
private func _confirmOrSave() {

@ -0,0 +1,51 @@
//
// TournamentSelectorView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 09/10/2025.
//
import SwiftUI
import LeStorage
import PadelClubData
struct TournamentSelectorView: View {
@Binding var selectedTournament: Tournament?
@Environment(Tournament.self) private var tournament: Tournament
@EnvironmentObject private var dataStore: DataStore
@Environment(\.dismiss) var dismiss
@State private var allTournaments: Bool = false
var tournaments: [Tournament] {
dataStore.tournaments.filter({ $0.isDeleted == false && $0.id != tournament.id && (allTournaments || ($0.endDate != nil && $0.level == tournament.level && $0.category == tournament.category && $0.age == tournament.age)) }).sorted(by: \.startDate, order: .descending)
}
var body: some View {
List {
Picker(selection: $selectedTournament) {
Text("Aucun tournoi").tag(nil as Tournament?)
ForEach(tournaments) { tournament in
TournamentCellView(tournament: tournament, displayContext: .selection).tag(tournament)
}
} label: {
Text("Sélection d'un tournoi")
}
.labelsHidden()
.pickerStyle(.inline)
}
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Toggle("Tous les tournois", isOn: $allTournaments)
} label: {
Label("Filtre", systemImage: "line.3.horizontal.decrease")
}
}
})
.navigationTitle("Sélection d'un tournoi")
.onChange(of: selectedTournament) { oldValue, newValue in
dismiss()
}
}
}

@ -54,7 +54,10 @@ struct InscriptionManagerView: View {
@State private var refreshResult: String? = nil
@State private var refreshStatus: Bool?
@State private var showLegendView: Bool = false
@State private var isLoading = false
@State private var showAlert = false
@State private var alertMessage = ""
var tournamentStore: TournamentStore? {
return self.tournament.tournamentStore
}
@ -247,9 +250,9 @@ struct InscriptionManagerView: View {
}
if tournament.enableOnlineRegistration {
RowButtonView("Rafraîchir la liste", cornerRadius: 20) {
await _refreshList(forced: true)
}
// RowButtonView("Rafraîchir la liste", cornerRadius: 20) {
// await _refreshList(forced: true)
// }
} else if tournament.onlineRegistrationCanBeEnabled() {
RowButtonView("Inscription en ligne") {
navigation.path.append(Screen.settings)
@ -261,16 +264,14 @@ struct InscriptionManagerView: View {
}
}
}
.task(priority: .background) {
await _refreshList(forced: false)
}
.refreshable {
await _refreshList(forced: true)
}
// .task(priority: .background) {
// await _refreshList(forced: false)
// }
// .refreshable {
// await _refreshList(forced: true)
// }
.onAppear {
if tournament.enableOnlineRegistration == false || refreshStatus == true {
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
.onDisappear {
_handleHashDiff(selectedSortedTeams: selectedSortedTeams)
@ -335,8 +336,13 @@ struct InscriptionManagerView: View {
}
.tint(.master)
}
.alert("Requête de paiement", isPresented: $showAlert) {
Button("OK") { }
} message: {
Text(alertMessage)
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Toggle(isOn: $compactMode) {
Text("Vue compact")
@ -364,7 +370,49 @@ struct InscriptionManagerView: View {
LabelFilter()
.symbolVariant(filterMode == .all ? .none : .fill)
}
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .navigationBarTrailing)
}
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
#if PRODTEST
if tournament.enableOnlinePayment {
Button {
isLoading = true
Task {
do {
try await selectedSortedTeams.filter { team in
team.hasPaidOnline() == false && team.hasPaid() == false
}.concurrentForEach { team in
_ = try await PaymentService.resendPaymentEmail(teamRegistrationId: team.id)
}
await MainActor.run {
isLoading = false
alertMessage = "Relance effectuée avec succès"
showAlert = true
}
} catch {
Logger.error(error)
await MainActor.run {
isLoading = false
alertMessage = "Erreur lors de la requête"
showAlert = true
}
}
}
} label: {
Text("Requête de paiement")
}
.disabled(isLoading)
}
#endif
if tournament.inscriptionClosed() == false {
Menu {
_sortingTypePickerView()
@ -388,7 +436,7 @@ struct InscriptionManagerView: View {
if tournament.isAnimation() == false {
Divider()
Section {
Menu {
Button("+1 en tableau") {
tournament.addWildCard(1, .bracket)
_setHash()
@ -400,7 +448,7 @@ struct InscriptionManagerView: View {
_setHash()
}
}
} header: {
} label: {
Text("Ajout de wildcards")
}
@ -413,21 +461,29 @@ struct InscriptionManagerView: View {
}
Divider()
Button {
presentImportView = true
Menu {
Button {
presentImportView = true
} label: {
Label("Importer un fichier", systemImage: "square.and.arrow.down")
}
Link(destination: URLs.beachPadel.url) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
}
} label: {
Label("Importer beach-padel", systemImage: "square.and.arrow.down")
}
Link(destination: URLs.beachPadel.url) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
Text("Beach Padel")
}
if tournament.inscriptionClosed() == false {
Divider()
_importTeamsMenuView(title: "Importer des paires")
_sharingTeamsMenuView()
Menu {
_importTeamsMenuView(title: "Importer des paires d'un autre tournoi")
_sharingTeamsMenuView()
} label: {
Text("Importer / Exporter")
}
} else {
_sharingTeamsMenuView()
@ -451,19 +507,20 @@ struct InscriptionManagerView: View {
}
//rankingDateSourcePickerView(showDateInLabel: true)
Divider()
_sharingTeamsMenuView()
Divider()
_importTeamsMenuView(title: "Importer des paires")
Button {
presentImportView = true
Menu {
_sharingTeamsMenuView()
_importTeamsMenuView(title: "Importer des paires d'un autre tournoi")
Button {
presentImportView = true
} label: {
Label("Importer un fichier", systemImage: "square.and.arrow.down")
}
} label: {
Label("Importer un fichier", systemImage: "square.and.arrow.down")
Text("Importer / Exporter")
}
}
} label: {
@ -477,6 +534,11 @@ struct InscriptionManagerView: View {
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscriptions")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
.navigationBarTitleDisplayMode(.inline)
.onChange(of: tournament.hideTeamsWeight) {
_save()
@ -485,11 +547,25 @@ struct InscriptionManagerView: View {
private func _sharingTeamsMenuView() -> some View {
Menu {
ShareLink(item: teamPaste(), preview: .init("Inscriptions")) {
Text("En texte")
Menu {
ShareLink(item: teamPaste(.rawText, type: .sharing), preview: .init(ExportType.sharing.localizedString().capitalized)) {
Text("En texte")
}
ShareLink(item: teamPaste(.csv, type: .sharing), preview: .init(ExportType.sharing.localizedString().capitalized)) {
Text("En csv")
}
} label: {
Text("Pour diffusion")
}
ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) {
Text("En csv")
Menu {
ShareLink(item: teamPaste(.rawText, type: .payment), preview: .init(ExportType.payment.localizedString().capitalized)) {
Text("En texte")
}
ShareLink(item: teamPaste(.csv, type: .payment), preview: .init(ExportType.payment.localizedString().capitalized)) {
Text("En csv")
}
} label: {
Text("Pour encaissement")
}
} label: {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
@ -513,8 +589,8 @@ struct InscriptionManagerView: View {
tournament.unsortedTeamsWithoutWO()
}
func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat)
func teamPaste(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat, type: type)
}
var unsortedPlayers: [PlayerRegistration] {
@ -573,31 +649,31 @@ struct InscriptionManagerView: View {
// try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// }
//
private func _refreshList(forced: Bool) async {
if refreshStatus == true, forced == false { return }
if tournament.enableOnlineRegistration == false { return }
if tournament.hasEnded() { return }
if tournament.refreshInProgress { return }
refreshResult = nil
refreshStatus = nil
do {
await self.tournament.refreshTeamList(forced: forced)
_setHash()
if let lastTeamRefresh = self.tournament.lastTeamRefresh?.formatted(date: .abbreviated, time: .shortened) {
self.refreshResult = "Dernière m-à-j : \(lastTeamRefresh)"
} else {
self.refreshResult = "La synchronization a réussi"
}
self.refreshStatus = true
} catch {
Logger.error(error)
self.refreshResult = "La synchronization a échoué"
self.refreshStatus = false
}
}
// private func _refreshList(forced: Bool) async {
// if refreshStatus == true, forced == false { return }
// if tournament.enableOnlineRegistration == false { return }
// if tournament.hasEnded() { return }
// if tournament.refreshInProgress { return }
//
// refreshResult = nil
// refreshStatus = nil
// do {
// await self.tournament.refreshTeamList(forced: forced)
//
// _setHash()
// if let lastTeamRefresh = self.tournament.lastTeamRefresh?.formatted(date: .abbreviated, time: .shortened) {
// self.refreshResult = "Dernière m-à-j : \(lastTeamRefresh)"
// } else {
// self.refreshResult = "La synchronization a réussi"
// }
// self.refreshStatus = true
//
// } catch {
// Logger.error(error)
// self.refreshResult = "La synchronization a échoué"
// self.refreshStatus = false
// }
// }
private func _teamRegisteredView(selectedSortedTeams: [TeamRegistration]) -> some View {
List {
@ -728,7 +804,7 @@ struct InscriptionManagerView: View {
.pickerStyle(.menu)
if currentRankSourceDate == SourceFileManager.shared.mostRecentDateAvailable {
Button("Rafraîchir") {
Button("Rafraîchir le rang des joueurs") {
confirmUpdateRank = true
}
}
@ -878,19 +954,14 @@ struct InscriptionManagerView: View {
if tournament.enableOnlineRegistration {
LabeledContent {
Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted())
.font(.largeTitle)
.fontWeight(.bold)
} label: {
Text("Inscriptions en ligne")
if let refreshResult {
Text(refreshResult).foregroundStyle(.secondary)
} else {
Text(" ")
}
}
RowButtonView("Rafraîchir les inscriptions en ligne") {
await _refreshList(forced: true)
}
// RowButtonView("Rafraîchir les inscriptions en ligne") {
// await _refreshList(forced: true)
// }
}
} header: {
HStack {
@ -1194,10 +1265,11 @@ struct TournamentGroupStageShareContent: Transferable {
struct TournamentShareFile: Transferable {
let tournament: Tournament
let exportFormat: ExportFormat
let type: ExportType
func shareFile() -> URL {
print("Generating URL...")
return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat)
return tournament.pasteDataForImporting(exportFormat, type: type).createFile(self.tournament.tournamentTitle()+"-"+type.localizedString(), exportFormat)
}
static var transferRepresentation: some TransferRepresentation {

@ -169,6 +169,11 @@ struct PrintSettingsView: View {
}
}
.navigationTitle("Imprimer")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $presentShareView) {
@ -201,7 +206,7 @@ struct PrintSettingsView: View {
Text("Partager le code source HTML")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
LabelOptions()
}
}
}

@ -331,20 +331,18 @@ struct RegistrationSetupView: View {
})
.toolbar {
if focusedField == ._stripeAccountId, stripeAccountId.isEmpty == false {
ToolbarItem(placement: .keyboard) {
HStack {
Button("Effacer") {
stripeAccountId = ""
stripeAccountIdIsInvalid = nil
tournament.stripeAccountId = nil
}
.buttonStyle(.borderless)
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.bordered)
ToolbarItemGroup(placement: .keyboard) {
Button("Effacer") {
stripeAccountId = ""
stripeAccountIdIsInvalid = nil
tournament.stripeAccountId = nil
}
.buttonStyle(.borderedProminent)
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.borderedProminent)
}
}
}
@ -355,9 +353,13 @@ struct RegistrationSetupView: View {
}, message: {
Text(ValidationError.onlinePaymentNotEnabled.localizedDescription)
})
.toolbarRole(.editor)
.headerProminence(.increased)
.navigationTitle("Inscription en ligne")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarBackButtonHidden(hasChanges)

@ -10,7 +10,7 @@ import LeStorage
import PadelClubData
struct TableStructureView: View {
@Environment(Tournament.self) private var tournament: Tournament
var tournament: Tournament
@EnvironmentObject private var dataStore: DataStore
@Environment(\.dismiss) var dismiss
@State private var presentRefreshStructureWarning: Bool = false
@ -23,6 +23,12 @@ struct TableStructureView: View {
@State private var structurePreset: PadelTournamentStructurePreset = .manual
@State private var buildWildcards: Bool = true
@FocusState private var stepperFieldIsFocused: Bool
@State private var confirmReset: Bool = false
@State private var selectedTournament: Tournament?
@State private var initialSeedCount: Int = 0
@State private var initialSeedRound: Int = 0
@State private var showSeedRepartition: Bool = false
@State private var seedRepartition: [Int] = []
func displayWarning() -> Bool {
let unsortedTeamsCount = tournament.unsortedTeamsCount()
@ -59,9 +65,22 @@ struct TableStructureView: View {
var tsPure: Int {
max(teamCount - groupStageCount * teamsPerGroupStage, 0)
}
@ViewBuilder
var tf: Int {
max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
}
init(tournament: Tournament) {
self.tournament = tournament
_teamCount = .init(wrappedValue: tournament.teamCount)
_groupStageCount = .init(wrappedValue: tournament.groupStageCount)
_teamsPerGroupStage = .init(wrappedValue: tournament.teamsPerGroupStage)
_qualifiedPerGroupStage = .init(wrappedValue: tournament.qualifiedPerGroupStage)
_groupStageAdditionalQualified = .init(wrappedValue: tournament.groupStageAdditionalQualified)
_initialSeedCount = .init(wrappedValue: tournament.initialSeedCount)
_initialSeedRound = .init(wrappedValue: tournament.initialSeedRound)
}
var body: some View {
List {
if displayWarning() {
@ -78,12 +97,29 @@ struct TableStructureView: View {
} label: {
Text("Préréglage")
}
.disabled(selectedTournament != nil)
} footer: {
Text(structurePreset.localizedDescriptionStructurePresetTitle())
}
.onChange(of: structurePreset) {
_updatePreset()
}
Section {
NavigationLink {
TournamentSelectorView(selectedTournament: $selectedTournament)
.environment(tournament)
} label: {
if let selectedTournament {
TournamentCellView(tournament: selectedTournament, displayContext: .selection)
} else {
Text("À partir d'un tournoi existant")
}
}
}
.onChange(of: selectedTournament) {
_updatePreset()
}
}
Section {
@ -203,7 +239,6 @@ struct TableStructureView: View {
}
Section {
let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
if groupStageCount > 0 {
if structurePreset != .doubleGroupStage {
LabeledContent {
@ -225,17 +260,20 @@ struct TableStructureView: View {
LabeledContent {
Text(tsPure.formatted())
} label: {
Text("Nombre de têtes de série")
Text("Équipes à placer en tableau")
if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
Text("Attention !").foregroundStyle(.red)
}
}
LabeledContent {
Text(tf.formatted())
} label: {
Text("Équipes en tableau final")
}
// if groupStageCount > 0 {
// LabeledContent {
// Text(tf.formatted())
// } label: {
// Text("Effectif tableau")
// }
// }
} else {
LabeledContent {
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
@ -245,16 +283,30 @@ struct TableStructureView: View {
Text("Total de matchs")
}
}
} footer: {
if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0 {
if tsPure > teamCount / 2 {
Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif.").foregroundStyle(.red)
} else if tsPure < teamCount / 8 {
Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif.").foregroundStyle(.red)
} else if tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified {
Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red)
LabeledContent {
FooterButtonView("configurer") {
showSeedRepartition = true
}
} label: {
if tournament.state() == .build {
Text("Répartition des équipes")
} else if selectedTournament != nil {
Text("La configuration du tournoi séléctionné sera utilisée.")
} else {
Text(_seeds())
}
}
.onAppear {
if seedRepartition.isEmpty && tournament.state() == .initial && selectedTournament == nil {
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
}
} footer: {
if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0, tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified {
Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red)
}
}
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
@ -265,7 +317,7 @@ struct TableStructureView: View {
}
}
if tournament.rounds().isEmpty {
if tournament.rounds().isEmpty && tournament.state() == .build {
Section {
RowButtonView("Ajouter un tableau", role: .destructive) {
tournament.buildBracket(minimalBracketTeamCount: 4)
@ -277,46 +329,61 @@ struct TableStructureView: View {
if tournament.state() != .initial {
if seedRepartition.isEmpty == false {
Section {
RowButtonView("Répartir les équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") {
await _handleSeedRepartition()
}
} footer: {
Text("Cette action va effacer le répartition actuelle des équipes dans le tableau et la refaire, les manches seront ré-initialisées")
}
}
Section {
RowButtonView("Sauver sans reconstuire l'existant") {
_saveWithoutRebuild()
}
} footer: {
Text("Cette action sauve les paramètres du tournoi sans modifier vos poules / tableaux actuels.")
}
Section {
RowButtonView("Reconstruire les poules", role:.destructive) {
_save(rebuildEverything: false)
await _save(rebuildEverything: false)
}
} footer: {
Text("Cette action efface les poules existantes et les reconstruits, leurs données seront perdues.")
}
Section {
RowButtonView("Tout refaire", role: .destructive) {
_save(rebuildEverything: true)
await _save(rebuildEverything: true)
}
} footer: {
Text("Cette action efface le tableau et les poules existantes et reconstruit tout de zéro, leurs données seront perdues.")
}
Section {
RowButtonView("Remise-à-zéro", role: .destructive) {
tournament.removeWildCards()
tournament.deleteGroupStages()
tournament.deleteStructure()
if structurePreset != .manual {
structurePreset = PadelTournamentStructurePreset.manual
} else {
_updatePreset()
_reset()
}
} footer: {
Text("Retourne à la structure initiale, comme si vous veniez de créer le tournoi. Les données existantes seront perdues.")
}
Section {
RowButtonView("Retirer toutes les équipes de poules", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetGroupeStagePosition()
}
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
Section {
RowButtonView("Retirer toutes les équipes du tableau", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetBracketPosition()
}
}
}
@ -331,19 +398,20 @@ struct TableStructureView: View {
}
}
.toolbarBackground(.visible, for: .navigationBar)
.onAppear {
teamCount = tournament.teamCount
groupStageCount = tournament.groupStageCount
teamsPerGroupStage = tournament.teamsPerGroupStage
qualifiedPerGroupStage = tournament.qualifiedPerGroupStage
groupStageAdditionalQualified = tournament.groupStageAdditionalQualified
}
.sheet(isPresented: $showSeedRepartition, content: {
NavigationStack {
HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in
self.seedRepartition = seedRepartition
}
}
})
.onChange(of: teamCount) {
if teamCount != tournament.teamCount {
updatedElements.insert(.teamCount)
} else {
updatedElements.remove(.teamCount)
}
_verifyValueIntegrity()
}
.onChange(of: groupStageCount) {
if groupStageCount != tournament.groupStageCount {
@ -355,37 +423,65 @@ struct TableStructureView: View {
if structurePreset.isFederalPreset(), groupStageCount == 0 {
teamCount = structurePreset.tableDimension()
}
_verifyValueIntegrity()
}
.onChange(of: teamsPerGroupStage) {
if teamsPerGroupStage != tournament.teamsPerGroupStage {
updatedElements.insert(.teamsPerGroupStage)
} else {
updatedElements.remove(.teamsPerGroupStage)
} }
}
_verifyValueIntegrity()
}
.onChange(of: qualifiedPerGroupStage) {
if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage {
updatedElements.insert(.qualifiedPerGroupStage)
} else {
updatedElements.remove(.qualifiedPerGroupStage)
} }
}
_verifyValueIntegrity()
}
.onChange(of: groupStageAdditionalQualified) {
if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified {
updatedElements.insert(.groupStageAdditionalQualified)
} else {
updatedElements.remove(.groupStageAdditionalQualified)
}
_verifyValueIntegrity()
}
.toolbar {
if tournament.state() != .initial {
ToolbarItem(placement: .topBarTrailing) {
Button("Remise-à-zéro", systemImage: "trash", role: .destructive) {
confirmReset.toggle()
}
.confirmationDialog("Remise-à-zéro", isPresented: $confirmReset, titleVisibility: .visible, actions: {
Button("Tout effacer") {
_reset()
}
Button("Annuler") {
}
}, message: {
Text("Vous êtes sur le point d'effacer le tableau et les poules déjà créés et perdre les matchs et scores correspondant.")
})
}
}
ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial {
ButtonValidateView {
_save(rebuildEverything: true)
Task {
await _save(rebuildEverything: true)
}
}
} else {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
ButtonValidateView(role: .destructive) {
if requirements.isEmpty {
_save(rebuildEverything: false)
Task {
await _save(rebuildEverything: false)
}
} else {
presentRefreshStructureWarning = true
}
@ -396,11 +492,15 @@ struct TableStructureView: View {
}
Button("Reconstruire les poules") {
_save(rebuildEverything: false)
Task {
await _save(rebuildEverything: false)
}
}
Button("Tout refaire", role: .destructive) {
_save(rebuildEverything: true)
Task {
await _save(rebuildEverything: true)
}
}
}, message: {
ForEach(Array(requirements)) { requirement in
@ -412,8 +512,49 @@ struct TableStructureView: View {
}
.navigationTitle("Structure")
.navigationBarTitleDisplayMode(.inline)
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
private func _seeds() -> String {
if seedRepartition.isEmpty || seedRepartition.reduce(0, +) == 0 {
return "Aucune configuration"
}
return seedRepartition.enumerated().compactMap { (index, count) in
if count > 0 {
return RoundRule.roundName(fromRoundIndex: index) + " : \(count)"
} else {
return nil
}
}.joined(separator: ", ")
}
private func _reset() {
tournament.unsortedTeams().forEach {
$0.resetPositions()
}
tournament.removeWildCards()
tournament.deleteGroupStages()
tournament.deleteStructure()
if structurePreset != .manual {
structurePreset = PadelTournamentStructurePreset.manual
} else {
_updatePreset()
}
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
dataStore.tournaments.addOrUpdate(instance: tournament)
}
private func _saveWithoutRebuild() {
tournament.teamCount = teamCount
@ -453,7 +594,7 @@ struct TableStructureView: View {
}
}
private func _save(rebuildEverything: Bool = false) {
private func _save(rebuildEverything: Bool = false) async {
_verifyValueIntegrity()
do {
@ -466,12 +607,63 @@ struct TableStructureView: View {
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
if rebuildEverything {
if let selectedTournament {
tournament.matchFormat = selectedTournament.matchFormat
tournament.groupStageMatchFormat = selectedTournament.groupStageMatchFormat
tournament.loserBracketMatchFormat = selectedTournament.loserBracketMatchFormat
tournament.initialSeedRound = selectedTournament.initialSeedRound
tournament.initialSeedCount = selectedTournament.initialSeedCount
} else {
tournament.initialSeedRound = seedRepartition.firstIndex(where: { $0 > 0 }) ?? 0
tournament.initialSeedCount = seedRepartition.first(where: { $0 > 0 }) ?? 0
}
tournament.removeWildCards()
if structurePreset.hasWildcards(), buildWildcards {
tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket)
tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage)
}
tournament.deleteAndBuildEverything(preset: structurePreset)
if let selectedTournament {
let oldTournamentStart = selectedTournament.startDate
let newTournamentStart = tournament.startDate
let calendar = Calendar.current
let oldComponents = calendar.dateComponents([.hour, .minute, .second], from: oldTournamentStart)
var newComponents = calendar.dateComponents([.year, .month, .day], from: newTournamentStart)
newComponents.hour = oldComponents.hour
newComponents.minute = oldComponents.minute
newComponents.second = oldComponents.second ?? 0
if let updatedStartDate = calendar.date(from: newComponents) {
tournament.startDate = updatedStartDate
}
let allRounds = selectedTournament.rounds()
let allRoundsNew = tournament.rounds()
allRoundsNew.forEach { round in
if let pRound = allRounds.first(where: { r in
round.index == r.index
}) {
round.setData(from: pRound, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
}
}
let allGroupStages = selectedTournament.allGroupStages()
let allGroupStagesNew = tournament.allGroupStages()
allGroupStagesNew.forEach { groupStage in
if let pGroupStage = allGroupStages.first(where: { gs in
groupStage.index == gs.index
}) {
groupStage.setData(from: pGroupStage, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
}
}
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled())
}
if seedRepartition.count > 0 {
await _handleSeedRepartition()
}
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
tournament.deleteGroupStages()
tournament.buildGroupStages()
@ -490,13 +682,82 @@ struct TableStructureView: View {
}
}
private func _handleSeedRepartition() async {
while tournament.rounds().count < seedRepartition.count {
await tournament.addNewRound(tournament.rounds().count)
}
if seedRepartition.reduce(0, +) > 0 {
let rounds = tournament.rounds()
let roundsToDelete = rounds.suffix(rounds.count - seedRepartition.count)
for round in roundsToDelete {
await tournament.removeRound(round)
}
}
for (index, seedCount) in seedRepartition.enumerated() {
if let round = tournament.rounds().first(where: { $0.index == index }) {
let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index)
let playedMatches = round.playedMatches().map { $0.index - baseIndex }
let allMatches = round._matches()
let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in
playedMatches.contains(index)
}).prefix(seedCount)
if playedMatches.count == numberOfMatches && seedCount == numberOfMatches * 2 {
continue
}
for (index, value) in seedSorted.enumerated() {
let isOpponentTurn = index >= playedMatches.count
if let match = allMatches[safe:value] {
if match.index - baseIndex < numberOfMatches / 2 {
if isOpponentTurn {
match.previousMatch(.two)?.disableMatch()
} else {
match.previousMatch(.one)?.disableMatch()
}
} else {
if isOpponentTurn {
match.previousMatch(.one)?.disableMatch()
} else {
match.previousMatch(.two)?.disableMatch()
}
}
}
}
if seedCount > 0 {
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches())
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches())
round.deleteLoserBracket()
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
}
}
}
private func _updatePreset() {
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount()
groupStageCount = structurePreset.groupStageCount()
teamsPerGroupStage = structurePreset.teamsPerGroupStage()
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
if let selectedTournament {
seedRepartition = []
teamCount = selectedTournament.teamCount
groupStageCount = selectedTournament.groupStageCount
teamsPerGroupStage = selectedTournament.teamsPerGroupStage
qualifiedPerGroupStage = selectedTournament.qualifiedPerGroupStage
groupStageAdditionalQualified = selectedTournament.groupStageAdditionalQualified
buildWildcards = tournament.level.wildcardArePossible()
} else {
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount()
groupStageCount = structurePreset.groupStageCount()
teamsPerGroupStage = structurePreset.teamsPerGroupStage()
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
}
_verifyValueIntegrity()
}
private func _verifyValueIntegrity() {
@ -542,6 +803,7 @@ struct TableStructureView: View {
}
}
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
}
@ -596,3 +858,44 @@ extension TableStructureView {
// .environmentObject(DataStore.shared)
// }
//}
func frenchUmpireOrder(for matches: [Int]) -> [Int] {
if matches.count <= 1 { return matches }
if matches.count == 2 { return [matches[1], matches[0]] }
var result: [Int] = []
// Step 1: Take last, then first
result.append(matches.last!)
result.append(matches.first!)
// Step 2: Get remainder (everything except first and last)
let remainder = Array(matches[1..<matches.count-1])
if remainder.isEmpty { return result }
// Step 3: Split remainder in half
let mid = remainder.count / 2
let firstHalf = Array(remainder[0..<mid])
let secondHalf = Array(remainder[mid..<remainder.count])
// Step 4: Take first of 2nd half, then last of 1st half
if !secondHalf.isEmpty {
result.append(secondHalf.first!)
}
if !firstHalf.isEmpty {
result.append(firstHalf.last!)
}
// Step 5: Build new remainder from what's left and recurse
let newRemainder = Array(firstHalf.dropLast()) + Array(secondHalf.dropFirst())
result.append(contentsOf: frenchUmpireOrder(for: newRemainder))
return result
}
/// Convenience wrapper
func frenchUmpireOrder(for matchCount: Int) -> [Int] {
let m = frenchUmpireOrder(for: Array(0..<matchCount))
return m + m.reversed()
}

@ -137,6 +137,11 @@ struct TournamentCallView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Convocations")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
}

@ -166,6 +166,11 @@ struct TournamentCashierView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle(tournament.isFree() ? "Présence" : "Encaissement")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
} else {
Text("no store")
}

@ -42,7 +42,7 @@ struct TournamentRankView: View {
Section {
let all = tournament.allMatches()
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 en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false)
@ -158,6 +158,11 @@ struct TournamentRankView: View {
}
}
.navigationTitle("Classement")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {

@ -98,9 +98,14 @@ struct TournamentScheduleView: View {
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarRole(.editor)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Horaires et formats")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
}

@ -81,6 +81,12 @@ struct TournamentSettingsView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Réglages du tournoi")
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
}

@ -16,6 +16,7 @@ struct TournamentCellView: View {
let tournament: FederalTournamentHolder
// let color: Color = .black
var displayStyle: DisplayStyle = .wide
var displayContext: DisplayContext = .edition
@State var shouldTournamentBeOver: Bool = false
var event: Event? {
@ -28,10 +29,16 @@ struct TournamentCellView: View {
if let federalTournament = tournament as? FederalTournament {
if FederalDataViewModel.shared.isFederalTournamentValidForFilters(federalTournament, build: build) {
if navigation.agendaDestination == .around {
NavigationLink {
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user)
} label: {
_buildView(build, existingTournament: event?.existingBuild(build))
if #available(iOS 26.0, *) {
NavigationLink(value: SubScreen.subscription(federalTournament, build as! TournamentBuild)) {
_buildView(build, existingTournament: event?.existingBuild(build))
}
} else {
NavigationLink {
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user)
} label: {
_buildView(build, existingTournament: event?.existingBuild(build))
}
}
} else {
_buildView(build, existingTournament: event?.existingBuild(build))
@ -70,6 +77,12 @@ struct TournamentCellView: View {
Text(tournament.startDate.formatted(.dateTime.day(.twoDigits)))
.font(.title).fontWeight(.semibold)
// .monospacedDigit()
if displayContext == .selection {
Text(tournament.startDate.formatted(.dateTime.month(.abbreviated)).uppercased())
.font(.caption)
Text(tournament.startDate.formatted(.dateTime.year()))
.font(.caption)
}
}
}
// DateBoxView(date: tournament.startDate, displayStyle: displayStyle == .wide ? .short : .wide)

@ -48,6 +48,8 @@ struct PaymentStatusView: View {
@State var payment: TournamentPayment? = .free
@State var noOfferMessage: String? = nil
var body: some View {
Group {
@ -58,7 +60,8 @@ struct PaymentStatusView: View {
let text = "Tournoi offert (\(remaining) restant\(end))"
ImageInfoView(systemImage: "gift.fill", text: text, tip: FreeTournamentTip())
case nil:
ImageInfoView(systemImage: "exclamationmark.bubble.fill", text: "Veuillez souscrire à une offre pour convoquer ou entrer un résultat", textColor: .white, backgroundColor: .logoRed, tip: NoPaymentTip())
var text = noOfferMessage ?? "Veuillez souscrire à une offre pour entrer un résultat"
ImageInfoView(systemImage: "exclamationmark.bubble.fill", text: text, textColor: .white, backgroundColor: .logoRed, tip: NoPaymentTip())
default:
EmptyView()
}
@ -77,7 +80,7 @@ struct PaymentStatusView: View {
struct FreeTournamentTip: Tip {
var title: Text {
return Text("Nous vous offrons vos 3 premiers tournois ! Convoquez les équipes, créez les poules, le tableau comme vous le souhaitez. \nEnregistrez les résultats de chaque équipes et diffusez les scores en temps réel sur les écrans de votre club !\n\n Votre tournoi est décompté lorsque vous convoquez ou que vous rentrez un résultat.")
return Text("Nous vous offrons vos 3 premiers tournois ! Convoquez les équipes, créez les poules, le tableau comme vous le souhaitez. \nEnregistrez les résultats de chaque équipes et diffusez les scores en temps réel sur les écrans de votre club !\n\n Votre tournoi est décompté lorsque vous rentrez un résultat.")
}
var image: Image? {
@ -88,7 +91,7 @@ struct FreeTournamentTip: Tip {
struct NoPaymentTip: Tip {
var title: Text {
return Text("Vous ne disposez plus d'une offre vous permettant de convoquer les joueurs et de rentrer les résultats des matchs. Nous vous invitons à consulter les offres dans l'onglet JA.").foregroundStyle(.white)
return Text("Vous ne disposez plus d'une offre vous permettant de rentrer les résultats des matchs. Nous vous invitons à consulter les offres dans l'onglet JA.").foregroundStyle(.white)
}
var image: Image? {

@ -22,7 +22,7 @@ struct TournamentRunningView: View {
let runningMatches = Tournament.runningMatches(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)
Section {

@ -12,6 +12,7 @@ import PadelClubData
struct TournamentView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@State var tournament: Tournament
@ -137,7 +138,7 @@ struct TournamentView: View {
Group {
switch screen {
case .structure:
TableStructureView()
TableStructureView(tournament: tournament)
case .settings:
TournamentSettingsView()
case .inscription:
@ -167,12 +168,17 @@ struct TournamentView: View {
case .print:
PrintSettingsView(tournament: tournament)
case .share:
ShareModelView(instance: tournament)
ShareModelView(instance: tournament, handler: { users in
if users.count > 0 {
Task {
try? await self.tournament.payIfNecessary()
}
}
})
case .restingTime:
TeamRestingView()
case .stateSettings:
TournamentStatusView(tournament: tournament)
}
}
.environment(tournament)
@ -230,24 +236,29 @@ struct TournamentView: View {
}
#endif
if presentationContext == .agenda {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow")
}
Divider()
}
// if presentationContext == .agenda {
// Button {
// navigation.openTournamentInOrganizer(tournament)
// } label: {
// Label("Gestionnaire", systemImage: "pin")
// }
// Divider()
// }
NavigationLink(value: Screen.event) {
Text("Réglages de l'événement")
Label("Événement", systemImage: "info")
}
NavigationLink(value: Screen.settings) {
LabelSettings()
Label("Tournoi", systemImage: "gearshape")
}
NavigationLink(value: Screen.structure) {
Label("Structure", systemImage: "flowchart")
}
NavigationLink(value: Screen.call) {
Text("Convocations")
Label("Convocations", systemImage: "calendar.badge.clock")
}
if tournament.tournamentLevel.haveDeadlines() {
@ -256,24 +267,19 @@ struct TournamentView: View {
TournamentDeadlinesView(tournament: tournament)
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Rappel des délais")
.navigationTitle("Rappel des échéances")
} label: {
Text("Rappel des délais")
Label("Échéances", systemImage: "checklist")
}
}
NavigationLink(value: Screen.structure) {
LabelStructure()
}
NavigationLink(value: Screen.cashier) {
Text(tournament.isFree() ? "Présence" : "Encaissement")
Label(tournament.isFree() ? "Présence" : "Encaissement", systemImage: tournament.isFree() ? "person.crop.circle.badge.checkmark" : "eurosign.circle")
}
NavigationLink(value: Screen.statistics) {
Text("Statistiques")
}
// NavigationLink(value: Screen.statistics) {
// Label("Statistiques", systemImage: "123.rectangle")
// }
NavigationLink(value: Screen.rankings) {
LabeledContent {
@ -285,30 +291,41 @@ struct TournamentView: View {
.foregroundStyle(.green)
}
} label: {
Text("Classement final")
if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
}
Label("Classement", systemImage: "trophy")
}
}
NavigationLink(value: Screen.broadcast) {
Label("Publication", systemImage: "airplayvideo")
}
// NavigationLink(value: Screen.broadcast) {
// Label("Publication", systemImage: "airplayvideo")
// }
//
NavigationLink(value: Screen.print) {
Label("Imprimer", systemImage: "printer")
}
// NavigationLink(value: Screen.share) {
// Label("Partager", systemImage: "square.and.arrow.up")
// }
if self.tournament.sharing == nil {
NavigationLink(value: Screen.share) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
Divider()
NavigationLink(value: Screen.stateSettings) {
Text("Gestion du tournoi")
Text("Annuler, supprimer ou terminer le tournoi")
if self.tournament.sharing == nil {
NavigationLink(value: Screen.stateSettings) {
Label("Clôturer", systemImage: "stop.fill")
}
} else {
Button {
self._removeUserFromSharing()
// StoreCenter.main.setAuthorizedUsers(for: <#T##T#>, users: T##[String])
//
// DataStore.shared.deleteTournament(self.tournament, noSync: true)
} label: {
Label("Quitter la supervision", systemImage: "person.crop.circle.badge.minus")
}
}
} label: {
LabelOptions()
@ -322,6 +339,19 @@ struct TournamentView: View {
Logger.log("Tournament Id = \(self.tournament.id), Payment = \(String(describing: self.tournament.payment))")
}
}
fileprivate func _removeUserFromSharing() {
guard let userId = StoreCenter.main.userId else { return }
var users = StoreCenter.main.authorizedUsers(for: self.tournament.id)
users.removeAll(where: { $0 == userId })
do {
try StoreCenter.main.setAuthorizedUsers(for: self.tournament, users: users)
DataStore.shared.deleteTournament(self.tournament, noSync: true)
dismiss()
} catch {
Logger.error(error)
}
}
private func _save() {
dataStore.tournaments.addOrUpdate(instance: tournament)

@ -12,6 +12,7 @@ import PadelClubData
struct AccountView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var dataStore: DataStore
var user: CustomUser
var handler: () -> ()

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save