Compare commits

...

137 Commits

Author SHA1 Message Date
Razmig Sarkissian 745f5884ab b2 4 days ago
Razmig Sarkissian e6aaa620fe ios 26.1 fixes 4 days ago
Razmig Sarkissian a58541b5bf add some progressview 4 days ago
Razmig Sarkissian 425451424a build 2 5 days ago
Razmig Sarkissian 431a388b13 fix an issue with search player and add format helper view in toolbox 5 days ago
Razmig Sarkissian e05dfa66b8 v1.2.62 1 week ago
Razmig Sarkissian 78f31c45d3 v61 2 weeks ago
Razmig Sarkissian d9657ace50 fix ongoing 2 weeks ago
Razmig Sarkissian a16897f3ed v1.2.60 2 weeks ago
Razmig Sarkissian 4d366b437d add a way to filter out tournament in ongoing view 2 weeks ago
Razmig Sarkissian 00d759dd6c build 4 2 weeks ago
Razmig Sarkissian 57945f6cfd build 3 2 weeks ago
Razmig Sarkissian a5445e7280 fix issue with auto structure 2 weeks ago
Razmig Sarkissian b287b67a0c build 2 2 weeks ago
Razmig Sarkissian 41fbbc3c95 v1.2.59 2 weeks ago
Razmig Sarkissian 5d49680cca Merge remote-tracking branch 'refs/remotes/origin/main' 2 weeks ago
Razmig Sarkissian ec5fc5b5e2 fix menu option 2 weeks ago
Razmig Sarkissian 769f29c41a fix menu option 3 weeks ago
Razmig Sarkissian dd54cfa9fd v1.2.58 3 weeks ago
Razmig Sarkissian aaeebd6d75 fix planning stuff 3 weeks ago
Razmig Sarkissian 9fb5ed889e fix crash in head manager 3 weeks ago
Razmig Sarkissian 7ba7012c57 v1.2.57 3 weeks ago
Razmig Sarkissian 51deb72da0 1.2.57 build 3 3 weeks ago
Razmig Sarkissian 236406e262 small improvements 3 weeks ago
Razmig Sarkissian 9bb753cce1 build 2 3 weeks ago
Razmig Sarkissian 11914c054f v1.2.57 3 weeks ago
Razmig Sarkissian 63496d334f fix send all by event 3 weeks ago
Razmig Sarkissian ceaa03c41f add a call all event method 3 weeks ago
Razmig Sarkissian 7c3801cb51 Merge remote-tracking branch 'refs/remotes/origin/main' 3 weeks ago
Razmig Sarkissian 757f22cc67 improve call team view 3 weeks ago
Laurent 4e92e23f84 Merge branch 'main' of https://gitea.staxriver.com/staxriver/PadelClub 3 weeks ago
Laurent 46af357538 add websocket infos 3 weeks ago
Razmig Sarkissian 858a68c572 add global search 3 weeks ago
Razmig Sarkissian 413e2436dd 1.2.56 4 weeks ago
Laurent f6cf835ebf Merge branch 'main' of https://gitea.staxriver.com/staxriver/PadelClub 4 weeks ago
Laurent 1b2fb9dc0c backup now contains part of the parent folder 4 weeks ago
Razmig Sarkissian 0de19382d8 fix request payment positionning 4 weeks ago
Razmig Sarkissian ce7fce7dfd Merge remote-tracking branch 'refs/remotes/origin/main' 4 weeks ago
Razmig Sarkissian b5d5cd4aeb add payment link api 4 weeks ago
Laurent 99cf9df1ef Merge branch 'main' of https://gitea.staxriver.com/staxriver/PadelClub 4 weeks ago
Laurent 035f8ccc9d Adds CLAUDE.md file 4 weeks ago
Razmig Sarkissian bd03321cc0 improve export data capability for teams / players 4 weeks ago
Razmig Sarkissian 18228396bf build 2 4 weeks ago
Razmig Sarkissian dbd970f87f add custom club name option in tournament for calling teams 4 weeks ago
Razmig Sarkissian 8379eccfb6 fix registion issues not displayed 4 weeks ago
Razmig Sarkissian 43f5ac97a4 add helper footer 4 weeks ago
Razmig Sarkissian a3880b04bd fix head manager match count 4 weeks ago
Razmig Sarkissian 45319790aa fix stuff 4 weeks ago
Razmig Sarkissian b41e8064d7 some fixes 4 weeks ago
Razmig Sarkissian 6c634399d7 fix stuff headmanager 4 weeks ago
Razmig Sarkissian 13011e2b1c add heads config system 4 weeks ago
Razmig Sarkissian ac18a14863 fix toolbox debug view 4 weeks ago
Razmig Sarkissian 05f132316c add global format picker 1 month ago
Razmig Sarkissian 15ae97faf5 v1.2.55 1 month ago
Razmig Sarkissian 4badce1a06 couple of fixes 1 month ago
Razmig Sarkissian ef28a98f20 add format selection to horaire and format view 1 month ago
Razmig Sarkissian 0f14852858 fix icons 1 month ago
Razmig Sarkissian cc533081ac build 2 1 month ago
Razmig Sarkissian 44f9ab1b1c fix agenda 1 month ago
Razmig Sarkissian fbd2a083b1 fix wording 1 month ago
Razmig Sarkissian e466543628 add sharing back 1 month ago
Razmig Sarkissian 8fdeff82f1 overhaul screens disposition 1 month ago
Laurent 2183f2863f Remove sharing button 1 month ago
Laurent dec6f21db9 Adds payment when adding supervisors 1 month ago
Razmig Sarkissian b239ff9a07 v1.2.54 1 month ago
Razmig Sarkissian 15e480cf28 fix main menu 1 month ago
Razmig Sarkissian b2bc59c19e Merge remote-tracking branch 'refs/remotes/origin/sync3' 1 month ago
Razmig Sarkissian 2d40e5b816 fix shared tournament umpire stuff 1 month ago
Laurent 0520ad75a5 Merge branch 'sync3' of https://gitea.staxriver.com/staxriver/PadelClub into sync3 1 month ago
Laurent cc50cc45ac draft 1 month ago
Razmig Sarkissian 09d5da914c fix title partager 1 month ago
Razmig Sarkissian c2ccbf5dd7 improve view sharing 1 month ago
Razmig Sarkissian 154137d25f Merge branch 'main' 1 month ago
Razmig Sarkissian c7d5f4930e fix match format setup 1 month ago
Razmig Sarkissian b8680eeea1 Merge branch 'main' 1 month ago
Razmig Sarkissian d1435688eb fix payment stuff 1 month ago
Laurent 08cf60629b Implement the capacity from a user to remove himself from the sharing 1 month ago
Laurent 4a07d430b7 adds code to remove tournament from being shared 1 month ago
Laurent e7b0571e9f Bumps version to 1.2.53 1 month ago
Razmig Sarkissian 4acb1e8ea4 fix ios 26 stuff 1 month ago
Razmig Sarkissian 58f61f395f fix sharing stuff 1 month ago
Razmig Sarkissian 4441554881 Merge branch 'main' 1 month ago
Razmig Sarkissian 06dd0fc3cc fix stuff 1 month ago
Laurent a0d6580a98 fix sync issues 1 month ago
Razmig Sarkissian b9a052e7d9 Merge branch 'main' 1 month ago
Razmig Sarkissian 445a180762 fix left aligne planning view 1 month ago
Razmig Sarkissian ea13b13101 Merge branch 'main' 1 month ago
Razmig Sarkissian 052204a8d6 v1.2.53 1 month ago
Razmig Sarkissian 8602881562 fix ios 26 1 month ago
Razmig Sarkissian aaa1c16660 fix merge issue 1 month ago
Razmig Sarkissian d1bbd75015 Merge remote-tracking branch 'refs/remotes/origin/sync3' 1 month ago
Laurent f95bc4de92 merge 1 month ago
Laurent 67cfa830a8 fix build 1 month ago
Razmig Sarkissian 86c1fa26cc add subtitle to menu gestion du tournoi 1 month ago
Razmig Sarkissian 74b61c6046 remove refresh team action and task 1 month ago
Razmig Sarkissian 64d0d7c307 fix issue with merge 1 month ago
Razmig Sarkissian 0a613e6376 Merge branch 'main' 1 month ago
Razmig Sarkissian 7259777ba5 fix tournament menu 1 month ago
Razmig Sarkissian dd91369c6b animation fix in debug 1 month ago
Laurent ed161df9ba merge 2 months ago
Razmig Sarkissian 3305c9aaf4 v1.2.52 2 months ago
Razmig Sarkissian 3af1de6ff9 fix issue with ios 26 2 months ago
Razmig Sarkissian cb4e2c5ed6 Merge remote-tracking branch 'refs/remotes/origin/main' 2 months ago
Razmig Sarkissian 1601f72ad2 fix issue with ios 26 2 months ago
Laurent 9e46c3234f Payment upgrade 2 months ago
Laurent 2e2b83f804 change sync server to padelclub.app 2 months ago
Razmig Sarkissian 604d24c326 v1.2.51 2 months ago
Laurent 8987c46fae Merge branch 'main' of https://gitea.staxriver.com/staxriver/PadelClub 2 months ago
Laurent a80e2c6945 always show offer 2 months ago
Laurent dc02001505 Merge branch 'main' of https://gitea.staxriver.com/staxriver/PadelClub 2 months ago
Laurent 488e33e253 remove network icon 2 months ago
Laurent 2299e941b2 Consequences of Guard changes 2 months ago
Razmig Sarkissian 463a7af43b fix issue with field setup 2 months ago
Razmig Sarkissian 29636ac374 ios 26 version 2 months ago
Laurent 3875d8558b Merge branch 'sync3' of https://gitea.staxriver.com/staxriver/PadelClub into sync3 2 months ago
Laurent a9ce9659f1 add information about sync 2 months ago
Razmig Sarkissian 7b5968b1d0 fix issue with import session 2 months ago
Razmig Sarkissian 8b7202b0eb fix issue with import session 2 months ago
Razmig Sarkissian 862b7deced fix team name playerblock view 2 months ago
Razmig Sarkissian 6237502cec fix playerblock view 2 months ago
Razmig Sarkissian 6320697c7e Merge branch 'main' 2 months ago
Razmig Sarkissian b8bf7f99e8 fix editscoreview yes 2 months ago
Razmig Sarkissian dd98548a15 fix csv export birth year FFT new system 2 months ago
Razmig Sarkissian ed06b68405 Merge branch 'main' 3 months ago
Laurent faa0f8ab22 version 1.2.40 for testflight 4 months ago
Laurent 892db419fe Merge branch 'main' into sync3 5 months ago
Laurent 4ee338df6d merge main 5 months ago
Laurent d35e312c3f Merge branch 'main' into sync3 5 months ago
Laurent 57439e4a93 Merge branch 'main' into sync3 5 months ago
Laurent a41080685c adds sync and fix build 5 months ago
Laurent 07eb633ce6 Merge branch 'main' into sync3 5 months ago
Laurent 8d33bc0204 cleanup view 5 months ago
Laurent c4bd58a1af Improve tournament selection 5 months ago
Laurent a28a72075e Adds restriction for shared tournaments 5 months ago
Laurent c7575d1d67 refactoring 6 months ago
Laurent 6cefe91b37 add sharing buttons 6 months ago
Laurent df8609d1a0 Adds sharing for matches 6 months ago
  1. 8
      CLAUDE.md
  2. 183
      PadelClub.xcodeproj/project.pbxproj
  3. 25
      PadelClub/AppDelegate.swift
  4. 5
      PadelClub/Data/Federal/FederalPlayer.swift
  5. 104
      PadelClub/Data/Federal/FederalTournament.swift
  6. 36
      PadelClub/Extensions/Tournament+Extensions.swift
  7. 22
      PadelClub/Extensions/View+Extensions.swift
  8. 13
      PadelClub/PadelClubApp.swift
  9. 2
      PadelClub/Utils/FileImportManager.swift
  10. 74
      PadelClub/Utils/Network/FederalDataService.swift
  11. 2
      PadelClub/Utils/Network/NetworkFederalService.swift
  12. 77
      PadelClub/Utils/Network/PaymentService.swift
  13. 15
      PadelClub/Utils/Tips.swift
  14. 6
      PadelClub/ViewModel/AgendaDestination.swift
  15. 13
      PadelClub/ViewModel/FederalDataViewModel.swift
  16. 2
      PadelClub/ViewModel/NavigationViewModel.swift
  17. 10
      PadelClub/ViewModel/SearchViewModel.swift
  18. 5
      PadelClub/ViewModel/TabDestination.swift
  19. 2
      PadelClub/Views/Calling/BracketCallingView.swift
  20. 11
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  21. 4
      PadelClub/Views/Calling/CallSettingsView.swift
  22. 109
      PadelClub/Views/Calling/CallView.swift
  23. 24
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  24. 7
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  25. 50
      PadelClub/Views/Calling/SendToAllView.swift
  26. 60
      PadelClub/Views/Calling/TeamsCallingView.swift
  27. 14
      PadelClub/Views/Cashier/CashierSettingsView.swift
  28. 14
      PadelClub/Views/Cashier/CashierView.swift
  29. 6
      PadelClub/Views/Cashier/Event/EventCreationView.swift
  30. 29
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  31. 25
      PadelClub/Views/Cashier/Event/EventTournamentsView.swift
  32. 11
      PadelClub/Views/Cashier/Event/EventView.swift
  33. 91
      PadelClub/Views/Cashier/Event/TournamentPickerView.swift
  34. 5
      PadelClub/Views/Club/ClubDetailView.swift
  35. 47
      PadelClub/Views/Club/ClubsView.swift
  36. 15
      PadelClub/Views/Club/Shared/ClubCourtSetupView.swift
  37. 17
      PadelClub/Views/Components/BarButtonView.swift
  38. 10
      PadelClub/Views/Components/ButtonValidateView.swift
  39. 2
      PadelClub/Views/Components/FortuneWheelView.swift
  40. 8
      PadelClub/Views/Components/Labels.swift
  41. 7
      PadelClub/Views/Components/StepperView.swift
  42. 20
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  43. 6
      PadelClub/Views/GroupStage/GroupStageView.swift
  44. 8
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  45. 7
      PadelClub/Views/GroupStage/GroupStagesView.swift
  46. 18
      PadelClub/Views/GroupStage/RankingGroupStageSetupView.swift
  47. 10
      PadelClub/Views/Match/Components/MatchDateView.swift
  48. 4
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  49. 76
      PadelClub/Views/Match/MatchDetailView.swift
  50. 1
      PadelClub/Views/Match/MatchSetupView.swift
  51. 175
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  52. 5
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  53. 129
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  54. 113
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  55. 52
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  56. 36
      PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift
  57. 127
      PadelClub/Views/Navigation/MainView.swift
  58. 226
      PadelClub/Views/Navigation/MyAccount/MyAccountView.swift
  59. 239
      PadelClub/Views/Navigation/OnboardingView.swift
  60. 45
      PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift
  61. 2
      PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift
  62. 9
      PadelClub/Views/Navigation/Toolbox/APICallsListView.swift
  63. 54
      PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift
  64. 185
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  65. 2
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  66. 68
      PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift
  67. 85
      PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift
  68. 317
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  69. 6
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  70. 12
      PadelClub/Views/Planning/PlanningSettingsView.swift
  71. 92
      PadelClub/Views/Planning/PlanningView.swift
  72. 2
      PadelClub/Views/Planning/SchedulerView.swift
  73. 6
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  74. 32
      PadelClub/Views/Player/PlayerDetailView.swift
  75. 6
      PadelClub/Views/Round/DrawLogsView.swift
  76. 3
      PadelClub/Views/Round/LoserRoundView.swift
  77. 7
      PadelClub/Views/Round/LoserRoundsView.swift
  78. 9
      PadelClub/Views/Round/RoundSettingsView.swift
  79. 7
      PadelClub/Views/Round/RoundView.swift
  80. 6
      PadelClub/Views/Round/RoundsView.swift
  81. 16
      PadelClub/Views/Score/EditScoreView.swift
  82. 13
      PadelClub/Views/Score/FollowUpMatchView.swift
  83. 4
      PadelClub/Views/Score/SetInputView.swift
  84. 2
      PadelClub/Views/Shared/LearnMoreSheetView.swift
  85. 70
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  86. 78
      PadelClub/Views/Shared/SupportButtonView.swift
  87. 2
      PadelClub/Views/Shared/TournamentFilterView.swift
  88. 98
      PadelClub/Views/Team/EditingTeamView.swift
  89. 268
      PadelClub/Views/Team/PaymentLinkManagerView.swift
  90. 51
      PadelClub/Views/Team/PaymentRequestButton.swift
  91. 24
      PadelClub/Views/Team/PaymentService.swift
  92. 52
      PadelClub/Views/Team/TeamMatchesView.swift
  93. 2
      PadelClub/Views/Team/TeamRestingView.swift
  94. 6
      PadelClub/Views/Tournament/ConsolationTournamentImportView.swift
  95. 4
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  96. 55
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  97. 240
      PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift
  98. 13
      PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift
  99. 62
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  100. 3
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,8 @@
## 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

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD2F22C412681000DBD9A /* AppDelegate.swift */; }; C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD2F22C412681000DBD9A /* AppDelegate.swift */; };
C410F54E2DF340FE009713ED /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4D05D462DC10AE5009B053C /* WebKit.framework */; };
C411C9C32BEBA453003017AD /* ServerDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C22BEBA453003017AD /* ServerDataTests.swift */; }; C411C9C32BEBA453003017AD /* ServerDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C22BEBA453003017AD /* ServerDataTests.swift */; };
C411C9C92BF219CB003017AD /* UserDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C82BF219CB003017AD /* UserDataTests.swift */; }; C411C9C92BF219CB003017AD /* UserDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C82BF219CB003017AD /* UserDataTests.swift */; };
C411C9D02BF38F41003017AD /* TokenExemptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */; }; C411C9D02BF38F41003017AD /* TokenExemptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */; };
@ -150,10 +151,34 @@
FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; }; 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 */; }; 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 */; }; 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 */; }; FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */; };
FF2B51612C7E302C00FFF126 /* local.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = FF2B51602C7E302C00FFF126 /* local.sqlite */; }; FF2B51612C7E302C00FFF126 /* local.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = FF2B51602C7E302C00FFF126 /* local.sqlite */; };
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; }; FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; };
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; }; FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; };
FF30ACED2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; };
FF30ACEE2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; };
FF30ACEF2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; };
FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; };
FF39B6152DC8825E004E10CE /* PadelClubData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49770202DC25A23005CD239 /* PadelClubData.framework */; }; FF39B6152DC8825E004E10CE /* PadelClubData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49770202DC25A23005CD239 /* PadelClubData.framework */; };
@ -615,7 +640,6 @@
FF70FBAF2C90584900129CC2 /* UpdateSourceRankDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */; }; FF70FBAF2C90584900129CC2 /* UpdateSourceRankDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */; };
FF70FBB02C90584900129CC2 /* GlobalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */; }; FF70FBB02C90584900129CC2 /* GlobalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */; };
FF70FBB22C90584900129CC2 /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; }; FF70FBB22C90584900129CC2 /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; };
FF70FBB42C90584900129CC2 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = FF70FABE2C90584900129CC2 /* Algorithms */; };
FF70FBB52C90584900129CC2 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF70FAC02C90584900129CC2 /* Zip */; }; FF70FBB52C90584900129CC2 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF70FAC02C90584900129CC2 /* Zip */; };
FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; }; FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; };
FF70FBB82C90584900129CC2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; }; FF70FBB82C90584900129CC2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; };
@ -711,6 +735,21 @@
FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; };
FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; };
FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; };
FFA97C9E2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; };
FFA97C9F2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; };
FFA97CA02E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; };
FFA97CA22E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; };
FFA97CA32E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; };
FFA97CA42E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; };
FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; };
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
@ -734,6 +773,12 @@
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; };
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; };
FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.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 */; }; FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */; };
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; }; FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; };
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; };
@ -1002,11 +1047,19 @@
FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; FF2B51622C7F073100FFF126 /* Model_1_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_1_1.xcdatamodel; sourceTree = "<group>"; };
FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLinksView.swift; sourceTree = "<group>"; }; FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLinksView.swift; sourceTree = "<group>"; };
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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; }; FF39B60F2DC87FEB004E10CE /* PadelClubData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PadelClubData.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1112,6 +1165,11 @@
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = "<group>"; }; FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = "<group>"; };
FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; };
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentPickerView.swift; sourceTree = "<group>"; };
FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingGroupStageSetupView.swift; sourceTree = "<group>"; };
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdaySelectionView.swift; sourceTree = "<group>"; };
FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; };
FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.swift; sourceTree = "<group>"; }; FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.swift; sourceTree = "<group>"; };
FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; }; FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; };
@ -1127,6 +1185,8 @@
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = "<group>"; };
@ -1217,7 +1277,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FF70FBB42C90584900129CC2 /* Algorithms in Frameworks */, C410F54E2DF340FE009713ED /* WebKit.framework in Frameworks */,
FF39B6172DC88267004E10CE /* PadelClubData.framework in Frameworks */, FF39B6172DC88267004E10CE /* PadelClubData.framework in Frameworks */,
FF70FBB52C90584900129CC2 /* Zip in Frameworks */, FF70FBB52C90584900129CC2 /* Zip in Frameworks */,
FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */, FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */,
@ -1333,6 +1393,7 @@
C49771DE2DC25F04005CD239 /* Badge+Extensions.swift */, C49771DE2DC25F04005CD239 /* Badge+Extensions.swift */,
C49771DF2DC25F04005CD239 /* SpinDrawable+Extensions.swift */, C49771DF2DC25F04005CD239 /* SpinDrawable+Extensions.swift */,
C49772022DC260D3005CD239 /* Round+Extensions.swift */, C49772022DC260D3005CD239 /* Round+Extensions.swift */,
FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1565,11 +1626,21 @@
path = SeedData; path = SeedData;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FF30AD2E2E92A936008B6006 /* MyAccount */ = {
isa = PBXGroup;
children = (
FF30AD2F2E92A994008B6006 /* MyAccountView.swift */,
);
path = MyAccount;
sourceTree = "<group>";
};
FF39719B2B8DE04B004C4E75 /* Navigation */ = { FF39719B2B8DE04B004C4E75 /* Navigation */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */, FF59FFB62B90EFBF0061EFF9 /* MainView.swift */,
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */,
FFD783FB2B91B919000F62A6 /* Agenda */, FFD783FB2B91B919000F62A6 /* Agenda */,
FF30AD2E2E92A936008B6006 /* MyAccount */,
FF3F74FA2B91A04B004CFE0E /* Organizer */, FF3F74FA2B91A04B004CFE0E /* Organizer */,
FF3F74FB2B91A060004CFE0E /* Toolbox */, FF3F74FB2B91A060004CFE0E /* Toolbox */,
FF3F74FC2B91A06B004CFE0E /* Umpire */, FF3F74FC2B91A06B004CFE0E /* Umpire */,
@ -1602,6 +1673,7 @@
FF7091692B90F95E00AB08DA /* DateBoxView.swift */, FF7091692B90F95E00AB08DA /* DateBoxView.swift */,
FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */, FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */,
FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */, FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */,
FF20990B2EA1430E003CE880 /* PlayerSearchView.swift */,
); );
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1653,6 +1725,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */, FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */,
FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */,
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */, FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */, C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */,
@ -1744,6 +1818,7 @@
FF4AB6B42B9248200002987F /* NetworkManager.swift */, FF4AB6B42B9248200002987F /* NetworkManager.swift */,
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */, FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */,
FFE8B5BA2DA9896800BDE966 /* RefundService.swift */, FFE8B5BA2DA9896800BDE966 /* RefundService.swift */,
FF30ACEC2E8D700B008B6006 /* PaymentService.swift */,
FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */, FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */,
FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */, FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */,
FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */, FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */,
@ -1769,6 +1844,8 @@
FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */, FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */,
FFE8B5BE2DAA325400BDE966 /* RefundResultsView.swift */, FFE8B5BE2DAA325400BDE966 /* RefundResultsView.swift */,
FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */, FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */,
FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */,
FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1796,6 +1873,7 @@
FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */,
FF5DA18E2BB9268800A33061 /* GroupStagesSettingsView.swift */, FF5DA18E2BB9268800A33061 /* GroupStagesSettingsView.swift */,
FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */, FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */,
FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */,
FF9AC3932BE3625D00C2E883 /* Components */, FF9AC3932BE3625D00C2E883 /* Components */,
FF9AC3922BE3625200C2E883 /* Shared */, FF9AC3922BE3625200C2E883 /* Shared */,
); );
@ -1824,6 +1902,9 @@
FF1162862BD004AD000C4809 /* EditingTeamView.swift */, FF1162862BD004AD000C4809 /* EditingTeamView.swift */,
FF17CA562CC02FEA003C7323 /* CoachListView.swift */, FF17CA562CC02FEA003C7323 /* CoachListView.swift */,
FF7DCD382CC330260041110C /* TeamRestingView.swift */, FF7DCD382CC330260041110C /* TeamRestingView.swift */,
FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */,
FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */,
FF2099072EA140A2003CE880 /* TeamMatchesView.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */, FF025AD62BD0C0FB00A86CF8 /* Components */,
); );
path = Team; path = Team;
@ -1857,6 +1938,7 @@
FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */, FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */,
FF8F263C2BAD627A00650388 /* TournamentConfiguratorView.swift */, FF8F263C2BAD627A00650388 /* TournamentConfiguratorView.swift */,
FF8E52332DF01D6100099B75 /* EventStatusView.swift */, FF8E52332DF01D6100099B75 /* EventStatusView.swift */,
FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */,
); );
name = Event; name = Event;
path = Cashier/Event; path = Cashier/Event;
@ -1897,6 +1979,7 @@
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */, FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */,
FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */, FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */,
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */, FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */,
FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */,
FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */, FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */,
); );
path = Agenda; path = Agenda;
@ -2065,7 +2148,6 @@
); );
name = "PadelClub TestFlight"; name = "PadelClub TestFlight";
packageProductDependencies = ( packageProductDependencies = (
FF70FABE2C90584900129CC2 /* Algorithms */,
FF70FAC02C90584900129CC2 /* Zip */, FF70FAC02C90584900129CC2 /* Zip */,
); );
productName = PadelClub; productName = PadelClub;
@ -2236,11 +2318,13 @@
FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */, FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */,
FFE103122C366E5900684FC9 /* ImagePickerView.swift in Sources */, FFE103122C366E5900684FC9 /* ImagePickerView.swift in Sources */,
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */, FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */,
FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */,
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */, FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */,
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */, FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */, FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */,
@ -2261,6 +2345,7 @@
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */,
FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */, FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FF30ACEF2E8D700B008B6006 /* PaymentService.swift in Sources */,
FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */, FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */,
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */,
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */, FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */,
@ -2295,6 +2380,7 @@
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */,
FFC91B032BD85E2400B29808 /* CourtView.swift in Sources */, FFC91B032BD85E2400B29808 /* CourtView.swift in Sources */,
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */, FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */, C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */,
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */, FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */,
@ -2310,6 +2396,7 @@
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */,
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FFC2DB252E97DD0A00869317 /* HeadManagerView.swift in Sources */,
FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */, FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */,
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, FF3795662B9399AA004EA093 /* Persistence.swift in Sources */,
@ -2336,6 +2423,7 @@
FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */, FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
FFA97CA02E8A7C080089EA22 /* View+Extensions.swift in Sources */,
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */, FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FFF03C942BD91D0C00B516FC /* ButtonValidateView.swift in Sources */, FFF03C942BD91D0C00B516FC /* ButtonValidateView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */, FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
@ -2360,17 +2448,20 @@
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF20990D2EA1430E003CE880 /* PlayerSearchView.swift in Sources */,
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */, FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */,
FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */, FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */, FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */,
FFD8837A2E1E63880004D7DD /* FederalDataService.swift in Sources */, FFD8837A2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF2099092EA140A2003CE880 /* TeamMatchesView.swift in Sources */,
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFC2DB212E97D00300869317 /* TournamentSelectorView.swift in Sources */,
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
@ -2386,7 +2477,9 @@
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723A2DC28A92005CD239 /* ComposeViews.swift in Sources */, C497723A2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
@ -2401,6 +2494,7 @@
FF8E52342DF01D6100099B75 /* EventStatusView.swift in Sources */, FF8E52342DF01D6100099B75 /* EventStatusView.swift in Sources */,
FFE8B63C2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FFE8B63C2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */, FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */,
FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */, FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
@ -2422,11 +2516,14 @@
FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */, FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */,
FFC91AF92BD6A09100B29808 /* FortuneWheelView.swift in Sources */, FFC91AF92BD6A09100B29808 /* FortuneWheelView.swift in Sources */,
FFE8B5C82DAA390900BDE966 /* StripeValidationService.swift in Sources */, FFE8B5C82DAA390900BDE966 /* StripeValidationService.swift in Sources */,
FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */, C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */,
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */, FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */,
FF2099052EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */,
FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */,
FFA97CA32E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */,
FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */, FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */,
FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */, FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */,
FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */, FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */,
@ -2501,11 +2598,13 @@
FF4CBF532C996C0600151637 /* ClubsView.swift in Sources */, FF4CBF532C996C0600151637 /* ClubsView.swift in Sources */,
FF4CBF542C996C0600151637 /* ImagePickerView.swift in Sources */, FF4CBF542C996C0600151637 /* ImagePickerView.swift in Sources */,
FF4CBF552C996C0600151637 /* MatchTypeSelectionView.swift in Sources */, FF4CBF552C996C0600151637 /* MatchTypeSelectionView.swift in Sources */,
FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */,
FF4CBF562C996C0600151637 /* MatchSetupView.swift in Sources */, FF4CBF562C996C0600151637 /* MatchSetupView.swift in Sources */,
FF4CBF572C996C0600151637 /* NetworkManager.swift in Sources */, FF4CBF572C996C0600151637 /* NetworkManager.swift in Sources */,
FF4CBF582C996C0600151637 /* EventLinksView.swift in Sources */, FF4CBF582C996C0600151637 /* EventLinksView.swift in Sources */,
FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */, FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */,
FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */, FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */,
FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */, FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */,
FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */, FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */,
@ -2526,6 +2625,7 @@
FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */, FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */,
FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */, FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */,
FF30ACEE2E8D700B008B6006 /* PaymentService.swift in Sources */,
FF4CBF722C996C0600151637 /* SwiftParser.swift in Sources */, FF4CBF722C996C0600151637 /* SwiftParser.swift in Sources */,
FF4CBF732C996C0600151637 /* ChangePasswordView.swift in Sources */, FF4CBF732C996C0600151637 /* ChangePasswordView.swift in Sources */,
FF4CBF742C996C0600151637 /* TournamentSubscriptionView.swift in Sources */, FF4CBF742C996C0600151637 /* TournamentSubscriptionView.swift in Sources */,
@ -2560,6 +2660,7 @@
FF4CBF882C996C0600151637 /* TeamRowView.swift in Sources */, FF4CBF882C996C0600151637 /* TeamRowView.swift in Sources */,
FF4CBF8A2C996C0600151637 /* AppDelegate.swift in Sources */, FF4CBF8A2C996C0600151637 /* AppDelegate.swift in Sources */,
FF4CBF8C2C996C0600151637 /* EditScoreView.swift in Sources */, FF4CBF8C2C996C0600151637 /* EditScoreView.swift in Sources */,
FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */,
FF4CBF8D2C996C0600151637 /* TournamentOrganizerView.swift in Sources */, FF4CBF8D2C996C0600151637 /* TournamentOrganizerView.swift in Sources */,
FF4CBF8F2C996C0600151637 /* TournamentRunningView.swift in Sources */, FF4CBF8F2C996C0600151637 /* TournamentRunningView.swift in Sources */,
FF4CBF902C996C0600151637 /* TournamentDatePickerView.swift in Sources */, FF4CBF902C996C0600151637 /* TournamentDatePickerView.swift in Sources */,
@ -2575,6 +2676,7 @@
FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */, FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */,
FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF4CBFA12C996C0600151637 /* LoserRoundSettingsView.swift in Sources */, FF4CBFA12C996C0600151637 /* LoserRoundSettingsView.swift in Sources */,
FFC2DB262E97DD0A00869317 /* HeadManagerView.swift in Sources */,
FF4CBFA22C996C0600151637 /* Persistence.swift in Sources */, FF4CBFA22C996C0600151637 /* Persistence.swift in Sources */,
FF4CBFA32C996C0600151637 /* CloseDatePicker.swift in Sources */, FF4CBFA32C996C0600151637 /* CloseDatePicker.swift in Sources */,
FF4CBFA42C996C0600151637 /* BarButtonView.swift in Sources */, FF4CBFA42C996C0600151637 /* BarButtonView.swift in Sources */,
@ -2601,6 +2703,7 @@
FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */, FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */,
FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */, FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */,
FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */, FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */,
FFA97C9E2E8A7C080089EA22 /* View+Extensions.swift in Sources */,
FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */, FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */,
FF4CBFC32C996C0600151637 /* GroupStageCallingView.swift in Sources */, FF4CBFC32C996C0600151637 /* GroupStageCallingView.swift in Sources */,
FF4CBFC52C996C0600151637 /* CashierSettingsView.swift in Sources */, FF4CBFC52C996C0600151637 /* CashierSettingsView.swift in Sources */,
@ -2625,17 +2728,20 @@
FF4CBFD82C996C0600151637 /* ImportedPlayer+Extensions.swift in Sources */, FF4CBFD82C996C0600151637 /* ImportedPlayer+Extensions.swift in Sources */,
FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */, FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */,
FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF20990C2EA1430E003CE880 /* PlayerSearchView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD883792E1E63880004D7DD /* FederalDataService.swift in Sources */, FFD883792E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF20990A2EA140A2003CE880 /* TeamMatchesView.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */, FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */,
FF4CBFDF2C996C0600151637 /* FederalTournamentSearchScope.swift in Sources */, FF4CBFDF2C996C0600151637 /* FederalTournamentSearchScope.swift in Sources */,
FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */, FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */,
FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */, FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */,
FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FFC2DB202E97D00300869317 /* TournamentSelectorView.swift in Sources */,
FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */, FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */,
FFE8B63B2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FFE8B63B2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */, FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */,
@ -2651,7 +2757,9 @@
FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */, FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */,
FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C49772392DC28A92005CD239 /* ComposeViews.swift in Sources */, C49772392DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */, FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -2666,6 +2774,7 @@
FF8E52362DF01D6100099B75 /* EventStatusView.swift in Sources */, FF8E52362DF01D6100099B75 /* EventStatusView.swift in Sources */,
FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */, FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */,
FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */,
FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */,
FFE8B5B52DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFE8B5B52DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
@ -2687,11 +2796,14 @@
FF4CC0102C996C0600151637 /* LoadingViewModifier.swift in Sources */, FF4CC0102C996C0600151637 /* LoadingViewModifier.swift in Sources */,
FF4CC0112C996C0600151637 /* PlayerView.swift in Sources */, FF4CC0112C996C0600151637 /* PlayerView.swift in Sources */,
FF4CC0122C996C0600151637 /* MatchDetailView.swift in Sources */, FF4CC0122C996C0600151637 /* MatchDetailView.swift in Sources */,
FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF4CC0142C996C0600151637 /* PlayerBlockView.swift in Sources */, FF4CC0142C996C0600151637 /* PlayerBlockView.swift in Sources */,
FF4CC0172C996C0600151637 /* PointView.swift in Sources */, FF4CC0172C996C0600151637 /* PointView.swift in Sources */,
FF2099042EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */, FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */,
FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */, FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */,
C49771E72DC25F04005CD239 /* Color+Extensions.swift in Sources */, C49771E72DC25F04005CD239 /* Color+Extensions.swift in Sources */,
FFA97CA22E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */,
C49771E82DC25F04005CD239 /* Badge+Extensions.swift in Sources */, C49771E82DC25F04005CD239 /* Badge+Extensions.swift in Sources */,
C49771E92DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */, C49771E92DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */,
FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */, FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */,
@ -2744,11 +2856,13 @@
FF70FAD22C90584900129CC2 /* ClubsView.swift in Sources */, FF70FAD22C90584900129CC2 /* ClubsView.swift in Sources */,
FF70FAD32C90584900129CC2 /* ImagePickerView.swift in Sources */, FF70FAD32C90584900129CC2 /* ImagePickerView.swift in Sources */,
FF70FAD42C90584900129CC2 /* MatchTypeSelectionView.swift in Sources */, FF70FAD42C90584900129CC2 /* MatchTypeSelectionView.swift in Sources */,
FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */,
FF70FAD52C90584900129CC2 /* MatchSetupView.swift in Sources */, FF70FAD52C90584900129CC2 /* MatchSetupView.swift in Sources */,
FF70FAD62C90584900129CC2 /* NetworkManager.swift in Sources */, FF70FAD62C90584900129CC2 /* NetworkManager.swift in Sources */,
FF70FAD72C90584900129CC2 /* EventLinksView.swift in Sources */, FF70FAD72C90584900129CC2 /* EventLinksView.swift in Sources */,
FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */, FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */,
FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */, FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */,
FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */, FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */,
FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */, FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */,
@ -2769,6 +2883,7 @@
FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */, FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */,
FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */, FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */,
FF30ACED2E8D700B008B6006 /* PaymentService.swift in Sources */,
FF70FAF12C90584900129CC2 /* SwiftParser.swift in Sources */, FF70FAF12C90584900129CC2 /* SwiftParser.swift in Sources */,
FF70FAF22C90584900129CC2 /* ChangePasswordView.swift in Sources */, FF70FAF22C90584900129CC2 /* ChangePasswordView.swift in Sources */,
FF70FAF32C90584900129CC2 /* TournamentSubscriptionView.swift in Sources */, FF70FAF32C90584900129CC2 /* TournamentSubscriptionView.swift in Sources */,
@ -2803,6 +2918,7 @@
FF70FB072C90584900129CC2 /* TeamRowView.swift in Sources */, FF70FB072C90584900129CC2 /* TeamRowView.swift in Sources */,
FF70FB092C90584900129CC2 /* AppDelegate.swift in Sources */, FF70FB092C90584900129CC2 /* AppDelegate.swift in Sources */,
FF70FB0B2C90584900129CC2 /* EditScoreView.swift in Sources */, FF70FB0B2C90584900129CC2 /* EditScoreView.swift in Sources */,
FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */,
FF70FB0C2C90584900129CC2 /* TournamentOrganizerView.swift in Sources */, FF70FB0C2C90584900129CC2 /* TournamentOrganizerView.swift in Sources */,
FF70FB0E2C90584900129CC2 /* TournamentRunningView.swift in Sources */, FF70FB0E2C90584900129CC2 /* TournamentRunningView.swift in Sources */,
FF70FB0F2C90584900129CC2 /* TournamentDatePickerView.swift in Sources */, FF70FB0F2C90584900129CC2 /* TournamentDatePickerView.swift in Sources */,
@ -2818,6 +2934,7 @@
FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */, FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */,
FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF70FB202C90584900129CC2 /* LoserRoundSettingsView.swift in Sources */, FF70FB202C90584900129CC2 /* LoserRoundSettingsView.swift in Sources */,
FFC2DB242E97DD0A00869317 /* HeadManagerView.swift in Sources */,
FF70FB212C90584900129CC2 /* Persistence.swift in Sources */, FF70FB212C90584900129CC2 /* Persistence.swift in Sources */,
FF70FB222C90584900129CC2 /* CloseDatePicker.swift in Sources */, FF70FB222C90584900129CC2 /* CloseDatePicker.swift in Sources */,
FF70FB232C90584900129CC2 /* BarButtonView.swift in Sources */, FF70FB232C90584900129CC2 /* BarButtonView.swift in Sources */,
@ -2844,6 +2961,7 @@
FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */, FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */,
FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */, FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */,
FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */, FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */,
FFA97C9F2E8A7C080089EA22 /* View+Extensions.swift in Sources */,
FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */, FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */,
FF70FB422C90584900129CC2 /* GroupStageCallingView.swift in Sources */, FF70FB422C90584900129CC2 /* GroupStageCallingView.swift in Sources */,
FF70FB442C90584900129CC2 /* CashierSettingsView.swift in Sources */, FF70FB442C90584900129CC2 /* CashierSettingsView.swift in Sources */,
@ -2868,17 +2986,20 @@
FF70FB572C90584900129CC2 /* ImportedPlayer+Extensions.swift in Sources */, FF70FB572C90584900129CC2 /* ImportedPlayer+Extensions.swift in Sources */,
FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */, FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */,
FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF20990E2EA1430E003CE880 /* PlayerSearchView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD8837B2E1E63880004D7DD /* FederalDataService.swift in Sources */, FFD8837B2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF2099082EA140A2003CE880 /* TeamMatchesView.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */, FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */,
FF70FB5E2C90584900129CC2 /* FederalTournamentSearchScope.swift in Sources */, FF70FB5E2C90584900129CC2 /* FederalTournamentSearchScope.swift in Sources */,
FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */, FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */,
FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */, FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */,
FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FFC2DB222E97D00300869317 /* TournamentSelectorView.swift in Sources */,
FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */, FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */,
FFE8B63A2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FFE8B63A2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */, FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */,
@ -2894,7 +3015,9 @@
FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */, FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */,
FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723B2DC28A92005CD239 /* ComposeViews.swift in Sources */, C497723B2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */, FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -2909,6 +3032,7 @@
FF8E52352DF01D6100099B75 /* EventStatusView.swift in Sources */, FF8E52352DF01D6100099B75 /* EventStatusView.swift in Sources */,
FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */, FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */,
FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */, FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */,
FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */,
FFE8B5B42DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFE8B5B42DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
@ -2930,11 +3054,14 @@
FF70FB8F2C90584900129CC2 /* LoadingViewModifier.swift in Sources */, FF70FB8F2C90584900129CC2 /* LoadingViewModifier.swift in Sources */,
FF70FB902C90584900129CC2 /* PlayerView.swift in Sources */, FF70FB902C90584900129CC2 /* PlayerView.swift in Sources */,
FF70FB912C90584900129CC2 /* MatchDetailView.swift in Sources */, FF70FB912C90584900129CC2 /* MatchDetailView.swift in Sources */,
FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF70FB932C90584900129CC2 /* PlayerBlockView.swift in Sources */, FF70FB932C90584900129CC2 /* PlayerBlockView.swift in Sources */,
FF70FB962C90584900129CC2 /* PointView.swift in Sources */, FF70FB962C90584900129CC2 /* PointView.swift in Sources */,
FF2099062EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */, FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */,
FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */, FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */,
C49771E42DC25F04005CD239 /* Color+Extensions.swift in Sources */, C49771E42DC25F04005CD239 /* Color+Extensions.swift in Sources */,
FFA97CA42E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */,
C49771E52DC25F04005CD239 /* Badge+Extensions.swift in Sources */, C49771E52DC25F04005CD239 /* Badge+Extensions.swift in Sources */,
C49771E62DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */, C49771E62DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */,
FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */, FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */,
@ -3118,7 +3245,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3140,12 +3267,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.48; MARKETING_VERSION = 1.2.64;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3166,10 +3293,11 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3186,12 +3314,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.48; MARKETING_VERSION = 1.2.64;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3284,11 +3412,12 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3305,12 +3434,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.37; MARKETING_VERSION = 1.2.57;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3330,10 +3459,11 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3350,12 +3480,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.37; MARKETING_VERSION = 1.2.57;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3394,18 +3524,19 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.37; MARKETING_VERSION = 1.2.53;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -3436,12 +3567,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.1; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.37; MARKETING_VERSION = 1.2.53;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3449,6 +3580,7 @@
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = TESTFLIGHT; SWIFT_ACTIVE_COMPILATION_CONDITIONS = TESTFLIGHT;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -3530,14 +3662,6 @@
minimumVersion = 2.1.2; minimumVersion = 2.1.2;
}; };
}; };
FF70FABF2C90584900129CC2 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-algorithms.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */ = { FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/marmelroy/Zip"; repositoryURL = "https://github.com/marmelroy/Zip";
@ -3567,11 +3691,6 @@
package = FF4CBF422C996C0600151637 /* XCRemoteSwiftPackageReference "Zip" */; package = FF4CBF422C996C0600151637 /* XCRemoteSwiftPackageReference "Zip" */;
productName = Zip; productName = Zip;
}; };
FF70FABE2C90584900129CC2 /* Algorithms */ = {
isa = XCSwiftPackageProductDependency;
package = FF70FABF2C90584900129CC2 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
productName = Algorithms;
};
FF70FAC02C90584900129CC2 /* Zip */ = { FF70FAC02C90584900129CC2 /* Zip */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */; package = FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */;

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

@ -80,8 +80,13 @@ class FederalPlayer: Decodable {
let ageSportif = try container.decodeIfPresent(Int.self, forKey: .ageSportif) let ageSportif = try container.decodeIfPresent(Int.self, forKey: .ageSportif)
if let ageSportif { if let ageSportif {
let month = Calendar.current.component(.month, from: Date())
if month > 8 {
birthYear = Calendar.current.component(.year, from: Date()) + 1 - ageSportif
} else {
birthYear = Calendar.current.component(.year, from: Date()) - ageSportif birthYear = Calendar.current.component(.year, from: Date()) - ageSportif
} }
}
clubCode = try container.decodeIfPresent(String.self, forKey: .codeClub) ?? "" clubCode = try container.decodeIfPresent(String.self, forKey: .codeClub) ?? ""
club = try container.decodeIfPresent(String.self, forKey: .nomClub) ?? "" club = try container.decodeIfPresent(String.self, forKey: .nomClub) ?? ""
ligue = try container.decodeIfPresent(String.self, forKey: .ligue) ?? "" ligue = try container.decodeIfPresent(String.self, forKey: .ligue) ?? ""

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

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

@ -0,0 +1,22 @@
//
// View+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/09/2025.
//
import SwiftUI
extension View {
/// Runs a transform only on iOS 26+, otherwise returns self
@ViewBuilder
func ifAvailableiOS26<Content: View>(
@ViewBuilder transform: (Self) -> Content
) -> some View {
if #available(iOS 26.0, *) {
transform(self)
} else {
self
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -43,9 +43,11 @@ struct MenuWarningView: View {
} }
} }
} label: { } label: {
Text("Prévenir") Label("Prévenir", systemImage: "phone")
.underline() .labelStyle(.iconOnly)
} }
.menuStyle(.button)
.buttonStyle(.borderedProminent)
.sheet(isPresented: self.$showSubscriptionView, content: { .sheet(isPresented: self.$showSubscriptionView, content: {
NavigationStack { NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
@ -159,14 +161,16 @@ struct MenuWarningView: View {
} }
} }
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { // fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
do { // Task {
try tournament.payIfNecessary() // do {
handler() // try await tournament.payIfNecessary()
} catch { // handler()
self.showSubscriptionView = true // } catch {
} // self.showSubscriptionView = true
} // }
// }
// }
} }

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

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

