sync2
Laurent 9 months ago
commit ac3c21c413
  1. 88
      PadelClub.xcodeproj/project.pbxproj
  2. 6
      PadelClub/Assets.xcassets/logoRed.colorset/Contents.json
  3. 6
      PadelClub/Assets.xcassets/logoYellow.colorset/Contents.json
  4. 5
      PadelClub/Data/Federal/FederalPlayer.swift
  5. 2
      PadelClub/Data/Gen/BaseDrawLog.swift
  6. 29
      PadelClub/Data/Gen/BasePlayerRegistration.swift
  7. 65
      PadelClub/Data/Gen/BaseTournament.swift
  8. 17
      PadelClub/Data/Gen/PlayerRegistration.json
  9. 45
      PadelClub/Data/Gen/Tournament.json
  10. 2
      PadelClub/Data/GroupStage.swift
  11. 20
      PadelClub/Data/Match.swift
  12. 8
      PadelClub/Data/MatchScheduler.swift
  13. 53
      PadelClub/Data/PlayerPaymentType.swift
  14. 220
      PadelClub/Data/PlayerRegistration.swift
  15. 5
      PadelClub/Data/Round.swift
  16. 82
      PadelClub/Data/TeamRegistration.swift
  17. 246
      PadelClub/Data/Tournament.swift
  18. 5
      PadelClub/Extensions/Date+Extensions.swift
  19. 4
      PadelClub/Extensions/String+Extensions.swift
  20. 74
      PadelClub/InscriptionLegendView.swift
  21. 2
      PadelClub/PadelClubApp.swift
  22. 85
      PadelClub/RegistrationInfoSheetView.swift
  23. 49
      PadelClub/Utils/FileImportManager.swift
  24. 2
      PadelClub/Utils/LocationManager.swift
  25. 11
      PadelClub/Utils/PadelRule.swift
  26. 8
      PadelClub/Utils/SourceFileManager.swift
  27. 138
      PadelClub/Utils/Tips.swift
  28. 4
      PadelClub/Utils/URLs.swift
  29. 6
      PadelClub/ViewModel/FederalDataViewModel.swift
  30. 4
      PadelClub/ViewModel/MatchDescriptor.swift
  31. 1
      PadelClub/ViewModel/Screen.swift
  32. 5
      PadelClub/ViewModel/SearchViewModel.swift
  33. 23
      PadelClub/ViewModel/SetDescriptor.swift
  34. 2
      PadelClub/Views/Cashier/Event/EventCreationView.swift
  35. 2
      PadelClub/Views/Cashier/Event/EventTournamentsView.swift
  36. 4
      PadelClub/Views/Club/ClubDetailView.swift
  37. 4
      PadelClub/Views/Components/CopyPasteButtonView.swift
  38. 8
      PadelClub/Views/Components/RowButtonView.swift
  39. 33
      PadelClub/Views/Components/StepperView.swift
  40. 28
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  41. 2
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  42. 8
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  43. 56
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  44. 44
      PadelClub/Views/Navigation/MainView.swift
  45. 170
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  46. 8
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  47. 24
      PadelClub/Views/Player/PlayerDetailView.swift
  48. 33
      PadelClub/Views/Shared/DateMenuView.swift
  49. 3
      PadelClub/Views/Shared/ImportedPlayerView.swift
  50. 593
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  51. 99
      PadelClub/Views/Team/EditingTeamView.swift
  52. 12
      PadelClub/Views/Team/TeamDetailView.swift
  53. 8
      PadelClub/Views/Team/TeamRowView.swift
  54. 2
      PadelClub/Views/Tournament/FileImportView.swift
  55. 2
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  56. 4
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  57. 1
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  58. 48
      PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift
  59. 96
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  60. 5
      PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift
  61. 38
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  62. 139
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  63. 377
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  64. 42
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  65. 42
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  66. 7
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  67. 21
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  68. 7
      PadelClub/Views/Tournament/Subscription/Guard.swift
  69. 2
      PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift
  70. 95
      PadelClub/Views/Tournament/TournamentView.swift
  71. 7
      PadelClub/Views/ViewModifiers/ListRowViewModifier.swift
  72. 30
      PadelClubTests/ServerDataTests.swift
  73. 6
      PadelClubTests/TokenExemptionTests.swift
  74. 14
      PadelClubTests/UserDataTests.swift

@ -127,6 +127,9 @@
C488C8832CCBE8FC0082001F /* NetworkStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */; }; C488C8832CCBE8FC0082001F /* NetworkStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */; };
C488C8842CCBE8FC0082001F /* NetworkStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */; }; C488C8842CCBE8FC0082001F /* NetworkStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */; };
C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */; }; C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */; };
C49C73142D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C73152D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C73162D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; }; C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; };
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; }; C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; };
C49EF0262BD80AE80077B5AA /* SubscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */; }; C49EF0262BD80AE80077B5AA /* SubscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */; };
@ -234,10 +237,19 @@
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; }; FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.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 */; };
FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */; };
FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */; };
FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */; };
FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */; };
FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */; };
FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */; };
FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */; }; FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */; };
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; }; FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; };
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */; }; FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */; };
FF44421C2BE39FA2008BBF0B /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */; }; FF44421C2BE39FA2008BBF0B /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */; };
FF4623CB2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4623CA2D1340D200CB57B5 /* TournamentCategorySettingsView.swift */; };
FF4623CC2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4623CA2D1340D200CB57B5 /* TournamentCategorySettingsView.swift */; };
FF4623CD2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4623CA2D1340D200CB57B5 /* TournamentCategorySettingsView.swift */; };
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6B42B9248200002987F /* NetworkManager.swift */; }; FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6B42B9248200002987F /* NetworkManager.swift */; };
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */; }; FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */; };
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; };
@ -906,6 +918,12 @@
FFBF41822BF73EB3001B24CB /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41812BF73EB3001B24CB /* EventView.swift */; }; FFBF41822BF73EB3001B24CB /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41812BF73EB3001B24CB /* EventView.swift */; };
FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */; }; FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */; };
FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; };
FFBFC3912CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3902CEE3A0E000EBD8D /* RegistrationSetupView.swift */; };
FFBFC3922CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3902CEE3A0E000EBD8D /* RegistrationSetupView.swift */; };
FFBFC3932CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3902CEE3A0E000EBD8D /* RegistrationSetupView.swift */; };
FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */; };
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */; };
FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */; };
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; };
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 */; };
@ -1070,6 +1088,7 @@
C488C8702CC816410082001F /* Purchase.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Purchase.json; sourceTree = "<group>"; }; C488C8702CC816410082001F /* Purchase.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Purchase.json; sourceTree = "<group>"; };
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusView.swift; sourceTree = "<group>"; }; C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusView.swift; sourceTree = "<group>"; };
C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = "<group>"; }; C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = "<group>"; };
C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPaymentType.swift; sourceTree = "<group>"; };
C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; }; C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; };
C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; }; C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = "<group>"; }; C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = "<group>"; };
@ -1208,9 +1227,12 @@
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>"; };
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>"; };
FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationInfoSheetView.swift; sourceTree = "<group>"; };
FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionLegendView.swift; sourceTree = "<group>"; };
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduler.swift; sourceTree = "<group>"; }; FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduler.swift; sourceTree = "<group>"; };
FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = "<group>"; }; FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = "<group>"; };
FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestination.swift; sourceTree = "<group>"; }; FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestination.swift; sourceTree = "<group>"; };
FF4623CA2D1340D200CB57B5 /* TournamentCategorySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCategorySettingsView.swift; sourceTree = "<group>"; };
FF4AB6B42B9248200002987F /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; }; FF4AB6B42B9248200002987F /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; }; FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = "<group>"; }; FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = "<group>"; };
@ -1333,6 +1355,8 @@
FFBF41812BF73EB3001B24CB /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; }; FFBF41812BF73EB3001B24CB /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; };
FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTournamentsView.swift; sourceTree = "<group>"; }; FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTournamentsView.swift; sourceTree = "<group>"; };
FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSettingsView.swift; sourceTree = "<group>"; }; FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSettingsView.swift; sourceTree = "<group>"; };
FFBFC3902CEE3A0E000EBD8D /* RegistrationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationSetupView.swift; sourceTree = "<group>"; };
FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateMenuView.swift; sourceTree = "<group>"; };
FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = "<group>"; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = "<group>"; };
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>"; };
@ -1473,6 +1497,8 @@
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */, FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */,
FF2B515F2C7E300500FFF126 /* SeedData */, FF2B515F2C7E300500FFF126 /* SeedData */,
C4A47D722B72881500ADC637 /* Views */, C4A47D722B72881500ADC637 /* Views */,
FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */,
FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */,
FF3F74FD2B91A087004CFE0E /* ViewModel */, FF3F74FD2B91A087004CFE0E /* ViewModel */,
C4A47D5F2B6D3B2D00ADC637 /* Data */, C4A47D5F2B6D3B2D00ADC637 /* Data */,
FFF8ACD02B9238A2008466FA /* Utils */, FFF8ACD02B9238A2008466FA /* Utils */,
@ -1578,6 +1604,7 @@
FF967CED2BAECBD700A9A3BD /* Round.swift */, FF967CED2BAECBD700A9A3BD /* Round.swift */,
FF967CEB2BAECB9900A9A3BD /* Match.swift */, FF967CEB2BAECB9900A9A3BD /* Match.swift */,
FF967CF12BAECC0B00A9A3BD /* PlayerRegistration.swift */, FF967CF12BAECC0B00A9A3BD /* PlayerRegistration.swift */,
C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */,
FF967CF02BAECC0B00A9A3BD /* TeamRegistration.swift */, FF967CF02BAECC0B00A9A3BD /* TeamRegistration.swift */,
FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */, FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */,
C4A47D622B6D3D6500ADC637 /* Club.swift */, C4A47D622B6D3D6500ADC637 /* Club.swift */,
@ -1861,6 +1888,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */, FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */,
FFBFC3902CEE3A0E000EBD8D /* RegistrationSetupView.swift */,
FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */, FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */,
FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */, FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */,
FF8F26532BAE1E4400650388 /* TableStructureView.swift */, FF8F26532BAE1E4400650388 /* TableStructureView.swift */,
@ -1962,6 +1990,7 @@
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */, FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */,
FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */, FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */,
FFE103112C366E5900684FC9 /* ImagePickerView.swift */, FFE103112C366E5900684FC9 /* ImagePickerView.swift */,
FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */,
); );
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2011,6 +2040,7 @@
FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */, FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */,
FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */, FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */,
FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */, FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */,
FF4623CA2D1340D200CB57B5 /* TournamentCategorySettingsView.swift */,
FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */, FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */,
FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */, FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */,
); );
@ -2650,6 +2680,7 @@
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */,
C4A36F5A2CE2626A003738C6 /* TournamentLibrary.swift in Sources */, C4A36F5A2CE2626A003738C6 /* TournamentLibrary.swift in Sources */,
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */, FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */,
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
C4FC2E2B2C2C0E4D0021F3BF /* TournamentStore.swift in Sources */, C4FC2E2B2C2C0E4D0021F3BF /* TournamentStore.swift in Sources */,
@ -2681,6 +2712,7 @@
C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */, C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */,
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */,
FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */, FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */,
FFBFC3922CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */,
FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */, FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
@ -2713,6 +2745,7 @@
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 */,
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 */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
@ -2737,8 +2770,10 @@
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */, C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */,
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */, FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */,
C49C73142D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */, FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */,
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */, FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */,
@ -2809,6 +2844,7 @@
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */, FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */,
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */,
FF4623CC2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */,
C4EC6F592BE92D88000CEAB4 /* PListReader.swift in Sources */, C4EC6F592BE92D88000CEAB4 /* PListReader.swift in Sources */,
FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */, FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */,
FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */, FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */,
@ -2913,6 +2949,7 @@
C488C84D2CC7E4240082001F /* BaseEvent.swift in Sources */, C488C84D2CC7E4240082001F /* BaseEvent.swift in Sources */,
C488C84E2CC7E4240082001F /* BaseRound.swift in Sources */, C488C84E2CC7E4240082001F /* BaseRound.swift in Sources */,
C488C84F2CC7E4240082001F /* BaseMatchScheduler.swift in Sources */, C488C84F2CC7E4240082001F /* BaseMatchScheduler.swift in Sources */,
C49C73152D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */,
C488C8512CC7E4240082001F /* BasePlayerRegistration.swift in Sources */, C488C8512CC7E4240082001F /* BasePlayerRegistration.swift in Sources */,
C488C8522CC7E4240082001F /* BaseTeamScore.swift in Sources */, C488C8522CC7E4240082001F /* BaseTeamScore.swift in Sources */,
C488C8532CC7E4240082001F /* BaseTournament.swift in Sources */, C488C8532CC7E4240082001F /* BaseTournament.swift in Sources */,
@ -2955,6 +2992,7 @@
C4339BFD2CFF7D68004E5F09 /* ShareModelView.swift in Sources */, C4339BFD2CFF7D68004E5F09 /* ShareModelView.swift in Sources */,
FF4CBF9C2C996C0600151637 /* ImportedPlayerView.swift in Sources */, FF4CBF9C2C996C0600151637 /* ImportedPlayerView.swift in Sources */,
FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */, FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */,
FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF4CBF9E2C996C0600151637 /* NetworkManagerError.swift in Sources */, FF4CBF9E2C996C0600151637 /* NetworkManagerError.swift in Sources */,
FF4CBF9F2C996C0600151637 /* Tournament.swift in Sources */, FF4CBF9F2C996C0600151637 /* Tournament.swift in Sources */,
FF4CBFA02C996C0600151637 /* TournamentStore.swift in Sources */, FF4CBFA02C996C0600151637 /* TournamentStore.swift in Sources */,
@ -2986,6 +3024,7 @@
FF4CBFB92C996C0600151637 /* Purchase.swift in Sources */, FF4CBFB92C996C0600151637 /* Purchase.swift in Sources */,
FF4CBFBA2C996C0600151637 /* Screen.swift in Sources */, FF4CBFBA2C996C0600151637 /* Screen.swift in Sources */,
FF4CBFBB2C996C0600151637 /* Round.swift in Sources */, FF4CBFBB2C996C0600151637 /* Round.swift in Sources */,
FFBFC3912CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */,
FF4CBFBC2C996C0600151637 /* FederalDataViewModel.swift in Sources */, FF4CBFBC2C996C0600151637 /* FederalDataViewModel.swift in Sources */,
FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */, FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */,
FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */, FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */,
@ -3019,6 +3058,7 @@
FF4CBFD72C996C0600151637 /* LoserBracketFromGroupStageView.swift in Sources */, FF4CBFD72C996C0600151637 /* LoserBracketFromGroupStageView.swift in Sources */,
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 */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
@ -3043,6 +3083,7 @@
FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */, FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */,
FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */, FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */,
FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */, FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */,
FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF4CBFF02C996C0600151637 /* URLs.swift in Sources */, FF4CBFF02C996C0600151637 /* URLs.swift in Sources */,
FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */, FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
@ -3115,6 +3156,7 @@
FF4CC02C2C996C0600151637 /* DateBoxView.swift in Sources */, FF4CC02C2C996C0600151637 /* DateBoxView.swift in Sources */,
FF4CC02D2C996C0600151637 /* LearnMoreSheetView.swift in Sources */, FF4CC02D2C996C0600151637 /* LearnMoreSheetView.swift in Sources */,
FF4CC02E2C996C0600151637 /* SourceFileManager.swift in Sources */, FF4CC02E2C996C0600151637 /* SourceFileManager.swift in Sources */,
FF4623CB2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */,
FF4CC02F2C996C0600151637 /* PListReader.swift in Sources */, FF4CC02F2C996C0600151637 /* PListReader.swift in Sources */,
FF4CC0302C996C0600151637 /* UpdateSourceRankDateView.swift in Sources */, FF4CC0302C996C0600151637 /* UpdateSourceRankDateView.swift in Sources */,
FF4CC0312C996C0600151637 /* GlobalSettingsView.swift in Sources */, FF4CC0312C996C0600151637 /* GlobalSettingsView.swift in Sources */,
@ -3197,6 +3239,7 @@
C488C85A2CC7E4240082001F /* BaseEvent.swift in Sources */, C488C85A2CC7E4240082001F /* BaseEvent.swift in Sources */,
C488C85B2CC7E4240082001F /* BaseRound.swift in Sources */, C488C85B2CC7E4240082001F /* BaseRound.swift in Sources */,
C488C85C2CC7E4240082001F /* BaseMatchScheduler.swift in Sources */, C488C85C2CC7E4240082001F /* BaseMatchScheduler.swift in Sources */,
C49C73162D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */,
C488C85E2CC7E4240082001F /* BasePlayerRegistration.swift in Sources */, C488C85E2CC7E4240082001F /* BasePlayerRegistration.swift in Sources */,
C488C85F2CC7E4240082001F /* BaseTeamScore.swift in Sources */, C488C85F2CC7E4240082001F /* BaseTeamScore.swift in Sources */,
C488C8602CC7E4240082001F /* BaseTournament.swift in Sources */, C488C8602CC7E4240082001F /* BaseTournament.swift in Sources */,
@ -3239,6 +3282,7 @@
C4339BFC2CFF7D68004E5F09 /* ShareModelView.swift in Sources */, C4339BFC2CFF7D68004E5F09 /* ShareModelView.swift in Sources */,
FF70FB1B2C90584900129CC2 /* ImportedPlayerView.swift in Sources */, FF70FB1B2C90584900129CC2 /* ImportedPlayerView.swift in Sources */,
FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */, FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */,
FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF70FB1D2C90584900129CC2 /* NetworkManagerError.swift in Sources */, FF70FB1D2C90584900129CC2 /* NetworkManagerError.swift in Sources */,
FF70FB1E2C90584900129CC2 /* Tournament.swift in Sources */, FF70FB1E2C90584900129CC2 /* Tournament.swift in Sources */,
FF70FB1F2C90584900129CC2 /* TournamentStore.swift in Sources */, FF70FB1F2C90584900129CC2 /* TournamentStore.swift in Sources */,
@ -3270,6 +3314,7 @@
FF70FB382C90584900129CC2 /* Purchase.swift in Sources */, FF70FB382C90584900129CC2 /* Purchase.swift in Sources */,
FF70FB392C90584900129CC2 /* Screen.swift in Sources */, FF70FB392C90584900129CC2 /* Screen.swift in Sources */,
FF70FB3A2C90584900129CC2 /* Round.swift in Sources */, FF70FB3A2C90584900129CC2 /* Round.swift in Sources */,
FFBFC3932CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */,
FF70FB3B2C90584900129CC2 /* FederalDataViewModel.swift in Sources */, FF70FB3B2C90584900129CC2 /* FederalDataViewModel.swift in Sources */,
FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */, FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */,
FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */, FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */,
@ -3303,6 +3348,7 @@
FF70FB562C90584900129CC2 /* LoserBracketFromGroupStageView.swift in Sources */, FF70FB562C90584900129CC2 /* LoserBracketFromGroupStageView.swift in Sources */,
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 */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
@ -3327,6 +3373,7 @@
FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */, FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */,
FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */, FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */,
FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */, FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */,
FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */, FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */,
FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */, FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
@ -3399,6 +3446,7 @@
FF70FBAB2C90584900129CC2 /* DateBoxView.swift in Sources */, FF70FBAB2C90584900129CC2 /* DateBoxView.swift in Sources */,
FF70FBAC2C90584900129CC2 /* LearnMoreSheetView.swift in Sources */, FF70FBAC2C90584900129CC2 /* LearnMoreSheetView.swift in Sources */,
FF70FBAD2C90584900129CC2 /* SourceFileManager.swift in Sources */, FF70FBAD2C90584900129CC2 /* SourceFileManager.swift in Sources */,
FF4623CD2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */,
FF70FBAE2C90584900129CC2 /* PListReader.swift in Sources */, FF70FBAE2C90584900129CC2 /* PListReader.swift in Sources */,
FF70FBAF2C90584900129CC2 /* UpdateSourceRankDateView.swift in Sources */, FF70FBAF2C90584900129CC2 /* UpdateSourceRankDateView.swift in Sources */,
FF70FBB02C90584900129CC2 /* GlobalSettingsView.swift in Sources */, FF70FBB02C90584900129CC2 /* GlobalSettingsView.swift in Sources */,
@ -3573,7 +3621,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "En utilisant votre position, Padel Club peut trouver plus rapidement les clubs et les tournois autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3584,7 +3635,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.42; MARKETING_VERSION = 1.1.8;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3617,7 +3668,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "En utilisant votre position, Padel Club peut trouver plus rapidement les clubs et les tournois autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3628,7 +3682,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.42; MARKETING_VERSION = 1.1.8;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3721,7 +3775,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 = 2; CURRENT_PROJECT_VERSION = 1;
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\"";
@ -3734,7 +3788,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3745,7 +3802,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.30; MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3766,7 +3823,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 = 2; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3778,7 +3835,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3789,7 +3849,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.30; MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3811,7 +3871,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 = 2; CURRENT_PROJECT_VERSION = 1;
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\"";
@ -3834,7 +3894,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.24; MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3854,7 +3914,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 = 2; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3876,7 +3936,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.24; MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0.220", "blue" : "0x38",
"green" : "0.251", "green" : "0x40",
"red" : "0.910" "red" : "0xE8"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0.000", "blue" : "0x00",
"green" : "0.827", "green" : "0xD2",
"red" : "1.000" "red" : "0xFF"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

@ -197,8 +197,6 @@ class FederalPlayer: Decodable {
} }
lastPlayerFetch.predicate = predicate lastPlayerFetch.predicate = predicate
let count = try? context.count(for: lastPlayerFetch) let count = try? context.count(for: lastPlayerFetch)
print("count", count)
do { do {
if let lr = try context.fetch(lastPlayerFetch).first?.rank { if let lr = try context.fetch(lastPlayerFetch).first?.rank {
let fetch = ImportedPlayer.fetchRequest() let fetch = ImportedPlayer.fetchRequest()
@ -207,8 +205,9 @@ class FederalPlayer: Decodable {
rankPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [rankPredicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)]) rankPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [rankPredicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)])
} }
fetch.predicate = rankPredicate fetch.predicate = rankPredicate
print(fetch.predicate)
let lastPlayersCount = try context.count(for: fetch) let lastPlayersCount = try context.count(for: fetch)
print(Int(lr), Int(lastPlayersCount) - 1, count)
return (Int(lr) + Int(lastPlayersCount) - 1, count) return (Int(lr) + Int(lastPlayersCount) - 1, count)
} }
} catch { } catch {

@ -93,4 +93,4 @@ class BaseDrawLog: SyncedModelObject, SyncedStorable {
] ]
} }
} }

