Compare commits

..

65 Commits
sync3 ... main

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

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

@ -151,6 +151,15 @@
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 */; };
@ -161,6 +170,15 @@
FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; }; FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30ACF22E8D7078008B6006 /* 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 */; }; 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 */; };
@ -755,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 */; };
@ -1023,6 +1047,9 @@
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>"; };
@ -1030,6 +1057,9 @@
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>"; }; 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>"; }; 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; };
@ -1155,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>"; };
@ -1594,12 +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 */, FFB0FF662E81B671009EDEAC /* OnboardingView.swift */,
FFD783FB2B91B919000F62A6 /* Agenda */, FFD783FB2B91B919000F62A6 /* Agenda */,
FF30AD2E2E92A936008B6006 /* MyAccount */,
FF3F74FA2B91A04B004CFE0E /* Organizer */, FF3F74FA2B91A04B004CFE0E /* Organizer */,
FF3F74FB2B91A060004CFE0E /* Toolbox */, FF3F74FB2B91A060004CFE0E /* Toolbox */,
FF3F74FC2B91A06B004CFE0E /* Umpire */, FF3F74FC2B91A06B004CFE0E /* Umpire */,
@ -1632,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>";
@ -1683,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 */,
@ -1800,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>";
@ -1857,6 +1903,8 @@
FF17CA562CC02FEA003C7323 /* CoachListView.swift */, FF17CA562CC02FEA003C7323 /* CoachListView.swift */,
FF7DCD382CC330260041110C /* TeamRestingView.swift */, FF7DCD382CC330260041110C /* TeamRestingView.swift */,
FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */, FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */,
FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */,
FF2099072EA140A2003CE880 /* TeamMatchesView.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */, FF025AD62BD0C0FB00A86CF8 /* Components */,
); );
path = Team; path = Team;
@ -2270,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 */,
@ -2346,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 */,
@ -2397,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 */,
@ -2440,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 */,
@ -2464,6 +2519,7 @@
FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.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 */,
@ -2542,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 */,
@ -2618,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 */,
@ -2669,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 */,
@ -2712,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 */,
@ -2736,6 +2799,7 @@
FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.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 */,
@ -2792,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 */,
@ -2868,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 */,
@ -2919,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 */,
@ -2962,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 */,
@ -2986,6 +3057,7 @@
FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.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 */,
@ -3173,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\"";
@ -3200,7 +3272,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.53; 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 = "";
@ -3221,7 +3293,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;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3247,7 +3319,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.53; 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 = "";
@ -3340,7 +3412,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 = 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\"";
@ -3367,7 +3439,7 @@
"$(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 = "";
@ -3387,7 +3459,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 = 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;
@ -3413,7 +3485,7 @@
"$(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 = "";

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

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

@ -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,+)

@ -13,8 +13,8 @@ class PaymentService {
static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse { static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse {
let service = try StoreCenter.main.service() let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest( let urlRequest = try service._baseRequest(
servicePath: "resend-payment-email/\(teamRegistrationId)/", servicePath: "resend-payment-email/\(teamRegistrationId)/",
method: .post, method: .post,
requiresToken: true requiresToken: true
) )
@ -27,6 +27,42 @@ class PaymentService {
return try JSON.decoder.decode(SimpleResponse.self, from: data) 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 { enum PaymentError: Error {

@ -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:

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

@ -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)
} }
@ -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) {
if self.teams.count == 1 { let callWord : String = summonType.mainWord()
if simpleMode { if self.teams.count == 1 {
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 { if let caption = summonType.caption() {
Text("\(callWord) cette paire") 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)
} }
Divider()
if simpleMode == false { Button("Contacter") {
Divider() self._summon(byMessage: byMessage, summonType: .contact, forcedEmptyMessage: true)
Button("Contacter") { }
self._summon(byMessage: byMessage, reSummon: false, 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,7 +43,8 @@ struct MenuWarningView: View {
} }
} }
} label: { } label: {
Text("Prévenir") Label("Prévenir", systemImage: "phone")
.labelStyle(.iconOnly)
} }
.menuStyle(.button) .menuStyle(.button)
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)

@ -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,40 +73,52 @@ struct SendToAllView: View {
.labelsHidden() .labelsHidden()
.pickerStyle(.inline) .pickerStyle(.inline)
} }
if let event {
Section { LabeledContent {
ForEach(tournament.groupStages()) { groupStage in Text(event.selectedTeams().filter({ contactRecipients.isEmpty || contactRecipients.contains($0.tournament) }).count.formatted())
let teams = groupStage.teams() } label: {
if teams.isEmpty == false { Text("Participants")
LabeledContent { }
Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix)
} label: { let confirmedTournaments = event.confirmedTournaments()
Text(groupStage.groupStageTitle()) ForEach(confirmedTournaments) { tournament in
TournamentCellView(tournament: tournament).tag(tournament.id)
}
} else if let tournament {
Section {
ForEach(tournament.groupStages()) { groupStage in
let teams = groupStage.teams()
if teams.isEmpty == false {
LabeledContent {
Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix)
} label: {
Text(groupStage.groupStageTitle())
}
.tag(groupStage.id)
} }
.tag(groupStage.id)
} }
} ForEach(tournament.rounds()) { round in
ForEach(tournament.rounds()) { round in let teams = round.teams()
let teams = round.teams() if teams.isEmpty == false {
if teams.isEmpty == false { LabeledContent {
LabeledContent { Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix)
Text(teams.count.formatted() + " équipe" + teams.count.pluralSuffix) } label: {
} label: { Text(round.roundTitle())
Text(round.roundTitle()) }
.tag(round.id)
} }
.tag(round.id)
} }
Toggle("Inclure la liste d'attente", isOn: $includeWaitingList)
if includeWaitingList {
Toggle("Seulement la liste d'attente", isOn: $onlyWaitingList)
}
} footer: {
Text("Si vous ne souhaitez pas contacter toutes les équipes, choisissez un ou plusieurs groupes d'équipes manuellement.")
} }
Toggle("Inclure la liste d'attente", isOn: $includeWaitingList)
if includeWaitingList {
Toggle("Seulement la liste d'attente", isOn: $onlyWaitingList)
}
} footer: {
Text("Si vous ne souhaitez pas contacter toutes les équipes, choisissez un ou plusieurs groupes d'équipes manuellement.")
} }
if addLink { 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()
} }

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