@ -129,21 +129,28 @@ struct CallMenuOptionsView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration let team: TeamRegistration
let action: (() -> Void)? 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> { var confirmed: Binding<Bool> {
Binding { Binding {
team.confirmed() team.confirmed()
} set: { _ in } set: { _ in
team.toggleSummonConfirmation() team.toggleSummonConfirmation()
do { _save()
try self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?() action?()
} }
} }
private func _save() {
self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
}
var body: some View { var body: some View {
List { List {
Section { Section {
@ -151,11 +158,36 @@ struct CallMenuOptionsView: View {
Toggle(isOn: confirmed) { Toggle(isOn: confirmed) {
Text("Confirmation reçue") 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 { if team.expectedSummonDate() != nil {
CallView(team: team, displayContext: .menu) CallView(team: team, displayContext: .menu, summonType: .summon)
CallView(team: team, displayContext: .menu, summonType: .summonWalkoutFollowUp)
CallView(team: team, displayContext: .menu, summonType: .summonErrorFollowUp)
} }
} footer: {
CallView(teams: [team]) CallView(team: team, displayContext: .menu, summonType: .contact)
CallView(team: team, displayContext: .menu, summonType: .contactWithoutSignature)
} }
Section { Section {
@ -170,11 +202,7 @@ struct CallMenuOptionsView: View {
Section { Section {
RowButtonView("Effacer la date de convocation", role: .destructive) { RowButtonView("Effacer la date de convocation", role: .destructive) {
team.callDate = nil team.callDate = nil
do { _save()
try self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?() action?()
dismiss() dismiss()
} }
@ -183,11 +211,7 @@ struct CallMenuOptionsView: View {
Section { Section {
RowButtonView("Indiquer comme convoquée", role: .destructive) { RowButtonView("Indiquer comme convoquée", role: .destructive) {
team.callDate = team.initialMatch()?.startDate ?? tournament.startDate team.callDate = team.initialMatch()?.startDate ?? tournament.startDate
do { _save()
try self.tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?() action?()
dismiss() dismiss()
} }

@ -127,8 +127,7 @@ struct CashierSettingsView: View {
} }
} }
ToolbarItem(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
HStack {
if focusedField == ._entryFee { if focusedField == ._entryFee {
if tournament.isFree() { if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in ForEach(priceTags, id: \.self) { priceTag in
@ -137,7 +136,7 @@ struct CashierSettingsView: View {
tournament.entryFee = priceTag tournament.entryFee = priceTag
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
} }
} else { } else {
Button("Gratuit") { Button("Gratuit") {
@ -145,7 +144,7 @@ struct CashierSettingsView: View {
tournament.entryFee = nil tournament.entryFee = nil
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
} }
} else if focusedField == ._clubMemberFeeDeduction { } else if focusedField == ._clubMemberFeeDeduction {
@ -155,21 +154,20 @@ struct CashierSettingsView: View {
tournament.clubMemberFeeDeduction = deductionTag tournament.clubMemberFeeDeduction = deductionTag
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
} }
Button("Gratuit") { Button("Gratuit") {
clubMemberFeeDeduction = entryFee clubMemberFeeDeduction = entryFee
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
} }
Spacer() Spacer()
Button("Valider") { Button("Valider") {
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
}
} }
} }
} }

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

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

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

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

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

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

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

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

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

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

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

@ -157,6 +157,7 @@ struct SpinDrawView: View {
ToolbarItem(placement: .status) { ToolbarItem(placement: .status) {
Text("Tous les tirages sont terminés") Text("Tous les tirages sont terminés")
.frame(maxWidth: .infinity)
} }
} }
} }
@ -220,6 +221,7 @@ struct FortuneWheelContainerView: View {
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.rotationEffect(.degrees(180)) .rotationEffect(.degrees(180))
} }
.frame(maxWidth: 600, maxHeight: 600)
.onAppear { .onAppear {
if autoMode { if autoMode {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

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

@ -67,15 +67,14 @@ struct StepperView: View {
} }
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.toolbar { .toolbar {
ToolbarItem(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
if amountIsFocused { if amountIsFocused {
HStack {
Spacer() Spacer()
Button("Confirmer") { Button("Confirmer") {
amountIsFocused = false amountIsFocused = false
_validate() _validate()
} }
} .buttonStyle(.borderedProminent)
} }
} }
} }
@ -95,7 +94,7 @@ struct StepperView: View {
} }
fileprivate func _plusIsDisabled() -> Bool { fileprivate func _plusIsDisabled() -> Bool {
count >= (maximum ?? 90_415) count >= (maximum ?? 92_327)
} }
fileprivate func _add() { fileprivate func _add() {

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

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

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

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

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

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

@ -77,7 +77,7 @@ struct PlayerBlockView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
VStack { VStack {
if let teamName = team?.name { if let teamName = team?.name, teamName.isEmpty == false {
Text(teamName).foregroundStyle(.secondary).font(.footnote) Text(teamName).foregroundStyle(.secondary).font(.footnote)
} }
Text("longLabelPlayerOne").lineLimit(1) Text("longLabelPlayerOne").lineLimit(1)
@ -91,7 +91,7 @@ struct PlayerBlockView: View {
Text("Repêchée").italic().font(.caption) Text("Repêchée").italic().font(.caption)
} }
if let teamName = team.name { if let teamName = team.name, teamName.isEmpty == false {
Text(teamName).foregroundStyle(.secondary).font(.footnote) Text(teamName).foregroundStyle(.secondary).font(.footnote)
} else if team.players().isEmpty { } else if team.players().isEmpty {
if team.isWildCard() { if team.isWildCard() {

@ -309,6 +309,13 @@ struct MatchDetailView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {
NavigationLink {
ShareModelView(instance: self.match)
} label: {
Label("Partager", systemImage: "square.and.arrow.up")
}
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
return match.confirmed return match.confirmed
}, set: { value in }, set: { value in
@ -318,6 +325,17 @@ struct MatchDetailView: View {
Text(match.confirmed ? "Confirmé" : "Non confirmé") Text(match.confirmed ? "Confirmé" : "Non confirmé")
} }
if match.hasWalkoutTeam() == true {
Divider()
Button(role: .destructive) {
match.removeWalkOut()
save()
} label: {
Text("Annuler le forfait")
}
}
Divider() Divider()
if match.courtIndex != nil { if match.courtIndex != nil {
@ -354,6 +372,33 @@ struct MatchDetailView: View {
Text("Remise-à-zéro") Text("Remise-à-zéro")
} }
Menu {
Button("Effacer le nom") {
match.name = nil
tournamentStore?.matches.addOrUpdate(instance: match)
}
if let tournament = match.currentTournament() {
Menu {
ForEach(tournament.generateSeedGroups(base: 16, teamCount: 8), id: \.self) { seedGroup in
Button {
match.name = seedGroup.localizedInterval()
tournamentStore?.matches.addOrUpdate(instance: match)
} label: {
Text(seedGroup.localizedInterval())
}
}
} label: {
Text("Choisir un nom")
}
}
} label: {
Text("[Site] Nom du match")
if let name = match.name {
Text("(\(name))")
}
}
if match.teamScores.isEmpty == false { if match.teamScores.isEmpty == false {
Divider() Divider()
Menu { Menu {
@ -382,7 +427,13 @@ struct MatchDetailView: View {
.navigationTitle(match.matchTitle()) .navigationTitle(match.matchTitle())
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
if let tournament = match.currentTournament() {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
}
} }
var quickLookHeader: some View { var quickLookHeader: some View {
@ -581,9 +632,20 @@ struct MatchDetailView: View {
} }
self._verifyUser { self._verifyUser {
self._payTournamentAndExecute {
Task {
do {
try await self._payTournamentAndExecute()
self.scoreType = .edition self.scoreType = .edition
} catch {
self.showSubscriptionView = true
}
} }
// self._payTournamentAndExecute {
// self.scoreType = .edition
// }
} }
} }
@ -595,15 +657,9 @@ struct MatchDetailView: View {
} }
} }
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { fileprivate func _payTournamentAndExecute() async throws {
guard let tournament = match.currentTournament() else { fatalError("missing tournament") } guard let tournament = match.currentTournament() else { fatalError("missing tournament") }
try await tournament.payIfNecessary()
do {
try tournament.payIfNecessary()
handler()
} catch {
self.showSubscriptionView = true
}
} }
private func save() { private func save() {

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

@ -25,6 +25,7 @@ struct ActivityView: View {
@State private var quickAccessScreen: QuickAccessScreen? = nil @State private var quickAccessScreen: QuickAccessScreen? = nil
@State private var displaySearchView: Bool = false @State private var displaySearchView: Bool = false
@State private var pasteString: String? = nil @State private var pasteString: String? = nil
@State private var presentOnboarding: Bool = false
enum QuickAccessScreen : Identifiable, Hashable { enum QuickAccessScreen : Identifiable, Hashable {
case inscription case inscription
@ -38,17 +39,17 @@ struct ActivityView: View {
} }
var runningTournaments: [FederalTournamentHolder] { var runningTournaments: [FederalTournamentHolder] {
return dataStore.tournaments.filter({ $0.endDate == nil }) return dataStore.tournaments.filter({ $0.endDate == nil && $0.sharing != .granted })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) })
} }
func getRunningTournaments() -> [Tournament] { func getRunningTournaments() -> [Tournament] {
return dataStore.tournaments.filter({ $0.endDate == nil }) return dataStore.tournaments.filter({ $0.endDate == nil && $0.sharing != .granted })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) })
} }
var endedTournaments: [Tournament] { var endedTournaments: [Tournament] {
return dataStore.tournaments.filter({ $0.endDate != nil }) return dataStore.tournaments.filter({ $0.endDate != nil && $0.sharing != .granted })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) })
} }
// //
@ -77,6 +78,11 @@ struct ActivityView: View {
@ViewBuilder @ViewBuilder
private func _pasteView() -> some View { private func _pasteView() -> some View {
if #available(iOS 26.0, *) {
Button("Ajouter une équipe", systemImage: "person.badge.plus") {
quickAccessScreen = .inscription
}
} else {
Button { Button {
quickAccessScreen = .inscription quickAccessScreen = .inscription
} label: { } label: {
@ -86,6 +92,7 @@ struct ActivityView: View {
.frame(minHeight: 32) .frame(minHeight: 32)
} }
.accessibilityLabel("Ajouter une équipe") .accessibilityLabel("Ajouter une équipe")
}
// if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true { // if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true {
// PasteButton(payloadType: String.self) { strings in // PasteButton(payloadType: String.self) { strings in
@ -106,6 +113,8 @@ struct ActivityView: View {
@Bindable var navigation = navigation @Bindable var navigation = navigation
NavigationStack(path: $navigation.path) { NavigationStack(path: $navigation.path) {
VStack(spacing: 0) { VStack(spacing: 0) {
Color.clear.frame(height: 1)
.background(Material.ultraThinMaterial)
GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false)
ScrollViewReader { proxy in ScrollViewReader { proxy in
@ -217,12 +226,40 @@ struct ActivityView: View {
.navigationDestination(for: Tournament.self) { tournament in .navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament) TournamentView(tournament: tournament)
} }
.navigationDestination(for: SubScreen.self) { build in
switch build {
case .subscription(let federalTournament, let build):
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user)
}
}
// .onDisappear(perform: { // .onDisappear(perform: {
// pasteButtonIsDisplayed = nil // pasteButtonIsDisplayed = nil
// print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed) // print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed)
// }) // })
.toolbar { .toolbar {
ToolbarItemGroup(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
if #available(iOS 26.0, *) {
if viewStyle == .calendar {
Button("Vue calendrier", systemImage: "calendar") {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
}
.buttonStyle(.borderedProminent)
} else {
Button("Vue calendrier", systemImage: "calendar") {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
}
}
} else {
Button { Button {
switch viewStyle { switch viewStyle {
case .list: case .list:
@ -237,7 +274,27 @@ struct ActivityView: View {
.frame(minHeight: 32) .frame(minHeight: 32)
} }
.symbolVariant(viewStyle == .calendar ? .fill : .none) .symbolVariant(viewStyle == .calendar ? .fill : .none)
}
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarLeading)
}
ToolbarItem(placement: .topBarLeading) {
if #available(iOS 26.0, *) {
if federalDataViewModel.areFiltersEnabled() {
Button("Filtre", systemImage: "line.3.horizontal.decrease") {
presentFilterView.toggle()
}
.buttonStyle(.borderedProminent)
} else {
Button("Filtre", systemImage: "line.3.horizontal.decrease") {
presentFilterView.toggle()
}
}
} else {
Button { Button {
presentFilterView.toggle() presentFilterView.toggle()
} label: { } label: {
@ -247,14 +304,25 @@ struct ActivityView: View {
.frame(minHeight: 32) .frame(minHeight: 32)
} }
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) .symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
}
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarLeading)
}
ToolbarItem(placement: .topBarLeading) {
_pasteView() _pasteView()
} }
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 26.0, *) {
Button("Ajouter", systemImage: "plus") {
newTournament = Tournament.newEmptyInstance()
}
} else {
Button { Button {
newTournament = Tournament.newEmptyInstance() newTournament = Tournament.newEmptyInstance()
} label: { } label: {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.resizable() .resizable()
@ -262,40 +330,20 @@ struct ActivityView: View {
.frame(minHeight: 32) .frame(minHeight: 32)
} }
} }
if tournaments.isEmpty == false, federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around {
ToolbarItemGroup(placement: .bottomBar) {
VStack(spacing: 0) {
let searchStatus = _searchStatus()
if searchStatus.isEmpty == false {
Text(_searchStatus())
.font(.footnote)
.foregroundStyle(.secondary)
} }
HStack { if #unavailable(iOS 26.0) {
if navigation.agendaDestination == .around { if _shouldDisplaySearchStatus() {
FooterButtonView("modifier votre recherche") { ToolbarItemGroup(placement: .bottomBar) {
displaySearchView = true _searchBoxView()
}
if federalDataViewModel.areFiltersEnabled() {
Text("ou")
}
}
if federalDataViewModel.areFiltersEnabled() {
FooterButtonView(_filterButtonTitle()) {
presentFilterView = true
}
}
}
.padding(.bottom, 8)
} }
} }
} }
} }
.sheet(isPresented: $presentOnboarding, content: {
OnboardingView()
.environmentObject(dataStore)
})
.sheet(isPresented: $presentFilterView) { .sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel) TournamentFilterView(federalDataViewModel: federalDataViewModel)
.environment(navigation) .environment(navigation)
@ -397,6 +445,41 @@ struct ActivityView: View {
} }
} }
private func _shouldDisplaySearchStatus() -> Bool {
tournaments.isEmpty == false && (federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around)
}
private func _searchBoxView() -> some View {
VStack(spacing: 0) {
let searchStatus = _searchStatus()
if searchStatus.isEmpty == false {
Text(_searchStatus())
.font(.footnote)
.foregroundStyle(.secondary)
}
HStack {
if navigation.agendaDestination == .around {
FooterButtonView("modifier votre recherche") {
displaySearchView = true
}
if federalDataViewModel.areFiltersEnabled() {
Text("ou")
}
}
if federalDataViewModel.areFiltersEnabled() {
FooterButtonView(_filterButtonTitle()) {
presentFilterView = true
}
}
}
.padding(.bottom, 8)
}
}
private func _searchStatus() -> String { private func _searchStatus() -> String {
var searchStatus : [String] = [] var searchStatus : [String] = []
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
@ -465,7 +548,7 @@ struct ActivityView: View {
.frame(width: 100) .frame(width: 100)
} }
} description: { } description: {
Text("Aucun événement en cours ou à venir dans votre agenda.") Text("Aucun événement dans votre agenda.")
} actions: { } actions: {
RowButtonView("Créer un nouvel événement") { RowButtonView("Créer un nouvel événement") {
newTournament = Tournament.newEmptyInstance() newTournament = Tournament.newEmptyInstance()
@ -473,7 +556,12 @@ struct ActivityView: View {
RowButtonView("Importer via Tenup") { RowButtonView("Importer via Tenup") {
navigation.agendaDestination = .tenup navigation.agendaDestination = .tenup
} }
SupportButtonView(contentIsUnavailable: true) SupportButtonView(supportButtonType: .contentIsUnavailable)
FooterButtonView("Vous n'êtes pas un juge-arbitre ou un organisateur de tournoi ? En savoir plus") {
presentOnboarding = true
}
.tint(.logoBackground)
} }
} }
@ -485,6 +573,7 @@ struct ActivityView: View {
} }
} }
@ViewBuilder
private func _tenupEmptyView() -> some View { private func _tenupEmptyView() -> some View {
if dataStore.user.hasTenupClubs() == false { if dataStore.user.hasTenupClubs() == false {
ContentUnavailableView { ContentUnavailableView {
@ -496,6 +585,10 @@ struct ActivityView: View {
presentClubSearchView = true presentClubSearchView = true
} }
.padding() .padding()
FooterButtonView("Cette app est dédié aux juge-arbitres et organisateurs de tournoi. Vous êtes un joueur à la recherche d'un tournoi homologué ? Utilisez notre outil de recherche") {
navigation.agendaDestination = .around
}
.tint(.logoBackground)
} }
} else { } else {
ContentUnavailableView { ContentUnavailableView {
@ -518,13 +611,16 @@ struct ActivityView: View {
ContentUnavailableView { ContentUnavailableView {
Label("Recherche de tournoi", systemImage: "magnifyingglass") Label("Recherche de tournoi", systemImage: "magnifyingglass")
} description: { } description: {
Text("Chercher les tournois autour de vous pour mieux décider les tournois à proposer dans votre club. Padel Club vous facilite même l'inscription !") Text("Chercher les tournois homologués autour de vous. Padel Club vous facilite même l'inscription !")
} actions: { } actions: {
RowButtonView("Lancer la recherche") { RowButtonView("Chercher un tournoi") {
displaySearchView = true displaySearchView = true
} }
.padding() .padding()
} }
.onAppear {
displaySearchView = true
}
} else { } else {
if federalDataViewModel.lastError == nil { if federalDataViewModel.lastError == nil {
ContentUnavailableView { ContentUnavailableView {
@ -561,3 +657,8 @@ struct ActivityView: View {
//#Preview { //#Preview {
// ActivityView() // ActivityView()
//} //}
enum SubScreen: Hashable {
case subscription(FederalTournament, TournamentBuild)
}

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

@ -37,7 +37,15 @@ struct EventListView: View {
if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate } if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate }
) { ) {
Section { Section {
if sectionImporting == sectionIndex {
LabeledContent {
ProgressView()
} label: {
Text("Récupération en cours")
}
} else {
_listView(_tournaments) _listView(_tournaments)
}
} header: { } header: {
HStack { HStack {
Text(section.monthYearFormatted) Text(section.monthYearFormatted)
@ -50,7 +58,6 @@ struct EventListView: View {
if let pcTournaments = _tournaments as? [Tournament] { if let pcTournaments = _tournaments as? [Tournament] {
_menuOptions(pcTournaments) _menuOptions(pcTournaments)
} else if let federalTournaments = _tournaments as? [FederalTournament], navigation.agendaDestination == .tenup { } else if let federalTournaments = _tournaments as? [FederalTournament], navigation.agendaDestination == .tenup {
HStack {
FooterButtonView("Tout récupérer", role: .destructive) { FooterButtonView("Tout récupérer", role: .destructive) {
Task { Task {
sectionImporting = sectionIndex sectionImporting = sectionIndex
@ -60,11 +67,7 @@ struct EventListView: View {
sectionImporting = nil sectionImporting = nil
} }
} }
if sectionImporting == sectionIndex { .disabled(sectionImporting == sectionIndex)
Spacer()
ProgressView()
}
}
} }
} }
} }
@ -158,6 +161,29 @@ struct EventListView: View {
} }
Divider() Divider()
} }
Menu {
Picker("Choix du montant", selection: Binding<Double>(get: {
// If all tournaments share the same price, show it; otherwise default to 0
let prices = Set(pcTournaments.compactMap { $0.entryFee })
return prices.count == 1 ? prices.first ?? 0.0 : 0.0
}, set: { (newValue: Double) in
// Apply the chosen price to every tournament
pcTournaments.forEach { tournament in
tournament.entryFee = newValue
}
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
})) {
ForEach([Double](stride(from: 0.0, through: 50.0, by: 5.0)), id: \.self) { (price: Double) in
Text(price.formatted(.currency(code: Locale.current.currency?.identifier ?? "EUR"))).tag(price as Double)
}
}
} label: {
Text("Montant de l'inscription")
}
Divider()
Menu { Menu {
Button { Button {
pcTournaments.forEach { tournament in pcTournaments.forEach { tournament in
@ -181,15 +207,15 @@ struct EventListView: View {
} }
Divider() Divider()
Menu { Menu {
Button { // Button {
Task { // Task {
await pcTournaments.concurrentForEach { tournament in // await pcTournaments.concurrentForEach { tournament in
await tournament.refreshTeamList(forced: true) // await tournament.refreshTeamList(forced: true)
} // }
} // }
} label: { // } label: {
Text("M-à-j des inscriptions") // Text("M-à-j des inscriptions")
} // }
Button { Button {
pcTournaments.forEach { tournament in pcTournaments.forEach { tournament in
@ -350,6 +376,18 @@ struct EventListView: View {
} label: { } label: {
Text("Terminer les tournois encore ouverts") 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: { } label: {
Text("Options avancées") Text("Options avancées")
} }
@ -390,27 +428,27 @@ struct EventListView: View {
private func _tournamentView(_ tournament: Tournament) -> some View { private func _tournamentView(_ tournament: Tournament) -> some View {
NavigationLink(value: tournament) { NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament) TournamentCellView(tournament: tournament)
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name.CollectionDidLoad), perform: { notification in // .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name.CollectionDidLoad), perform: { notification in
//
if let store = notification.object as? SyncedCollection<TeamRegistration> { // if let store = notification.object as? SyncedCollection<TeamRegistration> {
if store.storeId == tournament.id { // if store.storeId == tournament.id {
if tournament.store?.fileCollectionsAllLoaded() == true { // if tournament.store?.fileCollectionsAllLoaded() == true {
tournament.lastTeamRefresh = nil // tournament.lastTeamRefresh = nil
} // }
} // }
} // }
if let store = notification.object as? SyncedCollection<PlayerRegistration> { // if let store = notification.object as? SyncedCollection<PlayerRegistration> {
if store.storeId == tournament.id { // if store.storeId == tournament.id {
if tournament.store?.fileCollectionsAllLoaded() == true { // if tournament.store?.fileCollectionsAllLoaded() == true {
tournament.lastTeamRefresh = nil // tournament.lastTeamRefresh = nil
} // }
} // }
} // }
}) // })
.id(tournament.lastTeamRefresh) // .id(tournament.lastTeamRefresh)
.task(priority: .background) { // .task(priority: .background) {
await tournament.refreshTeamList(forced: false) // await tournament.refreshTeamList(forced: false)
} // }
} }
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)
.onChange(of: tournament.isTemplate) { .onChange(of: tournament.isTemplate) {
@ -425,26 +463,18 @@ struct EventListView: View {
Divider() Divider()
if tournament.hasEnded() == false { if tournament.hasEnded() == false {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Afficher dans le gestionnaire", systemImage: "line.diagonal.arrow")
}
Divider()
_options([tournament]) _options([tournament])
} }
} }
#if DEBUG #if DEBUG
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
if tournament.sharing == nil {
Button(role: .destructive) { Button(role: .destructive) {
dataStore.deleteTournament(tournament) dataStore.deleteTournament(tournament)
} label: { } label: {
LabelDelete() LabelDelete()
} }
}
Button() { Button() {
dataStore.deleteTournament(tournament, noSync: true) dataStore.deleteTournament(tournament, noSync: true)
} label: { } label: {
@ -507,11 +537,15 @@ struct EventListView: View {
if federalTournament.umpireLabel().isEmpty == false { if federalTournament.umpireLabel().isEmpty == false {
newTournament.umpireCustomContact = federalTournament.umpireLabel() newTournament.umpireCustomContact = federalTournament.umpireLabel()
} else {
newTournament.umpireCustomContact = DataStore.shared.user.fullName()
} }
if federalTournament.mailLabel().isEmpty == false { if federalTournament.mailLabel().isEmpty == false {
newTournament.umpireCustomMail = federalTournament.mailLabel() newTournament.umpireCustomMail = federalTournament.mailLabel()
} else {
newTournament.umpireCustomMail = DataStore.shared.user.email
} }
newTournament.umpireCustomPhone = DataStore.shared.user.phone
do { do {
let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id) let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id)
if let email = umpireData.email { if let email = umpireData.email {
@ -534,3 +568,4 @@ struct EventListView: View {
//#Preview { //#Preview {
// EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true) // EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true)
//} //}

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

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

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

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

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

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

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

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

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

@ -10,6 +10,10 @@ import LeStorage
import PadelClubData import PadelClubData
struct DebugSettingsView: View { struct DebugSettingsView: View {
@State private var errorMessage: String?
@State private var showingError = false
@State private var isSynchronizing = false
var body: some View { var body: some View {
List { List {
@ -17,7 +21,23 @@ struct DebugSettingsView: View {
LabeledContent("Has Websocket Manager", value: self._hasWebSocketManager) LabeledContent("Has Websocket Manager", value: self._hasWebSocketManager)
LabeledContent("Websocket ping", value: self._wsPingStatus) LabeledContent("Websocket ping", value: self._wsPingStatus)
LabeledContent("Websocket failure", value: self._wsFailure) LabeledContent("Websocket failure", value: self._wsFailure)
LabeledContent("Last sync date", value: self._lastSyncDate) 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 {
ProgressView()
.scaleEffect(0.8)
Text("Synchronizing...")
.foregroundColor(.secondary)
}
} else {
Button("Synchronize") {
self._synchronize()
}
}
} }
Section("Settings") { Section("Settings") {
@ -40,6 +60,11 @@ struct DebugSettingsView: View {
} }
} }
.alert("Synchronization Error", isPresented: $showingError) {
Button("OK") { }
} message: {
Text(errorMessage ?? "An unknown error occurred")
}
} }
fileprivate var _userId: String { fileprivate var _userId: String {
@ -73,12 +98,39 @@ struct DebugSettingsView: View {
fileprivate var _wsFailure: String { fileprivate var _wsFailure: String {
return "\(StoreCenter.main.websocketFailure)" return "\(StoreCenter.main.websocketFailure)"
} }
fileprivate var _wsError: String? {
if let error = StoreCenter.main.websocketError {
return error.localizedDescription
}
return nil
}
fileprivate var _hasWebSocketManager: String { fileprivate var _hasWebSocketManager: String {
return "\(StoreCenter.main.hasWebSocketManager)" return "\(StoreCenter.main.hasWebSocketManager)"
} }
fileprivate var _lastSyncDate: String { fileprivate var _lastSyncDate: String {
return "\(StoreCenter.main.lastSyncDate)" return "\(StoreCenter.main.lastSyncDate)"
} }
fileprivate func _synchronize() {
Logger.log("launch sync...")
Task {
await MainActor.run {
isSynchronizing = true
}
let error = await StoreCenter.main.synchronizeLastUpdates()
if let error {
await MainActor.run {
errorMessage = error.localizedDescription
showingError = true
}
}
await MainActor.run {
isSynchronizing = false
}
}
}
} }
struct DebugPurchaseView: View { struct DebugPurchaseView: View {

@ -21,6 +21,7 @@ struct ToolboxView: View {
@State private var tapCount = 0 @State private var tapCount = 0
@State private var lastTapTime: Date? = nil @State private var lastTapTime: Date? = nil
private let tapTimeThreshold: TimeInterval = 1.0 private let tapTimeThreshold: TimeInterval = 1.0
@State private var displaySearchPlayer: Bool = false
var lastDataSource: String? { var lastDataSource: String? {
dataStore.appSettings.lastDataSource dataStore.appSettings.lastDataSource
@ -40,31 +41,37 @@ struct ToolboxView: View {
NavigationStack(path: $navigation.toolboxPath) { NavigationStack(path: $navigation.toolboxPath) {
List { List {
Section { Section {
Link(destination: URLs.main.url) { NavigationLink {
Text("Accéder à padelclub.app") PadelClubView()
} label: {
if let _lastDataSourceDate {
LabeledContent {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} label: {
Label(_lastDataSourceDate.monthYearFormatted, systemImage: "calendar.badge.checkmark")
} }
.contextMenu { } else {
ShareLink(item: URLs.main.url) LabeledContent {
Image(systemName: "xmark.circle.fill")
.tint(.logoRed)
} label: {
if let _mostRecentDateAvailable {
Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "calendar.badge")
} else {
Label("Aucun", systemImage: "calendar.badge.exclamationmark")
} }
Text("Classement mensuel disponible")
SupportButtonView(contentIsUnavailable: false)
Link(destination: URLs.appReview.url) {
Text("Partagez vos impressions !")
} }
Link(destination: URLs.instagram.url) {
Text("Compte Instagram PadelClub.app")
} }
} }
} header: {
if self.showDebugViews { Text("Classement mensuel utilisé")
DebugView()
} }
Section { Section {
NavigationLink { Button {
SelectablePlayerListView(isPresented: false, lastDataSource: true) displaySearchPlayer = true
} label: { } label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
} }
@ -74,33 +81,25 @@ struct ToolboxView: View {
} label: { } label: {
Label("Calculateur de points", systemImage: "scalemass") Label("Calculateur de points", systemImage: "scalemass")
} }
}
Section {
NavigationLink { NavigationLink {
PadelClubView() MatchFormatGuideView()
} label: {
if let _lastDataSourceDate {
LabeledContent {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} label: { } label: {
Text(_lastDataSourceDate.monthYearFormatted) Label("Formats et limites", systemImage: "clock")
Text("Classement mensuel utilisé")
} }
} else {
LabeledContent {
Image(systemName: "xmark.circle.fill")
.tint(.logoRed)
} label: {
if let _mostRecentDateAvailable {
Text(_mostRecentDateAvailable.monthYearFormatted)
} else {
Text("Aucun")
} }
Text("Classement mensuel disponible")
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")
} }
} }
@ -118,19 +117,8 @@ struct ToolboxView: View {
} }
} }
Section { if self.showDebugViews {
Link(destination: URLs.appDescription.url) { DebugView()
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 { Section {
@ -140,6 +128,19 @@ 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 { .onAppear {
#if DEBUG #if DEBUG
self.showDebugViews = true self.showDebugViews = true
@ -162,16 +163,9 @@ struct ToolboxView: View {
} }
} }
} }
// .navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
// .navigationTitle(TabDestination.toolbox.title) .navigationTitle(TabDestination.toolbox.title)
.toolbar { .toolbar {
ToolbarItem(placement: .principal) {
Text(TabDestination.toolbox.title)
.font(.headline)
.onTapGesture {
_handleTitleTap()
}
}
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Link(destination: URLs.appStore.url) { Link(destination: URLs.appStore.url) {
Text("v\(PadelClubApp.appVersion)") Text("v\(PadelClubApp.appVersion)")
@ -179,15 +173,16 @@ struct ToolboxView: View {
} }
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {
ShareLink(item: URLs.appStore.url) {
Label("Lien AppStore", systemImage: "link")
}
ShareLink(item: ZipLog(), preview: .init("Mon archive")) { ShareLink(item: ZipLog(), preview: .init("Mon archive")) {
Label("Mes données", systemImage: "server.rack") Text("Archiver mes données")
} }
Divider()
Toggle("Outils avancées", isOn: $showDebugViews)
} label: { } label: {
Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly) LabelOptions()
} }
} }
} }
@ -244,66 +239,6 @@ struct DebugView: View {
Logger.log("Api calls reset") 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 { if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted()) Text(maleUnrankedValue.formatted())
} else { } else {
Text(90_415.formatted()) Text(92_327.formatted())
} }
} label: { } label: {
Text("Rang d'un non classé") Text("Rang d'un non classé")

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

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

@ -17,8 +17,6 @@ struct UmpireView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State private var presentSearchView: Bool = false @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 umpireCustomMail: String
@State private var umpireCustomPhone: String @State private var umpireCustomPhone: String
@State private var umpireCustomContact: String @State private var umpireCustomContact: String
@ -38,101 +36,9 @@ struct UmpireView: View {
_umpireCustomContact = State(wrappedValue: DataStore.shared.user.umpireCustomContact ?? "") _umpireCustomContact = State(wrappedValue: DataStore.shared.user.umpireCustomContact ?? "")
} }
enum UmpireScreen {
case login
}
var body: some View { var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.umpirePath) {
List { List {
PurchaseListView()
if Guard.main.currentPlan != .monthlyUnlimited {
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 { if StoreCenter.main.isAuthenticated {
NavigationLink {
AccountView(user: dataStore.user) { }
} label: {
AccountRowView(userName: dataStore.user.username)
}
let onlineRegPaymentMode = dataStore.user.registrationPaymentMode
Section {
LabeledContent {
switch onlineRegPaymentMode {
case .corporate:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .disabled:
Text("Désactivé")
.bold()
case .noFee:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .stripe:
Text("Activé")
.bold()
.foregroundStyle(.green)
}
} label: {
Text("Option 'Paiement en ligne'")
if onlineRegPaymentMode == .corporate {
Text("Mode Padel Club")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .noFee {
Text("Commission Stripe")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .stripe {
Text("Commission Stripe et Padel Club")
.foregroundStyle(.secondary)
}
}
} footer: {
if onlineRegPaymentMode == .disabled {
FooterButtonView("Contactez nous pour activer cette option.") {
let emailTo: String = "support@padelclub.app"
let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)"
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
.font(.callout)
.multilineTextAlignment(.leading)
} else {
Text("Permet de proposer le paiement de vos tournois en ligne.")
}
}
} else {
NavigationLink(value: UmpireScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
let currentPlayerData = dataStore.user.currentPlayerData() let currentPlayerData = dataStore.user.currentPlayerData()
Section { Section {
if let reason = licenseMessage { if let reason = licenseMessage {
@ -157,6 +63,8 @@ struct UmpireView: View {
.autocorrectionDisabled() .autocorrectionDisabled()
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} header: {
Text("Mes infos licencié")
} footer: { } footer: {
if dataStore.user.licenceId == nil { if dataStore.user.licenceId == nil {
Text("Si vous avez participé à un tournoi dans les 12 derniers mois, Padel Club peut vous retrouver.") Text("Si vous avez participé à un tournoi dans les 12 derniers mois, Padel Club peut vous retrouver.")
@ -183,30 +91,6 @@ struct UmpireView: View {
} }
} }
} }
Section {
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() _customUmpireView()
Section { Section {
@ -226,110 +110,26 @@ struct UmpireView: View {
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("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.")
} }
} }
if dataStore.user.canEnableOnlinePayment() {
Section {
if let tournamentTemplate = Tournament.getTemplateTournament() {
NavigationLink {
RegistrationSetupView(tournament: tournamentTemplate)
} label: {
Text("Référence")
Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary)
}
} else {
Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité")
}
} header: {
Text("Inscription et paiement en ligne")
} footer: {
Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne")
}
}
Section {
@Bindable var user = dataStore.user
Toggle(isOn: $user.disableRankingFederalRuling) {
Text("Désactiver la règle fédéral")
}
.onChange(of: user.disableRankingFederalRuling) {
dataStore.saveUser()
}
} header: {
Text("Règle fédérale classement finale")
} 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: { .overlay(content: {
Text("Matchs de classement") 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 {
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) { .onChange(of: StoreCenter.main.userId) {
license = dataStore.user.licenceId ?? "" license = dataStore.user.licenceId ?? ""
licenseMessage = nil licenseMessage = nil
} }
.navigationTitle("Juge-Arbitre")
.toolbar {
#if DEBUG
ToolbarItem(placement: .topBarTrailing) {
NetworkStatusView()
// if StoreCenter.main.collectionsCanSynchronize {
// Image(systemName: "checkmark.icloud")
// } else {
// Image(systemName: "icloud.slash")
// }
}
#endif
}
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar(content: { .toolbar(content: {
if focusedField != nil { if focusedField != nil {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
@ -341,30 +141,28 @@ struct UmpireView: View {
}) })
.toolbar { .toolbar {
if focusedField != nil { if focusedField != nil {
ToolbarItem(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
HStack {
if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Effacer") { Button("Effacer") {
_deleteUmpireMail() _deleteUmpireMail()
} }
.buttonStyle(.borderless) .buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") { Button("Effacer") {
_deleteUmpirePhone() _deleteUmpirePhone()
} }
.buttonStyle(.borderless) .buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") { Button("Effacer") {
_deleteUmpireContact() _deleteUmpireContact()
} }
.buttonStyle(.borderless) .buttonStyle(.borderedProminent)
} }
Spacer() Spacer()
Button("Valider") { Button("Valider") {
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
}
} }
} }
} }
@ -385,15 +183,6 @@ struct UmpireView: View {
_confirmlicense() _confirmlicense()
} }
} }
.sheet(isPresented: self.$showSubscriptions, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptions)
.environment(\.colorScheme, .light)
}
})
.sheet(isPresented: self.$showProductIds, content: {
ProductIdsView()
})
.sheet(isPresented: $presentSearchView) { .sheet(isPresented: $presentSearchView) {
let user = dataStore.user let user = dataStore.user
NavigationStack { NavigationStack {
@ -418,13 +207,6 @@ struct UmpireView: View {
} }
} }
} }
.navigationDestination(for: UmpireScreen.self) { screen in
switch screen {
case .login:
LoginView {_ in }
}
}
}
} }
private func _confirmlicense() { private func _confirmlicense() {
@ -439,6 +221,10 @@ struct UmpireView: View {
} }
private func _openCreateAccountView() {
navigation.selectedTab = .myAccount
}
private func _updateUserLicense(license: String?) { private func _updateUserLicense(license: String?) {
guard let license else { return } guard let license else { return }
@ -546,67 +332,10 @@ struct UmpireView: View {
} } } }
} header: { } header: {
Text("Juge-arbitre") Text("Mes infos juge-arbitre")
} footer: { } 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.") Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.")
} }
} }
} }
struct AccountRowView: View {
@EnvironmentObject var dataStore: DataStore
var userName: String
var body: some View {
let isAuthenticated = StoreCenter.main.isAuthenticated
LabeledContent {
if isAuthenticated {
Text(self.userName)
} else if StoreCenter.main.userName != nil {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
}
} label: {
Label("Mon compte", systemImage: "person.fill")
if isAuthenticated && dataStore.user.email.isEmpty == false {
Text(dataStore.user.email)
}
}
}
}
struct ProductIdsView: View {
@State var transactions: [StoreKit.Transaction] = []
var body: some View {
VStack {
List {
LabeledContent("count", value: String(self.transactions.count))
ForEach(self.transactions) { transaction in
if #available(iOS 17.2, *) {
if let offer = transaction.offer {
LabeledContent(transaction.productID, value: "\(offer.type)")
} else {
LabeledContent(transaction.productID, value: "no offer")
}
} else {
Text("need ios 17.2")
}
}
}.onAppear {
Task {
self.transactions = Array(Guard.main.purchasedTransactions)
}
}
}
}
}
//#Preview {
// UmpireView()
//}

@ -111,11 +111,17 @@ struct CourtAvailabilitySettingsView: View {
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 26.0, *) {
BarButtonView("Ajouter une indisponibilité", icon: "plus") {
showingPopover = true
}
} else {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
showingPopover = true showingPopover = true
} }
} }
} }
}
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible") .navigationTitle("Créneau indisponible")

@ -56,6 +56,18 @@ struct PlanningSettingsView: View {
DatePicker(selection: $tournament.startDate) { DatePicker(selection: $tournament.startDate) {
Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized).lineLimit(1) 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 { LabeledContent {
StepperView(count: $tournament.dayDuration, minimum: 1) StepperView(count: $tournament.dayDuration, minimum: 1)
} label: { } label: {

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

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

@ -202,8 +202,7 @@ struct PlayerPopoverView: View {
} }
if licenseIsFocused || amountIsFocused { if licenseIsFocused || amountIsFocused {
ToolbarItem(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
HStack {
Spacer() Spacer()
Button("Confirmer") { Button("Confirmer") {
if licenseIsFocused { if licenseIsFocused {
@ -221,8 +220,7 @@ struct PlayerPopoverView: View {
amountIsFocused = false amountIsFocused = false
} }
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
}
} }
} }
} }

@ -30,6 +30,20 @@ struct PlayerDetailView: View {
return self.tournament.tournamentStore 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) { init(player: PlayerRegistration) {
self.player = player self.player = player
_licenceId = .init(wrappedValue: player.licenceId ?? "") _licenceId = .init(wrappedValue: player.licenceId ?? "")
@ -130,6 +144,11 @@ struct PlayerDetailView: View {
} }
Section { Section {
Toggle(isOn: unranked) {
Text("Non classé\(player.sex == .female ? "e" : "")")
}
LabeledContent { LabeledContent {
TextField("Rang", value: $player.rank, format: .number) TextField("Rang", value: $player.rank, format: .number)
.keyboardType(.decimalPad) .keyboardType(.decimalPad)
@ -147,7 +166,7 @@ struct PlayerDetailView: View {
} }
} }
let maxMaleUnrankedValue: Int = tournament.maleUnrankedValue ?? 90_415 let maxMaleUnrankedValue: Int = tournament.maleUnrankedValue ?? 92_327
if player.isMalePlayer() == false && tournament.tournamentCategory == .men && (player.rank == maxMaleUnrankedValue || player.rank == nil) { if player.isMalePlayer() == false && tournament.tournamentCategory == .men && (player.rank == maxMaleUnrankedValue || player.rank == nil) {
Section { Section {
@ -286,7 +305,7 @@ struct PlayerDetailView: View {
} }
LabeledContent { LabeledContent {
TextField("Téléphone contact", text: $contactPhoneNumber) TextField("Téléphone", text: $contactPhoneNumber)
.focused($focusedField, equals: ._contactPhoneNumber) .focused($focusedField, equals: ._contactPhoneNumber)
.keyboardType(.namePhonePad) .keyboardType(.namePhonePad)
.textContentType(nil) .textContentType(nil)
@ -315,12 +334,12 @@ struct PlayerDetailView: View {
CopyPasteButtonView(pasteValue: player.contactPhoneNumber) CopyPasteButtonView(pasteValue: player.contactPhoneNumber)
PasteButtonView(text: $contactPhoneNumber) PasteButtonView(text: $contactPhoneNumber)
} label: { } label: {
Text("Téléphone contact") Text("Téléphone")
} }
} }
LabeledContent { LabeledContent {
TextField("Email contact", text: $contactEmail) TextField("Email", text: $contactEmail)
.focused($focusedField, equals: ._contactEmail) .focused($focusedField, equals: ._contactEmail)
.keyboardType(.emailAddress) .keyboardType(.emailAddress)
.textContentType(nil) .textContentType(nil)
@ -342,7 +361,7 @@ struct PlayerDetailView: View {
CopyPasteButtonView(pasteValue: player.contactEmail) CopyPasteButtonView(pasteValue: player.contactEmail)
PasteButtonView(text: $contactEmail) PasteButtonView(text: $contactEmail)
} label: { } label: {
Text("Email contact") Text("Email")
} }
} }
} header: { } header: {
@ -372,7 +391,7 @@ struct PlayerDetailView: View {
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) { ShareLink(item: player.pasteData(type: .sharing)) {
Label("Partager", systemImage: "square.and.arrow.up") Label("Partager", systemImage: "square.and.arrow.up")
} }
} }
@ -401,6 +420,7 @@ struct PlayerDetailView: View {
} }
focusedField = nil focusedField = nil
} }
.buttonStyle(.borderedProminent)
} }
} }
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -31,6 +31,7 @@ struct EditingTeamView: View {
@State private var registrationDateModified: Date @State private var registrationDateModified: Date
@State private var uniqueRandomIndex: Int @State private var uniqueRandomIndex: Int
@State private var isDeleting: Bool = false @State private var isDeleting: Bool = false
@State private var showPaymentLinkManager: Bool = false
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
@ -88,8 +89,57 @@ struct EditingTeamView: View {
team.uniqueRandomIndex = 0 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 { var body: some View {
List { 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 { 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.") { 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 editedTeam = team
@ -103,7 +153,7 @@ struct EditingTeamView: View {
} }
} footer: { } footer: {
HStack { HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData()) CopyPasteButtonView(pasteValue: team.playersPasteData(type: .sharing))
Spacer() Spacer()
if team.isWildCard(), team.unsortedPlayers().isEmpty { if team.isWildCard(), team.unsortedPlayers().isEmpty {
TeamPickerView(pickTypeContext: .wildcard) { teamregistration in TeamPickerView(pickTypeContext: .wildcard) { teamregistration in
@ -126,11 +176,10 @@ struct EditingTeamView: View {
} }
.headerProminence(.increased) .headerProminence(.increased)
if team.hasRegisteredOnline() || team.hasPaidOnline() { if team.hasRegisteredOnline() || team.hasPaidOnline() || tournament.enableOnlineRegistration {
Section { Section {
LabeledContent {
Text(team.hasRegisteredOnline() ? "Oui" : "Non") Toggle(isOn: hasRegisteredOnline) {
} label: {
Text("Inscrits en ligne") Text("Inscrits en ligne")
} }
@ -140,6 +189,14 @@ struct EditingTeamView: View {
} label: { } label: {
Text("Payé en ligne") Text("Payé en ligne")
} }
if team.hasPaidOnline() == false {
#if PRODTEST
Button("Récupérer le lien de paiement") {
showPaymentLinkManager = true
}
#endif
}
} }
if let refundMessage, refundMessage.isEmpty == false { if let refundMessage, refundMessage.isEmpty == false {
@ -160,6 +217,8 @@ struct EditingTeamView: View {
} footer: { } footer: {
if team.hasPaidOnline() { if team.hasPaidOnline() {
Text("Le remboursement passe part le service de Stripe qui re-crédite le moyen de paiement utilisé du montant payé.") 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)
} }
} }
} }
@ -186,6 +245,12 @@ struct EditingTeamView: View {
Text("Équipe sur place") Text("Équipe sur place")
} }
} }
NavigationLink {
CallMenuOptionsView(team: team)
.environment(tournament)
} label: {
Text("Modifier la convocation")
}
} }
Section { Section {
@ -209,11 +274,6 @@ struct EditingTeamView: View {
Text(registrationDateModified.localizedWeekDay().capitalized) Text(registrationDateModified.localizedWeekDay().capitalized)
} }
} }
#if DEBUG
.disabled(false)
#else
.disabled(team.hasPaidOnline() || team.hasRegisteredOnline())
#endif
Toggle(isOn: $wildCardBracket) { Toggle(isOn: $wildCardBracket) {
Text("Wildcard Tableau") Text("Wildcard Tableau")
@ -309,6 +369,11 @@ struct EditingTeamView: View {
} }
} }
} }
.ifAvailableiOS26 {
if #available(iOS 26.0, *) {
$0.navigationSubtitle(tournament.tournamentTitle())
}
}
.alert("Attention", isPresented: hasChanged, actions: { .alert("Attention", isPresented: hasChanged, actions: {
Button("Confirmer") { Button("Confirmer") {
if walkOut == false && team.walkOut == true { if walkOut == false && team.walkOut == true {
@ -398,6 +463,19 @@ struct EditingTeamView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $showPaymentLinkManager) {
NavigationStack {
PaymentLinkManagerView(teamRegistration: team)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Fermer") {
showPaymentLinkManager = false
}
}
}
}
}
.fullScreenCover(item: $editedTeam) { editedTeam in .fullScreenCover(item: $editedTeam) { editedTeam in
NavigationStack { NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam) AddTeamView(tournament: tournament, editedTeam: editedTeam)

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

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

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

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

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

@ -101,7 +101,7 @@ struct ConsolationTournamentImportView: View {
Picker(selection: $selectedTournament) { Picker(selection: $selectedTournament) {
Text("Aucun tournoi").tag(nil as Tournament?) Text("Aucun tournoi").tag(nil as Tournament?)
ForEach(tournaments) { tournament in ForEach(tournaments) { tournament in
TournamentCellView(tournament: tournament).tag(tournament) TournamentCellView(tournament: tournament, displayContext: .selection).tag(tournament)
} }
} label: { } label: {
if selectedTournament == nil { if selectedTournament == nil {
@ -218,8 +218,8 @@ struct ConsolationTournamentImportView: View {
case .losers: case .losers:
if selectedGroupStagePosition.isEmpty { if selectedGroupStagePosition.isEmpty {
return $0.qualified == false return $0.qualified == false
} else if let position = $0.groupStagePosition { } else if let position = $0.groupStageObject()?.finalPosition(ofTeam: $0) {
return $0.qualified == false && selectedGroupStagePosition.contains(position) return $0.qualified == false && selectedGroupStagePosition.contains(position + 1)
} else { } else {
return $0.qualified == false return $0.qualified == false
} }

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

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

@ -0,0 +1,240 @@
//
// 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,12 +9,25 @@ import SwiftUI
import PadelClubData import PadelClubData
struct TournamentFormatSelectionView: View { struct TournamentFormatSelectionView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) private var tournament: Tournament @Environment(Tournament.self) private var tournament: Tournament
@State private var globalFormat: MatchFormat = DataStore.shared.user.bracketMatchFormatPreference ?? .nineGamesDecisivePoint
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
@Bindable var tournament = tournament @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 { Section {
MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule", additionalEstimationDuration: tournament.additionalEstimationDuration) MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule", additionalEstimationDuration: tournament.additionalEstimationDuration)
MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau", additionalEstimationDuration: tournament.additionalEstimationDuration) MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau", additionalEstimationDuration: tournament.additionalEstimationDuration)

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

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

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

Loading…
Cancel
Save