Compare commits

..

No commits in common. 'main' and 'sync3' have entirely different histories.
main ... sync3

  1. 8
      CLAUDE.md
  2. 88
      PadelClub.xcodeproj/project.pbxproj
  3. 2
      PadelClub/Extensions/Tournament+Extensions.swift
  4. 10
      PadelClub/PadelClubApp.swift
  5. 2
      PadelClub/Utils/FileImportManager.swift
  6. 36
      PadelClub/Utils/Network/PaymentService.swift
  7. 2
      PadelClub/ViewModel/AgendaDestination.swift
  8. 2
      PadelClub/ViewModel/NavigationViewModel.swift
  9. 5
      PadelClub/ViewModel/TabDestination.swift
  10. 2
      PadelClub/Views/Calling/BracketCallingView.swift
  11. 9
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  12. 4
      PadelClub/Views/Calling/CallSettingsView.swift
  13. 119
      PadelClub/Views/Calling/CallView.swift
  14. 3
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  15. 7
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  16. 98
      PadelClub/Views/Calling/SendToAllView.swift
  17. 60
      PadelClub/Views/Calling/TeamsCallingView.swift
  18. 14
      PadelClub/Views/Cashier/CashierView.swift
  19. 19
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  20. 11
      PadelClub/Views/Cashier/Event/EventView.swift
  21. 75
      PadelClub/Views/Club/ClubsView.swift
  22. 2
      PadelClub/Views/Components/FortuneWheelView.swift
  23. 2
      PadelClub/Views/Components/StepperView.swift
  24. 4
      PadelClub/Views/GroupStage/GroupStageView.swift
  25. 1
      PadelClub/Views/Match/MatchSetupView.swift
  26. 4
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  27. 51
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  28. 25
      PadelClub/Views/Navigation/MainView.swift
  29. 226
      PadelClub/Views/Navigation/MyAccount/MyAccountView.swift
  30. 51
      PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift
  31. 10
      PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift
  32. 194
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  33. 2
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  34. 68
      PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift
  35. 85
      PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift
  36. 408
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  37. 12
      PadelClub/Views/Planning/PlanningSettingsView.swift
  38. 110
      PadelClub/Views/Planning/PlanningView.swift
  39. 23
      PadelClub/Views/Player/PlayerDetailView.swift
  40. 3
      PadelClub/Views/Round/LoserRoundView.swift
  41. 9
      PadelClub/Views/Round/RoundSettingsView.swift
  42. 13
      PadelClub/Views/Round/RoundView.swift
  43. 6
      PadelClub/Views/Score/SetInputView.swift
  44. 2
      PadelClub/Views/Shared/LearnMoreSheetView.swift
  45. 1
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  46. 23
      PadelClub/Views/Shared/SupportButtonView.swift
  47. 91
      PadelClub/Views/Team/EditingTeamView.swift
  48. 268
      PadelClub/Views/Team/PaymentLinkManagerView.swift
  49. 2
      PadelClub/Views/Team/PaymentRequestButton.swift
  50. 52
      PadelClub/Views/Team/TeamMatchesView.swift
  51. 2
      PadelClub/Views/Tournament/ConsolationTournamentImportView.swift
  52. 1
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  53. 240
      PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift
  54. 13
      PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift
  55. 7
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  56. 51
      PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift
  57. 99
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  58. 448
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  59. 83
      PadelClub/Views/Tournament/Shared/PlayerSearchView.swift
  60. 37
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  61. 9
      PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift
  62. 2
      PadelClub/Views/Tournament/TournamentBuildView.swift
  63. 41
      PadelClub/Views/Tournament/TournamentView.swift
  64. 55
      PadelClub/Views/User/AccountView.swift
  65. 2
      PadelClub/Views/User/LoginView.swift
  66. 17
      PadelClub/Views/User/ShareModelView.swift

@ -1,8 +0,0 @@
## Padel Club
This is the main directory of a Swift app that helps padel tournament organizers.
The project is structured around three projects linked in the PadelClub.xcworkspace:
- PadelClub: this one, which mostly contains the UI for the project
- PadelClubData: the business logic for the app
- LeStorage: a local storage with a synchronization layer

@ -151,15 +151,6 @@
FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; };
FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */; };
FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7D2BFA0105000B4573 /* match-template.html */; };
FF2099042EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; };
FF2099052EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; };
FF2099062EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; };
FF2099082EA140A2003CE880 /* TeamMatchesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099072EA140A2003CE880 /* TeamMatchesView.swift */; };
FF2099092EA140A2003CE880 /* TeamMatchesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099072EA140A2003CE880 /* TeamMatchesView.swift */; };
FF20990A2EA140A2003CE880 /* TeamMatchesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099072EA140A2003CE880 /* TeamMatchesView.swift */; };
FF20990C2EA1430E003CE880 /* PlayerSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF20990B2EA1430E003CE880 /* PlayerSearchView.swift */; };
FF20990D2EA1430E003CE880 /* PlayerSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF20990B2EA1430E003CE880 /* PlayerSearchView.swift */; };
FF20990E2EA1430E003CE880 /* PlayerSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF20990B2EA1430E003CE880 /* PlayerSearchView.swift */; };
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */; };
FF2B51612C7E302C00FFF126 /* local.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = FF2B51602C7E302C00FFF126 /* local.sqlite */; };
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; };
@ -170,15 +161,6 @@
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 */; };
@ -773,12 +755,6 @@
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 */; };
@ -1047,9 +1023,6 @@
FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = "<group>"; };
FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = "<group>"; };
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = "<group>"; };
FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentLinkManagerView.swift; sourceTree = "<group>"; };
FF2099072EA140A2003CE880 /* TeamMatchesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamMatchesView.swift; sourceTree = "<group>"; };
FF20990B2EA1430E003CE880 /* PlayerSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSearchView.swift; sourceTree = "<group>"; };
FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningByCourtView.swift; sourceTree = "<group>"; };
FF2B51602C7E302C00FFF126 /* local.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = local.sqlite; sourceTree = "<group>"; };
FF2B51622C7F073100FFF126 /* Model_1_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_1_1.xcdatamodel; sourceTree = "<group>"; };
@ -1057,9 +1030,6 @@
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; };
@ -1185,8 +1155,6 @@
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>"; };
@ -1626,21 +1594,12 @@
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 */,
@ -1673,7 +1632,6 @@
FF7091692B90F95E00AB08DA /* DateBoxView.swift */,
FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */,
FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */,
FF20990B2EA1430E003CE880 /* PlayerSearchView.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -1725,8 +1683,6 @@
isa = PBXGroup;
children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */,
FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */,
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */,
@ -1844,8 +1800,6 @@
FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */,
FFE8B5BE2DAA325400BDE966 /* RefundResultsView.swift */,
FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */,
FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */,
FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -1903,8 +1857,6 @@
FF17CA562CC02FEA003C7323 /* CoachListView.swift */,
FF7DCD382CC330260041110C /* TeamRestingView.swift */,
FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */,
FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */,
FF2099072EA140A2003CE880 /* TeamMatchesView.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */,
);
path = Team;
@ -2318,13 +2270,11 @@
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 */,
@ -2396,7 +2346,6 @@
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 */,
@ -2448,20 +2397,17 @@
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF20990D2EA1430E003CE880 /* PlayerSearchView.swift in Sources */,
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */,
FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */,
FFD8837A2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF2099092EA140A2003CE880 /* TeamMatchesView.swift in Sources */,
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
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 */,
@ -2494,7 +2440,6 @@
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 */,
@ -2519,7 +2464,6 @@
FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */,
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */,
FF2099052EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */,
FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */,
@ -2598,13 +2542,11 @@
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 */,
@ -2676,7 +2618,6 @@
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 */,
@ -2728,20 +2669,17 @@
FF4CBFD82C996C0600151637 /* ImportedPlayer+Extensions.swift in Sources */,
FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */,
FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF20990C2EA1430E003CE880 /* PlayerSearchView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD883792E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF20990A2EA140A2003CE880 /* TeamMatchesView.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */,
FF4CBFDF2C996C0600151637 /* FederalTournamentSearchScope.swift in Sources */,
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 */,
@ -2774,7 +2712,6 @@
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 */,
@ -2799,7 +2736,6 @@
FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF4CC0142C996C0600151637 /* PlayerBlockView.swift in Sources */,
FF4CC0172C996C0600151637 /* PointView.swift in Sources */,
FF2099042EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */,
FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */,
C49771E72DC25F04005CD239 /* Color+Extensions.swift in Sources */,
@ -2856,13 +2792,11 @@
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 */,
@ -2934,7 +2868,6 @@
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 */,
@ -2986,20 +2919,17 @@
FF70FB572C90584900129CC2 /* ImportedPlayer+Extensions.swift in Sources */,
FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */,
FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF20990E2EA1430E003CE880 /* PlayerSearchView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD8837B2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF2099082EA140A2003CE880 /* TeamMatchesView.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */,
FF70FB5E2C90584900129CC2 /* FederalTournamentSearchScope.swift in Sources */,
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 */,
@ -3032,7 +2962,6 @@
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 */,
@ -3057,7 +2986,6 @@
FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF70FB932C90584900129CC2 /* PlayerBlockView.swift in Sources */,
FF70FB962C90584900129CC2 /* PointView.swift in Sources */,
FF2099062EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */,
FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */,
C49771E42DC25F04005CD239 /* Color+Extensions.swift in Sources */,
@ -3245,7 +3173,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\"";
@ -3272,7 +3200,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.64;
MARKETING_VERSION = 1.2.53;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3293,7 +3221,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;
@ -3319,7 +3247,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.64;
MARKETING_VERSION = 1.2.53;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3412,7 +3340,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3439,7 +3367,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.57;
MARKETING_VERSION = 1.2.37;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3459,7 +3387,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3485,7 +3413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.57;
MARKETING_VERSION = 1.2.37;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -205,7 +205,7 @@ extension Tournament {
}
func registrationIssues(selectedTeams: [TeamRegistration]) async -> Int {
let players : [PlayerRegistration] = selectedTeams.flatMap { $0.players() }
let players : [PlayerRegistration] = unsortedPlayers()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })

@ -193,11 +193,11 @@ struct PadelClubApp: App {
navigationViewModel.selectedTab = .umpire
}
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)
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)
}
}
}.resume()