@ -28,8 +28,11 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
var email: String? = nil var email: String? = nil
var birthdate: String? = nil var birthdate: String? = nil
var computedRank: Int = 0 var computedRank: Int = 0
var source: PlayerDataSource? = nil var source: PlayerRegistration.PlayerDataSource? = nil
var hasArrived: Bool = false var hasArrived: Bool = false
var coach: Bool = false
var captain: Bool = false
var registeredOnline: Bool = false
init( init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -49,8 +52,11 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
email: String? = nil, email: String? = nil,
birthdate: String? = nil, birthdate: String? = nil,
computedRank: Int = 0, computedRank: Int = 0,
source: PlayerDataSource? = nil, source: PlayerRegistration.PlayerDataSource? = nil,
hasArrived: Bool = false hasArrived: Bool = false,
coach: Bool = false,
captain: Bool = false,
registeredOnline: Bool = false
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -72,6 +78,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.computedRank = computedRank self.computedRank = computedRank
self.source = source self.source = source
self.hasArrived = hasArrived self.hasArrived = hasArrived
self.coach = coach
self.captain = captain
self.registeredOnline = registeredOnline
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -94,6 +103,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
case _computedRank = "computedRank" case _computedRank = "computedRank"
case _source = "source" case _source = "source"
case _hasArrived = "hasArrived" case _hasArrived = "hasArrived"
case _coach = "coach"
case _captain = "captain"
case _registeredOnline = "registeredOnline"
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -115,8 +127,11 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.email = try container.decodeIfPresent(String.self, forKey: ._email) ?? nil self.email = try container.decodeIfPresent(String.self, forKey: ._email) ?? nil
self.birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate) ?? nil self.birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate) ?? nil
self.computedRank = try container.decodeIfPresent(Int.self, forKey: ._computedRank) ?? 0 self.computedRank = try container.decodeIfPresent(Int.self, forKey: ._computedRank) ?? 0
self.source = try container.decodeIfPresent(PlayerDataSource.self, forKey: ._source) ?? nil self.source = try container.decodeIfPresent(PlayerRegistration.PlayerDataSource.self, forKey: ._source) ?? nil
self.hasArrived = try container.decodeIfPresent(Bool.self, forKey: ._hasArrived) ?? false self.hasArrived = try container.decodeIfPresent(Bool.self, forKey: ._hasArrived) ?? false
self.coach = try container.decodeIfPresent(Bool.self, forKey: ._coach) ?? false
self.captain = try container.decodeIfPresent(Bool.self, forKey: ._captain) ?? false
self.registeredOnline = try container.decodeIfPresent(Bool.self, forKey: ._registeredOnline) ?? false
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -141,6 +156,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
try container.encode(self.computedRank, forKey: ._computedRank) try container.encode(self.computedRank, forKey: ._computedRank)
try container.encode(self.source, forKey: ._source) try container.encode(self.source, forKey: ._source)
try container.encode(self.hasArrived, forKey: ._hasArrived) try container.encode(self.hasArrived, forKey: ._hasArrived)
try container.encode(self.coach, forKey: ._coach)
try container.encode(self.captain, forKey: ._captain)
try container.encode(self.registeredOnline, forKey: ._registeredOnline)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -170,6 +188,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.computedRank = playerregistration.computedRank self.computedRank = playerregistration.computedRank
self.source = playerregistration.source self.source = playerregistration.source
self.hasArrived = playerregistration.hasArrived self.hasArrived = playerregistration.hasArrived
self.coach = playerregistration.coach
self.captain = playerregistration.captain
self.registeredOnline = playerregistration.registeredOnline
} }
static func relationships() -> [Relationship] { static func relationships() -> [Relationship] {

@ -54,6 +54,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
var loserBracketMode: LoserBracketMode = .automatic var loserBracketMode: LoserBracketMode = .automatic
var initialSeedRound: Int = 0 var initialSeedRound: Int = 0
var initialSeedCount: Int = 0 var initialSeedCount: Int = 0
var enableOnlineRegistration: Bool = false
var registrationDateLimit: Date? = nil
var openingRegistrationDate: Date? = nil
var waitingListLimit: Int? = nil
var accountIsRequired: Bool = true
var licenseIsRequired: Bool = true
var minimumPlayerPerTeam: Int = 2
var maximumPlayerPerTeam: Int = 2
var information: String? = nil
init( init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -98,7 +107,16 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
publishRankings: Bool = false, publishRankings: Bool = false,
loserBracketMode: LoserBracketMode = .automatic, loserBracketMode: LoserBracketMode = .automatic,
initialSeedRound: Int = 0, initialSeedRound: Int = 0,
initialSeedCount: Int = 0 initialSeedCount: Int = 0,
enableOnlineRegistration: Bool = false,
registrationDateLimit: Date? = nil,
openingRegistrationDate: Date? = nil,
waitingListLimit: Int? = nil,
accountIsRequired: Bool = true,
licenseIsRequired: Bool = true,
minimumPlayerPerTeam: Int = 2,
maximumPlayerPerTeam: Int = 2,
information: String? = nil
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -144,6 +162,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.loserBracketMode = loserBracketMode self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount self.initialSeedCount = initialSeedCount
self.enableOnlineRegistration = enableOnlineRegistration
self.registrationDateLimit = registrationDateLimit
self.openingRegistrationDate = openingRegistrationDate
self.waitingListLimit = waitingListLimit
self.accountIsRequired = accountIsRequired
self.licenseIsRequired = licenseIsRequired
self.minimumPlayerPerTeam = minimumPlayerPerTeam
self.maximumPlayerPerTeam = maximumPlayerPerTeam
self.information = information
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -190,6 +217,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
case _loserBracketMode = "loserBracketMode" case _loserBracketMode = "loserBracketMode"
case _initialSeedRound = "initialSeedRound" case _initialSeedRound = "initialSeedRound"
case _initialSeedCount = "initialSeedCount" case _initialSeedCount = "initialSeedCount"
case _enableOnlineRegistration = "enableOnlineRegistration"
case _registrationDateLimit = "registrationDateLimit"
case _openingRegistrationDate = "openingRegistrationDate"
case _waitingListLimit = "waitingListLimit"
case _accountIsRequired = "accountIsRequired"
case _licenseIsRequired = "licenseIsRequired"
case _minimumPlayerPerTeam = "minimumPlayerPerTeam"
case _maximumPlayerPerTeam = "maximumPlayerPerTeam"
case _information = "information"
} }
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? { private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
@ -298,6 +334,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
self.initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0 self.initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0
self.initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0 self.initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0
self.enableOnlineRegistration = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlineRegistration) ?? false
self.registrationDateLimit = try container.decodeIfPresent(Date.self, forKey: ._registrationDateLimit) ?? nil
self.openingRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._openingRegistrationDate) ?? nil
self.waitingListLimit = try container.decodeIfPresent(Int.self, forKey: ._waitingListLimit) ?? nil
self.accountIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._accountIsRequired) ?? true
self.licenseIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._licenseIsRequired) ?? true
self.minimumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._minimumPlayerPerTeam) ?? 2
self.maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2
self.information = try container.decodeIfPresent(String.self, forKey: ._information) ?? nil
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -346,6 +391,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode) try container.encode(self.loserBracketMode, forKey: ._loserBracketMode)
try container.encode(self.initialSeedRound, forKey: ._initialSeedRound) try container.encode(self.initialSeedRound, forKey: ._initialSeedRound)
try container.encode(self.initialSeedCount, forKey: ._initialSeedCount) try container.encode(self.initialSeedCount, forKey: ._initialSeedCount)
try container.encode(self.enableOnlineRegistration, forKey: ._enableOnlineRegistration)
try container.encode(self.registrationDateLimit, forKey: ._registrationDateLimit)
try container.encode(self.openingRegistrationDate, forKey: ._openingRegistrationDate)
try container.encode(self.waitingListLimit, forKey: ._waitingListLimit)
try container.encode(self.accountIsRequired, forKey: ._accountIsRequired)
try container.encode(self.licenseIsRequired, forKey: ._licenseIsRequired)
try container.encode(self.minimumPlayerPerTeam, forKey: ._minimumPlayerPerTeam)
try container.encode(self.maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam)
try container.encode(self.information, forKey: ._information)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -399,6 +453,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.loserBracketMode = tournament.loserBracketMode self.loserBracketMode = tournament.loserBracketMode
self.initialSeedRound = tournament.initialSeedRound self.initialSeedRound = tournament.initialSeedRound
self.initialSeedCount = tournament.initialSeedCount self.initialSeedCount = tournament.initialSeedCount
self.enableOnlineRegistration = tournament.enableOnlineRegistration
self.registrationDateLimit = tournament.registrationDateLimit
self.openingRegistrationDate = tournament.openingRegistrationDate
self.waitingListLimit = tournament.waitingListLimit
self.accountIsRequired = tournament.accountIsRequired
self.licenseIsRequired = tournament.licenseIsRequired
self.minimumPlayerPerTeam = tournament.minimumPlayerPerTeam
self.maximumPlayerPerTeam = tournament.maximumPlayerPerTeam
self.information = tournament.information
} }
static func relationships() -> [Relationship] { static func relationships() -> [Relationship] {

@ -93,13 +93,28 @@
}, },
{ {
"name": "source", "name": "source",
"type": "PlayerDataSource", "type": "PlayerRegistration.PlayerDataSource",
"optional": true "optional": true
}, },
{ {
"name": "hasArrived", "name": "hasArrived",
"type": "Bool", "type": "Bool",
"defaultValue": "false" "defaultValue": "false"
},
{
"name": "coach",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "captain",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "registeredOnline",
"type": "Bool",
"defaultValue": "false"
} }
] ]
} }

@ -219,6 +219,51 @@
"name": "initialSeedCount", "name": "initialSeedCount",
"type": "Int", "type": "Int",
"defaultValue": "0" "defaultValue": "0"
},
{
"name": "enableOnlineRegistration",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "registrationDateLimit",
"type": "Date",
"optional": true
},
{
"name": "openingRegistrationDate",
"type": "Date",
"optional": true
},
{
"name": "waitingListLimit",
"type": "Int",
"optional": true
},
{
"name": "accountIsRequired",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "licenseIsRequired",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "minimumPlayerPerTeam",
"type": "Int",
"defaultValue": 2
},
{
"name": "maximumPlayerPerTeam",
"type": "Int",
"defaultValue": 2
},
{
"name": "information",
"type": "String",
"optional": true
} }
] ]
} }

@ -207,6 +207,8 @@ final class GroupStage: BaseGroupStage, SideStorable {
tournament.endDate = Date() tournament.endDate = Date()
DataStore.shared.tournaments.addOrUpdate(instance: tournament) DataStore.shared.tournaments.addOrUpdate(instance: tournament)
} }
tournament.updateTournamentState()
} }
} }

@ -195,7 +195,6 @@ defer {
servingTeamId = nil servingTeamId = nil
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() }) teams().forEach({ $0.resetRestingTime() })
} }
@ -497,7 +496,6 @@ defer {
losingTeamId = teamScoreWalkout.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
updateFollowingMatchTeamScore() updateFollowingMatchTeamScore()
} }
@ -536,7 +534,6 @@ defer {
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
tournament.updateTournamentState()
} }
updateFollowingMatchTeamScore() updateFollowingMatchTeamScore()
} }
@ -546,7 +543,18 @@ defer {
teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",") teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",")
let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two)) let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two))
teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",") teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",")
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo])
if matchDescriptor.teamOneScores.last?.contains("-") == true && matchDescriptor.teamTwoScores.last?.contains("-") == false {
teamScoreTwo.score = (matchDescriptor.teamTwoScores.dropLast() + [matchDescriptor.teamTwoScores.last! + "-0"]).joined(separator: ",")
} else if matchDescriptor.teamTwoScores.last?.contains("-") == true && matchDescriptor.teamOneScores.last?.contains("-") == false {
teamScoreOne.score = (matchDescriptor.teamOneScores.dropLast() + [matchDescriptor.teamOneScores.last! + "-0"]).joined(separator: ",")
}
do {
try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo])
} catch {
Logger.error(error)
}
matchFormat = matchDescriptor.matchFormat matchFormat = matchDescriptor.matchFormat
} }
@ -765,8 +773,8 @@ defer {
if teamPosition == team(.two)?.groupStagePositionAtStep(step) { if teamPosition == team(.two)?.groupStagePositionAtStep(step) {
reverseValue = -1 reverseValue = -1
} }
let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut())
var setDifference : Int = 0 var setDifference : Int = 0
let zip = zip(endedSetsOne, endedSetsTwo) let zip = zip(endedSetsOne, endedSetsTwo)
if matchFormat.setsToWin == 1 { if matchFormat.setsToWin == 1 {

@ -194,14 +194,10 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
return teamsAvailable return teamsAvailable
})) }))
if rotationIndex > 0 { if rotationIndex > 0, simultaneousStart == false {
rotationMatches = rotationMatches.sorted(by: { rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
if simultaneousStart { return $0.groupStageObject!.index < $1.groupStageObject!.index
return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1)
} else {
return $0.groupStageObject!.index < $1.groupStageObject!.index
}
} else { } else {
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
} }

@ -0,0 +1,53 @@
//
// PlayerPaymentType.swift
// PadelClub
//
// Created by Laurent Morvillier on 11/02/2025.
//
import Foundation
enum PlayerPaymentType: Int, CaseIterable, Identifiable, Codable {
init?(rawValue: Int?) {
guard let value = rawValue else { return nil }
self.init(rawValue: value)
}
var id: Self {
self
}
case cash = 0
case lydia = 1
case gift = 2
case check = 3
case paylib = 4
case bankTransfer = 5
case clubHouse = 6
case creditCard = 7
case forfeit = 8
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .check:
return "Chèque"
case .cash:
return "Cash"
case .lydia:
return "Lydia"
case .paylib:
return "Paylib"
case .bankTransfer:
return "Virement"
case .clubHouse:
return "Clubhouse"
case .creditCard:
return "CB"
case .forfeit:
return "Forfait"
case .gift:
return "Offert"
}
}
}

@ -10,39 +10,19 @@ import LeStorage
@Observable @Observable
final class PlayerRegistration: BasePlayerRegistration, SideStorable { final class PlayerRegistration: BasePlayerRegistration, SideStorable {
// static func resourceName() -> String { "player-registrations" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } func localizedSourceLabel() -> String {
// static func filterByStoreIdentifier() -> Bool { return true } switch source {
// static var relationshipNames: [String] = ["teamRegistration"] case .frenchFederation:
// return "base fédérale"
// var id: String = Store.randomId() case .beachPadel:
// var lastUpdate: Date return "beach-padel"
// var teamRegistration: String? case nil:
// var firstName: String return "créé par vous-même"
// var lastName: String }
// var licenceId: String? }
// var rank: Int?
// var paymentType: PlayerPaymentType?
// var sex: PlayerSexType?
//
// var tournamentPlayed: Int?
// var points: Double?
// var clubName: String?
// var ligueName: String?
// var assimilation: String?
//
// var phoneNumber: String?
// var email: String?
// var birthdate: String?
//
// var computedRank: Int = 0
// var source: PlayerDataSource?
//
// var hasArrived: Bool = false
//
// var storeId: String? = nil
init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerDataSource? = nil, hasArrived: Bool = false) { init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false) {
super.init() super.init()
self.teamRegistration = teamRegistration self.teamRegistration = teamRegistration
self.firstName = firstName self.firstName = firstName
@ -228,6 +208,10 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
source == .beachPadel source == .beachPadel
} }
func unrankedOrUnknown() -> Bool {
source == nil
}
func isValidLicenseNumber(year: Int) -> Bool { func isValidLicenseNumber(year: Int) -> Bool {
guard let licenceId else { return false } guard let licenceId else { return false }
guard licenceId.isLicenseNumber else { return false } guard licenceId.isLicenseNumber else { return false }
@ -252,60 +236,88 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
} }
} }
@MainActor
func updateRank(from sources: [CSVParser], lastRank: Int) async throws { func updateRank(from sources: [CSVParser], lastRank: Int) async throws {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if let dataFound = try await history(from: sources) { if let dataFound = try await history(from: sources) {
rank = dataFound.rankValue?.toInt() rank = dataFound.rankValue?.toInt()
points = dataFound.points points = dataFound.points
tournamentPlayed = dataFound.tournamentCountValue?.toInt() tournamentPlayed = dataFound.tournamentCountValue?.toInt()
} else if let dataFound = try await historyFromName(from: sources) {
rank = dataFound.rankValue?.toInt()
points = dataFound.points
tournamentPlayed = dataFound.tournamentCountValue?.toInt()
} else { } else {
rank = lastRank rank = lastRank
} }
} }
func history(from sources: [CSVParser]) async throws -> Line? { func history(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func history()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let license = licenceId?.strippedLicense else { guard let license = licenceId?.strippedLicense else {
return try await historyFromName(from: sources) return nil // Do NOT call historyFromName here, let updateRank handle it
} }
let filteredSources = sources.filter { $0.maleData == isMalePlayer() }
return await withTaskGroup(of: Line?.self) { group in return await withTaskGroup(of: Line?.self) { group in
for source in sources.filter({ $0.maleData == isMalePlayer() }) { for source in filteredSources {
group.addTask { group.addTask {
guard !Task.isCancelled else { print("Cancelled"); return nil } guard !Task.isCancelled else { print("Cancelled"); return nil }
return try? await source.first { $0.rawValue.contains(";\(license);") }
return try? await source.first(where: { line in
line.rawValue.contains(";\(license);")
})
} }
} }
if let first = await group.first(where: { $0 != nil }) { if let first = await group.first(where: { $0 != nil }) {
group.cancelAll() group.cancelAll()
return first return first
} else {
return nil
} }
return nil
} }
} }
func historyFromName(from sources: [CSVParser]) async throws -> Line? { func historyFromName(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func historyFromName()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let filteredSources = sources.filter { $0.maleData == isMalePlayer() }
let normalizedLastName = lastName.canonicalVersionWithPunctuation
let normalizedFirstName = firstName.canonicalVersionWithPunctuation
return await withTaskGroup(of: Line?.self) { group in return await withTaskGroup(of: Line?.self) { group in
for source in sources.filter({ $0.maleData == isMalePlayer() }) { for source in filteredSources {
group.addTask { [lastName, firstName] in group.addTask {
guard !Task.isCancelled else { print("Cancelled"); return nil } guard !Task.isCancelled else { print("Cancelled"); return nil }
return try? await source.first {
return try? await source.first(where: { line in let lineValue = $0.rawValue.canonicalVersionWithPunctuation
line.rawValue.canonicalVersionWithPunctuation.contains(";\(lastName.canonicalVersionWithPunctuation);\(firstName.canonicalVersionWithPunctuation);") return lineValue.contains(";\(normalizedLastName);\(normalizedFirstName);")
}) }
} }
} }
if let first = await group.first(where: { $0 != nil }) { if let first = await group.first(where: { $0 != nil }) {
group.cancelAll() group.cancelAll()
return first return first
} else {
return nil
} }
return nil
} }
} }
@ -315,7 +327,7 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return return
} }
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 70_000 let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000
switch tournament.tournamentCategory { switch tournament.tournamentCategory {
case .men: case .men:
computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0)
@ -353,58 +365,10 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return false return false
} }
// enum CodingKeys: String, CodingKey { enum PlayerDataSource: Int, Codable {
// case _id = "id" case frenchFederation = 0
// case _storeId = "storeId" case beachPadel = 1
// case _lastUpdate = "lastUpdate" }
// case _teamRegistration = "teamRegistration"
// case _firstName = "firstName"
// case _lastName = "lastName"
// case _licenceId = "licenceId"
// case _rank = "rank"
// case _paymentType = "paymentType"
// case _sex = "sex"
// case _tournamentPlayed = "tournamentPlayed"
// case _points = "points"
// case _clubName = "clubName"
// case _ligueName = "ligueName"
// case _assimilation = "assimilation"
// case _birthdate = "birthdate"
// case _phoneNumber = "phoneNumber"
// case _email = "email"
// case _computedRank = "computedRank"
// case _source = "source"
// case _hasArrived = "hasArrived"
//
// }
//
// func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)
// try container.encode(storeId, forKey: ._storeId)
// try container.encode(lastUpdate, forKey: ._lastUpdate)
// try container.encode(teamRegistration, forKey: ._teamRegistration)
//
// try container.encode(firstName, forKey: ._firstName)
// try container.encode(lastName, forKey: ._lastName)
// try container.encode(licenceId, forKey: ._licenceId)
// try container.encode(rank, forKey: ._rank)
// try container.encode(paymentType, forKey: ._paymentType)
// try container.encode(sex, forKey: ._sex)
// try container.encode(tournamentPlayed, forKey: ._tournamentPlayed)
// try container.encode(points, forKey: ._points)
// try container.encode(clubName, forKey: ._clubName)
// try container.encode(ligueName, forKey: ._ligueName)
// try container.encode(assimilation, forKey: ._assimilation)
// try container.encode(phoneNumber, forKey: ._phoneNumber)
// try container.encode(email, forKey: ._email)
// try container.encode(birthdate, forKey: ._birthdate)
// try container.encode(computedRank, forKey: ._computedRank)
// try container.encode(source, forKey: ._source)
// try container.encode(hasArrived, forKey: ._hasArrived)
// }
static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
switch playerRank { switch playerRank {
@ -491,47 +455,3 @@ enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable {
case female = 0 case female = 0
case male = 1 case male = 1
} }
enum PlayerPaymentType: Int, CaseIterable, Identifiable, Codable {
init?(rawValue: Int?) {
guard let value = rawValue else { return nil }
self.init(rawValue: value)
}
var id: Self {
self
}
case cash = 0
case lydia = 1
case gift = 2
case check = 3
case paylib = 4
case bankTransfer = 5
case clubHouse = 6
case creditCard = 7
case forfeit = 8
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .check:
return "Chèque"
case .cash:
return "Cash"
case .lydia:
return "Lydia"
case .paylib:
return "Paylib"
case .bankTransfer:
return "Virement"
case .clubHouse:
return "Clubhouse"
case .creditCard:
return "CB"
case .forfeit:
return "Forfait"
case .gift:
return "Offert"
}
}
}

