diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index ba8c6f5..5200355 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -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 = ""; }; C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusView.swift; sourceTree = ""; }; C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = ""; }; + C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPaymentType.swift; sourceTree = ""; }; C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = ""; }; C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = ""; }; @@ -1208,9 +1227,12 @@ FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = ""; }; FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + FF3A73F22D37C34C007E3032 /* RegistrationInfoSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationInfoSheetView.swift; sourceTree = ""; }; + FF3A74312D37DCF2007E3032 /* InscriptionLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionLegendView.swift; sourceTree = ""; }; FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduler.swift; sourceTree = ""; }; FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = ""; }; FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestination.swift; sourceTree = ""; }; + FF4623CA2D1340D200CB57B5 /* TournamentCategorySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCategorySettingsView.swift; sourceTree = ""; }; FF4AB6B42B9248200002987F /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = ""; }; @@ -1333,6 +1355,8 @@ FFBF41812BF73EB3001B24CB /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = ""; }; FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTournamentsView.swift; sourceTree = ""; }; FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSettingsView.swift; sourceTree = ""; }; + FFBFC3902CEE3A0E000EBD8D /* RegistrationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationSetupView.swift; sourceTree = ""; }; + FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateMenuView.swift; sourceTree = ""; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 = ""; diff --git a/PadelClub/Assets.xcassets/logoRed.colorset/Contents.json b/PadelClub/Assets.xcassets/logoRed.colorset/Contents.json index ded2ed9..7c05d3f 100644 --- a/PadelClub/Assets.xcassets/logoRed.colorset/Contents.json +++ b/PadelClub/Assets.xcassets/logoRed.colorset/Contents.json @@ -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" diff --git a/PadelClub/Assets.xcassets/logoYellow.colorset/Contents.json b/PadelClub/Assets.xcassets/logoYellow.colorset/Contents.json index afbfa83..93a4a81 100644 --- a/PadelClub/Assets.xcassets/logoYellow.colorset/Contents.json +++ b/PadelClub/Assets.xcassets/logoYellow.colorset/Contents.json @@ -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" diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index a8b2780..d5a6784 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -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 { diff --git a/PadelClub/Data/Gen/BaseDrawLog.swift b/PadelClub/Data/Gen/BaseDrawLog.swift index fc0cfba..b339153 100644 --- a/PadelClub/Data/Gen/BaseDrawLog.swift +++ b/PadelClub/Data/Gen/BaseDrawLog.swift @@ -93,4 +93,4 @@ class BaseDrawLog: SyncedModelObject, SyncedStorable { ] } -} +} \ No newline at end of file diff --git a/PadelClub/Data/Gen/BasePlayerRegistration.swift b/PadelClub/Data/Gen/BasePlayerRegistration.swift index 594aee1..6775fa1 100644 --- a/PadelClub/Data/Gen/BasePlayerRegistration.swift +++ b/PadelClub/Data/Gen/BasePlayerRegistration.swift @@ -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] { diff --git a/PadelClub/Data/Gen/BaseTournament.swift b/PadelClub/Data/Gen/BaseTournament.swift index 0c50107..c397195 100644 --- a/PadelClub/Data/Gen/BaseTournament.swift +++ b/PadelClub/Data/Gen/BaseTournament.swift @@ -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) 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] { diff --git a/PadelClub/Data/Gen/PlayerRegistration.json b/PadelClub/Data/Gen/PlayerRegistration.json index 77302ef..1f5eb98 100644 --- a/PadelClub/Data/Gen/PlayerRegistration.json +++ b/PadelClub/Data/Gen/PlayerRegistration.json @@ -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" } ] } diff --git a/PadelClub/Data/Gen/Tournament.json b/PadelClub/Data/Gen/Tournament.json index bbec383..2c752ea 100644 --- a/PadelClub/Data/Gen/Tournament.json +++ b/PadelClub/Data/Gen/Tournament.json @@ -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 } ] } diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 3542da6..694ff47 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -207,6 +207,8 @@ final class GroupStage: BaseGroupStage, SideStorable { tournament.endDate = Date() DataStore.shared.tournaments.addOrUpdate(instance: tournament) } + + tournament.updateTournamentState() } } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 385913f..055b719 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -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 { diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index e7ad809..11e4705 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -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 - } + return $0.groupStageObject!.index < $1.groupStageObject!.index } else { return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 } diff --git a/PadelClub/Data/PlayerPaymentType.swift b/PadelClub/Data/PlayerPaymentType.swift new file mode 100644 index 0000000..4ce8325 --- /dev/null +++ b/PadelClub/Data/PlayerPaymentType.swift @@ -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" + } + } +} diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index e6713b8..a9c1f83 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -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 + + 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: PlayerDataSource? = nil, hasArrived: Bool = false) { + init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false) { super.init() 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" - } - } -} diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index f0514d1..4469b28 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -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 { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 0acad7f..93007fd 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -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,13 +435,35 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { func updatePlayers(_ players: Set, 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) 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? { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 70f69b6..a05d5c0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -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 - self.isPrivate = Guard.main.purchasedTransactions.isEmpty + 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,14 +1096,16 @@ defer { groupStages.forEach { groupStage in let groupStageTeams = groupStage.teams(true) for (index, team) in groupStageTeams.enumerated() { - if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { - let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) - - let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0) - if let existingTeams = teams[_index] { - teams[_index] = existingTeams + [team.id] - } else { - teams[_index] = [team.id] + if groupStage.hasEnded() { + if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { + let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) + + let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0) + if let existingTeams = teams[_index] { + teams[_index] = existingTeams + [team.id] + } else { + teams[_index] = [team.id] + } } } } @@ -1121,7 +1136,20 @@ defer { } } - tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + do { + try self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + } catch { + Logger.error(error) + } + + if self.publishRankings == false { + self.publishRankings = true + do { + try DataStore.shared.tournaments.addOrUpdate(instance: self) + } catch { + Logger.error(error) + } + } return rankings } @@ -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 + + // Fetch current month data only once + let monthData = currentMonthData() - 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) + + 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 } + guard !dataURLs.isEmpty else { return } // Early return if no files found + let sources = dataURLs.map { CSVParser(url: $0) } - - try await unsortedPlayers().concurrentForEach { player in - try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 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,10 +2017,8 @@ defer { func updateTournamentState() { Task { - if hasEnded() { - let fr = await finalRanking() - _ = await setRankings(finalRanks: fr) - } + let fr = await finalRanking() + _ = await setRankings(finalRanks: fr) } } @@ -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) } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index de590b3..8214bb7 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -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() + } } diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 4655383..d1e895b 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -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) } diff --git a/PadelClub/InscriptionLegendView.swift b/PadelClub/InscriptionLegendView.swift new file mode 100644 index 0000000..031e206 --- /dev/null +++ b/PadelClub/InscriptionLegendView.swift @@ -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() + }) + } + } +} diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 3482b96..20c2389 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -99,7 +99,7 @@ print("Running in Release mode") } .task { - //try? Tips.resetDatastore() + try? Tips.resetDatastore() try? Tips.configure([ .displayFrequency(.immediate), diff --git a/PadelClub/RegistrationInfoSheetView.swift b/PadelClub/RegistrationInfoSheetView.swift new file mode 100644 index 0000000..a6dd9a4 --- /dev/null +++ b/PadelClub/RegistrationInfoSheetView.swift @@ -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 été 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") + } + } +} diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index 01ffa69..bc87eb6 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -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,32 +41,38 @@ 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 { - let federalPlayers = readCSV(inputFile: url) - let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements + 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 + + let federalPlayersDict = Dictionary(uniqueKeysWithValues: federalPlayers.map { ($0.license, $0) }) + + for (license, importedPlayer) in playersLeft { + guard let federalPlayer = federalPlayersDict[license] else { continue } - playersLeft.forEach { importedPlayer in - if let federalPlayer = federalPlayers.first(where: { $0.license == importedPlayer.license }) { - var lastName = federalPlayer.lastName - lastName.replace(characters: replacementsCharacters) - var firstName = federalPlayer.firstName - firstName.replace(characters: replacementsCharacters) - importedPlayer.lastName = lastName.trimmed.uppercased() - importedPlayer.firstName = firstName.trimmed.capitalized - } - } + var lastName = federalPlayer.lastName + 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 { guard let license = license?.strippedLicense else { return false @@ -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,+) diff --git a/PadelClub/Utils/LocationManager.swift b/PadelClub/Utils/LocationManager.swift index 0af0e36..afd5e3f 100644 --- a/PadelClub/Utils/LocationManager.swift +++ b/PadelClub/Utils/LocationManager.swift @@ -37,6 +37,8 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { func requestLocation() { lastError = nil manager.requestLocation() + city = nil + location = nil requestStarted = true } diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 4b21a3d..ff9b1d8 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -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 { diff --git a/PadelClub/Utils/SourceFileManager.swift b/PadelClub/Utils/SourceFileManager.swift index 436f30b..ffa4731 100644 --- a/PadelClub/Utils/SourceFileManager.swift +++ b/PadelClub/Utils/SourceFileManager.swift @@ -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" diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index ea69ae0..c44c9b3 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -21,7 +21,7 @@ struct PadelBeachExportTip: Tip { var image: Image? { Image(systemName: "square.and.arrow.up") } - + var actions: [Action] { Action(id: "more-info-export", title: "En savoir plus") Action(id: "beach-padel", title: "beach-padel.app.fft.fr") @@ -42,7 +42,7 @@ struct PadelBeachImportTip: Tip { var image: Image? { Image(systemName: "square.and.arrow.down") } - + var actions: [Action] { Action(id: "more-info-import", title: "Importer le fichier excel beach-padel") } @@ -61,8 +61,8 @@ struct GenerateLoserBracketTip: Tip { var image: Image? { nil } - - + + var actions: [Action] { Action(id: "generate-loser-bracket", title: "Générer les matchs de classements") } @@ -83,7 +83,7 @@ struct TeamChampionshipTip: Tip { var image: Image? { Image(systemName: "person.3") } - + var actions: [Action] { Action(id: "list-manager", title: "Ouvrir le gestionnaire d'équipe") } @@ -104,7 +104,7 @@ struct TeamChampionshipMainScreenTip: Tip { var image: Image? { Image(systemName: "arrow.uturn.backward") } - + var actions: [Action] { Action(id: "set-list-manager-main", title: "Afficher sur l'écran principal") } @@ -193,7 +193,7 @@ struct InscriptionManagerWomanRankTip: Tip { var image: Image? { Image(systemName: "figure.dress.line.vertical.figure") } - + var title: Text { Text("Rang d'une joueuse dans un tournoi messieurs") } @@ -213,7 +213,7 @@ struct InscriptionManagerRankUpdateTip: Tip { var message: Text? { Text("Padel Club vous permet de mettre à jour le classement des équipes inscrites. Si vous avez clôturé les inscriptions, la mise à jour du classement ne modifie pas la phase d'intégration de l'équipe, poule ou tableau final. Vous pouvez manuellement mettre à jour cette option.") } - + var image: Image? { Image(systemName: "list.number") } @@ -232,7 +232,7 @@ struct SharePictureTip: Tip { var message: Text? { Text("Lors d'un partage d'une photo, le texte est disponible dans le presse-papier du téléphone") } - + var image: Image? { Image(systemName: "photo.badge.checkmark.fill") } @@ -246,7 +246,7 @@ struct NewRankDataAvailableTip: Tip { var message: Text? { Text("Padel Club récupère toutes les données publique provenant de la FFT. L'importation de ce nouveau classement peut prendre plusieurs dizaines de secondes.") } - + var image: Image? { Image(systemName: "exclamationmark.icloud") } @@ -266,7 +266,7 @@ struct ClubSearchTip: Tip { var message: Text? { Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") } - + var image: Image? { Image(systemName: "house.and.flag.fill") } @@ -275,7 +275,7 @@ struct ClubSearchTip: Tip { Action(id: ActionKey.searchAroundMe.rawValue, title: "Chercher autour de moi") Action(id: ActionKey.searchCity.rawValue, title: "Chercher une ville") } - + enum ActionKey: String { case searchAroundMe = "search-around-me" case searchCity = "search-city" @@ -291,7 +291,7 @@ struct SlideToDeleteTip: Tip { var message: Text? { Text("Vous pouvez effacer un club en glissant votre doigt vers la gauche") } - + var image: Image? { Image(systemName: "trash") } @@ -306,7 +306,7 @@ struct MultiTournamentsEventTip: Tip { var message: Text? { Text("Padel Club permet de gérer plusieurs tournois ayant lieu en même temps. Un P100 homme et dame le même week-end par exemple.") } - + var image: Image? { Image(systemName: "trophy.circle") } @@ -320,7 +320,7 @@ struct NotFoundAreWalkOutTip: Tip { var message: Text? { Text("Si une équipe déjà présente dans votre liste d'attente n'est pas dans le fichier, elle sera mise WO") } - + var image: Image? { Image(systemName: "person.2.slash.fill") } @@ -338,7 +338,7 @@ struct TournamentPublishingTip: Tip { var message: Text? { Text("Padel Club vous permet de publier votre tournoi et rendre accessible à tous les résultats des matchs et l'évolution de l'événement. Les informations seront accessibles sur le site Padel Club.") } - + var image: Image? { Image("PadelClub_logo_fondclair_transparent") } @@ -352,7 +352,7 @@ struct TournamentTVBroadcastTip: Tip { var message: Text? { return Text("Padel Club vous propose un site spéficique à utiliser sur les écrans de votre club, présentant de manière intelligente l'évolution de votre tournoi.") } - + var image: Image? { Image(systemName: "sparkles.tv") } @@ -361,7 +361,7 @@ struct TournamentTVBroadcastTip: Tip { struct TournamentSelectionTip: Tip { @Parameter static var tournamentCount: Int? = nil - + var rules: [Rule] { [ // Define a rule based on the app state. @@ -379,7 +379,7 @@ struct TournamentSelectionTip: Tip { var message: Text? { return Text("Vous pouvez appuyer sur la barre de navigation pour accéder à un tournoi de votre événement.") } - + var image: Image? { Image(systemName: "filemenu.and.selection") } @@ -388,7 +388,7 @@ struct TournamentSelectionTip: Tip { struct TournamentRunningTip: Tip { @Parameter static var isRunning: Bool = false - + var rules: [Rule] { [ // Define a rule based on the app state. @@ -406,7 +406,7 @@ struct TournamentRunningTip: Tip { var message: Text? { return Text("Le tournoi a commencé, les options utiles surtout à sa préparation sont maintenant accessibles dans le menu en haut à droite.") } - + var image: Image? { Image(systemName: "ellipsis.circle") } @@ -421,18 +421,18 @@ struct CreateAccountTip: Tip { let message = "Un compte est nécessaire pour publier le tournoi sur [Padel Club](\(URLs.main.rawValue)) et profiter de toutes les pages du site, comme le mode TV pour transformer l'expérience de vos tournois !" return Text(.init(message)) } - + var image: Image? { Image(systemName: "person.crop.circle") } - + var actions: [Action] { Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte") //todo //Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus") Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Voir le site Padel Club") } - + enum ActionKey: String { case createAccount = "createAccount" case learnMore = "learnMore" @@ -443,7 +443,7 @@ struct CreateAccountTip: Tip { struct SlideToDeleteSeedTip: Tip { @Parameter static var seeds: Int = 0 - + var rules: [Rule] { [ // Define a rule based on the app state. @@ -461,7 +461,7 @@ struct SlideToDeleteSeedTip: Tip { var message: Text? { Text("Vous pouvez retirer une tête de série de sa position en glissant votre doigt vers la gauche") } - + var image: Image? { Image(systemName: "person.fill.xmark") } @@ -470,7 +470,7 @@ struct SlideToDeleteSeedTip: Tip { struct PrintTip: Tip { @Parameter static var seeds: Int = 0 - + var rules: [Rule] { [ // Define a rule based on the app state. @@ -480,7 +480,7 @@ struct PrintTip: Tip { } ] } - + var title: Text { Text("Coup d'oeil de votre tableau") } @@ -488,7 +488,7 @@ struct PrintTip: Tip { var message: Text? { Text("Vous pouvez avoir un aperçu de votre tableau ou l'imprimer.") } - + var image: Image? { Image(systemName: "printer") } @@ -505,9 +505,9 @@ struct PrintTip: Tip { struct BracketEditTip: Tip { @Parameter static var matchesHidden: Int = 0 - + var nextRoundName: String? - + var rules: [Rule] { [ // Define a rule based on the app state. @@ -528,14 +528,14 @@ struct BracketEditTip: Tip { let wording = nextRoundName != nil ? "en \(nextRoundName!)" : "dans la manche suivante" return Text("Padel Club a bien pris en compte \(article) tête\(Self.matchesHidden.pluralSuffix) de série positionnée\(Self.matchesHidden.pluralSuffix) \(wording). Le\(Self.matchesHidden.pluralSuffix) \(Self.matchesHidden) match\(Self.matchesHidden.pluralSuffix) inutile\(Self.matchesHidden.pluralSuffix) \(grammar) été désactivé automatiquement.") } - + var image: Image? { Image(systemName: "rectangle.slash") } } struct TeamsExportTip: Tip { - + var title: Text { Text("Exporter les paires") } @@ -543,7 +543,7 @@ struct TeamsExportTip: Tip { var message: Text? { Text("Partager les paires comme indiqué dans le guide de la compétition à J-6 avant midi.") } - + var image: Image? { Image(systemName: "square.and.arrow.up") } @@ -578,13 +578,79 @@ struct TimeSlotMoveOptionTip: Tip { } +struct PlayerTournamentSearchTip: Tip { + var title: Text { + Text("Cherchez un tournoi autour de vous !") + } + + var message: Text? { + Text("Padel Club facilite la recherche de tournois et l'inscription !") + } + + var image: Image? { + Image(systemName: "trophy.circle") + } + + var actions: [Action] { + Action(id: ActionKey.selectAction.rawValue, title: "Éssayer") + } + + enum ActionKey: String { + case selectAction = "selectAction" + } + +} + +struct OnlineRegistrationTip: Tip { + var title: Text { + Text("Inscription en ligne") + } + + var message: Text? { + Text("Facilitez les inscriptions à votre tournoi en activant l'inscription en ligne. Les joueurs pourront s'inscrire directement depuis l'application ou le site Padel Club.") + } + + var image: Image? { + Image(systemName: "person.2.crop.square.stack") + } + + var actions: [Action] { + [ + Action(id: ActionKey.more.rawValue, title: "En savoir plus"), + Action(id: ActionKey.enableOnlineRegistration.rawValue, title: "Activer dans les réglages du tournoi") + ] + } + + enum ActionKey: String { + case more = "more" + case enableOnlineRegistration = "enableOnlineRegistration" + } +} + +struct ShouldTournamentBeOverTip: Tip { + var title: Text { + Text("Clôturer le tournoi ?") + } + + var message: Text? { + Text("Le dernier match est terminé depuis plus de 2 heures. Si le tournoi a été annulé pour cause de météo vous pouvez l'indiquer comme 'Annulé' dans le menu en haut à droite, si ce n'est pas le cas, saisissez les scores manquants pour clôturer automatiquement le tournoi et publier le classement final.") + } + + var image: Image? { + Image(systemName: "clock.badge.questionmark") + } + + var actions: [Action] { + Action(id: "tournament-status", title: "Gérer le statut du tournoi") + } +} struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? var background: Color? var asSection: Bool - + func body(content: Content) -> some View { if asSection { Section { @@ -594,7 +660,7 @@ struct TipStyleModifier: ViewModifier { preparedContent(content: content) } } - + @ViewBuilder func preparedContent(content: Content) -> some View { if let background { diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index 4ec9daa..dc0afd5 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -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" diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 2ca3b2f..c841fb7 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -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)) && diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index dd88446..6bea89e 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -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 } diff --git a/PadelClub/ViewModel/Screen.swift b/PadelClub/ViewModel/Screen.swift index ab2187e..38a02b0 100644 --- a/PadelClub/ViewModel/Screen.swift +++ b/PadelClub/ViewModel/Screen.swift @@ -22,4 +22,5 @@ enum Screen: String, Codable { case print case share case restingTime + case stateSettings } diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index be8a85c..cc16ead 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -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 } diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index 6ef5bfe..2d3be78 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -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 + } } diff --git a/PadelClub/Views/Cashier/Event/EventCreationView.swift b/PadelClub/Views/Cashier/Event/EventCreationView.swift index 5ba6f87..cad9bac 100644 --- a/PadelClub/Views/Cashier/Event/EventCreationView.swift +++ b/PadelClub/Views/Cashier/Event/EventCreationView.swift @@ -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 { diff --git a/PadelClub/Views/Cashier/Event/EventTournamentsView.swift b/PadelClub/Views/Cashier/Event/EventTournamentsView.swift index 198a313..4fb75fe 100644 --- a/PadelClub/Views/Cashier/Event/EventTournamentsView.swift +++ b/PadelClub/Views/Cashier/Event/EventTournamentsView.swift @@ -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) diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index d31871e..f74d143 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -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: { diff --git a/PadelClub/Views/Components/CopyPasteButtonView.swift b/PadelClub/Views/Components/CopyPasteButtonView.swift index 0b5c976..b70b95b 100644 --- a/PadelClub/Views/Components/CopyPasteButtonView.swift +++ b/PadelClub/Views/Components/CopyPasteButtonView.swift @@ -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) + } } } } diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift index 6f741ed..0dce2d5 100644 --- a/PadelClub/Views/Components/RowButtonView.swift +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -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) } diff --git a/PadelClub/Views/Components/StepperView.swift b/PadelClub/Views/Components/StepperView.swift index 23f3108..217fce5 100644 --- a/PadelClub/Views/Components/StepperView.swift +++ b/PadelClub/Views/Components/StepperView.swift @@ -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() { diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index c98a724..d4c8d73 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -127,12 +127,28 @@ struct PlayerBlockView: View { } else { Divider().frame(width: width).overlay(Color(white: 0.9)) } - Text(string) - .font(.title3) - .frame(maxWidth: 20) - .scaledToFill() - .minimumScaleFactor(0.5) - .lineLimit(1) + + 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) + .scaledToFill() + .minimumScaleFactor(0.5) + .lineLimit(1) + } } } } else if let team { diff --git a/PadelClub/Views/Navigation/Agenda/CalendarView.swift b/PadelClub/Views/Navigation/Agenda/CalendarView.swift index 1dab0aa..f771644 100644 --- a/PadelClub/Views/Navigation/Agenda/CalendarView.swift +++ b/PadelClub/Views/Navigation/Agenda/CalendarView.swift @@ -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 { diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 7743ff5..43f2e64 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -39,7 +39,6 @@ struct EventListView: View { Text("\(count.formatted()) tournoi" + count.pluralSuffix) } } - .id(sectionIndex) .headerProminence(.increased) } } @@ -119,10 +118,11 @@ 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) - } + ShareModelView(instance: tournament) + } } .contextMenu { if tournament.hasEnded() == false { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 783359c..8ebd7ee 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -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,15 +312,34 @@ 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 { - locationManager.requestLocation() + if locationManager.manager.authorizationStatus == .notDetermined { + locationRequested = true + locationManager.manager.requestWhenInUseAuthorization() + } else if locationManager.manager.authorizationStatus == .denied { + showingSettingsAlert = true + } else { + locationRequested = true + locationManager.requestLocation() + } } .symbolVariant(.fill) .foregroundColor (Color.white) @@ -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) + } + } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 8ad7427..c92b57e 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -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,27 +229,21 @@ 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 { + if let current = monthData.last { Task { - await _checkSourceFileAvailability() - } - } else { - if let current = monthData.last { - Task { - let updated = await SourceFileManager.shared.fetchData(fromDate: mostRecentDateImported) - let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == mostRecentDateImported && $0.index == 0 }) - print("file updated", updated) - if let updated, updated == 1 { - await _startImporting(importingDate: mostRecentDateImported) - } else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() { - await _startImporting(importingDate: mostRecentDateImported) - } else if updated == 0 { - await _calculateMonthData(dataSource: current.monthKey) - } + let updated = await SourceFileManager.shared.fetchData(fromDate: mostRecentDateImported) + let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == mostRecentDateImported && $0.index == 0 }) + print("file updated", updated) + if let updated, updated == 1 { + await _startImporting(importingDate: mostRecentDateImported) + } else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() { + await _startImporting(importingDate: mostRecentDateImported) + } else if updated == 0 { + await _calculateMonthData(dataSource: current.monthKey) } } } diff --git a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift index 56e46d8..5263260 100644 --- a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -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("Retry Anonymous") { + await _retryAnonymous() + } + } + + Section { + RowButtonView("Write anonymous") { + _writeAnonymous() + } + } + 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) - - 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) - } - } + 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 } diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift index 2a283a3..f51de04 100644 --- a/PadelClub/Views/Player/Components/PlayerPopoverView.swift +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -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) } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 5ae6220..52036ae 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -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) { diff --git a/PadelClub/Views/Shared/DateMenuView.swift b/PadelClub/Views/Shared/DateMenuView.swift new file mode 100644 index 0000000..5e3ed15 --- /dev/null +++ b/PadelClub/Views/Shared/DateMenuView.swift @@ -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() + } + } +} diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index f81ae56..67b659e 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -114,5 +114,8 @@ struct ImportedPlayerView: View { } } } + .contextMenu { + CopyPasteButtonView(title: "Licence", pasteValue: player.formattedLicense()) + } } } diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 352251d..36feb3a 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -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: { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 8eaf36e..a499b15 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -22,7 +22,10 @@ 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 { Binding { sentError != nil @@ -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 { 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 { diff --git a/PadelClub/Views/Team/TeamDetailView.swift b/PadelClub/Views/Team/TeamDetailView.swift index 889341c..969731d 100644 --- a/PadelClub/Views/Team/TeamDetailView.swift +++ b/PadelClub/Views/Team/TeamDetailView.swift @@ -22,7 +22,17 @@ struct TeamDetailView: View { PlayerDetailView(player: player) .environment(tournament) } label: { - PlayerView(player: player) + VStack(alignment: .leading, spacing: 0) { + HStack { + if player.registeredOnline { + Text("inscrit en ligne") + } + Spacer() + Text(player.localizedSourceLabel()) + } + .font(.caption).foregroundStyle(.secondary) + PlayerView(player: player) + } } } } diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index f32365b..966f903 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -118,7 +118,13 @@ struct TeamRowView: View { var body: some View { ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + HStack(spacing: 4) { + if player.registeredOnline { + Image(systemName: "circle.fill").foregroundStyle(.green) + .font(.system(size: 8)) + } + Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + } } } } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 46cea98..673014c 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -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() } diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 784a928..93a3f24 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -16,7 +16,7 @@ struct AddTeamView: View { private var fetchRequest: FetchRequest private var fetchPlayers: FetchedResults { fetchRequest.wrappedValue } - var tournament: Tournament + let tournament: Tournament var cancelShouldDismiss: Bool = false enum FocusField: Hashable { case pasteField diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index 4e12408..4dcc1ba 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -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) diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index c63eba2..b83cc68 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -81,6 +81,7 @@ struct InscriptionInfoView: View { DisclosureGroup { ForEach(callDateIssue) { team in TeamCallView(team: team) + .environment(tournament) } } label: { LabeledContent { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift new file mode 100644 index 0000000..4ae0340 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift @@ -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) + } + } + +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index e9511b4..b3abcb9 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -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 { + 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 { - TournamentLevelPickerView() + ZStack { + Text(tournamentInformation).opacity(0) + Text(ContactType.defaultCustomMessage).opacity(0) + TextEditor(text: $tournamentInformation) + .keyboardType(.alphabet) + .focused($focusedField, equals: ._information) + } + .frame(maxHeight: 200) + .overlay { + if tournamentInformation.isEmpty { + Text("Texte visible dans l'onglet informations sur Padel Club.").italic() + } + } + } header: { + Text("Description du tournoi") + } footer: { + FooterButtonView("Ajouter le prix de l'inscription") { + tournamentInformation.append("\n" + tournament.entryFeeMessage) + } } Section { @@ -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, ]) { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift index 799d634..23bdad9 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift @@ -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 { diff --git a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift index 7527c1e..e39b33a 100644 --- a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift @@ -43,30 +43,30 @@ struct UpdateSourceRankDateView: View { Task { do { try await tournament.updateRank(to: currentRankSourceDate) - try await MainActor.run { - tournament.unsortedPlayers().forEach { player in - player.setComputedRank(in: tournament) - } - - try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers()) - - tournament.unsortedTeams().forEach { team in - team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory) - if forceRefreshLockWeight { - team.lockedWeight = team.weight - } - } - - try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) - try dataStore.tournaments.addOrUpdate(instance: tournament) - - updatingRank = false - confirmUpdateRank = false + let unsortedPlayers = tournament.unsortedPlayers() + tournament.unsortedPlayers().forEach { player in + player.setComputedRank(in: tournament) } + + try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) + + let unsortedTeams = tournament.unsortedTeams() + unsortedTeams.forEach { team in + team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory) + if forceRefreshLockWeight { + team.lockedWeight = team.weight + } + } + + try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams) + + try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } + updatingRank = false + confirmUpdateRank = false } }.disabled(updatingRank) diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 3f4b2c5..c09d34d 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -23,7 +23,8 @@ let teamsExportTip = TeamsExportTip() struct InscriptionManagerView: View { @EnvironmentObject var dataStore: DataStore - + @Environment(NavigationViewModel.self) var navigation: NavigationViewModel + @Environment(\.dismiss) var dismiss @Bindable var tournament: Tournament @@ -49,11 +50,15 @@ 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 } - + enum SortingMode: Int, Identifiable, CaseIterable { var id: Int { self.rawValue } case registrationDate @@ -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) { - _teamDeleteButtonView(team) + if tournament.enableOnlineRegistration == false { + _teamDeleteButtonView(team) + } } - .listRowView(isActive: true, color: team.initialRoundColor() ?? tournament.cutLabelColor(index: teamIndex, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count), hideColorVariation: true) + .listRowView(isActive: true, color: team.initialRoundColor() ?? tournament.cutLabelColor(index: teamIndex, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count), hideColorVariation: true, backgroundColor: color, alignment: .leading) } } header: { 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 { @@ -737,7 +838,35 @@ struct InscriptionManagerView: View { if let closedRegistrationDate = tournament.closedRegistrationDate { CloseDatePicker(closedRegistrationDate: closedRegistrationDate) } - + +// Button("bug fix") { +// _fixModel() +// } + + if tournament.enableOnlineRegistration { + Button { + Task { + await _refreshList() + } + } label: { + LabeledContent { + if refreshInProgress { + ProgressView() + } else if let refreshStatus { + if refreshStatus { + Image(systemName: "checkmark").foregroundStyle(.green).font(.headline) + } else { + Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline) + } + } + } label: { + Text("Récupérer les inscriptions en ligne") + if let refreshResult { + Text(refreshResult) + } + } + } + } } header: { HStack { Spacer() diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift new file mode 100644 index 0000000..ea4d552 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -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é" + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 35936f8..a43930f 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -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 { diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index e79b11d..ffc71f7 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -28,6 +28,14 @@ struct TournamentRankView: View { } } + var hideRankings: Binding { + 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) { - Text("Publier sur Padel Club") - if let url = tournament.shareURL(.rankings) { - Link(destination: url) { - Text("Accéder à la page") - } + if calculating { + ProgressView("Calcul en cours") + } else { + Text("Publier sur Padel Club") } } - .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 } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index 5dc380e..2b14cc7 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -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: diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index dd7169c..4512410 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -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,10 +149,15 @@ struct TournamentCellView: View { Text(tournament.durationLabel()) } Spacer() - if let tournament = tournament as? Tournament, tournament.isCanceled == false, let teamCount { - let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted() - let word = hasStarted ? "équipe" : "inscription" - Text(word + teamCount.pluralSuffix) + 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 { @@ -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 { diff --git a/PadelClub/Views/Tournament/Subscription/Guard.swift b/PadelClub/Views/Tournament/Subscription/Guard.swift index 0836041..d701e9c 100644 --- a/PadelClub/Views/Tournament/Subscription/Guard.swift +++ b/PadelClub/Views/Tournament/Subscription/Guard.swift @@ -19,7 +19,9 @@ import LeStorage var currentBestPurchase: Purchase? = nil var updateListenerTask: Task? = 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() diff --git a/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift b/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift index e3a21db..2354c0e 100644 --- a/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift @@ -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? { diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 12442b3..6b474f9 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -11,13 +11,17 @@ import TipKit struct TournamentView: View { @EnvironmentObject var dataStore: DataStore - + @Environment(NavigationViewModel.self) var navigation: NavigationViewModel @State var tournament: Tournament - var presentationContext: PresentationContext = .agenda + @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 { Binding( get: { tournament.id }, @@ -36,7 +40,7 @@ struct TournamentView: View { guard let lastDataSource else { return nil } return URL.importDateFormatter.date(from: lastDataSource) } - + init(tournament: Tournament, presentationContext: PresentationContext = .agenda) { self.tournament = tournament self.presentationContext = presentationContext @@ -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) @@ -128,17 +178,17 @@ struct TournamentView: View { Text(tournament.tournamentTitle(.title)).tag(tournament.id as String) } } label: { - + } - + Divider() } - + NavigationLink(value: Screen.event) { Text("Réglages de l'événement") } } - + } .toolbarBackground(.visible, for: .navigationBar) .toolbar { @@ -153,11 +203,11 @@ struct TournamentView: View { TournamentSelectionTip.tournamentCount = tournament.eventObject()?.tournaments.count } } - + if tournament.isCanceled == false { ToolbarItem(placement: .topBarTrailing) { Menu { - + #if DEBUG Button { do { @@ -169,7 +219,7 @@ struct TournamentView: View { Label("Payer le tournoi", systemImage: "dollarsign.circle.fill") } #endif - + if presentationContext == .agenda { Button { navigation.openTournamentInOrganizer(tournament) @@ -178,7 +228,7 @@ struct TournamentView: View { } Divider() } - + NavigationLink(value: Screen.event) { Text("Réglages de l'événement") } @@ -188,11 +238,28 @@ struct TournamentView: View { NavigationLink(value: Screen.structure) { 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") } - + NavigationLink(value: Screen.print) { Label("Imprimer", systemImage: "printer") } @@ -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") } diff --git a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift index 6cfc20a..02dcbc2 100644 --- a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift +++ b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift @@ -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)) } } diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 60ceaef..5a0d348 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -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,7 +280,10 @@ 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) + } func testMatch() async throws { @@ -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) diff --git a/PadelClubTests/TokenExemptionTests.swift b/PadelClubTests/TokenExemptionTests.swift index 764cd2d..7160028 100644 --- a/PadelClubTests/TokenExemptionTests.swift +++ b/PadelClubTests/TokenExemptionTests.swift @@ -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 } diff --git a/PadelClubTests/UserDataTests.swift b/PadelClubTests/UserDataTests.swift index 5d10a59..4d98ac1 100644 --- a/PadelClubTests/UserDataTests.swift +++ b/PadelClubTests/UserDataTests.swift @@ -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()