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

@ -127,6 +127,9 @@
C488C8832CCBE8FC0082001F /* 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 */; };
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 */; };
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.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 */; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
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 */; };
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.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 */; };
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 */; };
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.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 */; };
FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41832BF75ED7001B24CB /* EventTournamentsView.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 */; };
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.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>"; };
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>"; };
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>"; };
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>"; };
@ -1208,9 +1227,12 @@
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -1333,6 +1355,8 @@
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>"; };
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>"; };
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>"; };
@ -1473,6 +1497,8 @@
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */,
FF2B515F2C7E300500FFF126 /* SeedData */,
C4A47D722B72881500ADC637 /* Views */,
FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */,
FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */,
FF3F74FD2B91A087004CFE0E /* ViewModel */,
C4A47D5F2B6D3B2D00ADC637 /* Data */,
FFF8ACD02B9238A2008466FA /* Utils */,
@ -1578,6 +1604,7 @@
FF967CED2BAECBD700A9A3BD /* Round.swift */,
FF967CEB2BAECB9900A9A3BD /* Match.swift */,
FF967CF12BAECC0B00A9A3BD /* PlayerRegistration.swift */,
C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */,
FF967CF02BAECC0B00A9A3BD /* TeamRegistration.swift */,
FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */,
C4A47D622B6D3D6500ADC637 /* Club.swift */,
@ -1861,6 +1888,7 @@
isa = PBXGroup;
children = (
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */,
FFBFC3902CEE3A0E000EBD8D /* RegistrationSetupView.swift */,
FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */,
FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */,
FF8F26532BAE1E4400650388 /* TableStructureView.swift */,
@ -1962,6 +1990,7 @@
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */,
FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */,
FFE103112C366E5900684FC9 /* ImagePickerView.swift */,
FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -2011,6 +2040,7 @@
FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */,
FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */,
FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */,
FF4623CA2D1340D200CB57B5 /* TournamentCategorySettingsView.swift */,
FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */,
FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */,
);
@ -2650,6 +2680,7 @@
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */,
C4A36F5A2CE2626A003738C6 /* TournamentLibrary.swift in Sources */,
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */,
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
C4FC2E2B2C2C0E4D0021F3BF /* TournamentStore.swift in Sources */,
@ -2681,6 +2712,7 @@
C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */,
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */,
FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */,
FFBFC3922CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */,
FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
@ -2713,6 +2745,7 @@
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */,
FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
@ -2737,8 +2770,10 @@
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */,
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */,
C49C73142D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */,
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */,
@ -2809,6 +2844,7 @@
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */,
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */,
FF4623CC2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */,
C4EC6F592BE92D88000CEAB4 /* PListReader.swift in Sources */,
FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */,
FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */,
@ -2913,6 +2949,7 @@
C488C84D2CC7E4240082001F /* BaseEvent.swift in Sources */,
C488C84E2CC7E4240082001F /* BaseRound.swift in Sources */,
C488C84F2CC7E4240082001F /* BaseMatchScheduler.swift in Sources */,
C49C73152D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */,
C488C8512CC7E4240082001F /* BasePlayerRegistration.swift in Sources */,
C488C8522CC7E4240082001F /* BaseTeamScore.swift in Sources */,
C488C8532CC7E4240082001F /* BaseTournament.swift in Sources */,
@ -2955,6 +2992,7 @@
C4339BFD2CFF7D68004E5F09 /* ShareModelView.swift in Sources */,
FF4CBF9C2C996C0600151637 /* ImportedPlayerView.swift in Sources */,
FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */,
FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF4CBF9E2C996C0600151637 /* NetworkManagerError.swift in Sources */,
FF4CBF9F2C996C0600151637 /* Tournament.swift in Sources */,
FF4CBFA02C996C0600151637 /* TournamentStore.swift in Sources */,
@ -2986,6 +3024,7 @@
FF4CBFB92C996C0600151637 /* Purchase.swift in Sources */,
FF4CBFBA2C996C0600151637 /* Screen.swift in Sources */,
FF4CBFBB2C996C0600151637 /* Round.swift in Sources */,
FFBFC3912CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */,
FF4CBFBC2C996C0600151637 /* FederalDataViewModel.swift in Sources */,
FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */,
FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */,
@ -3019,6 +3058,7 @@
FF4CBFD72C996C0600151637 /* LoserBracketFromGroupStageView.swift in Sources */,
FF4CBFD82C996C0600151637 /* ImportedPlayer+Extensions.swift in Sources */,
FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */,
FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
@ -3043,6 +3083,7 @@
FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */,
FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */,
FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */,
FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF4CBFF02C996C0600151637 /* URLs.swift in Sources */,
FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
@ -3115,6 +3156,7 @@
FF4CC02C2C996C0600151637 /* DateBoxView.swift in Sources */,
FF4CC02D2C996C0600151637 /* LearnMoreSheetView.swift in Sources */,
FF4CC02E2C996C0600151637 /* SourceFileManager.swift in Sources */,
FF4623CB2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */,
FF4CC02F2C996C0600151637 /* PListReader.swift in Sources */,
FF4CC0302C996C0600151637 /* UpdateSourceRankDateView.swift in Sources */,
FF4CC0312C996C0600151637 /* GlobalSettingsView.swift in Sources */,
@ -3197,6 +3239,7 @@
C488C85A2CC7E4240082001F /* BaseEvent.swift in Sources */,
C488C85B2CC7E4240082001F /* BaseRound.swift in Sources */,
C488C85C2CC7E4240082001F /* BaseMatchScheduler.swift in Sources */,
C49C73162D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */,
C488C85E2CC7E4240082001F /* BasePlayerRegistration.swift in Sources */,
C488C85F2CC7E4240082001F /* BaseTeamScore.swift in Sources */,
C488C8602CC7E4240082001F /* BaseTournament.swift in Sources */,
@ -3239,6 +3282,7 @@
C4339BFC2CFF7D68004E5F09 /* ShareModelView.swift in Sources */,
FF70FB1B2C90584900129CC2 /* ImportedPlayerView.swift in Sources */,
FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */,
FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */,
FF70FB1D2C90584900129CC2 /* NetworkManagerError.swift in Sources */,
FF70FB1E2C90584900129CC2 /* Tournament.swift in Sources */,
FF70FB1F2C90584900129CC2 /* TournamentStore.swift in Sources */,
@ -3270,6 +3314,7 @@
FF70FB382C90584900129CC2 /* Purchase.swift in Sources */,
FF70FB392C90584900129CC2 /* Screen.swift in Sources */,
FF70FB3A2C90584900129CC2 /* Round.swift in Sources */,
FFBFC3932CEE3A0E000EBD8D /* RegistrationSetupView.swift in Sources */,
FF70FB3B2C90584900129CC2 /* FederalDataViewModel.swift in Sources */,
FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */,
FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */,
@ -3303,6 +3348,7 @@
FF70FB562C90584900129CC2 /* LoserBracketFromGroupStageView.swift in Sources */,
FF70FB572C90584900129CC2 /* ImportedPlayer+Extensions.swift in Sources */,
FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */,
FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
@ -3327,6 +3373,7 @@
FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */,
FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */,
FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */,
FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */,
FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
@ -3399,6 +3446,7 @@
FF70FBAB2C90584900129CC2 /* DateBoxView.swift in Sources */,
FF70FBAC2C90584900129CC2 /* LearnMoreSheetView.swift in Sources */,
FF70FBAD2C90584900129CC2 /* SourceFileManager.swift in Sources */,
FF4623CD2D1340D200CB57B5 /* TournamentCategorySettingsView.swift in Sources */,
FF70FBAE2C90584900129CC2 /* PListReader.swift in Sources */,
FF70FBAF2C90584900129CC2 /* UpdateSourceRankDateView.swift in Sources */,
FF70FBB02C90584900129CC2 /* GlobalSettingsView.swift in Sources */,
@ -3573,7 +3621,10 @@
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3584,7 +3635,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.42;
MARKETING_VERSION = 1.1.8;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3617,7 +3668,10 @@
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3628,7 +3682,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.42;
MARKETING_VERSION = 1.1.8;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3721,7 +3775,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3734,7 +3788,10 @@
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3745,7 +3802,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.30;
MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3766,7 +3823,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3778,7 +3835,10 @@
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_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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3789,7 +3849,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.30;
MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3811,7 +3871,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3834,7 +3894,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.24;
MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3854,7 +3914,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3876,7 +3936,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.24;
MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

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

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

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

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