@ -586,10 +586,13 @@ defer {
} }
func updateTournamentState() { func updateTournamentState() {
if let tournamentObject = tournamentObject(), index == 0, isUpperBracket(), hasEnded() { let tournamentObject = tournamentObject()
if let tournamentObject, index == 0, isUpperBracket(), hasEnded() {
tournamentObject.endDate = Date() tournamentObject.endDate = Date()
DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject) DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject)
} }
tournamentObject?.updateTournamentState()
} }
func roundStatus() -> String { func roundStatus() -> String {

@ -68,6 +68,19 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
self.qualified = qualified self.qualified = qualified
} }
func hasRegisteredOnline() -> Bool {
players().anySatisfy({ $0.registeredOnline })
}
func unrankedOrUnknown() -> Bool {
players().anySatisfy({ $0.source == nil })
}
func isOutOfTournament() -> Bool {
walkOut
}
required init(from decoder: any Decoder) throws { required init(from decoder: any Decoder) throws {
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -80,7 +93,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func unsortedPlayers() -> [PlayerRegistration] { func unsortedPlayers() -> [PlayerRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id } return tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false }
} }
// MARK: - // MARK: -
@ -113,6 +126,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func isHere() -> Bool { func isHere() -> Bool {
let unsortedPlayers = unsortedPlayers() let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.hasArrived }) return unsortedPlayers.allSatisfy({ $0.hasArrived })
} }
@ -188,7 +202,10 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
func isImported() -> Bool { func isImported() -> Bool {
return unsortedPlayers().allSatisfy({ $0.isImported() }) let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.isImported() })
} }
func isWildCard() -> Bool { func isWildCard() -> Bool {
@ -243,9 +260,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory) self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory)
} }
func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String { func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&") -> String {
if let name { return name } if let name { return name }
return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ") return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " \(separator) ")
} }
func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String { func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String {
@ -299,7 +316,10 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
func canPlay() -> Bool { func canPlay() -> Bool {
return matches().isEmpty == false || players().allSatisfy({ $0.hasPaid() || $0.hasArrived }) let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return matches().isEmpty == false || unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived })
} }
func availableForSeedPick() -> Bool { func availableForSeedPick() -> Bool {
@ -373,19 +393,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? { func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? {
guard let registrationDate else { return nil }
let formattedDate = registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
let onlineSuffix = hasRegisteredOnline() ? " en ligne" : ""
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
if let registrationDate { return "Inscrit\(onlineSuffix) le \(formattedDate)"
return "Inscrit le " + registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
}
case .csv: case .csv:
if let registrationDate { return formattedDate
return registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
}
} }
} }
@ -418,13 +435,35 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) { func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) {
let previousPlayers = Set(unsortedPlayers()) let previousPlayers = Set(unsortedPlayers())
players.forEach { player in
previousPlayers.forEach { oldPlayer in
if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense, player.licenceId?.strippedLicense != nil {
player.registeredOnline = oldPlayer.registeredOnline
player.coach = oldPlayer.coach
player.tournamentPlayed = oldPlayer.tournamentPlayed
player.points = oldPlayer.points
player.captain = oldPlayer.captain
player.assimilation = oldPlayer.assimilation
player.ligueName = oldPlayer.ligueName
}
}
}
let playersToRemove = previousPlayers.subtracting(players) let playersToRemove = previousPlayers.subtracting(players)
self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove) self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove)
setWeight(from: Array(players), inTournamentCategory: tournamentCategory) setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in players.forEach { player in
player.teamRegistration = id player.teamRegistration = id
} }
// do {
// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// } catch {
// Logger.error(error)
// }
} }
typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?) typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)
@ -462,8 +501,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool
func players() -> [PlayerRegistration] { func players() -> [PlayerRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in self.unsortedPlayers().sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [ let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $0.rank ?? Int.max < $1.rank ?? Int.max }, { $0.rank ?? Int.max < $1.rank ?? Int.max },
@ -483,6 +522,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
} }
func coaches() -> [PlayerRegistration] {
guard let store = self.tournamentStore else { return [] }
return store.playerRegistrations.filter { $0.coach }
}
func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) { func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) {
let significantPlayerCount = significantPlayerCount() let significantPlayerCount = significantPlayerCount()
weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+) weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
@ -506,7 +550,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
func unrankValue(for malePlayer: Bool) -> Int { func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 70_000 return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000
} }
func groupStageObject() -> GroupStage? { func groupStageObject() -> GroupStage? {

@ -15,7 +15,11 @@ final class Tournament: BaseTournament {
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0) { // internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0) {
// super.init()
// }
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) {
super.init() super.init()
self.event = event self.event = event
self.name = name self.name = name
@ -25,7 +29,11 @@ final class Tournament: BaseTournament {
#if DEBUG #if DEBUG
self.isPrivate = false self.isPrivate = false
#else #else
self.isPrivate = Guard.main.purchasedTransactions.isEmpty if Guard.main.currentPlan == .monthlyUnlimited {
self.isPrivate = true
} else {
self.isPrivate = Guard.main.purchasedTransactions.isEmpty
}
#endif #endif
self.groupStageFormat = groupStageFormat self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat self.roundFormat = roundFormat
@ -70,7 +78,16 @@ final class Tournament: BaseTournament {
self.loserBracketMode = loserBracketMode self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount self.initialSeedCount = initialSeedCount
self.enableOnlineRegistration = enableOnlineRegistration
self.registrationDateLimit = registrationDateLimit
self.openingRegistrationDate = openingRegistrationDate
self.waitingListLimit = waitingListLimit
self.accountIsRequired = accountIsRequired
self.licenseIsRequired = licenseIsRequired
self.minimumPlayerPerTeam = minimumPlayerPerTeam
self.maximumPlayerPerTeam = maximumPlayerPerTeam
self.information = information
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -83,6 +100,7 @@ final class Tournament: BaseTournament {
override func deleteDependencies() { override func deleteDependencies() {
guard let store = self.tournamentStore else { return } guard let store = self.tournamentStore else { return }
let drawLogs = Array(store.drawLogs) let drawLogs = Array(store.drawLogs)
for drawLog in drawLogs { for drawLog in drawLogs {
drawLog.deleteDependencies() drawLog.deleteDependencies()
@ -109,12 +127,6 @@ final class Tournament: BaseTournament {
store.matchSchedulers.deleteDependencies(self._matchSchedulers()) store.matchSchedulers.deleteDependencies(self._matchSchedulers())
// if let event = self.eventObject() {
// if event.tournaments.count == 1 && event.tournaments.first?.id == self.id {
// DataStore.shared.events.deleteDependencies([event])
// }
// }
} }
// MARK: - Computed Dependencies // MARK: - Computed Dependencies
@ -585,7 +597,7 @@ defer {
} }
#endif #endif
var _sortedTeams : [TeamRegistration] = [] var _sortedTeams : [TeamRegistration] = []
var _teams = unsortedTeams().filter({ $0.walkOut == false }) var _teams = unsortedTeams().filter({ $0.isOutOfTournament() == false })
if let closedRegistrationDate { if let closedRegistrationDate {
_teams = _teams.filter({ team in _teams = _teams.filter({ team in
@ -641,7 +653,7 @@ defer {
func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] { func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] {
let waitingList = Set(unsortedTeams()).subtracting(teams) let waitingList = Set(unsortedTeams()).subtracting(teams)
let waitings = waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) let waitings = waitingList.filter { $0.isOutOfTournament() == false }.sorted(using: _defaultSorting(), order: .ascending)
let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending) let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
if includingWalkOuts { if includingWalkOuts {
return waitings + walkOuts return waitings + walkOuts
@ -685,8 +697,7 @@ defer {
func unsortedTeamsWithoutWO() -> [TeamRegistration] { func unsortedTeamsWithoutWO() -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamRegistrations.filter { $0.walkOut == false } return tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false }
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
} }
func walkoutTeams() -> [TeamRegistration] { func walkoutTeams() -> [TeamRegistration] {
@ -721,7 +732,9 @@ defer {
func paidSelectedPlayers(type: PlayerPaymentType) -> Double? { func paidSelectedPlayers(type: PlayerPaymentType) -> Double? {
if let entryFee { if let entryFee {
return Double(self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }.filter { $0.paymentType == type }.count) * entryFee let flat = self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }
let count = flat.filter { $0.paymentType == type }.count
return Double(count) * entryFee
} else { } else {
return nil return nil
} }
@ -752,7 +765,7 @@ defer {
//todo //todo
func significantPlayerCount() -> Int { func significantPlayerCount() -> Int {
return 2 return minimumPlayerPerTeam
} }
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
@ -1083,14 +1096,16 @@ defer {
groupStages.forEach { groupStage in groupStages.forEach { groupStage in
let groupStageTeams = groupStage.teams(true) let groupStageTeams = groupStage.teams(true)
for (index, team) in groupStageTeams.enumerated() { for (index, team) in groupStageTeams.enumerated() {
if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { if groupStage.hasEnded() {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0)
if let existingTeams = teams[_index] { let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0)
teams[_index] = existingTeams + [team.id] if let existingTeams = teams[_index] {
} else { teams[_index] = existingTeams + [team.id]
teams[_index] = [team.id] } else {
teams[_index] = [team.id]
}
} }
} }
} }
@ -1121,7 +1136,20 @@ defer {
} }
} }
tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) do {
try self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
if self.publishRankings == false {
self.publishRankings = true
do {
try DataStore.shared.tournaments.addOrUpdate(instance: self)
} catch {
Logger.error(error)
}
}
return rankings return rankings
} }
@ -1160,31 +1188,55 @@ defer {
} }
func updateRank(to newDate: Date?) async throws { func updateRank(to newDate: Date?) async throws {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let newDate else { return } guard let newDate else { return }
rankSourceDate = newDate rankSourceDate = newDate
// Fetch current month data only once
let monthData = currentMonthData()
if currentMonthData() == nil {
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) if monthData == nil {
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) async let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
await MainActor.run { async let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
let formatted: String = URL.importDateFormatter.string(from: newDate)
let monthData: MonthData = MonthData(monthKey: formatted) let formatted = URL.importDateFormatter.string(from: newDate)
monthData.maleUnrankedValue = lastRankMan let newMonthData = MonthData(monthKey: formatted)
monthData.femaleUnrankedValue = lastRankWoman
DataStore.shared.monthData.addOrUpdate(instance: monthData) newMonthData.maleUnrankedValue = await lastRankMan
newMonthData.femaleUnrankedValue = await lastRankWoman
do {
try DataStore.shared.monthData.addOrUpdate(instance: newMonthData)
} catch {
Logger.error(error)
} }
} }
let lastRankMan = currentMonthData()?.maleUnrankedValue let lastRankMan = monthData?.maleUnrankedValue ?? 0
let lastRankWoman = currentMonthData()?.femaleUnrankedValue let lastRankWoman = monthData?.femaleUnrankedValue ?? 0
// Fetch only the required files
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
guard !dataURLs.isEmpty else { return } // Early return if no files found
let sources = dataURLs.map { CSVParser(url: $0) } let sources = dataURLs.map { CSVParser(url: $0) }
let players = unsortedPlayers()
try await unsortedPlayers().concurrentForEach { player in try await players.concurrentForEach { player in
try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0) let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan
try await player.updateRank(from: sources, lastRank: lastRank)
} }
} }
func missingUnrankedValue() -> Bool { func missingUnrankedValue() -> Bool {
return maleUnrankedValue == nil || femaleUnrankedValue == nil return maleUnrankedValue == nil || femaleUnrankedValue == nil
} }
@ -1774,12 +1826,20 @@ defer {
} }
} }
func setupFederalSettings() { func setupFederalSettings(fromEvent event: Event?) {
teamSorting = tournamentLevel.defaultTeamSortingType teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat() groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(5) loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
matchFormat = roundSmartMatchFormat(5) matchFormat = roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee entryFee = tournamentLevel.entryFee
if event?.tenupId != nil {
//enableOnlineRegistration = true
registrationDateLimit = deadline(for: .inscription)
}
}
func onlineRegistrationCanBeEnabled() -> Bool {
isAnimation() == false
} }
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
@ -1957,10 +2017,8 @@ defer {
func updateTournamentState() { func updateTournamentState() {
Task { Task {
if hasEnded() { let fr = await finalRanking()
let fr = await finalRanking() _ = await setRankings(finalRanks: fr)
_ = await setRankings(finalRanks: fr)
}
} }
} }
@ -2016,27 +2074,42 @@ defer {
} }
func removeAllSeeds() async { func removeAllSeeds() async {
unsortedTeams().forEach({ team in let teams = unsortedTeams()
teams.forEach({ team in
team.bracketPosition = nil team.bracketPosition = nil
team._cachedRestingTime = nil
}) })
let ts = allRoundMatches().flatMap { match in let allMatches = allRoundMatches()
let ts = allMatches.flatMap { match in
match.teamScores match.teamScores
} }
allMatches.forEach { match in
match.disabled = false
match.losingTeamId = nil
match.winningTeamId = nil
match.endDate = nil
match.removeCourt()
match.servingTeamId = nil
}
do { do {
try tournamentStore?.teamScores.delete(contentOfs: ts) try tournamentStore?.teamScores.delete(contentOfs: ts)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
do { do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) try tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
allRounds().forEach({ round in
round.enableRound()
})
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
updateTournamentState()
} }
func addNewRound(_ roundIndex: Int) async { func addNewRound(_ roundIndex: Int) async {
@ -2100,6 +2173,73 @@ defer {
}) })
} }
func getOnlineRegistrationStatus() -> OnlineRegistrationStatus {
if hasStarted() {
return .inProgress
}
if closedRegistrationDate != nil {
return .ended
}
if endDate != nil {
return .endedWithResults
}
let now = Date()
if let openingRegistrationDate = openingRegistrationDate {
let timezonedDateTime = openingRegistrationDate // Assuming dates are already in local timezone
if now < timezonedDateTime {
return .notStarted
}
}
if let registrationDateLimit = registrationDateLimit {
let timezonedDateTime = registrationDateLimit // Assuming dates are already in local timezone
if now > timezonedDateTime {
return .ended
}
}
let currentTeamCount = unsortedTeamsWithoutWO().count
if currentTeamCount >= teamCount {
if let waitingListLimit = waitingListLimit {
let waitingListCount = currentTeamCount - teamCount
if waitingListCount >= waitingListLimit {
return .waitingListFull
}
}
return .waitingListPossible
}
return .open
}
// MARK: - Status
func shouldTournamentBeOver() -> Bool {
#if _DEBUGING_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func shouldTournamentBeOver()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if isDeleted == false && hasEnded() == false && hasStarted() {
let allMatches = allMatches()
let remainingMatches = allMatches.filter({ $0.hasEnded() == false && $0.startDate != nil })
let calendar = Calendar.current
let anyTomorrow = remainingMatches.anySatisfy({ calendar.isDateInTomorrow($0.startDate!) })
if anyTomorrow == false, let endDate = allMatches.filter({ $0.hasEnded() }).sorted(by: \.endDate!, order: .ascending).last?.endDate, endDate.timeIntervalSinceNow <= -2 * 3600 {
return true
}
}
return false
}
// MARK: - // MARK: -
@ -2306,7 +2446,11 @@ extension Tournament {
func deadline(for type: TournamentDeadlineType) -> Date? { func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
if let date = Calendar.current.date(byAdding: .day, value: type.daysOffset, to: startDate) { var daysOffset = type.daysOffset
if tournamentLevel == .p500 {
daysOffset += 7
}
if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date) let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
} }

@ -254,4 +254,9 @@ extension Date {
formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short
return formatter return formatter
}() }()
func truncateMinutesAndSeconds() -> Date {
let calendar = Calendar.current
return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds()
}
} }

@ -18,6 +18,10 @@ extension String {
String(trimmed.prefix(length)) String(trimmed.prefix(length))
} }
func prefixMultilineTrimmed(_ length: Int) -> String {
String(trimmedMultiline.prefix(length))
}
var trimmed: String { var trimmed: String {
replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines) replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines)
} }

@ -0,0 +1,74 @@
//
// InscriptionLegendView.swift
// PadelClub
//
// Created by razmig on 15/01/2025.
//
import SwiftUI
struct InscriptionLegendView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
List {
Section {
Label("Inscrit en ligne", systemImage: "circle.fill").foregroundStyle(.green)
} footer: {
Text("Icône indiquant que le joueur s'est inscrit en ligne.")
}
Section {
ForEach(RoundRule.colors.prefix(6).indices, id: \.self) { colorIndex in
Text("Équipe placée en \(RoundRule.roundName(fromRoundIndex: colorIndex))")
.listRowView(isActive: true, color: Color(uiColor: .init(fromHex: RoundRule.colors[colorIndex])), hideColorVariation: true, alignment: .leading)
}
}
Section {
Text("Équipe placée en poule")
.listRowView(isActive: true, color: .blue, hideColorVariation: true, alignment: .leading)
}
Section {
Text("Équipe estimée en tableau")
.listRowView(isActive: true, color: .mint, hideColorVariation: true, alignment: .leading)
Text("Équipe estimée en poule")
.listRowView(isActive: true, color: .cyan, hideColorVariation: true, alignment: .leading)
}
Section {
Text("Équipe en liste d'attente")
.listRowView(isActive: true, color: .gray, hideColorVariation: true, alignment: .leading)
Text("Équipe forfaite")
.listRowView(isActive: true, color: .logoRed, hideColorVariation: true, alignment: .leading)
}
Section {
Text("Équipe ayant un joueur à vérifier")
.listRowView(isActive: true, color: .logoRed, hideColorVariation: true, backgroundColor: .logoRed, alignment: .leading)
} footer: {
Text("Une fois que vous avez importé votre fichier, Padel Club vous affiche ainsi les équipes ayant des joueurs ne provenant pas du fichier ni de la base fédérale.")
}
Section {
Text("Équipe ayant un joueur ne provenant pas du fichier beach-padel")
.listRowView(isActive: true, color: .beige, hideColorVariation: true, backgroundColor: .beige, alignment: .leading)
} footer: {
Text("Une fois que vous avez importé votre fichier, Padel Club vous affiche ainsi les équipes ayant des joueurs ne provenant pas du fichier.")
}
}
.headerProminence(.increased)
.navigationTitle("Légende")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarItems(trailing: Button("Fermer") {
dismiss()
})
}
}
}