@ -154,7 +154,7 @@ class FileImportManager {
}
let significantPlayerCount = 2
let pl = players.prefix(significantPlayerCount).map { $0.computedRank }
let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 92_327 : 10_000) }).prefix(significantPlayerCount)
let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 90_415 : 10_000) }).prefix(significantPlayerCount)
self.weight = pl.reduce(0,+) + missingPl.reduce(0,+)
} else {
self.weight = players.map { $0.computedRank }.reduce(0,+)

@ -27,42 +27,6 @@ class PaymentService {
return try JSON.decoder.decode(SimpleResponse.self, from: data)
}
static func getPaymentLink(teamRegistrationId: String) async throws -> PaymentLinkResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(
servicePath: "payment-link/\(teamRegistrationId)/",
method: .get,
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
}
// // Debug: Print the raw JSON response
// if let jsonString = String(data: data, encoding: .utf8) {
// print("Raw JSON Response: \(jsonString)")
// }
return try JSON.decoder.decode(PaymentLinkResponse.self, from: data)
}
}
struct PaymentLinkResponse: Codable {
let success: Bool
let paymentLink: String?
let message: String?
enum CodingKeys: String, CodingKey {
case success
case paymentLink
case message
}
}
enum PaymentError: Error {

@ -25,7 +25,7 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
var localizedTitleKey: String {
switch self {
case .activity:
return "À venir"
return "En cours"
case .history:
return "Terminé"
case .tenup:

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

@ -17,7 +17,6 @@ enum TabDestination: CaseIterable, Identifiable {
case tournamentOrganizer
case umpire
case ongoing
case myAccount
var title: String {
switch self {
@ -31,8 +30,6 @@ enum TabDestination: CaseIterable, Identifiable {
return "Gestionnaire"
case .umpire:
return "Juge-Arbitre"
case .myAccount:
return "Compte"
}
}
@ -48,8 +45,6 @@ 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().map { $0.startDate }).compacted().min()
let startDate = round.startDate ?? round.playedMatches().first?.startDate
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.customClubName ?? tournament.clubName ?? "Lieu du tournoi")
_customClubName = State(wrappedValue: tournament.clubName ?? "Lieu du tournoi")
_summonsAvailablePaymentMethods = State(wrappedValue: DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods)
}
@ -235,13 +235,14 @@ 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)
TextField("Nom du club", text: $customClubName, axis: .vertical)
.lineLimit(2)
.autocorrectionDisabled()
.focused($focusedField, equals: .clubName)
.onSubmit {
tournament.customClubName = customClubName.prefixTrimmed(100)
eventClub.name = customClubName
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
try dataStore.clubs.addOrUpdate(instance: eventClub)
} catch {
Logger.error(error)
}

@ -96,11 +96,11 @@ struct CallSettingsView: View {
//#endif
}
.sheet(isPresented: $showSendToAllView) {
SendToAllView(tournament: tournament, addLink: false)
SendToAllView(addLink: false)
.tint(.master)
}
.sheet(isPresented: $addLink) {
SendToAllView(tournament: tournament, addLink: true)
SendToAllView(addLink: true)
.tint(.master)
}
}