@ -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,10 +195,6 @@ struct EventSettingsView: View {
}) })
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) {
_linkLabel()
}
if focusedField != nil { if focusedField != nil {
ToolbarItemGroup(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._name, eventName.isEmpty == false { if focusedField == ._name, eventName.isEmpty == false {
@ -275,7 +284,7 @@ struct EventSettingsView: View {
} }
} }
} label: { } label: {
Text("Liens") Text("Liens à partager")
} }
} }

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

@ -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,23 +128,41 @@ struct ClubsView: View {
.tint(.master) .tint(.master)
} }
.toolbar { .toolbar {
ToolbarItemGroup(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button { if #available(iOS 26.0, *) {
presentClubSearchView = true Button("Chercher", systemImage: "magnifyingglass") {
} label: { presentClubSearchView = true
Image(systemName: "magnifyingglass.circle.fill") }
.resizable() } else {
.scaledToFit() Button {
.frame(minHeight: 28) presentClubSearchView = true
} label: {
Image(systemName: "magnifyingglass.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
} }
}
Button { if #available(iOS 26.0, *) {
newClub = Club.newEmptyInstance() ToolbarSpacer(placement: .topBarTrailing)
} label: { }
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit() ToolbarItem(placement: .topBarTrailing) {
.frame(minHeight: 28) if #available(iOS 26.0, *) {
Button("Ajouter", systemImage: "plus") {
newClub = Club.newEmptyInstance()
}
} else {
Button {
newClub = Club.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
} }
} }
} }

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

@ -94,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() {

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

@ -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 {

@ -113,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
@ -546,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()

@ -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 {
_listView(_tournaments) if sectionImporting == sectionIndex {
LabeledContent {
ProgressView()
} label: {
Text("Récupération en cours")
}
} else {
_listView(_tournaments)
}
} header: { } header: {
HStack { HStack {
Text(section.monthYearFormatted) Text(section.monthYearFormatted)
@ -50,21 +58,16 @@ 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 for federalTournament in federalTournaments {
for federalTournament in federalTournaments { await _importFederalTournamentBatch(federalTournament: federalTournament)
await _importFederalTournamentBatch(federalTournament: federalTournament)
}
sectionImporting = nil
} }
} sectionImporting = nil
if sectionImporting == sectionIndex {
Spacer()
ProgressView()
} }
} }
.disabled(sectionImporting == sectionIndex)
} }
} }
} }
@ -373,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")
} }
@ -448,17 +463,7 @@ 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

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

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

@ -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 { NavigationLink {
List(selection: $ongoingViewModel.selectedTournaments) {
ForEach(ongoingViewModel.tournaments()) { tournament in
TournamentCellView(tournament: tournament)
.tag(tournament.id)
}
}
.onChange(of: ongoingViewModel.selectedTournaments, { oldValue, newValue in
DataStore.shared.resetOngoingCache()
})
.environment(\.editMode, Binding.constant(EditMode.active))
.navigationTitle("Tournois à masquer")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
} label: {
Label("Masquer des tournois", systemImage: "circle.grid.2x2.topleft.checkmark.filled")
}
if ongoingViewModel.destination == .followUp {
Divider()
Toggle(isOn: $ongoingViewModel.hideUnconfirmedMatches) { Toggle(isOn: $ongoingViewModel.hideUnconfirmedMatches) {
Text("masquer non confirmés") Text("Masquer non confirmés")
} }
Toggle(isOn: $ongoingViewModel.hideNotReadyMatches) { Toggle(isOn: $ongoingViewModel.hideNotReadyMatches) {
Text("masquer incomplets") Text("Masquer incomplets")
} }
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
} }
.symbolVariant(ongoingViewModel.areFiltersEnabled() ? .fill : .none)
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
showMatchPicker = true
} label: { } label: {
Image(systemName: "rectangle.stack.badge.plus") Image(systemName: "line.3.horizontal.decrease")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
} }
} }
} }