@ -99,7 +99,7 @@ print("Running in Release mode")
} }
.task { .task {
//try? Tips.resetDatastore() try? Tips.resetDatastore()
try? Tips.configure([ try? Tips.configure([
.displayFrequency(.immediate), .displayFrequency(.immediate),

@ -0,0 +1,85 @@
//
// RegistrationInfoSheetView.swift
// PadelClub
//
// Created by razmig on 15/01/2025.
//
import SwiftUI
struct RegistrationInfoSheetView: View {
@Environment(\.dismiss) private var dismiss
let registrationInfoText: String =
"""
Comment fonctionnent les inscriptions en ligne ?
Les inscriptions en ligne permettent aux joueurs de s'inscrire directement au tournoi via la plateforme. Voici les informations importantes à connaître :
Conditions d'inscription :
- Un compte Padel Club est requis pour s'inscrire
- Une licence valide peut être nécessaire
- Les équipes des tournois homologués doivent être composées de 2 joueurs
- Les animations ont moins de restrictions
Déroulement des inscriptions :
1. Les inscriptions peuvent avoir une date et heure d'ouverture définies par l'organisateur
2. Le tournoi peut avoir une capacité maximale d'équipes
3. Si une capacité maximale est définie, les nouvelles inscriptions seront placées en liste d'attente une fois celle-ci atteinte
4. La liste d'attente peut également avoir une limite maximale d'équipes
5. Les inscriptions peuvent se terminer à une date limite fixée par l'organisateur
Désinscription :
La désinscription est possible tant que :
- Le tournoi n'a pas commencé
- La date limite d'inscription n'est pas dépassée
- Les inscriptions n'ont pas é clôturées par l'organisateur
Validation des inscriptions :
- L'inscription n'est définitive qu'après validation des critères d'éligibilité (catégorie, classement, âge...)
- En cas de désistement d'une équipe inscrite, la première équipe en liste d'attente est automatiquement intégrée au tableau
- Une équipe en liste d'attente peut se désinscrire à tout moment selon les mêmes conditions
L'organisateur se réserve le droit de modifier ces conditions ou de clôturer les inscriptions de manière anticipée.
"""
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// Content sections
ForEach(registrationInfoText.components(separatedBy: "\n\n"), id: \.self) { section in
if !section.isEmpty {
VStack(alignment: .leading, spacing: 10) {
if section.contains(":") {
Text(section.components(separatedBy: ":")[0])
.font(.headline)
.foregroundColor(.primary)
let bulletPoints = section.components(separatedBy: "\n-")
if bulletPoints.count > 1 {
ForEach(bulletPoints.dropFirst(), id: \.self) { point in
HStack(alignment: .top) {
Text("")
.padding(.trailing, 5)
Text(point)
.fixedSize(horizontal: false, vertical: true)
}
}
}
} else {
Text(section)
}
}
.padding(.bottom, 10)
}
}
}
.padding()
}
.navigationBarItems(trailing: Button("Fermer") {
dismiss()
})
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscription en ligne")
}
}
}

@ -28,9 +28,6 @@ class ImportObserver {
func currentlyImportingLabel() -> String { func currentlyImportingLabel() -> String {
guard let currentImportDate else { return "import en cours" } guard let currentImportDate else { return "import en cours" }
if URL.importDateFormatter.string(from: currentImportDate) == "07-2024" {
return "consolidation des données"
}
return "import " + currentImportDate.monthYearFormatted return "import " + currentImportDate.monthYearFormatted
} }
@ -44,32 +41,38 @@ class ImportObserver {
class FileImportManager { class FileImportManager {
static let shared = FileImportManager() static let shared = FileImportManager()
func updatePlayers(isMale: Bool, players: inout [FederalPlayer]) { func updatePlayers(isMale: Bool, players: inout [FederalPlayer]) {
let replacements: [(Character, Character)] = [("Á", "ç"), ("", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")] let replacements: [(Character, Character)] = [("Á", "ç"), ("", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")]
var playersLeft = players var playersLeft = Dictionary(uniqueKeysWithValues: players.map { ($0.license, $0) })
SourceFileManager.shared.allFilesSortedByDate(isMale).forEach({ url in
if playersLeft.isEmpty == false { SourceFileManager.shared.allFilesSortedByDate(isMale).forEach { url in
let federalPlayers = readCSV(inputFile: url) if playersLeft.isEmpty { return }
let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements
let federalPlayers = readCSV(inputFile: url)
let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements
let federalPlayersDict = Dictionary(uniqueKeysWithValues: federalPlayers.map { ($0.license, $0) })
for (license, importedPlayer) in playersLeft {
guard let federalPlayer = federalPlayersDict[license] else { continue }
playersLeft.forEach { importedPlayer in var lastName = federalPlayer.lastName
if let federalPlayer = federalPlayers.first(where: { $0.license == importedPlayer.license }) { var firstName = federalPlayer.firstName
var lastName = federalPlayer.lastName
lastName.replace(characters: replacementsCharacters) lastName.replace(characters: replacementsCharacters)
var firstName = federalPlayer.firstName firstName.replace(characters: replacementsCharacters)
firstName.replace(characters: replacementsCharacters)
importedPlayer.lastName = lastName.trimmed.uppercased() importedPlayer.lastName = lastName.trimmed.uppercased()
importedPlayer.firstName = firstName.trimmed.capitalized importedPlayer.firstName = firstName.trimmed.capitalized
}
} playersLeft.removeValue(forKey: license) // Remove processed player
} }
}) }
players = playersLeft players = Array(playersLeft.values)
} }
func foundInWomenData(license: String?) -> Bool { func foundInWomenData(license: String?) -> Bool {
guard let license = license?.strippedLicense else { guard let license = license?.strippedLicense else {
return false return false
@ -148,7 +151,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 ? 70_000 : 10_000) }).prefix(significantPlayerCount) let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 90_000 : 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,+)

@ -37,6 +37,8 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
func requestLocation() { func requestLocation() {
lastError = nil lastError = nil
manager.requestLocation() manager.requestLocation()
city = nil
location = nil
requestStarted = true requestStarted = true
} }

@ -369,6 +369,15 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
func wildcardArePossible() -> Bool {
switch self {
case .p500, .p1000, .p1500, .p2000:
return true
default:
return false
}
}
func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge) -> Int { func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge) -> Int {
switch self { switch self {
case .p25: case .p25:
@ -1648,7 +1657,7 @@ enum PlayersCountRange: Int, CaseIterable {
} }
enum RoundRule { enum RoundRule {
static let colors = ["#99ff99", "#66ff66", "#33cc33", "#009900", "#006600", "#006600", "#006600", "#006600", "#006600", "#006600"] static let colors = ["#99ff99", "#66ff66", "#33cc33", "#009900", "#006600", "#336633", "#DD6600", "#EE6633", "#EE6633", "#EE6633"]
static func loserBrackets(index: Int) -> [String] { static func loserBrackets(index: Int) -> [String] {
switch index { switch index {

@ -16,6 +16,7 @@ class SourceFileManager {
} }
let rankingSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "rankings") let rankingSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "rankings")
let anonymousSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "anonymous")
func createDirectoryIfNeeded() { func createDirectoryIfNeeded() {
let fileManager = FileManager.default let fileManager = FileManager.default
@ -193,6 +194,13 @@ class SourceFileManager {
} }
} }
func anonymousFiles() -> [URL] {
let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: anonymousSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "csv"
})
return allJSONFiles
}
func jsonFiles() -> [URL] { func jsonFiles() -> [URL] {
let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "json" url.pathExtension == "json"

@ -21,7 +21,7 @@ struct PadelBeachExportTip: Tip {
var image: Image? { var image: Image? {
Image(systemName: "square.and.arrow.up") Image(systemName: "square.and.arrow.up")
} }
var actions: [Action] { var actions: [Action] {
Action(id: "more-info-export", title: "En savoir plus") Action(id: "more-info-export", title: "En savoir plus")
Action(id: "beach-padel", title: "beach-padel.app.fft.fr") Action(id: "beach-padel", title: "beach-padel.app.fft.fr")
@ -42,7 +42,7 @@ struct PadelBeachImportTip: Tip {
var image: Image? { var image: Image? {
Image(systemName: "square.and.arrow.down") Image(systemName: "square.and.arrow.down")
} }
var actions: [Action] { var actions: [Action] {
Action(id: "more-info-import", title: "Importer le fichier excel beach-padel") Action(id: "more-info-import", title: "Importer le fichier excel beach-padel")
} }
@ -61,8 +61,8 @@ struct GenerateLoserBracketTip: Tip {
var image: Image? { var image: Image? {
nil nil
} }
var actions: [Action] { var actions: [Action] {
Action(id: "generate-loser-bracket", title: "Générer les matchs de classements") Action(id: "generate-loser-bracket", title: "Générer les matchs de classements")
} }
@ -83,7 +83,7 @@ struct TeamChampionshipTip: Tip {
var image: Image? { var image: Image? {
Image(systemName: "person.3") Image(systemName: "person.3")
} }
var actions: [Action] { var actions: [Action] {
Action(id: "list-manager", title: "Ouvrir le gestionnaire d'équipe") Action(id: "list-manager", title: "Ouvrir le gestionnaire d'équipe")
} }
@ -104,7 +104,7 @@ struct TeamChampionshipMainScreenTip: Tip {
var image: Image? { var image: Image? {
Image(systemName: "arrow.uturn.backward") Image(systemName: "arrow.uturn.backward")
} }
var actions: [Action] { var actions: [Action] {
Action(id: "set-list-manager-main", title: "Afficher sur l'écran principal") Action(id: "set-list-manager-main", title: "Afficher sur l'écran principal")
} }
@ -193,7 +193,7 @@ struct InscriptionManagerWomanRankTip: Tip {
var image: Image? { var image: Image? {
Image(systemName: "figure.dress.line.vertical.figure") Image(systemName: "figure.dress.line.vertical.figure")
} }
var title: Text { var title: Text {
Text("Rang d'une joueuse dans un tournoi messieurs") Text("Rang d'une joueuse dans un tournoi messieurs")
} }
@ -213,7 +213,7 @@ struct InscriptionManagerRankUpdateTip: Tip {
var message: Text? { var message: Text? {
Text("Padel Club vous permet de mettre à jour le classement des équipes inscrites. Si vous avez clôturé les inscriptions, la mise à jour du classement ne modifie pas la phase d'intégration de l'équipe, poule ou tableau final. Vous pouvez manuellement mettre à jour cette option.") Text("Padel Club vous permet de mettre à jour le classement des équipes inscrites. Si vous avez clôturé les inscriptions, la mise à jour du classement ne modifie pas la phase d'intégration de l'équipe, poule ou tableau final. Vous pouvez manuellement mettre à jour cette option.")
} }
var image: Image? { var image: Image? {
Image(systemName: "list.number") Image(systemName: "list.number")
} }
@ -232,7 +232,7 @@ struct SharePictureTip: Tip {
var message: Text? { var message: Text? {
Text("Lors d'un partage d'une photo, le texte est disponible dans le presse-papier du téléphone") Text("Lors d'un partage d'une photo, le texte est disponible dans le presse-papier du téléphone")
} }
var image: Image? { var image: Image? {
Image(systemName: "photo.badge.checkmark.fill") Image(systemName: "photo.badge.checkmark.fill")
} }
@ -246,7 +246,7 @@ struct NewRankDataAvailableTip: Tip {
var message: Text? { var message: Text? {
Text("Padel Club récupère toutes les données publique provenant de la FFT. L'importation de ce nouveau classement peut prendre plusieurs dizaines de secondes.") Text("Padel Club récupère toutes les données publique provenant de la FFT. L'importation de ce nouveau classement peut prendre plusieurs dizaines de secondes.")
} }
var image: Image? { var image: Image? {
Image(systemName: "exclamationmark.icloud") Image(systemName: "exclamationmark.icloud")
} }
@ -266,7 +266,7 @@ struct ClubSearchTip: Tip {
var message: Text? { var message: Text? {
Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
} }
var image: Image? { var image: Image? {
Image(systemName: "house.and.flag.fill") Image(systemName: "house.and.flag.fill")
} }
@ -275,7 +275,7 @@ struct ClubSearchTip: Tip {
Action(id: ActionKey.searchAroundMe.rawValue, title: "Chercher autour de moi") Action(id: ActionKey.searchAroundMe.rawValue, title: "Chercher autour de moi")
Action(id: ActionKey.searchCity.rawValue, title: "Chercher une ville") Action(id: ActionKey.searchCity.rawValue, title: "Chercher une ville")
} }
enum ActionKey: String { enum ActionKey: String {
case searchAroundMe = "search-around-me" case searchAroundMe = "search-around-me"
case searchCity = "search-city" case searchCity = "search-city"
@ -291,7 +291,7 @@ struct SlideToDeleteTip: Tip {
var message: Text? { var message: Text? {
Text("Vous pouvez effacer un club en glissant votre doigt vers la gauche") Text("Vous pouvez effacer un club en glissant votre doigt vers la gauche")
} }
var image: Image? { var image: Image? {
Image(systemName: "trash") Image(systemName: "trash")
} }
@ -306,7 +306,7 @@ struct MultiTournamentsEventTip: Tip {
var message: Text? { var message: Text? {
Text("Padel Club permet de gérer plusieurs tournois ayant lieu en même temps. Un P100 homme et dame le même week-end par exemple.") Text("Padel Club permet de gérer plusieurs tournois ayant lieu en même temps. Un P100 homme et dame le même week-end par exemple.")
} }
var image: Image? { var image: Image? {
Image(systemName: "trophy.circle") Image(systemName: "trophy.circle")
} }
@ -320,7 +320,7 @@ struct NotFoundAreWalkOutTip: Tip {
var message: Text? { var message: Text? {
Text("Si une équipe déjà présente dans votre liste d'attente n'est pas dans le fichier, elle sera mise WO") Text("Si une équipe déjà présente dans votre liste d'attente n'est pas dans le fichier, elle sera mise WO")
} }
var image: Image? { var image: Image? {
Image(systemName: "person.2.slash.fill") Image(systemName: "person.2.slash.fill")
} }
@ -338,7 +338,7 @@ struct TournamentPublishingTip: Tip {
var message: Text? { var message: Text? {
Text("Padel Club vous permet de publier votre tournoi et rendre accessible à tous les résultats des matchs et l'évolution de l'événement. Les informations seront accessibles sur le site Padel Club.") Text("Padel Club vous permet de publier votre tournoi et rendre accessible à tous les résultats des matchs et l'évolution de l'événement. Les informations seront accessibles sur le site Padel Club.")
} }
var image: Image? { var image: Image? {
Image("PadelClub_logo_fondclair_transparent") Image("PadelClub_logo_fondclair_transparent")
} }
@ -352,7 +352,7 @@ struct TournamentTVBroadcastTip: Tip {
var message: Text? { var message: Text? {
return Text("Padel Club vous propose un site spéficique à utiliser sur les écrans de votre club, présentant de manière intelligente l'évolution de votre tournoi.") return Text("Padel Club vous propose un site spéficique à utiliser sur les écrans de votre club, présentant de manière intelligente l'évolution de votre tournoi.")
} }
var image: Image? { var image: Image? {
Image(systemName: "sparkles.tv") Image(systemName: "sparkles.tv")
} }
@ -361,7 +361,7 @@ struct TournamentTVBroadcastTip: Tip {
struct TournamentSelectionTip: Tip { struct TournamentSelectionTip: Tip {
@Parameter @Parameter
static var tournamentCount: Int? = nil static var tournamentCount: Int? = nil
var rules: [Rule] { var rules: [Rule] {
[ [
// Define a rule based on the app state. // Define a rule based on the app state.
@ -379,7 +379,7 @@ struct TournamentSelectionTip: Tip {
var message: Text? { var message: Text? {
return Text("Vous pouvez appuyer sur la barre de navigation pour accéder à un tournoi de votre événement.") return Text("Vous pouvez appuyer sur la barre de navigation pour accéder à un tournoi de votre événement.")
} }
var image: Image? { var image: Image? {
Image(systemName: "filemenu.and.selection") Image(systemName: "filemenu.and.selection")
} }
@ -388,7 +388,7 @@ struct TournamentSelectionTip: Tip {
struct TournamentRunningTip: Tip { struct TournamentRunningTip: Tip {
@Parameter @Parameter
static var isRunning: Bool = false static var isRunning: Bool = false
var rules: [Rule] { var rules: [Rule] {
[ [
// Define a rule based on the app state. // Define a rule based on the app state.
@ -406,7 +406,7 @@ struct TournamentRunningTip: Tip {
var message: Text? { var message: Text? {
return Text("Le tournoi a commencé, les options utiles surtout à sa préparation sont maintenant accessibles dans le menu en haut à droite.") return Text("Le tournoi a commencé, les options utiles surtout à sa préparation sont maintenant accessibles dans le menu en haut à droite.")
} }
var image: Image? { var image: Image? {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")
} }
@ -421,18 +421,18 @@ struct CreateAccountTip: Tip {
let message = "Un compte est nécessaire pour publier le tournoi sur [Padel Club](\(URLs.main.rawValue)) et profiter de toutes les pages du site, comme le mode TV pour transformer l'expérience de vos tournois !" let message = "Un compte est nécessaire pour publier le tournoi sur [Padel Club](\(URLs.main.rawValue)) et profiter de toutes les pages du site, comme le mode TV pour transformer l'expérience de vos tournois !"
return Text(.init(message)) return Text(.init(message))
} }
var image: Image? { var image: Image? {
Image(systemName: "person.crop.circle") Image(systemName: "person.crop.circle")
} }
var actions: [Action] { var actions: [Action] {
Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte") Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte")
//todo //todo
//Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus") //Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus")
Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Voir le site Padel Club") Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Voir le site Padel Club")
} }
enum ActionKey: String { enum ActionKey: String {
case createAccount = "createAccount" case createAccount = "createAccount"
case learnMore = "learnMore" case learnMore = "learnMore"
@ -443,7 +443,7 @@ struct CreateAccountTip: Tip {
struct SlideToDeleteSeedTip: Tip { struct SlideToDeleteSeedTip: Tip {
@Parameter @Parameter
static var seeds: Int = 0 static var seeds: Int = 0
var rules: [Rule] { var rules: [Rule] {
[ [
// Define a rule based on the app state. // Define a rule based on the app state.
@ -461,7 +461,7 @@ struct SlideToDeleteSeedTip: Tip {
var message: Text? { var message: Text? {
Text("Vous pouvez retirer une tête de série de sa position en glissant votre doigt vers la gauche") Text("Vous pouvez retirer une tête de série de sa position en glissant votre doigt vers la gauche")
} }
var image: Image? { var image: Image? {
Image(systemName: "person.fill.xmark") Image(systemName: "person.fill.xmark")
} }
@ -470,7 +470,7 @@ struct SlideToDeleteSeedTip: Tip {
struct PrintTip: Tip { struct PrintTip: Tip {
@Parameter @Parameter
static var seeds: Int = 0 static var seeds: Int = 0
var rules: [Rule] { var rules: [Rule] {
[ [
// Define a rule based on the app state. // Define a rule based on the app state.
@ -480,7 +480,7 @@ struct PrintTip: Tip {
} }
] ]
} }
var title: Text { var title: Text {
Text("Coup d'oeil de votre tableau") Text("Coup d'oeil de votre tableau")
} }
@ -488,7 +488,7 @@ struct PrintTip: Tip {
var message: Text? { var message: Text? {
Text("Vous pouvez avoir un aperçu de votre tableau ou l'imprimer.") Text("Vous pouvez avoir un aperçu de votre tableau ou l'imprimer.")
} }
var image: Image? { var image: Image? {
Image(systemName: "printer") Image(systemName: "printer")
} }
@ -505,9 +505,9 @@ struct PrintTip: Tip {
struct BracketEditTip: Tip { struct BracketEditTip: Tip {
@Parameter @Parameter
static var matchesHidden: Int = 0 static var matchesHidden: Int = 0
var nextRoundName: String? var nextRoundName: String?
var rules: [Rule] { var rules: [Rule] {
[ [
// Define a rule based on the app state. // Define a rule based on the app state.
@ -528,14 +528,14 @@ struct BracketEditTip: Tip {
let wording = nextRoundName != nil ? "en \(nextRoundName!)" : "dans la manche suivante" let wording = nextRoundName != nil ? "en \(nextRoundName!)" : "dans la manche suivante"
return Text("Padel Club a bien pris en compte \(article) tête\(Self.matchesHidden.pluralSuffix) de série positionnée\(Self.matchesHidden.pluralSuffix) \(wording). Le\(Self.matchesHidden.pluralSuffix) \(Self.matchesHidden) match\(Self.matchesHidden.pluralSuffix) inutile\(Self.matchesHidden.pluralSuffix) \(grammar) été désactivé automatiquement.") return Text("Padel Club a bien pris en compte \(article) tête\(Self.matchesHidden.pluralSuffix) de série positionnée\(Self.matchesHidden.pluralSuffix) \(wording). Le\(Self.matchesHidden.pluralSuffix) \(Self.matchesHidden) match\(Self.matchesHidden.pluralSuffix) inutile\(Self.matchesHidden.pluralSuffix) \(grammar) été désactivé automatiquement.")
} }
var image: Image? { var image: Image? {
Image(systemName: "rectangle.slash") Image(systemName: "rectangle.slash")
} }
} }
struct TeamsExportTip: Tip { struct TeamsExportTip: Tip {
var title: Text { var title: Text {
Text("Exporter les paires") Text("Exporter les paires")
} }
@ -543,7 +543,7 @@ struct TeamsExportTip: Tip {
var message: Text? { var message: Text? {
Text("Partager les paires comme indiqué dans le guide de la compétition à J-6 avant midi.") Text("Partager les paires comme indiqué dans le guide de la compétition à J-6 avant midi.")
} }
var image: Image? { var image: Image? {
Image(systemName: "square.and.arrow.up") Image(systemName: "square.and.arrow.up")
} }
@ -578,13 +578,79 @@ struct TimeSlotMoveOptionTip: Tip {
} }
struct PlayerTournamentSearchTip: Tip {
var title: Text {
Text("Cherchez un tournoi autour de vous !")
}
var message: Text? {
Text("Padel Club facilite la recherche de tournois et l'inscription !")
}
var image: Image? {
Image(systemName: "trophy.circle")
}
var actions: [Action] {
Action(id: ActionKey.selectAction.rawValue, title: "Éssayer")
}
enum ActionKey: String {
case selectAction = "selectAction"
}
}
struct OnlineRegistrationTip: Tip {
var title: Text {
Text("Inscription en ligne")
}
var message: Text? {
Text("Facilitez les inscriptions à votre tournoi en activant l'inscription en ligne. Les joueurs pourront s'inscrire directement depuis l'application ou le site Padel Club.")
}
var image: Image? {
Image(systemName: "person.2.crop.square.stack")
}
var actions: [Action] {
[
Action(id: ActionKey.more.rawValue, title: "En savoir plus"),
Action(id: ActionKey.enableOnlineRegistration.rawValue, title: "Activer dans les réglages du tournoi")
]
}
enum ActionKey: String {
case more = "more"
case enableOnlineRegistration = "enableOnlineRegistration"
}
}
struct ShouldTournamentBeOverTip: Tip {
var title: Text {
Text("Clôturer le tournoi ?")
}
var message: Text? {
Text("Le dernier match est terminé depuis plus de 2 heures. Si le tournoi a été annulé pour cause de météo vous pouvez l'indiquer comme 'Annulé' dans le menu en haut à droite, si ce n'est pas le cas, saisissez les scores manquants pour clôturer automatiquement le tournoi et publier le classement final.")
}
var image: Image? {
Image(systemName: "clock.badge.questionmark")
}
var actions: [Action] {
Action(id: "tournament-status", title: "Gérer le statut du tournoi")
}
}
struct TipStyleModifier: ViewModifier { struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var tint: Color? var tint: Color?
var background: Color? var background: Color?
var asSection: Bool var asSection: Bool
func body(content: Content) -> some View { func body(content: Content) -> some View {
if asSection { if asSection {
Section { Section {
@ -594,7 +660,7 @@ struct TipStyleModifier: ViewModifier {
preparedContent(content: content) preparedContent(content: content)
} }
} }
@ViewBuilder @ViewBuilder
func preparedContent(content: Content) -> some View { func preparedContent(content: Content) -> some View {
if let background { if let background {

@ -31,8 +31,8 @@ enum URLs: String, Identifiable {
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app" //case padelClub = "https://padelclub.app"
case tenup = "https://tenup.fft.fr" case tenup = "https://tenup.fft.fr"
case padelCompetitionGeneralGuide = "https://fft-site.cdn.prismic.io/fft-site/Zqi2PB5LeNNTxlrS_1-REGLESGENERALESDELACOMPETITION-ANNEESPORTIVE2025.pdf" case padelCompetitionGeneralGuide = "https://fft-site.cdn.prismic.io/fft-site/Z2mH0ZbqstJ98yso_CHAPITREIRèglesgénérales.pdf"
case padelCompetitionSpecificGuide = "https://fft-site.cdn.prismic.io/fft-site/Zqi4ax5LeNNTxlsu_3-CAHIERDESCHARGESDESTOURNOIS-ANNEESPORTIVE2025.pdf" case padelCompetitionSpecificGuide = "https://fft-site.cdn.prismic.io/fft-site/Z2mHz5bqstJ98ysm_CHAPITREIIICahierdeschargesdestournois.pdf"
case padelRules = "https://xlr.alwaysdata.net/static/rules/padel-rules-2024.pdf" case padelRules = "https://xlr.alwaysdata.net/static/rules/padel-rules-2024.pdf"
case restingDischarge = "https://club.fft.fr/tennisfirmidecazeville/60120370_d/data_1/pdf/fo/formlairededechargederesponsabilitetournoidepadel.pdf" case restingDischarge = "https://club.fft.fr/tennisfirmidecazeville/60120370_d/data_1/pdf/fo/formlairededechargederesponsabilitetournoidepadel.pdf"
case appReview = "https://apps.apple.com/app/padel-club/id6484163558?mt=8&action=write-review" case appReview = "https://apps.apple.com/app/padel-club/id6484163558?mt=8&action=write-review"

@ -90,7 +90,7 @@ class FederalDataViewModel {
&& &&
(ageCategories.isEmpty || tournament.tournaments.anySatisfy({ ageCategories.contains($0.age) })) (ageCategories.isEmpty || tournament.tournaments.anySatisfy({ ageCategories.contains($0.age) }))
&& &&
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!)) (selectedClubs.isEmpty || (tournament.codeClub != nil && selectedClubs.contains(tournament.codeClub!)))
&& &&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&& &&
@ -100,7 +100,7 @@ class FederalDataViewModel {
func countForTournamentBuilds(from tournaments: [any FederalTournamentHolder]) -> Int { func countForTournamentBuilds(from tournaments: [any FederalTournamentHolder]) -> Int {
tournaments.filter({ tournament in tournaments.filter({ tournament in
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!)) (selectedClubs.isEmpty || (tournament.codeClub != nil && selectedClubs.contains(tournament.codeClub!)))
&& &&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&& &&
@ -151,7 +151,7 @@ class FederalDataViewModel {
&& &&
(ageCategories.isEmpty || ageCategories.contains(build.age)) (ageCategories.isEmpty || ageCategories.contains(build.age))
&& &&
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!)) (selectedClubs.isEmpty || (tournament.codeClub != nil && selectedClubs.contains(tournament.codeClub!)))
&& &&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&& &&

@ -94,11 +94,11 @@ class MatchDescriptor: ObservableObject {
} }
var teamOneScores: [String] { var teamOneScores: [String] {
setDescriptors.compactMap { $0.valueTeamOne }.map { "\($0)" } setDescriptors.compactMap { $0.getValue(teamPosition: .one) }
} }
var teamTwoScores: [String] { var teamTwoScores: [String] {
setDescriptors.compactMap { $0.valueTeamTwo }.map { "\($0)" } setDescriptors.compactMap { $0.getValue(teamPosition: .two) }
} }
var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count } var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count }

@ -22,4 +22,5 @@ enum Screen: String, Codable {
case print case print
case share case share
case restingTime case restingTime
case stateSettings
} }

@ -319,10 +319,10 @@ class SearchViewModel: ObservableObject, Identifiable {
// Remove all characters that are not in the allowedCharacterSet // Remove all characters that are not in the allowedCharacterSet
var text = pasteField.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmedMultiline var text = pasteField.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmedMultiline
// Define the regex pattern to match digits // Define the regex pattern to match digits
let digitPattern = /\d+/ let digitPattern = /\b\w*\d\w*\b/
// Replace all occurrences of the pattern (digits) with an empty string // Replace all occurrences of the pattern (digits) with an empty string
text = text.replacing(digitPattern, with: "") text = text.replacing(digitPattern, with: "").trimmingCharacters(in: .whitespacesAndNewlines)
let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines) let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines)
let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 } let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 }
@ -367,6 +367,7 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
print(predicate)
return predicate return predicate
} }

@ -40,4 +40,27 @@ struct SetDescriptor: Identifiable, Equatable {
var shouldTieBreak: Bool { var shouldTieBreak: Bool {
setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0) setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0)
} }
func getValue(teamPosition: TeamPosition) -> String? {
switch teamPosition {
case .one:
if let valueTeamOne {
if let tieBreakValueTeamOne {
return "\(valueTeamOne)-\(tieBreakValueTeamOne)"
} else {
return "\(valueTeamOne)"
}
}
case .two:
if let valueTeamTwo {
if let tieBreakValueTeamTwo {
return "\(valueTeamTwo)-\(tieBreakValueTeamTwo)"
} else {
return "\(valueTeamTwo)"
}
}
}
return nil
}
} }

@ -143,7 +143,7 @@ struct EventCreationView: View {
tournament.courtCount = selectedClub?.courtCount ?? 2 tournament.courtCount = selectedClub?.courtCount ?? 2
tournament.startDate = startingDate tournament.startDate = startingDate
tournament.dayDuration = duration tournament.dayDuration = duration
tournament.setupFederalSettings() tournament.setupFederalSettings(fromEvent: event)
} }
do { do {

@ -63,7 +63,7 @@ struct EventTournamentsView: View {
newTournament.courtCount = event.eventCourtCount() newTournament.courtCount = event.eventCourtCount()
newTournament.startDate = event.eventStartDate() newTournament.startDate = event.eventStartDate()
newTournament.dayDuration = event.eventDayDuration() newTournament.dayDuration = event.eventDayDuration()
newTournament.setupFederalSettings() newTournament.setupFederalSettings(fromEvent: event)
do { do {
try dataStore.tournaments.addOrUpdate(instance: newTournament) try dataStore.tournaments.addOrUpdate(instance: newTournament)

@ -135,9 +135,9 @@ struct ClubDetailView: View {
.onChange(of: acronymMode) { .onChange(of: acronymMode) {
focusedField = ._acronym focusedField = ._acronym
if acronymMode == .custom { if acronymMode == .custom {
club.acronym = "" //club.acronym = ""
} else { } else {
club.acronym = club.automaticShortName().uppercased() //club.acronym = club.automaticShortName().uppercased()
} }
} }
} footer: { } footer: {

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
struct CopyPasteButtonView: View { struct CopyPasteButtonView: View {
var title: String?
let pasteValue: String? let pasteValue: String?
@State private var copied: Bool = false @State private var copied: Bool = false
@ -20,6 +21,9 @@ struct CopyPasteButtonView: View {
copied = true copied = true
} label: { } label: {
Label(copied ? "Copié" : "Copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) Label(copied ? "Copié" : "Copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none)
if let title {
Text(title)
}
} }
} }
} }

@ -15,28 +15,31 @@ struct RowButtonView: View {
var systemImage: String? = nil var systemImage: String? = nil
var image: String? = nil var image: String? = nil
let confirmationMessage: String let confirmationMessage: String
let cornerRadius: CGFloat
var action: (() -> ())? = nil var action: (() -> ())? = nil
var asyncAction: (() async -> ())? = nil var asyncAction: (() async -> ())? = nil
@State private var askConfirmation: Bool = false @State private var askConfirmation: Bool = false
@State private var isLoading = false @State private var isLoading = false
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, action: @escaping (() -> ())) { init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, cornerRadius: CGFloat = 8, confirmationMessage: String? = nil, action: @escaping (() -> ())) {
self.role = role self.role = role
self.title = title self.title = title
self.systemImage = systemImage self.systemImage = systemImage
self.image = image self.image = image
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.action = action self.action = action
self.cornerRadius = cornerRadius
} }
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, asyncAction: @escaping (() async -> ())) { init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, cornerRadius: CGFloat = 8, confirmationMessage: String? = nil, asyncAction: @escaping (() async -> ())) {
self.role = role self.role = role
self.title = title self.title = title
self.systemImage = systemImage self.systemImage = systemImage
self.image = image self.image = image
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.asyncAction = asyncAction self.asyncAction = asyncAction
self.cornerRadius = cornerRadius
} }
var body: some View { var body: some View {
@ -79,6 +82,7 @@ struct RowButtonView: View {
if isLoading { if isLoading {
ZStack { ZStack {
Color.master Color.master
.cornerRadius(cornerRadius)
ProgressView() ProgressView()
.tint(.white) .tint(.white)
} }

@ -16,6 +16,8 @@ struct StepperView: View {
var maximum: Int? = nil var maximum: Int? = nil
var countChanged: (() -> ())? = nil var countChanged: (() -> ())? = nil
var submitFollowUpAction: (() -> ())? = nil
@FocusState private var amountIsFocused: Bool
var body: some View { var body: some View {
VStack { VStack {
@ -32,17 +34,14 @@ struct StepperView: View {
.buttonStyle(.borderless) .buttonStyle(.borderless)
TextField("00", value: $count, format: .number) TextField("00", value: $count, format: .number)
.focused($amountIsFocused)
.keyboardType(.numberPad) .keyboardType(.numberPad)
.fixedSize() .fixedSize()
// .font(.title2) // .font(.title2)
.monospacedDigit() .monospacedDigit()
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.onSubmit { .onSubmit {
if let minimum, count < minimum { _validate()
count = minimum
} else if let maximum, count > maximum {
count = maximum
}
} }
Button(action: { Button(action: {
self._add() self._add()
@ -60,6 +59,28 @@ struct StepperView: View {
} }
} }
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.toolbar {
ToolbarItem(placement: .keyboard) {
if amountIsFocused {
HStack {
Spacer()
Button("Confirmer") {
amountIsFocused = false
_validate()
}
}
}
}
}
}
fileprivate func _validate() {
if let minimum, count < minimum {
count = minimum
} else if let maximum, count > maximum {
count = maximum
}
submitFollowUpAction?()
} }
fileprivate func _minusIsDisabled() -> Bool { fileprivate func _minusIsDisabled() -> Bool {
@ -67,7 +88,7 @@ struct StepperView: View {
} }
fileprivate func _plusIsDisabled() -> Bool { fileprivate func _plusIsDisabled() -> Bool {
count >= (maximum ?? 70_000) count >= (maximum ?? 90_000)
} }
fileprivate func _add() { fileprivate func _add() {

@ -127,12 +127,28 @@ struct PlayerBlockView: View {
} else { } else {
Divider().frame(width: width).overlay(Color(white: 0.9)) Divider().frame(width: width).overlay(Color(white: 0.9))
} }
Text(string)
.font(.title3) let parts = string.components(separatedBy: "-")
.frame(maxWidth: 20) if parts.count == 2, let mainScore = parts.first, let supScore = parts.last {
.scaledToFill() HStack(spacing: 0) {
.minimumScaleFactor(0.5) Text(mainScore)
.lineLimit(1) .font(.title3)
.frame(maxWidth: 20)
.scaledToFill()
.minimumScaleFactor(0.5)
.lineLimit(1)
Text(supScore)
.font(.caption2)
.baselineOffset(10)
}
} else {
Text(string)
.font(.title3)
.frame(maxWidth: 20)
.scaledToFill()
.minimumScaleFactor(0.5)
.lineLimit(1)
}
} }
} }
} else if let team { } else if let team {

@ -173,7 +173,7 @@ struct CalendarView: View {
newTournament.federalTournamentAge = build.age newTournament.federalTournamentAge = build.age
newTournament.dayDuration = federalTournament.dayDuration newTournament.dayDuration = federalTournament.dayDuration
newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9)
newTournament.setupFederalSettings() newTournament.setupFederalSettings(fromEvent: event)
do { do {
try dataStore.tournaments.addOrUpdate(instance: newTournament) try dataStore.tournaments.addOrUpdate(instance: newTournament)
} catch { } catch {

@ -39,7 +39,6 @@ struct EventListView: View {
Text("\(count.formatted()) tournoi" + count.pluralSuffix) Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} }
.id(sectionIndex)
.headerProminence(.increased) .headerProminence(.increased)
} }
} }
@ -119,10 +118,11 @@ struct EventListView: View {
private func _tournamentView(_ tournament: Tournament) -> some View { private func _tournamentView(_ tournament: Tournament) -> some View {
NavigationLink(value: tournament) { NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament)
TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver())
.popover(isPresented: self.$showUserSearch) { .popover(isPresented: self.$showUserSearch) {
ShareModelView(instance: tournament) ShareModelView(instance: tournament)
} }
} }
.contextMenu { .contextMenu {
if tournament.hasEnded() == false { if tournament.hasEnded() == false {

@ -14,16 +14,18 @@ struct TournamentLookUpView: View {
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@StateObject var locationManager = LocationManager() @StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@FocusState private var isFocused: Bool
@State private var searchField: String = "" @State private var searchField: String = ""
@State var page: Int = 0 @State var page: Int = 0
@State var total: Int = 0 @State var total: Int = 0
@State private var showingSettingsAlert = false
@State private var searching: Bool = false @State private var searching: Bool = false
@State private var requestedToGetAllPages: Bool = false @State private var requestedToGetAllPages: Bool = false
@State private var revealSearchParameters: Bool = true @State private var revealSearchParameters: Bool = true
@State private var presentAlert: Bool = false @State private var presentAlert: Bool = false
@State private var confirmSearch: Bool = false @State private var confirmSearch: Bool = false
@State private var locationRequested = false
var tournaments: [FederalTournament] { var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments federalDataViewModel.searchedFederalTournaments
@ -57,6 +59,16 @@ struct TournamentLookUpView: View {
} message: { } message: {
Text("Aucune ville n'a été indiqué, il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.") Text("Aucune ville n'a été indiqué, il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.")
} }
.alert(isPresented: $showingSettingsAlert) {
Alert(
title: Text("Réglages"),
message: Text("Pour trouver les clubs autour de vous, vous devez l'autorisation à Padel Club de récupérer votre position."),
primaryButton: .default(Text("Ouvrir les réglages"), action: {
_openSettings()
}),
secondaryButton: .cancel()
)
}
.alert("Attention", isPresented: $presentAlert, actions: { .alert("Attention", isPresented: $presentAlert, actions: {
Button { Button {
presentAlert = false presentAlert = false
@ -86,8 +98,9 @@ struct TournamentLookUpView: View {
.navigationTitle("Chercher un tournoi") .navigationTitle("Chercher un tournoi")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.onChange(of: locationManager.city) { .onChange(of: locationManager.city) {
if let newValue = locationManager.city, dataStore.appSettings.city.isEmpty { if locationRequested, let newValue = locationManager.city {
dataStore.appSettings.city = newValue dataStore.appSettings.city = newValue
locationRequested = false
} }
} }
.toolbarTitleDisplayMode(.large) .toolbarTitleDisplayMode(.large)
@ -299,15 +312,34 @@ struct TournamentLookUpView: View {
HStack { HStack {
TextField("Ville", text: $appSettings.city) TextField("Ville", text: $appSettings.city)
if let city = locationManager.city { .onSubmit(of: .text) {
Divider() locationManager.city = nil
Text(city).italic() locationManager.location = nil
} }
.focused($isFocused)
.onChange(of: isFocused) {
if isFocused {
appSettings.city = ""
}
}
// if let city = locationManager.city {
// Divider()
// Text(city).italic()
// }
if locationManager.requestStarted { if locationManager.requestStarted {
ProgressView() ProgressView()
} else { } else if locationManager.manager.authorizationStatus != .restricted {
LocationButton { LocationButton {
locationManager.requestLocation() if locationManager.manager.authorizationStatus == .notDetermined {
locationRequested = true
locationManager.manager.requestWhenInUseAuthorization()
} else if locationManager.manager.authorizationStatus == .denied {
showingSettingsAlert = true
} else {
locationRequested = true
locationManager.requestLocation()
}
} }
.symbolVariant(.fill) .symbolVariant(.fill)
.foregroundColor (Color.white) .foregroundColor (Color.white)
@ -485,4 +517,12 @@ struct TournamentLookUpView: View {
return "Distance" return "Distance"
} }
} }
private func _openSettings() {
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsURL)
}
} }

@ -185,11 +185,6 @@ struct MainView: View {
importObserver.checkingFilesAttempt += 1 importObserver.checkingFilesAttempt += 1
importObserver.checkingFiles = false importObserver.checkingFiles = false
if lastDataSource == nil || (dataStore.monthData.first(where: { $0.monthKey == "07-2024" }) == nil) {
// await _downloadPreviousDate()
await _importMandatoryData()
}
if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, mostRecentDateAvailable > SourceFileManager.shared.lastDataSourceDate() ?? .distantPast { if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, mostRecentDateAvailable > SourceFileManager.shared.lastDataSourceDate() ?? .distantPast {
print("importing \(mostRecentDateAvailable)") print("importing \(mostRecentDateAvailable)")
@ -222,17 +217,6 @@ struct MainView: View {
await SourceFileManager.shared.getAllFiles(initialDate: "05-2024") await SourceFileManager.shared.getAllFiles(initialDate: "05-2024")
} }
private func _importMandatoryData() async {
let mandatoryKey = "07-2024"
if dataStore.monthData.first(where: { $0.monthKey == mandatoryKey }) == nil, let importingDate = URL.importDateFormatter.date(from: mandatoryKey) {
print("importing mandatory july data")
dataStore.appSettings.lastDataSource = mandatoryKey
dataStore.appSettingsStorage.write()
await SourceFileManager.shared.getAllFiles(initialDate: "07-2024")
await _calculateMonthData(dataSource: mandatoryKey)
}
}
private func _checkingDataIntegrity() { private func _checkingDataIntegrity() {
guard importObserver.checkingFiles == false, importObserver.isImportingFile() == false else { guard importObserver.checkingFiles == false, importObserver.isImportingFile() == false else {
return return
@ -245,27 +229,21 @@ struct MainView: View {
Task { Task {
await self._checkSourceFileAvailability() await self._checkSourceFileAvailability()
} }
} else if let lastDataSource, let mostRecentDateImported = URL.importDateFormatter.date(from: lastDataSource), SourceFileManager.isDateAfterUrlImportDate(date:mostRecentDateImported, dateString: "07-2024") { } else if let lastDataSource, let mostRecentDateImported = URL.importDateFormatter.date(from: lastDataSource) {
let monthData = dataStore.monthData.sorted(by: \.creationDate) let monthData = dataStore.monthData.sorted(by: \.creationDate)
if monthData.first(where: { $0.monthKey == "07-2024" }) == nil { if let current = monthData.last {
Task { Task {
await _checkSourceFileAvailability() let updated = await SourceFileManager.shared.fetchData(fromDate: mostRecentDateImported)
} let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == mostRecentDateImported && $0.index == 0 })
} else { print("file updated", updated)
if let current = monthData.last { if let updated, updated == 1 {
Task { await _startImporting(importingDate: mostRecentDateImported)
let updated = await SourceFileManager.shared.fetchData(fromDate: mostRecentDateImported) } else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() {
let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == mostRecentDateImported && $0.index == 0 }) await _startImporting(importingDate: mostRecentDateImported)
print("file updated", updated) } else if updated == 0 {
if let updated, updated == 1 { await _calculateMonthData(dataSource: current.monthKey)
await _startImporting(importingDate: mostRecentDateImported)
} else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() {
await _startImporting(importingDate: mostRecentDateImported)
} else if updated == 0 {
await _calculateMonthData(dataSource: current.monthKey)
}
} }
} }
} }

@ -40,10 +40,10 @@ struct PadelClubView: View {
if let currentMonth = monthData.first, currentMonth.incompleteMode { if let currentMonth = monthData.first, currentMonth.incompleteMode {
Section { Section {
Text("Attention, depuis Août 2024, les données fédérales publiques des joueurs (messieurs) récupérables sont incomplètes car limité au 40.000 premiers joueurs.") Text("Attention, depuis Août 2024, les données fédérales publiques des joueurs (messieurs) récupérables sont incomplètes car limité au 80.000 premiers joueurs.")
if currentMonth.maleUnrankedValue == nil { if currentMonth.maleUnrankedValue == nil {
Text("Le rang d'un joueur non-classé n'est donc pas calculable pour le moment, Padel Club utilisera une valeur par défaut de de 70.000.") Text("Le rang d'un joueur non-classé n'est donc pas calculable pour le moment, Padel Club utilisera une valeur par défaut de de 90.000.")
} }
Text("Un classement souligné comme ci-dessous indiquera que l'information provient d'un mois précédent.") Text("Un classement souligné comme ci-dessous indiquera que l'information provient d'un mois précédent.")
@ -61,32 +61,22 @@ struct PadelClubView: View {
["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"] ["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"]
*/ */
Section {
RowButtonView("Retry Anonymous") {
await _retryAnonymous()
}
}
Section {
RowButtonView("Write anonymous") {
_writeAnonymous()
}
}
Section { Section {
RowButtonView("Exporter en csv") { RowButtonView("Exporter en csv") {
for fileURL in SourceFileManager.shared.jsonFiles() { await _exportCsv()
let decoder = JSONDecoder()
decoder.userInfo[.maleData] = fileURL.manData
do {
let data = try Data(contentsOf: fileURL)
let players = try decoder.decode([FederalPlayer].self, from: data)
var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false }
print("before anonymousPlayers.count", anonymousPlayers.count)
FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers)
print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
await fetchPlayersDataSequentially(for: &anonymousPlayers)
print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
.count)
SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
} catch {
Logger.error(error)
}
}
} }
} }
#endif #endif
@ -169,7 +159,7 @@ struct PadelClubView: View {
if let maleUnrankedValue = monthData.maleUnrankedValue { if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted()) Text(maleUnrankedValue.formatted())
} else { } else {
Text(70_000.formatted()) Text(90_000.formatted())
} }
} label: { } label: {
Text("Rang d'un non classé") Text("Rang d'un non classé")
@ -187,6 +177,11 @@ struct PadelClubView: View {
Text("Rang d'une non classée") Text("Rang d'une non classée")
Text("Dames") Text("Dames")
} }
#if DEBUG
RowButtonView("recalc") {
await _calculateLastRank(dataSource: monthData.monthKey)
}
#endif
} header: { } header: {
HStack { HStack {
Text(monthData.monthKey) Text(monthData.monthKey)
@ -242,15 +237,110 @@ struct PadelClubView: View {
await SourceFileManager.shared.getAllFiles(initialDate: "08-2022") await SourceFileManager.shared.getAllFiles(initialDate: "08-2022")
self.uuid = UUID() self.uuid = UUID()
} }
#if DEBUG
private func _calculateMonthData(dataSource: String?) async {
if let dataSource, let mostRecentDate = URL.importDateFormatter.date(from: dataSource) {
await MonthData.calculateCurrentUnrankedValues(fromDate: mostRecentDate)
}
}
private func _calculateLastRank(dataSource: String) async {
await _calculateMonthData(dataSource: dataSource)
}
private func _writeAnonymous() {
for fileURL in SourceFileManager.shared.anonymousFiles() {
let lastDateString = URL.importDateFormatter.string(from: fileURL.dateFromPath)
let sourceType = fileURL.manData ? SourceFile.messieurs : SourceFile.dames
let dateString = ["CLASSEMENT-PADEL", sourceType.rawValue, lastDateString].filter({ $0.isEmpty == false }).joined(separator: "-") + "." + "csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("rankings").appendingPathComponent("\(dateString)")
updateCSVFile(sourceCSVURL: destinationFileUrl, updatedCSVURL: fileURL)
}
}
private func _retryAnonymous() async {
for fileURL in SourceFileManager.shared.anonymousFiles() {
let players = FileImportManager.shared.readCSV(inputFile: fileURL)
var anonymousPlayers = players
print("before anonymousPlayers.count", anonymousPlayers.count)
await fetchPlayersDataSequentially(for: &anonymousPlayers)
print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
.count)
SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
}
}
private func _exportCsv() async {
for fileURL in SourceFileManager.shared.jsonFiles() {
let decoder = JSONDecoder()
decoder.userInfo[.maleData] = fileURL.manData
do {
let data = try Data(contentsOf: fileURL)
let players = try decoder.decode([FederalPlayer].self, from: data)
var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false }
print("before anonymousPlayers.count", anonymousPlayers.count)
FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers)
print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
await fetchPlayersDataSequentially(for: &anonymousPlayers)
print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
.count)
SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
} catch {
Logger.error(error)
}
}
}
#endif
} }
//#Preview { func updateCSVFile(sourceCSVURL: URL, updatedCSVURL: URL) {
// PadelClubView() do {
//} let sourceCSVContent = try String(contentsOf: sourceCSVURL, encoding: .utf8)
var sourceCSVLines = sourceCSVContent.components(separatedBy: "\n")
let delimiter = ";"
let updatedCSVContent = try String(contentsOf: updatedCSVURL, encoding: .utf8)
let updatedCSVLines = updatedCSVContent.components(separatedBy: "\n")
// Create a dictionary of updated player data by licenseId
var updatedPlayerDict: [String: String] = [:]
for line in updatedCSVLines {
let components = line.components(separatedBy: delimiter)
if let licenseId = components.dropFirst(5).first {
updatedPlayerDict[licenseId] = line
}
}
// Update the source CSV lines if licenseId matches
for (index, line) in sourceCSVLines.enumerated() {
let components = line.components(separatedBy: delimiter)
if let licenseId = components.dropFirst(5).first, let updatedLine = updatedPlayerDict[licenseId] {
sourceCSVLines[index] = updatedLine
}
}
// Write back to the file
let finalCSVContent = sourceCSVLines.joined(separator: "\n")
try finalCSVContent.write(to: sourceCSVURL, atomically: true, encoding: .utf8)
print("CSV file updated successfully.")
} catch {
print("Error updating CSV file: \(error)")
}
}
// Function to fetch data for a single license ID // Function to fetch data for a single license ID
func fetchPlayerData(for licenseID: String) async throws -> [Player]? { func fetchPlayerData(for licenseID: String, idHomologation: String, sessionId: String) async throws -> [Player]? {
guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=82477107&numeroLicence=\(licenseID)") else { guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=\(idHomologation)&numeroLicence=\(licenseID)") else {
throw URLError(.badURL) throw URLError(.badURL)
} }
@ -268,7 +358,7 @@ func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With") request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
// Add cookies if needed (example cookie header value shown, replace with valid cookies) // Add cookies if needed (example cookie header value shown, replace with valid cookies)
request.setValue("JSESSIONID=F4ED2A1BCF3CD2694FE0B111B8027999; AWSALB=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; AWSALBCORS=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; datadome=KlbIdnrCgaY1zLVIZ5CfLJm~KXv9_YnXGhaQdqMEn6Ja9R6imBH~vhzmyuiLxGi1D0z90v5x2EiGDvQ7zsw~fajWLbOupFEajulc86PSJ7RIHpOiduCQ~cNoITQYJOXa; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQLNQOPLOSLJOZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQKSMOZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOQMSLNZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQNSJMZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOSJMLJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLRPQMQQNRQRZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLRPQNKSLOMSZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLSNSOPMSOPJZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMJQSRLJSOOJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMJRJPJMSSKRZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; tCdebugLib=1; incap_ses_2222_2712217=ui9wOOAjNziUTlU3gCHWHtv/KWcAAAAAhSzbpyITRp7YwRT3vJB2vg==; incap_ses_2224_2712217=NepDAr2kUDShMiCJaDzdHqbjKWcAAAAA0kLlk3lgvGnwWSTMceZoEw==; xtan=-; xtant=1; incap_ses_1350_2712217=g+XhSJRwOS8JlWTYCSq8EtOBJGcAAAAAffg2IobkPUW2BtvgJGHbMw==; TCSESSION=124101910177775608913; nlbi_2712217=jnhtOC5KDiLvfpy/b9lUTgAAAAA7zduh8JyZOVrEfGsEdFlq; TCID=12481811494814553052; xtvrn=$548419$; TCPID=12471746148351334672; visid_incap_2712217=PSfJngzoSuiowsuXXhvOu5K+7mUAAAAAQUIPAAAAAAAleL9ldvN/FC1VykkU9ret; SessionStatId=10.91.140.42.1662124965429001", forHTTPHeaderField: "Cookie") request.setValue(sessionId, forHTTPHeaderField: "Cookie")
let (data, _) = try await URLSession.shared.data(for: request) let (data, _) = try await URLSession.shared.data(for: request)
let decoder = JSONDecoder() let decoder = JSONDecoder()
@ -288,9 +378,21 @@ func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
// Function to fetch data for multiple license IDs using TaskGroup // Function to fetch data for multiple license IDs using TaskGroup
func fetchPlayersDataSequentially(for licenseIDs: inout [FederalPlayer]) async { func fetchPlayersDataSequentially(for licenseIDs: inout [FederalPlayer]) async {
var idHomologation: String = "82469282"
if let _idHomologation = PListReader.readString(plist: "local", key: "idHomologation") {
idHomologation = _idHomologation
}
var sessionId: String = ""
if let _sessionId = PListReader.readString(plist: "local", key: "JSESSIONID") {
sessionId = _sessionId
}
for licenseID in licenseIDs.filter({ $0.firstName.isEmpty && $0.lastName.isEmpty }) { for licenseID in licenseIDs.filter({ $0.firstName.isEmpty && $0.lastName.isEmpty }) {
do { do {
if let playerData = try await fetchPlayerData(for: licenseID.license)?.first { if let playerData = try await fetchPlayerData(for: licenseID.license, idHomologation: idHomologation, sessionId: sessionId)?.first {
licenseID.lastName = playerData.nom licenseID.lastName = playerData.nom
licenseID.firstName = playerData.prenom licenseID.firstName = playerData.prenom
} }

@ -251,7 +251,13 @@ struct PlayerPopoverView: View {
} }
func createManualPlayer() { func createManualPlayer() {
let playerRegistration = PlayerRegistration(firstName: firstName.trimmedMultiline, lastName: lastName.trimmedMultiline, licenceId: license.trimmedMultiline.isEmpty ? nil : license, rank: rank, sex: PlayerSexType(rawValue: sex))
let playerRegistration = PlayerRegistration(
firstName: firstName.trimmedMultiline,
lastName: lastName.trimmedMultiline,
licenceId: license.trimmedMultiline.isEmpty ? nil : license,
rank: rank,
sex: PlayerSexType(rawValue: sex))
self.creationCompletionHandler(playerRegistration) self.creationCompletionHandler(playerRegistration)
} }

@ -32,9 +32,29 @@ struct PlayerDetailView: View {
var body: some View { var body: some View {
Form { Form {
Section {
Text(player.localizedSourceLabel().firstCapitalized)
} header: {
Text("Source des informations")
}
if tournament.enableOnlineRegistration {
Section {
LabeledContent {
Text(player.registeredOnline ? "Oui" : "Non")
} label: {
Text("Inscription en ligne")
}
}
}
Section { Section {
Toggle("Joueur sur place", isOn: $player.hasArrived) Toggle("Joueur sur place", isOn: $player.hasArrived)
Toggle("Capitaine", isOn: $player.captain)
Toggle("Coach", isOn: $player.coach)
}
Section {
LabeledContent { LabeledContent {
TextField("Nom", text: $player.lastName) TextField("Nom", text: $player.lastName)
.keyboardType(.alphabet) .keyboardType(.alphabet)
@ -208,7 +228,7 @@ struct PlayerDetailView: View {
} }
} }
} }
.onChange(of: player.hasArrived) { .onChange(of: [player.hasArrived, player.captain, player.coach]) {
_save() _save()
} }
.onChange(of: player.sex) { .onChange(of: player.sex) {

@ -0,0 +1,33 @@
//
// DateMenuView.swift
// PadelClub
//
// Created by razmig on 22/11/2024.
//
import SwiftUI
struct DateMenuView: View {
@Binding var date: Date
private func adjustDate(byDays days: Int) {
let calendar = Calendar.current
if let newDate = calendar.date(byAdding: .day, value: days, to: date) {
date = newDate.truncateMinutesAndSeconds()
}
}
var body: some View {
Menu {
Button("24h avant") { adjustDate(byDays: -1) }
Button("48h avant") { adjustDate(byDays: -2) }
Divider()
Button("24h après") { adjustDate(byDays: 1) }
Button("48h après") { adjustDate(byDays: 2) }
} label: {
Text("Ajuster")
.underline()
}
}
}

@ -114,5 +114,8 @@ struct ImportedPlayerView: View {
} }
} }
} }
.contextMenu {
CopyPasteButtonView(title: "Licence", pasteValue: player.formattedLicense())
}
} }
} }

@ -379,102 +379,7 @@ struct MySearchView: View {
let array = Array(searchViewModel.selectedPlayers) let array = Array(searchViewModel.selectedPlayers)
Section { Section {
ForEach(array) { player in ForEach(array) { player in
let index : Int? = nil ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
.truncationMode(.tail)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
.onDelete { indexSet in .onDelete { indexSet in
for index in indexSet { for index in indexSet {
@ -489,103 +394,7 @@ struct MySearchView: View {
} else { } else {
Section { Section {
ForEach(players, id: \.self) { player in ForEach(players, id: \.self) { player in
let index : Int? = nil ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
.truncationMode(.tail)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
} header: { } header: {
if players.isEmpty == false { if players.isEmpty == false {
@ -601,106 +410,10 @@ struct MySearchView: View {
if searchViewModel.allowSingleSelection { if searchViewModel.allowSingleSelection {
Section { Section {
ForEach(players) { player in ForEach(players) { player in
let index : Int? = nil
Button { Button {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
VStack(alignment: .leading) { ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
.truncationMode(.tail)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -712,105 +425,9 @@ struct MySearchView: View {
.id(UUID()) .id(UUID())
} else { } else {
Section { Section {
ForEach(players.indices, id: \.self) { playerIndex in ForEach(players.indices, id: \.self) { index in
let player = players[playerIndex] let player = players[index]
let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
.truncationMode(.tail)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
} header: { } header: {
if players.isEmpty == false { if players.isEmpty == false {
@ -821,207 +438,19 @@ struct MySearchView: View {
} }
} else { } else {
Section { Section {
ForEach(players.indices, id: \.self) { playerIndex in ForEach(players.indices, id: \.self) { index in
let player = players[playerIndex] let player = players[index]
let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil
if searchViewModel.allowSingleSelection { if searchViewModel.allowSingleSelection {
Button { Button {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
VStack(alignment: .leading) { ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
HStack { .contentShape(Rectangle())
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
.truncationMode(.tail)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
} else { } else {
VStack(alignment: .leading) { ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
.truncationMode(.tail)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
} }
} header: { } header: {

@ -22,7 +22,10 @@ struct EditingTeamView: View {
@State private var registrationDate : Date @State private var registrationDate : Date
@State private var name: String @State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys? @FocusState private var focusedField: TeamRegistration.CodingKeys?
@State private var presentOnlineRegistrationWarning: Bool = false
@State private var currentWaitingList: TeamRegistration?
@State private var presentTeamToWarn: Bool = false
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
sentError != nil sentError != nil
@ -43,14 +46,35 @@ struct EditingTeamView: View {
_registrationDate = State(wrappedValue: team.registrationDate ?? Date()) _registrationDate = State(wrappedValue: team.registrationDate ?? Date())
} }
private func _resetTeam() {
self.currentWaitingList = tournament.waitingListSortedTeams().filter({ $0.hasRegisteredOnline() }).first
team.resetPositions()
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = false
}
private func _checkOnlineRegistrationWarning() {
guard let currentWaitingList else { return }
let selectedSortedTeams = tournament.selectedSortedTeams().map({ $0.id })
if selectedSortedTeams.contains(currentWaitingList.id) {
presentOnlineRegistrationWarning = true
}
}
var body: some View { var body: some View {
List { List {
Section { Section {
RowButtonView("Modifier la composition de l'équipe") { RowButtonView("Modifier la composition de l'équipe", role: team.hasRegisteredOnline() ? .destructive : .none, confirmationMessage: "Vous êtes sur le point de modifier une équipe qui s'est inscrite en ligne.") {
editedTeam = team editedTeam = team
} }
TeamDetailView(team: team) TeamDetailView(team: team)
} header: {
if team.hasRegisteredOnline() {
Text("Inscription en ligne")
} else {
Text("Inscription par vous-même")
}
} footer: { } footer: {
HStack { HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData()) CopyPasteButtonView(pasteValue: team.playersPasteData())
@ -64,6 +88,7 @@ struct EditingTeamView: View {
} }
} }
} }
.headerProminence(.increased)
Section { Section {
DatePicker(selection: $registrationDate) { DatePicker(selection: $registrationDate) {
@ -95,15 +120,10 @@ struct EditingTeamView: View {
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
return team.wildCardBracket return team.wildCardBracket
}, set: { value in }, set: { value in
team.resetPositions() _resetTeam()
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = value team.wildCardBracket = value
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team) _save()
} catch {
Logger.error(error)
}
})) { })) {
Text("Wildcard Tableau") Text("Wildcard Tableau")
} }
@ -111,15 +131,9 @@ struct EditingTeamView: View {
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
return team.wildCardGroupStage return team.wildCardGroupStage
}, set: { value in }, set: { value in
team.resetPositions() _resetTeam()
team.wildCardBracket = false
team.walkOut = false
team.wildCardGroupStage = value team.wildCardGroupStage = value
do { _save()
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
})) { })) {
Text("Wildcard Poule") Text("Wildcard Poule")
} }
@ -127,15 +141,10 @@ struct EditingTeamView: View {
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
return team.walkOut return team.walkOut
}, set: { value in }, set: { value in
team.resetPositions() _resetTeam()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = value team.walkOut = value
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team) _save()
} catch {
Logger.error(error)
}
})) { })) {
Text("Forfait") Text("Forfait")
} }
@ -193,6 +202,7 @@ struct EditingTeamView: View {
Section { Section {
RowButtonView("Effacer l'équipe", role: .destructive, systemImage: "trash") { RowButtonView("Effacer l'équipe", role: .destructive, systemImage: "trash") {
_resetTeam()
team.deleteTeamScores() team.deleteTeamScores()
do { do {
try tournamentStore?.teamRegistrations.delete(instance: team) try tournamentStore?.teamRegistrations.delete(instance: team)
@ -201,8 +211,37 @@ struct EditingTeamView: View {
} }
dismiss() dismiss()
} }
} footer: {
if team.hasRegisteredOnline() {
Text("Attention, supprimer cette équipe notifiera par email que leur inscription a été annulée.").foregroundStyle(.logoRed)
}
} }
} }
.sheet(isPresented: $presentTeamToWarn) {
if let currentWaitingList {
NavigationStack {
EditingTeamView(team: currentWaitingList)
}
.tint(.master)
}
}
.alert("Attention", isPresented: $presentOnlineRegistrationWarning, actions: {
if currentWaitingList != nil {
Button("Voir l'équipe") {
self.presentTeamToWarn = true
}
Button("OK") {
self.currentWaitingList = nil
self.presentOnlineRegistrationWarning = false
}
}
}, message: {
if let currentWaitingList {
Text("L'équipe \(currentWaitingList.teamLabel(separator: "/")), inscrite en ligne, rentre dans votre sélection suite à la modification que vous venez de faire, voulez-vous les prévenir ?")
}
})
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .toolbar(content: {
if focusedField != nil { if focusedField != nil {
@ -312,7 +351,7 @@ struct EditingTeamView: View {
private var hasArrived: Binding<Bool> { private var hasArrived: Binding<Bool> {
Binding { Binding {
team.unsortedPlayers().allSatisfy({ $0.hasArrived }) team.isHere()
} set: { hasArrived in } set: { hasArrived in
team.unsortedPlayers().forEach { team.unsortedPlayers().forEach {
$0.hasArrived = hasArrived $0.hasArrived = hasArrived
@ -323,14 +362,16 @@ struct EditingTeamView: View {
Logger.error(error) Logger.error(error)
} }
} }
} }
private func _save() { private func _save() {
do { do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team) try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
_checkOnlineRegistrationWarning()
} }
private var _networkErrorMessage: String { private var _networkErrorMessage: String {

@ -22,7 +22,17 @@ struct TeamDetailView: View {
PlayerDetailView(player: player) PlayerDetailView(player: player)
.environment(tournament) .environment(tournament)
} label: { } label: {
PlayerView(player: player) VStack(alignment: .leading, spacing: 0) {
HStack {
if player.registeredOnline {
Text("inscrit en ligne")
}
Spacer()
Text(player.localizedSourceLabel())
}
.font(.caption).foregroundStyle(.secondary)
PlayerView(player: player)
}
} }
} }
} }

@ -118,7 +118,13 @@ struct TeamRowView: View {
var body: some View { var body: some View {
ForEach(team.players()) { player in ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) HStack(spacing: 4) {
if player.registeredOnline {
Image(systemName: "circle.fill").foregroundStyle(.green)
.font(.system(size: 8))
}
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
} }
} }
} }

@ -140,7 +140,7 @@ struct FileImportView: View {
} }
} }
if tournament.unsortedTeams().count > 0 { if tournament.unsortedTeams().count > 0, tournament.enableOnlineRegistration == false {
RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) { RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) {
await _deleteTeams() await _deleteTeams()
} }

@ -16,7 +16,7 @@ struct AddTeamView: View {
private var fetchRequest: FetchRequest<ImportedPlayer> private var fetchRequest: FetchRequest<ImportedPlayer>
private var fetchPlayers: FetchedResults<ImportedPlayer> { fetchRequest.wrappedValue } private var fetchPlayers: FetchedResults<ImportedPlayer> { fetchRequest.wrappedValue }
var tournament: Tournament let tournament: Tournament
var cancelShouldDismiss: Bool = false var cancelShouldDismiss: Bool = false
enum FocusField: Hashable { enum FocusField: Hashable {
case pasteField case pasteField

@ -19,7 +19,7 @@ struct BroadcastView: View {
let filter = CIFilter.qrCodeGenerator() let filter = CIFilter.qrCodeGenerator()
@State private var urlToShow: String? @State private var urlToShow: String?
@State private var tvMode: Bool = false @State private var tvMode: Bool = false
@State private var pageLink: PageLink = .matches @State private var pageLink: PageLink = .info
let createAccountTip = CreateAccountTip() let createAccountTip = CreateAccountTip()
let tournamentPublishingTip = TournamentPublishingTip() let tournamentPublishingTip = TournamentPublishingTip()
@ -269,7 +269,7 @@ struct BroadcastView: View {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {
Section { Section {
let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings, .broadcast, .clubBroadcast] let links : [PageLink] = [.info, .teams, .summons, .groupStages, .matches, .rankings, .broadcast, .clubBroadcast]
Picker(selection: $pageLink) { Picker(selection: $pageLink) {
ForEach(links) { pageLink in ForEach(links) { pageLink in
Text(pageLink.localizedLabel()).tag(pageLink) Text(pageLink.localizedLabel()).tag(pageLink)

@ -81,6 +81,7 @@ struct InscriptionInfoView: View {
DisclosureGroup { DisclosureGroup {
ForEach(callDateIssue) { team in ForEach(callDateIssue) { team in
TeamCallView(team: team) TeamCallView(team: team)
.environment(tournament)
} }
} label: { } label: {
LabeledContent { LabeledContent {

@ -0,0 +1,48 @@
//
// TournamentCategorySettingsView.swift
// PadelClub
//
// Created by razmig on 18/12/2024.
//
import SwiftUI
import LeStorage
struct TournamentCategorySettingsView: View {
@Environment(Tournament.self) private var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var body: some View {
List {
TournamentLevelPickerView()
}
.onChange(of: [
tournament.federalCategory,
]) {
_save()
}
.onChange(of: [
tournament.federalLevelCategory,
]) {
_save()
}
.onChange(of: [
tournament.federalAgeCategory,
]) {
_save()
}
}
private func _save() {
do {
if tournament.onlineRegistrationCanBeEnabled() == false, tournament.enableOnlineRegistration {
tournament.enableOnlineRegistration = false
}
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}

@ -13,6 +13,7 @@ struct TournamentGeneralSettingsView: View {
@Bindable var tournament: Tournament @Bindable var tournament: Tournament
@State private var tournamentName: String = "" @State private var tournamentName: String = ""
@State private var tournamentInformation: String = ""
@State private var entryFee: Double? = nil @State private var entryFee: Double? = nil
@State private var confirmationRequired: Bool = false @State private var confirmationRequired: Bool = false
@State private var presentConfirmation: Bool = false @State private var presentConfirmation: Bool = false
@ -24,23 +25,13 @@ struct TournamentGeneralSettingsView: View {
self.tournament = tournament self.tournament = tournament
_loserBracketMode = .init(wrappedValue: tournament.loserBracketMode) _loserBracketMode = .init(wrappedValue: tournament.loserBracketMode)
_tournamentName = State(wrappedValue: tournament.name ?? "") _tournamentName = State(wrappedValue: tournament.name ?? "")
_tournamentInformation = State(wrappedValue: tournament.information ?? "")
_entryFee = State(wrappedValue: tournament.entryFee) _entryFee = State(wrappedValue: tournament.entryFee)
} }
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
Form { Form {
Section {
TextField("Nom du tournoi", text: $tournamentName, axis: .vertical)
.lineLimit(2)
.frame(maxWidth: .infinity)
.keyboardType(.alphabet)
.focused($focusedField, equals: ._name)
} header: {
Text("Nom du tournoi")
}
Section { Section {
TournamentDatePickerView() TournamentDatePickerView()
TournamentDurationManagerView() TournamentDurationManagerView()
@ -57,8 +48,61 @@ struct TournamentGeneralSettingsView: View {
Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.")
} }
if tournament.onlineRegistrationCanBeEnabled() {
Section {
NavigationLink {
RegistrationSetupView(tournament: tournament)
} label: {
LabeledContent {
if tournament.enableOnlineRegistration {
Text("activée").foregroundStyle(.green)
.font(.headline)
} else {
Text("désactivée").foregroundStyle(.logoRed)
.font(.headline)
}
} label: {
Text("Accéder aux paramètres")
Text(tournament.getOnlineRegistrationStatus().statusLocalized())
}
}
} header: {
Text("Inscription en ligne")
} footer: {
Text("Paramétrez les possibilités d'inscription en ligne à votre tournoi via Padel Club")
}
}
Section {
TextField("Nom du tournoi", text: $tournamentName, axis: .vertical)
.lineLimit(2)
.frame(maxWidth: .infinity)
.keyboardType(.alphabet)
.focused($focusedField, equals: ._name)
} header: {
Text("Nom du tournoi")
}
Section { Section {
TournamentLevelPickerView() ZStack {
Text(tournamentInformation).opacity(0)
Text(ContactType.defaultCustomMessage).opacity(0)
TextEditor(text: $tournamentInformation)
.keyboardType(.alphabet)
.focused($focusedField, equals: ._information)
}
.frame(maxHeight: 200)
.overlay {
if tournamentInformation.isEmpty {
Text("Texte visible dans l'onglet informations sur Padel Club.").italic()
}
}
} header: {
Text("Description du tournoi")
} footer: {
FooterButtonView("Ajouter le prix de l'inscription") {
tournamentInformation.append("\n" + tournament.entryFeeMessage)
}
} }
Section { Section {
@ -125,7 +169,7 @@ struct TournamentGeneralSettingsView: View {
if focusedField == ._entryFee { if focusedField == ._entryFee {
if tournament.isFree() { if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) { Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) {
entryFee = priceTag entryFee = priceTag
tournament.entryFee = priceTag tournament.entryFee = priceTag
focusedField = nil focusedField = nil
@ -145,12 +189,19 @@ struct TournamentGeneralSettingsView: View {
Spacer() Spacer()
Button("Valider") { Button("Valider") {
if focusedField == ._name { if focusedField == ._name {
let tournamentName = tournamentName.prefixTrimmed(200) let tournamentName = tournamentName.prefixMultilineTrimmed(200)
if tournamentName.isEmpty { if tournamentName.isEmpty {
tournament.name = nil tournament.name = nil
} else { } else {
tournament.name = tournamentName tournament.name = tournamentName
} }
} else if focusedField == ._information {
let tournamentInformation = tournamentInformation.prefixMultilineTrimmed(4000)
if tournamentInformation.isEmpty {
tournament.information = nil
} else {
tournament.information = tournamentInformation
}
} else if focusedField == ._entryFee { } else if focusedField == ._entryFee {
tournament.entryFee = entryFee tournament.entryFee = entryFee
} }
@ -167,27 +218,12 @@ struct TournamentGeneralSettingsView: View {
.onChange(of: tournament.entryFee) { .onChange(of: tournament.entryFee) {
_save() _save()
} }
.onChange(of: tournament.name) { .onChange(of: [tournament.name, tournament.information]) {
_save() _save()
} }
.onChange(of: tournament.dayDuration) { .onChange(of: tournament.dayDuration) {
_save() _save()
} }
.onChange(of: [
tournament.federalCategory,
]) {
_save()
}
.onChange(of: [
tournament.federalLevelCategory,
]) {
_save()
}
.onChange(of: [
tournament.federalAgeCategory,
]) {
_save()
}
.onChange(of: [ .onChange(of: [
tournament.groupStageSortMode, tournament.groupStageSortMode,
]) { ]) {

@ -48,6 +48,11 @@ struct TournamentStatusView: View {
do { do {
let event = tournament.eventObject() let event = tournament.eventObject()
let isLastTournament = event?.tournaments.count == 1 let isLastTournament = event?.tournaments.count == 1
tournament.isDeleted = true
try dataStore.tournaments.addOrUpdate(instance: tournament)
if let event, isLastTournament { if let event, isLastTournament {
try dataStore.events.delete(instance: event) try dataStore.events.delete(instance: event)
} else { } else {

@ -43,30 +43,30 @@ struct UpdateSourceRankDateView: View {
Task { Task {
do { do {
try await tournament.updateRank(to: currentRankSourceDate) try await tournament.updateRank(to: currentRankSourceDate)
try await MainActor.run {
tournament.unsortedPlayers().forEach { player in
player.setComputedRank(in: tournament)
}
try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers())
tournament.unsortedTeams().forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory)
if forceRefreshLockWeight {
team.lockedWeight = team.weight
}
}
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
try dataStore.tournaments.addOrUpdate(instance: tournament) let unsortedPlayers = tournament.unsortedPlayers()
tournament.unsortedPlayers().forEach { player in
updatingRank = false player.setComputedRank(in: tournament)
confirmUpdateRank = false
} }
try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
let unsortedTeams = tournament.unsortedTeams()
unsortedTeams.forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory)
if forceRefreshLockWeight {
team.lockedWeight = team.weight
}
}
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
updatingRank = false
confirmUpdateRank = false
} }
}.disabled(updatingRank) }.disabled(updatingRank)

@ -23,7 +23,8 @@ let teamsExportTip = TeamsExportTip()
struct InscriptionManagerView: View { struct InscriptionManagerView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Bindable var tournament: Tournament @Bindable var tournament: Tournament
@ -49,11 +50,15 @@ struct InscriptionManagerView: View {
@State private var compactMode: Bool = true @State private var compactMode: Bool = true
@State private var pasteString: String? @State private var pasteString: String?
@State private var registrationIssues: Int? = nil @State private var registrationIssues: Int? = nil
@State private var refreshResult: String? = nil
@State private var refreshInProgress: Bool = false
@State private var refreshStatus: Bool?
@State private var showLegendView: Bool = false
var tournamentStore: TournamentStore? { var tournamentStore: TournamentStore? {
return self.tournament.tournamentStore return self.tournament.tournamentStore
} }
enum SortingMode: Int, Identifiable, CaseIterable { enum SortingMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
case registrationDate case registrationDate
@ -72,6 +77,8 @@ struct InscriptionManagerView: View {
enum FilterMode: Int, Identifiable, CaseIterable { enum FilterMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
case all case all
case registeredLocally
case registeredOnline
case walkOut case walkOut
case waiting case waiting
case bracket case bracket
@ -88,6 +95,10 @@ struct InscriptionManagerView: View {
return "Vous n'avez aucune wildcard en poule." return "Vous n'avez aucune wildcard en poule."
case .all: case .all:
return "Vous n'avez encore aucune équipe inscrite." return "Vous n'avez encore aucune équipe inscrite."
case .registeredOnline:
return "Aucune équipe inscrite en ligne."
case .registeredLocally:
return "Aucune équipe inscrite par vous-même."
case .walkOut: case .walkOut:
return "Vous n'avez aucune équipe forfait." return "Vous n'avez aucune équipe forfait."
case .waiting: case .waiting:
@ -109,6 +120,10 @@ struct InscriptionManagerView: View {
return "Aucune wildcard en poule" return "Aucune wildcard en poule"
case .all: case .all:
return "Aucune équipe inscrite" return "Aucune équipe inscrite"
case .registeredLocally:
return "Aucune équipe inscrite par vous-même"
case .registeredOnline:
return "Aucune équipe inscrite en ligne"
case .walkOut: case .walkOut:
return "Aucune équipe forfait" return "Aucune équipe forfait"
case .waiting: case .waiting:
@ -130,6 +145,10 @@ struct InscriptionManagerView: View {
return displayStyle == .wide ? "Wildcard Poule" : "wc poule" return displayStyle == .wide ? "Wildcard Poule" : "wc poule"
case .all: case .all:
return displayStyle == .wide ? "Équipes inscrites" : "inscris" return displayStyle == .wide ? "Équipes inscrites" : "inscris"
case .registeredLocally:
return displayStyle == .wide ? "Inscrites par vous-même" : "par vous-même"
case .registeredOnline:
return displayStyle == .wide ? "Inscrites en ligne" : "en ligne"
case .bracket: case .bracket:
return displayStyle == .wide ? "En Tableau" : "tableau" return displayStyle == .wide ? "En Tableau" : "tableau"
case .groupStage: case .groupStage:
@ -144,6 +163,7 @@ struct InscriptionManagerView: View {
} }
} }
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate)
@ -225,10 +245,23 @@ struct InscriptionManagerView: View {
RowButtonView("Importer un fichier") { RowButtonView("Importer un fichier") {
presentImportView = true presentImportView = true
} }
if tournament.enableOnlineRegistration {
RowButtonView("Rafraîchir la liste", cornerRadius: 20) {
await _refreshList()
}
} else if tournament.onlineRegistrationCanBeEnabled() {
RowButtonView("Inscription en ligne") {
navigation.path.append(Screen.settings)
}
}
} }
} }
} }
} }
.refreshable {
await _refreshList()
}
.onAppear { .onAppear {
_setHash() _setHash()
} }
@ -339,6 +372,7 @@ struct InscriptionManagerView: View {
} }
} }
if tournament.isAnimation() == false { if tournament.isAnimation() == false {
if tournament.inscriptionClosed() == false { if tournament.inscriptionClosed() == false {
Divider() Divider()
@ -346,11 +380,13 @@ struct InscriptionManagerView: View {
Section { Section {
Button("+1 en tableau") { Button("+1 en tableau") {
tournament.addWildCard(1, .bracket) tournament.addWildCard(1, .bracket)
_setHash()
} }
if tournament.groupStageCount > 0 { if tournament.groupStageCount > 0 {
Button("+1 en poules") { Button("+1 en poules") {
tournament.addWildCard(1, .groupStage) tournament.addWildCard(1, .groupStage)
_setHash()
} }
} }
} header: { } header: {
@ -359,6 +395,7 @@ struct InscriptionManagerView: View {
Button("Bloquer une place") { Button("Bloquer une place") {
tournament.addEmptyTeamRegistration(1) tournament.addEmptyTeamRegistration(1)
_setHash()
} }
Divider() Divider()
@ -477,6 +514,10 @@ struct InscriptionManagerView: View {
teams = teams.filter({ $0.inGroupStage() }) teams = teams.filter({ $0.inGroupStage() })
case .notImported: case .notImported:
teams = teams.filter({ $0.isImported() == false }) teams = teams.filter({ $0.isImported() == false })
case .registeredLocally:
teams = teams.filter({ $0.hasRegisteredOnline() == false })
case .registeredOnline:
teams = teams.filter({ $0.hasRegisteredOnline() == true })
default: default:
break break
} }
@ -492,6 +533,45 @@ struct InscriptionManagerView: View {
} }
} }
// private func _fixModel() {
// let players = tournament.players()
//
// players.forEach { player in
// if player.source == .onlineRegistration {
// player.source = .frenchFederation
// player.registeredOnline = true
// }
// }
//
// try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// }
//
private func _refreshList() async {
if refreshInProgress { return }
refreshResult = nil
refreshStatus = nil
refreshInProgress = true
do {
try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
_setHash()
self.refreshResult = "la synchronization a réussi"
self.refreshStatus = true
refreshInProgress = false
} catch {
Logger.error(error)
self.refreshResult = "la synchronization a échoué"
self.refreshStatus = false
refreshInProgress = false
}
}
private func _teamRegisteredView() -> some View { private func _teamRegisteredView() -> some View {
List { List {
let selectedSortedTeams = tournament.selectedSortedTeams() let selectedSortedTeams = tournament.selectedSortedTeams()
@ -529,11 +609,15 @@ struct InscriptionManagerView: View {
} }
} }
let isImported = teams.anySatisfy({ $0.isImported() })
if teams.isEmpty == false { if teams.isEmpty == false {
if compactMode { if compactMode {
Section { Section {
ForEach(teams) { team in ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams) let teamIndex = team.index(in: sortedTeams)
let color: Color? = isImported ? (team.unrankedOrUnknown() ? .logoRed : (team.isImported() == false ? .beige : nil)) : nil
NavigationLink { NavigationLink {
EditingTeamView(team: team) EditingTeamView(team: team)
.environment(tournament) .environment(tournament)
@ -541,9 +625,11 @@ struct InscriptionManagerView: View {
TeamRowView(team: team) TeamRowView(team: team)
} }
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
_teamDeleteButtonView(team) if tournament.enableOnlineRegistration == false {
_teamDeleteButtonView(team)
}
} }
.listRowView(isActive: true, color: team.initialRoundColor() ?? tournament.cutLabelColor(index: teamIndex, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count), hideColorVariation: true) .listRowView(isActive: true, color: team.initialRoundColor() ?? tournament.cutLabelColor(index: teamIndex, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count), hideColorVariation: true, backgroundColor: color, alignment: .leading)
} }
} header: { } header: {
if filterMode == .all && walkoutTeams.isEmpty == false { if filterMode == .all && walkoutTeams.isEmpty == false {
@ -551,6 +637,10 @@ struct InscriptionManagerView: View {
} else { } else {
Text("\(teams.count.formatted()) équipe\(teams.count.pluralSuffix)") Text("\(teams.count.formatted()) équipe\(teams.count.pluralSuffix)")
} }
} footer: {
FooterButtonView("Légende des codes couleurs") {
showLegendView = true
}
} }
.headerProminence(.increased) .headerProminence(.increased)
} else { } else {
@ -578,9 +668,13 @@ struct InscriptionManagerView: View {
} }
} }
} }
.id(refreshStatus)
.searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites")) .searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites"))
.keyboardType(.alphabet) .keyboardType(.alphabet)
.autocorrectionDisabled() .autocorrectionDisabled()
.sheet(isPresented: $showLegendView) {
InscriptionLegendView()
}
} }
@ViewBuilder @ViewBuilder
@ -662,6 +756,12 @@ struct InscriptionManagerView: View {
case .notImported: case .notImported:
let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count) let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count)
return notImported.formatted() return notImported.formatted()
case .registeredLocally:
let registeredLocally: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() == false }).count)
return registeredLocally.formatted()
case .registeredOnline:
let registeredOnline: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() }).count)
return registeredOnline.formatted()
} }
} }
@ -719,6 +819,7 @@ struct InscriptionManagerView: View {
if tournament.isAnimation() == false { if tournament.isAnimation() == false {
NavigationLink { NavigationLink {
InscriptionInfoView(tournament: tournament) InscriptionInfoView(tournament: tournament)
.environment(tournament)
} label: { } label: {
LabeledContent { LabeledContent {
if let registrationIssues { if let registrationIssues {
@ -737,7 +838,35 @@ struct InscriptionManagerView: View {
if let closedRegistrationDate = tournament.closedRegistrationDate { if let closedRegistrationDate = tournament.closedRegistrationDate {
CloseDatePicker(closedRegistrationDate: closedRegistrationDate) CloseDatePicker(closedRegistrationDate: closedRegistrationDate)
} }
// Button("bug fix") {
// _fixModel()
// }
if tournament.enableOnlineRegistration {
Button {
Task {
await _refreshList()
}
} label: {
LabeledContent {
if refreshInProgress {
ProgressView()
} else if let refreshStatus {
if refreshStatus {
Image(systemName: "checkmark").foregroundStyle(.green).font(.headline)
} else {
Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline)
}
}
} label: {
Text("Récupérer les inscriptions en ligne")
if let refreshResult {
Text(refreshResult)
}
}
}
}
} header: { } header: {
HStack { HStack {
Spacer() Spacer()

@ -0,0 +1,377 @@
//
// RegistrationSetupView.swift
// PadelClub
//
// Created by razmig on 20/11/2024.
//
import SwiftUI
import LeStorage
struct RegistrationSetupView: View {
@EnvironmentObject var dataStore: DataStore
@Bindable var tournament: Tournament
@State private var enableOnlineRegistration: Bool
@State private var registrationDateLimit: Date
@State private var openingRegistrationDate: Date
@State private var targetTeamCount: Int
@State private var waitingListLimit: Int
@State private var registrationDateLimitEnabled: Bool
@State private var targetTeamCountEnabled: Bool
@State private var waitingListLimitEnabled: Bool
@State private var openingRegistrationDateEnabled: Bool
@State private var userAccountIsRequired: Bool
@State private var licenseIsRequired: Bool
@State private var minPlayerPerTeam: Int
@State private var maxPlayerPerTeam: Int
@State private var showMoreInfos: Bool = false
@State private var hasChanges: Bool = false
@Environment(\.dismiss) private var dismiss
init(tournament: Tournament) {
self.tournament = tournament
_enableOnlineRegistration = .init(wrappedValue: tournament.enableOnlineRegistration)
// Registration Date Limit
if let registrationDateLimit = tournament.registrationDateLimit {
_registrationDateLimit = .init(wrappedValue: registrationDateLimit)
_registrationDateLimitEnabled = .init(wrappedValue: true)
} else {
_registrationDateLimit = .init(wrappedValue: tournament.startDate.truncateMinutesAndSeconds())
_registrationDateLimitEnabled = .init(wrappedValue: false)
}
// Opening Registration Date
if let openingRegistrationDate = tournament.openingRegistrationDate {
_openingRegistrationDate = .init(wrappedValue: openingRegistrationDate)
_openingRegistrationDateEnabled = .init(wrappedValue: true)
} else {
_openingRegistrationDate = .init(wrappedValue: tournament.creationDate.truncateMinutesAndSeconds())
_openingRegistrationDateEnabled = .init(wrappedValue: false)
}
// Target Team Count
_targetTeamCount = .init(wrappedValue: tournament.teamCount) // Default value
_targetTeamCountEnabled = .init(wrappedValue: false)
// Waiting List Limit
if let waitingListLimit = tournament.waitingListLimit {
_waitingListLimit = .init(wrappedValue: waitingListLimit)
_waitingListLimitEnabled = .init(wrappedValue: true)
} else {
_waitingListLimit = .init(wrappedValue: 0) // Default value
_waitingListLimitEnabled = .init(wrappedValue: false)
}
_userAccountIsRequired = .init(wrappedValue: tournament.accountIsRequired)
_licenseIsRequired = .init(wrappedValue: tournament.licenseIsRequired)
_maxPlayerPerTeam = .init(wrappedValue: tournament.maximumPlayerPerTeam)
_minPlayerPerTeam = .init(wrappedValue: tournament.minimumPlayerPerTeam)
}
var body: some View {
List {
Section {
Toggle(isOn: $enableOnlineRegistration) {
Text("Activer")
}
} footer: {
VStack(alignment: .leading) {
Text("Les inscriptions en ligne permettent à des joueurs de s'inscrire à votre tournoi en passant par le site Padel Club. Vous verrez alors votre liste d'inscription s'agrandir dans la vue Gestion des Inscriptions de l'application.")
FooterButtonView("En savoir plus") {
self.showMoreInfos = true
}
}
}
if enableOnlineRegistration {
if let shareURL = tournament.shareURL(.info) {
Section {
Link(destination: shareURL) {
Text(shareURL.absoluteString)
}
} header: {
Text("Page d'inscription")
} footer: {
HStack {
CopyPasteButtonView(pasteValue: shareURL.absoluteString)
Spacer()
ShareLink(item: shareURL) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
}
}
Section {
Toggle(isOn: $openingRegistrationDateEnabled) {
Text("Définir une date")
}
if openingRegistrationDateEnabled {
DatePicker(selection: $openingRegistrationDate) {
DateMenuView(date: $openingRegistrationDate)
}
}
} header: {
Text("Date d'ouverture des inscriptions")
} footer: {
Text("Activez et définissez une date d'ouverture pour les inscriptions au tournoi. Les inscriptions en ligne ne seront possible qu'à partir de cette date.")
}
Section {
Toggle(isOn: $registrationDateLimitEnabled) {
Text("Définir une date")
}
if registrationDateLimitEnabled {
DatePicker(selection: $registrationDateLimit) {
DateMenuView(date: $registrationDateLimit)
}
}
} header: {
Text("Date de fermeture des inscriptions")
} footer: {
Text("Si une date de fermeture des inscriptions en ligne est définie, alors plus aucune inscription ne sera possible après cette date. Sinon, la date du début du tournoi ou la date de clôture des inscriptions seront utilisées.")
}
Section {
Toggle(isOn: $targetTeamCountEnabled) {
Text("Activer une limite")
}
if targetTeamCountEnabled {
StepperView(count: $targetTeamCount, minimum: 4)
}
} header: {
Text("Paires admises")
} footer: {
Text("Si une limite de paire existe, les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.")
}
Section {
Toggle(isOn: $waitingListLimitEnabled) {
Text("Activer une limite")
}
if waitingListLimitEnabled {
StepperView(count: $waitingListLimit, minimum: 1)
}
} header: {
Text("Liste d'attente")
} footer: {
Text("Si une limite à la liste d'attente existe, les inscriptions ne seront plus possibles une fois la liste d'attente pleine. Si aucune limite de liste d'attente n'est active, alors les inscriptions seront toujours possibles. Les joueurs auront une indication comme quoi ils sont en liste d'attente.")
}
if tournament.isAnimation() {
Section {
// Toggle(isOn: $userAccountIsRequired) {
// Text("Compte Padel Club requis pour s'inscrire")
// }
// .disabled(true)
Toggle(isOn: $licenseIsRequired) {
Text("Licence FFT requise pour s'inscrire")
}
LabeledContent {
StepperView(count: $minPlayerPerTeam, minimum: 1, maximum: maxPlayerPerTeam)
} label: {
Text("Nombre minimum de joueurs possible")
}
LabeledContent {
StepperView(count: $maxPlayerPerTeam, minimum: minPlayerPerTeam)
} label: {
Text("Nombre maximum de joueurs possible")
}
}
}
} else {
ContentUnavailableView(
"Activez les inscriptions en ligne",
systemImage: "person.2.crop.square.stack.fill",
description: Text("Permettez aux joueurs de s'inscrire eux-mêmes à ce tournoi. Les équipes inscrites apparaîtront automatiquement dans la liste de l'arbitre. L'inscription en ligne requiert un email de contact et une licence FFT.")
)
}
}
.sheet(isPresented: $showMoreInfos) {
RegistrationInfoSheetView()
}
.toolbar(content: {
if hasChanges {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView(role: .destructive) {
_save()
dismiss()
}
}
}
})
.toolbarRole(.editor)
.headerProminence(.increased)
.navigationTitle("Inscription en ligne")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarBackButtonHidden(hasChanges)
.onChange(of: enableOnlineRegistration, {
_hasChanged()
})
.onChange(of: openingRegistrationDateEnabled) {
_hasChanged()
}
.onChange(of: openingRegistrationDate) {
_hasChanged()
}
.onChange(of: registrationDateLimitEnabled) {
_hasChanged()
}
.onChange(of: registrationDateLimit) {
_hasChanged()
}
.onChange(of: targetTeamCountEnabled) {
_hasChanged()
}
.onChange(of: targetTeamCount) {
_hasChanged()
}
.onChange(of: waitingListLimitEnabled) {
_hasChanged()
}
.onChange(of: waitingListLimit) {
_hasChanged()
}
.onChange(of: [minPlayerPerTeam, maxPlayerPerTeam]) {
_hasChanged()
}
.onChange(of: [userAccountIsRequired, licenseIsRequired]) {
_hasChanged()
}
}
private func _hasChanged() {
hasChanges = true
}
private func _save() {
hasChanges = false
tournament.enableOnlineRegistration = enableOnlineRegistration
if enableOnlineRegistration {
tournament.accountIsRequired = userAccountIsRequired
tournament.licenseIsRequired = licenseIsRequired
tournament.minimumPlayerPerTeam = minPlayerPerTeam
tournament.maximumPlayerPerTeam = maxPlayerPerTeam
} else {
tournament.accountIsRequired = true
tournament.licenseIsRequired = true
tournament.minimumPlayerPerTeam = 2
tournament.maximumPlayerPerTeam = 2
}
if openingRegistrationDateEnabled == false {
tournament.openingRegistrationDate = nil
} else {
tournament.openingRegistrationDate = openingRegistrationDate
}
if registrationDateLimitEnabled == false {
tournament.registrationDateLimit = nil
} else {
tournament.registrationDateLimit = registrationDateLimit
}
if targetTeamCountEnabled == false {
tournament.teamCount = 24
} else {
tournament.teamCount = targetTeamCount
}
if waitingListLimitEnabled == false {
tournament.waitingListLimit = nil
} else {
tournament.waitingListLimit = waitingListLimit
}
do {
try self.dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
dismiss()
}
}
enum OnlineRegistrationStatus: Int {
case open = 1
case notEnabled = 2
case notStarted = 3
case ended = 4
case waitingListPossible = 5
case waitingListFull = 6
case inProgress = 7
case endedWithResults = 8
var displayName: String {
switch self {
case .open:
return "Open"
case .notEnabled:
return "Not Enabled"
case .notStarted:
return "Not Started"
case .ended:
return "Ended"
case .waitingListPossible:
return "Waiting List Possible"
case .waitingListFull:
return "Waiting List Full"
case .inProgress:
return "In Progress"
case .endedWithResults:
return "Ended with Results"
}
}
func statusLocalized() -> String {
switch self {
case .open:
return "Inscription ouverte"
case .notEnabled:
return "Inscription désactivée"
case .notStarted:
return "Inscription pas encore ouverte"
case .ended:
return "Inscription terminée"
case .waitingListPossible:
return "Liste d'attente disponible"
case .waitingListFull:
return "Liste d'attente complète"
case .inProgress:
return "Tournoi en cours"
case .endedWithResults:
return "Tournoi terminé"
}
}
}

@ -77,17 +77,26 @@ struct TableStructureView: View {
teamsPerGroupStage = structurePreset.teamsPerGroupStage() teamsPerGroupStage = structurePreset.teamsPerGroupStage()
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
groupStageAdditionalQualified = 0 groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
} }
} }
Section { Section {
LabeledContent { LabeledContent {
StepperView(count: $teamCount, minimum: 4, maximum: 128) StepperView(count: $teamCount, minimum: 4, maximum: 128) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: { } label: {
Text("Nombre d'équipes") Text("Nombre d'équipes")
} }
LabeledContent { LabeledContent {
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: { } label: {
Text("Nombre de poules") Text("Nombre de poules")
} }
@ -99,21 +108,33 @@ struct TableStructureView: View {
if (teamCount / groupStageCount) > 1 { if (teamCount / groupStageCount) > 1 {
Section { Section {
LabeledContent { LabeledContent {
StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount)) StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount)) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: { } label: {
Text("Équipes par poule") Text("Équipes par poule")
} }
if structurePreset != .doubleGroupStage { if structurePreset != .doubleGroupStage {
LabeledContent { LabeledContent {
StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: { } label: {
Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule") Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule")
} }
if qualifiedPerGroupStage < teamsPerGroupStage - 1 { if qualifiedPerGroupStage < teamsPerGroupStage - 1 {
LabeledContent { LabeledContent {
StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: { } label: {
Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires") Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires")
Text(moreQualifiedLabel) Text(moreQualifiedLabel)
@ -223,7 +244,7 @@ struct TableStructureView: View {
} }
} }
if structurePreset.hasWildcards() { if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
Section { Section {
Toggle("Avec wildcards", isOn: $buildWildcards) Toggle("Avec wildcards", isOn: $buildWildcards)
} footer: { } footer: {
@ -310,14 +331,9 @@ struct TableStructureView: View {
updatedElements.insert(.groupStageAdditionalQualified) updatedElements.insert(.groupStageAdditionalQualified)
} else { } else {
updatedElements.remove(.groupStageAdditionalQualified) updatedElements.remove(.groupStageAdditionalQualified)
} }
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Confirmer") {
stepperFieldIsFocused = false
_verifyValueIntegrity()
}
} }
}
.toolbar {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial { if tournament.state() == .initial {
ButtonValidateView { ButtonValidateView {

@ -28,6 +28,14 @@ struct TournamentRankView: View {
} }
} }
var hideRankings: Binding<Bool> {
Binding {
tournament.publishRankings == false
} set: { value in
tournament.publishRankings = !value
}
}
var body: some View { var body: some View {
List { List {
@Bindable var tournament = tournament @Bindable var tournament = tournament
@ -53,18 +61,17 @@ struct TournamentRankView: View {
} }
//affiche l'onglet sur le site, car sur le broadcast c'est dispo automatiquement de toute façon //affiche l'onglet sur le site, car sur le broadcast c'est dispo automatiquement de toute façon
Toggle(isOn: $tournament.publishRankings) { Toggle(isOn: $tournament.publishRankings) {
Text("Publier sur Padel Club") if calculating {
if let url = tournament.shareURL(.rankings) { ProgressView("Calcul en cours")
Link(destination: url) { } else {
Text("Accéder à la page") Text("Publier sur Padel Club")
}
} }
} }
.onChange(of: tournament.publishRankings) { .disabled(calculating)
do { } footer: {
try dataStore.tournaments.addOrUpdate(instance: tournament) if let url = tournament.shareURL(.rankings) {
} catch { Link(destination: url) {
Logger.error(error) Text("Voir la page des classements sur Padel Club")
} }
} }
} }
@ -88,6 +95,8 @@ struct TournamentRankView: View {
team.finalRanking = nil team.finalRanking = nil
team.pointsEarned = nil team.pointsEarned = nil
} }
tournament.publishRankings = false
_save() _save()
} }
} }
@ -107,6 +116,13 @@ struct TournamentRankView: View {
} }
} }
.id(calculating) .id(calculating)
.onChange(of: tournament.publishRankings) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
.alert("Position", isPresented: isEditingTeam) { .alert("Position", isPresented: isEditingTeam) {
if let selectedTeam { if let selectedTeam {
@Bindable var team = selectedTeam @Bindable var team = selectedTeam
@ -131,16 +147,12 @@ struct TournamentRankView: View {
} }
} }
} }
.overlay(content: {
if calculating {
ProgressView()
}
})
.onAppear { .onAppear {
let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil }) let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
if rankingPublished == false { if rankingPublished == false {
Task { Task {
await _calculateRankings() await _calculateRankings()
tournament.publishRankings = true
} }
} }
} }

@ -16,6 +16,7 @@ enum TournamentSettings: Identifiable, Selectable, Equatable {
case general case general
case club(Tournament) case club(Tournament)
case matchFormats case matchFormats
case tournamentType
var id: String { String(describing: self) } var id: String { String(describing: self) }
@ -29,6 +30,8 @@ enum TournamentSettings: Identifiable, Selectable, Equatable {
return "Général" return "Général"
case .club: case .club:
return "Terrains" return "Terrains"
case .tournamentType:
return "Type"
} }
} }
@ -55,7 +58,7 @@ struct TournamentSettingsView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
private func destinations() -> [TournamentSettings] { private func destinations() -> [TournamentSettings] {
[.general, .club(tournament), .matchFormats] [.general, .tournamentType, .club(tournament), .matchFormats]
} }
var body: some View { var body: some View {
@ -66,6 +69,8 @@ struct TournamentSettingsView: View {
TournamentStatusView(tournament: tournament) TournamentStatusView(tournament: tournament)
case .matchFormats: case .matchFormats:
TournamentMatchFormatsSettingsView() TournamentMatchFormatsSettingsView()
case .tournamentType:
TournamentCategorySettingsView()
case .general: case .general:
TournamentGeneralSettingsView(tournament: tournament) TournamentGeneralSettingsView(tournament: tournament)
case .club: case .club:

@ -15,6 +15,7 @@ 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 shouldTournamentBeOver: Bool = false
var event: Event? { var event: Event? {
guard let federalTournament = tournament as? FederalTournament else { return nil } guard let federalTournament = tournament as? FederalTournament else { return nil }
@ -115,8 +116,9 @@ struct TournamentCellView: View {
if let tournament = tournament as? Tournament, displayStyle == .wide { if let tournament = tournament as? Tournament, displayStyle == .wide {
if tournament.isCanceled { if tournament.isCanceled {
Text("Annulé".uppercased()) Text("Annulé".uppercased())
.capsule(foreground: .white, background: .red) .capsule(foreground: .white, background: .logoRed)
} else if shouldTournamentBeOver {
Image(systemName: "clock.badge.questionmark").foregroundStyle(.logoRed)
} else if let teamCount { } else if let teamCount {
Text(teamCount.formatted()) Text(teamCount.formatted())
} }
@ -147,10 +149,15 @@ struct TournamentCellView: View {
Text(tournament.durationLabel()) Text(tournament.durationLabel())
} }
Spacer() Spacer()
if let tournament = tournament as? Tournament, tournament.isCanceled == false, let teamCount { if let tournament = tournament as? Tournament, tournament.isCanceled == false {
let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted() if shouldTournamentBeOver {
let word = hasStarted ? "équipe" : "inscription" Text("à clôturer ?")
Text(word + teamCount.pluralSuffix) .foregroundStyle(.logoRed)
} else if let teamCount {
let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted()
let word = hasStarted ? "équipe" : "inscription"
Text(word + teamCount.pluralSuffix)
}
} }
} }
} else { } else {
@ -178,7 +185,7 @@ struct TournamentCellView: View {
newTournament.federalTournamentAge = build.age newTournament.federalTournamentAge = build.age
newTournament.dayDuration = federalTournament.dayDuration newTournament.dayDuration = federalTournament.dayDuration
newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9)
newTournament.setupFederalSettings() newTournament.setupFederalSettings(fromEvent: event)
do { do {
try dataStore.tournaments.addOrUpdate(instance: newTournament) try dataStore.tournaments.addOrUpdate(instance: newTournament)
} catch { } catch {

@ -19,7 +19,9 @@ import LeStorage
var currentBestPurchase: Purchase? = nil var currentBestPurchase: Purchase? = nil
var updateListenerTask: Task<Void, Never>? = nil var updateListenerTask: Task<Void, Never>? = nil
fileprivate let _freeTournaments: Int = 3
override init() { override init() {
super.init() super.init()
@ -143,6 +145,7 @@ import LeStorage
purchase.revocationDate = transaction.revocationDate purchase.revocationDate = transaction.revocationDate
purchase.expirationDate = transaction.expirationDate purchase.expirationDate = transaction.expirationDate
purchase.purchaseDate = transaction.purchaseDate purchase.purchaseDate = transaction.purchaseDate
purchase.productId = transaction.productID
try purchases.addOrUpdate(instance: purchase) try purchases.addOrUpdate(instance: purchase)
} else { } else {
let purchase: Purchase = try transaction.purchase() let purchase: Purchase = try transaction.purchase()
@ -272,7 +275,7 @@ import LeStorage
fileprivate func _paymentWithoutSubscription() -> TournamentPayment? { fileprivate func _paymentWithoutSubscription() -> TournamentPayment? {
let freelyPayed: Int = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count let freelyPayed: Int = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count
if freelyPayed < 1 { if freelyPayed < self._freeTournaments {
return TournamentPayment.free return TournamentPayment.free
} }
let tournamentCreditCount: Int = self._purchasedTournamentCount() let tournamentCreditCount: Int = self._purchasedTournamentCount()

@ -33,7 +33,7 @@ struct SubscriptionInfoView: View {
struct FreeTournamentTip: Tip { struct FreeTournamentTip: Tip {
var title: Text { var title: Text {
return Text("Nous vous offrons votre premier tournoi ! 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 !") 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 !")
} }
var image: Image? { var image: Image? {

@ -11,13 +11,17 @@ import TipKit
struct TournamentView: View { struct TournamentView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel @Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@State var tournament: Tournament @State var tournament: Tournament
var presentationContext: PresentationContext = .agenda @State private var showMoreInfos: Bool = false
var presentationContext: PresentationContext = .agenda
let tournamentSelectionTip: TournamentSelectionTip = TournamentSelectionTip() let tournamentSelectionTip: TournamentSelectionTip = TournamentSelectionTip()
let tournamentRunningTip: TournamentRunningTip = TournamentRunningTip() let tournamentRunningTip: TournamentRunningTip = TournamentRunningTip()
let onlineRegistrationTip: OnlineRegistrationTip = OnlineRegistrationTip()
let shouldTournamentBeOverTip: ShouldTournamentBeOverTip = ShouldTournamentBeOverTip()
var selectedTournamentId: Binding<String> { Binding( var selectedTournamentId: Binding<String> { Binding(
get: { tournament.id }, get: { tournament.id },
@ -36,7 +40,7 @@ struct TournamentView: View {
guard let lastDataSource else { return nil } guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource) return URL.importDateFormatter.date(from: lastDataSource)
} }
init(tournament: Tournament, presentationContext: PresentationContext = .agenda) { init(tournament: Tournament, presentationContext: PresentationContext = .agenda) {
self.tournament = tournament self.tournament = tournament
self.presentationContext = presentationContext self.presentationContext = presentationContext
@ -59,17 +63,57 @@ struct TournamentView: View {
} }
} }
case .initial: case .initial:
if tournament.enableOnlineRegistration == false, tournament.onlineRegistrationCanBeEnabled() {
TipView(onlineRegistrationTip) { action in
if let actionKey = OnlineRegistrationTip.ActionKey(rawValue: action.id) {
switch actionKey {
case .enableOnlineRegistration:
navigation.path.append(Screen.settings)
case .more:
self.showMoreInfos = true
}
} else {
print("Unknown action: \(action.id)")
}
}
.tipStyle(tint: .master, asSection: true)
}
Section { Section {
TournamentInscriptionView(tournament: tournament) TournamentInscriptionView(tournament: tournament)
} }
TournamentInitView(tournament: tournament) TournamentInitView(tournament: tournament)
case .build: case .build:
if tournament.enableOnlineRegistration == false, tournament.onlineRegistrationCanBeEnabled() {
TipView(onlineRegistrationTip) { action in
if let actionKey = OnlineRegistrationTip.ActionKey(rawValue: action.id) {
switch actionKey {
case .enableOnlineRegistration:
navigation.path.append(Screen.settings)
case .more:
self.showMoreInfos = true
}
} else {
print("Unknown action: \(action.id)")
}
}
.tipStyle(tint: .master, asSection: true)
}
Section { Section {
TournamentInscriptionView(tournament: tournament) TournamentInscriptionView(tournament: tournament)
} }
TournamentBuildView(tournament: tournament) TournamentBuildView(tournament: tournament)
TournamentInitView(tournament: tournament) TournamentInitView(tournament: tournament)
case .running: case .running:
if tournament.shouldTournamentBeOver() {
Section {
TipView(shouldTournamentBeOverTip) { actions in
navigation.path.append(Screen.stateSettings)
}
.tipStyle(tint: .logoRed, asSection: true)
}
}
TournamentBuildView(tournament: tournament) TournamentBuildView(tournament: tournament)
TournamentRunningView(tournament: tournament) TournamentRunningView(tournament: tournament)
case .finished: case .finished:
@ -77,6 +121,9 @@ struct TournamentView: View {
TournamentRunningView(tournament: tournament) TournamentRunningView(tournament: tournament)
} }
} }
.sheet(isPresented: $showMoreInfos) {
RegistrationInfoSheetView()
}
} }
.environment(tournament) .environment(tournament)
.id(tournament.id) .id(tournament.id)
@ -115,6 +162,9 @@ struct TournamentView: View {
ShareModelView(instance: tournament) ShareModelView(instance: tournament)
case .restingTime: case .restingTime:
TeamRestingView() TeamRestingView()
case .stateSettings:
TournamentStatusView(tournament: tournament)
} }
} }
.environment(tournament) .environment(tournament)
@ -128,17 +178,17 @@ struct TournamentView: View {
Text(tournament.tournamentTitle(.title)).tag(tournament.id as String) Text(tournament.tournamentTitle(.title)).tag(tournament.id as String)
} }
} label: { } label: {
} }
Divider() Divider()
} }
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
Text("Réglages de l'événement") Text("Réglages de l'événement")
} }
} }
} }
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
@ -153,11 +203,11 @@ struct TournamentView: View {
TournamentSelectionTip.tournamentCount = tournament.eventObject()?.tournaments.count TournamentSelectionTip.tournamentCount = tournament.eventObject()?.tournaments.count
} }
} }
if tournament.isCanceled == false { if tournament.isCanceled == false {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {
#if DEBUG #if DEBUG
Button { Button {
do { do {
@ -169,7 +219,7 @@ struct TournamentView: View {
Label("Payer le tournoi", systemImage: "dollarsign.circle.fill") Label("Payer le tournoi", systemImage: "dollarsign.circle.fill")
} }
#endif #endif
if presentationContext == .agenda { if presentationContext == .agenda {
Button { Button {
navigation.openTournamentInOrganizer(tournament) navigation.openTournamentInOrganizer(tournament)
@ -178,7 +228,7 @@ struct TournamentView: View {
} }
Divider() Divider()
} }
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
Text("Réglages de l'événement") Text("Réglages de l'événement")
} }
@ -188,11 +238,28 @@ struct TournamentView: View {
NavigationLink(value: Screen.structure) { NavigationLink(value: Screen.structure) {
LabelStructure() LabelStructure()
} }
NavigationLink(value: Screen.rankings) {
LabeledContent {
if tournament.publishRankings == false {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.logoYellow)
} else {
Image(systemName: "checkmark")
.foregroundStyle(.green)
}
} label: {
Text("Classement final des équipes")
if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
}
}
}
NavigationLink(value: Screen.broadcast) { NavigationLink(value: Screen.broadcast) {
Label("Publication", systemImage: "airplayvideo") Label("Publication", systemImage: "airplayvideo")
} }
NavigationLink(value: Screen.print) { NavigationLink(value: Screen.print) {
Label("Imprimer", systemImage: "printer") Label("Imprimer", systemImage: "printer")
} }
@ -203,9 +270,7 @@ struct TournamentView: View {
Divider() Divider()
NavigationLink { NavigationLink(value: Screen.stateSettings) {
TournamentStatusView(tournament: tournament)
} label: {
Text("Gestion du tournoi") Text("Gestion du tournoi")
Text("Annuler, supprimer ou terminer le tournoi") Text("Annuler, supprimer ou terminer le tournoi")
} }

@ -11,10 +11,11 @@ struct ListRowViewModifier: ViewModifier {
let isActive: Bool let isActive: Bool
let color: Color let color: Color
var hideColorVariation: Bool = false var hideColorVariation: Bool = false
var backgroundColor: Color? = nil
let alignment: Alignment let alignment: Alignment
func colorVariation() -> Color { func colorVariation() -> Color {
hideColorVariation ? Color(uiColor: .systemBackground) : color.variation() hideColorVariation ? (backgroundColor ?? Color(uiColor: .systemBackground)) : color.variation()
} }
func body(content: Content) -> some View { func body(content: Content) -> some View {
@ -33,7 +34,7 @@ struct ListRowViewModifier: ViewModifier {
} }
extension View { extension View {
func listRowView(isActive: Bool = false, color: Color, hideColorVariation: Bool = false, alignment: Alignment = .leading) -> some View { func listRowView(isActive: Bool = false, color: Color, hideColorVariation: Bool = false, backgroundColor: Color? = nil, alignment: Alignment = .leading) -> some View {
modifier(ListRowViewModifier(isActive: isActive, color: color, hideColorVariation: hideColorVariation, alignment: alignment)) modifier(ListRowViewModifier(isActive: isActive, color: color, hideColorVariation: hideColorVariation, backgroundColor: backgroundColor, alignment: alignment))
} }
} }

@ -11,12 +11,12 @@ import LeStorage
final class ServerDataTests: XCTestCase { final class ServerDataTests: XCTestCase {
let username: String = "test" let username: String = "UserDataTests"
let password: String = "MyPass1234--" let password: String = "MyPass1234--"
override func setUpWithError() throws { override func setUpWithError() throws {
// StoreCenter.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/" // StoreCenter.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/"
StoreCenter.main.configureURLs(httpScheme: "http://", domain: "127.0.0.1:8000") StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000")
Task { Task {
do { do {
try await self.login() try await self.login()
@ -71,7 +71,7 @@ final class ServerDataTests: XCTestCase {
func testLogin() async throws { func testLogin() async throws {
let user: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) let user: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password)
assert(user.username == "test") assert(user.username == self.username)
} }
func testEvent() async throws { func testEvent() async throws {
@ -104,7 +104,7 @@ final class ServerDataTests: XCTestCase {
return return
} }
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual, initialSeedRound: 8, initialSeedCount: 4) let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual, initialSeedRound: 8, initialSeedCount: 4, accountIsRequired: false, licenseIsRequired: false, minimumPlayerPerTeam: 3, maximumPlayerPerTeam: 5, information: "Super")
let t = try await StoreCenter.main.service().post(tournament) let t = try await StoreCenter.main.service().post(tournament)
assert(t.lastUpdate.formatted() == tournament.lastUpdate.formatted()) assert(t.lastUpdate.formatted() == tournament.lastUpdate.formatted())
@ -148,6 +148,11 @@ final class ServerDataTests: XCTestCase {
assert(t.loserBracketMode == tournament.loserBracketMode) assert(t.loserBracketMode == tournament.loserBracketMode)
assert(t.initialSeedCount == tournament.initialSeedCount) assert(t.initialSeedCount == tournament.initialSeedCount)
assert(t.initialSeedRound == tournament.initialSeedRound) assert(t.initialSeedRound == tournament.initialSeedRound)
assert(t.accountIsRequired == tournament.accountIsRequired)
assert(t.licenseIsRequired == tournament.licenseIsRequired)
assert(t.minimumPlayerPerTeam == tournament.minimumPlayerPerTeam)
assert(t.maximumPlayerPerTeam == tournament.maximumPlayerPerTeam)
assert(t.information == tournament.information)
} }
func testGroupStage() async throws { func testGroupStage() async throws {
@ -158,7 +163,7 @@ final class ServerDataTests: XCTestCase {
return return
} }
let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, matchFormat: MatchFormat.nineGames, startDate: Date(), name: "Yeah!", step: 1) let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, format: MatchFormat.nineGames, startDate: Date(), name: "Yeah!", step: 1)
groupStage.storeId = "123" groupStage.storeId = "123"
let gs: GroupStage = try await StoreCenter.main.service().post(groupStage) let gs: GroupStage = try await StoreCenter.main.service().post(groupStage)
@ -239,7 +244,8 @@ final class ServerDataTests: XCTestCase {
assert(tr.lockedWeight == teamRegistration.lockedWeight) assert(tr.lockedWeight == teamRegistration.lockedWeight)
assert(tr.confirmationDate?.formatted() == teamRegistration.confirmationDate?.formatted()) assert(tr.confirmationDate?.formatted() == teamRegistration.confirmationDate?.formatted())
assert(tr.qualified == teamRegistration.qualified) assert(tr.qualified == teamRegistration.qualified)
assert(tr.finalRanking == teamRegistration.finalRanking)
assert(tr.pointsEarned == teamRegistration.pointsEarned)
} }
func testPlayerRegistration() async throws { func testPlayerRegistration() async throws {
@ -250,8 +256,9 @@ final class ServerDataTests: XCTestCase {
return return
} }
let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerRegistration.PlayerPaymentType.cash, sex: PlayerRegistration.PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true) let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerPaymentType.cash, sex: PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true)
playerRegistration.storeId = "123" playerRegistration.storeId = "123"
let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration) let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration)
assert(pr.storeId == playerRegistration.storeId) assert(pr.storeId == playerRegistration.storeId)
@ -273,7 +280,10 @@ final class ServerDataTests: XCTestCase {
assert(pr.computedRank == playerRegistration.computedRank) assert(pr.computedRank == playerRegistration.computedRank)
assert(pr.source == playerRegistration.source) assert(pr.source == playerRegistration.source)
assert(pr.hasArrived == playerRegistration.hasArrived) assert(pr.hasArrived == playerRegistration.hasArrived)
assert(pr.captain == playerRegistration.captain)
assert(pr.coach == playerRegistration.coach)
assert(pr.registeredOnline == playerRegistration.registeredOnline)
} }
func testMatch() async throws { func testMatch() async throws {
@ -286,7 +296,7 @@ final class ServerDataTests: XCTestCase {
let rounds: [Round] = try await StoreCenter.main.service().get() let rounds: [Round] = try await StoreCenter.main.service().get()
let parentRoundId = rounds.first?.id let parentRoundId = rounds.first?.id
let match: Match = Match(round: parentRoundId, groupStage: nil, startDate: Date(), endDate: Date(), index: 2, matchFormat: MatchFormat.twoSets, servingTeamId: teamRegistrationId, winningTeamId: teamRegistrationId, losingTeamId: teamRegistrationId, disabled: true, courtIndex: 1, confirmed: true) let match: Match = Match(round: parentRoundId, groupStage: nil, startDate: Date(), endDate: Date(), index: 2, format: MatchFormat.twoSets, servingTeamId: teamRegistrationId, winningTeamId: teamRegistrationId, losingTeamId: teamRegistrationId, disabled: true, courtIndex: 1, confirmed: true)
match.storeId = "123" match.storeId = "123"
let m: Match = try await StoreCenter.main.service().post(match) let m: Match = try await StoreCenter.main.service().post(match)
@ -382,7 +392,7 @@ final class ServerDataTests: XCTestCase {
let transactionId = UInt64.random(in: 0...100000) let transactionId = UInt64.random(in: 0...100000)
let quantity = Int.random(in: 0...10) let quantity = Int.random(in: 0...10)
let purchase: Purchase = Purchase(transactionId: transactionId, purchaseDate: Date(), productId: "app.padelclub.productId", quantity: quantity, revocationDate: Date(), expirationDate: Date()) let purchase: Purchase = Purchase(user: userId, transactionId: transactionId, purchaseDate: Date(), productId: "app.padelclub.productId", quantity: quantity, revocationDate: Date(), expirationDate: Date())
let p: Purchase = try await StoreCenter.main.service().post(purchase) let p: Purchase = try await StoreCenter.main.service().post(purchase)

@ -16,7 +16,7 @@ final class TokenExemptionTests: XCTestCase {
let password: String = "MyPass1234--" let password: String = "MyPass1234--"
override func setUpWithError() throws { override func setUpWithError() throws {
StoreCenter.main.configureURLs(httpScheme: "http://", domain: "127.0.0.1:8000") StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000")
StoreCenter.main.disconnect() StoreCenter.main.disconnect()
} }
@ -49,8 +49,8 @@ final class TokenExemptionTests: XCTestCase {
} }
func login() async throws -> User { func login() async throws -> CustomUser {
let user: User = try await StoreCenter.main.service().login(username: self.username, password: self.password) let user: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password)
return user return user
} }

@ -11,11 +11,11 @@ import LeStorage
final class UserDataTests: XCTestCase { final class UserDataTests: XCTestCase {
let username: String = "test" let username: String = "UserDataTests"
let password: String = "MyPass1234--" let password: String = "MyPass1234--"
override func setUpWithError() throws { override func setUpWithError() throws {
StoreCenter.main.configureURLs(httpScheme: "http://", domain: "127.0.0.1:8000") StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000")
} }
override func tearDownWithError() throws { override func tearDownWithError() throws {
@ -24,7 +24,11 @@ final class UserDataTests: XCTestCase {
func testUserCreation() async throws { func testUserCreation() async throws {
let userCreationForm = UserCreationForm(user: CustomUser.placeHolder(), username: self.username, password: self.password, firstName: "jean", lastName: "coco", email: "test@lolomo.com", phone: "0123", country: "France") //<<<<<<< HEAD
// let userCreationForm = UserCreationForm(user: CustomUser.placeHolder(), username: self.username, password: self.password, firstName: "jean", lastName: "coco", email: "test@lolomo.com", phone: "0123", country: "France")
// let user: CustomUser = try await StoreCenter.main.service().createAccount(user: userCreationForm)
//=======
let userCreationForm = UserCreationForm(user: CustomUser.placeHolder(), username: self.username, password: self.password, firstName: "jean", lastName: "coco", email: "UserDataTests@lolomo.net", phone: "0123", country: "France")
let user: CustomUser = try await StoreCenter.main.service().createAccount(user: userCreationForm) let user: CustomUser = try await StoreCenter.main.service().createAccount(user: userCreationForm)
assert(user.username == userCreationForm.username) assert(user.username == userCreationForm.username)
@ -41,6 +45,10 @@ final class UserDataTests: XCTestCase {
return user return user
} }
func testLogin() async throws {
let _ = try await self.login()
}
func testUserUpdate() async throws { func testUserUpdate() async throws {
let user = try await self.login() let user = try await self.login()

Loading…
Cancel
Save