From 992d7fb744a4f7d3ca149d1f28ff2da58bfe96d7 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 3 Apr 2024 16:26:37 +0200 Subject: [PATCH] clean up --- PadelClub.xcodeproj/project.pbxproj | 79 +++ PadelClub/Data/GroupStage.swift | 171 +++++- PadelClub/Data/Match.swift | 146 ++++- PadelClub/Data/PlayerRegistration.swift | 3 + PadelClub/Data/TeamRegistration.swift | 12 +- PadelClub/Data/TeamScore.swift | 7 +- PadelClub/Data/Tournament.swift | 12 +- .../Extensions/Sequence+Extensions.swift | 17 +- .../Network/NetworkFederalService.swift | 36 +- .../Manager/Network/NetworkManagerError.swift | 9 + PadelClub/Manager/PadelRule.swift | 4 + PadelClub/Manager/SourceFileManager.swift | 2 +- PadelClub/ViewModel/MatchDescriptor.swift | 99 ++++ PadelClub/ViewModel/SetDescriptor.swift | 33 ++ PadelClub/Views/Club/ClubSearchView.swift | 6 +- PadelClub/Views/Club/ClubsView.swift | 4 +- .../GenericDestinationPickerView.swift | 4 +- .../Views/Components/MatchListView.swift | 45 ++ .../Views/Components/RowButtonView.swift | 38 +- PadelClub/Views/Event/EventCreationView.swift | 4 +- .../GroupStage/GroupStageSettingsView.swift | 4 +- .../Views/GroupStage/GroupStageTeamView.swift | 107 ++++ .../Views/GroupStage/GroupStageView.swift | 553 +++++++----------- .../Views/GroupStage/GroupStagesView.swift | 73 ++- PadelClub/Views/Match/MatchDateView.swift | 2 +- PadelClub/Views/Match/MatchDetailView.swift | 73 +-- PadelClub/Views/Match/MatchSummaryView.swift | 4 +- .../Navigation/Agenda/ActivityView.swift | 33 +- .../Navigation/Agenda/EmptyActivityView.swift | 4 +- PadelClub/Views/Navigation/MainView.swift | 18 - .../Views/Navigation/PadelClubView.swift | 2 +- .../Player/Components/PlayerPopoverView.swift | 2 +- PadelClub/Views/Round/RoundSettingsView.swift | 8 +- PadelClub/Views/Score/EditScoreView.swift | 104 ++++ .../Views/Score/PointSelectionView.swift | 50 ++ PadelClub/Views/Score/PointView.swift | 26 + PadelClub/Views/Score/SetInputView.swift | 215 +++++++ PadelClub/Views/Score/SetLabelView.swift | 57 ++ .../Shared/MatchTypeSmallSelectionView.swift | 23 + PadelClub/Views/Team/TeamPickerView.swift | 2 +- .../Components/UpdateSourceRankDateView.swift | 2 +- .../Screen/InscriptionManagerView.swift | 16 +- .../Views/Tournament/TournamentView.swift | 2 +- .../Views/ViewModifiers/TabItemModifier.swift | 26 + 44 files changed, 1604 insertions(+), 533 deletions(-) create mode 100644 PadelClub/ViewModel/MatchDescriptor.swift create mode 100644 PadelClub/ViewModel/SetDescriptor.swift create mode 100644 PadelClub/Views/Components/MatchListView.swift create mode 100644 PadelClub/Views/GroupStage/GroupStageTeamView.swift create mode 100644 PadelClub/Views/Score/EditScoreView.swift create mode 100644 PadelClub/Views/Score/PointSelectionView.swift create mode 100644 PadelClub/Views/Score/PointView.swift create mode 100644 PadelClub/Views/Score/SetInputView.swift create mode 100644 PadelClub/Views/Score/SetLabelView.swift create mode 100644 PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift create mode 100644 PadelClub/Views/ViewModifiers/TabItemModifier.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5425ef6..47c1c1d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -102,6 +102,7 @@ FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */; }; FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; }; + FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; @@ -170,12 +171,23 @@ FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; + FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; }; + FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; FFC83D512BB8087E00750834 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D502BB8087E00750834 /* RoundView.swift */; }; + FFCFBFFE2BBBE86600B82851 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = FFCFBFFD2BBBE86600B82851 /* Algorithms */; }; + FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0012BBC39A600B82851 /* EditScoreView.swift */; }; + FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */; }; + FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0112BBC3E1A00B82851 /* PointView.swift */; }; + FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */; }; + FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */; }; + FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; + FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; + FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */; }; FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; }; FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; }; FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.swift */; }; @@ -332,6 +344,7 @@ FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = ""; }; FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = ""; }; + FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemModifier.swift; sourceTree = ""; }; FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = ""; }; FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = ""; }; @@ -401,12 +414,22 @@ FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = ""; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = ""; }; + FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamView.swift; sourceTree = ""; }; + FFBF065D2BBD8040009D6715 /* MatchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchListView.swift; sourceTree = ""; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.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 = ""; }; + FFCFC0012BBC39A600B82851 /* EditScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScoreView.swift; sourceTree = ""; }; + FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointSelectionView.swift; sourceTree = ""; }; + FFCFC0112BBC3E1A00B82851 /* PointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointView.swift; sourceTree = ""; }; + FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDescriptor.swift; sourceTree = ""; }; + FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetInputView.swift; sourceTree = ""; }; + FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetLabelView.swift; sourceTree = ""; }; + FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSmallSelectionView.swift; sourceTree = ""; }; + FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDescriptor.swift; sourceTree = ""; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = ""; }; FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; FFD784012B91C1B4000F62A6 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; @@ -426,6 +449,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FFCFBFFE2BBBE86600B82851 /* Algorithms in Frameworks */, FF2BE4872B85E27400592328 /* LeStorage.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -562,6 +586,7 @@ FFC83D4B2BB807C200750834 /* Round */, FF967CF92BAEE11500A9A3BD /* GroupStage */, FF967CFE2BAEEF5A00A9A3BD /* Match */, + FFCFC00B2BBC39A600B82851 /* Score */, FF967D072BAF3D3000A9A3BD /* Team */, FF089EB92BB011EE00F0AEC7 /* Player */, FF3F74F72B919F96004CFE0E /* Tournament */, @@ -613,6 +638,7 @@ C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */, FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */, FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */, + FFBF065D2BBD8040009D6715 /* MatchListView.swift */, FF967CF72BAEDF0000A9A3BD /* Labels.swift */, ); path = Components; @@ -784,6 +810,8 @@ FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */, FFB9C8702BBADDE200A0EF4F /* Selectable.swift */, FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */, + FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */, + FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, ); path = ViewModel; sourceTree = ""; @@ -796,6 +824,7 @@ FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */, + FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */, ); path = Shared; sourceTree = ""; @@ -859,6 +888,7 @@ FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */, FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */, + FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */, ); path = GroupStage; sourceTree = ""; @@ -896,6 +926,18 @@ path = Round; sourceTree = ""; }; + FFCFC00B2BBC39A600B82851 /* Score */ = { + isa = PBXGroup; + children = ( + FFCFC0012BBC39A600B82851 /* EditScoreView.swift */, + FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */, + FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */, + FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */, + FFCFC0112BBC3E1A00B82851 /* PointView.swift */, + ); + path = Score; + sourceTree = ""; + }; FFD783FB2B91B919000F62A6 /* Agenda */ = { isa = PBXGroup; children = ( @@ -913,6 +955,7 @@ children = ( FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */, FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */, + FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */, ); path = ViewModifiers; sourceTree = ""; @@ -967,6 +1010,9 @@ dependencies = ( ); name = PadelClub; + packageProductDependencies = ( + FFCFBFFD2BBBE86600B82851 /* Algorithms */, + ); productName = PadelClub; productReference = C425D3FD2B6D249D002A7B48 /* PadelClub.app */; productType = "com.apple.product-type.application"; @@ -1039,6 +1085,9 @@ Base, ); mainGroup = C425D3F42B6D249D002A7B48; + packageReferences = ( + FF4C7F052BBBE6B90031B6A3 /* XCRemoteSwiftPackageReference "swift-algorithms" */, + ); productRefGroup = C425D3FE2B6D249D002A7B48 /* Products */; projectDirPath = ""; projectReferences = ( @@ -1160,6 +1209,7 @@ FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */, + FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */, C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */, @@ -1182,7 +1232,9 @@ FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, + FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */, FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, + FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */, FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */, FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */, FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */, @@ -1201,6 +1253,7 @@ FF8F264B2BAE0B4100650388 /* TournamentLevelPickerView.swift in Sources */, FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */, C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */, + FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */, FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */, FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */, C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */, @@ -1212,6 +1265,7 @@ FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, + FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */, FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */, FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */, C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */, @@ -1230,15 +1284,19 @@ FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, + FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, + FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, + FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */, + FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */, FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */, FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, @@ -1263,11 +1321,13 @@ FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */, FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */, + FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */, FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */, FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */, + FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */, C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, @@ -1611,6 +1671,25 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + FF4C7F052BBBE6B90031B6A3 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-algorithms.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + FFCFBFFD2BBBE86600B82851 /* Algorithms */ = { + isa = XCSwiftPackageProductDependency; + package = FF4C7F052BBBE6B90031B6A3 /* XCRemoteSwiftPackageReference "swift-algorithms" */; + productName = Algorithms; + }; +/* End XCSwiftPackageProductDependency section */ + /* Begin XCVersionGroup section */ FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */ = { isa = XCVersionGroup; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index be36bc8..ed50a94 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -7,6 +7,7 @@ import Foundation import LeStorage +import Algorithms @Observable class GroupStage: ModelObject, Storable { @@ -36,8 +37,8 @@ class GroupStage: ModelObject, Storable { self.startDate = startDate } - func teamsAt(_ index: Int) -> TeamRegistration? { - teams().first(where: { $0.groupStagePosition == index }) + func teamAt(groupStagePosition: Int) -> TeamRegistration? { + teams().first(where: { $0.groupStagePosition == groupStagePosition }) } func tournamentObject() -> Tournament? { @@ -58,23 +59,25 @@ class GroupStage: ModelObject, Storable { } func isRunning() -> Bool { // at least a match has started - matches.anySatisfy({ $0.isRunning() }) + _matches().anySatisfy({ $0.isRunning() }) } func hasStarted() -> Bool { // meaning at least one match is over - matches.filter { $0.hasEnded() }.isEmpty == false + _matches().filter { $0.hasEnded() }.isEmpty == false } func hasEnded() -> Bool { - if matches.isEmpty { return false } - return matches.allSatisfy { $0.hasEnded() } + guard teams().count == size else { return false } + let _matches = _matches() + if _matches.isEmpty { return false } + return _matches.allSatisfy { $0.hasEnded() } } func buildMatches() { - removeMatches() + _removeMatches() var _matches = [Match]() - for i in 0.. [Match] { + let ordered = _matches() + if ordered.isEmpty == false && ordered.count == _matchOrder().count { + return _matchOrder().map { + ordered[$0] + } + } else { + return ordered + } + } + + func scoreLabel(forGroupStagePosition groupStagePosition: Int) -> String? { + if let scoreData = _score(forGroupStagePosition: groupStagePosition) { + return "\(scoreData.wins)/\(scoreData.loses) " + scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false))) + } else { + return nil + } + } + + fileprivate func _score(forGroupStagePosition groupStagePosition: Int) -> TeamGroupStageScore? { + guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } + let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) + let wins = matches.filter { $0.winningTeamId == team.id }.count + let loses = matches.filter { $0.losingTeamId == team.id }.count + let differences = matches.compactMap { $0.scoreDifference(groupStagePosition) } + let setDifference = differences.map { $0.set }.reduce(0,+) + let gameDifference = differences.map { $0.game }.reduce(0,+) + return (team, wins, loses, setDifference, gameDifference) + } + + func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] { + let combos = Array((0.. [Match] { + matches().filter({ $0.canBeStarted() && $0.isRunning() == false }) + } + + func runningMatches() -> [Match] { + matches().filter({ $0.isRunning() }) + } + + func readyMatches() -> [Match] { + matches().filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) + } + + func finishedMatches() -> [Match] { + matches().filter({ $0.hasEnded() }) + } + + private func _matchOrder() -> [Int] { + switch size { + case 3: + return [1, 2, 0] + case 4: + return [2, 3, 1, 4, 5, 0] + case 5: + return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] +// return [3, 5, 8, 2, 6, 7, 1, 9, 4, 0] + case 6: + return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] + //return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] + default: + return [] + } + } + + private func _matchUp(for matchIndex: Int) -> [Int] { + Array((0.. String { + let matchUp = _matchUp(for: matchIndex) + if let index = matchUp.first, let index2 = matchUp.last { + return "#\(index + 1) contre #\(index2 + 1)" + } else { + return "--" + } + } + + func team(whichTeam team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? { + let _teams = _teams(for: matchIndex) + switch team { + case .one: + return _teams.first! + case .two: + return _teams.last! + } + } + + private func _teams(for matchIndex: Int) -> [TeamRegistration?] { + let combinations = Array(0.. Int { (size * (size - 1)) / 2 } - var matches: [Match] { + private func _matches() -> [Match] { Store.main.filter { $0.groupStage == self.id } } - func teams() -> [TeamRegistration] { - Store.main.filter { $0.groupStage == self.id } + fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool + fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) + + fileprivate func _headToHead(_ whichTeam: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { + let indexes = [whichTeam, otherTeam].compactMap({ $0.groupStagePosition }).sorted() + let combos = Array((0.. [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 + let predicates: [TeamScoreAreInIncreasingOrder] = [ + { $0.wins < $1.wins }, + { $0.setDifference < $1.setDifference }, + { $0.gameDifference < $1.gameDifference}, + { self._headToHead($0.team, $1.team) }, + { $0.team.groupStagePosition! > $1.team.groupStagePosition! } + ] + + for predicate in predicates { + if !predicate(lhs, rhs) && !predicate(rhs, lhs) { + continue + } + + return predicate(lhs, rhs) + } + + return false + }.map({ $0.team }).reversed() + } else { + return teams.sorted(by: \TeamRegistration.groupStagePosition!) + } } override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.matches) + try Store.main.deleteDependencies(items: self._matches()) } } @@ -120,6 +261,6 @@ extension GroupStage: Selectable { } func badgeValue() -> Int? { - nil + runningMatches().count } } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 77f402b..43626b8 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -54,6 +54,10 @@ class Match: ModelObject, Storable { } func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String { + if let groupStageObject { + return groupStageObject.localizedMatchUpLabel(for: index) + } + switch displayStyle { case .wide: return "Match \(indexInRound() + 1)" @@ -124,6 +128,83 @@ class Match: ModelObject, Storable { } } + func setWalkOut(_ whichTeam: TeamData) { + let teamScoreWalkout = teamScore(whichTeam) ?? TeamScore(match: id, teamRegistration: team(whichTeam)?.id) + teamScoreWalkout.walkOut = 0 + let teamScoreWinning = teamScore(whichTeam.otherTeam) ?? TeamScore(match: id, teamRegistration: team(whichTeam.otherTeam)?.id) + teamScoreWinning.walkOut = nil + try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) + + if endDate == nil { + endDate = Date() + } + + winningTeamId = teamScoreWinning.teamRegistration + losingTeamId = teamScoreWalkout.teamRegistration + + // matchDescriptor.match?.tournament?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false) + // matchDescriptor.match?.loserBracket?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false) + // matchDescriptor.match?.currentTournament?.removeField(matchDescriptor.match?.fieldIndex) + } + + func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { + updateScore(fromMatchDescriptor: matchDescriptor) + if endDate == nil { + endDate = Date() + } + // matchDescriptor.match?.tournament?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false) + // matchDescriptor.match?.loserBracket?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false) + // matchDescriptor.match?.currentTournament?.removeField(matchDescriptor.match?.fieldIndex) + winningTeamId = team(matchDescriptor.winner)?.id + losingTeamId = team(matchDescriptor.winner.otherTeam)?.id + } + + func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { + let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, teamRegistration: team(.one)?.id) + teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",") + let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, teamRegistration: team(.two)?.id) + teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",") + try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo]) + matchFormat = matchDescriptor.matchFormat + } + + func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) { + if hasEnded() == false { + startDate = fromStartDate +// if match.isTournamentMatch() { +// switch fieldSetup { +// case .random: +// let field = match.freeFields.randomElement() ?? match.currentTournament?.freeFields.randomElement() ?? 1 +// match.setupFieldAndStartDateIfPossible(field) +// case .field(let courtIndex): +// let fieldIndex = Int64(courtIndex) +// match.setupFieldAndStartDateIfPossible(fieldIndex) +// } +// } + } else { + startDate = fromStartDate + endDate = toEndDate + } + } + + func canBeStarted() -> 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 }) + } + + 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 isReady() -> Bool { teams().count == 2 } @@ -133,7 +214,7 @@ class Match: ModelObject, Storable { } func hasEnded() -> Bool { - endDate != nil + endDate != nil || hasWalkoutTeam() || winningTeamId != nil } func isGroupStage() -> Bool { @@ -166,26 +247,33 @@ class Match: ModelObject, Storable { func teams() -> [TeamRegistration] { if groupStage != nil { - return scores().compactMap({ $0.team }).sorted(by: \.groupStagePosition!) + return [groupStageProjectedTeam(.one), groupStageProjectedTeam(.two)].compactMap { $0 } } return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } } + + func scoreDifference(_ whichTeam: Int) -> (set: Int, game: Int)? { + guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil } + var reverseValue = 1 + if whichTeam == team(.two)?.groupStagePosition { + reverseValue = -1 + } + let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) + let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) + var setDifference : Int = 0 + if endedSetsOne.count == 1 { + setDifference = endedSetsOne[0] - endedSetsTwo[0] + } else { + setDifference = endedSetsOne.filter { $0 == matchFormat.setFormat.scoreToWin }.count - endedSetsTwo.filter { $0 == matchFormat.setFormat.scoreToWin }.count + } + let zip = zip(endedSetsOne, endedSetsTwo) + let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) + return (setDifference * reverseValue, gameDifference * reverseValue) + } func groupStageProjectedTeam(_ team: TeamData) -> TeamRegistration? { - guard groupStage != nil else { return nil } - - switch team { - case .one: - if let teamId = topPreviousRoundMatch()?.winningTeamId { - return Store.main.findById(teamId) - } - case .two: - if let teamId = bottomPreviousRoundMatch()?.winningTeamId { - return Store.main.findById(teamId) - } - } - - return nil + guard let groupStageObject else { return nil } + return groupStageObject.team(whichTeam: team, inMatchIndex: index) } func seed(_ team: TeamData) -> TeamRegistration? { @@ -219,16 +307,17 @@ class Match: ModelObject, Storable { } func teamWon(_ team: TeamData) -> Bool { - true + guard let winningTeamId else { return false } + return winningTeamId == self.team(team)?.id } func team(_ team: TeamData) -> TeamRegistration? { if groupStage != nil { switch team { case .one: - return teams().first + return groupStageProjectedTeam(.one) case .two: - return teams().last + return groupStageProjectedTeam(.two) } } else { switch team { @@ -245,7 +334,7 @@ class Match: ModelObject, Storable { } func teamWalkOut(_ team: TeamData) -> Bool { - false + teamScore(team)?.isWalkOut() == true } func teamScore(_ team: TeamData) -> TeamScore? { @@ -313,3 +402,20 @@ class Match: ModelObject, Storable { case _disabled = "disabled" } } + +enum MatchDateSetup: Hashable, Identifiable { + case inMinutes(Int) + case now + case customDate + + var id: Int { hashValue } +} + + +enum MatchFieldSetup: Hashable, Identifiable { + case random +// case firstAvailable + case field(String) + + var id: Int { hashValue } +} diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 1295306..590adaa 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -35,6 +35,8 @@ class PlayerRegistration: ModelObject, Storable { var weight: Int = 0 var source: PlayerDataSource? + var hasArrived: Bool = false + internal init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int, source: PlayerDataSource? = nil) { self.teamRegistration = teamRegistration self.firstName = firstName @@ -251,6 +253,7 @@ class PlayerRegistration: ModelObject, Storable { case _email = "email" case _weight = "weight" case _source = "source" + case _hasArrived = "hasArrived" } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index a6bf4ef..7bef636 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -18,7 +18,7 @@ class TeamRegistration: ModelObject, Storable { var registrationDate: Date? var callDate: Date? var bracketPosition: Int? - var groupStagePosition: Int? + var groupStagePosition: Int? //todo devrait être non nil ? var comment: String? var source: String? var sourceValue: String? @@ -32,6 +32,7 @@ class TeamRegistration: ModelObject, Storable { var weight: Int = 0 var lockWeight: Int? var confirmationDate: Date? + var qualified: Bool = false internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, category: Int? = nil) { self.tournament = tournament @@ -159,7 +160,11 @@ class TeamRegistration: ModelObject, Storable { } } - func available() -> Bool { + func canPlay() -> Bool { + teamScores().isEmpty == false || players().allSatisfy({ $0.hasPaid() || $0.hasArrived }) + } + + func availableForSeedPick() -> Bool { groupStage == nil && bracketPosition == nil } @@ -203,7 +208,7 @@ class TeamRegistration: ModelObject, Storable { } } - func qualified() -> Bool { + func qualifiedFromGroupStage() -> Bool { groupStagePosition != nil && bracketPosition != nil } @@ -304,6 +309,7 @@ class TeamRegistration: ModelObject, Storable { case _walkOut = "walkOut" case _lockWeight = "lockWeight" case _confirmationDate = "confirmationDate" + case _qualified = "qualified" } } diff --git a/PadelClub/Data/TeamScore.swift b/PadelClub/Data/TeamScore.swift index 5d30135..7bc103a 100644 --- a/PadelClub/Data/TeamScore.swift +++ b/PadelClub/Data/TeamScore.swift @@ -21,7 +21,7 @@ class TeamScore: ModelObject, Storable { var walkOut: Int? var luckyLoser: Bool - internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Bool) { + internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Bool = false) { self.match = match self.teamRegistration = teamRegistration self.playerRegistrations = playerRegistrations @@ -30,6 +30,10 @@ class TeamScore: ModelObject, Storable { self.luckyLoser = luckyLoser } + func isWalkOut() -> Bool { + walkOut != nil + } + func matchObject() -> Match? { Store.main.findById(match) } @@ -39,7 +43,6 @@ class TeamScore: ModelObject, Storable { return nil } return DataStore.shared.teamRegistrations.findById(teamRegistration) - } enum CodingKeys: String, CodingKey { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 3c3fd7e..42b8569 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -461,11 +461,11 @@ class Tournament : ModelObject, Storable { let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) - await unsortedPlayers().concurrentForEach { player in + 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) + try await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0) } await MainActor.run { @@ -505,7 +505,7 @@ class Tournament : ModelObject, Storable { func qualifiedTeams() -> [TeamRegistration] { - unsortedTeams().filter({ $0.qualified() }) + unsortedTeams().filter({ $0.qualifiedFromGroupStage() }) } func moreQualifiedToDraw() -> Int { @@ -517,7 +517,7 @@ class Tournament : ModelObject, Storable { return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in groupStage.teams()[qualifiedPerGroupStage] } - .filter({ $0.qualified() == false }) + .filter({ $0.qualifiedFromGroupStage() == false }) } else { return [] } @@ -546,11 +546,11 @@ class Tournament : ModelObject, Storable { let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }) if ongoingGroupStages.isEmpty == false { - return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" + return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours" } return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix } else { - return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" + return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours" } } diff --git a/PadelClub/Extensions/Sequence+Extensions.swift b/PadelClub/Extensions/Sequence+Extensions.swift index cb5a0f8..02001b2 100644 --- a/PadelClub/Extensions/Sequence+Extensions.swift +++ b/PadelClub/Extensions/Sequence+Extensions.swift @@ -7,6 +7,13 @@ import Foundation +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} + extension Sequence { func sorted(by keyPath: KeyPath) -> [Element] { return sorted { a, b in @@ -23,16 +30,18 @@ extension Sequence { extension Sequence { func concurrentForEach( - _ operation: @escaping (Element) async -> Void - ) async { + _ operation: @escaping (Element) async throws -> Void + ) async throws { // A task group automatically waits for all of its // sub-tasks to complete, while also performing those // tasks in parallel: - await withTaskGroup(of: Void.self) { group in + try await withThrowingTaskGroup(of: Void.self) { group in for element in self { group.addTask { - await operation(element) + try await operation(element) } + + for try await _ in group {} } } } diff --git a/PadelClub/Manager/Network/NetworkFederalService.swift b/PadelClub/Manager/Network/NetworkFederalService.swift index 7146964..1a3be1b 100644 --- a/PadelClub/Manager/Network/NetworkFederalService.swift +++ b/PadelClub/Manager/Network/NetworkFederalService.swift @@ -92,7 +92,7 @@ class NetworkFederalService { } - func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil) async -> [FederalTournament] { + func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil) async throws -> [FederalTournament] { if formId.isEmpty { do { @@ -128,24 +128,28 @@ recherche_type=club&club[autocomplete][value_container][value_field]=\(codeClub. request.httpMethod = "POST" request.httpBody = postData - do { - let commands : [HttpCommand] = try await runTenupTask(request: request) - let resultCommand = commands.first(where: { $0.results != nil }) - if let gatheredTournaments = resultCommand?.results?.items { - var finalTournaments = tournaments + gatheredTournaments - if let count = resultCommand?.results?.nb_results { - if finalTournaments.count < count { - let newTournaments = await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub) - finalTournaments = finalTournaments + newTournaments - } + let commands : [HttpCommand] = try await runTenupTask(request: request) + if commands.anySatisfy({ $0.command == "alert" }) { + throw NetworkManagerError.maintenance + } + let resultCommand = commands.first(where: { $0.results != nil }) + if let gatheredTournaments = resultCommand?.results?.items { + var finalTournaments = tournaments + gatheredTournaments + if let count = resultCommand?.results?.nb_results { + if finalTournaments.count < count { + let newTournaments = try await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub) + finalTournaments = finalTournaments + newTournaments } - - return finalTournaments } - } catch { - print("getClubFederalTournaments", error) + + return finalTournaments } - + +// do { +// } catch { +// print("getClubFederalTournaments", error) +// } +// return [] } diff --git a/PadelClub/Manager/Network/NetworkManagerError.swift b/PadelClub/Manager/Network/NetworkManagerError.swift index 04c31a8..4b3eb5b 100644 --- a/PadelClub/Manager/Network/NetworkManagerError.swift +++ b/PadelClub/Manager/Network/NetworkManagerError.swift @@ -14,4 +14,13 @@ enum NetworkManagerError: LocalizedError { case mailNotSent //no network no error case messageFailed case messageNotSent //no network no error + + var errorDescription: String? { + switch self { + case .maintenance: + return "Le site de la FFT est en maintenance" + default: + return String(describing: self) + } + } } diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index ed3bde8..8cbf94f 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -971,6 +971,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { guard let value = rawValue else { return nil } self.init(rawValue: value) } + + func defaultWalkOutScore(_ asWalkOutTeam: Bool) -> [Int] { + Array(repeating: asWalkOutTeam ? 0 : setFormat.scoreToWin, count: setsToWin) + } var weight: Int { switch self { diff --git a/PadelClub/Manager/SourceFileManager.swift b/PadelClub/Manager/SourceFileManager.swift index bd20172..690e930 100644 --- a/PadelClub/Manager/SourceFileManager.swift +++ b/PadelClub/Manager/SourceFileManager.swift @@ -78,7 +78,7 @@ class SourceFileManager { allFiles.contains(where: { $0.dateFromPath == date }) == false } - await dates.concurrentForEach { date in + try? await dates.concurrentForEach { date in await self.fetchData(fromDate: date) } } diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift new file mode 100644 index 0000000..b31024c --- /dev/null +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -0,0 +1,99 @@ +// +// MatchDescriptor.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import Foundation + +class MatchDescriptor: ObservableObject { + @Published var matchFormat: MatchFormat + @Published var setDescriptors: [SetDescriptor] + var court: Int = 1 + var title: String = "Titre du match" + var teamLabelOne: String = "" + var teamLabelTwo: String = "" + var startDate: Date = Date() + var match: Match? + + init(match: Match? = nil) { + self.match = match + if let groupStage = match?.groupStageObject { + self.matchFormat = groupStage.matchFormat + self.setDescriptors = [SetDescriptor(setFormat: groupStage.matchFormat.setFormat)] + } else { + let format = match?.matchFormat ?? match?.currentTournament()?.matchFormat ?? .defaultFormatForMatchType(.groupStage) + self.matchFormat = format + self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)] + } + self.teamLabelOne = match?.team(.one)?.teamLabel() ?? "" + self.teamLabelTwo = match?.team(.two)?.teamLabel() ?? "" + + if let match, let scoresTeamOne = match.teamScore(.one)?.score, let scoresTeamTwo = match.teamScore(.two)?.score { + + self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in + SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat) + }) + } + } + + var teamOneScores: [String] { + setDescriptors.compactMap { $0.valueTeamOne }.map { "\($0)" } + } + + var teamTwoScores: [String] { + setDescriptors.compactMap { $0.valueTeamTwo }.map { "\($0)" } + } + + var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count } + var scoreTeamTwo: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .two }.count } + + var hasEnded: Bool { + return matchFormat.hasEnded(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo) + } + + func addNewSet() { + if hasEnded == false { + setDescriptors.append(SetDescriptor(setFormat: matchFormat.newSetFormat(setCount: setDescriptors.count))) + } + } + + var winner: TeamData { + matchFormat.winner(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo) + } + + var winnerLabel: String { + if winner == .one { + return teamLabelOne + } else { + return teamLabelTwo + } + } +} + +fileprivate func combineArraysIntoTuples(_ array1: [String], _ array2: [String]) -> [(String?, String?)] { + // Zip the two arrays together and map them to tuples of optional strings + let combined = zip(array1, array2).map { (element1, element2) in + return (element1, element2) + } + + // If one array is longer than the other, append the remaining elements + let remainingElements: [(String?, String?)] + if array1.count > array2.count { + let remaining = Array(array1[array2.count...]).map { (element) in + return (element, nil as String?) + } + remainingElements = remaining + } else if array2.count > array1.count { + let remaining = Array(array2[array1.count...]).map { (element) in + return (nil as String?, element) + } + remainingElements = remaining + } else { + remainingElements = [] + } + + // Concatenate the two arrays + return combined + remainingElements +} diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift new file mode 100644 index 0000000..42c65e1 --- /dev/null +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -0,0 +1,33 @@ +// +// SetDescriptor.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import Foundation + +struct SetDescriptor: Identifiable, Equatable { + let id: UUID = UUID() + var valueTeamOne: Int? + var valueTeamTwo: Int? + var tieBreakValueTeamOne: Int? + var tieBreakValueTeamTwo: Int? + var setFormat: SetFormat + + var hasEnded: Bool { + if let valueTeamTwo, let valueTeamOne { + return setFormat.hasEnded(teamOne: valueTeamOne, teamTwo: valueTeamTwo) + } else { + return false + } + } + + var winner: TeamData? { + if let valueTeamTwo, let valueTeamOne { + return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo) + } else { + return nil + } + } +} diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index e11974f..f6770ff 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -130,7 +130,7 @@ struct ClubSearchView: View { } description: { Text("Une erreur est survenue lors de la récupération de votre localisation.") } actions: { - RowButtonView(title: "D'accord") { + RowButtonView("D'accord") { locationManager.lastError = nil } } @@ -147,7 +147,7 @@ struct ClubSearchView: View { Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") } actions: { if locationManager.manager.authorizationStatus != .restricted { - RowButtonView(title: "Chercher autour de moi") { + RowButtonView("Chercher autour de moi") { if locationManager.manager.authorizationStatus == .notDetermined { locationManager.manager.requestWhenInUseAuthorization() } else if locationManager.manager.authorizationStatus == .denied { @@ -157,7 +157,7 @@ struct ClubSearchView: View { } } } - RowButtonView(title: "Chercher une ville ou un code postal") { + RowButtonView("Chercher une ville ou un code postal") { searchPresented = true } } diff --git a/PadelClub/Views/Club/ClubsView.swift b/PadelClub/Views/Club/ClubsView.swift index b7d217b..16d8cea 100644 --- a/PadelClub/Views/Club/ClubsView.swift +++ b/PadelClub/Views/Club/ClubsView.swift @@ -60,10 +60,10 @@ struct ClubsView: View { } description: { Text("Texte décrivant l'utilité d'un club et les features que cela apporte") } actions: { - RowButtonView(title: "Créer un nouveau club", systemImage: "plus.circle.fill") { + RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") { presentClubCreationView = true } - RowButtonView(title: "Chercher un club", systemImage: "magnifyingglass.circle.fill") { + RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") { presentClubSearchView = true } } diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 9390c99..943aa37 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -26,7 +26,7 @@ struct GenericDestinationPickerView: View { .background { Circle() .fill(Color.white) - .opacity(selectedDestination == nil ? 1.0 : 0.5) + .opacity(selectedDestination == nil ? 1.0 : 0.4) } .buttonStyle(.plain) } @@ -41,7 +41,7 @@ struct GenericDestinationPickerView: View { .background { Capsule() .fill(Color.white) - .opacity(selectedDestination?.id == destination.id ? 1.0 : 0.5) + .opacity(selectedDestination?.id == destination.id ? 1.0 : 0.4) } .buttonStyle(.plain) .overlay(alignment: .bottomTrailing) { diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift new file mode 100644 index 0000000..4f36123 --- /dev/null +++ b/PadelClub/Views/Components/MatchListView.swift @@ -0,0 +1,45 @@ +// +// MatchListView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/04/2024. +// + +import SwiftUI + +struct MatchListView: View { + @EnvironmentObject var dataStore: DataStore + let section: String + let matches: [Match] + var matchViewStyle: MatchViewStyle = .sectionedStandardStyle + + @State private var isExpanded: Bool = true + + @ViewBuilder + var body: some View { + if matches.isEmpty == false { + Section { + if isExpanded { + ForEach(matches) { match in + MatchRowView(match: match, matchViewStyle: matchViewStyle) + } + } + } header: { + Button { + isExpanded.toggle() + } label: { + HStack { + Text(section.capitalized) + Spacer() + Text(matches.count.formatted()) + Image(systemName: isExpanded ? "chevron.down.circle" : "chevron.right.circle") + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .frame(maxWidth: .infinity) + } + .headerProminence(.increased) + } + } +} diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift index 4aa12bd..b63c450 100644 --- a/PadelClub/Views/Components/RowButtonView.swift +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -7,16 +7,35 @@ import SwiftUI +fileprivate let defaultConfirmationMessage = "Êtes-vous sûr de vouloir faire cela ?" + struct RowButtonView: View { + var role: ButtonRole? = nil let title: String var systemImage: String? = nil var image: String? = nil var animatedProgress: Bool = false + let confirmationMessage: String let action: () -> () - + @State private var askConfirmation: Bool = false + + init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, animatedProgress: Bool = false, confirmationMessage: String? = nil, action: @escaping () -> Void) { + self.role = role + self.title = title + self.systemImage = systemImage + self.image = image + self.animatedProgress = animatedProgress + self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage + self.action = action + } + var body: some View { - Button { - action() + Button(role: role) { + if role == .destructive { + askConfirmation = true + } else { + action() + } } label: { HStack { if animatedProgress { @@ -47,8 +66,19 @@ struct RowButtonView: View { .disabled(animatedProgress) .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) - .tint(.launchScreenBackground) + .tint(role == .destructive ? Color.red : Color.launchScreenBackground) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(.zero)) + .confirmationDialog("Confirmation", + isPresented: $askConfirmation, + titleVisibility: .visible) { + Button("OK") { + action() + } + Button("Annuler", role: .cancel) {} + } message: { + Text(confirmationMessage) + } + } } diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index 6715484..3b9670f 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -88,7 +88,7 @@ struct EventCreationView: View { } Section { - RowButtonView(title:"Valider") { + RowButtonView("Valider") { if tournaments.count > 1 || eventName.trimmed.isEmpty == false || selectedClub != nil { let event = Event(name: eventName) event.club = selectedClub?.id @@ -143,7 +143,7 @@ struct EventCreationView: View { } Section { - RowButtonView(title: "Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") { + RowButtonView("Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") { let tournament = Tournament.newEmptyInstance() self.tournaments.append(tournament) } diff --git a/PadelClub/Views/GroupStage/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/GroupStageSettingsView.swift index d6fafb2..6262468 100644 --- a/PadelClub/Views/GroupStage/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStageSettingsView.swift @@ -38,7 +38,7 @@ struct GroupStageSettingsView: View { // if (tournament.groupStagesAreWrong || (tournament.emptySlotInGroupStages > 0 && tournament.entriesCount >= tournament.teamsFromGroupStages)) { // Section { -// RowButtonView(title: "Reconstruire les poules") { +// RowButtonView("Reconstruire les poules") { // confirmGroupStageRebuild = true // } // .modify { @@ -83,7 +83,7 @@ struct GroupStageSettingsView: View { // if tournament.isRoundSwissTournament() == false && (tournament.orderedGroupStages.allSatisfy({ $0.orderedMatches.count > 0 }) == false && tournament.groupStagesAreOver == false && tournament.groupStagesCount > 0) { // Section { -// RowButtonView(title: "Générer les matchs de poules") { +// RowButtonView("Générer les matchs de poules") { // startAllGroupStageConfirmation = true // } // .modify { diff --git a/PadelClub/Views/GroupStage/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/GroupStageTeamView.swift new file mode 100644 index 0000000..5dec47f --- /dev/null +++ b/PadelClub/Views/GroupStage/GroupStageTeamView.swift @@ -0,0 +1,107 @@ +// +// GroupStageTeamView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/04/2024. +// + +import SwiftUI + +struct GroupStageTeamView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) private var dismiss + let groupStage: GroupStage + var team: TeamRegistration + + var body: some View { + List { + ForEach(team.players()) { player in + Section { + ImportedPlayerView(player: player) + } footer: { + HStack { + Button("contacter") { + } + Spacer() + Button { + player.hasArrived.toggle() + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + } label: { + Label("présent", systemImage: player.hasArrived ? "checkmark.circle" : "circle") + } + } + } + } + + if groupStage.tournamentObject()?.hasEnded() == false { + Section { + if team.qualified == false { + RowButtonView("Qualifier l'équipe") { + team.qualified = true + team.bracketPosition = nil + _save() + } + } + } + Section { + if team.qualified { + RowButtonView("Annuler la qualification", role: .destructive) { + team.qualified = false + team.bracketPosition = nil + _save() + } + } + } + + Section { + // if let deltaLabel = bracket.tournament?.deltaLabel(index, bracketIndex: bracket.index.intValue) { + // Section { + // Button(role: .destructive) { + // entrant.resetBracketPosition() + // save() + // } label: { + // Text(deltaLabel) + // } + // Divider() + // } header: { + // Text("Toute l'équipe, poids: " + entrant.updatedRank.formatted()) + // } + // } + // + // ForEach(entrant.orderedPlayers) { player in + // if let deltaLabel = bracket.tournament?.playerDeltaLabel(rank: entrant.otherPlayer(player)?.currentRank, positionInBracket: index, bracketIndex: bracket.index.intValue) { + // Section { + // Button(role: .destructive) { + // entrant.team?.removeFromPlayers(player) + // save() + // } label: { + // Text(deltaLabel) + // } + // Divider() + // } header: { + // Text(player.longLabel + ", rang: " + player.formattedRank) + // } + // } + // } + } header: { + Text("Remplacement") + } + + Section { + RowButtonView("Retirer de la poule") { + team.groupStagePosition = nil + team.groupStage = nil + _save() + } + } + } + } + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Détail de l'équipe") + } + + private func _save() { + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + dismiss() + } +} diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 65dc19d..5b33f7b 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -8,209 +8,250 @@ import SwiftUI struct GroupStageView: View { + @EnvironmentObject var dataStore: DataStore @Bindable var groupStage: GroupStage -// @State private var selectedMenuLink: MenuLink? -// @State private var canUpdateTournament: Bool = false -// @AppStorage("showLongLabel") private var showLongLabel: Bool = false -// @AppStorage("hideRank") private var hideRank: Bool = false @State private var confirmGroupStageStart: Bool = false + @State private var sortingMode: GroupStageSortingMode = .auto + @State private var confirmRemoveAll: Bool = false + @State private var confirmResetMatch: Bool = false + + private enum GroupStageSortingMode { + case auto + case score + case weight + } - enum MenuLink: Int, Identifiable, Hashable { - var id: Int { self.rawValue } - case prepare + var sortByScore: Bool { + sortingMode == .auto ? groupStage.hasEnded() : sortingMode == .score } - var groupStageView: some View { - ForEach(0..<(groupStage.size), id: \.self) { index in -// let entrant : Entrant? = runningGroupStageOrderedByScore ? groupStage.orderedByScore[Int(index)] : groupStage.entrantAtIndex(Int(index)) - if let team = groupStage.teamsAt(index) { - Text(team.teamLabel()) -// GroupStageEntrantMenuView(entrant: entrant, groupStage: groupStage, index: index.intValue) - } else { - Menu { -// Section { -// EntrantPickerView(groupStage: groupStage, index: Int(index)) -// } -// -// if let tournament = groupStage.tournament, let deltaLabel = tournament.deltaLabel(index.intValue, groupStageIndex: groupStage.index.intValue) { -// let date = tournament.localizedDate ?? "" -// Divider() -// Section { -// ShareLink(item: "\(tournament.localizedTitle)\n\(date)\nCherche une équipe dont le poids d'équipe " + deltaLabel) { -// Text(deltaLabel) -// } -// } header: { -// Text("Remplacer avec un poids d'équipe") -// } -// } - } label: { - HStack { - Text("#\(index+1)") - Text("Aucune équipe") - } - } - } - } + func teamAt(atIndex index: Int) -> TeamRegistration? { + sortByScore ? groupStage.teams(sortByScore)[safe: index] : groupStage.teamAt(groupStagePosition: index) } var body: some View { List { Section { - groupStageView - // .disabled(canUpdateTournament == false) - // .sheet(item: $selectedMenuLink) { selectedMenuLink in - // switch selectedMenuLink { - // case .prepare: - // PrepareGroupStageView(groupStage: groupStage) - // } - // } + _groupStageView() } header: { HStack { - if groupStage.isBroadcasted() { - Label(groupStage.groupStageTitle(), systemImage: "airplayvideo") - } else { - Text(groupStage.groupStageTitle()) - } - Spacer() 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 + } else { + sortingMode = .weight + } + } label: { + Label(sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly) + } } - } footer: { - HStack { - if groupStage.matches.isEmpty { - Button { - //groupStage.startGroupStage() - //save() - } label: { - Text("Créer les matchs") + .buttonStyle(.plain) + } + + 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()) + MatchListView(section: "terminés", matches: groupStage.finishedMatches()).id(UUID()) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + _groupStageMenuView() + } + } + } + + private func _groupStageView() -> some View { + ForEach(0..<(groupStage.size), id: \.self) { index in + if let team = teamAt(atIndex: index), let groupStagePosition = team.groupStagePosition { + NavigationLink { + GroupStageTeamView(groupStage: groupStage, team: team) + } label: { + HStack(alignment: .center) { + VStack(alignment: .leading, spacing: 0) { + HStack { + Text("#\(groupStagePosition + 1)") + Text("Poids \(team.weight)") + } + .font(.caption) + HStack { + if let teamName = team.name { + Text(teamName) + } else { + VStack(alignment: .leading) { + ForEach(team.players()) { player in + Text(player.playerLabel()) + } + } + } + + if team.qualified { + Image(systemName: "checkmark.seal") + } + } + } + Spacer() + if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition) { + Text(score) } - .buttonStyle(.borderless) } - Spacer() - Menu { - // Button { - // selectedMenuLink = .prepare - // } label: { - // Label("Préparer", systemImage: "calendar") - // } - // - // Menu { - // MenuWarnView(warningSender: groupStage) - // } label: { - // Label("Prévenir", systemImage: "person.crop.circle") - // } - // - // if groupStage.isBroadcasted() { - // Button { - // groupStage.refreshBroadcast() - // } label: { - // Label("Rafraîchir", systemImage: "arrow.up.circle.fill") - // } - // Button { - // groupStage.stopBroadcast() - // save() - // } label: { - // Label("Arrêter la diffusion", systemImage: "stop.circle.fill") - // } - // } else if groupStage.tournament?.canBroadcast() == true { - // Button { - // Task { - // try? await groupStage.broadcastGroupStage() - // save() - // } - // } label: { - // Label("Diffuser", systemImage: "airplayvideo") - // } - // } - // - // Divider() - // if groupStage.tournament?.canBroadcast() == true { - // Menu { - // Button { - // Task { - // try? await groupStage.broadcastGroupStageMatches() - // save() - // } - // } label: { - // Label("Diffuser", systemImage: "airplayvideo") - // } - // - // Button { - // groupStage.refreshBroadcastMatches() - // } label: { - // Label("Rafraîchir", systemImage: "arrow.up.circle.fill") - // } - // Button { - // groupStage.stopBroadcastMatches() - // save() - // } label: { - // Label("Arrêter la diffusion", systemImage: "stop.circle.fill") - // } - // } label: { - // Text("Diffusion des matchs") - // } - // } - // - // Divider() - // Menu { - // if groupStage.orderedMatches.isEmpty == false { - // Button(role: .destructive) { - // groupStage.startGroupStage() - // save() - // } label: { - // Label("Re-démarrer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash") - // } - // } - // - // if groupStage.orderedMatches.isEmpty == false { - // Button(role: .destructive) { - // groupStage.removeMatches() - // save() - // } label: { - // Label("Supprimer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash") - // } - // } - // - // Button(role: .destructive) { - // groupStage.tournament?.completeEntries.filter { $0.groupStagePosition == groupStage.index }.forEach { $0.resetGroupStagePosition() } - // groupStage.tournament?.removeFromGroupStages(groupStage) - // groupStage.tournament?.numberOfGroupStages -= 1 - // save() - // } label: { - // Label("Supprimer la \(groupStage.titleLabel.lowercased())", systemImage: "trash") - // } - // } label: { - // Text("Éditer") - // } - } label: { + } + } else { + HStack(alignment: .center) { + VStack(alignment: .leading, spacing: 0) { HStack { - Spacer() - Label("Options", systemImage: "ellipsis.circle").labelStyle(.titleOnly) + Text("#\(index + 1)") } + .font(.caption) + TeamPickerView(teamPicked: { team in + print(team.pasteData()) + team.groupStage = groupStage.id + team.groupStagePosition = index + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + }) } - .buttonStyle(.borderless) } } + } + } + + private func _groupStageMenuView() -> some View { + Menu { + if groupStage.matches().isEmpty { + Button { + //groupStage.startGroupStage() + //save() + } label: { + Text("Créer les matchs") + } + .buttonStyle(.borderless) + } - if groupStage.matches.isEmpty == false { - Section { - ForEach(groupStage.matches) { match in - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) - } - } header: { - Text("Matchs de la " + groupStage.groupStageTitle()) + Button("Retirer tout le monde", role: .destructive) { + confirmRemoveAll = true + } + Button("Recommencer tous les matchs", role: .destructive) { + confirmResetMatch = true + } + + // Button { + // selectedMenuLink = .prepare + // } label: { + // Label("Préparer", systemImage: "calendar") + // } + // + // Menu { + // MenuWarnView(warningSender: groupStage) + // } label: { + // Label("Prévenir", systemImage: "person.crop.circle") + // } + // + // if groupStage.isBroadcasted() { + // Button { + // groupStage.refreshBroadcast() + // } label: { + // Label("Rafraîchir", systemImage: "arrow.up.circle.fill") + // } + // Button { + // groupStage.stopBroadcast() + // save() + // } label: { + // Label("Arrêter la diffusion", systemImage: "stop.circle.fill") + // } + // } else if groupStage.tournament?.canBroadcast() == true { + // Button { + // Task { + // try? await groupStage.broadcastGroupStage() + // save() + // } + // } label: { + // Label("Diffuser", systemImage: "airplayvideo") + // } + // } + // + // Divider() + // if groupStage.tournament?.canBroadcast() == true { + // Menu { + // Button { + // Task { + // try? await groupStage.broadcastGroupStageMatches() + // save() + // } + // } label: { + // Label("Diffuser", systemImage: "airplayvideo") + // } + // + // Button { + // groupStage.refreshBroadcastMatches() + // } label: { + // Label("Rafraîchir", systemImage: "arrow.up.circle.fill") + // } + // Button { + // groupStage.stopBroadcastMatches() + // save() + // } label: { + // Label("Arrêter la diffusion", systemImage: "stop.circle.fill") + // } + // } label: { + // Text("Diffusion des matchs") + // } + // } + // + // Divider() + // Menu { + // if groupStage.orderedMatches.isEmpty == false { + // Button(role: .destructive) { + // groupStage.startGroupStage() + // save() + // } label: { + // Label("Re-démarrer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash") + // } + // } + // + // if groupStage.orderedMatches.isEmpty == false { + // Button(role: .destructive) { + // groupStage.removeMatches() + // save() + // } label: { + // Label("Supprimer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash") + // } + // } + // + // Button(role: .destructive) { + // groupStage.tournament?.completeEntries.filter { $0.groupStagePosition == groupStage.index }.forEach { $0.resetGroupStagePosition() } + // groupStage.tournament?.removeFromGroupStages(groupStage) + // groupStage.tournament?.numberOfGroupStages -= 1 + // save() + // } label: { + // Label("Supprimer la \(groupStage.titleLabel.lowercased())", systemImage: "trash") + // } + // } label: { + // Text("Éditer") + // } + } label: { + LabelOptions() + } + .confirmationDialog("Êtes-vous sûr de vouloir faire cela ?", isPresented: $confirmRemoveAll, titleVisibility: .visible) { + Button("Oui") { + let teams = groupStage.teams() + teams.forEach { team in + team.groupStagePosition = nil + team.groupStage = nil } + try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams) } - } -// .onAppear { -// if let tournament = groupStage.tournament { -// canUpdateTournament = PersistenceController.shared.container.canUpdateRecord(forManagedObjectWith: tournament.objectID) -// } else { -// canUpdateTournament = true -// } -// } + .confirmationDialog("Êtes-vous sûr de vouloir faire cela ?", isPresented: $confirmResetMatch, titleVisibility: .visible) { + Button("Oui") { + groupStage.buildMatches() + } + } + } // func save() { @@ -228,163 +269,3 @@ struct GroupStageView: View { // } // } } - - -//struct GroupStageEntrantMenuView: View { -// @ObservedObject var entrant: Entrant -// @ObservedObject var groupStage: GroupStage -// @Environment(\.managedObjectContext) private var viewContext -// @AppStorage("showLongLabel") private var showLongLabel: Bool = false -// @AppStorage("hideRank") private var hideRank: Bool = false -// -// let index: Int -// -// var body: some View { -// Menu { -// ForEach(entrant.orderedPlayers) { player in -// Menu { -// Text(player.formattedRank) -// Text(player.localizedAge) -// if let computedClubName = player.computedClubName { -// Text(computedClubName) -// } -// } label: { -// Text(player.longLabel) -// } -// } -// -// if groupStage.tournament?.isOver == false { -// if entrant.qualified == false { -// Divider() -// Button { -// entrant.addToQualifiedGroup() -// entrant.objectWillChange.send() -// entrant.orderedGroupStages.forEach { $0.objectWillChange.send() } -// entrant.currentTournament?.objectWillChange.send() -// entrant.currentTournament?.orderedMatches.forEach { $0.objectWillChange.send() } -// save() -// } label: { -// Label("Qualifier l'équipe", systemImage: "checkmark") -// } -// } -// -// Divider() -// if entrant.qualified { -// Menu { -// Button(role: .destructive) { -// entrant.unqualified() -// entrant.objectWillChange.send() -// entrant.orderedGroupStages.forEach { $0.objectWillChange.send() } -// entrant.currentTournament?.objectWillChange.send() -// entrant.currentTournament?.orderedMatches.forEach { $0.objectWillChange.send() } -// save() -// } label: { -// Label("Annuler la qualification", systemImage: "xmark") -// } -// } label: { -// Text("Qualification") -// } -// } -// -// Menu { -// if let deltaLabel = groupStage.tournament?.deltaLabel(index, groupStageIndex: groupStage.index.intValue) { -// Section { -// Button(role: .destructive) { -// entrant.resetGroupStagePosition() -// save() -// } label: { -// Text(deltaLabel) -// } -// Divider() -// } header: { -// Text("Toute l'équipe, poids: " + entrant.updatedRank.formatted()) -// } -// } -// -// ForEach(entrant.orderedPlayers) { player in -// if let deltaLabel = groupStage.tournament?.playerDeltaLabel(rank: entrant.otherPlayer(player)?.currentRank, positionInGroupStage: index, groupStageIndex: groupStage.index.intValue) { -// Section { -// Button(role: .destructive) { -// entrant.team?.removeFromPlayers(player) -// save() -// } label: { -// Text(deltaLabel) -// } -// Divider() -// } header: { -// Text(player.longLabel + ", rang: " + player.formattedRank) -// } -// } -// } -// } label: { -// Text("Remplacement") -// } -// -// Menu { -// Button(role: .destructive) { -// entrant.resetGroupStagePosition() -// save() -// } label: { -// Label("Retirer l'équipe", systemImage: "xmark") -// } -// } label: { -// Text("Retirer") -// } -// -// } -// } label: { -// HStack(alignment: .center) { -// if let tournament = groupStage.tournament, groupStage.hasEnded, groupStage.groupStageRound > 0 { -// Text("#\(index + Int((groupStage.index - tournament.numberOfGroupStages)*tournament.teamsPerGroupStage) + 1)") -// } else { -// Text("#\(index + 1)") -// } -// VStack(alignment: .leading, spacing: 0) { -// if hideRank == false { -// Text("Poids \(entrant.updatedRank)") -// .font(.caption) -// } -// -// HStack { -// if let brand = entrant.team?.brand?.title { -// Text(brand) -// } else { -// -// VStack(alignment: .leading) { -// Text(entrant.longLabelPlayerOne) -// Text(entrant.longLabelPlayerTwo) -// } -// -// } -// -// if groupStage.tournament?.isRoundSwissTournament() == true { -// if entrant.groupStagePosition == groupStage.index { -// Text("forcé") -// } else { -// Text("auto") -// } -// } else { -// if entrant.qualified { -// Image(systemName: "checkmark.seal") -// } -// } -// } -// } -// Spacer() -// Text(groupStage.scoreLabel(for: entrant.position(in: groupStage))) -// } -// } -// .buttonStyle(.plain) -// } -// -// func save() { -// do { -// 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)") -// } -// } -//} diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index f3ef141..a5ea044 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -9,23 +9,78 @@ import SwiftUI struct GroupStagesView: View { var tournament: Tournament - @State private var selectedGroupStage: GroupStage? - + @State private var selectedDestination: GroupStageDestination? + + enum GroupStageDestination: Selectable, Identifiable { + case all + case groupStage(GroupStage) + + var id: String { + switch self { + case .all: + return "all-group-stage" + case .groupStage(let groupStage): + return groupStage.id + } + } + + func selectionLabel() -> String { + switch self { + case .all: + return "Tout" + case .groupStage(let groupStage): + return groupStage.groupStageTitle() + } + } + + func badgeValue() -> Int? { + switch self { + case .all: + return nil + case .groupStage(let groupStage): + return groupStage.badgeValue() + } + } + } + init(tournament: Tournament) { self.tournament = tournament - _selectedGroupStage = State(wrappedValue: tournament.getActiveGroupStage()) + let gs = tournament.getActiveGroupStage() + if let gs { + _selectedDestination = State(wrappedValue: .groupStage(gs)) + } + } + + func allDestinations() -> [GroupStageDestination] { + var allDestinations : [GroupStageDestination] = [.all] + let groupStageDestinations : [GroupStageDestination] = tournament.groupStages().map { GroupStageDestination.groupStage($0) } + allDestinations.append(contentsOf: groupStageDestinations) + return allDestinations } var body: some View { VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $selectedGroupStage, destinations: tournament.groupStages(), nilDestinationIsValid: true) - switch selectedGroupStage { - case .none: - GroupStageSettingsView() - .navigationTitle("Réglages") - case .some(let groupStage): + GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) + switch selectedDestination { + case .all: + List { + let allGroupStages = tournament.groupStages() + let availableToStart = allGroupStages.flatMap({ $0.availableToStart() }) + let runningMatches = allGroupStages.flatMap({ $0.runningMatches() }) + let readyMatches = allGroupStages.flatMap({ $0.readyMatches() }) + let finishedMatches = allGroupStages.flatMap({ $0.finishedMatches() }) + MatchListView(section: "disponible", matches: availableToStart, matchViewStyle: .standardStyle) + MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle) + MatchListView(section: "à lancer", matches: readyMatches, matchViewStyle: .standardStyle) + MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle) + } + .navigationTitle("Toutes les poules") + case .groupStage(let groupStage): GroupStageView(groupStage: groupStage) .navigationTitle(groupStage.groupStageTitle()) + case nil: + GroupStageSettingsView() + .navigationTitle("Réglages") } } .navigationBarTitleDisplayMode(.inline) diff --git a/PadelClub/Views/Match/MatchDateView.swift b/PadelClub/Views/Match/MatchDateView.swift index a8c3185..9dd35c5 100644 --- a/PadelClub/Views/Match/MatchDateView.swift +++ b/PadelClub/Views/Match/MatchDateView.swift @@ -83,7 +83,7 @@ struct MatchDateView: View { .monospacedDigit() } - if match.startDate == nil { + if match.startDate == nil && match.hasEnded() == false { Text("démarrage").font(.footnote).foregroundStyle(.secondary) Text("non défini") } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 0215b38..ccf3b9b 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -8,6 +8,7 @@ import SwiftUI struct MatchDetailView: View { + @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) var dismiss let matchViewStyle: MatchViewStyle @@ -202,11 +203,14 @@ struct MatchDetailView: View { // } // .presentationDetents([.fraction(0.66)]) // } -// .sheet(item: $scoreType, onDismiss: { -// if match.hasEnded() && match.isTournamentMatch() { -// dismiss() -// } -// }) { scoreType in + .sheet(item: $scoreType, onDismiss: { + if match.hasEnded() && match.isTournamentMatch() { + dismiss() + } + }) { scoreType in + let matchDescriptor = MatchDescriptor(match: match) + EditScoreView(matchDescriptor: matchDescriptor) + // switch scoreType { // case .edition: // let matchDescriptor = MatchDescriptor(match: match) @@ -238,8 +242,8 @@ struct MatchDetailView: View { // FeedbackView(feedbackData: feedbackData) // } // } -// -// } + + } // .refreshable { // if match.isBroadcasted() { @@ -308,24 +312,6 @@ struct MatchDetailView: View { .navigationBarTitleDisplayMode(.large) } - enum MatchDateSetup: Hashable, Identifiable { - case inMinutes(Int) - case now - case customDate - - var id: Int { hashValue } - } - - - enum MatchFieldSetup: Hashable, Identifiable { - case random -// case firstAvailable - case field(String) - - var id: Int { hashValue } - } - - enum ScoreType: Int, Identifiable, Hashable { var id: Int { self.rawValue @@ -338,14 +324,6 @@ struct MatchDetailView: View { case health = 5 } - var entrantLabelOne: String { - return "match.longLabelTeamOne" - } - - var entrantLabelTwo: String { - return "match.longLabelTeamTwo" - } - @ViewBuilder var menuView: some View { if match.isReady() { @@ -374,7 +352,7 @@ struct MatchDetailView: View { } var inputScoreView: some View { - RowButtonView(title: "Saisir les résultats", systemImage: "list.clipboard") { + RowButtonView("Saisir les résultats", systemImage: "list.clipboard") { scoreType = .edition } } @@ -461,27 +439,9 @@ struct MatchDetailView: View { // } // } - RowButtonView(title: "Valider") { - if match.hasEnded() == false { - match.startDate = startDate + RowButtonView("Valider") { + match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) - if match.isTournamentMatch() { -// switch fieldSetup { -// case .random: -// let field = match.freeFields.randomElement() ?? match.currentTournament?.freeFields.randomElement() ?? 1 -// match.setupFieldAndStartDateIfPossible(field) -// case .field(let courtIndex): -// let fieldIndex = Int64(courtIndex) -// match.setupFieldAndStartDateIfPossible(fieldIndex) -// } - } - } else { - match.startDate = startDate - if match.endDate != nil { - match.endDate = endDate - } - } - if broadcasted { broadcastAndSave() } else { @@ -501,12 +461,12 @@ struct MatchDetailView: View { var broadcastView: some View { Section { // if match.isBroadcasted() { -// RowButtonView(title: "Arrêter de diffuser") { +// RowButtonView("Arrêter de diffuser") { // match.stopBroadcast() // save() // } // } else if match.canBroadcast() == true { -// RowButtonView(title: "Diffuser", systemImage: "airplayvideo") { +// RowButtonView("Diffuser", systemImage: "airplayvideo") { // broadcastAndSave() // } // } @@ -523,6 +483,7 @@ struct MatchDetailView: View { private func save() { + try? dataStore.matches.addOrUpdate(instance: match) } private func broadcastAndSave() { diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 9b9e90a..7a1f021 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -61,9 +61,7 @@ struct MatchSummaryView: View { if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle { Text(groupStage.groupStageTitle()) } -// if let index = match.entrantOne()?.bracketPositions?.first, let index2 = match.entrantTwo()?.bracketPositions?.first { -// Text("#\(index) contre #\(index2)") -// } + Text(match.matchTitle()) } else if let currentTournament = match.currentTournament() { if matchViewStyle == .feedStyle { //tournamentHeaderView(currentTournament) diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 1b53516..a6271f0 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -20,7 +20,8 @@ struct ActivityView: View { @State private var federalTournaments: [FederalTournament] = [] @State private var isGatheringFederalTournaments: Bool = false @Binding var selectedTab: TabDestination? - + @State private var error: Error? + var runningTournaments: [FederalTournamentHolder] { dataStore.tournaments.filter({ $0.endDate == nil }) } @@ -65,7 +66,17 @@ struct ActivityView: View { } } .overlay { - if isGatheringFederalTournaments { + if let error, agendaDestination == .tenup { + ContentUnavailableView { + Label("Erreur", systemImage: "exclamationmark") + } description: { + Text(error.localizedDescription) + } actions: { + RowButtonView("D'accord.") { + self.error = nil + } + } + } else if isGatheringFederalTournaments { ProgressView() } else { if tournaments.isEmpty { @@ -77,7 +88,7 @@ struct ActivityView: View { } description: { Text("Description du filtre") } actions: { - RowButtonView(title: "supprimer le filtre") { + RowButtonView("supprimer le filtre") { filterEnabled.toggle() } } @@ -182,8 +193,12 @@ struct ActivityView: View { private func _gatherFederalTournaments() { isGatheringFederalTournaments = true Task { - await dataStore.clubs.filter { $0.code != nil }.concurrentForEach { club in - federalTournaments += await NetworkFederalService.shared.getClubFederalTournaments(page: 0, tournaments: [], club: club.name, codeClub: club.code!, startDate: .now.startOfMonth) + do { + try await dataStore.clubs.filter { $0.code != nil }.concurrentForEach { club in + federalTournaments += try await NetworkFederalService.shared.getClubFederalTournaments(page: 0, tournaments: [], club: club.name, codeClub: club.code!, startDate: .now.startOfMonth) + } + } catch { + self.error = error } isGatheringFederalTournaments = false } @@ -215,10 +230,10 @@ struct ActivityView: View { } description: { Text("Aucun événement en cours ou à venir dans votre agenda.") } actions: { - RowButtonView(title: "Créer un nouvel événement") { + RowButtonView("Créer un nouvel événement") { newTournament = Tournament.newEmptyInstance() } - RowButtonView(title: "Importer via Tenup") { + RowButtonView("Importer via Tenup") { agendaDestination = .tenup } } @@ -239,7 +254,7 @@ struct ActivityView: View { } description: { Text("Pour voir vos tournois tenup ici, indiquez vos clubs préférés.") } actions: { - RowButtonView(title: "Choisir mes clubs préférés") { + RowButtonView("Choisir mes clubs préférés") { selectedTab = .umpire } } @@ -249,7 +264,7 @@ struct ActivityView: View { } description: { Text("Aucun tournoi n'a pu être récupéré via tenup.") } actions: { - RowButtonView(title: "Rafraîchir") { + RowButtonView("Rafraîchir") { _gatherFederalTournaments() } } diff --git a/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift b/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift index 862193b..a04b81d 100644 --- a/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/EmptyActivityView.swift @@ -16,13 +16,13 @@ struct EmptyActivityView: View { WelcomeView() Section { - RowButtonView(title: "Créer votre premier événement", action: { + RowButtonView("Créer votre premier événement", action: { newTournament = Tournament.newEmptyInstance() }) } Section { - RowButtonView(title: "Importer vos tournois Tenup", action: { + RowButtonView("Importer vos tournois Tenup", action: { }) } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 2bb94a5..d61c9db 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -132,24 +132,6 @@ struct MainView: View { } } -fileprivate extension View { - func tabItem(for tabDestination: TabDestination) -> some View { - modifier(TabItemModifier(tabDestination: tabDestination)) - } -} - -fileprivate struct TabItemModifier: ViewModifier { - let tabDestination: TabDestination - - func body(content: Content) -> some View { - content - .tabItem { - Label(tabDestination.title, systemImage: tabDestination.image) - } - .tag(tabDestination as TabDestination?) - } -} - #Preview { MainView() } diff --git a/PadelClub/Views/Navigation/PadelClubView.swift b/PadelClub/Views/Navigation/PadelClubView.swift index 8c3eea0..fd7d081 100644 --- a/PadelClub/Views/Navigation/PadelClubView.swift +++ b/PadelClub/Views/Navigation/PadelClubView.swift @@ -54,7 +54,7 @@ struct PadelClubView: View { // } description: { // Text("Padel peut importer toutes les données publique de la FFT concernant tous les compétiteurs et compétitrices.") // } actions: { -// RowButtonView(title: "Démarrer l'importation") { +// RowButtonView("Démarrer l'importation") { // _startImporting() // } // } diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift index 2e33a63..8be49b6 100644 --- a/PadelClub/Views/Player/Components/PlayerPopoverView.swift +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -166,7 +166,7 @@ struct PlayerPopoverView: View { .multilineTextAlignment(.trailing) Section { - RowButtonView(title: "Valider et ajouter un autre") { + RowButtonView("Valider et ajouter un autre") { createManualPlayer() lastName = "" firstName = "" diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index edef755..b3ad616 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -23,14 +23,14 @@ struct RoundSettingsView: View { Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed) Section { - RowButtonView(title: "Retirer toutes les têtes de séries") { + RowButtonView("Retirer toutes les têtes de séries") { tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) } } Section { if let lastRound = tournament.rounds().first { // first is final, last round - RowButtonView(title: "Supprimer " + lastRound.roundTitle()) { + RowButtonView("Supprimer " + lastRound.roundTitle()) { try? dataStore.rounds.delete(instance: lastRound) } } @@ -38,7 +38,7 @@ struct RoundSettingsView: View { Section { let roundIndex = tournament.rounds().count - RowButtonView(title: "Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex)) { + RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex)) { let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) @@ -64,7 +64,7 @@ struct RoundSettingsView: View { if let roundIndex { - RowButtonView(title: "Valider") { + RowButtonView("Valider") { if availableSeedGroup == SeedInterval(first: 1, last: 2) { let seeds = tournament.seeds() // let startIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift new file mode 100644 index 0000000..4e5bd6c --- /dev/null +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -0,0 +1,104 @@ +// +// EditScoreView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 27/02/2023. +// + +import SwiftUI + +struct EditScoreView: View { + @EnvironmentObject var dataStore: DataStore + @ObservedObject var matchDescriptor: MatchDescriptor + @Environment(\.dismiss) private var dismiss + + func walkout(_ team: TeamData) { + matchDescriptor.match?.setWalkOut(team) + save() + dismiss() + } + + var body: some View { + Form { + Section { + Text(matchDescriptor.teamLabelOne) + Text(matchDescriptor.teamLabelTwo) + } footer: { + HStack { + Menu { + Button { + walkout(.one) + } label: { + Text(matchDescriptor.teamLabelOne) + } + Button { + walkout(.two) + } label: { + Text(matchDescriptor.teamLabelTwo) + } + } label: { + Text("Forfait") + } + Spacer() + + MatchTypeSmallSelectionView(selectedFormat: $matchDescriptor.matchFormat, format: "Format") + .onChange(of: matchDescriptor.matchFormat) { newValue in + matchDescriptor.setDescriptors.removeAll() + matchDescriptor.addNewSet() + } + } + } + ForEach($matchDescriptor.setDescriptors) { $setDescriptor in + SetInputView(setDescriptor: $setDescriptor) + .onChange(of: setDescriptor.hasEnded) { hasEnded in + if hasEnded { + if matchDescriptor.hasEnded == false { + matchDescriptor.addNewSet() + } + } else { + let index = matchDescriptor.setDescriptors.firstIndex(where: { $0 == setDescriptor }) ?? 0 + matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index]) + } + } + } + + if matchDescriptor.hasEnded { + Section { + HStack { + Spacer() + VStack { + Text(matchDescriptor.winnerLabel) + } + .multilineTextAlignment(.center) + Spacer() + } + RowButtonView("Victoire") { + matchDescriptor.match?.setScore(fromMatchDescriptor: matchDescriptor) + save() + dismiss() + } + } footer: { + Text("Termine la rencontre sur ce score") + } + } + + if matchDescriptor.match?.hasEnded() == false { + Section { + RowButtonView("Mise à jour") { + matchDescriptor.match?.updateScore(fromMatchDescriptor: matchDescriptor) + save() + dismiss() + } + } footer: { + Text("Met à jour le score pour la diffusion, ne termine pas la rencontre") + } + } + } + } + + func save() { + if let match = matchDescriptor.match { + try? dataStore.matches.addOrUpdate(instance: match) + } + } +} diff --git a/PadelClub/Views/Score/PointSelectionView.swift b/PadelClub/Views/Score/PointSelectionView.swift new file mode 100644 index 0000000..40d0579 --- /dev/null +++ b/PadelClub/Views/Score/PointSelectionView.swift @@ -0,0 +1,50 @@ +// +// PointSelectionView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 27/02/2023. +// + +import SwiftUI + +struct PointSelectionView: View { + @Binding var valueSelected: Int? + var values: [Int] + var possibleValues: [Int] + var disableValues: [Int] = [] + var deleteAction: () -> () + let gridItems: [GridItem] = [GridItem(.adaptive(minimum: 65), spacing: 20)] + + + init(valueSelected: Binding, values: [Int], possibleValues: [Int], disableValues: [Int], deleteAction: @escaping () -> Void) { + _valueSelected = valueSelected + self.values = values + self.possibleValues = Set(values + possibleValues).sorted().reversed() + self.disableValues = disableValues + self.deleteAction = deleteAction + } + + var body: some View { + + LazyVGrid(columns: gridItems, alignment: .center, spacing: 20) { + ForEach(possibleValues, id: \.self) { value in + Button { + valueSelected = value + } label: { + PointView(value: "\(value).circle.fill") + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .disabled(disableValues.contains(value) || values.contains(value) == false ) + } + Button { + deleteAction() + } label: { + PointView(value: "delete.left.fill") + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + } + .padding() + } +} diff --git a/PadelClub/Views/Score/PointView.swift b/PadelClub/Views/Score/PointView.swift new file mode 100644 index 0000000..471c749 --- /dev/null +++ b/PadelClub/Views/Score/PointView.swift @@ -0,0 +1,26 @@ +// +// PointView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 27/02/2023. +// + +import SwiftUI + +struct PointView: View { + let value: String + + var body: some View { + Image(systemName: value) + .resizable() + .aspectRatio(contentMode: .fit) + .font(.largeTitle) + .frame(height: 40) + } +} + +struct PointView_Previews: PreviewProvider { + static var previews: some View { + PointView(value:"delete.left.fill") + } +} diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift new file mode 100644 index 0000000..724c1b1 --- /dev/null +++ b/PadelClub/Views/Score/SetInputView.swift @@ -0,0 +1,215 @@ +// +// SetInputView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import SwiftUI + +struct SetInputView: View { + @Binding var setDescriptor: SetDescriptor + @State private var showSetInputView: Bool = true + @State private var showTieBreakInputView: Bool = false + + var setFormat: SetFormat { setDescriptor.setFormat } + + private var showTieBreakView: Bool { + setFormat.shouldTiebreak(scoreTeamOne: setDescriptor.valueTeamOne ?? 0, scoreTeamTwo: setDescriptor.valueTeamTwo ?? 0) + } + + private var isMainViewTieBreakView: Bool { + setFormat == .superTieBreak || setFormat == .megaTieBreak + } + + private var currentValue: Binding { + Binding { + if setDescriptor.valueTeamOne != nil { + return setDescriptor.valueTeamTwo + } else { + return setDescriptor.valueTeamOne + } + } set: { newValue, _ in + if setDescriptor.valueTeamOne != nil { + setDescriptor.valueTeamTwo = newValue + } else { + setDescriptor.valueTeamOne = newValue + } + } + } + + private var currentTiebreakValue: Binding { + Binding { + if setDescriptor.tieBreakValueTeamOne != nil { + return setDescriptor.tieBreakValueTeamTwo + } else { + return setDescriptor.tieBreakValueTeamOne + } + } set: { newValue, _ in + + if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if tieBreakValueTeamOne < tieBreakValueTeamTwo && tieBreakValueTeamTwo > 6 { + setDescriptor.tieBreakValueTeamOne = newValue + } + else if tieBreakValueTeamOne > tieBreakValueTeamTwo && tieBreakValueTeamOne > 6 { + setDescriptor.tieBreakValueTeamTwo = newValue + } + } + else if setDescriptor.tieBreakValueTeamOne != nil { + setDescriptor.tieBreakValueTeamTwo = newValue + } else { + setDescriptor.tieBreakValueTeamOne = newValue + } + } + } + + private var disableValues: [Int] { + if let valueTeamOne = setDescriptor.valueTeamOne { + return setFormat.disableValuesForTeamTwo(with: valueTeamOne) + } + return [] + } + + private var disableTieBreakValues: [Int] { + if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne { + if tieBreakValueTeamOne == 7 { + return [7,6] + } + } + return [] + } + + func deleteLastValue() { + setDescriptor.valueTeamOne = nil + setDescriptor.valueTeamTwo = nil + } + + func deleteLastTiebreakValue() { + setDescriptor.tieBreakValueTeamOne = nil + setDescriptor.tieBreakValueTeamTwo = nil + } + + func possibleValues() -> [Int] { + if let valueTeamOne = setDescriptor.valueTeamOne { + if valueTeamOne == 7 && setFormat == .six { + return [6,5] + } + if valueTeamOne == 5 && setFormat == .four { + return [3,2] + } + } + return setFormat.possibleValues + } + + func tieBreakPossibleValues() -> [Int] { + if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if tieBreakValueTeamOne == 6 && tieBreakValueTeamTwo == 8 { + return [] + } + if tieBreakValueTeamOne < 7 && tieBreakValueTeamTwo == 7 { + return [9, 5, 4, 3, 2, 1, 0] + } + if tieBreakValueTeamOne == 7 && tieBreakValueTeamTwo < 7 { + return [9, 5, 4, 3, 2, 1, 0] + } + return Array(((max(tieBreakValueTeamOne, tieBreakValueTeamTwo)+2).. 10 && setFormat == .superTieBreak { + setDescriptor.valueTeamTwo = newValue - 2 + } else if newValue > 15 && setFormat == .megaTieBreak { + setDescriptor.valueTeamTwo = newValue - 2 + } + + } + }) + .onChange(of: setDescriptor.valueTeamTwo, perform: { newValue in + if setDescriptor.valueTeamOne != nil && setDescriptor.valueTeamTwo != nil { + showSetInputView = false + } + }) + .onChange(of: setDescriptor.tieBreakValueTeamOne, perform: { newValue in + if let newValue, setDescriptor.tieBreakValueTeamTwo == nil { + if newValue > 7 { + setDescriptor.tieBreakValueTeamTwo = newValue - 2 + } + if newValue == 6 { + setDescriptor.tieBreakValueTeamTwo = newValue + 2 + } + if newValue <= 5 { + setDescriptor.tieBreakValueTeamTwo = 7 + } + } + else if let newValue, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if newValue > 6 && tieBreakValueTeamTwo < newValue { + setDescriptor.tieBreakValueTeamTwo = newValue - 2 + } + if newValue > 6 && tieBreakValueTeamTwo > newValue { + setDescriptor.tieBreakValueTeamTwo = newValue + 2 + } + if newValue == 6 { + setDescriptor.tieBreakValueTeamTwo = newValue + 2 + } + if newValue <= 5 { + setDescriptor.tieBreakValueTeamTwo = 7 + showTieBreakInputView = false + } + } + }) + .onChange(of: setDescriptor.tieBreakValueTeamTwo, perform: { newValue in + if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, tieBreakValueTeamOne <= 5 { + showTieBreakInputView = false + } else { + if let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if let newValue { + if newValue > 6 && tieBreakValueTeamTwo > setDescriptor.tieBreakValueTeamOne ?? 0 { + setDescriptor.tieBreakValueTeamOne = newValue - 2 + } + if newValue > 4 && tieBreakValueTeamTwo < setDescriptor.tieBreakValueTeamOne ?? 0 { + setDescriptor.tieBreakValueTeamOne = newValue + 2 + } + } + } + + if let newValue, let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne { + if newValue < 6 && tieBreakValueTeamOne == 7 { + showTieBreakInputView = false + } + } + + } + }) + + .listRowSeparator(.hidden) + } +} diff --git a/PadelClub/Views/Score/SetLabelView.swift b/PadelClub/Views/Score/SetLabelView.swift new file mode 100644 index 0000000..7071bc3 --- /dev/null +++ b/PadelClub/Views/Score/SetLabelView.swift @@ -0,0 +1,57 @@ +// +// SetLabelView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import SwiftUI + +struct SetLabelView: View { + @Binding var initialValueLeft: Int? + @Binding var initialValueRight: Int? + @State private var valueLeft: Int = 0 + @State private var valueRight: Int = 0 + var shouldDisplaySteppers: Bool = false + var isTieBreak: Bool = false + + var body: some View { + HStack(spacing: 0) { + if shouldDisplaySteppers { + Stepper(value: $valueLeft, in: 0...Int.max) { + + } onEditingChanged: { didChange in + initialValueLeft = valueLeft + } + .fixedSize() + .scaleEffect(0.7) + } + Spacer() + Text("\(valueLeft) / \(valueRight)") + .font(isTieBreak ? .headline : .largeTitle).monospacedDigit() + .scaledToFit() + .minimumScaleFactor(0.5) + .lineLimit(1) + Spacer() + if shouldDisplaySteppers { + Stepper(value: $valueRight, in: 0...Int.max) { + } onEditingChanged: { didChange in + initialValueRight = valueRight + } + .fixedSize() + .scaleEffect(0.7) + } + } + .onChange(of: initialValueLeft) { newValue in + valueLeft = initialValueLeft ?? 0 + } + .onChange(of: initialValueRight) { newValue in + valueRight = initialValueRight ?? 0 + } + .onAppear { + valueLeft = initialValueLeft ?? 0 + valueRight = initialValueRight ?? 0 + } + } +} + diff --git a/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift b/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift new file mode 100644 index 0000000..cc30267 --- /dev/null +++ b/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift @@ -0,0 +1,23 @@ +// +// MatchTypeSmallSelectionView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import SwiftUI + +struct MatchTypeSmallSelectionView: View { + @Binding var selectedFormat: MatchFormat + let format: String + + var body: some View { + Picker(selection: $selectedFormat) { + ForEach(MatchFormat.allCases, id: \.rawValue) { matchFormat in + Text(format + " " + matchFormat.format) + .tag(matchFormat) + } + } label: { + } + } +} diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index d33d429..efc9aba 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -24,7 +24,7 @@ struct TeamPickerView: View { List { let teams = tournament.sortedTeams() Section { - _teamListView(teams.filter({ $0.available() }).sorted(by: \.weight).reversed()) + _teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed()) } header: { Text("Disponible") } diff --git a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift index 0a92146..4cd9f91 100644 --- a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift @@ -32,7 +32,7 @@ struct UpdateSourceRankDateView: View { } } - RowButtonView(title: "Valider") { + RowButtonView("Valider") { updatingRank = true Task { do { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 8eacdd6..fd409dc 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -208,12 +208,12 @@ struct InscriptionManagerView: View { } description: { Text("\(searchField) est introuvable dans les équipes inscrites.") } actions: { - RowButtonView(title: "Modifier la recherche") { + RowButtonView("Modifier la recherche") { searchField = "" presentSearch = true } - RowButtonView(title: "Créer une équipe") { + RowButtonView("Créer une équipe") { Task { await MainActor.run() { fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) @@ -222,7 +222,7 @@ struct InscriptionManagerView: View { } } - RowButtonView(title: "D'accord") { + RowButtonView("D'accord") { searchField = "" presentSearch = false } @@ -564,16 +564,16 @@ struct InscriptionManagerView: View { if editedTeam == nil { if createdPlayerIds.isEmpty { - RowButtonView(title: "Bloquer une place") { + RowButtonView("Bloquer une place") { _createTeam() } } else { - RowButtonView(title: "Ajouter l'équipe") { + RowButtonView("Ajouter l'équipe") { _createTeam() } } } else { - RowButtonView(title: "Modifier l'équipe") { + RowButtonView("Modifier l'équipe") { _updateTeam() } } @@ -601,11 +601,11 @@ struct InscriptionManagerView: View { } description: { Text("Aucun joueur classé n'a été trouvé dans ce message.") } actions: { - RowButtonView(title: "Créer un joueur non classé") { + RowButtonView("Créer un joueur non classé") { presentPlayerCreation = true } - RowButtonView(title: "Effacer cette recherche") { + RowButtonView("Effacer cette recherche") { self.pasteString = nil } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 7d219e0..aa200a6 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -49,7 +49,7 @@ struct TournamentView: View { } if endOfInscriptionDate < Date() { - RowButtonView(title: "Clôturer les inscriptions") { + RowButtonView("Clôturer les inscriptions") { tournament.lockRegistration() _save() } diff --git a/PadelClub/Views/ViewModifiers/TabItemModifier.swift b/PadelClub/Views/ViewModifiers/TabItemModifier.swift new file mode 100644 index 0000000..ede0203 --- /dev/null +++ b/PadelClub/Views/ViewModifiers/TabItemModifier.swift @@ -0,0 +1,26 @@ +// +// TabItemModifier.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import SwiftUI + +struct TabItemModifier: ViewModifier { + let tabDestination: TabDestination + + func body(content: Content) -> some View { + content + .tabItem { + Label(tabDestination.title, systemImage: tabDestination.image) + } + .tag(tabDestination as TabDestination?) + } +} + +extension View { + func tabItem(for tabDestination: TabDestination) -> some View { + modifier(TabItemModifier(tabDestination: tabDestination)) + } +}