@ -21,6 +21,10 @@ 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)
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) LabeledContent("Last synced object date", value: self._lastSyncDate)
if isSynchronizing { if isSynchronizing {
HStack { HStack {
@ -94,6 +98,12 @@ 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)"
} }

@ -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
@ -39,44 +40,6 @@ struct ToolboxView: View {
@Bindable var navigation = navigation @Bindable var navigation = navigation
NavigationStack(path: $navigation.toolboxPath) { NavigationStack(path: $navigation.toolboxPath) {
List { List {
Section {
Link(destination: URLs.main.url) {
Text("Accéder à padelclub.app")
}
.contextMenu {
ShareLink(item: URLs.main.url)
}
SupportButtonView(supportButtonType: .bugReport)
Link(destination: URLs.appReview.url) {
Text("Partagez vos impressions !")
}
Link(destination: URLs.instagram.url) {
Text("Compte Instagram PadelClub.app")
}
}
if self.showDebugViews {
DebugView()
}
Section {
NavigationLink {
SelectablePlayerListView(isPresented: false, lastDataSource: true)
.toolbar(.hidden, for: .tabBar)
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
NavigationLink {
RankCalculatorView()
} label: {
Label("Calculateur de points", systemImage: "scalemass")
}
}
Section { Section {
NavigationLink { NavigationLink {
PadelClubView() PadelClubView()
@ -86,8 +49,7 @@ struct ToolboxView: View {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(.green)
} label: { } label: {
Text(_lastDataSourceDate.monthYearFormatted) Label(_lastDataSourceDate.monthYearFormatted, systemImage: "calendar.badge.checkmark")
Text("Classement mensuel utilisé")
} }
} else { } else {
LabeledContent { LabeledContent {
@ -95,16 +57,52 @@ struct ToolboxView: View {
.tint(.logoRed) .tint(.logoRed)
} label: { } label: {
if let _mostRecentDateAvailable { if let _mostRecentDateAvailable {
Text(_mostRecentDateAvailable.monthYearFormatted) Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "calendar.badge")
} else { } else {
Text("Aucun") Label("Aucun", systemImage: "calendar.badge.exclamationmark")
} }
Text("Classement mensuel disponible") Text("Classement mensuel disponible")
} }
} }
} }
} header: {
Text("Classement mensuel utilisé")
}
Section {
Button {
displaySearchPlayer = true
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
NavigationLink {
RankCalculatorView()
} label: {
Label("Calculateur de points", systemImage: "scalemass")
}
NavigationLink {
MatchFormatGuideView()
} label: {
Label("Formats et limites", systemImage: "clock")
}
} }
Section {
Link(destination: URLs.main.url) {
Label("Padel Club sur le Web", systemImage: "link")
}
.contextMenu {
ShareLink(item: URLs.main.url)
}
ShareLink(item: URLs.appStore.url) {
Label("Padel Club sur l'App Store", systemImage: "square.and.arrow.up")
}
}
Section { Section {
Link("Guide de la compétition", destination: URLs.padelCompetitionGeneralGuide.url) Link("Guide de la compétition", destination: URLs.padelCompetitionGeneralGuide.url)
Link("CDC des tournois", destination: URLs.padelCompetitionSpecificGuide.url) Link("CDC des tournois", destination: URLs.padelCompetitionSpecificGuide.url)
@ -119,21 +117,10 @@ 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 {
RowButtonView("Effacer les logs", role: .destructive) { RowButtonView("Effacer les logs", role: .destructive) {
StoreCenter.main.resetLoggingCollections() StoreCenter.main.resetLoggingCollections()
@ -141,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
@ -163,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)")
@ -180,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()
} }
} }
} }
@ -245,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,50 +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 List {
NavigationStack(path: $navigation.umpirePath) { if StoreCenter.main.isAuthenticated {
List {
PurchaseListView()
Section {
Button {
self.showSubscriptions = true
} label: {
Label("Les offres", systemImage: "bookmark.fill")
}.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
self.showProductIds = true
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
self.showSubscriptions = true
}
)
}
if StoreCenter.main.isAuthenticated {
NavigationLink {
AccountView(user: dataStore.user) { }
} label: {
AccountRowView(userName: dataStore.user.username)
}
} else {
NavigationLink(value: UmpireScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
let currentPlayerData = dataStore.user.currentPlayerData() let currentPlayerData = dataStore.user.currentPlayerData()
Section { Section {
if let reason = licenseMessage { if let reason = licenseMessage {
@ -90,11 +47,11 @@ struct UmpireView: View {
if let currentPlayerData { if let currentPlayerData {
//todo palmares //todo palmares
ImportedPlayerView(player: currentPlayerData, showProgression: true) ImportedPlayerView(player: currentPlayerData, showProgression: true)
// NavigationLink { // NavigationLink {
// //
// } label: { // } label: {
// ImportedPlayerView(player: currentPlayerData) // ImportedPlayerView(player: currentPlayerData)
// } // }
} else { } else {
RowButtonView("Ma fiche joueur", systemImage: "person.bust") { RowButtonView("Ma fiche joueur", systemImage: "person.bust") {
presentSearchView = true presentSearchView = true
@ -106,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.")
@ -124,7 +83,7 @@ struct UmpireView: View {
self.licenseMessage = nil self.licenseMessage = nil
self.dataStore.saveUser() self.dataStore.saveUser()
} }
} label: { } label: {
Text("options") Text("options")
.foregroundStyle(Color.master) .foregroundStyle(Color.master)
@ -132,231 +91,119 @@ struct UmpireView: View {
} }
} }
} }
_customUmpireView()
Section { Section {
NavigationLink { @Bindable var user = dataStore.user
ClubsView() if dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone {
} label: { Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed)
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()
Section { Toggle(isOn: $user.hideUmpireMail) {
@Bindable var user = dataStore.user Text("Masquer l'email")
if dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone {
Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed)
}
Toggle(isOn: $user.hideUmpireMail) {
Text("Masquer l'email")
}
Toggle(isOn: $user.hideUmpirePhone) {
Text("Masquer le téléphone")
}
} footer: {
Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.")
} }
} Toggle(isOn: $user.hideUmpirePhone) {
Text("Masquer le téléphone")
if dataStore.user.canEnableOnlinePayment() {
Section {
if let tournamentTemplate = Tournament.getTemplateTournament() {
NavigationLink {
RegistrationSetupView(tournament: tournamentTemplate)
} label: {
Text("Référence")
Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary)
}
} else {
Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité")
}
} header: {
Text("Inscription et paiement en ligne")
} footer: {
Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne")
} }
}
Section {
@Bindable var user = dataStore.user
Toggle(isOn: $user.disableRankingFederalRuling) {
Text("Désactiver la règle fédéral")
}
.onChange(of: user.disableRankingFederalRuling) {
dataStore.saveUser()
}
} header: {
Text("Règle fédérale classement finale")
} footer: { } footer: {
Text("Dernier de poule ≠ dernier du tournoi") Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.")
} }
}
Section { }
@Bindable var user = dataStore.user .overlay(content: {
Picker(selection: $user.loserBracketMode) { if StoreCenter.main.isAuthenticated == false {
ForEach(LoserBracketMode.allCases) { ContentUnavailableView {
Text($0.localizedLoserBracketMode()).tag($0) Label("Aucun compte", systemImage: "person.crop.circle.badge.exclamationmark")
} } description: {
} label: { Text("Créer un compte Padel Club pour personnaliser vos informations de Juge-Arbitre")
Text("Position des perdants") } actions: {
} RowButtonView("Créer un compte") {
.onChange(of: user.loserBracketMode) { _openCreateAccountView()
dataStore.saveUser()
} }
} header: {
Text("Matchs de classement")
} }
}
Section { })
NavigationLink { .onChange(of: StoreCenter.main.userId) {
GlobalSettingsView() license = dataStore.user.licenceId ?? ""
} label: { licenseMessage = nil
Label("Formats de jeu par défaut", systemImage: "megaphone") }
} .navigationBarBackButtonHidden(focusedField != nil)
NavigationLink { .toolbarBackground(.visible, for: .navigationBar)
DurationSettingsView() .toolbar(content: {
} label: { if focusedField != nil {
Label("Définir les durées moyennes", systemImage: "deskclock") ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
} }
} footer: {
Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.")
} }
// Section {
// Text("Tenup ID")
// }
//
// Section {
// Text("Tournois")
// }
//
// Section {
// NavigationLink {
//
// } label: {
// Text("Favori")
// }
// NavigationLink {
//
// } label: {
// Text("Black list")
// }
// }
}
.onChange(of: StoreCenter.main.userId) {
license = dataStore.user.licenceId ?? ""
licenseMessage = nil
} }
.navigationTitle("Juge-Arbitre") })
.navigationBarBackButtonHidden(focusedField != nil) .toolbar {
.toolbar(content: { if focusedField != nil {
if focusedField != nil { ToolbarItemGroup(placement: .keyboard) {
ToolbarItem(placement: .topBarLeading) { if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Annuler", role: .cancel) { Button("Effacer") {
focusedField = nil _deleteUmpireMail()
} }
} .buttonStyle(.borderedProminent)
} } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
}) Button("Effacer") {
.toolbar { _deleteUmpirePhone()
if focusedField != nil {
ToolbarItemGroup(placement: .keyboard) {
if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
Button("Effacer") {
_deleteUmpireMail()
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") {
_deleteUmpirePhone()
}
.buttonStyle(.borderedProminent)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") {
_deleteUmpireContact()
}
.buttonStyle(.borderedProminent)
} }
Spacer() .buttonStyle(.borderedProminent)
Button("Valider") { } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
focusedField = nil Button("Effacer") {
_deleteUmpireContact()
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
Spacer()
Button("Valider") {
focusedField = nil
}
.buttonStyle(.borderedProminent)
} }
} }
.onChange(of: [dataStore.user.umpireCustomMail, dataStore.user.umpireCustomPhone, dataStore.user.umpireCustomContact]) { }
self.dataStore.saveUser() .onChange(of: [dataStore.user.umpireCustomMail, dataStore.user.umpireCustomPhone, dataStore.user.umpireCustomContact]) {
} self.dataStore.saveUser()
.onChange(of: [dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone]) { }
self.dataStore.saveUser() .onChange(of: [dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone]) {
} self.dataStore.saveUser()
.onChange(of: focusedField) { old, new in }
if old == ._umpireCustomMail { .onChange(of: focusedField) { old, new in
_confirmUmpireMail() if old == ._umpireCustomMail {
} else if old == ._umpireCustomPhone { _confirmUmpireMail()
_confirmUmpirePhone() } else if old == ._umpireCustomPhone {
} else if old == ._umpireCustomContact { _confirmUmpirePhone()
_confirmUmpireContact() } else if old == ._umpireCustomContact {
} else if old == ._licenceId { _confirmUmpireContact()
_confirmlicense() } else if old == ._licenceId {
} _confirmlicense()
} }
.sheet(isPresented: self.$showSubscriptions, content: { }
NavigationStack { .sheet(isPresented: $presentSearchView) {
SubscriptionView(isPresented: self.$showSubscriptions) let user = dataStore.user
.environment(\.colorScheme, .light) NavigationStack {
} SelectablePlayerListView(allowSelection: 1, searchField: user.firstName + " " + user.lastName, playerSelectionAction: { players in
}) if let player = players.first {
.sheet(isPresented: self.$showProductIds, content: { if user.clubsObjects().contains(where: { $0.code == player.clubCode }) == false {
ProductIdsView() let userClub = Club.findOrCreate(name: player.clubName!, code: player.clubCode)
}) if userClub.hasBeenCreated(by: StoreCenter.main.userId) {
.sheet(isPresented: $presentSearchView) { dataStore.clubs.addOrUpdate(instance: userClub)
let user = dataStore.user
NavigationStack {
SelectablePlayerListView(allowSelection: 1, searchField: user.firstName + " " + user.lastName, playerSelectionAction: { players in
if let player = players.first {
if user.clubsObjects().contains(where: { $0.code == player.clubCode }) == false {
let userClub = Club.findOrCreate(name: player.clubName!, code: player.clubCode)
if userClub.hasBeenCreated(by: StoreCenter.main.userId) {
dataStore.clubs.addOrUpdate(instance: userClub)
}
user.setUserClub(userClub)
} }
self._updateUserLicense(license: player.license?.computedLicense) user.setUserClub(userClub)
} }
}) self._updateUserLicense(license: player.license?.computedLicense)
}
.task {
do {
try await dataStore.clubs.loadDataFromServerIfAllowed()
} catch {
Logger.error(error)
} }
} })
} }
.navigationDestination(for: UmpireScreen.self) { screen in .task {
switch screen { do {
case .login: try await dataStore.clubs.loadDataFromServerIfAllowed()
LoginView {_ in } } catch {
Logger.error(error)
} }
} }
} }
@ -374,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 }
@ -481,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()
//}