@ -54,6 +54,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
var loserBracketMode: LoserBracketMode = .automatic
var initialSeedRound: 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(
id: String = Store.randomId(),
@ -98,7 +107,16 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
publishRankings: Bool = false,
loserBracketMode: LoserBracketMode = .automatic,
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()
self.id = id
@ -144,6 +162,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound
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 {
@ -190,6 +217,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
case _loserBracketMode = "loserBracketMode"
case _initialSeedRound = "initialSeedRound"
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? {
@ -298,6 +334,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
self.initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 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)
}
@ -346,6 +391,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode)
try container.encode(self.initialSeedRound, forKey: ._initialSeedRound)
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)
}
@ -399,6 +453,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.loserBracketMode = tournament.loserBracketMode
self.initialSeedRound = tournament.initialSeedRound
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] {

@ -93,13 +93,28 @@
},
{
"name": "source",
"type": "PlayerDataSource",
"type": "PlayerRegistration.PlayerDataSource",
"optional": true
},
{
"name": "hasArrived",
"type": "Bool",
"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",
"type": "Int",
"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()
DataStore.shared.tournaments.addOrUpdate(instance: tournament)
}
tournament.updateTournamentState()
}
}

@ -195,7 +195,6 @@ defer {
servingTeamId = nil
groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
}
@ -497,7 +496,6 @@ defer {
losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
updateFollowingMatchTeamScore()
}
@ -536,7 +534,6 @@ defer {
} catch {
Logger.error(error)
}
tournament.updateTournamentState()
}
updateFollowingMatchTeamScore()
}
@ -546,7 +543,18 @@ defer {
teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",")
let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two))
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
}
@ -765,8 +773,8 @@ defer {
if teamPosition == team(.two)?.groupStagePositionAtStep(step) {
reverseValue = -1
}
let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.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({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut())
var setDifference : Int = 0
let zip = zip(endedSetsOne, endedSetsTwo)
if matchFormat.setsToWin == 1 {

@ -194,14 +194,10 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
return teamsAvailable
}))
if rotationIndex > 0 {
if rotationIndex > 0, simultaneousStart == false {
rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
if simultaneousStart {
return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1)
} else {
return $0.groupStageObject!.index < $1.groupStageObject!.index
}
} else {
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
final class PlayerRegistration: BasePlayerRegistration, SideStorable {
// static func resourceName() -> String { "player-registrations" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = ["teamRegistration"]
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var teamRegistration: String?
// var firstName: String
// 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) {
func localizedSourceLabel() -> String {
switch source {
case .frenchFederation:
return "base fédérale"
case .beachPadel:
return "beach-padel"
case nil:
return "créé par vous-même"
}
}
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()
self.teamRegistration = teamRegistration
self.firstName = firstName
@ -228,6 +208,10 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
source == .beachPadel
}
func unrankedOrUnknown() -> Bool {
source == nil
}
func isValidLicenseNumber(year: Int) -> Bool {
guard let licenceId 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 {
#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) {
rank = dataFound.rankValue?.toInt()
points = dataFound.points
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 {
rank = lastRank
}
}
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 {
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
for source in sources.filter({ $0.maleData == isMalePlayer() }) {
for source in filteredSources {
group.addTask {
guard !Task.isCancelled else { print("Cancelled"); return nil }
return try? await source.first(where: { line in
line.rawValue.contains(";\(license);")
})
return try? await source.first { $0.rawValue.contains(";\(license);") }
}
}
if let first = await group.first(where: { $0 != nil }) {
group.cancelAll()
return first
} else {
return nil
}
return nil
}
}
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
for source in sources.filter({ $0.maleData == isMalePlayer() }) {
group.addTask { [lastName, firstName] in
for source in filteredSources {
group.addTask {
guard !Task.isCancelled else { print("Cancelled"); return nil }
return try? await source.first(where: { line in
line.rawValue.canonicalVersionWithPunctuation.contains(";\(lastName.canonicalVersionWithPunctuation);\(firstName.canonicalVersionWithPunctuation);")
})
return try? await source.first {
let lineValue = $0.rawValue.canonicalVersionWithPunctuation
return lineValue.contains(";\(normalizedLastName);\(normalizedFirstName);")
}
}
}
if let first = await group.first(where: { $0 != nil }) {
group.cancelAll()
return first
} else {
return nil
}
return nil
}
}
@ -315,7 +327,7 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return
}
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 70_000
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000
switch tournament.tournamentCategory {
case .men:
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
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _storeId = "storeId"
// 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)
// }
enum PlayerDataSource: Int, Codable {
case frenchFederation = 0
case beachPadel = 1
}
static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
switch playerRank {
@ -491,47 +455,3 @@ enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable {
case female = 0
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() {
if let tournamentObject = tournamentObject(), index == 0, isUpperBracket(), hasEnded() {
let tournamentObject = tournamentObject()
if let tournamentObject, index == 0, isUpperBracket(), hasEnded() {
tournamentObject.endDate = Date()
DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject)
}
tournamentObject?.updateTournamentState()
}
func roundStatus() -> String {

@ -68,6 +68,19 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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 {
try super.init(from: decoder)
}
@ -80,7 +93,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func unsortedPlayers() -> [PlayerRegistration] {
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: -
@ -113,6 +126,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func isHere() -> Bool {
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.hasArrived })
}
@ -188,7 +202,10 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
func isImported() -> Bool {
return unsortedPlayers().allSatisfy({ $0.isImported() })
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.isImported() })
}
func isWildCard() -> Bool {
@ -243,9 +260,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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 }
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 {
@ -299,7 +316,10 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
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 {
@ -373,19 +393,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
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 {
case .rawText:
if let registrationDate {
return "Inscrit le " + registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
}
return "Inscrit\(onlineSuffix) le \(formattedDate)"
case .csv:
if let registrationDate {
return registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
}
return formattedDate
}
}
@ -418,6 +435,22 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) {
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)
self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove)
setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
@ -425,6 +458,12 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
players.forEach { player in
player.teamRegistration = id
}
// do {
// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// } catch {
// Logger.error(error)
// }
}
typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)
@ -462,8 +501,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool
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] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $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) {
let significantPlayerCount = significantPlayerCount()
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 {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 70_000
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000
}
func groupStageObject() -> GroupStage? {

@ -15,7 +15,11 @@ final class Tournament: BaseTournament {
@ObservationIgnored
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()
self.event = event
self.name = name
@ -25,7 +29,11 @@ final class Tournament: BaseTournament {
#if DEBUG
self.isPrivate = false
#else
if Guard.main.currentPlan == .monthlyUnlimited {
self.isPrivate = true
} else {
self.isPrivate = Guard.main.purchasedTransactions.isEmpty
}
#endif
self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat
@ -70,7 +78,16 @@ final class Tournament: BaseTournament {
self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound
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 {
@ -83,6 +100,7 @@ final class Tournament: BaseTournament {
override func deleteDependencies() {
guard let store = self.tournamentStore else { return }
let drawLogs = Array(store.drawLogs)
for drawLog in drawLogs {
drawLog.deleteDependencies()
@ -109,12 +127,6 @@ final class Tournament: BaseTournament {
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
@ -585,7 +597,7 @@ defer {
}
#endif
var _sortedTeams : [TeamRegistration] = []
var _teams = unsortedTeams().filter({ $0.walkOut == false })
var _teams = unsortedTeams().filter({ $0.isOutOfTournament() == false })
if let closedRegistrationDate {
_teams = _teams.filter({ team in
@ -641,7 +653,7 @@ defer {
func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] {
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)
if includingWalkOuts {
return waitings + walkOuts
@ -685,8 +697,7 @@ defer {
func unsortedTeamsWithoutWO() -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamRegistrations.filter { $0.walkOut == false }
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
return tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false }
}
func walkoutTeams() -> [TeamRegistration] {
@ -721,7 +732,9 @@ defer {
func paidSelectedPlayers(type: PlayerPaymentType) -> Double? {
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 {
return nil
}
@ -752,7 +765,7 @@ defer {
//todo
func significantPlayerCount() -> Int {
return 2
return minimumPlayerPerTeam
}
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
@ -1083,6 +1096,7 @@ defer {
groupStages.forEach { groupStage in
let groupStageTeams = groupStage.teams(true)
for (index, team) in groupStageTeams.enumerated() {
if groupStage.hasEnded() {
if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
@ -1096,6 +1110,7 @@ defer {
}
}
}
}
return teams
}
@ -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
}
@ -1160,31 +1188,55 @@ defer {
}
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 }
rankSourceDate = newDate
if currentMonthData() == nil {
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
await MainActor.run {
let formatted: String = URL.importDateFormatter.string(from: newDate)
let monthData: MonthData = MonthData(monthKey: formatted)
monthData.maleUnrankedValue = lastRankMan
monthData.femaleUnrankedValue = lastRankWoman
DataStore.shared.monthData.addOrUpdate(instance: monthData)
// Fetch current month data only once
let monthData = currentMonthData()
if monthData == nil {
async let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
async let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
let formatted = URL.importDateFormatter.string(from: newDate)
let newMonthData = MonthData(monthKey: formatted)
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 lastRankWoman = currentMonthData()?.femaleUnrankedValue
let lastRankMan = monthData?.maleUnrankedValue ?? 0
let lastRankWoman = monthData?.femaleUnrankedValue ?? 0
// Fetch only the required files
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
let sources = dataURLs.map { CSVParser(url: $0) }
guard !dataURLs.isEmpty else { return } // Early return if no files found
try await unsortedPlayers().concurrentForEach { player in
try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0)
let sources = dataURLs.map { CSVParser(url: $0) }
let players = unsortedPlayers()
try await players.concurrentForEach { player in
let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan
try await player.updateRank(from: sources, lastRank: lastRank)
}
}
func missingUnrankedValue() -> Bool {
return maleUnrankedValue == nil || femaleUnrankedValue == nil
}
@ -1774,12 +1826,20 @@ defer {
}
}
func setupFederalSettings() {
func setupFederalSettings(fromEvent event: Event?) {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
matchFormat = roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee
if event?.tenupId != nil {
//enableOnlineRegistration = true
registrationDateLimit = deadline(for: .inscription)
}
}
func onlineRegistrationCanBeEnabled() -> Bool {
isAnimation() == false
}
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
@ -1957,12 +2017,10 @@ defer {
func updateTournamentState() {
Task {
if hasEnded() {
let fr = await finalRanking()
_ = await setRankings(finalRanks: fr)
}
}
}
func allLoserRoundMatches() -> [Match] {
rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) }
@ -2016,27 +2074,42 @@ defer {
}
func removeAllSeeds() async {
unsortedTeams().forEach({ team in
let teams = unsortedTeams()
teams.forEach({ team in
team.bracketPosition = nil
team._cachedRestingTime = nil
})
let ts = allRoundMatches().flatMap { match in
let allMatches = allRoundMatches()
let ts = allMatches.flatMap { match in
match.teamScores
}
allMatches.forEach { match in
match.disabled = false
match.losingTeamId = nil
match.winningTeamId = nil
match.endDate = nil
match.removeCourt()
match.servingTeamId = nil
}
do {
try tournamentStore?.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
try tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
} catch {
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 {
@ -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: -
@ -2306,7 +2446,11 @@ extension Tournament {
func deadline(for type: TournamentDeadlineType) -> Date? {
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)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
}

@ -254,4 +254,9 @@ extension Date {
formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short
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))
}
func prefixMultilineTrimmed(_ length: Int) -> String {
String(trimmedMultiline.prefix(length))
}
var trimmed: String {
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 {
//try? Tips.resetDatastore()
try? Tips.resetDatastore()
try? Tips.configure([
.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 {
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
}
@ -44,30 +41,36 @@ class ImportObserver {
class FileImportManager {
static let shared = FileImportManager()
func updatePlayers(isMale: Bool, players: inout [FederalPlayer]) {
let replacements: [(Character, Character)] = [("Á", "ç"), ("", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")]
var playersLeft = players
SourceFileManager.shared.allFilesSortedByDate(isMale).forEach({ url in
if playersLeft.isEmpty == false {
var playersLeft = Dictionary(uniqueKeysWithValues: players.map { ($0.license, $0) })
SourceFileManager.shared.allFilesSortedByDate(isMale).forEach { url in
if playersLeft.isEmpty { return }
let federalPlayers = readCSV(inputFile: url)
let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements
playersLeft.forEach { importedPlayer in
if let federalPlayer = federalPlayers.first(where: { $0.license == importedPlayer.license }) {
let federalPlayersDict = Dictionary(uniqueKeysWithValues: federalPlayers.map { ($0.license, $0) })
for (license, importedPlayer) in playersLeft {
guard let federalPlayer = federalPlayersDict[license] else { continue }
var lastName = federalPlayer.lastName
lastName.replace(characters: replacementsCharacters)
var firstName = federalPlayer.firstName
lastName.replace(characters: replacementsCharacters)
firstName.replace(characters: replacementsCharacters)
importedPlayer.lastName = lastName.trimmed.uppercased()
importedPlayer.firstName = firstName.trimmed.capitalized
playersLeft.removeValue(forKey: license) // Remove processed player
}
}
}
})
players = playersLeft
players = Array(playersLeft.values)
}
func foundInWomenData(license: String?) -> Bool {
@ -148,7 +151,7 @@ class FileImportManager {
}
let significantPlayerCount = 2
let pl = players.prefix(significantPlayerCount).map { $0.computedRank }
let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 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,+)
} else {
self.weight = players.map { $0.computedRank }.reduce(0,+)

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

@ -369,6 +369,15 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
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 {
switch self {
case .p25:
@ -1648,7 +1657,7 @@ enum PlayersCountRange: Int, CaseIterable {
}
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] {
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 anonymousSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "anonymous")
func createDirectoryIfNeeded() {
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] {
let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "json"

@ -578,6 +578,72 @@ 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 {
@Environment(\.colorScheme) var colorScheme

@ -31,8 +31,8 @@ enum URLs: String, Identifiable {
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app"
case tenup = "https://tenup.fft.fr"
case padelCompetitionGeneralGuide = "https://fft-site.cdn.prismic.io/fft-site/Zqi2PB5LeNNTxlrS_1-REGLESGENERALESDELACOMPETITION-ANNEESPORTIVE2025.pdf"
case padelCompetitionSpecificGuide = "https://fft-site.cdn.prismic.io/fft-site/Zqi4ax5LeNNTxlsu_3-CAHIERDESCHARGESDESTOURNOIS-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/Z2mHz5bqstJ98ysm_CHAPITREIIICahierdeschargesdestournois.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 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) }))
&&
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!))
(selectedClubs.isEmpty || (tournament.codeClub != nil && selectedClubs.contains(tournament.codeClub!)))
&&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
@ -100,7 +100,7 @@ class FederalDataViewModel {
func countForTournamentBuilds(from tournaments: [any FederalTournamentHolder]) -> Int {
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))
&&
@ -151,7 +151,7 @@ class FederalDataViewModel {
&&
(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))
&&

@ -94,11 +94,11 @@ class MatchDescriptor: ObservableObject {
}
var teamOneScores: [String] {
setDescriptors.compactMap { $0.valueTeamOne }.map { "\($0)" }
setDescriptors.compactMap { $0.getValue(teamPosition: .one) }
}
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 }

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

@ -319,10 +319,10 @@ class SearchViewModel: ObservableObject, Identifiable {
// Remove all characters that are not in the allowedCharacterSet
var text = pasteField.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmedMultiline
// 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
text = text.replacing(digitPattern, with: "")
text = text.replacing(digitPattern, with: "").trimmingCharacters(in: .whitespacesAndNewlines)
let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines)
let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 }
@ -367,6 +367,7 @@ class SearchViewModel: ObservableObject, Identifiable {
}
print(predicate)
return predicate
}

@ -40,4 +40,27 @@ struct SetDescriptor: Identifiable, Equatable {
var shouldTieBreak: Bool {
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.startDate = startingDate
tournament.dayDuration = duration
tournament.setupFederalSettings()
tournament.setupFederalSettings(fromEvent: event)
}
do {

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

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

@ -8,6 +8,7 @@
import SwiftUI
struct CopyPasteButtonView: View {
var title: String?
let pasteValue: String?
@State private var copied: Bool = false
@ -20,6 +21,9 @@ struct CopyPasteButtonView: View {
copied = true
} label: {
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 image: String? = nil
let confirmationMessage: String
let cornerRadius: CGFloat
var action: (() -> ())? = nil
var asyncAction: (() async -> ())? = nil
@State private var askConfirmation: Bool = 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.title = title
self.systemImage = systemImage
self.image = image
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
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.title = title
self.systemImage = systemImage
self.image = image
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.asyncAction = asyncAction
self.cornerRadius = cornerRadius
}
var body: some View {
@ -79,6 +82,7 @@ struct RowButtonView: View {
if isLoading {
ZStack {
Color.master
.cornerRadius(cornerRadius)
ProgressView()
.tint(.white)
}

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

@ -127,6 +127,21 @@ struct PlayerBlockView: View {
} else {
Divider().frame(width: width).overlay(Color(white: 0.9))
}
let parts = string.components(separatedBy: "-")
if parts.count == 2, let mainScore = parts.first, let supScore = parts.last {
HStack(spacing: 0) {
Text(mainScore)
.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)
@ -135,6 +150,7 @@ struct PlayerBlockView: View {
.lineLimit(1)
}
}
}
} else if let team {
TeamWeightView(team: team, teamPosition: teamPosition)
}

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

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

@ -14,16 +14,18 @@ struct TournamentLookUpView: View {
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss
@FocusState private var isFocused: Bool
@State private var searchField: String = ""
@State var page: Int = 0
@State var total: Int = 0
@State private var showingSettingsAlert = false
@State private var searching: Bool = false
@State private var requestedToGetAllPages: Bool = false
@State private var revealSearchParameters: Bool = true
@State private var presentAlert: Bool = false
@State private var confirmSearch: Bool = false
@State private var locationRequested = false
var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments
@ -57,6 +59,16 @@ struct TournamentLookUpView: View {
} 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.")
}
.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: {
Button {
presentAlert = false
@ -86,8 +98,9 @@ struct TournamentLookUpView: View {
.navigationTitle("Chercher un tournoi")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: locationManager.city) {
if let newValue = locationManager.city, dataStore.appSettings.city.isEmpty {
if locationRequested, let newValue = locationManager.city {
dataStore.appSettings.city = newValue
locationRequested = false
}
}
.toolbarTitleDisplayMode(.large)
@ -299,16 +312,35 @@ struct TournamentLookUpView: View {
HStack {
TextField("Ville", text: $appSettings.city)
if let city = locationManager.city {
Divider()
Text(city).italic()
.onSubmit(of: .text) {
locationManager.city = nil
locationManager.location = nil
}
.focused($isFocused)
.onChange(of: isFocused) {
if isFocused {
appSettings.city = ""
}
}
// if let city = locationManager.city {
// Divider()
// Text(city).italic()
// }
if locationManager.requestStarted {
ProgressView()
} else {
} else if locationManager.manager.authorizationStatus != .restricted {
LocationButton {
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)
.foregroundColor (Color.white)
.cornerRadius (20)
@ -485,4 +517,12 @@ struct TournamentLookUpView: View {
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.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 {
print("importing \(mostRecentDateAvailable)")
@ -222,17 +217,6 @@ struct MainView: View {
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() {
guard importObserver.checkingFiles == false, importObserver.isImportingFile() == false else {
return
@ -245,15 +229,10 @@ struct MainView: View {
Task {
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)
if monthData.first(where: { $0.monthKey == "07-2024" }) == nil {
Task {
await _checkSourceFileAvailability()
}
} else {
if let current = monthData.last {
Task {
let updated = await SourceFileManager.shared.fetchData(fromDate: mostRecentDateImported)
@ -270,7 +249,6 @@ struct MainView: View {
}
}
}
}
}
//#Preview {

@ -40,10 +40,10 @@ struct PadelClubView: View {
if let currentMonth = monthData.first, currentMonth.incompleteMode {
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 {
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.")
@ -61,32 +61,22 @@ struct PadelClubView: View {
["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"]
*/
Section {
RowButtonView("Exporter en csv") {
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)
Section {
RowButtonView("Retry Anonymous") {
await _retryAnonymous()
}
}
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)
Section {
RowButtonView("Write anonymous") {
_writeAnonymous()
}
}
Section {
RowButtonView("Exporter en csv") {
await _exportCsv()
}
}
#endif
@ -169,7 +159,7 @@ struct PadelClubView: View {
if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted())
} else {
Text(70_000.formatted())
Text(90_000.formatted())
}
} label: {
Text("Rang d'un non classé")
@ -187,6 +177,11 @@ struct PadelClubView: View {
Text("Rang d'une non classée")
Text("Dames")
}
#if DEBUG
RowButtonView("recalc") {
await _calculateLastRank(dataSource: monthData.monthKey)
}
#endif
} header: {
HStack {
Text(monthData.monthKey)
@ -242,15 +237,110 @@ struct PadelClubView: View {
await SourceFileManager.shared.getAllFiles(initialDate: "08-2022")
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 {
// PadelClubView()
//}
func updateCSVFile(sourceCSVURL: URL, updatedCSVURL: URL) {
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
func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=82477107&numeroLicence=\(licenseID)") else {
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=\(idHomologation)&numeroLicence=\(licenseID)") else {
throw URLError(.badURL)
}
@ -268,7 +358,7 @@ func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
// 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 decoder = JSONDecoder()
@ -288,9 +378,21 @@ func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
// Function to fetch data for multiple license IDs using TaskGroup
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 }) {
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.firstName = playerData.prenom
}

@ -251,7 +251,13 @@ struct PlayerPopoverView: View {
}
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)
}

@ -32,9 +32,29 @@ struct PlayerDetailView: View {
var body: some View {
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 {
Toggle("Joueur sur place", isOn: $player.hasArrived)
Toggle("Capitaine", isOn: $player.captain)
Toggle("Coach", isOn: $player.coach)
}
Section {
LabeledContent {
TextField("Nom", text: $player.lastName)
.keyboardType(.alphabet)
@ -208,7 +228,7 @@ struct PlayerDetailView: View {
}
}
}
.onChange(of: player.hasArrived) {
.onChange(of: [player.hasArrived, player.captain, player.coach]) {
_save()
}
.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)
Section {
ForEach(array) { player in
let index : Int? = nil
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)
}
}
ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
.onDelete { indexSet in
for index in indexSet {
@ -489,103 +394,7 @@ struct MySearchView: View {
} else {
Section {
ForEach(players, id: \.self) { player in
let index : Int? = nil
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)
}
}
ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
} header: {
if players.isEmpty == false {
@ -601,106 +410,10 @@ struct MySearchView: View {
if searchViewModel.allowSingleSelection {
Section {
ForEach(players) { player in
let index : Int? = nil
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
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)
}
}
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
.buttonStyle(.plain)
}
@ -712,105 +425,9 @@ struct MySearchView: View {
.id(UUID())
} else {
Section {
ForEach(players.indices, id: \.self) { playerIndex in
let player = players[playerIndex]
let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil
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)
}
}
ForEach(players.indices, id: \.self) { index in
let player = players[index]
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
} header: {
if players.isEmpty == false {
@ -821,207 +438,19 @@ struct MySearchView: View {
}
} else {
Section {
ForEach(players.indices, id: \.self) { playerIndex in
let player = players[playerIndex]
let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil
ForEach(players.indices, id: \.self) { index in
let player = players[index]
if searchViewModel.allowSingleSelection {
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
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)
}
}
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
} else {
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)
}
}
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
}
} header: {

@ -22,6 +22,9 @@ struct EditingTeamView: View {
@State private var registrationDate : Date
@State private var name: String
@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> {
Binding {
@ -43,14 +46,35 @@ struct EditingTeamView: View {
_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 {
List {
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
}
TeamDetailView(team: team)
} header: {
if team.hasRegisteredOnline() {
Text("Inscription en ligne")
} else {
Text("Inscription par vous-même")
}
} footer: {
HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData())
@ -64,6 +88,7 @@ struct EditingTeamView: View {
}
}
}
.headerProminence(.increased)
Section {
DatePicker(selection: $registrationDate) {
@ -95,15 +120,10 @@ struct EditingTeamView: View {
Toggle(isOn: .init(get: {
return team.wildCardBracket
}, set: { value in
team.resetPositions()
team.wildCardGroupStage = false
team.walkOut = false
_resetTeam()
team.wildCardBracket = value
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_save()
})) {
Text("Wildcard Tableau")
}
@ -111,15 +131,9 @@ struct EditingTeamView: View {
Toggle(isOn: .init(get: {
return team.wildCardGroupStage
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.walkOut = false
_resetTeam()
team.wildCardGroupStage = value
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_save()
})) {
Text("Wildcard Poule")
}
@ -127,15 +141,10 @@ struct EditingTeamView: View {
Toggle(isOn: .init(get: {
return team.walkOut
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
_resetTeam()
team.walkOut = value
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_save()
})) {
Text("Forfait")
}
@ -193,6 +202,7 @@ struct EditingTeamView: View {
Section {
RowButtonView("Effacer l'équipe", role: .destructive, systemImage: "trash") {
_resetTeam()
team.deleteTeamScores()
do {
try tournamentStore?.teamRegistrations.delete(instance: team)
@ -201,8 +211,37 @@ struct EditingTeamView: View {
}
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)
.toolbar(content: {
if focusedField != nil {
@ -312,7 +351,7 @@ struct EditingTeamView: View {
private var hasArrived: Binding<Bool> {
Binding {
team.unsortedPlayers().allSatisfy({ $0.hasArrived })
team.isHere()
} set: { hasArrived in
team.unsortedPlayers().forEach {
$0.hasArrived = hasArrived
@ -323,14 +362,16 @@ struct EditingTeamView: View {
Logger.error(error)
}
}
}
private func _save() {
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_checkOnlineRegistrationWarning()
}
private var _networkErrorMessage: String {

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

@ -118,8 +118,14 @@ struct TeamRowView: View {
var body: some View {
ForEach(team.players()) { player in
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) {
await _deleteTeams()
}

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

@ -19,7 +19,7 @@ struct BroadcastView: View {
let filter = CIFilter.qrCodeGenerator()
@State private var urlToShow: String?
@State private var tvMode: Bool = false
@State private var pageLink: PageLink = .matches
@State private var pageLink: PageLink = .info
let createAccountTip = CreateAccountTip()
let tournamentPublishingTip = TournamentPublishingTip()
@ -269,7 +269,7 @@ struct BroadcastView: View {
ToolbarItem(placement: .topBarTrailing) {
Menu {
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) {
ForEach(links) { pageLink in
Text(pageLink.localizedLabel()).tag(pageLink)

@ -81,6 +81,7 @@ struct InscriptionInfoView: View {
DisclosureGroup {
ForEach(callDateIssue) { team in
TeamCallView(team: team)
.environment(tournament)
}
} label: {
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
@State private var tournamentName: String = ""
@State private var tournamentInformation: String = ""
@State private var entryFee: Double? = nil
@State private var confirmationRequired: Bool = false
@State private var presentConfirmation: Bool = false
@ -24,23 +25,13 @@ struct TournamentGeneralSettingsView: View {
self.tournament = tournament
_loserBracketMode = .init(wrappedValue: tournament.loserBracketMode)
_tournamentName = State(wrappedValue: tournament.name ?? "")
_tournamentInformation = State(wrappedValue: tournament.information ?? "")
_entryFee = State(wrappedValue: tournament.entryFee)
}
var body: some View {
@Bindable var tournament = tournament
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 {
TournamentDatePickerView()
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.")
}
if tournament.onlineRegistrationCanBeEnabled() {
Section {
TournamentLevelPickerView()
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 {
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 {
@ -125,7 +169,7 @@ struct TournamentGeneralSettingsView: View {
if focusedField == ._entryFee {
if tournament.isFree() {
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
tournament.entryFee = priceTag
focusedField = nil
@ -145,12 +189,19 @@ struct TournamentGeneralSettingsView: View {
Spacer()
Button("Valider") {
if focusedField == ._name {
let tournamentName = tournamentName.prefixTrimmed(200)
let tournamentName = tournamentName.prefixMultilineTrimmed(200)
if tournamentName.isEmpty {
tournament.name = nil
} else {
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 {
tournament.entryFee = entryFee
}
@ -167,27 +218,12 @@ struct TournamentGeneralSettingsView: View {
.onChange(of: tournament.entryFee) {
_save()
}
.onChange(of: tournament.name) {
.onChange(of: [tournament.name, tournament.information]) {
_save()
}
.onChange(of: tournament.dayDuration) {
_save()
}
.onChange(of: [
tournament.federalCategory,
]) {
_save()
}
.onChange(of: [
tournament.federalLevelCategory,
]) {
_save()
}
.onChange(of: [
tournament.federalAgeCategory,
]) {
_save()
}
.onChange(of: [
tournament.groupStageSortMode,
]) {

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

@ -43,30 +43,30 @@ struct UpdateSourceRankDateView: View {
Task {
do {
try await tournament.updateRank(to: currentRankSourceDate)
try await MainActor.run {
let unsortedPlayers = tournament.unsortedPlayers()
tournament.unsortedPlayers().forEach { player in
player.setComputedRank(in: tournament)
}
try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers())
try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
tournament.unsortedTeams().forEach { team in
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: tournament.unsortedTeams())
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
try dataStore.tournaments.addOrUpdate(instance: tournament)
updatingRank = false
confirmUpdateRank = false
}
} catch {
Logger.error(error)
}
updatingRank = false
confirmUpdateRank = false
}
}.disabled(updatingRank)

@ -23,6 +23,7 @@ let teamsExportTip = TeamsExportTip()
struct InscriptionManagerView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@Environment(\.dismiss) var dismiss
@ -49,6 +50,10 @@ struct InscriptionManagerView: View {
@State private var compactMode: Bool = true
@State private var pasteString: String?
@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? {
return self.tournament.tournamentStore
@ -72,6 +77,8 @@ struct InscriptionManagerView: View {
enum FilterMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case all
case registeredLocally
case registeredOnline
case walkOut
case waiting
case bracket
@ -88,6 +95,10 @@ struct InscriptionManagerView: View {
return "Vous n'avez aucune wildcard en poule."
case .all:
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:
return "Vous n'avez aucune équipe forfait."
case .waiting:
@ -109,6 +120,10 @@ struct InscriptionManagerView: View {
return "Aucune wildcard en poule"
case .all:
return "Aucune équipe inscrite"
case .registeredLocally:
return "Aucune équipe inscrite par vous-même"
case .registeredOnline:
return "Aucune équipe inscrite en ligne"
case .walkOut:
return "Aucune équipe forfait"
case .waiting:
@ -130,6 +145,10 @@ struct InscriptionManagerView: View {
return displayStyle == .wide ? "Wildcard Poule" : "wc poule"
case .all:
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:
return displayStyle == .wide ? "En Tableau" : "tableau"
case .groupStage:
@ -144,6 +163,7 @@ struct InscriptionManagerView: View {
}
}
init(tournament: Tournament) {
self.tournament = tournament
_currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate)
@ -225,10 +245,23 @@ struct InscriptionManagerView: View {
RowButtonView("Importer un fichier") {
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 {
_setHash()
}
@ -339,6 +372,7 @@ struct InscriptionManagerView: View {
}
}
if tournament.isAnimation() == false {
if tournament.inscriptionClosed() == false {
Divider()
@ -346,11 +380,13 @@ struct InscriptionManagerView: View {
Section {
Button("+1 en tableau") {
tournament.addWildCard(1, .bracket)
_setHash()
}
if tournament.groupStageCount > 0 {
Button("+1 en poules") {
tournament.addWildCard(1, .groupStage)
_setHash()
}
}
} header: {
@ -359,6 +395,7 @@ struct InscriptionManagerView: View {
Button("Bloquer une place") {
tournament.addEmptyTeamRegistration(1)
_setHash()
}
Divider()
@ -477,6 +514,10 @@ struct InscriptionManagerView: View {
teams = teams.filter({ $0.inGroupStage() })
case .notImported:
teams = teams.filter({ $0.isImported() == false })
case .registeredLocally:
teams = teams.filter({ $0.hasRegisteredOnline() == false })
case .registeredOnline:
teams = teams.filter({ $0.hasRegisteredOnline() == true })
default:
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 {
List {
let selectedSortedTeams = tournament.selectedSortedTeams()
@ -529,11 +609,15 @@ struct InscriptionManagerView: View {
}
}
let isImported = teams.anySatisfy({ $0.isImported() })
if teams.isEmpty == false {
if compactMode {
Section {
ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams)
let color: Color? = isImported ? (team.unrankedOrUnknown() ? .logoRed : (team.isImported() == false ? .beige : nil)) : nil
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
@ -541,9 +625,11 @@ struct InscriptionManagerView: View {
TeamRowView(team: team)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
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: {
if filterMode == .all && walkoutTeams.isEmpty == false {
@ -551,6 +637,10 @@ struct InscriptionManagerView: View {
} else {
Text("\(teams.count.formatted()) équipe\(teams.count.pluralSuffix)")
}
} footer: {
FooterButtonView("Légende des codes couleurs") {
showLegendView = true
}
}
.headerProminence(.increased)
} else {
@ -578,9 +668,13 @@ struct InscriptionManagerView: View {
}
}
}
.id(refreshStatus)
.searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites"))
.keyboardType(.alphabet)
.autocorrectionDisabled()
.sheet(isPresented: $showLegendView) {
InscriptionLegendView()
}
}
@ViewBuilder
@ -662,6 +756,12 @@ struct InscriptionManagerView: View {
case .notImported:
let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count)
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 {
NavigationLink {
InscriptionInfoView(tournament: tournament)
.environment(tournament)
} label: {
LabeledContent {
if let registrationIssues {
@ -738,6 +839,34 @@ struct InscriptionManagerView: View {
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: {
HStack {
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()
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
}
}
Section {
LabeledContent {
StepperView(count: $teamCount, minimum: 4, maximum: 128)
StepperView(count: $teamCount, minimum: 4, maximum: 128) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Nombre d'équipes")
}
LabeledContent {
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages)
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Nombre de poules")
}
@ -99,21 +108,33 @@ struct TableStructureView: View {
if (teamCount / groupStageCount) > 1 {
Section {
LabeledContent {
StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount))
StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount)) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Équipes par poule")
}
if structurePreset != .doubleGroupStage {
LabeledContent {
StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1))
StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule")
}
if qualifiedPerGroupStage < teamsPerGroupStage - 1 {
LabeledContent {
StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified)
StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) {
} submitFollowUpAction: {
_verifyValueIntegrity()
}
} label: {
Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires")
Text(moreQualifiedLabel)
@ -223,7 +244,7 @@ struct TableStructureView: View {
}
}
if structurePreset.hasWildcards() {
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
Section {
Toggle("Avec wildcards", isOn: $buildWildcards)
} footer: {
@ -310,14 +331,9 @@ struct TableStructureView: View {
updatedElements.insert(.groupStageAdditionalQualified)
} else {
updatedElements.remove(.groupStageAdditionalQualified)
} }
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Confirmer") {
stepperFieldIsFocused = false
_verifyValueIntegrity()
}
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial {
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 {
List {
@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
Toggle(isOn: $tournament.publishRankings) {
if calculating {
ProgressView("Calcul en cours")
} else {
Text("Publier sur Padel Club")
if let url = tournament.shareURL(.rankings) {
Link(destination: url) {
Text("Accéder à la page")
}
}
}
.onChange(of: tournament.publishRankings) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
.disabled(calculating)
} footer: {
if let url = tournament.shareURL(.rankings) {
Link(destination: url) {
Text("Voir la page des classements sur Padel Club")
}
}
}
@ -88,6 +95,8 @@ struct TournamentRankView: View {
team.finalRanking = nil
team.pointsEarned = nil
}
tournament.publishRankings = false
_save()
}
}
@ -107,6 +116,13 @@ struct TournamentRankView: View {
}
}
.id(calculating)
.onChange(of: tournament.publishRankings) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
.alert("Position", isPresented: isEditingTeam) {
if let selectedTeam {
@Bindable var team = selectedTeam
@ -131,16 +147,12 @@ struct TournamentRankView: View {
}
}
}
.overlay(content: {
if calculating {
ProgressView()
}
})
.onAppear {
let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
if rankingPublished == false {
Task {
await _calculateRankings()
tournament.publishRankings = true
}
}
}

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

@ -15,6 +15,7 @@ struct TournamentCellView: View {
let tournament: FederalTournamentHolder
// let color: Color = .black
var displayStyle: DisplayStyle = .wide
var shouldTournamentBeOver: Bool = false
var event: Event? {
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 tournament.isCanceled {
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 {
Text(teamCount.formatted())
}
@ -147,12 +149,17 @@ struct TournamentCellView: View {
Text(tournament.durationLabel())
}
Spacer()
if let tournament = tournament as? Tournament, tournament.isCanceled == false, let teamCount {
if let tournament = tournament as? Tournament, tournament.isCanceled == false {
if shouldTournamentBeOver {
Text("à clôturer ?")
.foregroundStyle(.logoRed)
} else if let teamCount {
let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted()
let word = hasStarted ? "équipe" : "inscription"
Text(word + teamCount.pluralSuffix)
}
}
}
} else {
Text(build.category.localizedLabel())
Text(build.age.localizedFederalAgeLabel())
@ -178,7 +185,7 @@ struct TournamentCellView: View {
newTournament.federalTournamentAge = build.age
newTournament.dayDuration = federalTournament.dayDuration
newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9)
newTournament.setupFederalSettings()
newTournament.setupFederalSettings(fromEvent: event)
do {
try dataStore.tournaments.addOrUpdate(instance: newTournament)
} catch {

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

@ -33,7 +33,7 @@ struct SubscriptionInfoView: View {
struct FreeTournamentTip: Tip {
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? {

@ -14,10 +14,14 @@ struct TournamentView: View {
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@State var tournament: Tournament
@State private var showMoreInfos: Bool = false
var presentationContext: PresentationContext = .agenda
let tournamentSelectionTip: TournamentSelectionTip = TournamentSelectionTip()
let tournamentRunningTip: TournamentRunningTip = TournamentRunningTip()
let onlineRegistrationTip: OnlineRegistrationTip = OnlineRegistrationTip()
let shouldTournamentBeOverTip: ShouldTournamentBeOverTip = ShouldTournamentBeOverTip()
var selectedTournamentId: Binding<String> { Binding(
get: { tournament.id },
@ -59,17 +63,57 @@ struct TournamentView: View {
}
}
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 {
TournamentInscriptionView(tournament: tournament)
}
TournamentInitView(tournament: tournament)
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 {
TournamentInscriptionView(tournament: tournament)
}
TournamentBuildView(tournament: tournament)
TournamentInitView(tournament: tournament)
case .running:
if tournament.shouldTournamentBeOver() {
Section {
TipView(shouldTournamentBeOverTip) { actions in
navigation.path.append(Screen.stateSettings)
}
.tipStyle(tint: .logoRed, asSection: true)
}
}
TournamentBuildView(tournament: tournament)
TournamentRunningView(tournament: tournament)
case .finished:
@ -77,6 +121,9 @@ struct TournamentView: View {
TournamentRunningView(tournament: tournament)
}
}
.sheet(isPresented: $showMoreInfos) {
RegistrationInfoSheetView()
}
}
.environment(tournament)
.id(tournament.id)
@ -115,6 +162,9 @@ struct TournamentView: View {
ShareModelView(instance: tournament)
case .restingTime:
TeamRestingView()
case .stateSettings:
TournamentStatusView(tournament: tournament)
}
}
.environment(tournament)
@ -189,6 +239,23 @@ struct TournamentView: View {
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) {
Label("Publication", systemImage: "airplayvideo")
}
@ -203,9 +270,7 @@ struct TournamentView: View {
Divider()
NavigationLink {
TournamentStatusView(tournament: tournament)
} label: {
NavigationLink(value: Screen.stateSettings) {
Text("Gestion du tournoi")
Text("Annuler, supprimer ou terminer le tournoi")
}

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

@ -11,12 +11,12 @@ import LeStorage
final class ServerDataTests: XCTestCase {
let username: String = "test"
let username: String = "UserDataTests"
let password: String = "MyPass1234--"
override func setUpWithError() throws {
// 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 {
do {
try await self.login()
@ -71,7 +71,7 @@ final class ServerDataTests: XCTestCase {
func testLogin() async throws {
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 {
@ -104,7 +104,7 @@ final class ServerDataTests: XCTestCase {
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)
assert(t.lastUpdate.formatted() == tournament.lastUpdate.formatted())
@ -148,6 +148,11 @@ final class ServerDataTests: XCTestCase {
assert(t.loserBracketMode == tournament.loserBracketMode)
assert(t.initialSeedCount == tournament.initialSeedCount)
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 {
@ -158,7 +163,7 @@ final class ServerDataTests: XCTestCase {
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"
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.confirmationDate?.formatted() == teamRegistration.confirmationDate?.formatted())
assert(tr.qualified == teamRegistration.qualified)
assert(tr.finalRanking == teamRegistration.finalRanking)
assert(tr.pointsEarned == teamRegistration.pointsEarned)
}
func testPlayerRegistration() async throws {
@ -250,8 +256,9 @@ final class ServerDataTests: XCTestCase {
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"
let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration)
assert(pr.storeId == playerRegistration.storeId)
@ -273,6 +280,9 @@ final class ServerDataTests: XCTestCase {
assert(pr.computedRank == playerRegistration.computedRank)
assert(pr.source == playerRegistration.source)
assert(pr.hasArrived == playerRegistration.hasArrived)
assert(pr.captain == playerRegistration.captain)
assert(pr.coach == playerRegistration.coach)
assert(pr.registeredOnline == playerRegistration.registeredOnline)
}
@ -286,7 +296,7 @@ final class ServerDataTests: XCTestCase {
let rounds: [Round] = try await StoreCenter.main.service().get()
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"
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 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)

@ -16,7 +16,7 @@ final class TokenExemptionTests: XCTestCase {
let password: String = "MyPass1234--"
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()
}
@ -49,8 +49,8 @@ final class TokenExemptionTests: XCTestCase {
}
func login() async throws -> User {
let user: User = try await StoreCenter.main.service().login(username: self.username, password: self.password)
func login() async throws -> CustomUser {
let user: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password)
return user
}

@ -11,11 +11,11 @@ import LeStorage
final class UserDataTests: XCTestCase {
let username: String = "test"
let username: String = "UserDataTests"
let password: String = "MyPass1234--"
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 {
@ -24,7 +24,11 @@ final class UserDataTests: XCTestCase {
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)
assert(user.username == userCreationForm.username)
@ -41,6 +45,10 @@ final class UserDataTests: XCTestCase {
return user
}
func testLogin() async throws {
let _ = try await self.login()
}
func testUserUpdate() async throws {
let user = try await self.login()

Loading…
Cancel
Save