@ -60,10 +60,9 @@ struct CallView: View {
@State var showUserCreationView: Bool = false
@State var summonParamByMessage: Bool = false
@State var summonParamReSummon: SummonType = .contact
@State var summonParamReSummon: Bool = false
let simpleMode : Bool
let summonType: SummonType
init(teams: [TeamRegistration], callDate: Date, matchFormat: MatchFormat, roundLabel: String) {
self.teams = teams
@ -72,7 +71,6 @@ struct CallView: View {
self.roundLabel = roundLabel
self.simpleMode = false
self.displayContext = .footer
self.summonType = .contact
}
init(teams: [TeamRegistration]) {
@ -82,14 +80,12 @@ struct CallView: View {
self.roundLabel = ""
self.simpleMode = true
self.displayContext = .footer
self.summonType = .contact
}
init(team: TeamRegistration, displayContext: SummoningDisplayContext, summonType: SummonType) {
init(team: TeamRegistration, displayContext: SummoningDisplayContext) {
self.teams = [team]
let expectedSummonDate = team.expectedSummonDate()
self.displayContext = displayContext
self.summonType = summonType
if let expectedSummonDate, let initialMatch = team.initialMatch() {
self.callDate = expectedSummonDate
@ -101,11 +97,6 @@ struct CallView: View {
self.matchFormat = initialGroupStage.matchFormat
self.roundLabel = "poule"
self.simpleMode = false
} else if let expectedSummonDate {
self.callDate = expectedSummonDate
self.matchFormat = MatchFormat.nineGames
self.roundLabel = ""
self.simpleMode = false
} else {
self.callDate = Date()
self.matchFormat = MatchFormat.nineGames
@ -135,7 +126,7 @@ struct CallView: View {
if success {
calledTeams.forEach { team in
team.callDate = callDate
if summonType.shouldConfirm() {
if reSummon {
team.confirmationDate = nil
}
}
@ -147,26 +138,21 @@ struct CallView: View {
}
}
func finalMessage(summonType: SummonType, forcedEmptyMessage: Bool) -> String {
if summonType == .contactWithoutSignature {
return ""
}
func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String {
if simpleMode || forcedEmptyMessage {
let signature = dataStore.user.getSummonsMessageSignature() ?? dataStore.user.defaultSignature(tournament)
return "\n\n\n\n" + signature
}
return ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, summonType: summonType)
return ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon)
}
var reSummon: Bool {
if simpleMode {
return false
}
return self.teams.allSatisfy({ $0.called() })
}
//
// var summonType: SummonType {
// if simpleMode {
// return .contact
// }
// return self.teams.allSatisfy({ $0.called() }) == true ? .summon : .summonWalkoutFollowUp
// }
var mainWord: String {
if simpleMode {
@ -277,7 +263,7 @@ struct CallView: View {
LoginView(reason: LoginReason.loginRequiredForFeature) { _ in
self.showUserCreationView = false
self._summon(byMessage: self.summonParamByMessage,
summonType: self.summonType)
reSummon: self.summonParamByMessage)
}
}
})
@ -285,7 +271,7 @@ struct CallView: View {
private func _footerStyleView() -> some View {
HStack {
let callWord : String = (summonType.isRecall() ? "Reconvoquer" : mainWord)
let callWord : String = (reSummon ? "Reconvoquer" : mainWord)
if self.teams.count == 1 {
if simpleMode {
Text("\(callWord) cette paire par")
@ -313,16 +299,19 @@ struct CallView: View {
self._summonMenu(byMessage: true)
self._summonMenu(byMessage: false)
} label: {
VStack(alignment: .leading) {
let callWord : String = summonType.mainWord()
if self.teams.count == 1 {
let callWord : String = (reSummon ? "Reconvoquer" : mainWord)
if self.teams.count == 1 {
if simpleMode {
Text("\(callWord) cette paire")
} else {
Text("\(callWord) ces \(self.teams.count) paires")
}
if let caption = summonType.caption() {
Text(caption).foregroundStyle(.secondary).font(.caption)
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer \(self.callDate.localizedDate())")
} else {
Text("\(callWord) cette paire")
}
}
} else {
Text("\(callWord) ces \(self.teams.count) paires")
}
}
}
@ -330,45 +319,21 @@ struct CallView: View {
@ViewBuilder
private func _summonMenu(byMessage: Bool) -> some View {
if displayContext == .menu {
Button {
switch summonType {
case .contact:
self._summon(byMessage: byMessage, summonType: .contact, forcedEmptyMessage: true)
case .summon:
self._summon(byMessage: byMessage, summonType: .summon)
case .summonWalkoutFollowUp:
self._summon(byMessage: byMessage, summonType: .summonWalkoutFollowUp)
case .summonErrorFollowUp:
self._summon(byMessage: byMessage, summonType: .summonErrorFollowUp)
case .contactWithoutSignature:
self._summon(byMessage: byMessage, summonType: .contactWithoutSignature, forcedEmptyMessage: true)
}
} label: {
Text(byMessage ? "sms" : "mail")
.underline()
}
} else if summonType.isRecall() {
if self.reSummon {
Menu {
Button(mainWord) {
self._summon(byMessage: byMessage, summonType: simpleMode ? .contact : .summon)
}
Button("Re-convoquer suite à des forfaits") {
self._summon(byMessage: byMessage, summonType: .summonWalkoutFollowUp)
self._summon(byMessage: byMessage, reSummon: false)
}
Button("Re-convoquer suite à une erreur") {
self._summon(byMessage: byMessage, summonType: .summonErrorFollowUp)
Button("Re-convoquer") {
self._summon(byMessage: byMessage, reSummon: true)
}
Divider()
Button("Contacter") {
self._summon(byMessage: byMessage, summonType: .contact, forcedEmptyMessage: true)
}
Button("Contacter sans texte par défaut") {
self._summon(byMessage: byMessage, summonType: .contactWithoutSignature, forcedEmptyMessage: true)
if simpleMode == false {
Divider()
Button("Contacter") {
self._summon(byMessage: byMessage, reSummon: false, forcedEmptyMessage: true)
}
}
} label: {
@ -377,19 +342,19 @@ struct CallView: View {
}
} else {
FooterButtonView(byMessage ? "sms" : "mail") {
self._summon(byMessage: byMessage, summonType: summonType)
self._summon(byMessage: byMessage, reSummon: false)
}
}
}
private func _summon(byMessage: Bool, summonType: SummonType, forcedEmptyMessage: Bool = false) {
private func _summon(byMessage: Bool, reSummon: Bool, forcedEmptyMessage: Bool = false) {
self.summonParamByMessage = byMessage
self.summonParamReSummon = summonType
self.summonParamReSummon = reSummon
self._verifyUser {
if byMessage {
self._contactByMessage(summonType: summonType, forcedEmptyMessage: forcedEmptyMessage)
self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
} else {
self._contactByMail(summonType: summonType, forcedEmptyMessage: forcedEmptyMessage)
self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
}
}
}
@ -411,18 +376,18 @@ struct CallView: View {
// }
// }
fileprivate func _contactByMessage(summonType: SummonType, forcedEmptyMessage: Bool) {
fileprivate func _contactByMessage(reSummon: Bool, forcedEmptyMessage: Bool) {
self.contactType = .message(date: callDate,
recipients: teams.flatMap { $0.getPhoneNumbers() },
body: finalMessage(summonType: summonType, forcedEmptyMessage: forcedEmptyMessage),
body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
tournamentBuild: nil)
}
fileprivate func _contactByMail(summonType: SummonType, forcedEmptyMessage: Bool) {
fileprivate func _contactByMail(reSummon: Bool, forcedEmptyMessage: Bool) {
self.contactType = .mail(date: callDate,
recipients: tournament.umpireMail(),
bccRecipients: teams.flatMap { $0.getMail() },
body: finalMessage(summonType: summonType, forcedEmptyMessage: forcedEmptyMessage),
body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
subject: tournament.mailSubject(),
tournamentBuild: nil)
}

@ -43,8 +43,7 @@ struct MenuWarningView: View {
}
}
} label: {
Label("Prévenir", systemImage: "phone")
.labelStyle(.iconOnly)
Text("Prévenir")
}
.menuStyle(.button)
.buttonStyle(.borderedProminent)

@ -14,7 +14,7 @@ struct PlayersWithoutContactView: View {
var body: some View {
Section {
let withoutEmails = players.filter({ $0.hasMail() == false })
let withoutEmails = players.filter({ $0.email?.isEmpty == true || $0.email == nil })
DisclosureGroup {
ForEach(withoutEmails) { player in
NavigationLink {
@ -32,7 +32,7 @@ struct PlayersWithoutContactView: View {
}
}
let withoutPhones = players.filter({ $0.hasMobilePhone() == false })
let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false })
DisclosureGroup {
ForEach(withoutPhones) { player in
NavigationLink {
@ -46,7 +46,7 @@ struct PlayersWithoutContactView: View {
LabeledContent {
Text(withoutPhones.count.formatted())
} label: {
Text(Locale.current.region?.identifier == "FR" ? "Joueurs sans téléphone portable français" : "Joueurs sans téléphone")
Text("Joueurs sans téléphone portable français")
}
}
} header: {
@ -54,4 +54,3 @@ struct PlayersWithoutContactView: View {
}
}
}

@ -14,16 +14,14 @@ struct SendToAllView: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var networkMonitor: NetworkMonitor
@State private var contactType: ContactType? = nil
@State private var contactMethod: Int = 1
@State private var contactRecipients: Set<String> = Set()
@State private var sentError: ContactManagerError? = nil
var event: Event?
var tournament: Tournament?
var addLink: Bool
let addLink: Bool
// @State var cannotPayForTournament: Bool = false
@State private var pageLink: PageLink = .matches
@State private var includeWaitingList: Bool = false
@ -35,19 +33,8 @@ struct SendToAllView: View {
@State var summonParamByMessage: Bool = false
@State var summonParamReSummon: Bool = false
init(event: Event) {
self.event = event
self.addLink = false
_contactRecipients = .init(wrappedValue: Set(event.confirmedTournaments().map(\.id)))
}
init(tournament: Tournament, addLink: Bool) {
self.tournament = tournament
self.addLink = addLink
}
var tournamentStore: TournamentStore? {
return self.tournament?.tournamentStore
return self.tournament.tournamentStore
}
var messageSentFailed: Binding<Bool> {
@ -73,52 +60,40 @@ struct SendToAllView: View {
.labelsHidden()
.pickerStyle(.inline)
}
if let event {
LabeledContent {
Text(event.selectedTeams().filter({ contactRecipients.isEmpty || contactRecipients.contains($0.tournament) }).count.formatted())
} label: {
Text("Participants")
}
let confirmedTournaments = event.confirmedTournaments()
ForEach(confirmedTournaments) { tournament in
TournamentCellView(tournament: tournament).tag(tournament.id)
}
} else if let tournament {
Section {
ForEach(tournament.groupStages()) { groupStage in
let teams = groupStage.teams()
if teams.isEmpty == false {
LabeledContent {
Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix)
} label: {
Text(groupStage.groupStageTitle())
}
.tag(groupStage.id)
Section {
ForEach(tournament.groupStages()) { groupStage in
let teams = groupStage.teams()
if teams.isEmpty == false {
LabeledContent {
Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix)
} label: {
Text(groupStage.groupStageTitle())
}
.tag(groupStage.id)
}
ForEach(tournament.rounds()) { round in
let teams = round.teams()
if teams.isEmpty == false {
LabeledContent {
Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix)
} label: {
Text(round.roundTitle())
}
.tag(round.id)
}
ForEach(tournament.rounds()) { round in
let teams = round.teams()
if teams.isEmpty == false {
LabeledContent {
Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix)
} label: {
Text(round.roundTitle())
}
.tag(round.id)
}
}
Toggle("Inclure la liste d'attente", isOn: $includeWaitingList)
if includeWaitingList {
Toggle("Seulement la liste d'attente", isOn: $onlyWaitingList)
}
} footer: {
Text("Si vous ne souhaitez pas contacter toutes les équipes, choisissez un ou plusieurs groupes d'équipes manuellement.")
Toggle("Inclure la liste d'attente", isOn: $includeWaitingList)
if includeWaitingList {
Toggle("Seulement la liste d'attente", isOn: $onlyWaitingList)
}
} footer: {
Text("Si vous ne souhaitez pas contacter toutes les équipes, choisissez un ou plusieurs groupes d'équipes manuellement.")
}
if addLink, event == nil {
if addLink {
Section {
let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings]
Picker(selection: $pageLink) {
@ -160,7 +135,7 @@ struct SendToAllView: View {
Button("OK") {
}
if case .uncalledTeams(let uncalledTeams) = sentError, let tournament {
if case .uncalledTeams(let uncalledTeams) = sentError {
NavigationLink("Voir les équipes non contactées") {
TeamsCallingView(teams: uncalledTeams)
.environment(tournament)
@ -249,11 +224,6 @@ struct SendToAllView: View {
}
func _teams() -> [TeamRegistration] {
if let event {
return event.selectedTeams().filter({ contactRecipients.isEmpty || contactRecipients.contains($0.tournament) })
}
guard let tournament else { return [] }
let selectedSortedTeams = tournament.selectedSortedTeams()
if onlyWaitingList {
return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams)
@ -288,10 +258,8 @@ struct SendToAllView: View {
func finalMessage() -> String {
var message = [String?]()
message.append("\n\n")
if let tournament, addLink, event == nil {
if addLink {
message.append(tournament.shareURL(pageLink)?.absoluteString)
} else if let event {
message.append(event.shareURL()?.absoluteString)
}
let signature = dataStore.user.getSummonsMessageSignature() ?? dataStore.user.defaultSignature(tournament)
@ -305,11 +273,9 @@ struct SendToAllView: View {
self._verifyUser {
if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.phoneNumber, $0.contactPhoneNumber] }.compactMap({ $0 }), body: finalMessage(), tournamentBuild: nil)
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil)
} else {
let umpireMail = tournament?.umpireMail() ?? event?.umpireMail()
let subject = tournament?.mailSubject() ?? event?.mailSubject()
contactType = .mail(date: nil, recipients: umpireMail, bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.email, $0.contactEmail] }.compactMap({ $0 }), body: finalMessage(), subject: subject, tournamentBuild: nil)
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil)
}
}

@ -129,28 +129,21 @@ struct CallMenuOptionsView: View {
@Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration
let action: (() -> Void)?
@State private var callDate: Date
init(team: TeamRegistration, action: (() -> Void)? = nil) {
self.team = team
self.action = action
_callDate = .init(wrappedValue: team.expectedSummonDate() ?? team.tournamentObject()?.startDate ?? Date())
}
var confirmed: Binding<Bool> {
Binding {
team.confirmed()
} set: { _ in
team.toggleSummonConfirmation()
_save()
do {
try self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?()
}
}
private func _save() {
self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
}
var body: some View {
List {
Section {
@ -158,36 +151,11 @@ struct CallMenuOptionsView: View {
Toggle(isOn: confirmed) {
Text("Confirmation reçue")
}
DatePicker(selection: $callDate) {
if callDate != team.expectedSummonDate() {
HStack {
Button("Valider", systemImage: "checkmark.circle") {
team.callDate = callDate
_save()
}
.tint(.green)
Divider()
Button("Annuler", systemImage: "xmark.circle", role: .cancel) {
callDate = team.expectedSummonDate() ?? tournament.startDate
}
.tint(.logoRed)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderedProminent)
} else {
Text("Heure de convocation")
}
}
if team.expectedSummonDate() != nil {
CallView(team: team, displayContext: .menu, summonType: .summon)
CallView(team: team, displayContext: .menu, summonType: .summonWalkoutFollowUp)
CallView(team: team, displayContext: .menu, summonType: .summonErrorFollowUp)
CallView(team: team, displayContext: .menu)
}
CallView(team: team, displayContext: .menu, summonType: .contact)
CallView(team: team, displayContext: .menu, summonType: .contactWithoutSignature)
} footer: {
CallView(teams: [team])
}
Section {
@ -202,7 +170,11 @@ struct CallMenuOptionsView: View {
Section {
RowButtonView("Effacer la date de convocation", role: .destructive) {
team.callDate = nil
_save()
do {
try self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?()
dismiss()
}
@ -211,7 +183,11 @@ struct CallMenuOptionsView: View {
Section {
RowButtonView("Indiquer comme convoquée", role: .destructive) {
team.callDate = team.initialMatch()?.startDate ?? tournament.startDate
_save()
do {
try self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?()
dismiss()
}

@ -18,7 +18,7 @@ struct ShareableObject {
func sharedData() async -> Data? {
let _players = players.filter({ cashierViewModel._shouldDisplayPlayer($0) })
.map {
[$0.pasteData(type: .payment)]
[$0.pasteData()]
.compacted()
.joined(separator: "\n")
}
@ -51,7 +51,6 @@ class CashierViewModel: ObservableObject {
@Published var sortOrder: PadelClubData.SortOrder = .ascending
@Published var searchText: String = ""
@Published var isSearching: Bool = false
@Published var paymentType: PlayerPaymentType? = nil
func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {
team.unsortedPlayers().anySatisfy({
@ -66,7 +65,6 @@ class CashierViewModel: ObservableObject {
sortOption.shouldDisplayPlayer(player)
&& filterOption.shouldDisplayPlayer(player)
&& presenceFilterOption.shouldDisplayPlayer(player)
&& (paymentType == nil || player.paymentType == paymentType)
}
}
@ -263,16 +261,6 @@ struct CashierView: View {
} label: {
Text("Statut du règlement")
}
Picker(selection: $cashierViewModel.paymentType) {
Text("N'importe").tag(nil as PlayerPaymentType?)
ForEach(PlayerPaymentType.allCases) { paymentType in
Text(paymentType.localizedLabel()).tag(paymentType)
}
} label: {
Text("Type de règlement")
}
}
Picker(selection: $cashierViewModel.sortOption) {

@ -16,7 +16,6 @@ struct EventSettingsView: View {
@State private var pageLink: PageLink = .teams
@State private var tournamentInformation: String = ""
@State private var eventStartDate: Date
@State private var showSendToAllView: Bool = false
@FocusState private var focusedField: Tournament.CodingKeys?
var visibleOnPadelClub: Binding<Bool> {
@ -91,14 +90,6 @@ struct EventSettingsView: View {
}
}
_linkLabel()
Section {
RowButtonView("Contactez toutes les équipes") {
showSendToAllView = true
}
}
Section {
DatePicker(selection: $eventStartDate) {
Text(eventStartDate.formatted(.dateTime.weekday(.wide)).capitalized).lineLimit(1)
@ -179,10 +170,6 @@ struct EventSettingsView: View {
}
}
}
.sheet(isPresented: $showSendToAllView) {
SendToAllView(event: event)
.tint(.master)
}
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
@ -195,6 +182,10 @@ struct EventSettingsView: View {
})
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
_linkLabel()
}
if focusedField != nil {
ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._name, eventName.isEmpty == false {
@ -284,7 +275,7 @@ struct EventSettingsView: View {
}
}
} label: {
Text("Liens à partager")
Text("Liens")
}
}

@ -76,7 +76,6 @@ enum EventDestination: Identifiable, Selectable, Equatable {
struct EventView: View {
let event: Event
@State private var selectedDestination: EventDestination?
@State private var presentSearchView: Bool = false
init(event: Event) {
self.event = event
@ -111,16 +110,6 @@ struct EventView: View {
}
}
}
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) {
Button("Recherche", systemImage: "magnifyingglass") {
presentSearchView = true
}
}
})
.sheet(isPresented: $presentSearchView, content: {
PlayerSearchView(event: event)
})
.headerProminence(.increased)
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)

@ -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,6 +106,7 @@ struct ClubsView: View {
}
}
.navigationTitle(selection == nil ? "Clubs favoris" : "Choisir un club")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.sheet(isPresented: presentClubCreationView) {
if let newClub {
@ -128,41 +129,23 @@ struct ClubsView: View {
.tint(.master)
}
.toolbar {
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)
}
ToolbarItemGroup(placement: .topBarTrailing) {
Button {
presentClubSearchView = true
} label: {
Image(systemName: "magnifyingglass.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)
}
Button {
newClub = Club.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
}
}

@ -157,7 +157,6 @@ struct SpinDrawView: View {
ToolbarItem(placement: .status) {
Text("Tous les tirages sont terminés")
.frame(maxWidth: .infinity)
}
}
}
@ -221,7 +220,6 @@ 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) {

@ -94,7 +94,7 @@ struct StepperView: View {
}
fileprivate func _plusIsDisabled() -> Bool {
count >= (maximum ?? 92_327)
count >= (maximum ?? 90_415)
}
fileprivate func _add() {

@ -96,9 +96,6 @@ struct GroupStageView: View {
}
}
.onAppear(perform: {
groupStage.clearScoreCache()
})
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
_groupStageMenuView()
@ -245,6 +242,7 @@ 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() })

@ -65,6 +65,7 @@ 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 {

@ -113,8 +113,6 @@ struct ActivityView: View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.path) {
VStack(spacing: 0) {
Color.clear.frame(height: 1)
.background(Material.ultraThinMaterial)
GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false)
ScrollViewReader { proxy in
@ -548,7 +546,7 @@ struct ActivityView: View {
.frame(width: 100)
}
} description: {
Text("Aucun événement dans votre agenda.")
Text("Aucun événement en cours ou à venir dans votre agenda.")
} actions: {
RowButtonView("Créer un nouvel événement") {
newTournament = Tournament.newEmptyInstance()

@ -37,15 +37,7 @@ struct EventListView: View {
if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate }
) {
Section {
if sectionImporting == sectionIndex {
LabeledContent {
ProgressView()
} label: {
Text("Récupération en cours")
}
} else {
_listView(_tournaments)
}
_listView(_tournaments)
} header: {
HStack {
Text(section.monthYearFormatted)
@ -58,16 +50,21 @@ struct EventListView: View {
if let pcTournaments = _tournaments as? [Tournament] {
_menuOptions(pcTournaments)
} else if let federalTournaments = _tournaments as? [FederalTournament], navigation.agendaDestination == .tenup {
FooterButtonView("Tout récupérer", role: .destructive) {
Task {
sectionImporting = sectionIndex
for federalTournament in federalTournaments {
await _importFederalTournamentBatch(federalTournament: federalTournament)
HStack {
FooterButtonView("Tout récupérer", role: .destructive) {
Task {
sectionImporting = sectionIndex
for federalTournament in federalTournaments {
await _importFederalTournamentBatch(federalTournament: federalTournament)
}
sectionImporting = nil
}
sectionImporting = nil
}
if sectionImporting == sectionIndex {
Spacer()
ProgressView()
}
}
.disabled(sectionImporting == sectionIndex)
}
}
}
@ -376,18 +373,6 @@ struct EventListView: View {
} label: {
Text("Terminer les tournois encore ouverts")
}
#if DEBUG
Button {
Task {
await pcTournaments.concurrentForEach { tournament in
dataStore.deleteTournament(tournament)
}
}
} label: {
Text("(DEBUG) Tout effacer")
}
#endif
} label: {
Text("Options avancées")
}
@ -463,7 +448,17 @@ struct EventListView: View {
Divider()
if tournament.hasEnded() == false {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Afficher dans le gestionnaire", systemImage: "line.diagonal.arrow")
}
Divider()
_options([tournament])
}
}
#if DEBUG

@ -80,28 +80,27 @@ 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)
MyAccountView()
.tabItem(for: .myAccount)
.toolbarBackground(.visible, for: .tabBar)
UmpireView()
.tabItem(for: .umpire)
.badge(badgeText)
.toolbarBackground(.visible, for: .tabBar)
// PadelClubView()
// .tabItem(for: .padelClub)
}
.applyTabViewBottomAccessory(isVisible: (navigation.selectedTab == .activity || navigation.selectedTab == nil) && _shouldDisplaySearchStatus(), content: {
_searchBoxView()
.applyTabViewBottomAccessory(content: {
if (navigation.selectedTab == .activity || navigation.selectedTab == nil) && _shouldDisplaySearchStatus() {
_searchBoxView()
}
})
.sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel)
@ -355,10 +354,10 @@ struct MainView: View {
fileprivate extension View {
@ViewBuilder
func applyTabViewBottomAccessory<Content: View>(isVisible: Bool,
func applyTabViewBottomAccessory<Content: View>(
@ViewBuilder content: () -> Content
) -> some View {
if #available(iOS 26.0, *), isVisible {
if #available(iOS 26.0, *) {
self.tabViewBottomAccessory {
content()
}

@ -1,226 +0,0 @@
//
// 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)
}
}
}
}
}

@ -16,11 +16,6 @@ class OngoingViewModel {
var destination: OngoingDestination? = .running
var hideUnconfirmedMatches: Bool = false
var hideNotReadyMatches: Bool = false
var selectedTournaments: Set<String> = Set()
func tournaments() -> [Tournament] {
Set(DataStore.shared.runningAndNextMatches().compactMap({ $0.currentTournament() })).sorted(by: \.startDate)
}
func areFiltersEnabled() -> Bool {
hideUnconfirmedMatches || hideNotReadyMatches
@ -29,7 +24,7 @@ class OngoingViewModel {
let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.startDate!), .keyPath(\Match.index), .keyPath(\Match.courtIndexForSorting)]
var runningAndNextMatches: [Match] {
DataStore.shared.runningAndNextMatches(selectedTournaments).sorted(using: defaultSorting, order: .ascending)
DataStore.shared.runningAndNextMatches().sorted(using: defaultSorting, order: .ascending)
}
var filteredRunningAndNextMatches: [Match] {
@ -62,36 +57,32 @@ struct OngoingContainerView: View {
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Programmation")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Menu {
NavigationLink {
List(selection: $ongoingViewModel.selectedTournaments) {
ForEach(ongoingViewModel.tournaments()) { tournament in
TournamentCellView(tournament: tournament)
.tag(tournament.id)
}
}
.onChange(of: ongoingViewModel.selectedTournaments, { oldValue, newValue in
DataStore.shared.resetOngoingCache()
})
.environment(\.editMode, Binding.constant(EditMode.active))
.navigationTitle("Tournois à masquer")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
} label: {
Label("Masquer des tournois", systemImage: "circle.grid.2x2.topleft.checkmark.filled")
}
if ongoingViewModel.destination == .followUp {
Divider()
if ongoingViewModel.destination == .followUp {
ToolbarItem(placement: .topBarLeading) {
Menu {
Toggle(isOn: $ongoingViewModel.hideUnconfirmedMatches) {
Text("Masquer non confirmés")
Text("masquer non confirmés")
}
Toggle(isOn: $ongoingViewModel.hideNotReadyMatches) {
Text("Masquer incomplets")
Text("masquer incomplets")
}
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.symbolVariant(ongoingViewModel.areFiltersEnabled() ? .fill : .none)
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
showMatchPicker = true
} label: {
Image(systemName: "line.3.horizontal.decrease")
Image(systemName: "rectangle.stack.badge.plus")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
}
}

@ -21,10 +21,6 @@ struct DebugSettingsView: View {
LabeledContent("Has Websocket Manager", value: self._hasWebSocketManager)
LabeledContent("Websocket ping", value: self._wsPingStatus)
LabeledContent("Websocket failure", value: self._wsFailure)
if let error = self._wsError {
LabeledContent("Websocket error", value: error)
LabeledContent("Reconnect attempts", value: StoreCenter.main.websocketReconnectAttempts.formatted())
}
LabeledContent("Last synced object date", value: self._lastSyncDate)
if isSynchronizing {
HStack {
@ -98,12 +94,6 @@ struct DebugSettingsView: View {
fileprivate var _wsFailure: String {
return "\(StoreCenter.main.websocketFailure)"
}
fileprivate var _wsError: String? {
if let error = StoreCenter.main.websocketError {
return error.localizedDescription
}
return nil
}
fileprivate var _hasWebSocketManager: String {
return "\(StoreCenter.main.hasWebSocketManager)"
}

@ -21,7 +21,6 @@ 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
@ -40,6 +39,44 @@ 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(supportButtonType: .bugReport)
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)
.toolbar(.hidden, for: .tabBar)
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
NavigationLink {
RankCalculatorView()
} label: {
Label("Calculateur de points", systemImage: "scalemass")
}
}
Section {
NavigationLink {
PadelClubView()
@ -49,7 +86,8 @@ struct ToolboxView: View {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} label: {
Label(_lastDataSourceDate.monthYearFormatted, systemImage: "calendar.badge.checkmark")
Text(_lastDataSourceDate.monthYearFormatted)
Text("Classement mensuel utilisé")
}
} else {
LabeledContent {
@ -57,50 +95,14 @@ struct ToolboxView: View {
.tint(.logoRed)
} label: {
if let _mostRecentDateAvailable {
Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "calendar.badge")
Text(_mostRecentDateAvailable.monthYearFormatted)
} else {
Label("Aucun", systemImage: "calendar.badge.exclamationmark")
Text("Aucun")
}
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")
}
NavigationLink {
MatchFormatGuideView()
} label: {
Label("Formats et limites", systemImage: "clock")
}
}
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 {
@ -117,8 +119,19 @@ struct ToolboxView: View {
}
}
if self.showDebugViews {
DebugView()
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")
}
}
Section {
@ -128,19 +141,6 @@ struct ToolboxView: View {
}
}
}
.fullScreenCover(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
@ -163,9 +163,16 @@ 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)")
@ -173,16 +180,15 @@ struct ToolboxView: View {
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
ShareLink(item: ZipLog(), preview: .init("Mon archive")) {
Text("Archiver mes données")
ShareLink(item: URLs.appStore.url) {
Label("Lien AppStore", systemImage: "link")
}
Divider()
Toggle("Outils avancées", isOn: $showDebugViews)
ShareLink(item: ZipLog(), preview: .init("Mon archive")) {
Label("Mes données", systemImage: "server.rack")
}
} label: {
LabelOptions()
Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
}
}
}
@ -239,6 +245,66 @@ 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)
}
}
}
}
}
}
}

@ -160,7 +160,7 @@ struct PadelClubView: View {
if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted())
} else {
Text(92_327.formatted())
Text(90_415.formatted())
}
} label: {
Text("Rang d'un non classé")

@ -1,68 +0,0 @@
//
// 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
}
}

@ -1,85 +0,0 @@
//
// 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,6 +17,8 @@ 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
@ -36,9 +38,50 @@ struct UmpireView: View {
_umpireCustomContact = State(wrappedValue: DataStore.shared.user.umpireCustomContact ?? "")
}
enum UmpireScreen {
case login
}
var body: some View {
List {
if StoreCenter.main.isAuthenticated {
@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)
}
} else {
NavigationLink(value: UmpireScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
let currentPlayerData = dataStore.user.currentPlayerData()
Section {
if let reason = licenseMessage {
@ -47,11 +90,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
@ -63,8 +106,6 @@ 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.")
@ -91,119 +132,231 @@ struct UmpireView: View {
}
}
}
_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)
NavigationLink {
ClubsView()
} label: {
LabeledContent {
Text(dataStore.user.clubs.count.formatted())
} label: {
Label("Clubs favoris", systemImage: "house.and.flag")
}
}
} 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()
Toggle(isOn: $user.hideUmpireMail) {
Text("Masquer l'email")
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.")
}
Toggle(isOn: $user.hideUmpirePhone) {
Text("Masquer le téléphone")
}
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")
}
.onChange(of: user.disableRankingFederalRuling) {
dataStore.saveUser()
}
} header: {
Text("Règle fédérale classement finale")
} 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.")
Text("Dernier de poule ≠ dernier du tournoi")
}
}
}
.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()
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")
}
}
})
.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
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.")
}
// 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
}
})
.toolbar {
if focusedField != nil {
ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Effacer") {
_deleteUmpireMail()
.navigationTitle("Juge-Arbitre")
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") {
_deleteUmpirePhone()
}
}
})
.toolbar {
if focusedField != nil {
ToolbarItemGroup(placement: .keyboard) {
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)
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") {
_deleteUmpireContact()
Spacer()
Button("Valider") {
focusedField = nil
}
.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()
}
}
.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)
.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)
}
user.setUserClub(userClub)
self._updateUserLicense(license: player.license?.computedLicense)
}
self._updateUserLicense(license: player.license?.computedLicense)
})
}
.task {
do {
try await dataStore.clubs.loadDataFromServerIfAllowed()
} catch {
Logger.error(error)
}
})
}
}
.task {
do {
try await dataStore.clubs.loadDataFromServerIfAllowed()
} catch {
Logger.error(error)
.navigationDestination(for: UmpireScreen.self) { screen in
switch screen {
case .login:
LoginView {_ in }
}
}
}
@ -221,10 +374,6 @@ struct UmpireView: View {
}
private func _openCreateAccountView() {
navigation.selectedTab = .myAccount
}
private func _updateUserLicense(license: String?) {
guard let license else { return }
@ -332,10 +481,67 @@ struct UmpireView: View {
} }
} header: {
Text("Mes infos juge-arbitre")
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.")
}
}
}
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()
//}

@ -56,18 +56,6 @@ 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: {

@ -27,14 +27,10 @@ struct PlanningView: View {
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>, event: Event? = nil) {
self.event = event
if let event {
let allMatches = event.confirmedTournaments().flatMap { $0.allMatches() }
self.allMatches = allMatches
_showFinishedMatches = .init(wrappedValue: !allMatches.anySatisfy({ $0.hasEnded() == false && $0.plannedStartDate != nil }))
self.allMatches = event.confirmedTournaments().flatMap { $0.allMatches() }
} else {
self.allMatches = matches
_showFinishedMatches = .init(wrappedValue: !matches.anySatisfy({ $0.hasEnded() == false && $0.plannedStartDate != nil }))
}
_selectedScheduleDestination = selectedScheduleDestination
}
@ -88,7 +84,7 @@ struct PlanningView: View {
let keys = self.keys(timeSlots: timeSlots)
let days = self.days(timeSlots: timeSlots)
let matches = matches
let notSlots = matches.allSatisfy({ $0.plannedStartDate == nil })
let notSlots = matches.allSatisfy({ $0.startDate == nil })
BySlotView(
days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay
)
@ -154,7 +150,7 @@ struct PlanningView: View {
Button {
_planEvent(event: event)
} label: {
Text("Tout planifier")
Text("Planifier")
}
} label: {
Text("Planifier l'événement")
@ -172,28 +168,29 @@ struct PlanningView: View {
.popoverTip(timeSlotMoveOptionTip)
.disabled(_confirmationMode())
Toggle(isOn: enableEditionBinding) {
Label("Modifier un horaire", systemImage: "clock.arrow.trianglehead.2.counterclockwise.rotate.90")
Text("Modifier un horaire")
}
.disabled(_confirmationMode())
}
Divider()
Section {
Picker(selection: $showFinishedMatches) {
Label("Afficher tous les matchs", systemImage: "eye").tag(true)
Label("Masquer les matchs terminés", systemImage: "eye.slash").tag(false)
} label: {
Menu {
Section {
Picker(selection: $showFinishedMatches) {
Text("Afficher tous les matchs").tag(true)
Text("Masquer les matchs terminés").tag(false)
} label: {
Text("Option de filtrage")
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de filtrage")
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de filtrage")
}
Divider()
Menu {
Divider()
Section {
Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) {
@ -209,14 +206,14 @@ struct PlanningView: View {
}
} label: {
Label("Trier", systemImage: "line.3.horizontal.decrease")
Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(
filterOption == .byCourt || showFinishedMatches ? .fill : .none)
}
Divider()
Button("Mettre à jour", systemImage: "arrow.trianglehead.2.clockwise.rotate.90.icloud") {
Button("Mettre à jour", systemImage: "arrow.trianglehead.2.clockwise.rotate.90.circle") {
let now = Date()
matches.forEach {
if let startDate = $0.startDate, startDate > now {
@ -241,20 +238,7 @@ struct PlanningView: View {
}
})
.overlay {
if notSlots, showFinishedMatches == false, self.allMatches.isEmpty == false {
ContentUnavailableView {
Label("Aucun match à jouer", systemImage: "clock.badge.checkmark")
} description: {
Text(
"Tous les matchs plannifiés sont terminés."
)
} actions: {
RowButtonView("Afficher tous les matchs") {
showFinishedMatches = true
}
}
} else if notSlots {
if notSlots {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {
@ -331,9 +315,8 @@ struct PlanningView: View {
matchesToUpdate = matches.filter({ selectedIds.contains($0.stringId) })
} label: {
Text("Modifier")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.buttonStyle(.borderless)
.disabled(selectedIds.isEmpty)
}
}
@ -517,7 +500,6 @@ struct PlanningView: View {
}
CourtOptionsView(timeSlots: timeSlots, underlined: true)
.labelStyle(.titleOnly)
}
}
.onChange(of: selectAll, { oldValue, newValue in
@ -745,6 +727,9 @@ 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()
@ -764,26 +749,15 @@ struct PlanningView: View {
Button("Tirer au sort") {
_removeCourts()
let eventCourtCount = _eventCourtCount()
for slot in timeSlots {
var courtsAvailable = Array(0...eventCourtCount)
let matches = slot.value
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() {
matches.forEach { match in
if let rand = courtsAvailable.randomElement() {
match.courtIndex = rand
// Remove from local copy and assign back into the dictionary
courts.remove(rand)
courtsByTournament[tournament.id] = courts
courtsAvailable.remove(elements: [rand])
}
}
}
@ -794,33 +768,22 @@ 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 {
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()
if !courtsAvailable.isEmpty {
let court = courtsAvailable.removeFirst()
matches[i].courtIndex = court
// Remove from local copy and assign back into the dictionary
courtsByTournament[tournament.id] = Set(courts)
}
}
}
_save()
}
} label: {
Label("Pistes", systemImage: "123.rectangle")
Text("Pistes")
.underline(underlined)
}
@ -1020,4 +983,3 @@ extension EnvironmentValues {
set { self[EnableMoveKey.self] = newValue }
}
}

@ -30,20 +30,6 @@ struct PlayerDetailView: View {
return self.tournament.tournamentStore
}
var unranked: Binding<Bool> {
Binding {
player.isUnranked()
} set: { isUnranked in
player.rank = nil
player.setComputedRank(in: tournament)
if let team = player.team() {
team.setWeight(from: team.players(), inTournamentCategory: tournament.category)
}
_save()
}
}
init(player: PlayerRegistration) {
self.player = player
_licenceId = .init(wrappedValue: player.licenceId ?? "")
@ -144,11 +130,6 @@ struct PlayerDetailView: View {
}
Section {
Toggle(isOn: unranked) {
Text("Non classé\(player.sex == .female ? "e" : "")")
}
LabeledContent {
TextField("Rang", value: $player.rank, format: .number)
.keyboardType(.decimalPad)
@ -166,7 +147,7 @@ struct PlayerDetailView: View {
}
}
let maxMaleUnrankedValue: Int = tournament.maleUnrankedValue ?? 92_327
let maxMaleUnrankedValue: Int = tournament.maleUnrankedValue ?? 90_415
if player.isMalePlayer() == false && tournament.tournamentCategory == .men && (player.rank == maxMaleUnrankedValue || player.rank == nil) {
Section {
@ -391,7 +372,7 @@ struct PlayerDetailView: View {
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData(type: .sharing)) {
ShareLink(item: player.pasteData()) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}

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

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

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

@ -234,11 +234,7 @@ struct SetInputView: View {
} else if newValue == setFormat.scoreToWin - 2 && setFormat.tieBreak == 8 {
otherTeamValueBinding.wrappedValue = setFormat.scoreToWin
} else if newValue == setFormat.scoreToWin - 1 {
if setFormat == .three {
otherTeamValueBinding.wrappedValue = setFormat.scoreToWin
} else {
otherTeamValueBinding.wrappedValue = setFormat.scoreToWin + 1
}
otherTeamValueBinding.wrappedValue = setFormat.scoreToWin + 1
} else if newValue <= setFormat.scoreToWin - 2 {
otherTeamValueBinding.wrappedValue = setFormat.scoreToWin
} else if newValue > 10 && setFormat == .superTieBreak {

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

@ -502,6 +502,7 @@ struct MySearchView: View {
headerView()
}
}
.id(UUID())
}
} else {
let filteredPlayers = searchedPlayers()

@ -90,15 +90,8 @@ struct SupportButtonView: View {
_zip()
}
case .bugReport:
if showIcon {
Button("Signaler un problème", systemImage: "square.and.pencil") {
_zip()
}
.labelStyle(.titleAndIcon)
} else {
Button("Signaler un problème") {
_zip()
}
Button("Signaler un problème") {
_zip()
}
}
}
@ -154,17 +147,9 @@ struct SupportButtonView: View {
}
private func _zip() {
var urls: [URL] = []
if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
urls.append(dir.appending(path: "appsettings.json"))
urls.append(dir.appending(path: "settings.json"))
urls.append(dir.appending(path: "storage"))
}
do {
// let filePath = try StoreCenter.main.directoryURL()
self.zipFilePath = try Zip.quickZipFiles(urls, fileName: "backup") // Zip
let filePath = try StoreCenter.main.directoryURL()
self.zipFilePath = try Zip.quickZipFiles([filePath], fileName: "backup") // Zip
} catch {
Logger.error(error)
}

@ -31,7 +31,6 @@ struct EditingTeamView: View {
@State private var registrationDateModified: Date
@State private var uniqueRandomIndex: Int
@State private var isDeleting: Bool = false
@State private var showPaymentLinkManager: Bool = false
var messageSentFailed: Binding<Bool> {
Binding {
@ -89,57 +88,8 @@ 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 {
#if PRODTEST
if let pid = team.players().first(where: { $0.paymentId != nil })?.paymentId {
Section {
Text(pid)
} footer: {
CopyPasteButtonView(pasteValue: pid)
}
}
// } else {
// if let paste = UIPasteboard.general.string {
// RowButtonView("Coller le payment id de l'équipe", role: .destructive) {
// let p = team.players()
// p.forEach { player in
// player.paymentId = UIPasteboard.general.string
// player.paymentType = .creditCard
// }
// team.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: p)
// }
// }
// }
// } footer: {
// if let paste = UIPasteboard.general.string {
// Text(paste)
// }
// }
#endif
Section {
NavigationLink {
TeamMatchesView(team: team)
} label: {
Text("Voir les matchs de l'équipe")
}
}
Section {
RowButtonView("Modifier la composition de l'équipe", role: (team.hasRegisteredOnline() || team.hasPaidOnline()) ? .destructive : .none, confirmationMessage: "Vous êtes sur le point de modifier une équipe qui s'est inscrite en ligne.") {
editedTeam = team
@ -153,7 +103,7 @@ struct EditingTeamView: View {
}
} footer: {
HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData(type: .sharing))
CopyPasteButtonView(pasteValue: team.playersPasteData())
Spacer()
if team.isWildCard(), team.unsortedPlayers().isEmpty {
TeamPickerView(pickTypeContext: .wildcard) { teamregistration in
@ -176,10 +126,11 @@ struct EditingTeamView: View {
}
.headerProminence(.increased)
if team.hasRegisteredOnline() || team.hasPaidOnline() || tournament.enableOnlineRegistration {
if team.hasRegisteredOnline() || team.hasPaidOnline() {
Section {
Toggle(isOn: hasRegisteredOnline) {
LabeledContent {
Text(team.hasRegisteredOnline() ? "Oui" : "Non")
} label: {
Text("Inscrits en ligne")
}
@ -191,11 +142,7 @@ struct EditingTeamView: View {
}
if team.hasPaidOnline() == false {
#if PRODTEST
Button("Récupérer le lien de paiement") {
showPaymentLinkManager = true
}
#endif
PaymentRequestButton(teamRegistration: team)
}
}
@ -217,8 +164,6 @@ struct EditingTeamView: View {
} footer: {
if team.hasPaidOnline() {
Text("Le remboursement passe part le service de Stripe qui re-crédite le moyen de paiement utilisé du montant payé.")
} else {
PaymentRequestButton(teamRegistration: team)
}
}
}
@ -245,12 +190,6 @@ struct EditingTeamView: View {
Text("Équipe sur place")
}
}
NavigationLink {
CallMenuOptionsView(team: team)
.environment(tournament)
} label: {
Text("Modifier la convocation")
}
}
Section {
@ -369,11 +308,6 @@ struct EditingTeamView: View {
}
}
}
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
.alert("Attention", isPresented: hasChanged, actions: {
Button("Confirmer") {
if walkOut == false && team.walkOut == true {
@ -463,19 +397,6 @@ struct EditingTeamView: View {
}
.tint(.master)
}
.sheet(isPresented: $showPaymentLinkManager) {
NavigationStack {
PaymentLinkManagerView(teamRegistration: team)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Fermer") {
showPaymentLinkManager = false
}
}
}
}
}
.fullScreenCover(item: $editedTeam) { editedTeam in
NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam)

@ -1,268 +0,0 @@
//
// PaymentLinkManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/10/2025.
//
//
// PaymentLinkManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/10/2025.
//
import SwiftUI
import PadelClubData
struct PaymentLinkManagerView: View {
let teamRegistration: TeamRegistration
@State private var isLoading = false
@State private var showAlert = false
@State private var alertMessage = ""
@State private var paymentLink: String?
@State private var showCopiedConfirmation = false
var body: some View {
VStack(spacing: 20) {
// Header
header
// Get Payment Link Button
getPaymentLinkButton
// Payment Link Display and Actions
if let link = paymentLink {
paymentLinkSection(link: link)
}
Spacer()
}
.padding()
.alert("Erreur", isPresented: $showAlert) {
Button("OK") { }
} message: {
Text(alertMessage)
}
}
// MARK: - ViewBuilder Components
@ViewBuilder
private var header: some View {
VStack(spacing: 8) {
Image(systemName: "creditcard.circle.fill")
.font(.system(size: 50))
.foregroundColor(.blue)
Text("Lien de paiement")
.font(.title2)
.fontWeight(.bold)
Text("Obtenez un lien de paiement à partager avec l'équipe")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
@ViewBuilder
private var getPaymentLinkButton: some View {
Button {
getPaymentLink()
} label: {
HStack {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.tint(.white)
} else {
Image(systemName: "link.circle")
}
Text(paymentLink == nil ? "Obtenir le lien de paiement" : "Régénérer le lien")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(isLoading)
}
@ViewBuilder
private func paymentLinkSection(link: String) -> some View {
VStack(spacing: 16) {
// Success message
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Lien copié dans le presse-papiers!")
.font(.subheadline)
.foregroundColor(.green)
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.background(Color.green.opacity(0.1))
.cornerRadius(8)
// Link display
linkDisplayView(link: link)
// Action buttons
actionButtons(link: link)
}
.transition(.move(edge: .top).combined(with: .opacity))
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: paymentLink)
}
@ViewBuilder
private func linkDisplayView(link: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text("Lien de paiement:")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
ScrollView(.horizontal, showsIndicators: false) {
Text(link)
.font(.system(.caption, design: .monospaced))
.padding(12)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
}
@ViewBuilder
private func actionButtons(link: String) -> some View {
VStack(spacing: 12) {
// Copy button
copyButton(link: link)
// Share button
shareButton(link: link)
// Open in browser button
openInBrowserButton(link: link)
}
}
@ViewBuilder
private func copyButton(link: String) -> some View {
Button {
UIPasteboard.general.string = link
showCopiedConfirmation = true
// Haptic feedback
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
showCopiedConfirmation = false
}
} label: {
HStack {
Image(systemName: showCopiedConfirmation ? "checkmark.circle.fill" : "doc.on.doc.fill")
Text(showCopiedConfirmation ? "Copié !" : "Copier le lien")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding()
.background(showCopiedConfirmation ? Color.green : Color.blue.opacity(0.1))
.foregroundColor(showCopiedConfirmation ? .white : .blue)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(showCopiedConfirmation ? Color.green : Color.blue, lineWidth: 1)
)
}
.disabled(showCopiedConfirmation)
.animation(.easeInOut(duration: 0.2), value: showCopiedConfirmation)
}
@ViewBuilder
private func shareButton(link: String) -> some View {
ShareLink(item: link) {
HStack {
Image(systemName: "square.and.arrow.up.fill")
Text("Partager le lien")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.blue, lineWidth: 1)
)
}
}
@ViewBuilder
private func openInBrowserButton(link: String) -> some View {
Button {
if let url = URL(string: link) {
UIApplication.shared.open(url)
}
} label: {
HStack {
Image(systemName: "safari.fill")
Text("Ouvrir dans Safari")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.blue, lineWidth: 1)
)
}
}
// MARK: - Private Methods
private func getPaymentLink() {
isLoading = true
showCopiedConfirmation = false
Task {
do {
let response = try await PaymentService.getPaymentLink(
teamRegistrationId: teamRegistration.id
)
await MainActor.run {
isLoading = false
if response.success, let link = response.paymentLink {
paymentLink = link
// Automatically copy to clipboard
UIPasteboard.general.string = link
// Haptic feedback
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
} else {
alertMessage = response.message ?? "Impossible d'obtenir le lien de paiement"
showAlert = true
}
}
} catch {
await MainActor.run {
isLoading = false
alertMessage = "Erreur lors de la récupération du lien"
showAlert = true
}
}
}
}
}
#Preview {
PaymentLinkManagerView(teamRegistration: TeamRegistration())
}

@ -16,7 +16,7 @@ struct PaymentRequestButton: View {
@State private var alertMessage = ""
var body: some View {
FooterButtonView("Renvoyer l'email de paiement", role: .destructive, confirmationMessage: "Cette action permet de renvoyer le mail de confirmation de sélection de l'équipe incluant la demande du paiement.") {
Button("Renvoyer email de paiement") {
resendEmail()
}
.disabled(isLoading)

@ -1,52 +0,0 @@
//
// TeamMatchesView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/10/2025.
//
import SwiftUI
import LeStorage
import PadelClubData
struct TeamMatchesView: View {
let team: TeamRegistration
var body: some View {
List {
Section {
TeamRowView(team: team)
}
let followingMatches = team.followingMatches()
if let currentMatch = team.currentMatch() {
Section {
MatchRowView(match: currentMatch)
} header: {
Text("Match en cours")
}
} else if let numbers = team.numberOfRotation(in: followingMatches) {
Section {
Text("Joue dans \(numbers.0) rotation\(numbers.0.pluralSuffix)")
Text("Joue dans \(numbers.1) terrain\(numbers.1.pluralSuffix) disponible\(numbers.1.pluralSuffix)")
} footer: {
Text("Indique dans combien de rotations ou terrains disponible cette équipe est sensée jouer.")
}
}
if followingMatches.isEmpty == false {
Section {
ForEach(followingMatches) { match in
MatchRowView(match: match)
}
} header: {
Text("Tous les matchs")
}
} else {
ContentUnavailableView("Aucun match à venir", systemImage: "calendar.badge.exclamation", description: Text("Il n’y a pas de matchs prévus pour cette équipe."))
}
}
.navigationTitle("Liste des matchs")
}
}

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

@ -231,7 +231,6 @@ struct AddTeamView: View {
.disabled(_limitPlayerCount())
.foregroundStyle(.master)
.labelStyle(.titleAndIcon)
.frame(maxWidth: .infinity)
.buttonBorderShape(.capsule)
}
}

@ -1,240 +0,0 @@
//
// 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
}
if startingRound > 0 {
teamsPerRound = Array(repeating: 0, count: startingRound)
} else {
teamsPerRound = []
}
}
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,25 +9,12 @@ 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)

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

@ -1,51 +0,0 @@
//
// 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()
}
}
}

@ -271,7 +271,9 @@ struct InscriptionManagerView: View {
// await _refreshList(forced: true)
// }
.onAppear {
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
if tournament.enableOnlineRegistration == false || refreshStatus == true {
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
}
.onDisappear {
_handleHashDiff(selectedSortedTeams: selectedSortedTeams)
@ -379,6 +381,40 @@ struct InscriptionManagerView: View {
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()
@ -513,25 +549,11 @@ struct InscriptionManagerView: View {
private func _sharingTeamsMenuView() -> some View {
Menu {
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(), preview: .init("Inscriptions")) {
Text("En texte")
}
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")
ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) {
Text("En csv")
}
} label: {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
@ -555,8 +577,8 @@ struct InscriptionManagerView: View {
tournament.unsortedTeamsWithoutWO()
}
func teamPaste(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat, type: type)
func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat)
}
var unsortedPlayers: [PlayerRegistration] {
@ -842,31 +864,6 @@ struct InscriptionManagerView: View {
@ViewBuilder
private func _informationView(for teams: [TeamRegistration]) -> some View {
#if PRODTEST
if tournament.enableOnlinePayment {
RowButtonView("Requête de paiement", role: .destructive) {
do {
try await teams.filter { team in
team.hasPaidOnline() == false && team.hasPaid() == false
}.concurrentForEach { team in
_ = try await PaymentService.resendPaymentEmail(teamRegistrationId: team.id)
}
await MainActor.run {
alertMessage = "Relance effectuée avec succès"
showAlert = true
}
} catch {
Logger.error(error)
await MainActor.run {
alertMessage = "Erreur lors de la requête"
showAlert = true
}
}
}
}
#endif
Section {
HStack {
// VStack(alignment: .leading, spacing: 0) {
@ -945,9 +942,14 @@ struct InscriptionManagerView: View {
if tournament.enableOnlineRegistration {
LabeledContent {
Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted())
.fontWeight(.bold)
.font(.largeTitle)
} label: {
Text("Inscriptions en ligne")
if let refreshResult {
Text(refreshResult).foregroundStyle(.secondary)
} else {
Text(" ")
}
}
// RowButtonView("Rafraîchir les inscriptions en ligne") {
@ -1256,11 +1258,10 @@ 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, type: type).createFile(self.tournament.tournamentTitle()+"-"+type.localizedString(), exportFormat)
return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat)
}
static var transferRepresentation: some TransferRepresentation {

@ -10,7 +10,7 @@ import LeStorage
import PadelClubData
struct TableStructureView: View {
var tournament: Tournament
@Environment(Tournament.self) private var tournament: Tournament
@EnvironmentObject private var dataStore: DataStore
@Environment(\.dismiss) var dismiss
@State private var presentRefreshStructureWarning: Bool = false
@ -24,11 +24,6 @@ struct TableStructureView: View {
@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()
@ -66,21 +61,8 @@ struct TableStructureView: View {
max(teamCount - groupStageCount * teamsPerGroupStage, 0)
}
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)
}
@ViewBuilder
var body: some View {
List {
if displayWarning() {
@ -97,34 +79,17 @@ 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 {
LabeledContent {
StepperView(count: $teamCount, minimum: 2, maximum: 128) {
StepperView(count: $teamCount, minimum: 4, maximum: 128) {
} submitFollowUpAction: {
_verifyValueIntegrity()
@ -238,156 +203,103 @@ struct TableStructureView: View {
}
}
if selectedTournament == nil {
Section {
if groupStageCount > 0 {
if structurePreset != .doubleGroupStage {
LabeledContent {
Text(teamsFromGroupStages.formatted())
} label: {
Text("Équipes en poule")
}
LabeledContent {
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
} label: {
Text("Équipes qualifiées de poule")
}
}
}
Section {
let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
if groupStageCount > 0 {
if structurePreset != .doubleGroupStage {
LabeledContent {
Text(tsPure.formatted())
Text(teamsFromGroupStages.formatted())
} label: {
Text("Équipes à placer en tableau")
if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
Text("Attention !").foregroundStyle(.red)
}
Text("Équipes en poule")
}
// if groupStageCount > 0 {
// LabeledContent {
// Text(tf.formatted())
// } label: {
// Text("Effectif tableau")
// }
// }
} else {
LabeledContent {
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage
Text((mp1 + mp2).formatted())
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
} label: {
Text("Total de matchs")
Text("Équipes qualifiées de poule")
}
}
}
if structurePreset != .doubleGroupStage {
LabeledContent {
FooterButtonView("configurer") {
showSeedRepartition = true
}
Text(tsPure.formatted())
} 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())
Text("Nombre de têtes de série")
if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
Text("Attention !").foregroundStyle(.red)
}
}
.onAppear {
if seedRepartition.isEmpty && tournament.state() == .initial && selectedTournament == nil {
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
LabeledContent {
Text(tf.formatted())
} label: {
Text("Équipes en tableau final")
}
} 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)
} else {
LabeledContent {
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage
Text((mp1 + mp2).formatted())
} label: {
Text("Total de matchs")
}
}
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
Section {
Toggle("Avec wildcards", isOn: $buildWildcards)
} footer: {
Text("Padel Club réservera des places pour eux dans votre liste d'inscription.")
} 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)
}
}
}
if tournament.rounds().isEmpty && tournament.state() == .build {
Section {
RowButtonView("Ajouter un tableau", role: .destructive) {
tournament.buildBracket(minimalBracketTeamCount: 4)
}
} footer: {
Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.")
}
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
Section {
Toggle("Avec wildcards", isOn: $buildWildcards)
} footer: {
Text("Padel Club réservera des places pour eux dans votre liste d'inscription.")
}
}
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")
if tournament.rounds().isEmpty {
Section {
RowButtonView("Ajouter un tableau", role: .destructive) {
tournament.buildBracket(minimalBracketTeamCount: 4)
}
} footer: {
Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.")
}
}
if tournament.state() != .initial {
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) {
await _save(rebuildEverything: false)
_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) {
await _save(rebuildEverything: true)
_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) {
_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()
}
}
}
Section {
RowButtonView("Retirer toutes les équipes du tableau", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetBracketPosition()
}
}
}
}
}
@ -400,20 +312,19 @@ struct TableStructureView: View {
}
}
.toolbarBackground(.visible, for: .navigationBar)
.sheet(isPresented: $showSeedRepartition, content: {
NavigationStack {
HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in
self.seedRepartition = seedRepartition
}
}
})
.onAppear {
teamCount = tournament.teamCount
groupStageCount = tournament.groupStageCount
teamsPerGroupStage = tournament.teamsPerGroupStage
qualifiedPerGroupStage = tournament.qualifiedPerGroupStage
groupStageAdditionalQualified = tournament.groupStageAdditionalQualified
}
.onChange(of: teamCount) {
if teamCount != tournament.teamCount {
updatedElements.insert(.teamCount)
} else {
updatedElements.remove(.teamCount)
}
_verifyValueIntegrity()
}
.onChange(of: groupStageCount) {
if groupStageCount != tournament.groupStageCount {
@ -425,31 +336,25 @@ 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 {
@ -473,17 +378,13 @@ struct TableStructureView: View {
ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial {
ButtonValidateView {
Task {
await _save(rebuildEverything: true)
}
_save(rebuildEverything: true)
}
} else {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
ButtonValidateView(role: .destructive) {
if requirements.isEmpty {
Task {
await _save(rebuildEverything: false)
}
_save(rebuildEverything: false)
} else {
presentRefreshStructureWarning = true
}
@ -494,15 +395,11 @@ struct TableStructureView: View {
}
Button("Reconstruire les poules") {
Task {
await _save(rebuildEverything: false)
}
_save(rebuildEverything: false)
}
Button("Tout refaire", role: .destructive) {
Task {
await _save(rebuildEverything: true)
}
_save(rebuildEverything: true)
}
}, message: {
ForEach(Array(requirements)) { requirement in
@ -522,24 +419,7 @@ struct TableStructureView: View {
}
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() {
let tc = tournament.teamCount
tournament.unsortedTeams().forEach {
$0.resetPositions()
}
tournament.removeWildCards()
tournament.deleteGroupStages()
tournament.deleteStructure()
@ -550,8 +430,7 @@ struct TableStructureView: View {
_updatePreset()
}
teamCount = tc
tournament.teamCount = tc
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
@ -598,9 +477,9 @@ struct TableStructureView: View {
}
}
private func _save(rebuildEverything: Bool = false) async {
_verifyValueIntegrity(keepSeedRepartition: true)
let tc = tournament.teamCount
private func _save(rebuildEverything: Bool = false) {
_verifyValueIntegrity()
do {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
@ -611,63 +490,12 @@ 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, selectedTournament == nil {
await _handleSeedRepartition()
}
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
tournament.deleteGroupStages()
tournament.buildGroupStages()
@ -686,88 +514,16 @@ struct TableStructureView: View {
}
}
private func _handleSeedRepartition() async {
let rounds = tournament.rounds()
var roundsCount = rounds.count
while roundsCount < seedRepartition.count {
await tournament.addNewRound(roundsCount)
roundsCount += 1
}
if seedRepartition.reduce(0, +) > 0 {
let rounds = tournament.rounds()
let roundsToDelete = rounds.prefix(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() {
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()
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount()
groupStageCount = structurePreset.groupStageCount()
teamsPerGroupStage = structurePreset.teamsPerGroupStage()
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
}
private func _verifyValueIntegrity(keepSeedRepartition: Bool = false) {
private func _verifyValueIntegrity() {
if teamCount > 128 {
teamCount = 128
}
@ -776,8 +532,8 @@ struct TableStructureView: View {
groupStageCount = maxGroupStages
}
if teamCount < 2 {
teamCount = 2
if teamCount < 4 {
teamCount = 4
}
if groupStageCount < 0 {
@ -810,9 +566,6 @@ struct TableStructureView: View {
}
}
if keepSeedRepartition == false {
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
}
}
@ -867,44 +620,3 @@ 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()
}

@ -1,83 +0,0 @@
//
// PlayerSearchView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/10/2025.
//
import SwiftUI
import LeStorage
import PadelClubData
struct PlayerSearchView: View {
@State private var searchText: String = ""
@State private var presentSearch: Bool = true
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
let event: Event
let tournaments: [Tournament]
let players: [PlayerRegistration]
init(event: Event) {
self.event = event
self.tournaments = event.tournaments
self.players = event.tournaments.flatMap({ $0.players() })
}
var searchedPlayers: [PlayerRegistration] {
if searchText.isEmpty {
return []
} else {
return players.filter({ $0.contains(searchText) })
}
}
var body: some View {
NavigationStack {
let _searchedPlayers = searchedPlayers
let teams = Set(searchedPlayers.compactMap({ $0.team() }))
List {
ForEach(teams.sorted(by: \.tournament)) { team in
if let tournament = team.tournamentObject() {
Section {
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
TeamRowView(team: team)
}
} footer: {
Text(tournament.tournamentTitle(.title))
}
}
}
}
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(event.eventTitle())
}
}
.navigationTitle("Rechercher un joueur")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Fermer", systemImage: "xmark") {
dismiss()
}
}
}
.overlay(content: {
if _searchedPlayers.isEmpty {
if searchText.isEmpty {
ContentUnavailableView("Rechercher des joueurs", systemImage: "magnifyingglass", description: Text("Tapez un nom de joueur ci-dessus pour commencer la recherche.\nVous pouvez aussi rechercher par email, numéro de téléphone, licence ou identifiant de paiement Stripe."))
} else {
ContentUnavailableView.search(text: searchText)
}
}
})
.searchable(text: $searchText, isPresented: $presentSearch, placement: .navigationBarDrawer(displayMode: .always), prompt: "Rechercher un joueur")
}
}
}

@ -16,9 +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
@State private var inProgressBuild: (String, any TournamentBuildHolder)? = nil
var event: Event? {
guard let federalTournament = tournament as? FederalTournament else { return nil }
@ -78,12 +76,6 @@ 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)
@ -145,20 +137,15 @@ struct TournamentCellView: View {
Text(teamCount.formatted())
}
} else if let federalTournament = tournament as? FederalTournament, navigation.agendaDestination != .around {
if let inProgressBuild, build.age == inProgressBuild.1.age, build.level == inProgressBuild.1.level, build.category == inProgressBuild.1.category, inProgressBuild.0 == federalTournament.id {
ProgressView()
} else {
Button {
_createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build)
} label: {
Image(systemName: existingTournament != nil ? "checkmark.circle.fill" : "square.and.arrow.down")
.resizable()
.scaledToFit()
.frame(height: 28)
.accessibilityLabel("importer ou ouvrir")
.tint(existingTournament != nil ? Color.green : nil)
}
Button {
_createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build)
} label: {
Image(systemName: existingTournament != nil ? "checkmark.circle.fill" : "square.and.arrow.down")
.resizable()
.scaledToFit()
.frame(height: 28)
.accessibilityLabel("importer ou ouvrir")
.tint(existingTournament != nil ? Color.green : nil)
}
}
}
@ -227,8 +214,6 @@ struct TournamentCellView: View {
newTournament.umpireCustomMail = federalTournament.mailLabel()
}
inProgressBuild = (federalTournament.id, build)
Task {
do {
let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id)
@ -248,10 +233,6 @@ struct TournamentCellView: View {
} catch {
Logger.error(error)
}
await MainActor.run {
inProgressBuild = nil
}
}
}
}

@ -48,8 +48,6 @@ struct PaymentStatusView: View {
@State var payment: TournamentPayment? = .free
@State var noOfferMessage: String? = nil
var body: some View {
Group {
@ -60,8 +58,7 @@ struct PaymentStatusView: View {
let text = "Tournoi offert (\(remaining) restant\(end))"
ImageInfoView(systemImage: "gift.fill", text: text, tip: FreeTournamentTip())
case nil:
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())
ImageInfoView(systemImage: "exclamationmark.bubble.fill", text: "Veuillez souscrire à une offre pour convoquer ou entrer un résultat", textColor: .white, backgroundColor: .logoRed, tip: NoPaymentTip())
default:
EmptyView()
}
@ -80,7 +77,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 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 convoquez ou que vous rentrez un résultat.")
}
var image: Image? {
@ -91,7 +88,7 @@ struct FreeTournamentTip: Tip {
struct NoPaymentTip: Tip {
var title: Text {
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)
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)
}
var image: Image? {

@ -21,7 +21,7 @@ struct TournamentBuildView: View {
let state = tournament.state()
Section {
if tournament.hasGroupeStages() {
if tournament.groupStageCount > 0 {
NavigationLink(value: Screen.groupStage) {
LabeledContent {
if let groupStageStatus {

@ -18,7 +18,7 @@ struct TournamentView: View {
@State var tournament: Tournament
@State private var showMoreInfos: Bool = false
@State var shouldTournamentBeOver: Bool = false
@State private var presentSearchView: Bool = false
var presentationContext: PresentationContext = .agenda
let tournamentSelectionTip: TournamentSelectionTip = TournamentSelectionTip()
@ -138,7 +138,7 @@ struct TournamentView: View {
Group {
switch screen {
case .structure:
TableStructureView(tournament: tournament)
TableStructureView()
case .settings:
TournamentSettingsView()
case .inscription:
@ -168,13 +168,7 @@ struct TournamentView: View {
case .print:
PrintSettingsView(tournament: tournament)
case .share:
ShareModelView(instance: tournament, handler: { users in
if users.count > 0 {
Task {
try? await self.tournament.payIfNecessary()
}
}
})
ShareModelView(instance: tournament)
case .restingTime:
TeamRestingView()
case .stateSettings:
@ -186,13 +180,6 @@ struct TournamentView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarTitleMenu {
if let event = tournament.eventObject() {
Button("Rechercher un joueur", systemImage: "magnifyingglass") {
presentSearchView = true
}
.labelStyle(.titleOnly)
Divider()
if presentationContext == .agenda {
Picker(selection: selectedTournamentId) {
ForEach(event.tournaments) { tournament in
@ -243,14 +230,14 @@ struct TournamentView: View {
}
#endif
// if presentationContext == .agenda {
// Button {
// navigation.openTournamentInOrganizer(tournament)
// } label: {
// Label("Gestionnaire", systemImage: "pin")
// }
// Divider()
// }
if presentationContext == .agenda {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Gestionnaire", systemImage: "pin")
}
Divider()
}
NavigationLink(value: Screen.event) {
Label("Événement", systemImage: "info")
@ -287,6 +274,7 @@ struct TournamentView: View {
// NavigationLink(value: Screen.statistics) {
// Label("Statistiques", systemImage: "123.rectangle")
// }
//
NavigationLink(value: Screen.rankings) {
LabeledContent {
@ -341,11 +329,6 @@ struct TournamentView: View {
}
}
}
.sheet(isPresented: $presentSearchView, content: {
if let event = tournament.eventObject() {
PlayerSearchView(event: event)
}
})
.onAppear {
TournamentRunningTip.isRunning = tournament.state() == .running
Logger.log("Tournament Id = \(self.tournament.id), Payment = \(String(describing: self.tournament.payment))")

@ -24,6 +24,61 @@ struct AccountView: View {
PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item))
}
#endif
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 {
Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?")
SupportButtonView(supportButtonType: .sharingRequest)
}
Section {
NavigationLink("Changer de mot de passe") {
ChangePasswordView()

@ -207,7 +207,7 @@ struct LoginView: View {
dataStore.appSettingsStorage.write()
}
self.handler(user)
navigation.accountPath.removeAll()
navigation.umpirePath.removeAll()
} catch {
self.isLoading = false
self.errorText = ErrorUtils.message(error: error)

@ -10,22 +10,16 @@ import LeStorage
import SwiftUI
import PadelClubData
typealias UsersClosure = (([String]) -> ())
struct ShareModelView<T: SyncedStorable> : View {
@StateObject private var viewModel = UserSearchViewModel()
let instance: T
var handler: UsersClosure? = nil
@State var payment: TournamentPayment? = nil
var body: some View {
List {
if T.self is Tournament.Type {
Section {
PaymentStatusView(noOfferMessage: "Veuillez souscrire à une offre afin de payer le tournoi")
}
}
if !self.viewModel.availableUsers.isEmpty {
ForEach(self.viewModel.availableUsers, id: \.id) { user in
let isSelected = viewModel.contains(user.id)
@ -56,10 +50,9 @@ struct ShareModelView<T: SyncedStorable> : View {
.toolbarBackground(.visible, for: .navigationBar)
.onAppear {
self.viewModel.selectedUsers = StoreCenter.main.authorizedUsers(for: self.instance.stringId)
}.onDisappear {
if let handler {
handler(self.viewModel.selectedUsers)
}
}
.task {
self.payment = await Guard.main.paymentForNewTournament()
}
}

Loading…
Cancel
Save