@ -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,29 +172,28 @@ 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) { Label("Afficher tous les matchs", systemImage: "eye").tag(true)
Text("Afficher tous les matchs").tag(true) Label("Masquer les matchs terminés", systemImage: "eye.slash").tag(false)
Text("Masquer les matchs terminés").tag(false) } label: {
} label: {
Text("Option de filtrage")
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de filtrage") Text("Option de filtrage")
} }
.labelsHidden()
Divider() .pickerStyle(.inline)
} header: {
Text("Option de filtrage")
}
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()
@ -748,16 +763,27 @@ 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
} }
} }
} }
@ -767,23 +793,34 @@ 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 }
} }
} }

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

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

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

@ -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 {
otherTeamValueBinding.wrappedValue = setFormat.scoreToWin + 1 if setFormat == .three {
otherTeamValueBinding.wrappedValue = setFormat.scoreToWin
} else {
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")
} }

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

@ -90,8 +90,15 @@ struct SupportButtonView: View {
_zip() _zip()
} }
case .bugReport: case .bugReport:
Button("Signaler un problème") { if showIcon {
_zip() Button("Signaler un problème", systemImage: "square.and.pencil") {
_zip()
}
.labelStyle(.titleAndIcon)
} else {
Button("Signaler un problème") {
_zip()
}
} }
} }
} }
@ -147,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)
} }

@ -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")
} }
@ -142,7 +191,11 @@ struct EditingTeamView: View {
} }
if team.hasPaidOnline() == false { if team.hasPaidOnline() == false {
PaymentRequestButton(teamRegistration: team) #if PRODTEST
Button("Récupérer le lien de paiement") {
showPaymentLinkManager = true
}
#endif
} }
} }
@ -164,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)
} }
} }
} }
@ -190,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 {
@ -308,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 {
@ -397,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())
}

