diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 55b073d..a6143cf 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4002B6D249D002A7B48 /* PadelClubApp.swift */; }; - C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4022B6D249D002A7B48 /* ContentView.swift */; }; C425D4052B6D249E002A7B48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; }; C425D4082B6D249E002A7B48 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; }; C425D4122B6D249E002A7B48 /* PadelClubTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4112B6D249E002A7B48 /* PadelClubTests.swift */; }; @@ -22,7 +21,6 @@ C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D592B6D383C00ADC637 /* Tournament.swift */; }; C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */; }; C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; }; - C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D732B72881F00ADC637 /* ClubView.swift */; }; C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D762B73789100ADC637 /* TournamentV1.swift */; }; C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D7A2B73C0F900ADC637 /* TournamentV2.swift */; }; C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D7C2B73CDC300ADC637 /* ClubV1.swift */; }; @@ -37,6 +35,18 @@ C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; }; C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB02B86375E00ADC637 /* MainUserView.swift */; }; C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; }; + FF025AD82BD0C10F00A86CF8 /* TeamHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */; }; + FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */; }; + FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */; }; + FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */; }; + FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */; }; + FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; + FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */; }; + FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */; }; + FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; + FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AEC2BD1513700A86CF8 /* AppScreen.swift */; }; + FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */; }; + FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */; }; FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */; }; FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */; }; FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; }; @@ -87,6 +97,15 @@ FF0EC5752BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5342BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv */; }; FF0EC5762BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5452BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv */; }; FF0EC5772BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */; }; + FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */; }; + FF11627D2BCF941A000C4809 /* CashierSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */; }; + FF11627F2BCF9432000C4809 /* PlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627E2BCF9432000C4809 /* PlayerListView.swift */; }; + FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162802BCF945C000C4809 /* TournamentCashierView.swift */; }; + FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; }; + FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; }; + FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; + FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; + FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -195,7 +214,7 @@ FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; - FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */; }; + FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */; }; FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; FFC83D512BB8087E00750834 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D502BB8087E00750834 /* RoundView.swift */; }; @@ -214,6 +233,8 @@ FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; }; FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; }; FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; }; + FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E02BD2A9B600A33B06 /* DateInterval.swift */; }; + FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */; }; FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; }; FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; }; FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; }; @@ -276,7 +297,6 @@ /* Begin PBXFileReference section */ C425D3FD2B6D249D002A7B48 /* PadelClub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PadelClub.app; sourceTree = BUILT_PRODUCTS_DIR; }; C425D4002B6D249D002A7B48 /* PadelClubApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubApp.swift; sourceTree = ""; }; - C425D4022B6D249D002A7B48 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; C425D4042B6D249E002A7B48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; C425D40D2B6D249E002A7B48 /* PadelClubTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PadelClubTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -293,7 +313,6 @@ C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = ""; }; C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = ""; }; - C4A47D732B72881F00ADC637 /* ClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubView.swift; sourceTree = ""; }; C4A47D762B73789100ADC637 /* TournamentV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentV1.swift; sourceTree = ""; }; C4A47D7A2B73C0F900ADC637 /* TournamentV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentV2.swift; sourceTree = ""; }; C4A47D7C2B73CDC300ADC637 /* ClubV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubV1.swift; sourceTree = ""; }; @@ -308,6 +327,18 @@ C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = ""; }; C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; + FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamHeaderView.swift; sourceTree = ""; }; + FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTeamDetailView.swift; sourceTree = ""; }; + FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterButtonView.swift; sourceTree = ""; }; + FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamWeightView.swift; sourceTree = ""; }; + FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentClubSettingsView.swift; sourceTree = ""; }; + FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentMatchFormatsSettingsView.swift; sourceTree = ""; }; + FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentGeneralSettingsView.swift; sourceTree = ""; }; + FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSettingsView.swift; sourceTree = ""; }; + FF025AE82BD1307E00A86CF8 /* MonthData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthData.swift; sourceTree = ""; }; + FF025AEC2BD1513700A86CF8 /* AppScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppScreen.swift; sourceTree = ""; }; + FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationSettingsView.swift; sourceTree = ""; }; + FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatStorageView.swift; sourceTree = ""; }; FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSexPickerView.swift; sourceTree = ""; }; FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRowView.swift; sourceTree = ""; }; FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = ""; }; @@ -358,6 +389,15 @@ FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-10-2022.csv"; sourceTree = ""; }; FF0EC54B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-11-2022.csv"; sourceTree = ""; }; FF0EC54C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-12-2022.csv"; sourceTree = ""; }; + FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageCustomizationView.swift; sourceTree = ""; }; + FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierSettingsView.swift; sourceTree = ""; }; + FF11627E2BCF9432000C4809 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = ""; }; + FF1162802BCF945C000C4809 /* TournamentCashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCashierView.swift; sourceTree = ""; }; + FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = ""; }; + FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; + FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = ""; }; + FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; + FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -465,7 +505,7 @@ FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = ""; }; - FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketView.swift; sourceTree = ""; }; + FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundView.swift; sourceTree = ""; }; FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = ""; }; FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = ""; }; FFC83D502BB8087E00750834 /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; @@ -484,6 +524,8 @@ FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = ""; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; + FFF116E02BD2A9B600A33B06 /* DateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInterval.swift; sourceTree = ""; }; + FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtAvailabilitySettingsView.swift; sourceTree = ""; }; FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = ""; }; FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = ""; }; FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = ""; }; @@ -622,6 +664,7 @@ FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */, C4A47D622B6D3D6500ADC637 /* Club.swift */, FF8F263E2BAD7D5C00650388 /* Event.swift */, + FF025AE82BD1307E00A86CF8 /* MonthData.swift */, FF1DC5522BAB354A00FD8220 /* MockData.swift */, FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FF6EC9012B94799200EA7F5A /* Coredata */, @@ -633,8 +676,6 @@ C4A47D722B72881500ADC637 /* Views */ = { isa = PBXGroup; children = ( - C425D4022B6D249D002A7B48 /* ContentView.swift */, - C4A47D732B72881F00ADC637 /* ClubView.swift */, FF39719B2B8DE04B004C4E75 /* Navigation */, FF8F26392BAD526A00650388 /* Event */, FF1DC54D2BAB34FA00FD8220 /* Club */, @@ -646,6 +687,7 @@ FF089EB92BB011EE00F0AEC7 /* Player */, FF9267FD2BCE94520080F940 /* Calling */, FFF964512BC2628600EEF017 /* Planning */, + FF11627B2BCF937F000C4809 /* Cashier */, FF3F74F72B919F96004CFE0E /* Tournament */, C4A47D882B7BBB5000ADC637 /* Subscription */, C4A47D852B7BA33F00ADC637 /* User */, @@ -697,18 +739,39 @@ C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */, FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */, FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */, + FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */, FFBF065D2BBD8040009D6715 /* MatchListView.swift */, FF967CF72BAEDF0000A9A3BD /* Labels.swift */, ); path = Components; sourceTree = ""; }; + FF025AD62BD0C0FB00A86CF8 /* Components */ = { + isa = PBXGroup; + children = ( + FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */, + FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */, + ); + path = Components; + sourceTree = ""; + }; + FF025AD92BD0C2BD00A86CF8 /* Components */ = { + isa = PBXGroup; + children = ( + FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */, + FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */, + FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */, + ); + path = Components; + sourceTree = ""; + }; FF089EB02BB001EA00F0AEC7 /* Components */ = { isa = PBXGroup; children = ( FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */, FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */, FF9267FB2BCE84870080F940 /* PlayerPayView.swift */, + FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */, ); path = Components; sourceTree = ""; @@ -717,6 +780,7 @@ isa = PBXGroup; children = ( FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */, + FF1162842BD00279000C4809 /* PlayerDetailView.swift */, FF089EB02BB001EA00F0AEC7 /* Components */, ); path = Player; @@ -771,6 +835,25 @@ path = CSV; sourceTree = ""; }; + FF11627B2BCF937F000C4809 /* Cashier */ = { + isa = PBXGroup; + children = ( + FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */, + FF9267F72BCE78C70080F940 /* CashierView.swift */, + FF11627E2BCF9432000C4809 /* PlayerListView.swift */, + FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */, + ); + path = Cashier; + sourceTree = ""; + }; + FF1162882BD0523B000C4809 /* Components */ = { + isa = PBXGroup; + children = ( + FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */, + ); + path = Components; + sourceTree = ""; + }; FF1DC54D2BAB34FA00FD8220 /* Club */ = { isa = PBXGroup; children = ( @@ -830,8 +913,7 @@ FF8F26532BAE1E4400650388 /* TableStructureView.swift */, FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */, FF9268062BCE94D90080F940 /* TournamentCallView.swift */, - FF9267F72BCE78C70080F940 /* CashierView.swift */, - FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */, + FF1162802BCF945C000C4809 /* TournamentCashierView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -852,6 +934,9 @@ children = ( FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */, FF5D0D822BB48997005CB568 /* RankCalculatorView.swift */, + FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */, + FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */, + FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */, ); path = Toolbox; sourceTree = ""; @@ -868,6 +953,7 @@ isa = PBXGroup; children = ( FF7091652B90F0B000AB08DA /* TabDestination.swift */, + FF025AEC2BD1513700A86CF8 /* AppScreen.swift */, FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */, FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */, FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */, @@ -878,6 +964,7 @@ FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */, + FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, ); path = ViewModel; sourceTree = ""; @@ -944,6 +1031,9 @@ FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */, FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */, FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */, + FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */, + FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */, + FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */, ); path = Components; sourceTree = ""; @@ -955,6 +1045,7 @@ FF9268002BCE94920080F940 /* SeedsCallingView.swift */, FF9268022BCE94A30080F940 /* GroupStageCallingView.swift */, FF9268082BCEDC2C0080F940 /* CallView.swift */, + FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */, ); path = Calling; sourceTree = ""; @@ -977,8 +1068,7 @@ FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */, FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */, FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */, - FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */, - FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */, + FF025AD92BD0C2BD00A86CF8 /* Components */, ); path = Match; sourceTree = ""; @@ -989,6 +1079,8 @@ FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */, FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */, FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, + FF1162862BD004AD000C4809 /* EditingTeamView.swift */, + FF025AD62BD0C0FB00A86CF8 /* Components */, ); path = Team; sourceTree = ""; @@ -999,7 +1091,7 @@ FFC83D4E2BB807D100750834 /* RoundsView.swift */, FFC83D502BB8087E00750834 /* RoundView.swift */, FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */, - FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */, + FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, ); path = Round; @@ -1084,8 +1176,11 @@ FFF964542BC266CF00EEF017 /* SchedulerView.swift */, FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */, FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */, + FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */, FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, + FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */, + FF1162882BD0523B000C4809 /* Components */, ); path = Planning; sourceTree = ""; @@ -1297,6 +1392,7 @@ FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */, FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, + FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */, C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */, FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */, @@ -1308,8 +1404,10 @@ FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */, + FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, + FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */, FF9268012BCE94920080F940 /* SeedsCallingView.swift in Sources */, FF9268092BCEDC2C0080F940 /* CallView.swift in Sources */, FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */, @@ -1320,6 +1418,8 @@ C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */, FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, + FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, + FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */, FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */, @@ -1335,6 +1435,8 @@ FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, + FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, + FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */, FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */, @@ -1343,14 +1445,17 @@ FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */, FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */, FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */, + FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */, FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, - C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */, + FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, + FF11627F2BCF9432000C4809 /* PlayerListView.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF967CF32BAECC0B00A9A3BD /* PlayerRegistration.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, + FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */, C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */, @@ -1361,6 +1466,7 @@ FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */, C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */, FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */, + FF025AD82BD0C10F00A86CF8 /* TeamHeaderView.swift in Sources */, FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */, FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */, FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */, @@ -1368,7 +1474,10 @@ FF8F263D2BAD627A00650388 /* TournamentConfiguratorView.swift in Sources */, FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */, FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */, + FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */, + FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */, FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */, + FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */, FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, @@ -1379,12 +1488,14 @@ FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */, FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */, FF9268032BCE94A30080F940 /* GroupStageCallingView.swift in Sources */, + FF11627D2BCF941A000C4809 /* CashierSettingsView.swift in Sources */, FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */, C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */, FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, - FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */, + FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */, + FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */, FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, @@ -1399,12 +1510,14 @@ FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, + FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, + FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, + FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, - C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, @@ -1418,6 +1531,7 @@ FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */, FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */, FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */, + FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, @@ -1454,11 +1568,13 @@ FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */, + FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */, FF5D0D852BB48997005CB568 /* RankCalculatorView.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */, FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */, + FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */, ); @@ -1623,6 +1739,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; ENABLE_PREVIEWS = YES; @@ -1654,6 +1771,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; ENABLE_PREVIEWS = YES; diff --git a/PadelClub/Data/AppSettings.swift b/PadelClub/Data/AppSettings.swift index 864ea23..48909e9 100644 --- a/PadelClub/Data/AppSettings.swift +++ b/PadelClub/Data/AppSettings.swift @@ -7,14 +7,48 @@ import Foundation import LeStorage +import SwiftUI +@Observable class AppSettings: MicroStorable { static var fileName: String { "appsettings.json" } + + var lastDataSource: String? = nil + var callMessageBody : String? = nil + var callMessageSignature: String? = nil + var callDisplayFormat: Bool = false + var callDisplayEntryFee: Bool = false + var callUseFullCustomMessage: Bool = false + var matchFormatsDefaultDuration: [MatchFormat: Int]? = nil + var bracketMatchFormatPreference: Int? + var groupStageMatchFormatPreference: Int? + var loserBracketMatchFormatPreference: Int? required init() { } -// var id: String = Store.randomId() + func saveMatchFormatsDefaultDuration(_ matchFormat: MatchFormat, estimatedDuration: Int) { + if estimatedDuration == matchFormat.defaultEstimatedDuration { + matchFormatsDefaultDuration?.removeValue(forKey: matchFormat) + } else { + matchFormatsDefaultDuration = matchFormatsDefaultDuration ?? [MatchFormat: Int]() + matchFormatsDefaultDuration?[matchFormat] = estimatedDuration + } + } + + enum CodingKeys: String, CodingKey { + case _lastDataSource = "lastDataSource" + case _callMessageBody = "callMessageBody" + case _callMessageSignature = "callMessageSignature" + case _callDisplayFormat = "callDisplayFormat" + case _callDisplayEntryFee = "callDisplayEntryFee" + case _callUseFullCustomMessage = "callUseFullCustomMessage" + case _matchFormatsDefaultDuration = "matchFormatsDefaultDuration" + case _bracketMatchFormatPreference = "bracketMatchFormatPreference" + case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" + case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" + + } } diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index 7596ec8..aca7233 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -32,7 +32,9 @@ class Club : ModelObject, Storable, Hashable { var zipCode: String? var latitude: Double? var longitude: Double? - + var courtCount: Int? + var courtNames: [String]? = nil + internal init(name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil) { self.name = name self.acronym = acronym ?? name.acronym() @@ -45,12 +47,7 @@ class Club : ModelObject, Storable, Hashable { self.longitude = longitude } - var tournaments: [Tournament] { - return [] - } - override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.tournaments) } enum CodingKeys: String, CodingKey { @@ -64,6 +61,8 @@ class Club : ModelObject, Storable, Hashable { case _zipCode = "zipCode" case _latitude = "latitude" case _longitude = "longitude" + case _courtCount = "courtCount" + case _courtNames = "courtNames" } } diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index f89dd00..4d2bf0b 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -24,8 +24,29 @@ class DataStore: ObservableObject { fileprivate(set) var playerRegistrations: StoredCollection fileprivate(set) var rounds: StoredCollection fileprivate(set) var teamScores: StoredCollection + fileprivate(set) var monthData: StoredCollection fileprivate var _userStorage: OptionalStorage = OptionalStorage(fileName: "user.json") + fileprivate var _appSettingsStorage: MicroStorage = MicroStorage() + + var appSettings: AppSettings { + _appSettingsStorage.item + } + + func updateSettings() { + _appSettingsStorage.update { settings in + settings.lastDataSource = appSettings.lastDataSource + settings.callMessageBody = appSettings.callMessageBody + settings.callDisplayFormat = appSettings.callDisplayFormat + settings.callMessageSignature = appSettings.callMessageSignature + settings.callDisplayEntryFee = appSettings.callDisplayEntryFee + settings.callUseFullCustomMessage = appSettings.callUseFullCustomMessage + settings.matchFormatsDefaultDuration = appSettings.matchFormatsDefaultDuration + settings.bracketMatchFormatPreference = appSettings.bracketMatchFormatPreference + settings.groupStageMatchFormatPreference = appSettings.groupStageMatchFormatPreference + settings.loserBracketMatchFormatPreference = appSettings.loserBracketMatchFormatPreference + } + } var user: User? { return self._userStorage.item @@ -44,16 +65,18 @@ class DataStore: ObservableObject { // store.addMigration(Migration(version: 2)) // store.addMigration(Migration(version: 3)) - self.clubs = store.registerCollection(synchronized: false, indexed: true) - self.tournaments = store.registerCollection(synchronized: false, indexed: true) - self.events = store.registerCollection(synchronized: false, indexed: true) - self.groupStages = store.registerCollection(synchronized: false, indexed: true) - self.teamScores = store.registerCollection(synchronized: false, indexed: true) - self.teamRegistrations = store.registerCollection(synchronized: false, indexed: true) - self.playerRegistrations = store.registerCollection(synchronized: false, indexed: true) - self.rounds = store.registerCollection(synchronized: false, indexed: true) - self.matches = store.registerCollection(synchronized: false, indexed: true) - + let indexed : Bool = true + self.clubs = store.registerCollection(synchronized: false, indexed: indexed) + self.tournaments = store.registerCollection(synchronized: false, indexed: indexed) + self.events = store.registerCollection(synchronized: false, indexed: indexed) + self.groupStages = store.registerCollection(synchronized: false, indexed: indexed) + self.teamScores = store.registerCollection(synchronized: false, indexed: indexed) + self.teamRegistrations = store.registerCollection(synchronized: false, indexed: indexed) + self.playerRegistrations = store.registerCollection(synchronized: false, indexed: indexed) + self.rounds = store.registerCollection(synchronized: false, indexed: indexed) + self.matches = store.registerCollection(synchronized: false, indexed: indexed) + self.monthData = store.registerCollection(synchronized: false, indexed: indexed) + NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil) } diff --git a/PadelClub/Data/Event.swift b/PadelClub/Data/Event.swift index 07cee7a..c3d7a23 100644 --- a/PadelClub/Data/Event.swift +++ b/PadelClub/Data/Event.swift @@ -22,6 +22,7 @@ class Event: ModelObject, Storable { var groupStageFormat: Int? var roundFormat: Int? var loserRoundFormat: Int? + //var timeslots ? internal init(club: String? = nil, name: String? = nil, courtCount: Int? = nil, tenupId: String? = nil, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil) { self.club = club diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 7bbed12..091421b 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -21,6 +21,8 @@ protocol PlayerHolder { var clubName: String? { get } var ligueName: String? { get } var assimilation: String? { get } + var computedAge: Int? { get } + func getAssimilatedAsMaleRank() -> Int? } extension PlayerHolder { @@ -29,8 +31,32 @@ extension PlayerHolder { } } +fileprivate extension Int { + var femaleInMaleAssimilation: Int { + self + femaleInMaleAssimilationAddition + } + + var femaleInMaleAssimilationAddition: Int { + switch self { + case 1...10: return 400 + case 11...30: return 1000 + case 31...60: return 2000 + case 61...100: return 3000 + case 101...200: return 8000 + case 201...500: return 12000 + default: + return 15000 + } + } +} extension ImportedPlayer: PlayerHolder { + func getAssimilatedAsMaleRank() -> Int? { + guard male == false else { return nil } + return getRank()?.femaleInMaleAssimilation + } + + var computedAge: Int? { nil } var tournamentPlayed: Int? { Int(tournamentCount) diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index 7e29cd4..c9391ec 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -203,6 +203,7 @@ enum TypePratique: String, Codable { case beach = "BEACH" case padel = "PADEL" case tennis = "TENNIS" + case pickle = "PICKLE" } // MARK: - CategorieTournoi diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index edca863..e17e819 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -19,6 +19,7 @@ class GroupStage: ModelObject, Storable { var size: Int var format: Int? var startDate: Date? + var name: String? var matchFormat: MatchFormat { get { @@ -136,7 +137,8 @@ class GroupStage: ModelObject, Storable { } func availableToStart() -> [Match] { - playedMatches().filter({ $0.canBeStarted() && $0.isRunning() == false }) + let runningMatches = runningMatches() + return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) } func runningMatches() -> [Match] { @@ -208,6 +210,10 @@ class GroupStage: ModelObject, Storable { Store.main.filter { $0.groupStage == self.id } } + func unsortedPlayers() -> [PlayerRegistration] { + unsortedTeams().flatMap({ $0.unsortedPlayers() }) + } + fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) @@ -221,10 +227,13 @@ class GroupStage: ModelObject, Storable { } } + func unsortedTeams() -> [TeamRegistration] { + Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } + } + func teams(_ sortedByScore: Bool = false) -> [TeamRegistration] { - let teams: [TeamRegistration] = Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } if sortedByScore { - return teams.compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in + return unsortedTeams().compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in let predicates: [TeamScoreAreInIncreasingOrder] = [ { $0.wins < $1.wins }, { $0.setDifference < $1.setDifference }, @@ -244,9 +253,18 @@ class GroupStage: ModelObject, Storable { return false }.map({ $0.team }).reversed() } else { - return teams.sorted(by: \TeamRegistration.groupStagePosition!) + return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) } } + + func updateMatchFormat(_ matchFormat: MatchFormat) { + self.matchFormat = matchFormat + let playedMatches = playedMatches() + playedMatches.forEach { match in + match.matchFormat = matchFormat + } + try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches) + } override func deleteDependencies() throws { try Store.main.deleteDependencies(items: self._matches()) @@ -261,6 +279,7 @@ extension GroupStage { case _size = "size" case _format = "format" case _startDate = "startDate" + case _name = "name" } } @@ -272,4 +291,8 @@ extension GroupStage: Selectable { func badgeValue() -> Int? { runningMatches().count } + + func badgeImage() -> Badge? { + hasEnded() ? .checkmark : nil + } } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ae76ae3..4650a0e 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -11,6 +11,7 @@ import LeStorage @Observable class Match: ModelObject, Storable { static func resourceName() -> String { "matches" } + var byeState: Bool = false var id: String = Store.randomId() var round: String? @@ -27,6 +28,7 @@ class Match: ModelObject, Storable { var name: String? var order: Int var disabled: Bool = false + var courtIndex: Int? internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) { self.round = round @@ -100,8 +102,8 @@ class Match: ModelObject, Storable { return index * 2 + teamPosition.rawValue == bracketPosition } - func estimatedEndDate() -> Date? { - let minutesToAdd = Double(matchFormat.estimatedDuration) + func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? { + let minutesToAdd = Double(matchFormat.getEstimatedDuration(additionalEstimationDuration)) return startDate?.addingTimeInterval(minutesToAdd * 60.0) } @@ -159,31 +161,99 @@ class Match: ModelObject, Storable { _toggleMatchDisableState(false) } - private func _toggleLoserMatchDisableState(_ state: Bool) { - if isLoserBracket == false { - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) - if let loserMatch = roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) { - loserMatch.disabled = state - try? DataStore.shared.matches.addOrUpdate(instance: loserMatch) - loserMatch._toggleLoserMatchDisableState(state) - } - } else { - roundObject?.loserRounds().forEach({ round in - round.handleLoserRoundState() - }) + private func _loserMatch() -> Match? { + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) + return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) + } + + func _toggleLoserMatchDisableState(_ state: Bool) { + guard let loserMatch = _loserMatch() else { return } + guard let next = _otherMatch() else { return } + loserMatch.byeState = true + if next.disabled { + loserMatch.byeState = false } + loserMatch._toggleMatchDisableState(state, forward: true) } - fileprivate func _toggleMatchDisableState(_ state: Bool) { + fileprivate func _otherMatch() -> Match? { + guard let round else { return nil } + guard index > 0 else { return nil } + let nextIndex = (index - 1) / 2 + let topMatchIndex = (nextIndex * 2) + 1 + let bottomMatchIndex = (nextIndex + 1) * 2 + let isTopMatch = topMatchIndex + 1 == index + let lookingForIndex = isTopMatch ? topMatchIndex : bottomMatchIndex + return Store.main.filter(isIncluded: { $0.round == round && $0.index == lookingForIndex }).first + } + + private func _forwardMatch(inRound round: Round) -> Match? { + guard let roundObjectNextRound = round.nextRound() else { return nil } + let nextIndex = (index - 1) / 2 + return Store.main.filter(isIncluded: { $0.round == roundObjectNextRound.id && $0.index == nextIndex }).first + } + + func _toggleForwardMatchDisableState(_ state: Bool) { + guard let roundObject else { return } + guard roundObject.loser != nil else { return } + guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } + guard let next = _otherMatch() else { return } + if next.disabled && byeState == false && next.byeState == false { + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(state, forward: true) + } else if byeState && next.byeState { + print("don't disable forward match") + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(false, forward: true) + } else { + forwardMatch.byeState = true + forwardMatch._toggleMatchDisableState(state, forward: true) + } + +// if next.disabled == false { +// forwardMatch.byeState = state +// } + + + +// +// if next.disabled == state { +// if next.byeState != byeState { +// //forwardMatch.byeState = state +// forwardMatch._toggleMatchDisableState(state) +// } else { +// forwardMatch._toggleByeState(state) +// } +// } else { +// } +// forwardMatch._toggleByeState(state) + } + + func _toggleMatchDisableState(_ state: Bool, forward: Bool = false) { + //if disabled == state { return } disabled = state + //byeState = false + //try? DataStore.shared.matches.addOrUpdate(instance: self) + _toggleLoserMatchDisableState(state) - topPreviousRoundMatch()?._toggleMatchDisableState(state) - bottomPreviousRoundMatch()?._toggleMatchDisableState(state) - try? DataStore.shared.matches.addOrUpdate(instance: self) + if forward { + _toggleForwardMatchDisableState(state) + } else { + topPreviousRoundMatch()?._toggleMatchDisableState(state) + bottomPreviousRoundMatch()?._toggleMatchDisableState(state) + } } func next() -> Match? { - Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first + Store.main.filter(isIncluded: { $0.round == round && $0.index > index }).sorted(by: \.index).first + } + + func getDuration() -> Int { + if let tournament = currentTournament() { + matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + } else { + matchFormat.getEstimatedDuration() + } } func roundTitle() -> String? { @@ -203,14 +273,14 @@ class Match: ModelObject, Storable { func topPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } return Store.main.filter { match in - match.index == topPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id + match.index == topPreviousRoundMatchIndex() && match.round != nil && match.round == roundObject.previousRound()?.id }.sorted(by: \.index).first } func bottomPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } return Store.main.filter { match in - match.index == bottomPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id + match.index == bottomPreviousRoundMatchIndex() && match.round != nil && match.round == roundObject.previousRound()?.id }.sorted(by: \.index).first } @@ -315,7 +385,7 @@ class Match: ModelObject, Storable { } } - func courtIndex() -> Int? { + func getCourtIndex() -> Int? { guard let court else { return nil } if let courtIndex = Int(court) { return courtIndex - 1 } return nil @@ -349,24 +419,26 @@ class Match: ModelObject, Storable { court = String(courtIndex) } - func canBeStarted() -> Bool { + func canBeStarted(inMatches matches: [Match]) -> Bool { let teams = teams() guard teams.count == 2 else { return false } guard hasEnded() == false else { return false } guard hasStarted() == false else { return false } - return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0) == false }) + return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0, inMatches: matches) == false }) } - func isTeamPlaying(_ team: TeamRegistration) -> Bool { - if isGroupStage() { - let isPlaying = groupStageObject?.runningMatches().filter({ $0.teams().contains(team) }).isEmpty == false - return isPlaying - } else { - //todo - return false - } + func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool { + matches.filter({ $0.teams().contains(team) }).isEmpty == false } - + + var computedStartDateForSorting: Date { + startDate ?? .distantFuture + } + + var computedEndDateForSorting: Date { + endDate ?? .distantFuture + } + func isReady() -> Bool { teams().count == 2 } @@ -534,6 +606,7 @@ class Match: ModelObject, Storable { case _index = "index" case _format = "format" case _court = "court" + case _courtIndex = "courtIndex" case _servingTeamId = "servingTeamId" case _winningTeamId = "winningTeamId" case _losingTeamId = "losingTeamId" diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 10bcd26..7cad2cb 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -35,27 +35,19 @@ extension Tournament { } static func newEmptyInstance() -> Tournament { - let lastDataSource: String? = UserDefaults.standard.string(forKey: "lastDataSource") - let lastDataSourceMaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceMaleUnranked") - let lastDataSourceFemaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceFemaleUnranked") - + let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource var _mostRecentDateAvailable: Date? { guard let lastDataSource else { return nil } return URL.importDateFormatter.date(from: lastDataSource) } - let maleUnrankedValue : Int? = lastDataSourceMaleUnranked == 0 ? nil : lastDataSourceMaleUnranked - let femaleUnrankedValue : Int? = lastDataSourceFemaleUnranked == 0 ? nil : lastDataSourceMaleUnranked let rankSourceDate = _mostRecentDateAvailable + let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil }.sorted(by: \.startDate).reversed() + let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) + let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) + let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) - //todo - /* - tournament.tournamentLevel = TournamentLevel.mostUsed(tournaments: tournaments) - tournament.tournamentCategory = TournamentCategory.mostUsed(tournaments: tournaments) - tournament.federalTournamentAge = FederalTournamentAge.mostUsed(tournaments: tournaments) - */ - - return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior, maleUnrankedValue: maleUnrankedValue, femaleUnrankedValue: femaleUnrankedValue) + return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge) } } diff --git a/PadelClub/Data/MonthData.swift b/PadelClub/Data/MonthData.swift new file mode 100644 index 0000000..0bb8d46 --- /dev/null +++ b/PadelClub/Data/MonthData.swift @@ -0,0 +1,53 @@ +// +// MonthData.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import Foundation +import SwiftUI +import LeStorage + +@Observable +class MonthData : ModelObject, Storable { + + static func resourceName() -> String { return "month-data" } + + private(set) var id: String = Store.randomId() + private(set) var monthKey: String + private(set) var creationDate: Date + + var maleUnrankedValue: Int? = nil + var femaleUnrankedValue: Int? = nil + + init(monthKey: String) { + self.monthKey = monthKey + self.creationDate = Date() + } + + static func calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { + let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) + let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false) + + await MainActor.run { + if let lastDataSource = DataStore.shared.appSettings.lastDataSource { + let currentMonthData : MonthData = Store.main.filter(isIncluded: { $0.monthKey == lastDataSource }).first ?? MonthData(monthKey: lastDataSource) + currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked + currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked + try? DataStore.shared.monthData.addOrUpdate(instance: currentMonthData) + } + } + } + + override func deleteDependencies() throws { + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _monthKey = "monthKey" + case _creationDate = "creationDate" + case _maleUnrankedValue = "maleUnrankedValue" + case _femaleUnrankedValue = "femaleUnrankedValue" + } +} diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index f5ffa14..5f06bf3 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -87,6 +87,26 @@ class PlayerRegistration: ModelObject, Storable { } } + var computedAge: Int? { + if let birthdate { + let components = birthdate.components(separatedBy: "/") + if components.count == 3 { + if let year = Calendar.current.dateComponents([.year], from: Date()).year, let age = components.last, let ageInt = Int(age) { + if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier + if ageInt < 23 { + return year - 2000 - ageInt + } else { + return year - 2000 + 100 - ageInt + } + } else { //si l'année est représenté sur 4 chiffres + return year - ageInt + } + } + } + } + return nil + } + func pasteData() -> String { [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ") } @@ -145,7 +165,11 @@ class PlayerRegistration: ModelObject, Storable { func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { if let rank, rank > 0 { - return rank.formatted() + if rank != weight { + return weight.formatted() + " (" + rank.formatted() + ")" + } else { + return rank.formatted() + } } else { return "non classé" + (isMalePlayer() ? "" : "e") } @@ -319,7 +343,6 @@ class PlayerRegistration: ModelObject, Storable { return 15000 } } - } extension PlayerRegistration: Hashable { @@ -333,6 +356,9 @@ extension PlayerRegistration: Hashable { } extension PlayerRegistration: PlayerHolder { + func getAssimilatedAsMaleRank() -> Int? { + nil + } func getFirstName() -> String { firstName diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 37a4ab2..bd96ed2 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -92,26 +92,29 @@ class Round: ModelObject, Storable { func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { return Store.main.filter(isIncluded: { - $0.tournament == tournament && $0.bracketPosition != nil - }).first(where: { - ($0.bracketPosition! / 2) == matchIndex + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! % 2) == team.rawValue - }) + }).first } func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] { return Store.main.filter(isIncluded: { - $0.tournament == tournament && $0.bracketPosition != nil - }).filter({ - ($0.bracketPosition! / 2) == matchIndex + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) == matchIndex }) } func seeds() -> [TeamRegistration] { + let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index) + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index) return Store.main.filter(isIncluded: { - $0.tournament == tournament && $0.bracketPosition != nil - }).filter({ - ($0.bracketPosition! / 2) >= RoundRule.matchIndex(fromRoundIndex: index) && ($0.bracketPosition! / 2) < RoundRule.matchIndex(fromRoundIndex: index) + RoundRule.numberOfMatches(forRoundIndex: index) + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) >= initialMatchIndex + && ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches }) } @@ -209,7 +212,7 @@ class Round: ModelObject, Storable { } func loserRounds(forRoundIndex roundIndex: Int) -> [Round] { - return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount) + return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount) } func isDisabled() -> Bool { @@ -246,8 +249,8 @@ class Round: ModelObject, Storable { } func getActiveLoserRound() -> Round? { - let rounds = loserRounds() - return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false }) + let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed() + return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first } func enableRound() { @@ -268,39 +271,6 @@ class Round: ModelObject, Storable { try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) } - func handleLoserRoundState() { - let _matches = _matches() - _matches.forEach { match in - let previousRound = self.previousRound() - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index) - var parentMatches = [Match]() - if isLoserBracket(), previousRound == nil, let parentRound = parentRound { - let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) - let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) - parentMatches = [upperBracketTopMatch, upperBracketBottomMatch].compactMap({ $0 }) - } else if let previousRound { - let previousRoundTopMatch : Match? = Store.main.filter { - $0.round == previousRound.id && $0.index == match.topPreviousRoundMatchIndex() - }.first - let previousRoundBottomMatch : Match? = Store.main.filter { - $0.round == previousRound.id && $0.index == match.bottomPreviousRoundMatchIndex() - }.first - parentMatches = [previousRoundTopMatch, previousRoundBottomMatch].compactMap({ $0 }) - } - - if parentMatches.anySatisfy({ $0.disabled }) { - match.disabled = true - } else if parentMatches.allSatisfy({ $0.disabled == false }) { - match.disabled = false - } - } - - try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) - loserRounds().forEach { round in - round.handleLoserRoundState() - } - } - var cumulativeMatchCount: Int { var totalMatches = playedMatches().count if let parent = parentRound { @@ -317,13 +287,58 @@ class Round: ModelObject, Storable { } } + + func disabledMatches() -> [Match] { + _matches().filter({ $0.disabled }) + } + + var theoryCumulativeMatchCount: Int { + var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index) + if let parent = parentRound { + totalMatches += parent.theoryCumulativeMatchCount + } + return totalMatches + } + + + func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { + let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) + let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: { + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex + }) + let playedMatches = playedMatches() + let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) + return seedInterval.localizedLabel(displayStyle) + } + + func seedInterval() -> SeedInterval? { + if loser == nil { + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index + 1) + let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) + let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: { + $0.tournament == tournament + && $0.bracketPosition != nil + && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex + }) + let playedMatches = playedMatches() + let reduce = numberOfMatches / 2 - (playedMatches.count + seedsAfterThisRound.count) + return SeedInterval(first: 1, last: numberOfMatches, reduce: reduce) + } + + if let previousRound = previousRound() { + return previousRound.seedInterval()?.chunks()?.first + } else if let parentRound = parentRound { + return parentRound.seedInterval()?.chunks()?.last + } + + return nil + } + func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { - if let parentRound, let initialRound = parentRound.initialRound() { - let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count -// print("initialRound", initialRound.roundTitle()) - if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() { - return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle) - } + if loser != nil { + return seedInterval()?.localizedLabel(displayStyle) ?? "Pas trouvé" } return RoundRule.roundName(fromRoundIndex: index) } @@ -387,6 +402,15 @@ class Round: ModelObject, Storable { return Store.main.findById(parentRound) } + func updateMatchFormat(_ matchFormat: MatchFormat) { + self.matchFormat = matchFormat + let playedMatches = _matches() + playedMatches.forEach { match in + match.matchFormat = matchFormat + } + try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches) + } + override func deleteDependencies() throws { try Store.main.deleteDependencies(items: _matches()) try Store.main.deleteDependencies(items: loserRoundsAndChildren()) @@ -418,4 +442,8 @@ extension Round: Selectable { return playedMatches().filter({ $0.isRunning() }).count } } + + func badgeImage() -> Badge? { + hasEnded() ? .checkmark : nil + } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 0f42cb0..706508c 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -128,9 +128,9 @@ class TeamRegistration: ModelObject, Storable { func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide: - unsortedPlayers().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") + players().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") case .short: - unsortedPlayers().map { $0.playerLabel(.wide) }.joined(separator: "\n") + players().map { $0.playerLabel(.wide) }.joined(separator: "\n") } } @@ -283,6 +283,19 @@ class TeamRegistration: ModelObject, Storable { return Store.main.findById(groupStage) } + func initialRound() -> Round? { + guard let bracketPosition else { return nil } + let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2) + return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == roundIndex }).first + } + + func initialMatch() -> Match? { + guard let bracketPosition else { return nil } + guard let initialRoundObject = initialRound() else { return nil } + return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }).first + } + + func tournamentObject() -> Tournament? { Store.main.findById(tournament) } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 97820c6..0da9e8c 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -44,11 +44,14 @@ class Tournament : ModelObject, Storable { var maleUnrankedValue: Int? var femaleUnrankedValue: Int? var payment: TournamentPayment? = nil + var additionalEstimationDuration: Int = 0 + var courtsUnavailability: [Int: [DateInterval]]? = nil + @ObservationIgnored var navigationPath: [Screen] = [] - internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) { + internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil) { self.event = event self.creator = creator self.name = name @@ -77,8 +80,6 @@ class Tournament : ModelObject, Storable { self.qualifiedPerGroupStage = qualifiedPerGroupStage self.teamsPerGroupStage = teamsPerGroupStage self.entryFee = entryFee - self.maleUnrankedValue = maleUnrankedValue - self.femaleUnrankedValue = femaleUnrankedValue self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType } @@ -260,12 +261,16 @@ class Tournament : ModelObject, Storable { let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) - if availableSeeds.count == availableSeedSpot.count { + if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { return availableSeedGroup - } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) { + } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.count == availableSeedOpponentSpot.count { return availableSeedGroup - } else if let chunk = availableSeedGroup.chunk() { - return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) + } else if let chunks = availableSeedGroup.chunks() { + if let chunk = chunks.first(where: { seedInterval in + seedInterval.first >= self.seededTeams().count + }) { + return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) + } } } @@ -300,8 +305,12 @@ class Tournament : ModelObject, Storable { for (index, seed) in availableSeeds.enumerated() { seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) } - } else if let chunk = seedGroup.chunk() { - setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } else if let chunks = seedGroup.chunks() { + if let chunk = chunks.first(where: { seedInterval in + seedInterval.first >= self.seededTeams().count + }) { + setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } } } } @@ -320,17 +329,36 @@ class Tournament : ModelObject, Storable { return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first } - func getActiveRound() -> Round? { + func getActiveRound(withSeeds: Bool = false) -> Round? { let rounds = rounds() - return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + let round = rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + + if withSeeds { + if round?.seeds().isEmpty == false { + return round + } else { + return nil + } + } else { + return round + } + } + + func allRoundMatches() -> [Match] { + allRounds().flatMap { $0._matches() } } func allMatches() -> [Match] { let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } - let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() } + let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRoundMatches() return matches.filter({ $0.disabled == false }) } - + + func _allMatchesIncludingDisabled() -> [Match] { + let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } + return unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() } + } + func allRounds() -> [Round] { Store.main.filter { $0.tournament == self.id } } @@ -376,7 +404,7 @@ class Tournament : ModelObject, Storable { _sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending) } - let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + //let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) //print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) return _sortedTeams } @@ -448,7 +476,7 @@ class Tournament : ModelObject, Storable { //todo var clubName: String? { - nil + eventObject?.clubObject?.name } //todo @@ -493,7 +521,14 @@ class Tournament : ModelObject, Storable { func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] { let licenseYearValidity = licenseYearValidity() - return players.filter({ ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.licenceId?.isLicenseNumber == false || $0.licenceId?.isEmpty == true)) }) + return players.filter({ + ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true)) + }) + } + + func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { + guard let seedIndex else { return nil } + return selectedSortedTeams()[safe: seedIndex]?.callDate } func importTeams(_ teams: [FileImportManager.TeamHolder]) { @@ -503,7 +538,7 @@ class Tournament : ModelObject, Storable { previousTeam.updatePlayers(team.players) teamsToImport.append(previousTeam) } else { - let newTeam = addTeam(team.players) + let newTeam = addTeam(team.players, registrationDate: team.registrationDate) teamsToImport.append(newTeam) } } @@ -513,6 +548,59 @@ class Tournament : ModelObject, Storable { } + func maximumCourtsPerGroupSage() -> Int { + if teamsPerGroupStage > 1 { + return min(teamsPerGroupStage / 2, courtCount) + } else { + return max(1, courtCount) + } + } + + func registrationIssues() -> Int { + let players : [PlayerRegistration] = unsortedPlayers() + let selectedTeams : [TeamRegistration] = selectedSortedTeams() + let callDateIssue : [TeamRegistration] = selectedTeams.filter { isStartDateIsDifferentThanCallDate($0) } + let duplicates : [PlayerRegistration] = duplicates(in: players) + let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == -1 }) + let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players) + let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players) + let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) + let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams) + let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) + let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) + + return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + } + + func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { + guard let callDate = team.callDate else { return false } + if let groupStageStartDate = team.groupStageObject()?.startDate { + return Calendar.current.compare(callDate, to: groupStageStartDate, toGranularity: .minute) != ComparisonResult.orderedSame + } else if let roundMatchStartDate = team.initialMatch()?.startDate { + return Calendar.current.compare(callDate, to: roundMatchStartDate, toGranularity: .minute) != ComparisonResult.orderedSame + } + return false + } + + func availableToStart(_ allMatches: [Match]) -> [Match] { + let runningMatches = allMatches.filter({ $0.isRunning() && $0.isReady() }) + return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) + } + + func runningMatches(_ allMatches: [Match]) -> [Match] { + allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) + } + + func readyMatches(_ allMatches: [Match]) -> [Match] { + return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + } + + func finishedMatches(_ allMatches: [Match], limit: Int? = nil) -> [Match] { + let _limit = limit ?? courtCount + return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit)) + } + + func lockRegistration() { closedRegistrationDate = Date() let count = selectedSortedTeams().count @@ -540,22 +628,27 @@ class Tournament : ModelObject, Storable { func updateRank(to newDate: Date?) async throws { guard let newDate else { return } rankSourceDate = newDate - - let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) - let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) - + 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 monthData = MonthData(monthKey: URL.importDateFormatter.string(from: newDate)) + monthData.maleUnrankedValue = lastRankMan + monthData.femaleUnrankedValue = lastRankWoman + try? DataStore.shared.monthData.addOrUpdate(instance: monthData) + } + } + + let lastRankMan = currentMonthData()?.maleUnrankedValue + let lastRankWoman = currentMonthData()?.femaleUnrankedValue + try await unsortedPlayers().concurrentForEach { player in let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate }) let sources = dataURLs.map { CSVParser(url: $0) } try await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0) } - - await MainActor.run { - self.maleUnrankedValue = lastRankMan - self.femaleUnrankedValue = lastRankWoman - } } func missingUnrankedValue() -> Bool { @@ -629,16 +722,11 @@ class Tournament : ModelObject, Storable { } func umpireMail() -> [String]? { - if let mail = UserDefaults.standard.string(forKey: "umpireMail"), mail.isEmpty == false { - return [mail] + if let email = DataStore.shared.user?.email { + return [email] } else { return nil } - -// if let umpireMail = federalTournament?.courrielEngagement { -// return [umpireMail] -// } else { -// } } func earnings() -> Double { @@ -655,19 +743,32 @@ class Tournament : ModelObject, Storable { return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count) } - func cashierStatus() -> String { - //todo - return "todo" + typealias TournamentStatus = (label:String, completion: String) + func cashierStatus() -> TournamentStatus { + let selectedPlayers = selectedPlayers() + let paid = selectedPlayers.filter({ $0.hasPaid() }) + let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" + let completion = (Double(paid.count) / Double(selectedPlayers.count)) + let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completionLabel) } - func scheduleStatus() -> String { - //todo - return "todo" + func scheduleStatus() -> TournamentStatus { + let allMatches = allMatches() + let ready = allMatches.filter({ $0.startDate != nil }) + let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés" + let completion = (Double(ready.count) / Double(allMatches.count)) + let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completionLabel) } - func callStatus() -> String { - //todo - return "todo" + func callStatus() -> TournamentStatus { + let selectedSortedTeams = selectedSortedTeams() + let called = selectedSortedTeams.filter{ $0.called() } + let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " paires convoquées" + let completion = (Double(called.count) / Double(selectedSortedTeams.count)) + let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) + return TournamentStatus(label: label, completion: completionLabel) } func bracketStatus() -> String { @@ -826,8 +927,12 @@ class Tournament : ModelObject, Storable { entryFee == nil || entryFee == 0 } - func addTeam(_ players: Set) -> TeamRegistration { - let team = TeamRegistration(tournament: id, registrationDate: Date()) + func indexOf(team: TeamRegistration) -> Int? { + selectedSortedTeams().firstIndex(where: { $0.id == team.id }) + } + + func addTeam(_ players: Set, registrationDate: Date? = nil) -> TeamRegistration { + let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date()) team.tournamentCategory = tournamentCategory team.setWeight(from: Array(players)) players.forEach { player in @@ -926,7 +1031,14 @@ class Tournament : ModelObject, Storable { return groupStageMatchFormat } } - + + func setupFederalSettings() { + teamSorting = tournamentLevel.defaultTeamSortingType + groupStageMatchFormat = groupStageSmartMatchFormat() + loserBracketMatchFormat = loserBracketSmartMatchFormat(1) + matchFormat = roundSmartMatchFormat(1) + } + func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForBracketRound(roundIndex) if matchFormat.rank > format.rank { @@ -958,6 +1070,21 @@ class Tournament : ModelObject, Storable { try Store.main.deleteDependencies(items: self.groupStages()) try Store.main.deleteDependencies(items: self.rounds()) } + + func currentMonthData() -> MonthData? { + guard let rankSourceDate else { return nil } + let dateString = URL.importDateFormatter.string(from: rankSourceDate) + return Store.main.filter(isIncluded: { $0.monthKey == dateString }).first + } + + var maleUnrankedValue: Int? { + currentMonthData()?.maleUnrankedValue + } + + var femaleUnrankedValue: Int? { + currentMonthData()?.femaleUnrankedValue + } + } extension Tournament { @@ -991,8 +1118,7 @@ extension Tournament { case _qualifiedPerGroupStage = "qualifiedPerGroupStage" case _teamsPerGroupStage = "teamsPerGroupStage" case _entryFee = "entryFee" - case _maleUnrankedValue = "maleUnrankedValue" - case _femaleUnrankedValue = "femaleUnrankedValue" + case _additionalEstimationDuration = "additionalEstimationDuration" } } @@ -1036,6 +1162,4 @@ extension Tournament: TournamentBuildHolder { var age: FederalTournamentAge { federalTournamentAge } - - } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 835302e..bac5698 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -89,6 +89,10 @@ extension Date { } } + func atBeginningOfDay(hourInt: Int = 9) -> Date { + Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)! + } + static var firstDayOfWeek = Calendar.current.firstWeekday static var capitalizedFirstLettersOfWeekdays: [String] { let calendar = Calendar.current @@ -180,9 +184,15 @@ extension Date { var dayInt: Int { Calendar.current.component(.day, from: self) } + var startOfDay: Date { Calendar.current.startOfDay(for: self) } + + func endOfDay() -> Date { + let calendar = Calendar.current + return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)! + } } extension Date { @@ -191,3 +201,12 @@ extension Date { } } +extension Date { + func localizedTime() -> String { + self.formatted(.dateTime.hour().minute()) + } + + func localizedDay() -> String { + self.formatted(.dateTime.weekday(.wide).day()) + } +} diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 1b233af..b515c10 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -159,3 +159,8 @@ extension String { } } } + +extension StringProtocol { + var firstUppercased: String { prefix(1).uppercased() + dropFirst() } + var firstCapitalized: String { prefix(1).capitalized + dropFirst() } +} diff --git a/PadelClub/Manager/ContactManager.swift b/PadelClub/Manager/ContactManager.swift index d139a89..005e736 100644 --- a/PadelClub/Manager/ContactManager.swift +++ b/PadelClub/Manager/ContactManager.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import MessageUI +import LeStorage enum ContactManagerError: LocalizedError { case mailFailed @@ -33,7 +34,7 @@ extension ContactType { static let defaultSignature = "" static func callingGroupStageCustomMessage(tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { - let tournamentCustomMessage = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage + let tournamentCustomMessage = DataStore.shared.appSettings.callMessageBody ?? defaultCustomMessage let clubName = tournament?.clubName ?? "" var text = tournamentCustomMessage @@ -48,7 +49,7 @@ extension ContactType { text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))") - let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature + let signature = DataStore.shared.appSettings.callMessageSignature ?? defaultSignature text = text.replacingOccurrences(of: "#signature", with: signature) return text @@ -56,7 +57,7 @@ extension ContactType { static func callingGroupStageMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?) -> String { - let useFullCustomMessage = UserDefaults.standard.bool(forKey: "useFullCustomMessage") + let useFullCustomMessage = DataStore.shared.appSettings.callUseFullCustomMessage ?? false if useFullCustomMessage { return callingGroupStageCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel) @@ -65,17 +66,17 @@ extension ContactType { let date = startDate ?? tournament?.startDate ?? Date() let clubName = tournament?.clubName ?? "" - let message = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage - let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature + let message = DataStore.shared.appSettings.callMessageBody ?? defaultCustomMessage + let signature = DataStore.shared.appSettings.callMessageSignature ?? defaultSignature let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s" var formatMessage: String? { - UserDefaults.standard.bool(forKey: "displayFormat") ? matchFormat?.computedLongLabel.appending(".") : nil + (DataStore.shared.appSettings.callDisplayFormat ?? false) ? matchFormat?.computedLongLabel.appending(".") : nil } var entryFeeMessage: String? { - UserDefaults.standard.bool(forKey: "displayEntryFee") ? tournament?.entryFeeMessage : nil + (DataStore.shared.appSettings.callDisplayEntryFee ?? false) ? tournament?.entryFeeMessage : nil } var computedMessage: String { diff --git a/PadelClub/Manager/FileImportManager.swift b/PadelClub/Manager/FileImportManager.swift index ac224b4..b2d6ca5 100644 --- a/PadelClub/Manager/FileImportManager.swift +++ b/PadelClub/Manager/FileImportManager.swift @@ -44,10 +44,13 @@ class FileImportManager { var id: Self { self } case frenchFederation + case padelClub case unknown var localizedLabel: String { switch self { + case .padelClub: + return "Padel Club" case .frenchFederation: return "FFT" case .unknown: @@ -58,28 +61,32 @@ class FileImportManager { struct TeamHolder: Identifiable { let id: UUID = UUID() - let playerOne: PlayerRegistration - let playerTwo: PlayerRegistration + let players: Set let weight: Int let tournamentCategory: TournamentCategory let previousTeam: TeamRegistration? - - init(playerOne: PlayerRegistration, playerTwo: PlayerRegistration, tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?) { - self.playerOne = playerOne - self.playerTwo = playerTwo + var registrationDate: Date? = nil + + init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) { + self.players = Set(players) self.tournamentCategory = tournamentCategory self.previousTeam = previousTeam - self.weight = playerOne.weight + playerTwo.weight - } - - var players: Set { - Set([playerOne, playerTwo]) + self.weight = players.map { $0.weight }.reduce(0,+) + self.registrationDate = registrationDate } - + func index(in teams: [TeamHolder]) -> Int? { teams.firstIndex(where: { $0.id == id }) } - + + func formattedSeedIndex(index: Int?) -> String { + if let index { + return "#\(index + 1)" + } else { + return "###" + } + } + func formattedSeed(in teams: [TeamHolder]) -> String { if let index = index(in: teams) { return "#\(index + 1)" @@ -92,6 +99,60 @@ class FileImportManager { static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur" func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation) async -> [TeamHolder] { + + switch fileProvider { + case .frenchFederation: + return await _getFederalTeams(from: fileContent, tournament: tournament) + case .padelClub: + return await _getPadelClubTeams(from: fileContent, tournament: tournament) + case .unknown: + return await _getPadelBusinessLeagueTeams(from: fileContent, tournament: tournament) + } + } + + func importDataFromFFT() async -> String? { + if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { + for source in SourceFile.allCases { + for fileURL in source.currentURLs { + let p = readCSV(inputFile: fileURL) + await importingChunkOfPlayers(p, importingDate: importingDate) + } + } + return URL.importDateFormatter.string(from: importingDate) + } + return nil + } + + + func readCSV(inputFile: URL) -> [FederalPlayer] { + do { + let fileContent = try String(contentsOf: inputFile) + return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData) + } catch { + print("error: \(error)") // to do deal with errors + } + return [] + } + + func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] { + let lines = fileContent.components(separatedBy: "\n") + return lines.compactMap { line in + if line.components(separatedBy: ";").count < 10 { + } else { + let data = line.components(separatedBy: ";").joined(separator: "\n") + return FederalPlayer(data, isMale: isMale) + } + return nil + } + } + + func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { + for chunk in players.chunked(into: 1000) { + await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) + } + } + + private func _getFederalTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { let lines = fileContent.components(separatedBy: "\n") guard let firstLine = lines.first else { return [] } var separator = "," @@ -100,58 +161,7 @@ class FileImportManager { } let headerCount = firstLine.components(separatedBy: separator).count var results: [TeamHolder] = [] - if headerCount == 23 && fileProvider == .unknown { //PBL - let fetchRequest = ImportedPlayer.fetchRequest() - let federalContext = PersistenceController.shared.localContainer.viewContext - - lines.dropFirst().forEach { line in - let data = line.components(separatedBy: separator) - if data.count == 23 { - -// let team = Team(context: context) -// let brand = Brand(context: context) -// brand.title = data[2].trimmed -// brand.qualifier = data[0].trimmed -// brand.country = data[1].trimmed -// brand.lineOfBusiness = data[3].trimmed -// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix -// brand.lineOfBusiness = "Bâtiment / Immo / Transport" -// } -// brand.name = data[4].trimmed -// team.brand = brand -// -// for i in 0...5 { -// let sex = data[i*3+5] -// let lastName = data[i*3+6].trimmed -// let firstName = data[i*3+7].trimmed -// if lastName.isEmpty == false { -// let playerOne = Player(context: context) -// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens) -// fetchRequest.predicate = predicate -// if let playerFound = try? federalContext.fetch(fetchRequest).first { -// playerOne.updateWithImportedPlayer(playerFound) -// } else { -// playerOne.lastName = lastName -// playerOne.firstName = firstName -// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1 -// playerOne.currentRank = tournament?.lastRankMan ?? 0 -// } -// team.addToPlayers(playerOne) -// } -// } -// team.category = TournamentCategory.men.importingRawValue -// -// if let players = team.players, players.count > 0 { -// results.append(team) -// } else { -// context.delete(team) -// } - } - } - - - return results - } else if headerCount <= 18 && fileProvider == .frenchFederation { + if headerCount <= 18 { Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in if teamLines.count == 2 { let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) @@ -203,13 +213,13 @@ class FileImportManager { playerOne.setWeight(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setWeight(in: tournament) - let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) results.append(team) } } } return results - } else if headerCount > 18 && fileProvider == .frenchFederation { + } else { lines.dropFirst().forEach { line in let data = line.components(separatedBy: separator) if data.count > 18 { @@ -236,7 +246,7 @@ class FileImportManager { case .mix: return 1 } } - + var sexPlayerTwo : Int { switch tournamentCategory { case .men: return 1 @@ -249,56 +259,108 @@ class FileImportManager { playerOne.setWeight(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setWeight(in: tournament) - - let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) results.append(team) } } return results - } else { - return [] } } - func importDataFromFFT() async -> String? { - if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { - for source in SourceFile.allCases { - for fileURL in source.currentURLs { - let p = readCSV(inputFile: fileURL) - await importingChunkOfPlayers(p, importingDate: importingDate) + private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { + let lines = fileContent.components(separatedBy: "\n\n") + var results: [TeamHolder] = [] + let fetchRequest = ImportedPlayer.fetchRequest() + let federalContext = PersistenceController.shared.localContainer.viewContext + + lines.forEach { team in + let data = team.components(separatedBy: "\n") + let players = team.licencesFound() + fetchRequest.predicate = NSPredicate(format: "license IN %@", players) + let found = try? federalContext.fetch(fetchRequest) + let registeredPlayers = found?.map({ importedPlayer in + let player = PlayerRegistration(importedPlayer: importedPlayer) + player.setWeight(in: tournament) + return player + }) + if let registeredPlayers, registeredPlayers.isEmpty == false { + var registrationDate: Date? { + if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "inscrit le ", with: "") { + return try? Date(registrationDateData, strategy: .dateTime.weekday().day().month().hour().minute()) + } + return nil } + let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate) + results.append(team) } - return URL.importDateFormatter.string(from: importingDate) - } - return nil - } - - - func readCSV(inputFile: URL) -> [FederalPlayer] { - do { - let fileContent = try String(contentsOf: inputFile) - return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData) - } catch { - print("error: \(error)") // to do deal with errors } - return [] + + return results } - func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] { + private func _getPadelBusinessLeagueTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { let lines = fileContent.components(separatedBy: "\n") - return lines.compactMap { line in - if line.components(separatedBy: ";").count < 10 { - } else { - let data = line.components(separatedBy: ";").joined(separator: "\n") - return FederalPlayer(data, isMale: isMale) - } - return nil + guard let firstLine = lines.first else { return [] } + var separator = "," + if firstLine.contains(";") { + separator = ";" } - } - - func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { - for chunk in players.chunked(into: 1000) { - await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) + let headerCount = firstLine.components(separatedBy: separator).count + var results: [TeamHolder] = [] + if headerCount == 23 { + //todo + let fetchRequest = ImportedPlayer.fetchRequest() + let federalContext = PersistenceController.shared.localContainer.viewContext + + lines.dropFirst().forEach { line in + let data = line.components(separatedBy: separator) + if data.count == 23 { + +// let team = Team(context: context) +// let brand = Brand(context: context) +// brand.title = data[2].trimmed +// brand.qualifier = data[0].trimmed +// brand.country = data[1].trimmed +// brand.lineOfBusiness = data[3].trimmed +// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix +// brand.lineOfBusiness = "Bâtiment / Immo / Transport" +// } +// brand.name = data[4].trimmed +// team.brand = brand +// +// for i in 0...5 { +// let sex = data[i*3+5] +// let lastName = data[i*3+6].trimmed +// let firstName = data[i*3+7].trimmed +// if lastName.isEmpty == false { +// let playerOne = Player(context: context) +// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens) +// fetchRequest.predicate = predicate +// if let playerFound = try? federalContext.fetch(fetchRequest).first { +// playerOne.updateWithImportedPlayer(playerFound) +// } else { +// playerOne.lastName = lastName +// playerOne.firstName = firstName +// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1 +// playerOne.currentRank = tournament?.lastRankMan ?? 0 +// } +// team.addToPlayers(playerOne) +// } +// } +// team.category = TournamentCategory.men.importingRawValue +// +// if let players = team.players, players.count > 0 { +// results.append(team) +// } else { +// context.delete(team) +// } + } + } + + + return results } + return [] } } diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index 54d21e4..d3e2062 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -159,20 +159,18 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case a45 = 450 case a55 = 550 - static func mostRecent(tournaments: [Tournament] = []) -> Self { - .senior -// return tournaments.first?.federalTournamentAge ?? .a11_12 + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.federalTournamentAge ?? .senior } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! FederalTournamentAge -// } else { -// return mostRecent(tournaments: tournaments) -// } - .senior + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! FederalTournamentAge + } else { + return mostRecent(inTournaments: tournaments) + } } var id: Int { self.rawValue } @@ -236,22 +234,20 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { case p1500 = 1500 case p2000 = 2000 - static func mostRecent(tournaments: [Tournament] = []) -> Self { - //return tournaments.first?.tournamentLevel ?? .p25 - .p100 + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.tournamentLevel ?? .p100 } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! TournamentLevel -// } else { -// return mostRecent(tournaments: tournaments) -// } - .p100 + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! TournamentLevel + } else { + return mostRecent(inTournaments: tournaments) + } } - + var id: Int { self.rawValue } func maximumDuration() -> Double { @@ -631,20 +627,27 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable { } } - static func mostRecent(tournaments: [Tournament] = []) -> Self { - //return tournaments.first?.tournamentCategory ?? .mix - .men + var showFemaleInMaleAssimilation: Bool { + switch self { + case .men: + return true + default: + return false + } + } + + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.tournamentCategory ?? .men } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! TournamentCategory -// } else { -// return mostRecent(tournaments: tournaments) -// } - .men + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! TournamentCategory + } else { + return mostRecent(inTournaments: tournaments) + } } var next: TournamentCategory { @@ -794,10 +797,12 @@ enum TournamentType: Int, Hashable, Codable, CaseIterable, Identifiable { } } -enum TeamPosition: Int, Hashable, Codable, CaseIterable { +enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable { case one case two + var id: Int { self.rawValue } + var otherTeam: TeamPosition { switch self { case .one: @@ -1019,10 +1024,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } static func defaultFormatForMatchType(_ matchType: MatchType) -> MatchFormat { - if UserDefaults.standard.object(forKey: matchType.rawValue + "MatchFormatPreference") == nil { - return .nineGamesDecisivePoint + switch matchType { + case .bracket: + MatchFormat(rawValue: DataStore.shared.appSettings.bracketMatchFormatPreference) ?? .nineGamesDecisivePoint + case .groupStage: + MatchFormat(rawValue: DataStore.shared.appSettings.groupStageMatchFormatPreference) ?? .nineGamesDecisivePoint + case .loserBracket: + MatchFormat(rawValue: DataStore.shared.appSettings.loserBracketMatchFormatPreference) ?? .nineGamesDecisivePoint } - return MatchFormat(rawValue: UserDefaults.standard.integer(forKey: matchType.rawValue + "MatchFormatPreference")) ?? .nineGamesDecisivePoint } static var allCases: [MatchFormat] { @@ -1046,16 +1055,16 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } } - var estimatedDuration: Int { - if UserDefaults.standard.object(forKey: format) != nil { - return UserDefaults.standard.integer(forKey: format) - } else { - return defaultEstimatedDuration - } + func getEstimatedDuration(_ additionalDuration: Int = 0) -> Int { + estimatedDuration + additionalDuration } - func formattedEstimatedDuration() -> String { - Duration.seconds(estimatedDuration * 60).formatted(.units(allowed: [.minutes])) + private var estimatedDuration: Int { + DataStore.shared.appSettings.matchFormatsDefaultDuration?[self] ?? defaultEstimatedDuration + } + + func formattedEstimatedDuration(_ additionalDuration: Int = 0) -> String { + Duration.seconds((estimatedDuration + additionalDuration) * 60).formatted(.units(allowed: [.minutes])) } func formattedEstimatedBreakDuration() -> String { @@ -1419,6 +1428,10 @@ enum RoundRule { return (1 << roundIndex) - 1 } + static func matchIndex(fromBracketPosition: Int) -> Int { + roundIndex(fromMatchIndex: fromBracketPosition / 2) + fromBracketPosition%2 + } + static func roundIndex(fromMatchIndex matchIndex: Int) -> Int { Int(log2(Double(matchIndex + 1))) } diff --git a/PadelClub/Manager/SourceFileManager.swift b/PadelClub/Manager/SourceFileManager.swift index 690e930..018a341 100644 --- a/PadelClub/Manager/SourceFileManager.swift +++ b/PadelClub/Manager/SourceFileManager.swift @@ -12,7 +12,7 @@ class SourceFileManager { static let beachPadel = URL(string: "https://beach-padel.app.fft.fr/beachja/index/")! var lastDataSource: String? { - UserDefaults.standard.string(forKey: "lastDataSource") + DataStore.shared.appSettings.lastDataSource } func lastDataSourceDate() -> Date? { diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index ad20c44..4070cc8 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -55,4 +55,8 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable { nil } } + + func badgeImage() -> Badge? { + nil + } } diff --git a/PadelClub/ViewModel/AppScreen.swift b/PadelClub/ViewModel/AppScreen.swift new file mode 100644 index 0000000..1eba92f --- /dev/null +++ b/PadelClub/ViewModel/AppScreen.swift @@ -0,0 +1,13 @@ +// +// AppScreen.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import Foundation + +enum AppScreen: CaseIterable, Identifiable { + var id: Self { self } + case matchFormatSettings +} diff --git a/PadelClub/ViewModel/DateInterval.swift b/PadelClub/ViewModel/DateInterval.swift new file mode 100644 index 0000000..58876bf --- /dev/null +++ b/PadelClub/ViewModel/DateInterval.swift @@ -0,0 +1,32 @@ +// +// DateInterval.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/04/2024. +// + +import Foundation +import LeStorage + +struct DateInterval: Identifiable, Codable { + var id: String = Store.randomId() + + let startDate: Date + let endDate: Date + + var range: Range { + startDate.. Bool { + Calendar.current.isDate(startDate, inSameDayAs: endDate) + } + + func isDateInside(_ date: Date) -> Bool { + date >= startDate && date <= endDate + } + + func isDateOutside(_ date: Date) -> Bool { + date <= startDate && date <= endDate && date >= startDate && date >= endDate + } +} diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index 1d2d86a..0cd3a14 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -29,8 +29,8 @@ class MatchDescriptor: ObservableObject { } let teamOne = match?.team(.one) let teamTwo = match?.team(.two) - self.teamLabelOne = teamOne?.teamLabel() ?? "" - self.teamLabelTwo = teamTwo?.teamLabel() ?? "" + self.teamLabelOne = teamOne?.teamLabel(.short) ?? "" + self.teamLabelTwo = teamTwo?.teamLabel(.short) ?? "" if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score { diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 3337951..90ecb2d 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -62,10 +62,12 @@ enum MatchSchedulerOption: Hashable { class MatchScheduler { static let shared = MatchScheduler() + var additionalEstimationDuration : Int = 0 var options: Set = Set(arrayLiteral: .accountUpperBracketBreakTime) var timeDifferenceLimit: Double = 300.0 var loserBracketRotationDifference: Int = 0 var upperBracketRotationDifference: Int = 1 + var courtsUnavailability: [Int: [DateInterval]]? = nil func shouldHandleUpperRoundSlice() -> Bool { options.contains(.shouldHandleUpperRoundSlice) @@ -175,7 +177,18 @@ class MatchScheduler { } func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { - //print(roundObject.roundTitle(), match.matchTitle()) + print(roundObject.roundTitle(), match.matchTitle()) + + if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate { + print("can't start \(targetedStartDate) earlier than \(roundStartDate)") + if targetedStartDate == minimumTargetedEndDate { + minimumTargetedEndDate = roundStartDate + } else { + minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate) + } + return false + } + let previousMatches = roundObject.precedentMatches(ofMatch: match) if previousMatches.isEmpty { return true } @@ -254,7 +267,7 @@ class MatchScheduler { let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!) let lastMatch = matchesByCourt?.last var results = [(String, Date)]() - if let courtFreeDate = lastMatch?.estimatedEndDate() { + if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) { results.append((court, courtFreeDate)) } return results @@ -276,7 +289,8 @@ class MatchScheduler { _startDate = match.startDate rotationIndex += 1 } - let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex() ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.estimatedDuration, minimumBreakTime: match.matchFormat.breakTime.breakTime) + + let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.getCourtIndex() ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime) slots.append(timeMatch) } @@ -359,33 +373,55 @@ class MatchScheduler { func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) { var matchPerRound = [Int: Int]() var minimumTargetedEndDate: Date = rotationStartDate - courts.forEach { courtIndex in - //print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) - + print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex) + courts.sorted().forEach { courtIndex in + print("trying to find a match for \(courtIndex) in \(rotationIndex)") if let first = availableMatchs.first(where: { match in let roundObject = match.roundObject! + let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration)) + print("courtsUnavailable \(courtsUnavailable)") + if courtIndex >= availableCourts - courtsUnavailable { + return false + } + let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0 if shouldHandleUpperRoundSlice() { let roundMatchesCount = roundObject.playedMatches().count + print("shouldHandleUpperRoundSlice \(roundMatchesCount)") if roundObject.loser == nil && roundMatchesCount > courts.count { - if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { return false } + print("roundMatchesCount \(roundMatchesCount) > \(courts.count)") + if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { + print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))") + return false + } } } - if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, courts.count > 1, let nextMatch = match.next() { + let indexInRound = match.indexInRound() + + print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)") + if roundObject.loser == nil && roundObject.index > 0, indexInRound == 0, courts.count > 1, let nextMatch = match.next() { if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + + print("next match and this match can be played, returning true") + return true } else { + print("next match and this match can not be played at the same time, returning false") return false } } + + print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)") if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 { + print("we return false") return false } + return canBePlayed }) { @@ -398,7 +434,7 @@ class MatchScheduler { matchPerRound[first.roundObject!.index] = 1 } } - let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime) + let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: first.matchFormat.breakTime.breakTime) slots.append(timeMatch) availableMatchs.removeAll(where: { $0.id == first.id }) } else { @@ -407,11 +443,17 @@ class MatchScheduler { } if freeCourtPerRotation[rotationIndex]!.count == availableCourts { + print("no match found to be put in this rotation, check if we can put anything to another date") freeCourtPerRotation[rotationIndex] = [] let courtsUsed = getNextEarliestAvailableDate(from: slots) - let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in - availableDate <= minimumTargetedEndDate - }.sorted(by: \.1).map { $0.0 } + var freeCourts: [Int] = [] + if courtsUsed.isEmpty { + freeCourts = (0.. Int { + let endDate = startDate.addingTimeInterval(Double(duration) * 60) + guard let courtsUnavailability else { return 0 } + let courts = courtsUnavailability.keys + return courts.filter { + courtUnavailable(courtIndex: $0, from: startDate, to: endDate) + }.count + } + func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool { + guard let courtLockedSchedule = courtsUnavailability?[courtIndex] else { return true } + return courtLockedSchedule.anySatisfy({ dateInterval in + dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate) + }) + } +} diff --git a/PadelClub/ViewModel/NavigationViewModel.swift b/PadelClub/ViewModel/NavigationViewModel.swift index 70669d8..1a8467e 100644 --- a/PadelClub/ViewModel/NavigationViewModel.swift +++ b/PadelClub/ViewModel/NavigationViewModel.swift @@ -10,6 +10,7 @@ import SwiftUI @Observable class NavigationViewModel { var path = NavigationPath() + var selectedTab: TabDestination? var agendaDestination: AgendaDestination? = .activity var tournament: Tournament? } diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 6e4d80c..e906c6d 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -14,6 +14,8 @@ class SearchViewModel: ObservableObject, Identifiable { var codeClub: String? = nil var clubName: String? = nil var ligueName: String? = nil + var showFemaleInMaleAssimilation: Bool = false + @Published var debouncableText: String = "" @Published var searchText: String = "" @Published var task: DispatchWorkItem? diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index d0a82c0..d857a99 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -10,27 +10,36 @@ import Foundation struct SeedInterval: Hashable, Comparable { let first: Int let last: Int + var reduce: Int = 0 static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool { return lhs.first < rhs.first } - func chunk() -> SeedInterval? { - if (last - first) / 2 > 0 { - if last - (last - first) / 2 > first { - return SeedInterval(first: first, last: last - (last - first) / 2) - } + var count: Int { + dimension + } + + private var dimension: Int { + (last - (first - 1)) + } + + func chunks() -> [SeedInterval]? { + if dimension > 3 { + let split = dimension / 2 + let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce) + let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce) + return [firstHalf, secondHalf] + } else { + return nil } - return nil } -} -extension SeedInterval { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { - if last - first < 2 { - return "#\(first) / #\(last)" + if dimension < 2 { + return "#\(first - reduce) / #\(last - reduce)" } else { - return "#\(first) à #\(last)" + return "#\(first - reduce) à #\(last - reduce)" } } } diff --git a/PadelClub/ViewModel/Selectable.swift b/PadelClub/ViewModel/Selectable.swift index 5836812..7158823 100644 --- a/PadelClub/ViewModel/Selectable.swift +++ b/PadelClub/ViewModel/Selectable.swift @@ -6,8 +6,38 @@ // import Foundation +import SwiftUI protocol Selectable { func selectionLabel() -> String func badgeValue() -> Int? + func badgeImage() -> Badge? +} + +enum Badge { + case checkmark + case xmark + case custom(systemName: String, color: Color) + + func systemName() -> String { + switch self { + case .checkmark: + return "checkmark.circle.fill" + case .xmark: + return "xmark.circle.fill" + case .custom(let systemName, _): + return systemName + } + } + + func color() -> Color { + switch self { + case .checkmark: + .green + case .xmark: + .red + case .custom(_, let color): + color + } + } } diff --git a/PadelClub/ViewModel/TournamentSeedEditing.swift b/PadelClub/ViewModel/TournamentSeedEditing.swift index 4519355..5f4964b 100644 --- a/PadelClub/ViewModel/TournamentSeedEditing.swift +++ b/PadelClub/ViewModel/TournamentSeedEditing.swift @@ -8,22 +8,13 @@ import Foundation import SwiftUI -// Create an environment key private struct TournamentSeedEditing: EnvironmentKey { - static let defaultValue: Bool = false + static let defaultValue: Binding = .constant(false) } -// ## Introduce new value to EnvironmentValues extension EnvironmentValues { - var isEditingTournamentSeed: Bool { + var isEditingTournamentSeed: Binding { get { self[TournamentSeedEditing.self] } set { self[TournamentSeedEditing.self] = newValue } } } - -// Add a dedicated modifier (Optional) -extension View { - func editTournamentSeed(_ value: Bool) -> some View { - environment(\.isEditingTournamentSeed, value) - } -} diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift new file mode 100644 index 0000000..3187722 --- /dev/null +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -0,0 +1,186 @@ +// +// CallMessageCustomizationView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 02/11/2023. +// + +import SwiftUI + +struct CallMessageCustomizationView: View { + @EnvironmentObject var dataStore: DataStore + var tournament: Tournament + + @FocusState private var textEditor: Bool + @State private var customClubName: String = "" + @State private var customCallMessageBody: String = "" + @State private var customCallMessageSignature: String = "" + + init(tournament: Tournament) { + self.tournament = tournament + _customCallMessageBody = State(wrappedValue: DataStore.shared.appSettings.callMessageBody ?? "") + _customCallMessageSignature = State(wrappedValue: DataStore.shared.appSettings.callMessageSignature ?? "") + _customClubName = State(wrappedValue: tournament.clubName ?? "") + } + + var clubName: String { + customClubName + } + + var formatMessage: String? { + dataStore.appSettings.callDisplayFormat ? tournament.matchFormat.computedLongLabel + "." : nil + } + + var entryFeeMessage: String? { + dataStore.appSettings.callDisplayEntryFee ? tournament.entryFeeMessage : nil + } + + var computedMessage: String { + [formatMessage, entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmed }.joined(separator: "\n") + } + + var finalMessage: String? { + let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s" + return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle()) au \(clubName) le \(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(Date().formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" + } + + var body: some View { + @Bindable var appSettings = dataStore.appSettings + List { + Section { + ZStack { + Text(customCallMessageBody).hidden() + .padding(.vertical, 20) + TextEditor(text: $customCallMessageBody) + .autocorrectionDisabled() + .focused($textEditor) + } + } header: { + Text("Personnalisation du message de convocation") + } + + Section { + ZStack { + Text(customCallMessageSignature).hidden() + TextEditor(text: $customCallMessageSignature) + .autocorrectionDisabled() + .focused($textEditor) + } + } header: { + Text("Signature du message") + } + + Section { + TextField("Nom du club", text: $customClubName) + .autocorrectionDisabled() + .onSubmit { + if let eventClub = tournament.eventObject?.clubObject { + eventClub.name = customClubName + try? dataStore.clubs.addOrUpdate(instance: eventClub) + } + } + } header: { + Text("Nom du club") + } + + Section { + if appSettings.callUseFullCustomMessage { + Text(self.computedFullCustomMessage()) + .contextMenu { + Button("Coller dans le presse-papier") { + UIPasteboard.general.string = self.computedFullCustomMessage() + } + } + } + else if let finalMessage { + Text(finalMessage) + .contextMenu { + Button("Coller dans le presse-papier") { + UIPasteboard.general.string = finalMessage + } + } + } + } header: { + Text("Exemple") + } + + Section { + LabeledContent { + Toggle(isOn: $appSettings.callUseFullCustomMessage) { + + } + } label: { + Text("contrôle complet du message") + } + } header: { + Text("Personnalisation complète") + } footer: { + Text("Utilisez ces balises dans votre texte : #titre, #jour, #horaire, #club, #signature") + } + } + .navigationTitle("Message de convocation") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Picker(selection: $appSettings.callDisplayFormat) { + Text("Afficher le format").tag(true) + Text("Masquer le format").tag(false) + } label: { + + } + Picker(selection: $appSettings.callDisplayEntryFee) { + Text("Afficher le prix d'inscription").tag(true) + Text("Masquer le prix d'inscription").tag(false) + } label: { + + } + } label: { + LabelOptions() + } + } + ToolbarItemGroup(placement: .keyboard) { + if textEditor { + Spacer() + Button { + textEditor = false + } label: { + Label("Fermer", systemImage: "xmark") + } + } + } + } + .onChange(of: appSettings.callUseFullCustomMessage) { + if appSettings.callUseFullCustomMessage == false { + appSettings.callMessageBody = ContactType.defaultCustomMessage + } + _save() + } + .onChange(of: customCallMessageBody) { + appSettings.callMessageBody = customCallMessageBody + _save() + } + .onChange(of: customCallMessageSignature) { + appSettings.callMessageSignature = customCallMessageSignature + _save() + } + .onChange(of: appSettings.callDisplayEntryFee) { + _save() + } + .onChange(of: appSettings.callDisplayFormat) { + _save() + } + } + + private func _save() { + dataStore.updateSettings() + } + + func computedFullCustomMessage() -> String { + var text = customCallMessageBody.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle()) + text = text.replacingOccurrences(of: "#club", with: clubName) + text = text.replacingOccurrences(of: "#jour", with: "\(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") + text = text.replacingOccurrences(of: "#horaire", with: "\(Date().formatted(Date.FormatStyle().hour().minute()))") + text = text.replacingOccurrences(of: "#signature", with: customCallMessageSignature) + return text + } +} diff --git a/PadelClub/Views/Calling/CallSettingsView.swift b/PadelClub/Views/Calling/CallSettingsView.swift index f794e23..b718ef6 100644 --- a/PadelClub/Views/Calling/CallSettingsView.swift +++ b/PadelClub/Views/Calling/CallSettingsView.swift @@ -16,13 +16,20 @@ struct CallSettingsView: View { Section { NavigationLink { + CallMessageCustomizationView(tournament: tournament) } label: { - Text("Modifier le message de convocation") + Text("Personnaliser le message de convocation") } } Section { - RowButtonView("Annuler toutes les convocations") { + RowButtonView("Envoyer un message à tout le monde") { + + } + } + + Section { + RowButtonView("Annuler toutes les convocations", role: .destructive) { let teams = tournament.unsortedTeams() teams.forEach { team in team.callDate = nil @@ -32,13 +39,7 @@ struct CallSettingsView: View { } Section { - RowButtonView("Envoyer un message à tout le monde") { - - } - } - - Section { - RowButtonView("Tout le monde a été convoqué") { + RowButtonView("Tout le monde a été convoqué", role: .destructive) { let teams = tournament.unsortedTeams() teams.forEach { team in team.callDate = Date() diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 4abd713..84282d9 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -86,7 +86,11 @@ struct CallView: View { let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer" HStack { if teams.count == 1 { - Text(callWord + " cette paire par") + if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { + Text("Reconvoquer " + callDate.localizedDate() + " par") + } else { + Text(callWord + " cette paire par") + } } else { Text(callWord + " ces \(teams.count) paires par") } diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index c401f3e..1f8de11 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -19,8 +19,8 @@ struct GroupStageCallingView: View { ForEach(groupStages) { groupStage in let seeds = groupStage.teams() - let callSeeds = seeds.filter({ $0.callDate != nil }) - + let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + if seeds.isEmpty == false { Section { NavigationLink { @@ -48,15 +48,19 @@ struct GroupStageCallingView: View { groupStage.startDate } let keys = times.keys.compactMap { $0 }.sorted() - ForEach(keys, id: \.self) { key in - if let _groupStages = times[key], _groupStages.count > 1 { - let teams = _groupStages.flatMap { $0.teams() } - Section { - CallView.CallStatusView(count: teams.filter({ $0.callDate != nil }).count, total: teams.count, startDate: key) - } header: { - Text(groupStages.map { $0.groupStageTitle() }.joined(separator: ", ")) - } footer: { - CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule") + + if keys.count != groupStages.count { + ForEach(keys, id: \.self) { key in + if let _groupStages = times[key] { + let teams = _groupStages.flatMap { $0.teams() } + let callSeeds = teams.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + Section { + CallView.CallStatusView(count: callSeeds.count, total: teams.count, startDate: key) + } header: { + Text(_groupStages.map { $0.groupStageTitle() }.joined(separator: ", ")) + } footer: { + CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule") + } } } } diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index e41e049..d3b0400 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -15,7 +15,7 @@ struct SeedsCallingView: View { List { ForEach(tournament.rounds()) { round in let seeds = round.seeds() - let callSeeds = seeds.filter({ $0.callDate != nil }) + let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) if seeds.isEmpty == false { Section { NavigationLink { diff --git a/PadelClub/Views/Cashier/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift new file mode 100644 index 0000000..8942073 --- /dev/null +++ b/PadelClub/Views/Cashier/CashierDetailView.swift @@ -0,0 +1,76 @@ +// +// CashierDetailView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 31/03/2024. +// + +import SwiftUI + +struct CashierDetailView: View { + var tournaments : [Tournament] + + init(tournaments: [Tournament]) { + self.tournaments = tournaments + } + + init(tournament: Tournament) { + self.tournaments = [tournament] + } + + var body: some View { + List { + ForEach(tournaments) { tournament in + Section { + LabeledContent { + Text(tournament.earnings().formatted(.currency(code: "EUR").precision(.fractionLength(0)))) + } label: { + Text("Encaissement") + Text(tournament.paidCompletion().formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + } + _tournamentCashierDetailView(tournament) + } header: { + if tournaments.count > 1 { + Text(tournament.tournamentTitle()) + } + } + } + } + .headerProminence(.increased) + .navigationTitle("Bilan") + } + + private func _tournamentCashierDetailView(_ tournament: Tournament) -> some View { + DisclosureGroup { + ForEach(PlayerRegistration.PaymentType.allCases) { type in + let count = tournament.selectedPlayers().filter({ $0.registrationType == type }).count + LabeledContent { + if let entryFee = tournament.entryFee { + let sum = Double(count) * entryFee + Text(sum.formatted(.currency(code: "EUR"))) + } + } label: { + Text(type.localizedLabel()) + Text(count.formatted()) + } + } + } label: { + Text("Voir le détail") + } + +// +// Section { +// ForEach(tournaments) { tournament in +// } +//// HStack { +//// Text("Total") +//// Spacer() +//// Text(event.earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) +//// Text(event.paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) +//// } +// } header: { +// Text("Encaissement") +// } + + } +} diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift new file mode 100644 index 0000000..08d8331 --- /dev/null +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -0,0 +1,56 @@ +// +// CashierSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct CashierSettingsView: View { + @EnvironmentObject var dataStore: DataStore + var tournaments: [Tournament] + + init(tournaments: [Tournament]) { + self.tournaments = tournaments + } + + init(tournament: Tournament) { + self.tournaments = [tournament] + } + + var body: some View { + List { + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + let players = tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + if player.hasPaid() == false { + player.registrationType = .gift + } + } + try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players) + } + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") + } + + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + let players = tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + player.registrationType = nil + } + try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players) + } + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") + } + + } + } +} + +#Preview { + CashierSettingsView(tournaments: []) +} diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift new file mode 100644 index 0000000..2660f02 --- /dev/null +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -0,0 +1,311 @@ +// +// CashierView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 04/03/2023. +// + +import SwiftUI +import Combine + +struct CashierView: View { + @EnvironmentObject var dataStore: DataStore + var tournaments : [Tournament] + var teams: [TeamRegistration] + @State private var sortOption: SortOption = .callDate + @State private var filterOption: FilterOption = .all + @State private var sortOrder: SortOrder = .ascending + @State private var searchText = "" + @State private var isSearching: Bool = false + + init(event: Event) { + self.tournaments = event.tournaments + self.teams = [] + } + + init(tournament: Tournament, teams: [TeamRegistration]) { + self.tournaments = [tournament] + self.teams = teams + } + + private func _sharedData() -> String { + let players = teams + .flatMap({ $0.players() }) + .map { + [$0.pasteData()] + .compacted() + .joined(separator: "\n") + } + .joined(separator: "\n\n") + return players + } + + enum SortOption: Int, Identifiable, CaseIterable { + case teamRank + case alphabeticalLastName + case alphabeticalFirstName + case playerRank + case age + case callDate + + var id: Int { self.rawValue } + func localizedLabel() -> String { + switch self { + case .callDate: + return "Convocation" + case .teamRank: + return "Poids d'équipe" + case .alphabeticalLastName: + return "Nom" + case .alphabeticalFirstName: + return "Prénom" + case .playerRank: + return "Rang" + case .age: + return "Âge" + } + } + } + + enum FilterOption: Int, Identifiable, CaseIterable { + case all + case didPay + case didNotPay + + var id: Int { self.rawValue } + + func localizedLabel() -> String { + switch self { + case .all: + return "Tous" + case .didPay: + return "Réglé" + case .didNotPay: + return "Non réglé" + } + } + + func shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { + switch self { + case .all: + return true + case .didPay: + return player.hasPaid() + case .didNotPay: + return player.hasPaid() == false + + } + } + } + + var body: some View { + List { + if isSearching == false { + Section { + Picker(selection: $filterOption) { + ForEach(FilterOption.allCases) { filterOption in + Text(filterOption.localizedLabel()).tag(filterOption) + } + } label: { + Text("Statut du règlement") + } + + Picker(selection: $sortOption) { + ForEach(SortOption.allCases) { sortOption in + Text(sortOption.localizedLabel()).tag(sortOption) + } + } label: { + Text("Affichage par") + } + + Picker(selection: $sortOrder) { + Text("Croissant").tag(SortOrder.ascending) + Text("Décroissant").tag(SortOrder.descending) + } label: { + Text("Trier par ordre") + } + } header: { + Text("Options d'affichage") + } + } + + if _isContentUnavailable() { + _contentUnavailableView() + } + + switch sortOption { + case .teamRank: + _byTeamRankView() + case .alphabeticalLastName: + _byPlayerLastName() + case .alphabeticalFirstName: + _byPlayerFirstName() + case .playerRank: + _byPlayerRank() + case .age: + _byPlayerAge() + case .callDate: + _byCallDateView() + } + } + .headerProminence(.increased) + .searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur")) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + ShareLink(item: _sharedData()) + } + } + } + + @ViewBuilder + func computedPlayerView(_ player: PlayerRegistration) -> some View { + EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + } + + private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool { + team.players().allSatisfy({ + _shouldDisplayPlayer($0) + }) + } + + private func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { + if searchText.isEmpty == false { + filterOption.shouldDisplayPlayer(player) && player.contains(searchText) + } else { + filterOption.shouldDisplayPlayer(player) + } + } + + @ViewBuilder + private func _byPlayer(_ players: [PlayerRegistration]) -> some View { + let _players = sortOrder == .ascending ? players : players.reversed() + ForEach(_players) { player in + Section { + computedPlayerView(player) + } header: { + HStack { + if let teamCallDate = player.team()?.callDate { + Text(teamCallDate.localizedDate()) + } + Spacer() + Text(player.weight.formatted()) + } + } footer: { + if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() { + Text(tournamentTitle) + } + } + } + } + + @ViewBuilder + private func _byPlayerRank() -> some View { + let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.weight)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byPlayerAge() -> some View { + let players = teams.flatMap({ $0.players() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byPlayerLastName() -> some View { + let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.lastName)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byPlayerFirstName() -> some View { + let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.firstName)).filter({ _shouldDisplayPlayer($0) }) + _byPlayer(players) + } + + @ViewBuilder + private func _byTeamRankView() -> some View { + let _teams = sortOrder == .ascending ? teams : teams.reversed() + ForEach(_teams) { team in + if _shouldDisplayTeam(team) { + Section { + _cashierPlayersView(team.players()) + } header: { + HStack { + if let callDate = team.callDate { + Text(callDate.localizedDate()) + } + Spacer() + Text(team.weight.formatted()) + } + } footer: { + if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Text(tournamentTitle) + } + } + } + } + } + + + @ViewBuilder + private func _byCallDateView() -> some View { + let groupedTeams = Dictionary(grouping: teams) { team in + team.callDate + } + let keys = sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() + + ForEach(keys, id: \.self) { key in + if let _teams = groupedTeams[key] { + ForEach(_teams) { team in + if _shouldDisplayTeam(team) { + Section { + _cashierPlayersView(team.players()) + } header: { + Text(key.localizedDate()) + } footer: { + if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Text(tournamentTitle) + } + } + } + } + } + } + } + + @ViewBuilder + private func _cashierPlayersView(_ players: [PlayerRegistration]) -> some View { + ForEach(players) { player in + if _shouldDisplayPlayer(player) { + computedPlayerView(player) + } + } + } + + private func _isContentUnavailable() -> Bool { + switch sortOption { + case .teamRank, .callDate: + return teams.filter({ _shouldDisplayTeam($0) }).isEmpty + default: + return teams.flatMap({ $0.players() }).filter({ _shouldDisplayPlayer($0) }).isEmpty + } + } + + private func _unavailableIcon() -> String { + switch sortOption { + case .teamRank, .callDate: + return "person.2.slash.fill" + default: + return "person.slash.fill" + } + } + + @ViewBuilder + private func _contentUnavailableView() -> some View { + if isSearching { + ContentUnavailableView.search(text: searchText) + } else { + ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon()) + } + } +} diff --git a/PadelClub/Views/Cashier/PlayerListView.swift b/PadelClub/Views/Cashier/PlayerListView.swift new file mode 100644 index 0000000..14ddd19 --- /dev/null +++ b/PadelClub/Views/Cashier/PlayerListView.swift @@ -0,0 +1,18 @@ +// +// PlayerListView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct PlayerListView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + PlayerListView() +} diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index f6770ff..f753c68 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -339,6 +339,7 @@ enum Pratique: String, Codable { case beach = "BEACH" case padel = "PADEL" case tennis = "TENNIS" + case pickle = "PICKLE" } // MARK: - ClubMarker diff --git a/PadelClub/Views/ClubView.swift b/PadelClub/Views/ClubView.swift deleted file mode 100644 index 219d105..0000000 --- a/PadelClub/Views/ClubView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ClubView.swift -// PadelClub -// -// Created by Laurent Morvillier on 06/02/2024. -// - -import SwiftUI - -struct ClubView: View { - - var club: Club - - var body: some View { - List(club.tournaments) { tournament in - Text(tournament.tournamentTitle()) - }.navigationTitle(club.name) - } -} - -#Preview { - ClubView(club: Club(name: "AUC", acronym: "test", address: "")) -} diff --git a/PadelClub/Views/Components/FooterButtonView.swift b/PadelClub/Views/Components/FooterButtonView.swift new file mode 100644 index 0000000..2d7f99b --- /dev/null +++ b/PadelClub/Views/Components/FooterButtonView.swift @@ -0,0 +1,18 @@ +// +// FooterButtonView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct FooterButtonView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + FooterButtonView() +} diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 943aa37..9bbdbad 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -44,18 +44,27 @@ struct GenericDestinationPickerView: View { .opacity(selectedDestination?.id == destination.id ? 1.0 : 0.4) } .buttonStyle(.plain) - .overlay(alignment: .bottomTrailing) { - if let count = destination.badgeValue(), count > 0 { - Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") - .foregroundColor(.secondary) - .imageScale(.medium) - .background ( - Color(.systemBackground) - .clipShape(.circle) - ) - .offset(x: 5, y: 5) - } - } +// .overlay(alignment: .bottomTrailing) { +// if let badge = destination.badgeImage() { +// Image(systemName: badge.systemName()) +// .foregroundColor(badge.color()) +// .imageScale(.medium) +// .background ( +// Color(.systemBackground) +// .clipShape(.circle) +// ) +// .offset(x: 3, y: 3) +// } else if let count = destination.badgeValue(), count > 0 { +// Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") +// .foregroundColor(.red) +// .imageScale(.medium) +// .background ( +// Color(.systemBackground) +// .clipShape(.circle) +// ) +// .offset(x: 3, y: 3) +// } +// } } } .fixedSize() diff --git a/PadelClub/Views/Components/Labels.swift b/PadelClub/Views/Components/Labels.swift index 269c18d..eb1471c 100644 --- a/PadelClub/Views/Components/Labels.swift +++ b/PadelClub/Views/Components/Labels.swift @@ -15,13 +15,13 @@ struct LabelOptions: View { struct LabelStructure: View { var body: some View { - Label("Structure", systemImage: "hammer") + Label("Structure", systemImage: "hammer").labelStyle(.titleOnly) } } struct LabelSettings: View { var body: some View { - Label("Réglages", systemImage: "slider.horizontal.3") + Label("Réglages", systemImage: "slider.horizontal.3").labelStyle(.titleOnly) } } diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index 4f36123..1445dcb 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -19,27 +19,20 @@ struct MatchListView: View { var body: some View { if matches.isEmpty == false { Section { - if isExpanded { + DisclosureGroup(isExpanded: $isExpanded) { ForEach(matches) { match in MatchRowView(match: match, matchViewStyle: matchViewStyle) + .listRowInsets(EdgeInsets()) } - } - } header: { - Button { - isExpanded.toggle() } label: { - HStack { - Text(section.capitalized) - Spacer() - Text(matches.count.formatted()) - Image(systemName: isExpanded ? "chevron.down.circle" : "chevron.right.circle") + LabeledContent { + Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) + .foregroundStyle(.master) + } label: { + Text(section.firstCapitalized) } - .contentShape(Rectangle()) } - .buttonStyle(.plain) - .frame(maxWidth: .infinity) } - .headerProminence(.increased) } } } diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift index b63c450..bc7edc5 100644 --- a/PadelClub/Views/Components/RowButtonView.swift +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -14,66 +14,91 @@ struct RowButtonView: View { let title: String var systemImage: String? = nil var image: String? = nil - var animatedProgress: Bool = false let confirmationMessage: String - let action: () -> () + 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, animatedProgress: Bool = false, confirmationMessage: String? = nil, action: @escaping () -> Void) { + init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, action: @escaping (() -> ())) { self.role = role self.title = title self.systemImage = systemImage self.image = image - self.animatedProgress = animatedProgress self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage self.action = action } - + + init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, 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 + } + var body: some View { Button(role: role) { if role == .destructive { askConfirmation = true - } else { + } else if let action { action() + } else if let asyncAction { + isLoading = true + Task { + await asyncAction() + isLoading = false + } } } label: { HStack { - if animatedProgress { - Spacer() - ProgressView() - } else { - if let systemImage { - Image(systemName: systemImage) - .resizable() - .scaledToFit() - .frame(height: 24) - } - if let image { - Image(image) - .resizable() - .scaledToFit() - .frame(width: 32, height: 32) - } - Spacer() - Text(title) - .foregroundColor(.white) - .frame(height: 32) + if let systemImage { + Image(systemName: systemImage) + .resizable() + .scaledToFit() + .frame(height: 24) + } + if let image { + Image(image) + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) } Spacer() + Text(title) + .opacity(isLoading ? 0.0 : 1.0) + .foregroundColor(.white) + .frame(height: 32) + Spacer() } .font(.headline) } - .disabled(animatedProgress) + .overlay { + if isLoading { + ProgressView() + } + } + .disabled(isLoading) .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) - .tint(role == .destructive ? Color.red : Color.launchScreenBackground) + .tint(role == .destructive ? Color.red : Color.master) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(.zero)) .confirmationDialog("Confirmation", isPresented: $askConfirmation, titleVisibility: .visible) { Button("OK") { - action() + if let action { + action() + } else if let asyncAction { + isLoading = true + Task { + await asyncAction() + isLoading = false + } + } } Button("Annuler", role: .cancel) {} } message: { diff --git a/PadelClub/Views/Components/StepperView.swift b/PadelClub/Views/Components/StepperView.swift index 2df2604..197c2b8 100644 --- a/PadelClub/Views/Components/StepperView.swift +++ b/PadelClub/Views/Components/StepperView.swift @@ -13,12 +13,12 @@ struct StepperView: View { var title: String? = nil @Binding var count: Int - + var step: Int = 1 var minimum: Int? = nil var maximum: Int? = nil var body: some View { - VStack(spacing: 0) { + VStack { HStack(spacing: 8) { Button(action: { self._subtract() @@ -74,14 +74,14 @@ struct StepperView: View { if let maximum, self.count + 1 > maximum { return } - self.count += 1 + self.count += step } fileprivate func _subtract() { if let minimum, self.count - 1 < minimum { return } - self.count -= 1 + self.count -= step } } diff --git a/PadelClub/Views/ContentView.swift b/PadelClub/Views/ContentView.swift deleted file mode 100644 index 5d92628..0000000 --- a/PadelClub/Views/ContentView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// ContentView.swift -// PadelClub -// -// Created by Laurent Morvillier on 02/02/2024. -// - -import SwiftUI -import LeStorage - -struct ContentView: View { - - @StateObject var dataStore = DataStore() - - var body: some View { - NavigationStack { - - VStack { - - List(self.dataStore.clubs) { club in - - NavigationLink { - ClubView(club: club) - } label: { - Text(club.name) - } - } - - Button("add") { - self._add() - } - .padding() - .buttonStyle(.bordered) - } - .toolbar(content: { - ToolbarItem { - NavigationLink { - MainUserView() - .environmentObject(self.dataStore) - } label: { - Image(systemName: "person.circle.fill") - } - } - - ToolbarItem { - NavigationLink { - SubscriptionView() - } label: { - Image(systemName: "tennisball.circle.fill") - } - } - }) - .navigationTitle("Home") - - } - } - - func _add() { -// let id = (0...1000000).randomElement()! -// let club: Club = Club(name: "test\(id)", address: "some address") -// self.dataStore.clubs.addOrUpdate(instance: club) - -// for _ in 0...20 { -// var clubs: [Club] = [] -// for _ in 0...20 { -// let id = (0...1000000).randomElement()! -// let club: Club = Club(name: "test\(id)", acronym: "test", address: "some address") -// clubs.append(club) -// } -// do { -// try self.dataStore.clubs.append(contentOfs: clubs) -// } catch { -// Logger.error(error) -// } -// } - } - -} - -#Preview { - ContentView() -} diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index 3b9670f..9ee8d7e 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -40,12 +40,11 @@ struct EventCreationView: View { } if eventType == .approvedTournament { - Stepper(value: $duration, in: 1...3) { - HStack { - Text("Durée") - Spacer() - Text("\(duration) jour" + duration.pluralSuffix) - } + LabeledContent { + StepperView(count: $duration, minimum: 1, maximum: 3) + } label: { + Text("Durée") + Text("\(duration) jour" + duration.pluralSuffix) } } @@ -101,6 +100,7 @@ struct EventCreationView: View { tournaments.forEach { tournament in tournament.startDate = startingDate tournament.dayDuration = duration + tournament.setupFederalSettings() } try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments) diff --git a/PadelClub/Views/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Event/TournamentConfiguratorView.swift index 1f1231c..222fdaf 100644 --- a/PadelClub/Views/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Event/TournamentConfiguratorView.swift @@ -35,13 +35,11 @@ struct TournamentConfigurationView: View { Text(type.localizedLabel()).tag(type.rawValue) } } - - Stepper(value: $tournament.teamCount, in: minimumTeamsCount...maximumTeamsCount) { - HStack { - Text("Équipes souhaitées") - Spacer() - Text(tournament.teamCount.formatted()) - } + LabeledContent { + StepperView(count: $tournament.teamCount, minimum: minimumTeamsCount, maximum: maximumTeamsCount) + } label: { + Text("Équipes souhaitées") + Text(tournament.teamCount.formatted()) } } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 2789618..f276a95 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -34,26 +34,33 @@ struct GroupStageView: View { Section { _groupStageView() } header: { + if let startDate = groupStage.startDate { + Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) + } + } footer: { HStack { - if let startDate = groupStage.startDate { - Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) - } - Spacer() - Button { - if sortingMode == .weight { - sortingMode = .score + if sortingMode == .auto { + if groupStage.hasEnded() { + sortingMode = .weight + } else { + sortingMode = .score + } + } else if sortingMode == .weight { + sortingMode = .weight } else { sortingMode = .weight } } label: { Label(sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly) + .underline() } + .buttonStyle(.borderless) } - .buttonStyle(.plain) } - + .headerProminence(.increased) + MatchListView(section: "disponible", matches: groupStage.availableToStart()).id(UUID()) MatchListView(section: "en cours", matches: groupStage.runningMatches()).id(UUID()) MatchListView(section: "à lancer", matches: groupStage.readyMatches()).id(UUID()) diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index a5ea044..7cd5f0e 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -41,6 +41,10 @@ struct GroupStagesView: View { return groupStage.badgeValue() } } + + func badgeImage() -> Badge? { + nil + } } init(tournament: Tournament) { diff --git a/PadelClub/Views/Match/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift similarity index 64% rename from PadelClub/Views/Match/MatchDateView.swift rename to PadelClub/Views/Match/Components/MatchDateView.swift index 9dd35c5..d6e06f1 100644 --- a/PadelClub/Views/Match/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -8,13 +8,15 @@ import SwiftUI struct MatchDateView: View { + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore var match: Match var showPrefix: Bool = false var body: some View { Menu { - if match.startDate == nil { - Button("Commencer") { + if match.startDate == nil && match.isReady() { + Button("Démarrer") { match.startDate = Date() save() } @@ -23,12 +25,21 @@ struct MatchDateView: View { save() } } else { - Button("Recommencer") { - match.startDate = Date() - match.endDate = nil - save() + if match.isReady() { + Button("Démarrer maintenant") { + match.startDate = Date() + match.endDate = nil + save() + } + } else { + let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + Button("Décaler de \(estimatedDuration) minutes") { + match.startDate = match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0) + match.endDate = nil + save() + } } - Button("Remise à zéro") { + Button("Retirer l'horaire") { match.startDate = nil match.endDate = nil save() @@ -50,8 +61,16 @@ struct MatchDateView: View { if showPrefix { Text("en cours").font(.footnote).foregroundStyle(.secondary) } - Text(startDate, style: .timer) - .monospacedDigit() + if match.isReady() { + Text(startDate, style: .timer) + .monospacedDigit() + .foregroundStyle(Color.master) + .underline() + } else { + Text("en retard") + .foregroundStyle(Color.master) + .underline() + } } else if startDate.timeIntervalSinceNow <= 7200 && showPrefix { if showPrefix { Text("démarre dans") @@ -59,15 +78,21 @@ struct MatchDateView: View { } Text(startDate, style: .timer) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } else { if showPrefix { Text("le " + startDate.formatted(date: .abbreviated, time: .omitted)) .font(.footnote).foregroundStyle(.secondary) Text("à " + startDate.formatted(date: .omitted, time: .shortened)) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } else { Text(startDate.formatted(date: .abbreviated, time: .shortened)) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } } } @@ -81,11 +106,15 @@ struct MatchDateView: View { } Text(duration) .monospacedDigit() + .foregroundStyle(Color.master) + .underline() } if match.startDate == nil && match.hasEnded() == false { Text("démarrage").font(.footnote).foregroundStyle(.secondary) Text("non défini") + .foregroundStyle(Color.master) + .underline() } } } @@ -94,9 +123,7 @@ struct MatchDateView: View { func save() { do { -// match.currentTournament?.objectWillChange.send() -// match.objectWillChange.send() -// try viewContext.save() + try dataStore.matches.addOrUpdate(instance: match) } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift new file mode 100644 index 0000000..b09a46e --- /dev/null +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -0,0 +1,45 @@ +// +// MatchTeamDetailView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct MatchTeamDetailView: View { + let match: Match + + var body: some View { + NavigationStack { + let tournament = match.currentTournament() + List { + if let teamOne = match.team(.one) { + _teamDetailView(teamOne, inTournament: tournament) + } + if let teamTwo = match.team(.two) { + _teamDetailView(teamTwo, inTournament: tournament) + } + } + .headerProminence(.increased) + .tint(.master) + } + .presentationDetents([.fraction(0.66)]) + } + + @ViewBuilder + private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { + Section { + ForEach(team.players()) { player in + EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + } + } header: { + TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil) + } + } + +} + +#Preview { + MatchTeamDetailView(match: Match.mock()) +} diff --git a/PadelClub/Views/Match/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift similarity index 93% rename from PadelClub/Views/Match/PlayerBlockView.swift rename to PadelClub/Views/Match/Components/PlayerBlockView.swift index 68ee799..070928e 100644 --- a/PadelClub/Views/Match/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -43,9 +43,6 @@ struct PlayerBlockView: View { } private func _defaultLabel() -> String { - if match.upperBracketMatch(teamPosition)?.disabled == true { - return "Bye" - } return teamPosition.localizedLabel() } @@ -79,7 +76,7 @@ struct PlayerBlockView: View { Text("WO") } - if hideScore == false { + if hideScore == false && scores.isEmpty == false { ForEach(scores.indices, id: \.self) { index in let string = scores[index] if string.isEmpty == false { @@ -96,6 +93,8 @@ struct PlayerBlockView: View { .lineLimit(1) } } + } else if let team { + TeamWeightView(team: team, teamPosition: teamPosition) } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 8504db5..399f2b3 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -48,86 +48,40 @@ struct MatchDetailView: View { _fieldSetup = State(wrappedValue: .field(court)) } } - -// @ViewBuilder -// func entrantView(_ entrant: Entrant) -> some View { -// Section { -// ForEach(entrant.orderedPlayers) { player in -// if player.isPlaying(in: match) { -// playerView(player) -// } -// } -// } header: { -// LabeledContent { -// if let tournament = match.currentTournament, let index = tournament.indexOfEntrant(entrant) { -// Text("#\(index + 1)") -// } -// } label: { -// if let title = entrant.brand?.title { -// Text(title) -// } -// } -// } footer: { -// LabeledContent { -// let weight = entrant.orderedPlayers.filter { $0.isPlaying(in: match) }.map { $0.tournamentRank }.reduce(0, +) -// Text(weight.formatted()) -// } label: { -// Text("Poids de la paire") -// } -// } -// .headerProminence(.increased) -// } - -// @ViewBuilder -// func playerView(_ player: Player) -> some View { -// VStack(alignment: .leading) { -// HStack { -// Text(player.longLabel) -// Text(player.localizedAge) -// Spacer() -// Text(player.formattedRank) -// } -// -// if let computedClubName = player.computedClubName { -// Text(computedClubName).foregroundStyle(.secondary).font(.caption) -// } -// if let computedLicense = player.computedLicense { -// Text(computedLicense).foregroundStyle(.secondary).font(.caption) -// } -// } -// } var quickLookHeader: some View { Section { HStack { - if match.hasEnded() == false { - Menu { - Button("Non défini") { - match.removeCourt() + Menu { + Button("Non défini") { + match.removeCourt() + save() + } + ForEach(1...match.courtCount(), id: \.self) { courtIndex in + Button("Terrain #\(courtIndex.formatted())") { + match.setCourt(courtIndex) save() } - ForEach(1...match.courtCount(), id: \.self) { courtIndex in - Button("Terrain #\(courtIndex.formatted())") { - match.setCourt(courtIndex) - save() - } - } - } label: { - VStack(alignment: .leading) { - Text("terrain").font(.footnote).foregroundStyle(.secondary) - if let court = match.court { - Text("#" + court) - } else { - Text("Choisir") - } + } + } label: { + VStack(alignment: .leading) { + Text("terrain").font(.footnote).foregroundStyle(.secondary) + if let court = match.court { + Text("#" + court) + .foregroundStyle(Color.master) + .underline() + } else { + Text("Choisir") + .foregroundStyle(Color.master) + .underline() } } - .buttonStyle(.plain) } Spacer() MatchDateView(match: match, showPrefix: true) } .font(.title) + .buttonStyle(.plain) } footer: { // if match.hasWalkoutTeam() == false { // if let weatherData = match.weatherData { @@ -151,7 +105,6 @@ struct MatchDetailView: View { Section { MatchSummaryView(match: match, matchViewStyle: .plainStyle) - } header: { } footer: { if match.isEmpty() == false { HStack { @@ -171,34 +124,34 @@ struct MatchDetailView: View { } } - Section { - ForEach(match.teams()) { team in - ForEach(team.players().filter({ $0.hasPaid() == false })) { player in - HStack { - Text(player.playerLabel()) - Spacer() - //PlayerPayView(player: player) + let players = match.teams().flatMap { $0.players() } + let unpaid = players.filter({ $0.hasPaid() == false }) + + if unpaid.isEmpty == false { + Section { + DisclosureGroup { + ForEach(unpaid) { player in + LabeledContent { + PlayerPayView(player: player) + } label: { + Text(player.playerLabel()) + } + } + } label: { + LabeledContent { + Text(unpaid.count.formatted() + " / " + players.count.formatted()) + } label: { + Text("Encaissement manquant") } } } } - menuView } -// .sheet(isPresented: $showDetails) { -// NavigationStack { -// List { -// if let entrantOne = match.entrantOne() { -// entrantView(entrantOne) -// } -// if let entrantTwo = match.entrantTwo() { -// entrantView(entrantTwo) -// } -// } -// } -// .presentationDetents([.fraction(0.66)]) -// } + .sheet(isPresented: $showDetails) { + MatchTeamDetailView(match: match) + } .sheet(item: $scoreType, onDismiss: { if match.hasEnded() { dismiss() @@ -206,6 +159,7 @@ struct MatchDetailView: View { }) { scoreType in let matchDescriptor = MatchDescriptor(match: match) EditScoreView(matchDescriptor: matchDescriptor) + .tint(.master) // switch scoreType { // case .edition: @@ -305,7 +259,8 @@ struct MatchDetailView: View { // } // } .navigationTitle(match.matchTitle()) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } enum ScoreType: Int, Identifiable, Hashable { @@ -365,14 +320,15 @@ struct MatchDetailView: View { Section { if match.hasEnded() == false { + let rotationDuration = match.getDuration() Picker(selection: $startDateSetup) { if match.isReady() { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) Text("Tout de suite").tag(MatchDateSetup.now) } - Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-match.matchFormat.estimatedDuration)) - Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) + Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration)) + Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration)) Text("À").tag(MatchDateSetup.customDate) } label: { Text("Horaire") diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 79e1e3b..6d5dda4 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -10,13 +10,49 @@ import SwiftUI struct MatchRowView: View { var match: Match let matchViewStyle: MatchViewStyle - @Environment(\.editMode) private var editMode + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @ViewBuilder var body: some View { - if editMode?.wrappedValue.isEditing == true && match.isGroupStage() == false && match.isLoserBracket == false { + if isEditingTournamentSeed.wrappedValue == true && match.isGroupStage() == false && match.isLoserBracket == false { MatchSetupView(match: match) } else { +// MatchSummaryView(match: match, matchViewStyle: matchViewStyle) +// .overlay { +// if match.disabled { +// Image(systemName: "xmark") +// .resizable() +// .scaledToFit() +// .opacity(0.8) +// } +// } +// .contextMenu(menuItems: { +// Text("index: \(match.index)") +// Text("bye state : \(match.byeState)") +// Text("disable state : \(match.disabled)") +// Button("enable") { +// match._toggleMatchDisableState(false) +// } +// Button("disable") { +// match._toggleMatchDisableState(true) +// } +// Button("bye") { +// match.byeState = true +// } +// Button("not bye") { +// match.byeState = false +// } +// Button("solo toggle") { +// match.disabled.toggle() +// } +// Button("toggle fwrd match") { +// match._toggleForwardMatchDisableState(true) +// } +// Button("toggle loser match") { +// match._toggleLoserMatchDisableState(true) +// } +// }) + NavigationLink { MatchDetailView(match: match, matchViewStyle: matchViewStyle) } label: { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 6a26fd6..c29289a 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -14,8 +14,17 @@ struct MatchSetupView: View { @ViewBuilder var body: some View { - _teamView(inTeamPosition: .one) - _teamView(inTeamPosition: .two) + ForEach(TeamPosition.allCases) { teamPosition in + VStack(alignment: .leading) { + if teamPosition == .one { + Text("Branche du haut") + } + _teamView(inTeamPosition: teamPosition) + if teamPosition == .two { + Text("Branche du bas") + } + } + } } @ViewBuilder @@ -33,6 +42,7 @@ struct MatchSetupView: View { if match.isSeededBy(team: team, inTeamPosition: teamPosition) { team.bracketPosition = nil match.enableMatch() + try? dataStore.matches.addOrUpdate(instance: match) try? dataStore.teamRegistrations.addOrUpdate(instance: team) } else { match.teamWillBeWalkOut(team) @@ -88,7 +98,7 @@ struct MatchSetupView: View { } } } label: { - Text("Tirage").tag(nil as SeedInterval?) + Text("Tirer au sort").tag(nil as SeedInterval?) } .disabled(availableSeedGroups.isEmpty && walkOutSpot == false) @@ -106,9 +116,8 @@ struct MatchSetupView: View { } } .fixedSize(horizontal: false, vertical: true) - .buttonBorderShape(.capsule) - .buttonStyle(.borderedProminent) - + .buttonStyle(.borderless) + .underline() } } } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 3aec8bf..450ec45 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -19,7 +19,6 @@ struct ActivityView: View { @State private var viewStyle: AgendaDestination.ViewStyle = .list @State private var federalTournaments: [FederalTournament] = [] @State private var isGatheringFederalTournaments: Bool = false - @Binding var selectedTab: TabDestination? @State private var error: Error? var runningTournaments: [FederalTournamentHolder] { @@ -257,7 +256,7 @@ struct ActivityView: View { Text("Pour voir vos tournois tenup ici, indiquez vos clubs préférés.") } actions: { RowButtonView("Choisir mes clubs préférés") { - selectedTab = .umpire + navigation.selectedTab = .umpire } } } else { @@ -276,6 +275,5 @@ struct ActivityView: View { } #Preview { - ActivityView(selectedTab: .constant(.activity)) - .environmentObject(DataStore.shared) + ActivityView() } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index d61c9db..55cec3a 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -6,18 +6,20 @@ // import SwiftUI +import LeStorage struct MainView: View { @StateObject var dataStore = DataStore.shared @AppStorage("importingFiles") var importingFiles: Bool = false - + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @State private var checkingFilesAttempt: Int = 0 @State private var checkingFiles: Bool = false - @AppStorage("lastDataSource") var lastDataSource: String? - @AppStorage("lastDataSourceMaleUnranked") var lastDataSourceMaleUnranked: Int? - @AppStorage("lastDataSourceFemaleUnranked") var lastDataSourceFemaleUnranked: Int? - + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } + @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -25,10 +27,10 @@ struct MainView: View { animation: .default) private var players: FetchedResults - @State private var selectedTab: TabDestination? var body: some View { - TabView(selection: $selectedTab) { - ActivityView(selectedTab: $selectedTab) + @Bindable var navigation = navigation + TabView(selection: $navigation.selectedTab) { + ActivityView() .tabItem(for: .activity) TournamentOrganizerView() .tabItem(for: .tournamentOrganizer) @@ -61,15 +63,7 @@ struct MainView: View { func _activityStatusBoxView() -> some View { _activityStatus() - .font(.title3) - .frame(height: 28) - .padding() - .background { - RoundedRectangle(cornerRadius: 20, style: .continuous) - .fill(.white) - } - .shadow(radius: 2) - .offset(y: -64) + .toastFormatted() } @ViewBuilder @@ -95,7 +89,7 @@ struct MainView: View { } private func _checkSourceFileAvailability() async { - + print(dataStore.appSettings.lastDataSource) print("check internet") print("check files on internet") print("check if any files on internet are more recent than here") @@ -112,21 +106,18 @@ struct MainView: View { private func _startImporting() { importingFiles = true Task { - lastDataSource = await FileImportManager.shared.importDataFromFFT() + let lastDataSource = await FileImportManager.shared.importDataFromFFT() + dataStore.appSettings.lastDataSource = lastDataSource + dataStore.updateSettings() if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) { - await _calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) + await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) } importingFiles = false await _downloadPreviousDate() } } - - private func _calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { - lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) - lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false) - } - + private func _downloadPreviousDate() async { await SourceFileManager.shared.getAllFiles() } diff --git a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift index 8cd43e5..734e89a 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift @@ -24,6 +24,7 @@ struct TournamentOrganizerView: View { ContentUnavailableView("Aucun tournoi sélectionné", systemImage: "rectangle.slash", description: Text("Utilisez l'accès rapide ci-dessous pour éditer un tournoi et passer rapidement d'un tournoi à l'autre.")) .navigationTitle("Gestionnaire de tournois") .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } } Divider() diff --git a/PadelClub/Views/Navigation/PadelClubView.swift b/PadelClub/Views/Navigation/PadelClubView.swift index fd7d081..08c66d5 100644 --- a/PadelClub/Views/Navigation/PadelClubView.swift +++ b/PadelClub/Views/Navigation/PadelClubView.swift @@ -13,7 +13,12 @@ struct PadelClubView: View { @State private var checkingFiles: Bool = false @State private var importingFiles: Bool = false - @AppStorage("lastDataSource") var lastDataSource: String? + @EnvironmentObject var dataStore: DataStore + + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } + @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -36,17 +41,38 @@ struct PadelClubView: View { List { if let _lastDataSourceDate { Section { - HStack { - VStack(alignment: .leading) { - Text("Classement mensuel utilisé").font(.caption).foregroundStyle(.secondary) - Text(_lastDataSourceDate.monthYearFormatted) - } - Spacer() + LabeledContent { Image(systemName: "checkmark") + } label: { + Text(_lastDataSourceDate.monthYearFormatted) + Text("Classement mensuel utilisé") } } } + let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed() + ForEach(monthData) { monthData in + Section { + LabeledContent { + if let maleUnrankedValue = monthData.maleUnrankedValue { + Text(maleUnrankedValue.formatted()) + } + } label: { + Text("Messieurs") + Text("Rang d'un non classé") + } + LabeledContent { + if let femaleUnrankedValue = monthData.femaleUnrankedValue { + Text(femaleUnrankedValue.formatted()) + } + } label: { + Text("Dames") + Text("Rang d'une non classée") + } + } header: { + Text(monthData.monthKey) + } + } // // if players.isEmpty { // ContentUnavailableView { @@ -60,6 +86,7 @@ struct PadelClubView: View { // } // } } + .headerProminence(.increased) .navigationTitle(TabDestination.padelClub.title) // .task { // await self._checkSourceFileAvailability() @@ -101,7 +128,12 @@ struct PadelClubView: View { private func _startImporting() { importingFiles = true Task { - lastDataSource = await FileImportManager.shared.importDataFromFFT() + let lastDataSource = await FileImportManager.shared.importDataFromFFT() + dataStore.appSettings.lastDataSource = lastDataSource + dataStore.updateSettings() + if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) { + await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) + } importingFiles = false } } diff --git a/PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift new file mode 100644 index 0000000..1af446c --- /dev/null +++ b/PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift @@ -0,0 +1,25 @@ +// +// DurationSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct DurationSettingsView: View { + var body: some View { + List { + ForEach(MatchFormat.allCases, id: \.self) { matchFormat in + MatchFormatStorageView(matchFormat: matchFormat) + } + } + .navigationTitle("Durées moyennes") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +#Preview { + DurationSettingsView() +} diff --git a/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift new file mode 100644 index 0000000..3fc5739 --- /dev/null +++ b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift @@ -0,0 +1,67 @@ +// +// GlobalSettingsView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 16/10/2023. +// + +import SwiftUI + +struct GlobalSettingsView: View { + @EnvironmentObject var dataStore : DataStore + + var body: some View { + @Bindable var appSettings = dataStore.appSettings + List { + Section { + Picker(selection: $appSettings.groupStageMatchFormatPreference) { + Text("Automatique").tag(nil as Int?) + ForEach(MatchFormat.allCases, id: \.self) { format in + Text(format.format).tag(format.rawValue as Int?) + } + } label: { + HStack { + Text("Poule") + Spacer() + } + } + Picker(selection: $appSettings.bracketMatchFormatPreference) { + Text("Automatique").tag(nil as Int?) + ForEach(MatchFormat.allCases, id: \.self) { format in + Text(format.format).tag(format.rawValue as Int?) + } + } label: { + HStack { + Text("Tableau") + Spacer() + } + } + Picker(selection: $appSettings.loserBracketMatchFormatPreference) { + Text("Automatique").tag(nil as Int?) + ForEach(MatchFormat.allCases, id: \.self) { format in + Text(format.format).tag(format.rawValue as Int?) + } + } label: { + HStack { + Text("Match de classement") + Spacer() + } + } + } header: { + Text("Vos formats préférés") + } footer: { + Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") + } + } + .onChange(of: [ + appSettings.bracketMatchFormatPreference, + appSettings.groupStageMatchFormatPreference, + appSettings.loserBracketMatchFormatPreference + ]) { + dataStore.updateSettings() + } + .navigationTitle("Formats par défaut") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift new file mode 100644 index 0000000..717c440 --- /dev/null +++ b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift @@ -0,0 +1,50 @@ +// +// MatchFormatStorageView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct MatchFormatStorageView: View { + @State private var estimatedDuration: Int + @EnvironmentObject var dataStore: DataStore + + let matchFormat: MatchFormat + + init(matchFormat: MatchFormat) { + self.matchFormat = matchFormat + _estimatedDuration = State(wrappedValue: matchFormat.getEstimatedDuration()) + } + + var body: some View { + Section { + LabeledContent { + StepperView(title: "minutes", count: $estimatedDuration, step: 5) + } label: { + Text("Durée \(matchFormat.format)") + Text(matchFormat.computedShortLabelWithoutPrefix) + } + } footer: { + if estimatedDuration != matchFormat.defaultEstimatedDuration { + HStack { + Spacer() + Button { + self.estimatedDuration = matchFormat.defaultEstimatedDuration + } label: { + Text("remettre la durée par défault") + .underline() + } + .buttonStyle(.borderless) + + } + } + } + .onChange(of: estimatedDuration) { + dataStore.appSettings.saveMatchFormatsDefaultDuration(matchFormat, estimatedDuration: estimatedDuration) + dataStore.updateSettings() + } + } +} + diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index e5bae96..afb41de 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -11,17 +11,34 @@ struct ToolboxView: View { var body: some View { NavigationStack { List { - NavigationLink { - SelectablePlayerListView() - } label: { - Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") + Section { + NavigationLink { + SelectablePlayerListView() + } label: { + Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") + } } - NavigationLink { - RankCalculatorView() - } label: { - Label("Calculateur de points", systemImage: "scalemass") + + Section { + NavigationLink { + RankCalculatorView() + } label: { + Label("Calculateur de points", systemImage: "scalemass") + } } + Section { + NavigationLink { + GlobalSettingsView() + } label: { + Label("Formats de jeu par défaut", systemImage: "megaphone") + } + NavigationLink { + DurationSettingsView() + } label: { + Label("Estimation des durées moyennes", systemImage: "deskclock") + } + } } .navigationTitle(TabDestination.toolbox.title) } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index f667060..a71c0fd 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -64,7 +64,6 @@ struct UmpireView: View { user.licenceId = nil dataStore.setUser(user) } - .font(.caption) } } @@ -83,7 +82,6 @@ struct UmpireView: View { user.club = nil dataStore.setUser(user) } - .font(.caption) } } } diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift new file mode 100644 index 0000000..1c30303 --- /dev/null +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -0,0 +1,54 @@ +// +// DateUpdateManagerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +enum DateUpdate { + case nextRotation + case previousRotation + case tomorrowAtNine + case inMinutes(Int) + case afterRound(Round) + case afterGroupStage(GroupStage) +} + +struct DateUpdateManagerView: View { + @Binding var startDate: Date + @State private var dateUpdated: Bool = false + + var validateAction: () -> Void + + var body: some View { + HStack { + Menu { + Text("à demain 9h") + Text("à la prochaine rotation") + Text("à la précédente rotation") + } label: { + Text("décaler") + .underline() + } + Spacer() + + if dateUpdated { + Button { + validateAction() + dateUpdated = false + } label: { + Text("valider la modification") + .underline() + } + } + } + .font(.subheadline) + .buttonStyle(.borderless) + .onChange(of: startDate) { + dateUpdated = true + } + } +} + diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift new file mode 100644 index 0000000..3f34b5b --- /dev/null +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -0,0 +1,157 @@ +// +// CourtAvailabilitySettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/04/2024. +// + +import SwiftUI + +struct CourtAvailabilitySettingsView: View { + @Environment(Tournament.self) var tournament: Tournament + @State private var courtsUnavailability: [Int: [DateInterval]] = [Int:[DateInterval]]() + @State private var showingPopover: Bool = false + @State private var courtIndex: Int = 0 + @State private var startDate: Date = Date() + @State private var endDate: Date = Date() + + var body: some View { + List { + let keys = courtsUnavailability.keys.sorted(by: \.self) + ForEach(keys, id: \.self) { key in + if let dates = courtsUnavailability[key] { + Section { + ForEach(dates) { dateInterval in + HStack { + VStack(alignment: .leading, spacing: 0) { + Text(dateInterval.startDate.localizedTime()).font(.largeTitle) + Text(dateInterval.startDate.localizedDay()).font(.caption) + } + Spacer() + Image(systemName: "arrowshape.forward.fill") + .tint(.master) + Spacer() + VStack(alignment: .trailing, spacing: 0) { + Text(dateInterval.endDate.localizedTime()).font(.largeTitle) + Text(dateInterval.endDate.localizedDay()).font(.caption) + } + } + .contextMenu(menuItems: { + Button("dupliquer") { + + } + Button("éditer") { + + } + Button("effacer") { + + } + }) + .swipeActions { + Button(role: .destructive) { + courtsUnavailability[key]?.removeAll(where: { $0.id == dateInterval.id }) + } label: { + LabelDelete() + } + } + } + } header: { + Text("Terrain #\(key + 1)") + } + .headerProminence(.increased) + } + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showingPopover = true + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 28) + } + } + } + .onDisappear { + tournament.courtsUnavailability = courtsUnavailability + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Créneaux") + .popover(isPresented: $showingPopover) { + NavigationStack { + Form { + Section { + CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: 3) + } + + Section { + DatePicker("Début", selection: $startDate) + DatePicker("Fin", selection: $endDate) + } footer: { + Button("jour entier") { + startDate = startDate.startOfDay + endDate = endDate.endOfDay() + } + .buttonStyle(.borderless) + .underline() + } + } + .toolbar { + Button("Valider") { + let dateInterval = DateInterval(startDate: startDate, endDate: endDate) + var courtUnavailability = courtsUnavailability[courtIndex] ?? [DateInterval]() + courtUnavailability.append(dateInterval) + courtsUnavailability[courtIndex] = courtUnavailability + showingPopover = false + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Nouveau créneau") + } + .onAppear { + UIDatePicker.appearance().minuteInterval = 5 + } + .onDisappear { + UIDatePicker.appearance().minuteInterval = 1 + } + } + } +} + +struct CourtPicker: View { + let title: String + @Binding var selection: Int + let maxCourt: Int + + var body: some View { + Picker(title, selection: $selection) { + ForEach(0.. 1 { +// ForEach(0..= tournament.startDate } ) -// if times.isEmpty { -// groupStages.forEach({ $0.startDate = tournament.startDate }) -// times.insert(tournament.startDate) -// try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) -// } - var lastDate : Date = tournament.startDate groupStages.chunked(into: groupStageCourtCount).forEach { groups in groups.forEach({ $0.startDate = lastDate }) @@ -191,11 +213,12 @@ struct PlanningSettingsView: View { dispatch.timedMatches.forEach { matchSchedule in if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { - let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 + let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60 if let startDate = match.groupStageObject?.startDate { let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) match.startDate = matchStartDate - lastDate = matchStartDate.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60) + lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60) } match.setCourt(matchSchedule.courtIndex + 1) } @@ -204,9 +227,6 @@ struct PlanningSettingsView: View { try? dataStore.matches.addOrUpdate(contentOfs: matches) matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) - - scheduleSetup = true - } private func _save() { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 6214d5b..3cf5c2e 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -9,8 +9,8 @@ import SwiftUI struct PlanningView: View { @EnvironmentObject var dataStore: DataStore - @Environment(\.editMode) private var editMode - + @Environment(Tournament.self) var tournament: Tournament + let matches: [Match] @State private var timeSlots: [Date:[Match]] @State private var days: [Date] @@ -30,85 +30,27 @@ struct PlanningView: View { Section { ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in if let _matches = timeSlots[key] { - if editMode?.wrappedValue.isEditing == true { - HStack { - VStack(alignment: .leading) { - let index = keys.firstIndex(of: key) - Button { - let previousKey = keys[index! - 1] - let previousMatches = timeSlots[previousKey] - previousMatches?.forEach { match in - match.startDate = key - } - _matches.forEach { match in - match.startDate = previousKey - } - _update() - } label: { - Image(systemName: "arrow.up") - } - .buttonStyle(.bordered) - .disabled(index == 0) - Button { - let nextKey = keys[index! + 1] - let nextMatches = timeSlots[nextKey] - nextMatches?.forEach { match in - match.startDate = key - } - _matches.forEach { match in - match.startDate = nextKey - } - _update() - } label: { - Image(systemName: "arrow.down") - } - .buttonStyle(.bordered) - .disabled(index == keys.count - 1) - } - VStack(alignment: .leading) { + DisclosureGroup { + ForEach(_matches) { match in + NavigationLink { + MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) + } label: { LabeledContent { - Text(_matches.count.formatted() + " match" + _matches.count.pluralSuffix) - } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle) - } - - ForEach(_matches) { match in - LabeledContent { - Text(match.matchFormat.format) - } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle()) - } else if let round = match.roundObject { - Text(round.roundTitle()) - } - Text(match.matchTitle()) + if let court = match.court { + Text(court) } - } - } - } - } else { - DisclosureGroup { - ForEach(_matches) { match in - NavigationLink { - MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) } label: { - LabeledContent { - if let court = match.court { - Text(court) - } - } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle()) - } else if let round = match.roundObject { - Text(round.roundTitle()) - } - Text(match.matchTitle()) + if let groupStage = match.groupStageObject { + Text(groupStage.groupStageTitle()) + } else if let round = match.roundObject { + Text(round.roundTitle()) } + Text(match.matchTitle()) } } - } label: { - _timeSlotView(key: key, matches: _matches) } + } label: { + _timeSlotView(key: key, matches: _matches) } } } @@ -118,29 +60,9 @@ struct PlanningView: View { .headerProminence(.increased) } } - .toolbar { - EditButton() - } - .onChange(of: isEditing) { old, new in - if old == true && new == false { - print("save") - try? dataStore.matches.addOrUpdate(contentOfs: matches) - } - } .navigationTitle("Programmation") } - - private func _update() { - let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } - self.timeSlots = timeSlots - self.days = Set(timeSlots.keys.map { $0.startOfDay }).sorted() - self.keys = timeSlots.keys.sorted() - } - - private var isEditing: Bool { - editMode?.wrappedValue.isEditing == true - } - + private func _timeSlotView(key: Date, matches: [Match]) -> some View { LabeledContent { Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index d16ba98..007b19e 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -13,7 +13,7 @@ struct RoundScheduleEditorView: View { var round: Round @State private var startDate: Date - + init(round: Round) { self.round = round self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date()) @@ -25,10 +25,23 @@ struct RoundScheduleEditorView: View { Section { MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday())) + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) } - RowButtonView("Valider la modification") { - _updateSchedule() + } footer: { + HStack { + DateUpdateManagerView(startDate: $startDate) { + _updateSchedule() + } + + Spacer() + + if let roundStartDate = round.startDate { + Button("horaire automatique") { + round.startDate = nil + } + .underline() + .buttonStyle(.borderless) + } } } @@ -36,6 +49,8 @@ struct RoundScheduleEditorView: View { MatchScheduleEditorView(match: match) } } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } private func _updateSchedule() { @@ -47,6 +62,7 @@ struct RoundScheduleEditorView: View { _save() MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + round.startDate = startDate _save() } diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift new file mode 100644 index 0000000..b75a617 --- /dev/null +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -0,0 +1,104 @@ +// +// EditablePlayerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct EditablePlayerView: View { + + enum PlayerEditingOption { + case payment + case licenceId + } + + @EnvironmentObject var dataStore: DataStore + var player: PlayerRegistration + var editingOptions: [PlayerEditingOption] + @State private var editedLicenceId = "" + @State private var shouldPresentLicenceIdEdition: Bool = false + + var body: some View { + computedPlayerView(player) + .alert("Numéro de licence", isPresented: $shouldPresentLicenceIdEdition) { + TextField("Numéro de licence", text: $editedLicenceId) + .onSubmit { + player.licenceId = editedLicenceId + editedLicenceId = "" + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + } + } + } + + @ViewBuilder + func computedPlayerView(_ player: PlayerRegistration) -> some View { + VStack(alignment: .leading) { + ImportedPlayerView(player: player) + HStack { + Text(player.isImported() ? "importé" : "non importé") + Text(player.formattedLicense().isLicenseNumber ? "valide" : "non valide") + } + HStack { + Menu { + if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler", systemImage: "phone") + } + } + if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "sms:\(number)") { + Link(destination: url) { + Label("SMS", systemImage: "message") + } + } + + if editingOptions.contains(.licenceId) { + Divider() + if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil { + Button { + player.validateLicenceId(licenseYearValidity) + } label: { + Text("Valider la licence \(licenseYearValidity)") + } + } + } + + if let license = player.licenceId?.strippedLicense { + Button { + let pasteboard = UIPasteboard.general + pasteboard.string = license + } label: { + Label("Copier la licence", systemImage: "doc.on.doc") + } + } + + Section { + Button { + editedLicenceId = player.licenceId ?? "" + shouldPresentLicenceIdEdition = true + } label: { + if player.licenceId == nil { + Text("Ajouter la licence") + } else { + Text("Modifier la licence") + } + } + PasteButton(payloadType: String.self) { strings in + guard let first = strings.first else { return } + player.licenceId = first + } + } header: { + Text("Modification de licence") + } + } label: { + Text("Options") + } + if editingOptions.contains(.payment) { + Spacer() + PlayerPayView(player: player) + } + } + } + } +} diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift new file mode 100644 index 0000000..6b0624a --- /dev/null +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -0,0 +1,114 @@ +// +// PlayerDetailView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct PlayerDetailView: View { + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + @Bindable var player: PlayerRegistration + @FocusState private var textFieldIsFocus: Bool + + var body: some View { + Form { + Section { + LabeledContent { + TextField("Nom", text: $player.lastName) + .keyboardType(.alphabet) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } label: { + Text("Nom") + } + + LabeledContent { + TextField("Prénom", text: $player.firstName) + .keyboardType(.alphabet) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } label: { + Text("Prénom") + } + + PlayerSexPickerView(player: player) + } + + Section { + LabeledContent { + TextField("Rang", value: $player.rank, format: .number) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($textFieldIsFocus) + } label: { + Text("Rang") + } + } header: { + Text("Classement actuel") + } + + if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank { + Section { + let value = PlayerRegistration.addon(for: rank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) + LabeledContent { + Text(value.formatted()) + } label: { + Text("Valeur à rajouter") + } + LabeledContent { + TextField("Rang", value: $player.weight, format: .number) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($textFieldIsFocus) + } label: { + Text("Poids re-calculé") + } + } header: { + Text("Ré-assimilation") + } footer: { + Text("Calculé en fonction du sexe") + } + } + } + .scrollDismissesKeyboard(.immediately) + .onChange(of: player.sex) { + _save() + } + .onChange(of: player.weight) { + player.team()?.updateWeight() + _save() + } + .onChange(of: player.rank) { + player.setWeight(in: tournament) + player.team()?.updateWeight() + _save() + } + .headerProminence(.increased) + .navigationTitle("Édition") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .keyboard) { + Button("Valider") { + textFieldIsFocus = false + } + } + } + } + + private func _save() { + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + if let team = player.team() { + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } + } +} + +#Preview { + PlayerDetailView(player: PlayerRegistration.mock()) +} diff --git a/PadelClub/Views/Round/LoserBracketView.swift b/PadelClub/Views/Round/LoserBracketView.swift deleted file mode 100644 index 470c033..0000000 --- a/PadelClub/Views/Round/LoserBracketView.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// LoserBracketView.swift -// PadelClub -// -// Created by Razmig Sarkissian on 04/04/2024. -// - -import SwiftUI - -struct LoserBracketView: View { - @EnvironmentObject var dataStore: DataStore - let loserRounds: [Round] - - @ViewBuilder - var body: some View { - if let first = loserRounds.first { - List { - ForEach(loserRounds) { loserRound in - _loserRoundView(loserRound) - let childLoserRounds = loserRound.loserRounds() - if childLoserRounds.isEmpty == false { - let uniqueChildRound = childLoserRounds.first - if childLoserRounds.count == 1, let uniqueChildRound { - _loserRoundView(uniqueChildRound) - } else if let uniqueChildRound { - NavigationLink { - LoserBracketView(loserRounds: childLoserRounds) - } label: { - Text(uniqueChildRound.roundTitle()) - } - } - } - } - } - .navigationTitle(first.roundTitle()) - } - } - - private func _loserRoundView(_ loserRound: Round) -> some View { - Section { - ForEach(loserRound.playedMatches()) { match in - MatchRowView(match: match, matchViewStyle: .standardStyle) - } - } header: { - Text(loserRound.roundTitle()) - } - } -} - -#Preview { - LoserBracketView(loserRounds: [Round.mock()]) - .environmentObject(DataStore.shared) -} diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift new file mode 100644 index 0000000..424b7f4 --- /dev/null +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -0,0 +1,72 @@ +// +// LoserRoundView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 04/04/2024. +// + +import SwiftUI + +struct LoserRoundView: View { + @EnvironmentObject var dataStore: DataStore + let loserRounds: [Round] + @State private var isEditingTournamentSeed: Bool = false + + private func _roundDisabled() -> Bool { + loserRounds.allSatisfy({ $0.isDisabled() }) + } + + var body: some View { + List { + if isEditingTournamentSeed == true { + _editingView() + } + + ForEach(loserRounds) { loserRound in + if isEditingTournamentSeed || loserRound.isDisabled() == false { + Section { + let matches = isEditingTournamentSeed ? loserRound.playedMatches() : loserRound.playedMatches().filter({ $0.disabled == false }) + ForEach(matches) { match in + MatchRowView(match: match, matchViewStyle: .standardStyle) + .overlay { + if match.disabled /*&& isEditingTournamentSeed*/ { + Image(systemName: "xmark") + .resizable() + .scaledToFit() + .opacity(0.8) + } + } + .disabled(match.disabled) + } + } header: { + Text(loserRound.roundTitle(.wide)) + } + } + } + } + .headerProminence(.increased) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(isEditingTournamentSeed == true ? "Valider" : "Modifier") { + isEditingTournamentSeed.toggle() + } + } + } + } + + private func _editingView() -> some View { + if _roundDisabled() { + RowButtonView("Jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + round.enableRound() + } + } + } else { + RowButtonView("Ne pas jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + round.disableRound() + } + } + } + } +} diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index fd9dd32..0921324 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -7,90 +7,81 @@ import SwiftUI +struct LoserRound: Identifiable, Selectable { + let turnIndex: Int + let rounds: [Round] + + var id: Int { + return turnIndex + } + + + static func updateDestinations(fromLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [LoserRound] { + var rounds = [LoserRound]() + for (index, round) in loserRounds.enumerated() { + rounds.append(LoserRound(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index))) + } + + return rounds + } + + static func enabledLoserRounds(inLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [Round] { + return loserRounds.filter { loserRound in + upperBracketRound.loserRounds(forRoundIndex: loserRound.index).anySatisfy({ $0.isDisabled() == false }) + } + } + + +} + +extension LoserRound { + func selectionLabel() -> String { + return "Tour #\(turnIndex + 1)" + } + + func badgeValue() -> Int? { + return rounds.flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count + } + + func badgeImage() -> Badge? { + return rounds.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil + } +} + + struct LoserRoundsView: View { + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed var upperBracketRound: Round - @State private var selectedRound: Round? + @State private var selectedRound: LoserRound? let loserRounds: [Round] - + @State private var allDestinations: [LoserRound] + init(upperBracketRound: Round) { self.upperBracketRound = upperBracketRound - self.loserRounds = upperBracketRound.loserRounds() - _selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound()) + let _loserRounds = upperBracketRound.loserRounds() + self.loserRounds = _loserRounds + let enabledLoserRounds = LoserRound.enabledLoserRounds(inLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound) + let rounds = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound) + _allDestinations = State(wrappedValue: rounds) + + _selectedRound = State(wrappedValue: rounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? rounds.first) } var body: some View { VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: loserRounds, nilDestinationIsValid: true) - switch selectedRound { - case .none: - List { - RowButtonView("Effacer", role: .destructive) { - - } - } - case .some(let selectedRound): - LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index)) - } + GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: allDestinations, nilDestinationIsValid: false) + LoserRoundView(loserRounds: selectedRound!.rounds) } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - } -} - -struct LoserRoundView: View { - @EnvironmentObject var dataStore: DataStore - let loserRounds: [Round] - @Environment(\.editMode) private var editMode - - private func _roundDisabled() -> Bool { - loserRounds.allSatisfy({ $0.isDisabled() }) - } - - var body: some View { - List { - if editMode?.wrappedValue.isEditing == true { - _editingView() - } - - ForEach(loserRounds) { loserRound in - Section { - ForEach(loserRound.playedMatches()) { match in - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) - .overlay { - if match.disabled { - Image(systemName: "xmark") - .resizable() - .scaledToFit() - .opacity(0.8) - } - } - .disabled(match.disabled) - } - } header: { - Text(loserRound.roundTitle(.wide)) - } - } - } - .headerProminence(.increased) - .toolbar { - EditButton() + .onChange(of: isEditingTournamentSeed.wrappedValue) { + _updateDestinations() } } - private func _editingView() -> some View { - if _roundDisabled() { - RowButtonView("Jouer ce tour", role: .destructive) { - loserRounds.forEach { round in - round.enableRound() - round.handleLoserRoundState() - } - } - } else { - RowButtonView("Ne pas jouer ce tour", role: .destructive) { - loserRounds.forEach { round in - round.disableRound() - } - } - } + private func _updateDestinations() { + let enabledLoserRounds = isEditingTournamentSeed.wrappedValue ? loserRounds : LoserRound.enabledLoserRounds(inLoserRounds: loserRounds, inUpperBracketRound: upperBracketRound) + + self.allDestinations = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound) } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 8a412a0..1b535ec 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -9,11 +9,21 @@ import SwiftUI struct RoundSettingsView: View { @EnvironmentObject var dataStore: DataStore - @Environment(\.editMode) private var editMode + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(Tournament.self) var tournament: Tournament var body: some View { List { + Section { + RowButtonView("Enabled", role: .destructive) { + let allMatches = tournament._allMatchesIncludingDisabled() + allMatches.forEach({ + $0.disabled = false + $0.byeState = false + }) + try? dataStore.matches.addOrUpdate(contentOfs: allMatches) + } + } Section { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) @@ -21,7 +31,7 @@ struct RoundSettingsView: View { tournament.allRounds().forEach({ round in round.enableRound() }) - editMode?.wrappedValue = .active + self.isEditingTournamentSeed.wrappedValue = true } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index cc1cb11..7d78d83 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -8,7 +8,7 @@ import SwiftUI struct RoundView: View { - @Environment(\.editMode) private var editMode + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore @@ -17,16 +17,18 @@ struct RoundView: View { var body: some View { List { - if editMode?.wrappedValue.isEditing == false { + if isEditingTournamentSeed.wrappedValue == false { let loserRounds = round.loserRounds() - if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { + //(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue }) + if loserRounds.isEmpty == false, let first = loserRounds.first { + let correspondingLoserRoundTitle = round.correspondingLoserRoundTitle() Section { NavigationLink { LoserRoundsView(upperBracketRound: round) .environment(tournament) - .navigationTitle(first.roundTitle()) + .navigationTitle(correspondingLoserRoundTitle) } label: { - Text(first.roundTitle()) + Text(correspondingLoserRoundTitle) } } } @@ -34,10 +36,9 @@ struct RoundView: View { RowButtonView("Placer \(availableSeedGroup.localizedLabel())") { tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup) - try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) - if tournament.availableSeeds().isEmpty { - editMode?.wrappedValue = .inactive + _save() + self.isEditingTournamentSeed.wrappedValue = false } } } @@ -53,10 +54,21 @@ struct RoundView: View { .headerProminence(.increased) .toolbar { ToolbarItem(placement: .topBarTrailing) { - EditButton() + Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { + if isEditingTournamentSeed.wrappedValue { + _save() + } + isEditingTournamentSeed.wrappedValue.toggle() + } } } } + + private func _save() { + try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) + let allRoundMatches = tournament.allRoundMatches() + try? DataStore.shared.matches.addOrUpdate(contentOfs: allRoundMatches) + } } #Preview { diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index 1134fed..db3b83d 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -10,13 +10,13 @@ import SwiftUI struct RoundsView: View { var tournament: Tournament @State private var selectedRound: Round? - @State var editMode: EditMode = .inactive + @State private var isEditingTournamentSeed = false init(tournament: Tournament) { self.tournament = tournament _selectedRound = State(wrappedValue: tournament.getActiveRound()) if tournament.availableSeeds().isEmpty == false { - _editMode = .init(wrappedValue: .active) + _isEditingTournamentSeed = State(wrappedValue: true) } } @@ -32,7 +32,7 @@ struct RoundsView: View { .navigationTitle(selectedRound.roundTitle()) } } - .environment(\.editMode, $editMode) + .environment(\.isEditingTournamentSeed, $isEditingTournamentSeed) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 3c0cd33..a1bba95 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -22,7 +22,10 @@ struct EditScoreView: View { Form { Section { Text(matchDescriptor.teamLabelOne) - Text(matchDescriptor.teamLabelTwo) + HStack { + Spacer() + Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing) + } } footer: { HStack { Menu { @@ -37,7 +40,8 @@ struct EditScoreView: View { Text(matchDescriptor.teamLabelTwo) } } label: { - Text("Forfait") + Text("Forfait d'une équipe ?") + .underline() } Spacer() diff --git a/PadelClub/Views/Score/PointSelectionView.swift b/PadelClub/Views/Score/PointSelectionView.swift index 40d0579..1b70405 100644 --- a/PadelClub/Views/Score/PointSelectionView.swift +++ b/PadelClub/Views/Score/PointSelectionView.swift @@ -13,8 +13,8 @@ struct PointSelectionView: View { var possibleValues: [Int] var disableValues: [Int] = [] var deleteAction: () -> () - let gridItems: [GridItem] = [GridItem(.adaptive(minimum: 65), spacing: 20)] - + let columns = Array(repeating: GridItem(.flexible()), count: 3) + init(valueSelected: Binding, values: [Int], possibleValues: [Int], disableValues: [Int], deleteAction: @escaping () -> Void) { _valueSelected = valueSelected @@ -26,12 +26,13 @@ struct PointSelectionView: View { var body: some View { - LazyVGrid(columns: gridItems, alignment: .center, spacing: 20) { + LazyVGrid(columns: columns, alignment: .center, spacing: 8) { ForEach(possibleValues, id: \.self) { value in Button { valueSelected = value } label: { PointView(value: "\(value).circle.fill") + .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) @@ -41,10 +42,11 @@ struct PointSelectionView: View { deleteAction() } label: { PointView(value: "delete.left.fill") + .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) } - .padding() + .padding(8) } } diff --git a/PadelClub/Views/Score/PointView.swift b/PadelClub/Views/Score/PointView.swift index 471c749..c72fb1d 100644 --- a/PadelClub/Views/Score/PointView.swift +++ b/PadelClub/Views/Score/PointView.swift @@ -15,7 +15,7 @@ struct PointView: View { .resizable() .aspectRatio(contentMode: .fit) .font(.largeTitle) - .frame(height: 40) + .frame(height: 36) } } diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift index ebcea2f..d69919b 100644 --- a/PadelClub/Views/Score/SetInputView.swift +++ b/PadelClub/Views/Score/SetInputView.swift @@ -124,12 +124,14 @@ struct SetInputView: View { Section { DisclosureGroup(isExpanded: $showSetInputView) { PointSelectionView(valueSelected: currentValue, values: possibleValues(), possibleValues: setFormat.possibleValues, disableValues: disableValues, deleteAction: deleteLastValue) + .listRowInsets(EdgeInsets(top: -8, leading: -20, bottom: 0, trailing: 0)) } label: { SetLabelView(initialValueLeft: $setDescriptor.valueTeamOne, initialValueRight: $setDescriptor.valueTeamTwo, shouldDisplaySteppers: isMainViewTieBreakView) } if showTieBreakView { DisclosureGroup(isExpanded: $showTieBreakInputView) { PointSelectionView(valueSelected: currentTiebreakValue, values: tieBreakPossibleValues(), possibleValues: SetFormat.six.possibleValues, disableValues: disableTieBreakValues, deleteAction: deleteLastTiebreakValue) + .listRowInsets(EdgeInsets(top: -8, leading: -20, bottom: 0, trailing: 0)) } label: { SetLabelView(initialValueLeft: $setDescriptor.tieBreakValueTeamOne, initialValueRight: $setDescriptor.tieBreakValueTeamTwo, shouldDisplaySteppers: showTieBreakInputView, isTieBreak: true) } diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index c3ffb0c..3bfa6e2 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -10,6 +10,7 @@ import SwiftUI struct ImportedPlayerView: View { let player: PlayerHolder var index: Int? = nil + var showFemaleInMaleAssimilation: Bool = false var body: some View { VStack(alignment: .leading) { @@ -39,7 +40,7 @@ struct ImportedPlayerView: View { .font(.title3) if let rank = player.getRank() { Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) - .font(.caption) + .font(.caption) } } @@ -58,9 +59,26 @@ struct ImportedPlayerView: View { } } - Text(player.formattedLicense()) - .font(.caption) + if let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank(), showFemaleInMaleAssimilation { + 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) diff --git a/PadelClub/Views/Shared/MatchFormatPickerView.swift b/PadelClub/Views/Shared/MatchFormatPickerView.swift index f70c115..1de7981 100644 --- a/PadelClub/Views/Shared/MatchFormatPickerView.swift +++ b/PadelClub/Views/Shared/MatchFormatPickerView.swift @@ -8,6 +8,7 @@ import SwiftUI struct MatchFormatPickerView: View { + @Environment(Tournament.self) var tournament: Tournament let headerLabel: String @Binding var matchFormat: MatchFormat @State private var isExpanded: Bool = false @@ -40,7 +41,7 @@ struct MatchFormatPickerView: View { Text(matchFormat.format).font(.largeTitle) Spacer() VStack(alignment: .trailing) { - Text("~" + matchFormat.formattedEstimatedDuration()) + Text("~" + matchFormat.formattedEstimatedDuration(tournament.additionalEstimationDuration)) Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline) } } diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index e795259..318d7b9 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -17,10 +17,15 @@ struct SelectablePlayerListView: View { let allowSelection: Int let playerSelectionAction: PlayerSelectionAction? let contentUnavailableAction: ContentUnavailableAction? - + + @EnvironmentObject var dataStore: DataStore @StateObject private var searchViewModel: SearchViewModel @Environment(\.dismiss) var dismiss - @AppStorage("lastDataSource") var lastDataSource: String? + + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } + @AppStorage("importingFiles") var importingFiles: Bool = false @State private var searchText: String = "" @@ -29,12 +34,13 @@ struct SelectablePlayerListView: View { return URL.importDateFormatter.date(from: lastDataSource) } - init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { + init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { self.allowSelection = allowSelection // self.searchText = searchField ?? "" self.playerSelectionAction = playerSelectionAction self.contentUnavailableAction = contentUnavailableAction let searchViewModel = SearchViewModel() + searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation searchViewModel.searchText = searchField ?? "" searchViewModel.isPresented = allowSelection != 0 searchViewModel.user = user @@ -287,7 +293,7 @@ struct MySearchView: View { let array = Array(searchViewModel.selectedPlayers) Section { ForEach(array) { player in - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } .onDelete { indexSet in for index in indexSet { @@ -302,7 +308,7 @@ struct MySearchView: View { } else { Section { ForEach(players, id: \.self) { player in - ImportedPlayerView(player: player, index: nil) + ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } header: { if players.isEmpty == false { @@ -321,7 +327,7 @@ struct MySearchView: View { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } .buttonStyle(.plain) } @@ -334,7 +340,7 @@ struct MySearchView: View { } else { Section { ForEach(players) { player in - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } header: { if players.isEmpty == false { @@ -351,13 +357,13 @@ struct MySearchView: View { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil) + ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) } else { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } } header: { diff --git a/PadelClub/Views/Team/Components/TeamHeaderView.swift b/PadelClub/Views/Team/Components/TeamHeaderView.swift new file mode 100644 index 0000000..76818a6 --- /dev/null +++ b/PadelClub/Views/Team/Components/TeamHeaderView.swift @@ -0,0 +1,43 @@ +// +// TeamHeaderView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TeamHeaderView: View { + var team: TeamRegistration + var teamIndex: Int? + var tournament: Tournament? + + var body: some View { + _teamHeaderView(team, teamIndex: teamIndex) + } + + private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { + HStack { + if let teamIndex { + Text("#" + (teamIndex + 1).formatted()) + } + + if team.unsortedPlayers().isEmpty == false { + Text(team.weight.formatted()) + } + if team.isWildCard() { + Text("wildcard").italic().font(.caption) + } + Spacer() + if team.walkOut { + Text("WO") + } else if let teamIndex, let tournament { + Text(tournament.cutLabel(index: teamIndex)) + } + } + } +} + +#Preview { + TeamHeaderView(team: TeamRegistration.mock(), teamIndex: 1, tournament: nil) +} diff --git a/PadelClub/Views/Team/Components/TeamWeightView.swift b/PadelClub/Views/Team/Components/TeamWeightView.swift new file mode 100644 index 0000000..fc48af5 --- /dev/null +++ b/PadelClub/Views/Team/Components/TeamWeightView.swift @@ -0,0 +1,38 @@ +// +// TeamWeightView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TeamWeightView: View { + var team: TeamRegistration + var teamPosition: TeamPosition? = nil + + var body: some View { + VStack(alignment: .trailing, spacing: 0) { + if teamPosition == .one || teamPosition == nil { + Text(team.weight.formatted()) + .monospacedDigit() + .font(.caption) + } + if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) { + Text("#" + (index + 1).formatted(.number.precision(.integerLength(2...3)))) + .monospacedDigit() + .font(.title) + } + if teamPosition == .two { + Text(team.weight.formatted()) + .monospacedDigit() + .font(.caption) + + } + } + } +} + +#Preview { + TeamWeightView(team: TeamRegistration.mock(), teamPosition: .one) +} diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift new file mode 100644 index 0000000..db408eb --- /dev/null +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -0,0 +1,45 @@ +// +// EditingTeamView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct EditingTeamView: View { + @EnvironmentObject var dataStore: DataStore + var team: TeamRegistration + @State private var registrationDate : Date + + init(team: TeamRegistration) { + self.team = team + _registrationDate = State(wrappedValue: team.registrationDate ?? Date()) + } + + var body: some View { + List { + Section { + DatePicker(registrationDate.formatted(.dateTime.weekday()), selection: $registrationDate) + } header: { + Text("Date d'inscription") + } + } + .onChange(of: registrationDate) { + team.registrationDate = registrationDate + _save() + } + .headerProminence(.increased) + .navigationTitle("Édition") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } + + private func _save() { + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } +} + +#Preview { + EditingTeamView(team: TeamRegistration.mock()) +} diff --git a/PadelClub/Views/Team/TeamDetailView.swift b/PadelClub/Views/Team/TeamDetailView.swift index fce9049..8325fc2 100644 --- a/PadelClub/Views/Team/TeamDetailView.swift +++ b/PadelClub/Views/Team/TeamDetailView.swift @@ -8,6 +8,7 @@ import SwiftUI struct TeamDetailView: View { + @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore var team: TeamRegistration @@ -16,7 +17,12 @@ struct TeamDetailView: View { Text("Aucun joueur, espace réservé") } else { ForEach(team.players()) { player in - PlayerView(player: player) + NavigationLink { + PlayerDetailView(player: player) + .environment(tournament) + } label: { + PlayerView(player: player) + } } } } diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index 7747d5f..012f95d 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -23,7 +23,7 @@ struct TeamPickerView: View { .sheet(isPresented: $presentTeamPickerView) { NavigationStack { List { - let teams = tournament.sortedTeams() + let teams = tournament.selectedSortedTeams() if luckyLosers.isEmpty == false { Section { _teamListView(luckyLosers.sorted(by: \.weight)) diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 1b02d36..a6972c5 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -14,24 +14,10 @@ struct TeamRowView: View { var body: some View { LabeledContent { - VStack(alignment: .trailing, spacing: 0) { - if teamPosition == .one || teamPosition == nil { - Text(team.weight.formatted()) - .font(.caption) - } - if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) { - Text("#" + (index + 1).formatted()) - .font(.title) - } - if teamPosition == .two { - Text(team.weight.formatted()) - .font(.caption) - - } - } + TeamWeightView(team: team, teamPosition: teamPosition) } label: { Text(team.teamLabel(.short)) - if let callDate = team.callDate { + if let callDate = team.callDate, displayCallDate { Text("Déjà convoquée \(callDate.localizedDate())") .foregroundStyle(.red) .italic() diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 0974f39..39f7751 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -40,6 +40,14 @@ struct FileImportView: View { Label("beach-padel.app.fft.fr", systemImage: "tennisball") } + Picker(selection: $fileProvider) { + ForEach(FileImportManager.FileProvider.allCases) { + Text($0.localizedLabel).tag($0) + } + } label: { + Text("Source du fichier") + } + Button { convertingFile = false isShowing.toggle() @@ -141,29 +149,16 @@ struct FileImportView: View { } } Section { - ForEach(_filteredTeams) { team in - LabeledContent { - HStack { - if let previousTeam = team.previousTeam { - Text(previousTeam.formattedSeed(in: previousTeams)) - Image(systemName: "arrowshape.forward.fill") - } - Text(team.formattedSeed(in: _filteredTeams)) - } - } label: { - VStack(alignment: .leading) { - Text(team.playerOne.playerLabel()) - Text(team.playerTwo.playerLabel()) - } - } - } - } header: { - HStack { - Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") - Spacer() + LabeledContent { Text(_filteredTeams.count.formatted()) + } label: { + Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") } } + + ForEach(_filteredTeams) { team in + _teamView(team: team, inTeams: _filteredTeams, previousTeams: previousTeams) + } } } .onAppear { @@ -173,7 +168,7 @@ struct FileImportView: View { } } } - .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText], allowsMultipleSelection: false, onCompletion: { results in + .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in switch results { case .success(let fileurls): @@ -272,6 +267,35 @@ struct FileImportView: View { didImport = true } } + + @ViewBuilder + private func _teamView(team: FileImportManager.TeamHolder, inTeams teams: [FileImportManager.TeamHolder], previousTeams: [TeamRegistration]) -> some View { + + let newIndex = team.index(in: teams) + Section { + HStack { + VStack(alignment: .leading) { + ForEach(team.players.sorted(by: \.weight)) { + Text($0.playerLabel()) + } + } + Spacer() + HStack { + if let previousTeam = team.previousTeam { + Text(previousTeam.formattedSeed(in: previousTeams)) + Image(systemName: "arrowshape.forward.fill") + } + Text(team.formattedSeedIndex(index: newIndex)) + } + } + if let callDate = team.previousTeam?.callDate, let newDate = tournament.getStartDate(ofSeedIndex: newIndex), callDate != newDate { + Text("Attention, cette paire a déjà été convoquée à \(callDate.localizedDate())") + .foregroundStyle(.red) + .italic() + .font(.caption) + } + } + } private func _save() { try? dataStore.tournaments.addOrUpdate(instance: tournament) diff --git a/PadelClub/Views/Tournament/Screen/CashierDetailView.swift b/PadelClub/Views/Tournament/Screen/CashierDetailView.swift deleted file mode 100644 index 59cf60d..0000000 --- a/PadelClub/Views/Tournament/Screen/CashierDetailView.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// CashierDetailView.swift -// Padel Tournament -// -// Created by Razmig Sarkissian on 31/03/2024. -// - -import SwiftUI - -struct CashierDetailView: View { - var tournaments : [Tournament] - - var body: some View { - List { - ForEach(tournaments) { tournament in - _tournamentCashierDetailView(tournament) - } - } - .headerProminence(.increased) - .navigationTitle("Résumé") - } - - private func _tournamentCashierDetailView(_ tournament: Tournament) -> some View { - Section { - ForEach(PlayerRegistration.PaymentType.allCases) { type in - let count = tournament.selectedPlayers().filter({ $0.registrationType == type }).count - LabeledContent { - if let entryFee = tournament.entryFee { - let sum = Double(count) * entryFee - Text(sum.formatted(.currency(code: "EUR"))) - } - } label: { - Text(type.localizedLabel()) - Text(count.formatted()) - } - } - } header: { - Text(tournament.tournamentTitle()) - } - } -} diff --git a/PadelClub/Views/Tournament/Screen/CashierView.swift b/PadelClub/Views/Tournament/Screen/CashierView.swift deleted file mode 100644 index bec5b24..0000000 --- a/PadelClub/Views/Tournament/Screen/CashierView.swift +++ /dev/null @@ -1,539 +0,0 @@ -// -// CashierView.swift -// Padel Tournament -// -// Created by Razmig Sarkissian on 04/03/2023. -// - -import SwiftUI -import Combine - -struct CashierView: View { - @EnvironmentObject var dataStore: DataStore - var tournaments : [Tournament] - @Environment(\.dismiss) private var dismiss - @State var licenseCheck: Bool? - @State private var sortOption: SortOption = .callDate - @State private var filterOption: FilterOption = .all - @State private var sortOrder: SortOrder = .ascending - @State private var searchText = "" - @State private var licenseToEdit = "" - @State private var editPlayer: PlayerRegistration? - - let licenseMode: Bool - - init(event: Event, licenseCheck: Bool? = nil, sortOption: SortOption = .callDate) { - _licenseCheck = State(wrappedValue: licenseCheck) - _sortOption = State(wrappedValue: sortOption) - licenseMode = licenseCheck != nil - self.tournaments = event.tournaments - } - - init(tournament: Tournament, licenseCheck: Bool? = nil, sortOption: SortOption = .callDate) { - _licenseCheck = State(wrappedValue: licenseCheck) - _sortOption = State(wrappedValue: sortOption) - licenseMode = licenseCheck != nil - self.tournaments = [tournament] - } - - - func somePlayerToEdit() -> Binding { - Binding { - editPlayer != nil - } set: { _ in - } - } - - enum SortOption: Int, Identifiable, CaseIterable { - case round - case team - case alphabeticalLastName - case alphabeticalFirstName - case rank - case age - case callDate - - var id: Int { self.rawValue } - func localizedLabel() -> String { - switch self { - case .round, .callDate: - return "Convocation" - case .team: - return "Équipe" - case .alphabeticalLastName: - return "Nom" - case .alphabeticalFirstName: - return "Prénom" - case .rank: - return "Rang" - case .age: - return "Âge" - } - } - } - - enum FilterOption: Int, Identifiable, CaseIterable { - case all - case didPay - case didNotPay - - var id: Int { self.rawValue } - - func localizedLabel() -> String { - switch self { - case .all: - return "Tous" - case .didPay: - return "Réglé" - case .didNotPay: - return "Non réglé" - } - } - - func shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { - switch self { - case .all: - return true - case .didPay: - return player.hasPaid() - case .didNotPay: - return player.hasPaid() == false - - } - } - } - - var orderedPlayers: [PlayerRegistration] { - if sortOrder == .ascending { - return sortedPlayers - } else { - return sortedPlayers.reversed() - } - } - - var orderedTeams: [TeamRegistration] { - if sortOrder == .ascending { - return sortedTeams - } else { - return sortedTeams.reversed() - } - } - - var sortedTeams: [TeamRegistration] { - tournaments.flatMap({ $0.selectedSortedTeams() }) - } - - func playerHasValidLicense(_ player: PlayerRegistration) -> Bool { - return true - //todo -// if player.isImported() { -// if let licenseCheck, let licenseYearValidity = event.licenseYearValidity { -// return player.isValidLicenseNumber(year: licenseYearValidity) == licenseCheck -// } else { -// return true -// } -// } else { -// return true -// } - } - - var searchedPlayers: [PlayerRegistration] { - tournaments.flatMap({ $0.unsortedPlayers() }) -// if searchText.trimmed.isEmpty { -// return event.orderedPlayers.filter { playerHasValidLicense($0) } -// } else { -// let search = searchText.trimmed.folding(options: .diacriticInsensitive, locale: .current).lowercased() -// return event.orderedPlayers.filter { $0.canonicalName.contains(search) && playerHasValidLicense($0) } -// } - } - - var filteredPlayers: [PlayerRegistration] { - if licenseMode == false { - return searchedPlayers.filter { filterOption.shouldDisplayPlayer($0) } - } else { - return searchedPlayers - } - } - - var sortedPlayers: [PlayerRegistration] { - switch sortOption { - case .callDate, .team, .round: - return filteredPlayers.sorted(using: .keyPath(\.lastName), .keyPath(\.firstName)) - case .alphabeticalFirstName: - return filteredPlayers.sorted(using: .keyPath(\.firstName), .keyPath(\.lastName)) - case .alphabeticalLastName: - return filteredPlayers.sorted(using: .keyPath(\.lastName), .keyPath(\.firstName)) - case .rank: - return filteredPlayers.sorted(using: .keyPath(\.weight), .keyPath(\.lastName), .keyPath(\.firstName)) - case .age: - return filteredPlayers.sorted(using: .keyPath(\.weight), .keyPath(\.lastName), .keyPath(\.firstName)) - } - } - - func save() { -// do { -// event.objectWillChange.send() -// try viewContext.save() -// } catch { -// // Replace this implementation with code to handle the error appropriately. -// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. -// let nsError = error as NSError -// fatalError("Unresolved error \(nsError), \(nsError.userInfo)") -// } - } - - func bracketPlayers(in tournament: Tournament) -> [PlayerRegistration] { - tournament.selectedSortedTeams().filter { $0.groupStagePosition != nil }.flatMap { $0.unsortedPlayers() }.filter { orderedPlayers.contains($0) } - } - - func playersForRound(in round: Int, tournament: Tournament) -> [PlayerRegistration] { - tournament.selectedSortedTeams().filter { RoundRule.roundIndex(fromMatchIndex: 0) == round && $0.groupStagePosition == nil }.flatMap { $0.unsortedPlayers() }.filter { orderedPlayers.contains($0) } - } - - var displayOptionView: some View { - DisclosureGroup { - HStack { - Text("Voir") - Spacer() - Picker(selection: $licenseCheck) { - Text("Tous les joueurs").tag(nil as Bool?) - Text("Avec licence valide").tag(true as Bool?) - Text("Sans licence valide").tag(false as Bool?) - } label: { - } - } - HStack { - Text("Filtre") - Spacer() - Picker(selection: $filterOption) { - ForEach(FilterOption.allCases) { filterOption in - Text(filterOption.localizedLabel()).tag(filterOption) - } - } label: { - } - } - - HStack { - Text("Tri") - Spacer() - Picker(selection: $sortOption) { - ForEach(SortOption.allCases) { sortOption in - Text(sortOption.localizedLabel()).tag(sortOption) - } - } label: { - } - } - - HStack { - Text("Ordre") - Spacer() - Picker(selection: $sortOrder) { - Text("Croissant").tag(SortOrder.ascending) - Text("Décroissant").tag(SortOrder.descending) - } label: { - - } - } - } label: { - Text("Options d'affichage") - } - - } - - @ViewBuilder - var sortedPlayersView: some View { - if orderedPlayers.isEmpty { - ContentUnavailableView.search(text: searchText) - } else { - Section { - ForEach(orderedPlayers) { player in - computedPlayerView(player) - } - } header: { - HStack { - Text(orderedPlayers.count.formatted() + " joueurs") - } - } - } - } - - var body: some View { - List { - Section { - NavigationLink { - CashierDetailView(tournaments: tournaments) - } label: { - Text("Résumé") - } - } - - Section { - ForEach(tournaments) { tournament in - HStack { - Text(tournament.tournamentTitle()) - Spacer() - Text(tournament.earnings().formatted(.currency(code: "EUR").precision(.fractionLength(0)))) - Text(tournament.paidCompletion().formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) - } - } -// HStack { -// Text("Total") -// Spacer() -// Text(event.earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) -// Text(event.paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) -// } - } header: { - Text("Encaissement") - } - - if licenseMode == false { - Section { - Picker(selection: $filterOption) { - ForEach(FilterOption.allCases) { filterOption in - Text(filterOption.localizedLabel()).tag(filterOption) - } - } label: { - Text("Statut du règlement") - } - - Picker(selection: $sortOption) { - ForEach(SortOption.allCases) { sortOption in - Text(sortOption.localizedLabel()).tag(sortOption) - } - } label: { - Text("Affichage par") - } - - if sortOption != .round { - Picker(selection: $sortOrder) { - Text("Croissant").tag(SortOrder.ascending) - Text("Décroissant").tag(SortOrder.descending) - } label: { - Text(sortOption == .team ? "Tri par rang" : "Tri") - } - } - } header: { - Text("Options d'affichage") - } - } - - if searchText.isEmpty == false { - sortedPlayersView - } else { - - if sortOption == .team && licenseMode == false { - if orderedTeams.isEmpty { - ContentUnavailableView.search(text: searchText) - } else { - ForEach(orderedTeams) { team in - Section { - ForEach(team.players()) { player in - computedPlayerView(player) - } - } - } - } - } else if sortOption == .round && licenseMode == false { - if orderedPlayers.isEmpty { - ContentUnavailableView.search(text: searchText) - } else { - ForEach(tournaments) { tournament in - let bracketPlayers = bracketPlayers(in: tournament) - Section { - DisclosureGroup { - ForEach(bracketPlayers) { player in - computedPlayerView(player) - } - } label: { - HStack { - Text("Poules") - Spacer() - Text(bracketPlayers.count.formatted()) - } - } - } header: { - Text(tournament.tournamentTitle()) - } - } - - // ForEach(1...event.rounds) { round in - // ForEach(tournaments) { tournament in - // if tournament.isRoundHidden(round) == false { - // let players = playersForRound(in: round, tournament: tournament) - // - // if players.isEmpty == false { - // Section { - // DisclosureGroup { - // ForEach(players) { player in - // computedPlayerView(player) - // } - // } label: { - // HStack { - // Text(RoundLabel.labels[tournament.rounds - round]) - // Spacer() - // Text(players.count.formatted()) - // } - // } - // } header: { - // Text(tournament.localizedTitle) - // } - // } - // } - // } - // } - } - } else if sortOption == .callDate && licenseMode == false { - _byCallDateView() - } else { - sortedPlayersView - } - } - } -// .alert("Licence", isPresented: somePlayerToEdit(), actions: { -// TextField("Licence", text: $licenseToEdit) -// .keyboardType(.asciiCapable) -// .autocorrectionDisabled(true) -// .textContentType(.init(rawValue: "")) -// Button("OK") { -// editPlayer?.license = licenseToEdit -// licenseToEdit = "" -// editPlayer?.objectWillChange.send() -// editPlayer = nil -// save() -// } -// Button("Annuler", role : .cancel) { -// licenseToEdit = "" -// editPlayer = nil -// } -// }) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Menu { -// Button { -// event.orderedPlayers.forEach { player in -// if let registration = player.orderedRegistrations.first(where: { $0.tournament?.tournamentID == player.tournament?.tournamentID }) { -// if registration.paymentType == .notPaid { -// registration.paymentType = .gift -// } -// } else { -// let registration = Registration(context: viewContext) -// registration.paymentType = .gift -// registration.tournament = player.tournament -// player.addToRegistrations(registration) -// } -// save() -// } -// } label: { -// Text("Tout le monde a réglé") -// } -// -// Button { -// event.orderedPlayers.forEach { player in -// if let registration = player.orderedRegistrations.first(where: { $0.tournament?.tournamentID == player.tournament?.tournamentID }) { -// registration.paymentType = .notPaid -// } else { -// let registration = Registration(context: viewContext) -// registration.paymentType = .notPaid -// registration.tournament = player.tournament -// player.addToRegistrations(registration) -// } -// save() -// } -// } label: { -// Text("Personne n'a réglé") -// } -// - } label: { - LabelOptions() - } - } - } - .navigationTitle("Joueurs") - .navigationBarTitleDisplayMode(.large) - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher un joueur")) - } - - @ViewBuilder - func computedPlayerView(_ player: PlayerRegistration) -> some View { - VStack(alignment: .leading) { - ImportedPlayerView(player: player) - HStack { - Menu { - if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") { - Link(destination: url) { - Label("Appeler", systemImage: "phone") - } - } - if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "sms:\(number)") { - Link(destination: url) { - Label("SMS", systemImage: "message") - } - } - - Divider() - if let licenseCheck, let licenseYearValidity = player.tournament()?.licenseYearValidity(), licenseCheck == false { - Button { - player.validateLicenceId(licenseYearValidity) - save() - - if filteredPlayers.isEmpty { - dismiss() - } - - } label: { - Text("Valider la licence \(licenseYearValidity)") - } - } - - if let license = player.licenceId?.strippedLicense { - Button { - let pasteboard = UIPasteboard.general - pasteboard.string = license - } label: { - Label("Copier la licence", systemImage: "doc.on.doc") - } - } - - Section { - Button { - licenseToEdit = player.licenceId ?? "" - editPlayer = player - } label: { - if player.licenceId == nil { - Text("Ajouter la licence") - } else { - Text("Modifier la licence") - } - } - PasteButton(payloadType: String.self) { strings in - guard let first = strings.first else { return } - player.licenceId = first - } - } header: { - Text("Modification de licence") - } - } label: { - Text("Options") - } - Spacer() - PlayerPayView(player: player) - } - } - } - - - private func _byCallDateView() -> some View { - ForEach(tournaments) { tournament in - let teams = tournament.selectedSortedTeams() - let players = teams.filter({ $0.callDate != nil }).sorted(using: .keyPath(\.callDate!)).flatMap({ $0.players() }) + teams.filter({ $0.callDate == nil }).flatMap({ $0.players() }) - Section { - ForEach(players) { player in - computedPlayerView(player) - } - } header: { - Text(tournament.tournamentTitle()) - } - .headerProminence(.increased) - } - } -} diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index 4f7d8e9..5c9b896 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -10,14 +10,21 @@ import SwiftUI struct InscriptionInfoView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament - @State private var duplicates = [PlayerRegistration]() - @State private var problematicPlayers = [PlayerRegistration]() - @State private var inadequatePlayers = [PlayerRegistration]() - @State private var playersWithoutValidLicense = [PlayerRegistration]() - @State private var entriesFromBeachPadel = [TeamRegistration]() - @State private var playersMissing = [TeamRegistration]() - @State private var waitingList = [TeamRegistration]() - @State private var selectedTeams = [TeamRegistration]() + + var players : [PlayerRegistration] { tournament.unsortedPlayers() } + var selectedTeams : [TeamRegistration] { tournament.selectedSortedTeams() } + + var callDateIssue : [TeamRegistration] { + selectedTeams.filter { tournament.isStartDateIsDifferentThanCallDate($0) } + } + + var waitingList : [TeamRegistration] { tournament.waitingListTeams(in: selectedTeams) } + var duplicates : [PlayerRegistration] { tournament.duplicates(in: players) } + var problematicPlayers : [PlayerRegistration] { players.filter({ $0.sex == -1 }) } + var inadequatePlayers : [PlayerRegistration] { tournament.inadequatePlayers(in: players) } + var playersWithoutValidLicense : [PlayerRegistration] { tournament.playersWithoutValidLicense(in: players) } + var entriesFromBeachPadel : [TeamRegistration] { tournament.unsortedTeams().filter({ $0.isImported() }) } + var playersMissing : [TeamRegistration] { selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) } var body: some View { List { @@ -47,6 +54,30 @@ struct InscriptionInfoView: View { .listRowView(color: .green) } + Section { + DisclosureGroup { + ForEach(callDateIssue) { team in + CallView.TeamView(team: team) + if let groupStage = team.groupStageObject(), let callDate = groupStage.startDate { + CallView(teams: [team], callDate: callDate, matchFormat: groupStage.matchFormat, roundLabel: "poule") + } else if let initialRound = team.initialRound(), + let initialMatch = team.initialMatch(), + let callDate = initialMatch.startDate { + CallView(teams: [team], callDate: callDate, matchFormat: initialMatch.matchFormat, roundLabel: initialRound.roundTitle()) + } + } + } label: { + LabeledContent { + Text(callDateIssue.count.formatted()) + } label: { + Text("Erreur de convocation") + } + } + .listRowView(color: .brown) + } footer: { + Text("L'horaire de la convocation est différente du match initial") + } + let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) @@ -134,7 +165,7 @@ struct InscriptionInfoView: View { Section { DisclosureGroup { ForEach(playersWithoutValidLicense) { - ImportedPlayerView(player: $0) + EditablePlayerView(player: $0, editingOptions: [.licenceId]) } } label: { LabeledContent { @@ -166,22 +197,7 @@ struct InscriptionInfoView: View { .navigationTitle("Synthèse") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - .onAppear { - _initData() - } - } - - private func _initData() { - let players = tournament.unsortedPlayers() - selectedTeams = tournament.selectedSortedTeams() - waitingList = tournament.waitingListTeams(in: selectedTeams) - duplicates = tournament.duplicates(in: players) - problematicPlayers = players.filter({ $0.sex == -1 }) - inadequatePlayers = tournament.inadequatePlayers(in: players) - playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players) - entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() }) - playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) - } + } } #Preview { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift new file mode 100644 index 0000000..0b5e2aa --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift @@ -0,0 +1,68 @@ +// +// TournamentClubSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TournamentClubSettingsView: View { + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + + var body: some View { + @Bindable var tournament = tournament + List { + let event = tournament.eventObject + let selectedClub = event?.clubObject + Section { + if let selectedClub { + NavigationLink { + ClubDetailView(club: selectedClub, displayContext: .edition) + } label: { + ClubRowView(club: selectedClub) + } + } else { + NavigationLink { + ClubsView() { club in + if let event { + event.club = club.id + try? dataStore.events.addOrUpdate(instance: event) + } else { + let event = Event(club: club.id) + tournament.event = event.id + try? dataStore.events.addOrUpdate(instance: event) + } + } + } label: { + Text("Choisir un club") + } + } + } header: { + Text("Lieu du tournoi") + } footer: { + if let event, selectedClub != nil { + HStack { + Spacer() + Button("modifier", role: .destructive) { + event.club = nil + try? dataStore.events.addOrUpdate(instance: event) + } + } + } + } + + Section { + TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100) + } + } + .onChange(of: tournament.courtCount) { + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } + } +} + +#Preview { + TournamentClubSettingsView() +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift index f6e5b78..dd4887d 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentDurationManagerView.swift @@ -12,13 +12,11 @@ struct TournamentDurationManagerView: View { var body: some View { @Bindable var tournament = tournament - - Stepper(value: $tournament.dayDuration, in: 1...3) { - LabeledContent { - Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) - } label: { - Text("Durée") - } + LabeledContent { + StepperView(count: $tournament.dayDuration, minimum: 1, maximum: 3) + } label: { + Text("Durée") + Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) } } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift index 8f46f49..5ada9bf 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift @@ -10,19 +10,19 @@ import SwiftUI struct TournamentFieldsManagerView: View { let localizedStringKey: String @Binding var count: Int + let max: Int var body: some View { - Stepper(value: $count, in: 1...1_000) { - LabeledContent { - Text(count.formatted()) - } label: { - Text(localizedStringKey) - } + LabeledContent { + StepperView(count: $count, minimum: 1, maximum: max) + } label: { + Text(localizedStringKey) +// Text(count.formatted()) } } } #Preview { - TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2)) + TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2), max: 10) .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift new file mode 100644 index 0000000..0da60b0 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -0,0 +1,96 @@ +// +// TournamentGeneralSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TournamentGeneralSettingsView: View { + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + + @State private var tournamentName: String = "" + @FocusState private var textFieldIsFocus: Bool + + var body: some View { + @Bindable var tournament = tournament + Form { + Section { + TournamentDatePickerView() + TournamentDurationManagerView() + } + + Section { + TournamentLevelPickerView() + } + + Section { + LabeledContent { + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $tournament.entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } label: { + Text("Inscription") + } + } + + Section { + LabeledContent { + TextField("Nom", text: $tournamentName) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .keyboardType(.alphabet) + .autocorrectionDisabled() + .onSubmit { + if tournamentName.trimmed.isEmpty { + tournament.name = nil + } else { + tournament.name = tournamentName + } + } + } label: { + Text("Nom du tournoi") + } + } + } + .focused($textFieldIsFocus) + .scrollDismissesKeyboard(.immediately) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .keyboard) { + Button("Valider") { + textFieldIsFocus = false + } + } + } + .onChange(of: tournament.startDate) { + _save() + } + .onChange(of: tournament.entryFee) { + _save() + } + .onChange(of: tournament.name) { + _save() + } + .onChange(of: [ + tournament.dayDuration, + tournament.federalCategory, + tournament.federalLevelCategory, + tournament.federalAgeCategory, + tournament.groupStageSortMode, + ]) { + _save() + } + } + + private func _save() { + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } +} + +#Preview { + TournamentGeneralSettingsView() +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift new file mode 100644 index 0000000..76c9dce --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift @@ -0,0 +1,106 @@ +// +// TournamentMatchFormatsSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 18/04/2024. +// + +import SwiftUI + +struct TournamentMatchFormatsSettingsView: View { + @Environment(NavigationViewModel.self) var navigation: NavigationViewModel + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + @State private var confirmUpdate: Bool = false + @State private var updateCompleted: Bool = false + var body: some View { + @Bindable var tournament = tournament + List { + if confirmUpdate { + RowButtonView("Modifier les matchs existants", role: .destructive) { + _updateAllFormat() + } + } + + TournamentFormatSelectionView() + + Section { + LabeledContent { + StepperView(title: "minutes", count: $tournament.additionalEstimationDuration, step: 5) + } label: { + Text("Modifier les durées moyennes") + } + } footer: { + Text("Cette valeur est rajoutée ou soustraite aux valeurs par défaut. Par exemple, cela peut aider à mieux planifier un tournoi débutant ou jeune.") + } + + Section { + NavigationLink { + DurationSettingsView() + } label: { + Label("Estimation des durées moyennes", systemImage: "deskclock") + } + } + } + .onChange(of: [tournament.roundFormat, + tournament.groupStageFormat, + tournament.loserRoundFormat, + ]) { + _save() + _confirmOrSave() + } + .onChange(of: tournament.additionalEstimationDuration) { + _save() + } + .onChange(of: dataStore.appSettings.matchFormatsDefaultDuration) { + _confirmOrSave() + } + .overlay(alignment: .bottom) { + if updateCompleted { + Label("Formats mis à jour", systemImage: "checkmark.circle.fill") + .toastFormatted() + .deferredRendering(for: .seconds(2)) + } + } + } + + private func _confirmOrSave() { + switch tournament.state() { + case .initial: + break + case .build: + confirmUpdate = true + } + } + + private func _updateAllFormat() { + updateCompleted = false + let groupStages = tournament.groupStages() + groupStages.forEach { groupStage in + groupStage.updateMatchFormat(tournament.groupStageMatchFormat) + } + + let allRounds = tournament.allRounds() + allRounds.forEach { round in + if round.isLoserBracket() { + round.updateMatchFormat(tournament.loserBracketMatchFormat) + } else { + round.updateMatchFormat(tournament.matchFormat) + } + } + try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) + try? dataStore.rounds.addOrUpdate(contentOfs: allRounds) + + confirmUpdate = false + updateCompleted = true + + } + + private func _save() { + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } +} + +#Preview { + TournamentMatchFormatsSettingsView() +} diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 230fca8..52273e7 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -76,7 +76,7 @@ struct InscriptionManagerView: View { selectionSearchField = nil }) { NavigationStack { - SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption()) { players in + SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in selectionSearchField = nil players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) @@ -239,7 +239,7 @@ struct InscriptionManagerView: View { Section { TeamDetailView(team: team) } header: { - _teamHeaderView(team, teamIndex: teamIndex) + TeamHeaderView(team: team, teamIndex: teamIndex, tournament: tournament) } footer: { _teamFooterView(team) } @@ -418,14 +418,16 @@ struct InscriptionManagerView: View { .environment(tournament) } label: { LabeledContent { - Text(count.formatted() + "/" + tournament.teamCount.formatted()) + Text(tournament.registrationIssues().formatted()).font(.largeTitle) } label: { - Text("Analyse des inscriptions") + Text("Problèmes détéctés") if let closedRegistrationDate = tournament.closedRegistrationDate { Text("clôturé le " + closedRegistrationDate.formatted()) } } } + } header: { + Text(count.formatted() + "/" + tournament.teamCount.formatted() + " paires inscrites") } } @@ -715,31 +717,10 @@ struct InscriptionManagerView: View { } } - private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { - HStack { - if let teamIndex { - Text("#" + (teamIndex + 1).formatted()) - } - - if team.unsortedPlayers().isEmpty == false { - Text(team.weight.formatted()) - } - if team.isWildCard() { - Text("wildcard").italic().font(.caption) - } - Spacer() - if team.walkOut { - Text("WO") - } else if let teamIndex { - Text(tournament.cutLabel(index: teamIndex)) - } - } - } - private func _teamFooterView(_ team: TeamRegistration) -> some View { HStack { if let formattedRegistrationDate = team.formattedInscriptionDate() { - Text(formattedRegistrationDate).font(.caption).foregroundStyle(.secondary) + Text(formattedRegistrationDate) } Spacer() _teamMenuOptionView(team) @@ -748,7 +729,7 @@ struct InscriptionManagerView: View { private func _teamMenuOptionView(_ team: TeamRegistration) -> some View { Menu { Section { - Button("Éditer les joueurs") { + Button("Changer les joueurs") { editedTeam = team team.unsortedPlayers().forEach { player in createdPlayers.insert(player) @@ -756,7 +737,13 @@ struct InscriptionManagerView: View { } } Divider() - + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("Éditer une donnée de l'équipe") + } + Divider() Toggle(isOn: .init(get: { return team.wildCardBracket }, set: { value in @@ -799,12 +786,11 @@ struct InscriptionManagerView: View { } label: { LabelDelete() } - } header: { - Text(team.teamLabel(.short)) +// } header: { +// Text(team.teamLabel(.short)) } } label: { LabelOptions().labelStyle(.titleOnly) - .font(.caption) } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index 3f10f50..8a8dfa3 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -7,11 +7,18 @@ import SwiftUI -enum CallDestination: String, Identifiable, Selectable { - case seeds - case groupStages +enum CallDestination: Identifiable, Selectable { + case seeds(Tournament) + case groupStages(Tournament) - var id: String { self.rawValue } + var id: String { + switch self { + case .seeds: + return "seed" + case .groupStages: + return "groupStage" + } + } func selectionLabel() -> String { switch self { @@ -23,8 +30,27 @@ enum CallDestination: String, Identifiable, Selectable { } func badgeValue() -> Int? { - nil + switch self { + case .seeds(let tournament): + let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) + return allSeedCalled.count + case .groupStages(let tournament): + let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) + return allSeedCalled.count + } + } + + func badgeImage() -> Badge? { + switch self { + case .seeds(let tournament): + let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + return allSeedCalled ? .checkmark : nil + case .groupStages(let tournament): + let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + return allSeedCalled ? .checkmark : nil + } } + } @@ -38,13 +64,13 @@ struct TournamentCallView: View { var destinations = [CallDestination]() let groupStageTeams = tournament.groupStageTeams() if groupStageTeams.isEmpty == false { - destinations.append(.groupStages) - self._selectedDestination = State(wrappedValue: .groupStages) + destinations.append(.groupStages(tournament)) + self._selectedDestination = State(wrappedValue: .groupStages(tournament)) } if tournament.seededTeams().isEmpty == false { - destinations.append(.seeds) + destinations.append(.seeds(tournament)) if groupStageTeams.isEmpty { - self._selectedDestination = State(wrappedValue: .seeds) + self._selectedDestination = State(wrappedValue: .seeds(tournament)) } } self.allDestinations = destinations diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift new file mode 100644 index 0000000..4b1e91f --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -0,0 +1,125 @@ +// +// TournamentCashierView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +enum CashierDestination: Identifiable, Selectable { + case summary + case groupStage(GroupStage) + case bracket(Round) + case all(Tournament) + + var id: String { + switch self { + case .summary, .all: + return String(describing: self) + case .groupStage(let groupStage): + return groupStage.id + case .bracket(let round): + return round.id + } + } + + func selectionLabel() -> String { + switch self { + case .summary: + return "Bilan" + case .groupStage(let groupStage): + return groupStage.selectionLabel() + case .bracket(let round): + return round.selectionLabel() + case .all: + return "Tous" + } + } + + func badgeValue() -> Int? { + switch self { + case .summary: + return nil + case .groupStage(let groupStage): + return groupStage.unsortedPlayers().filter({ $0.hasPaid() == false }).count + case .bracket(let round): + return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count + case .all(let tournament): + return tournament.selectedPlayers().filter({ $0.hasPaid() == false }).count + } + } + + func badgeImage() -> Badge? { + switch self { + case .summary: + return nil + case .groupStage(let groupStage): + return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil + case .bracket(let round): + return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? .checkmark : nil + case .all(let tournament): + return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil + } + } + +} + +struct TournamentCashierView: View { + var tournament: Tournament + @State private var selectedDestination: CashierDestination? + + func allDestinations() -> [CashierDestination] { + var allDestinations : [CashierDestination] = [] + let destinations : [CashierDestination] = tournament.groupStages().map { CashierDestination.groupStage($0) } + allDestinations.append(contentsOf: destinations) + tournament.rounds().forEach { round in + if round.seeds().isEmpty == false { + allDestinations.append(CashierDestination.bracket(round)) + } + } + allDestinations.append(.all(tournament)) + allDestinations.append(.summary) + return allDestinations + } + + init(tournament: Tournament) { + self.tournament = tournament + let gs = tournament.getActiveGroupStage() + if let gs { + _selectedDestination = State(wrappedValue: .groupStage(gs)) + } else if let rs = tournament.getActiveRound(withSeeds: true) { + _selectedDestination = State(wrappedValue: .bracket(rs)) + } + } + + var body: some View { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) + switch selectedDestination { + case .none: + CashierSettingsView(tournament: tournament) + .navigationTitle("Réglages") + case .some(let selectedCall): + switch selectedCall { + case .summary: + CashierDetailView(tournament: tournament) + case .groupStage(let groupStage): + CashierView(tournament: tournament, teams: groupStage.teams()) + case .bracket(let round): + CashierView(tournament: tournament, teams: round.seeds()) + case .all(let tournament): + CashierView(tournament: tournament, teams: tournament.selectedSortedTeams()) + } + } + } + .environment(tournament) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Encaissement") + } +} + +#Preview { + TournamentCashierView(tournament: Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 93b3881..82bc8cb 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -41,6 +41,11 @@ enum ScheduleDestination: String, Identifiable, Selectable { func badgeValue() -> Int? { nil } + + func badgeImage() -> Badge? { + nil + } + } struct TournamentScheduleView: View { diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index c6c6500..a984d14 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -7,103 +7,69 @@ import SwiftUI -struct TournamentSettingsView: View { - @Environment(Tournament.self) private var tournament: Tournament - @EnvironmentObject var dataStore: DataStore +enum TournamentSettings: Identifiable, Selectable { + case general + case club(Tournament) + case matchFormats + + var id: String { String(describing: self) } + + func selectionLabel() -> String { + switch self { + case .matchFormats: + return "Formats de jeu" + case .general: + return "Général" + case .club: + return "Club" + } + } - @State private var tournamentName: String = "" + func badgeValue() -> Int? { + nil + } - var body: some View { - @Bindable var tournament = tournament - Form { - LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $tournament.entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) - .keyboardType(.decimalPad) - .fixedSize() - .multilineTextAlignment(.trailing) - } label: { - Text("Inscription") - } - - LabeledContent { - TextField("Nom", text: $tournamentName) - .multilineTextAlignment(.trailing) - .fixedSize() - .keyboardType(.alphabet) - .autocorrectionDisabled() - .onSubmit { - if tournamentName.trimmed.isEmpty { - tournament.name = nil - } else { - tournament.name = tournamentName - } - } - } label: { - Text("Nom du tournoi") + func badgeImage() -> Badge? { + switch self { + case .club(let tournament): + if tournament.club() != nil { + return .checkmark + } else { + return .xmark } - - TournamentLevelPickerView() - TournamentDurationManagerView() - TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) - TournamentDatePickerView() - + default: + return nil + } + } +} - let event = tournament.eventObject - let selectedClub = event?.clubObject - Section { - if let selectedClub { - NavigationLink { - ClubDetailView(club: selectedClub, displayContext: .edition) - } label: { - ClubRowView(club: selectedClub) - } - } else { - NavigationLink { - ClubsView() { club in - if let event { - event.club = club.id - try? dataStore.events.addOrUpdate(instance: event) - } else { - let event = Event(club: club.id) - tournament.event = event.id - try? dataStore.events.addOrUpdate(instance: event) - } - } - } label: { - Text("Choisir un club") - } - } - } header: { - Text("Lieu du tournoi") - } footer: { - if let event, selectedClub != nil { - HStack { - Spacer() - Button("modifier", role: .destructive) { - event.club = nil - try? dataStore.events.addOrUpdate(instance: event) - } - .font(.caption) - } - } +struct TournamentSettingsView: View { + @State private var selectedDestination: TournamentSettings? = .general + @Environment(Tournament.self) var tournament: Tournament + + private func destinations() -> [TournamentSettings] { + [.general, .club(tournament), .matchFormats] + } + + var body: some View { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: destinations(), nilDestinationIsValid: false) + switch selectedDestination! { + case .matchFormats: + TournamentMatchFormatsSettingsView() + case .general: + TournamentGeneralSettingsView() + case .club: + TournamentClubSettingsView() } - - TournamentFormatSelectionView() - } - .navigationTitle("Réglages") + .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - .onDisappear { - try? dataStore.tournaments.addOrUpdate(instance: tournament) - } + .navigationTitle("Réglages") } + } #Preview { - Group { - - TournamentSettingsView() - .environmentObject(DataStore.shared) - .environment(Tournament.mock()) - } + TournamentSettingsView() } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index a1b6e89..b7fae0a 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -61,11 +61,15 @@ struct TournamentCellView: View { let event = federalTournament.getEvent() let newTournament = Tournament.newEmptyInstance() newTournament.event = event.id + //todo + //newTournament.umpireMail() + //newTournament.jsonData = jsonData newTournament.tournamentLevel = build.level newTournament.tournamentCategory = build.category newTournament.federalTournamentAge = build.age newTournament.dayDuration = federalTournament.dayDuration - newTournament.startDate = federalTournament.startDate + newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) + newTournament.setupFederalSettings() try? dataStore.tournaments.addOrUpdate(instance: newTournament) } } label: { diff --git a/PadelClub/Views/Tournament/TournamentInitView.swift b/PadelClub/Views/Tournament/TournamentInitView.swift index 8e25141..e6d3f55 100644 --- a/PadelClub/Views/Tournament/TournamentInitView.swift +++ b/PadelClub/Views/Tournament/TournamentInitView.swift @@ -16,6 +16,7 @@ struct TournamentInitView: View { NavigationLink(value: Screen.settings) { LabeledContent { Text(tournament.settingsDescriptionLocalizedLabel()) + .tint(.master) } label: { LabelSettings() } @@ -28,6 +29,7 @@ struct TournamentInitView: View { NavigationLink(value: Screen.structure) { LabeledContent { Text(tournament.structureDescriptionLocalizedLabel()) + .tint(.master) } label: { LabelStructure() } diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index fad5286..9250f04 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -8,62 +8,79 @@ import SwiftUI struct TournamentRunningView: View { - @Environment(Tournament.self) private var tournament: Tournament - + var tournament: Tournament + var allMatches: [Match] + + init(tournament: Tournament) { + self.tournament = tournament + self.allMatches = tournament.allMatches() + } + @ViewBuilder var body: some View { Section { NavigationLink(value: Screen.schedule) { + let tournamentStatus = tournament.scheduleStatus() LabeledContent { - Text(tournament.scheduleStatus()) + Text(tournamentStatus.completion).foregroundStyle(.master) } label: { Text("Horaires") + Text(tournamentStatus.label) } } NavigationLink(value: Screen.call) { + let tournamentStatus = tournament.callStatus() LabeledContent { - Text(tournament.callStatus()) + Text(tournamentStatus.completion).foregroundStyle(.master) } label: { Text("Convocations") + Text(tournamentStatus.label) } } NavigationLink(value: Screen.cashier) { + let tournamentStatus = tournament.cashierStatus() LabeledContent { - Text(tournament.cashierStatus()) + Text(tournamentStatus.completion).foregroundStyle(.master) } label: { Text("Encaissement") + Text(tournamentStatus.label) } } } - if tournament.groupStages().isEmpty == false { - Section { + Section { + if tournament.groupStages().isEmpty == false { NavigationLink(value: Screen.groupStage) { LabeledContent { Text(tournament.groupStageStatus()) + .foregroundStyle(.master) } label: { Text("Poules") } } } - } - - Section { - NavigationLink(value: Screen.round) { - LabeledContent { - Text(tournament.bracketStatus()) - } label: { - Text("Tableau") + + if tournament.rounds().isEmpty == false { + NavigationLink(value: Screen.round) { + LabeledContent { + Text(tournament.bracketStatus()) + .foregroundStyle(.master) + } label: { + Text("Tableau") + } } } } - + + MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches)) + //MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches)) + //MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches)) + MatchListView(section: "terminés", matches: tournament.finishedMatches(allMatches)) } } #Preview { - TournamentRunningView() - .environment(Tournament.mock()) + TournamentRunningView(tournament: Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 0c07383..731ee8f 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -11,7 +11,10 @@ struct TournamentView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament var presentationContext: PresentationContext = .agenda - @AppStorage("lastDataSource") var lastDataSource: String? + + var lastDataSource: String? { + dataStore.appSettings.lastDataSource + } var _lastDataSourceDate: Date? { guard let lastDataSource else { return nil } @@ -34,6 +37,7 @@ struct TournamentView: View { NavigationLink(value: Screen.inscription) { LabeledContent { Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) + .foregroundStyle(.master) } label: { Text("Gestion des inscriptions") if let closedRegistrationDate = tournament.closedRegistrationDate { @@ -44,6 +48,7 @@ struct TournamentView: View { if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { LabeledContent { Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) + .foregroundStyle(.master) } label: { Text("Date limite") } @@ -61,7 +66,7 @@ struct TournamentView: View { case .initial: TournamentInitView() case .build: - TournamentRunningView() + TournamentRunningView(tournament: tournament) } } .toolbarBackground(.visible, for: .navigationBar) @@ -81,7 +86,7 @@ struct TournamentView: View { case .schedule: TournamentScheduleView(tournament: tournament) case .cashier: - CashierView(tournament: tournament) + TournamentCashierView(tournament: tournament) case .call: TournamentCallView(tournament: tournament) } @@ -89,6 +94,7 @@ struct TournamentView: View { .environment(tournament) }) .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .principal) { VStack { diff --git a/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift b/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift index 98c4591..6a2976d 100644 --- a/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift +++ b/PadelClub/Views/ViewModifiers/DeferredViewModifier.swift @@ -49,6 +49,19 @@ private struct DeferredViewModifier: ViewModifier { } extension View { + func toastFormatted() -> some View { + self + .font(.title3) + .frame(height: 28) + .padding() + .background { + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(.white) + } + .shadow(radius: 2) + .offset(y: -64) + } + func deferredRendering(for delay: DispatchTimeInterval) -> some View { modifier(DeferredViewModifier(delay: delay)) }