@ -16,7 +16,7 @@ struct PaymentRequestButton: View {
@State private var alertMessage = "" @State private var alertMessage = ""
var body: some View { var body: some View {
Button("Renvoyer email de paiement") { 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() resendEmail()
} }
.disabled(isLoading) .disabled(isLoading)

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

@ -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 {

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

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

@ -25,12 +25,10 @@ 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()
Section { Section {
@ -73,6 +71,7 @@ struct TournamentMatchFormatsSettingsView: View {
.deferredRendering(for: .seconds(2)) .deferredRendering(for: .seconds(2))
} }
} }
.navigationTitle("Formats")
} }
private func _confirmOrSave() { private func _confirmOrSave() {

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

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

@ -10,7 +10,7 @@ import LeStorage
import PadelClubData import PadelClubData
struct TableStructureView: View { struct TableStructureView: View {
@Environment(Tournament.self) private var tournament: Tournament var tournament: Tournament
@EnvironmentObject private var dataStore: DataStore @EnvironmentObject private var dataStore: DataStore
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@State private var presentRefreshStructureWarning: Bool = false @State private var presentRefreshStructureWarning: Bool = false
@ -24,6 +24,11 @@ struct TableStructureView: View {
@State private var buildWildcards: Bool = true @State private var buildWildcards: Bool = true
@FocusState private var stepperFieldIsFocused: Bool @FocusState private var stepperFieldIsFocused: Bool
@State private var confirmReset: Bool = false @State private var confirmReset: Bool = false
@State private var selectedTournament: Tournament?
@State private var initialSeedCount: Int = 0
@State private var initialSeedRound: Int = 0
@State private var showSeedRepartition: Bool = false
@State private var seedRepartition: [Int] = []
func displayWarning() -> Bool { func displayWarning() -> Bool {
let unsortedTeamsCount = tournament.unsortedTeamsCount() let unsortedTeamsCount = tournament.unsortedTeamsCount()
@ -60,9 +65,22 @@ struct TableStructureView: View {
var tsPure: Int { var tsPure: Int {
max(teamCount - groupStageCount * teamsPerGroupStage, 0) max(teamCount - groupStageCount * teamsPerGroupStage, 0)
} }
var tf: Int {
@ViewBuilder max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
}
init(tournament: Tournament) {
self.tournament = tournament
_teamCount = .init(wrappedValue: tournament.teamCount)
_groupStageCount = .init(wrappedValue: tournament.groupStageCount)
_teamsPerGroupStage = .init(wrappedValue: tournament.teamsPerGroupStage)
_qualifiedPerGroupStage = .init(wrappedValue: tournament.qualifiedPerGroupStage)
_groupStageAdditionalQualified = .init(wrappedValue: tournament.groupStageAdditionalQualified)
_initialSeedCount = .init(wrappedValue: tournament.initialSeedCount)
_initialSeedRound = .init(wrappedValue: tournament.initialSeedRound)
}
var body: some View { var body: some View {
List { List {
if displayWarning() { if displayWarning() {
@ -79,17 +97,34 @@ struct TableStructureView: View {
} label: { } label: {
Text("Préréglage") Text("Préréglage")
} }
.disabled(selectedTournament != nil)
} footer: { } footer: {
Text(structurePreset.localizedDescriptionStructurePresetTitle()) Text(structurePreset.localizedDescriptionStructurePresetTitle())
} }
.onChange(of: structurePreset) { .onChange(of: structurePreset) {
_updatePreset() _updatePreset()
} }
Section {
NavigationLink {
TournamentSelectorView(selectedTournament: $selectedTournament)
.environment(tournament)
} label: {
if let selectedTournament {
TournamentCellView(tournament: selectedTournament, displayContext: .selection)
} else {
Text("À partir d'un tournoi existant")
}
}
}
.onChange(of: selectedTournament) {
_updatePreset()
}
} }
Section { Section {
LabeledContent { LabeledContent {
StepperView(count: $teamCount, minimum: 4, maximum: 128) { StepperView(count: $teamCount, minimum: 2, maximum: 128) {
} submitFollowUpAction: { } submitFollowUpAction: {
_verifyValueIntegrity() _verifyValueIntegrity()
@ -203,103 +238,156 @@ struct TableStructureView: View {
} }
} }
Section { if selectedTournament == nil {
let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
if groupStageCount > 0 { Section {
if groupStageCount > 0 {
if structurePreset != .doubleGroupStage {
LabeledContent {
Text(teamsFromGroupStages.formatted())
} label: {
Text("Équipes en poule")
}
LabeledContent {
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
} label: {
Text("Équipes qualifiées de poule")
}
}
}
if structurePreset != .doubleGroupStage { if structurePreset != .doubleGroupStage {
LabeledContent { LabeledContent {
Text(teamsFromGroupStages.formatted()) Text(tsPure.formatted())
} label: { } label: {
Text("Équipes en poule") Text("Équipes à placer en tableau")
if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
Text("Attention !").foregroundStyle(.red)
}
} }
// if groupStageCount > 0 {
// LabeledContent {
// Text(tf.formatted())
// } label: {
// Text("Effectif tableau")
// }
// }
} else {
LabeledContent { LabeledContent {
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted()) let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage
Text((mp1 + mp2).formatted())
} label: { } label: {
Text("Équipes qualifiées de poule") Text("Total de matchs")
} }
} }
}
if structurePreset != .doubleGroupStage {
LabeledContent { LabeledContent {
Text(tsPure.formatted()) FooterButtonView("configurer") {
} label: { showSeedRepartition = true
Text("Nombre de têtes de série")
if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
Text("Attention !").foregroundStyle(.red)
} }
}
LabeledContent {
Text(tf.formatted())
} label: { } label: {
Text("Équipes en tableau final") if tournament.state() == .build {
Text("Répartition des équipes")
} else if selectedTournament != nil {
Text("La configuration du tournoi séléctionné sera utilisée.")
} else {
Text(_seeds())
}
} }
} else { .onAppear {
LabeledContent { if seedRepartition.isEmpty && tournament.state() == .initial && selectedTournament == nil {
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage }
Text((mp1 + mp2).formatted())
} label: {
Text("Total de matchs")
} }
}
} footer: { } footer: {
if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0 { if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0, tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified {
if tsPure > teamCount / 2 {
Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif.").foregroundStyle(.red)
} else if tsPure < teamCount / 8 {
Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif.").foregroundStyle(.red)
} else if tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified {
Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red) Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red)
} }
} }
}
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() { Section {
Section { Toggle("Avec wildcards", isOn: $buildWildcards)
Toggle("Avec wildcards", isOn: $buildWildcards) } footer: {
} footer: { Text("Padel Club réservera des places pour eux dans votre liste d'inscription.")
Text("Padel Club réservera des places pour eux dans votre liste d'inscription.") }
} }
}
if tournament.rounds().isEmpty && tournament.state() == .build {
if tournament.rounds().isEmpty { Section {
Section { RowButtonView("Ajouter un tableau", role: .destructive) {
RowButtonView("Ajouter un tableau", role: .destructive) { tournament.buildBracket(minimalBracketTeamCount: 4)
tournament.buildBracket(minimalBracketTeamCount: 4) }
} footer: {
Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.")
} }
} footer: {
Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.")
} }
} }
if tournament.state() != .initial { if tournament.state() != .initial {
if seedRepartition.isEmpty == false {
Section {
RowButtonView("Répartir les équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") {
await _handleSeedRepartition()
}
} footer: {
Text("Cette action va effacer le répartition actuelle des équipes dans le tableau et la refaire, les manches seront ré-initialisées")
}
}
Section { Section {
RowButtonView("Sauver sans reconstuire l'existant") { RowButtonView("Sauver sans reconstuire l'existant") {
_saveWithoutRebuild() _saveWithoutRebuild()
} }
} footer: {
Text("Cette action sauve les paramètres du tournoi sans modifier vos poules / tableaux actuels.")
} }
Section { Section {
RowButtonView("Reconstruire les poules", role:.destructive) { RowButtonView("Reconstruire les poules", role:.destructive) {
_save(rebuildEverything: false) await _save(rebuildEverything: false)
} }
} footer: {
Text("Cette action efface les poules existantes et les reconstruits, leurs données seront perdues.")
} }
Section { Section {
RowButtonView("Tout refaire", role: .destructive) { RowButtonView("Tout refaire", role: .destructive) {
_save(rebuildEverything: true) await _save(rebuildEverything: true)
} }
} footer: {
Text("Cette action efface le tableau et les poules existantes et reconstruit tout de zéro, leurs données seront perdues.")
} }
Section { Section {
RowButtonView("Remise-à-zéro", role: .destructive) { RowButtonView("Remise-à-zéro", role: .destructive) {
_reset() _reset()
} }
} footer: {
Text("Retourne à la structure initiale, comme si vous veniez de créer le tournoi. Les données existantes seront perdues.")
}
Section {
RowButtonView("Retirer toutes les équipes de poules", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetGroupeStagePosition()
}
}
}
Section {
RowButtonView("Retirer toutes les équipes du tableau", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetBracketPosition()
}
}
} }
} }
} }
@ -312,19 +400,20 @@ struct TableStructureView: View {
} }
} }
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.onAppear { .sheet(isPresented: $showSeedRepartition, content: {
teamCount = tournament.teamCount NavigationStack {
groupStageCount = tournament.groupStageCount HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in
teamsPerGroupStage = tournament.teamsPerGroupStage self.seedRepartition = seedRepartition
qualifiedPerGroupStage = tournament.qualifiedPerGroupStage }
groupStageAdditionalQualified = tournament.groupStageAdditionalQualified }
} })
.onChange(of: teamCount) { .onChange(of: teamCount) {
if teamCount != tournament.teamCount { if teamCount != tournament.teamCount {
updatedElements.insert(.teamCount) updatedElements.insert(.teamCount)
} else { } else {
updatedElements.remove(.teamCount) updatedElements.remove(.teamCount)
} }
_verifyValueIntegrity()
} }
.onChange(of: groupStageCount) { .onChange(of: groupStageCount) {
if groupStageCount != tournament.groupStageCount { if groupStageCount != tournament.groupStageCount {
@ -336,25 +425,31 @@ struct TableStructureView: View {
if structurePreset.isFederalPreset(), groupStageCount == 0 { if structurePreset.isFederalPreset(), groupStageCount == 0 {
teamCount = structurePreset.tableDimension() teamCount = structurePreset.tableDimension()
} }
_verifyValueIntegrity()
} }
.onChange(of: teamsPerGroupStage) { .onChange(of: teamsPerGroupStage) {
if teamsPerGroupStage != tournament.teamsPerGroupStage { if teamsPerGroupStage != tournament.teamsPerGroupStage {
updatedElements.insert(.teamsPerGroupStage) updatedElements.insert(.teamsPerGroupStage)
} else { } else {
updatedElements.remove(.teamsPerGroupStage) updatedElements.remove(.teamsPerGroupStage)
} } }
_verifyValueIntegrity()
}
.onChange(of: qualifiedPerGroupStage) { .onChange(of: qualifiedPerGroupStage) {
if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage { if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage {
updatedElements.insert(.qualifiedPerGroupStage) updatedElements.insert(.qualifiedPerGroupStage)
} else { } else {
updatedElements.remove(.qualifiedPerGroupStage) updatedElements.remove(.qualifiedPerGroupStage)
} } }
_verifyValueIntegrity()
}
.onChange(of: groupStageAdditionalQualified) { .onChange(of: groupStageAdditionalQualified) {
if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified { if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified {
updatedElements.insert(.groupStageAdditionalQualified) updatedElements.insert(.groupStageAdditionalQualified)
} else { } else {
updatedElements.remove(.groupStageAdditionalQualified) updatedElements.remove(.groupStageAdditionalQualified)
} }
_verifyValueIntegrity()
} }
.toolbar { .toolbar {
if tournament.state() != .initial { if tournament.state() != .initial {
@ -378,13 +473,17 @@ struct TableStructureView: View {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial { if tournament.state() == .initial {
ButtonValidateView { ButtonValidateView {
_save(rebuildEverything: true) Task {
await _save(rebuildEverything: true)
}
} }
} else { } else {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
ButtonValidateView(role: .destructive) { ButtonValidateView(role: .destructive) {
if requirements.isEmpty { if requirements.isEmpty {
_save(rebuildEverything: false) Task {
await _save(rebuildEverything: false)
}
} else { } else {
presentRefreshStructureWarning = true presentRefreshStructureWarning = true
} }
@ -395,11 +494,15 @@ struct TableStructureView: View {
} }
Button("Reconstruire les poules") { Button("Reconstruire les poules") {
_save(rebuildEverything: false) Task {
await _save(rebuildEverything: false)
}
} }
Button("Tout refaire", role: .destructive) { Button("Tout refaire", role: .destructive) {
_save(rebuildEverything: true) Task {
await _save(rebuildEverything: true)
}
} }
}, message: { }, message: {
ForEach(Array(requirements)) { requirement in ForEach(Array(requirements)) { requirement in
@ -418,8 +521,25 @@ struct TableStructureView: View {
} }
} }
private func _seeds() -> String {
if seedRepartition.isEmpty || seedRepartition.reduce(0, +) == 0 {
return "Aucune configuration"
}
return seedRepartition.enumerated().compactMap { (index, count) in
if count > 0 {
return RoundRule.roundName(fromRoundIndex: index) + " : \(count)"
} else {
return nil
}
}.joined(separator: ", ")
}
private func _reset() { private func _reset() {
let tc = tournament.teamCount
tournament.unsortedTeams().forEach {
$0.resetPositions()
}
tournament.removeWildCards() tournament.removeWildCards()
tournament.deleteGroupStages() tournament.deleteGroupStages()
tournament.deleteStructure() tournament.deleteStructure()
@ -430,7 +550,8 @@ struct TableStructureView: View {
_updatePreset() _updatePreset()
} }
tournament.teamCount = teamCount teamCount = tc
tournament.teamCount = tc
tournament.groupStageCount = groupStageCount tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
@ -477,9 +598,9 @@ struct TableStructureView: View {
} }
} }
private func _save(rebuildEverything: Bool = false) { private func _save(rebuildEverything: Bool = false) async {
_verifyValueIntegrity() _verifyValueIntegrity(keepSeedRepartition: true)
let tc = tournament.teamCount
do { do {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
@ -490,12 +611,63 @@ struct TableStructureView: View {
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
if rebuildEverything { if rebuildEverything {
if let selectedTournament {
tournament.matchFormat = selectedTournament.matchFormat
tournament.groupStageMatchFormat = selectedTournament.groupStageMatchFormat
tournament.loserBracketMatchFormat = selectedTournament.loserBracketMatchFormat
tournament.initialSeedRound = selectedTournament.initialSeedRound
tournament.initialSeedCount = selectedTournament.initialSeedCount
} else {
tournament.initialSeedRound = seedRepartition.firstIndex(where: { $0 > 0 }) ?? 0
tournament.initialSeedCount = seedRepartition.first(where: { $0 > 0 }) ?? 0
}
tournament.removeWildCards() tournament.removeWildCards()
if structurePreset.hasWildcards(), buildWildcards { if structurePreset.hasWildcards(), buildWildcards {
tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket) tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket)
tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage) tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage)
} }
tournament.deleteAndBuildEverything(preset: structurePreset) tournament.deleteAndBuildEverything(preset: structurePreset)
if let selectedTournament {
let oldTournamentStart = selectedTournament.startDate
let newTournamentStart = tournament.startDate
let calendar = Calendar.current
let oldComponents = calendar.dateComponents([.hour, .minute, .second], from: oldTournamentStart)
var newComponents = calendar.dateComponents([.year, .month, .day], from: newTournamentStart)
newComponents.hour = oldComponents.hour
newComponents.minute = oldComponents.minute
newComponents.second = oldComponents.second ?? 0
if let updatedStartDate = calendar.date(from: newComponents) {
tournament.startDate = updatedStartDate
}
let allRounds = selectedTournament.rounds()
let allRoundsNew = tournament.rounds()
allRoundsNew.forEach { round in
if let pRound = allRounds.first(where: { r in
round.index == r.index
}) {
round.setData(from: pRound, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
}
}
let allGroupStages = selectedTournament.allGroupStages()
let allGroupStagesNew = tournament.allGroupStages()
allGroupStagesNew.forEach { groupStage in
if let pGroupStage = allGroupStages.first(where: { gs in
groupStage.index == gs.index
}) {
groupStage.setData(from: pGroupStage, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
}
}
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled())
}
if seedRepartition.count > 0, selectedTournament == nil {
await _handleSeedRepartition()
}
} else if (rebuildEverything == false && requirements.contains(.groupStage)) { } else if (rebuildEverything == false && requirements.contains(.groupStage)) {
tournament.deleteGroupStages() tournament.deleteGroupStages()
tournament.buildGroupStages() tournament.buildGroupStages()
@ -514,16 +686,88 @@ struct TableStructureView: View {
} }
} }
private func _handleSeedRepartition() async {
let rounds = tournament.rounds()
var roundsCount = rounds.count
while roundsCount < seedRepartition.count {
await tournament.addNewRound(roundsCount)
roundsCount += 1
}
if seedRepartition.reduce(0, +) > 0 {
let rounds = tournament.rounds()
let roundsToDelete = rounds.prefix(rounds.count - seedRepartition.count)
for round in roundsToDelete {
await tournament.removeRound(round)
}
}
for (index, seedCount) in seedRepartition.enumerated() {
if let round = tournament.rounds().first(where: { $0.index == index }) {
let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index)
let playedMatches = round.playedMatches().map { $0.index - baseIndex }
let allMatches = round._matches()
let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in
playedMatches.contains(index)
}).prefix(seedCount)
if playedMatches.count == numberOfMatches && seedCount == numberOfMatches * 2 {
continue
}
for (index, value) in seedSorted.enumerated() {
let isOpponentTurn = index >= playedMatches.count
if let match = allMatches[safe:value] {
if match.index - baseIndex < numberOfMatches / 2 {
if isOpponentTurn {
match.previousMatch(.two)?.disableMatch()
} else {
match.previousMatch(.one)?.disableMatch()
}
} else {
if isOpponentTurn {
match.previousMatch(.one)?.disableMatch()
} else {
match.previousMatch(.two)?.disableMatch()
}
}
}
}
if seedCount > 0 {
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches())
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches())
round.deleteLoserBracket()
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
}
}
}
private func _updatePreset() { private func _updatePreset() {
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount() if let selectedTournament {
groupStageCount = structurePreset.groupStageCount() seedRepartition = []
teamsPerGroupStage = structurePreset.teamsPerGroupStage() teamCount = selectedTournament.teamCount
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() groupStageCount = selectedTournament.groupStageCount
groupStageAdditionalQualified = 0 teamsPerGroupStage = selectedTournament.teamsPerGroupStage
buildWildcards = tournament.level.wildcardArePossible() qualifiedPerGroupStage = selectedTournament.qualifiedPerGroupStage
groupStageAdditionalQualified = selectedTournament.groupStageAdditionalQualified
buildWildcards = tournament.level.wildcardArePossible()
} else {
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount()
groupStageCount = structurePreset.groupStageCount()
teamsPerGroupStage = structurePreset.teamsPerGroupStage()
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
}
_verifyValueIntegrity()
} }
private func _verifyValueIntegrity() { private func _verifyValueIntegrity(keepSeedRepartition: Bool = false) {
if teamCount > 128 { if teamCount > 128 {
teamCount = 128 teamCount = 128
} }
@ -532,8 +776,8 @@ struct TableStructureView: View {
groupStageCount = maxGroupStages groupStageCount = maxGroupStages
} }
if teamCount < 4 { if teamCount < 2 {
teamCount = 4 teamCount = 2
} }
if groupStageCount < 0 { if groupStageCount < 0 {
@ -565,7 +809,10 @@ struct TableStructureView: View {
groupStageAdditionalQualified = 0 groupStageAdditionalQualified = 0
} }
} }
if keepSeedRepartition == false {
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
} }
} }
@ -620,3 +867,44 @@ extension TableStructureView {
// .environmentObject(DataStore.shared) // .environmentObject(DataStore.shared)
// } // }
//} //}
func frenchUmpireOrder(for matches: [Int]) -> [Int] {
if matches.count <= 1 { return matches }
if matches.count == 2 { return [matches[1], matches[0]] }
var result: [Int] = []
// Step 1: Take last, then first
result.append(matches.last!)
result.append(matches.first!)
// Step 2: Get remainder (everything except first and last)
let remainder = Array(matches[1..<matches.count-1])
if remainder.isEmpty { return result }
// Step 3: Split remainder in half
let mid = remainder.count / 2
let firstHalf = Array(remainder[0..<mid])
let secondHalf = Array(remainder[mid..<remainder.count])
// Step 4: Take first of 2nd half, then last of 1st half
if !secondHalf.isEmpty {
result.append(secondHalf.first!)
}
if !firstHalf.isEmpty {
result.append(firstHalf.last!)
}
// Step 5: Build new remainder from what's left and recurse
let newRemainder = Array(firstHalf.dropLast()) + Array(secondHalf.dropFirst())
result.append(contentsOf: frenchUmpireOrder(for: newRemainder))
return result
}
/// Convenience wrapper
func frenchUmpireOrder(for matchCount: Int) -> [Int] {
let m = frenchUmpireOrder(for: Array(0..<matchCount))
return m + m.reversed()
}

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

@ -16,8 +16,10 @@ struct TournamentCellView: View {
let tournament: FederalTournamentHolder let tournament: FederalTournamentHolder
// let color: Color = .black // let color: Color = .black
var displayStyle: DisplayStyle = .wide var displayStyle: DisplayStyle = .wide
var displayContext: DisplayContext = .edition
@State var shouldTournamentBeOver: Bool = false @State var shouldTournamentBeOver: Bool = false
@State private var inProgressBuild: (String, any TournamentBuildHolder)? = nil
var event: Event? { var event: Event? {
guard let federalTournament = tournament as? FederalTournament else { return nil } guard let federalTournament = tournament as? FederalTournament else { return nil }
return dataStore.events.first(where: { $0.tenupId == federalTournament.id.string }) return dataStore.events.first(where: { $0.tenupId == federalTournament.id.string })
@ -76,6 +78,12 @@ struct TournamentCellView: View {
Text(tournament.startDate.formatted(.dateTime.day(.twoDigits))) Text(tournament.startDate.formatted(.dateTime.day(.twoDigits)))
.font(.title).fontWeight(.semibold) .font(.title).fontWeight(.semibold)
// .monospacedDigit() // .monospacedDigit()
if displayContext == .selection {
Text(tournament.startDate.formatted(.dateTime.month(.abbreviated)).uppercased())
.font(.caption)
Text(tournament.startDate.formatted(.dateTime.year()))
.font(.caption)
}
} }
} }
// DateBoxView(date: tournament.startDate, displayStyle: displayStyle == .wide ? .short : .wide) // DateBoxView(date: tournament.startDate, displayStyle: displayStyle == .wide ? .short : .wide)
@ -137,15 +145,20 @@ struct TournamentCellView: View {
Text(teamCount.formatted()) Text(teamCount.formatted())
} }
} else if let federalTournament = tournament as? FederalTournament, navigation.agendaDestination != .around { } else if let federalTournament = tournament as? FederalTournament, navigation.agendaDestination != .around {
Button {
_createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build) if let inProgressBuild, build.age == inProgressBuild.1.age, build.level == inProgressBuild.1.level, build.category == inProgressBuild.1.category, inProgressBuild.0 == federalTournament.id {
} label: { ProgressView()
Image(systemName: existingTournament != nil ? "checkmark.circle.fill" : "square.and.arrow.down") } else {
.resizable() Button {
.scaledToFit() _createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build)
.frame(height: 28) } label: {
.accessibilityLabel("importer ou ouvrir") Image(systemName: existingTournament != nil ? "checkmark.circle.fill" : "square.and.arrow.down")
.tint(existingTournament != nil ? Color.green : nil) .resizable()
.scaledToFit()
.frame(height: 28)
.accessibilityLabel("importer ou ouvrir")
.tint(existingTournament != nil ? Color.green : nil)
}
} }
} }
} }
@ -214,6 +227,8 @@ struct TournamentCellView: View {
newTournament.umpireCustomMail = federalTournament.mailLabel() newTournament.umpireCustomMail = federalTournament.mailLabel()
} }
inProgressBuild = (federalTournament.id, build)
Task { Task {
do { do {
let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id) let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id)
@ -233,6 +248,10 @@ struct TournamentCellView: View {
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
await MainActor.run {
inProgressBuild = nil
}
} }
} }
} }

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

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

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

@ -24,61 +24,6 @@ struct AccountView: View {
PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item)) PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item))
} }
#endif #endif
let onlineRegPaymentMode = dataStore.user.registrationPaymentMode
Section {
LabeledContent {
switch onlineRegPaymentMode {
case .corporate:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .disabled:
Text("Désactivé")
.bold()
case .noFee:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .stripe:
Text("Activé")
.bold()
.foregroundStyle(.green)
}
} label: {
Text("Option 'Paiement en ligne'")
if onlineRegPaymentMode == .corporate {
Text("Mode Padel Club")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .noFee {
Text("Commission Stripe")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .stripe {
Text("Commission Stripe et Padel Club")
.foregroundStyle(.secondary)
}
}
} footer: {
if onlineRegPaymentMode == .disabled {
FooterButtonView("Contactez nous pour activer cette option.") {
let emailTo: String = "support@padelclub.app"
let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)"
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
.font(.callout)
.multilineTextAlignment(.leading)
} else {
Text("Permet de proposer le paiement de vos tournois en ligne.")
}
}
Section {
Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?")
SupportButtonView(supportButtonType: .sharingRequest)
}
Section { Section {
NavigationLink("Changer de mot de passe") { NavigationLink("Changer de mot de passe") {
ChangePasswordView() ChangePasswordView()

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

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

Loading…
Cancel
Save