From 992d7fb744a4f7d3ca149d1f28ff2da58bfe96d7 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 3 Apr 2024 16:26:37 +0200 Subject: [PATCH 1/7] 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)) + } +} From ff0b236afc9fc64315240d258d92ccaaac53e06f Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 4 Apr 2024 07:04:36 +0200 Subject: [PATCH 2/7] add court management --- PadelClub.xcodeproj/project.pbxproj | 4 ++ PadelClub/Data/Match.swift | 51 +++++++++++++++---- PadelClub/Data/Tournament.swift | 5 ++ PadelClub/PadelClubApp.swift | 4 +- PadelClub/ViewModel/NavigationViewModel.swift | 14 +++++ PadelClub/Views/Match/MatchDetailView.swift | 26 +++++----- PadelClub/Views/Match/MatchSummaryView.swift | 2 +- .../Navigation/Agenda/ActivityView.swift | 29 ++++++----- .../Shared/TournamentCellView.swift | 17 ++++--- 9 files changed, 107 insertions(+), 45 deletions(-) create mode 100644 PadelClub/ViewModel/NavigationViewModel.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 47c1c1d..f1165bc 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ 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 */; }; + FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.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 */; }; @@ -416,6 +417,7 @@ 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 = ""; }; + FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.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 = ""; }; @@ -812,6 +814,7 @@ FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */, FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */, FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, + FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -1275,6 +1278,7 @@ FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, + FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 43626b8..ef1623b 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -171,22 +171,49 @@ class Match: ModelObject, Storable { 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) -// } -// } + + switch fieldSetup { + case .random: + let courtName = availableCourts().randomElement() + court = courtName + case .field(let courtName): + court = courtName + } + } else { startDate = fromStartDate endDate = toEndDate } } + func courtCount() -> Int { + currentTournament()?.courtCount ?? 1 + } + + func courtIsAvailable(_ courtIndex: Int) -> Bool { + let courtUsed = currentTournament()?.courtUsed() ?? [] + return courtUsed.contains(String(courtIndex)) == false +// return Set(availableCourts().map { String($0) }).subtracting(Set(courtUsed)) + } + + func courtIsPreferred(_ courtIndex: Int) -> Bool { + false + } + + func availableCourts() -> [String] { + let courtUsed = currentTournament()?.courtUsed() ?? [] + let availableCourts = Array(1...courtCount()) + return Array(Set(availableCourts.map { String($0) }).subtracting(Set(courtUsed))) + } + + func removeCourt() { + court = nil + } + + func setCourt(_ courtIndex: Int) { + court = String(courtIndex) + } + func canBeStarted() -> Bool { let teams = teams() guard teams.count == 2 else { return false } @@ -241,6 +268,10 @@ class Match: ModelObject, Storable { groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject() } + func tournamentId() -> String? { + groupStageObject?.tournament ?? roundObject?.tournament + } + func scores() -> [TeamScore] { Store.main.filter(isIncluded: { $0.match == id }) } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 42b8569..df68985 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -86,6 +86,11 @@ class Tournament : ModelObject, Storable { case build } + func courtUsed() -> [String] { + let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id }) + return Set(runningMatches.compactMap { $0.court }).sorted() + } + func hasStarted() -> Bool { startDate <= Date() } diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 6d3e7a9..bc1f33d 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -12,10 +12,12 @@ import TipKit @main struct PadelClubApp: App { let persistenceController = PersistenceController.shared - + @State private var navigationViewModel = NavigationViewModel() + var body: some Scene { WindowGroup { MainView() + .environment(navigationViewModel) .accentColor(.launchScreenBackground) .onAppear { self._onAppear() diff --git a/PadelClub/ViewModel/NavigationViewModel.swift b/PadelClub/ViewModel/NavigationViewModel.swift new file mode 100644 index 0000000..b39d960 --- /dev/null +++ b/PadelClub/ViewModel/NavigationViewModel.swift @@ -0,0 +1,14 @@ +// +// NavigationViewModel.swift +// PadelClub +// +// Created by Razmig Sarkissian on 03/04/2024. +// + +import SwiftUI + +@Observable +class NavigationViewModel { + var agendaDestination: AgendaDestination? = .activity + var tournament: Tournament? +} diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index ccf3b9b..cfe0778 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -103,15 +103,15 @@ struct MatchDetailView: View { if match.hasEnded() == false { Menu { Button("Non défini") { - match.court = nil + match.removeCourt() save() } -// ForEach(1...match.numberOfField, id: \.self) { courtIndex in -// Button("Terrain #\(courtIndex.formatted())") { -// match.fieldIndex = Int64(courtIndex) -// save() -// } -// } + ForEach(1...match.courtCount(), id: \.self) { courtIndex in + Button("Terrain #\(courtIndex.formatted())") { + match.setCourt(courtIndex) + save() + } + } } label: { VStack(alignment: .leading) { Text("terrain").font(.footnote).foregroundStyle(.secondary) @@ -377,9 +377,10 @@ struct MatchDetailView: View { if match.isReady() { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) - Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) Text("Tout de suite").tag(MatchDateSetup.now) } + Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-match.matchFormat.estimatedDuration)) + Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) Text("À").tag(MatchDateSetup.customDate) } label: { Text("Horaire") @@ -414,11 +415,10 @@ struct MatchDetailView: View { Picker(selection: $fieldSetup) { Text("Au hasard").tag(MatchFieldSetup.random) //Text("Premier disponible").tag(MatchFieldSetup.firstAvailable) -// ForEach(1...match.numberOfField, id: \.self) { courtIndex in -// let fieldIndex = Int64(courtIndex) -// let fieldIsAvailable : Bool = match.currentTournament?.fieldIsAvailable(fieldIndex) ?? true -// Label("Terrain #\(courtIndex)", systemImage: match.isFieldPreferred(fieldIndex) ? "heart" : "").tag(MatchFieldSetup.field(courtIndex)) -// } + ForEach(1...match.courtCount(), id: \.self) { courtIndex in + Text("Terrain #\(courtIndex)") + .tag(MatchFieldSetup.field(String(courtIndex))) + } } label: { Text("Choix du terrain") } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 7a1f021..e312219 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -74,7 +74,7 @@ struct MatchSummaryView: View { Spacer() if let court = match.court, match.hasEnded() == false { Spacer() - Text("Terrain \(court)") + Text("Terrain #\(court)") } } } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index a6271f0..9655ce0 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -9,9 +9,9 @@ import SwiftUI struct ActivityView: View { @EnvironmentObject var dataStore: DataStore - + @Environment(NavigationViewModel.self) private var navigation + @State private var searchText: String = "" - @State private var agendaDestination: AgendaDestination? = .activity @State private var filterEnabled: Bool = false @State private var presentToolbar: Bool = false @@ -41,7 +41,7 @@ struct ActivityView: View { } var tournaments: [FederalTournamentHolder] { - switch agendaDestination! { + switch navigation.agendaDestination! { case .activity: runningTournaments case .history: @@ -53,10 +53,11 @@ struct ActivityView: View { var body: some View { NavigationStack { + @Bindable var navigation = navigation VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) + GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) List { - switch agendaDestination! { + switch navigation.agendaDestination! { case .activity: EventListView(tournaments: runningTournaments, viewStyle: viewStyle) case .history: @@ -66,7 +67,7 @@ struct ActivityView: View { } } .overlay { - if let error, agendaDestination == .tenup { + if let error, navigation.agendaDestination == .tenup { ContentUnavailableView { Label("Erreur", systemImage: "exclamationmark") } description: { @@ -105,20 +106,20 @@ struct ActivityView: View { EventCreationView(tournaments: [tournament]) } .refreshable { - if agendaDestination == .tenup { + if navigation.agendaDestination == .tenup { federalTournaments.removeAll() _gatherFederalTournaments() } } .task { - if agendaDestination == .tenup + if navigation.agendaDestination == .tenup && dataStore.clubs.isEmpty == false && federalTournaments.isEmpty { _gatherFederalTournaments() } } - .onChange(of: agendaDestination) { - if agendaDestination == .tenup + .onChange(of: navigation.agendaDestination) { + if navigation.agendaDestination == .tenup && dataStore.clubs.isEmpty == false && federalTournaments.isEmpty { _gatherFederalTournaments() @@ -186,6 +187,10 @@ struct ActivityView: View { TournamentView() .environment(tournament) } + .navigationDestination(item: $navigation.tournament) { tournament in + TournamentView() + .environment(tournament) + } } } } @@ -206,7 +211,7 @@ struct ActivityView: View { @ViewBuilder private func _dataEmptyView() -> some View { - switch agendaDestination! { + switch navigation.agendaDestination! { case .activity: _runningEmptyView() case .history: @@ -234,7 +239,7 @@ struct ActivityView: View { newTournament = Tournament.newEmptyInstance() } RowButtonView("Importer via Tenup") { - agendaDestination = .tenup + navigation.agendaDestination = .tenup } } } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index cf6c708..b796d6b 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -9,6 +9,7 @@ import SwiftUI struct TournamentCellView: View { @EnvironmentObject var dataStore: DataStore + @Environment(NavigationViewModel.self) private var navigation let tournament: FederalTournamentHolder let color: Color = .black @@ -21,11 +22,11 @@ struct TournamentCellView: View { var body: some View { ForEach(tournament.tournaments, id: \.id) { build in - _buildView(build, alreadyExist: event?.existingBuild(build) != nil) + _buildView(build, existingTournament: event?.existingBuild(build)) } } - private func _buildView(_ build: any TournamentBuildHolder, alreadyExist: Bool) -> some View { + private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View { HStack { DateBoxView(date: tournament.startDate, displayStyle: displayStyle) Rectangle() @@ -53,7 +54,10 @@ struct TournamentCellView: View { Text(tournament.sortedTeams().count.formatted()) } else if let federalTournament = tournament as? FederalTournament { Button { - if alreadyExist == false { + if let existingTournament { + navigation.agendaDestination = .activity + navigation.tournament = existingTournament + } else { let event = federalTournament.getEvent() let newTournament = Tournament.newEmptyInstance() newTournament.event = event.id @@ -63,17 +67,14 @@ struct TournamentCellView: View { newTournament.dayDuration = federalTournament.dayDuration newTournament.startDate = federalTournament.startDate try? dataStore.tournaments.addOrUpdate(instance: newTournament) - } else { - //event?.existingBuild(build) } } label: { - Image(systemName: alreadyExist ? "checkmark.circle.fill" : "square.and.arrow.down") + Image(systemName: existingTournament != nil ? "checkmark.circle.fill" : "square.and.arrow.down") .resizable() .scaledToFit() .frame(height: 28) - .tint(alreadyExist ? Color.green : nil) + .tint(existingTournament != nil ? Color.green : nil) } - .buttonStyle(.borderless) } } .font(displayStyle == .wide ? .title : .title3) From db9d4637d08ccf40ba953de37213a12a3f2da4b8 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 4 Apr 2024 19:43:07 +0200 Subject: [PATCH 3/7] add loser bracket management --- PadelClub.xcodeproj/project.pbxproj | 8 ++ PadelClub/Data/Match.swift | 14 ++- PadelClub/Data/Round.swift | 110 ++++++++++++++++-- PadelClub/Data/Tournament.swift | 13 +-- PadelClub/Views/Round/LoserBracketView.swift | 53 +++++++++ PadelClub/Views/Round/LoserRoundsView.swift | 65 +++++++++++ PadelClub/Views/Round/RoundSettingsView.swift | 19 ++- PadelClub/Views/Round/RoundView.swift | 12 ++ 8 files changed, 267 insertions(+), 27 deletions(-) create mode 100644 PadelClub/Views/Round/LoserBracketView.swift create mode 100644 PadelClub/Views/Round/LoserRoundsView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f1165bc..8e3a6a6 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -178,6 +178,8 @@ FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; + FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */; }; + FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; FFC83D512BB8087E00750834 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D502BB8087E00750834 /* RoundView.swift */; }; FFCFBFFE2BBBE86600B82851 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = FFCFBFFD2BBBE86600B82851 /* Algorithms */; }; @@ -422,6 +424,8 @@ FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = ""; }; + FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketView.swift; sourceTree = ""; }; + FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = ""; }; FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = ""; }; FFC83D502BB8087E00750834 /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; FFCFC0012BBC39A600B82851 /* EditScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScoreView.swift; sourceTree = ""; }; @@ -925,6 +929,8 @@ FFC83D4E2BB807D100750834 /* RoundsView.swift */, FFC83D502BB8087E00750834 /* RoundView.swift */, FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */, + FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */, + FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, ); path = Round; sourceTree = ""; @@ -1275,6 +1281,7 @@ FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, + FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, @@ -1293,6 +1300,7 @@ FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, + FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */, diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ef1623b..d1ced08 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -318,18 +318,22 @@ class Match: ModelObject, Storable { } func roundProjectedTeam(_ team: TeamData) -> TeamRegistration? { - guard round != nil else { return nil } - if let seed = seed(team) { + guard let roundObject else { return nil } + if roundObject.isLoserBracket() == false, let seed = seed(team) { return seed } - + let indexInRound = indexInRound() switch team { case .one: - if let teamId = topPreviousRoundMatch()?.winningTeamId { + if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let loser = parentRound.matches.first(where: { parentRound.indexOfMatch($0) == indexInRound * 2 })?.losingTeamId { + return Store.main.findById(loser) + } else if let teamId = topPreviousRoundMatch()?.winningTeamId { return Store.main.findById(teamId) } case .two: - if let teamId = bottomPreviousRoundMatch()?.winningTeamId { + if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let loser = parentRound.matches.first(where: { parentRound.indexOfMatch($0) == indexInRound * 2 + 1 })?.losingTeamId { + return Store.main.findById(loser) + } else if let teamId = bottomPreviousRoundMatch()?.winningTeamId { return Store.main.findById(teamId) } } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 5943fb5..05b2db6 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -52,15 +52,47 @@ class Round: ModelObject, Storable { } func previousRound() -> Round? { - Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == index + 1 }).first + Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index + 1 }).first } func nextRound() -> Round? { - Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == index - 1 }).first + Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index - 1 }).first + } + + func loserRounds(forRoundIndex roundIndex: Int) -> [Round] { + return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount) + } + + func getActiveLoserRound() -> Round? { + let rounds = loserRounds() + return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + } + + var cumulativeMatchCount: Int { + var totalMatches = matches.count + if let parent = parentRound { + totalMatches += parent.cumulativeMatchCount + } + return totalMatches + } + + func initialRound() -> Round? { + if let parentRound { + return parentRound.initialRound() + } else { + return self + } } func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { - RoundRule.roundName(fromRoundIndex: index) + if let parentRound, let initialRound = parentRound.initialRound() { + let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.matches.count + print("initialRound", initialRound.roundTitle()) + if let initialRoundNextRound = initialRound.nextRound()?.matches { + return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + matches.count * 2).localizedLabel(displayStyle) + } + } + return RoundRule.roundName(fromRoundIndex: index) } func roundStatus() -> String { @@ -71,16 +103,64 @@ class Round: ModelObject, Storable { } } - var loserRound: Round? { - guard let loser else { return nil } - return Store.main.findById(loser) + func indexOfMatch(_ match: Match) -> Int? { + matches.firstIndex(where: { $0.id == match.id }) + } + + func loserRounds() -> [Round] { + return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed() + } + + func loserRoundsAndChildren() -> [Round] { + let loserRounds = loserRounds() + return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() }) + } + + func isLoserBracket() -> Bool { + loser != nil + } + + func buildLoserBracket() { + guard loserRounds().isEmpty else { return } + let currentRoundMatchCount = matches.count + guard currentRoundMatchCount > 1 else { return } + let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) + let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat + + let rounds = (0.. String { - roundTitle() + if let parentRound { + return "Tour #\(parentRound.loserRounds().count - index)" + } else { + return roundTitle() + } } func badgeValue() -> Int? { - nil + if let parentRound { + return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.matches }.filter({ $0.isRunning() }).count + } else { + return matches.filter({ $0.isRunning() }).count + } } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index df68985..12fe20a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -183,13 +183,6 @@ class Tournament : ModelObject, Storable { case 4...7: return SeedInterval(first: 5, last: 8) case 8...15: -// if 16 - 9 > availableSeeds().count { -// switch alreadySetupSeeds { -// case 8...15: -// return SeedInterval(first: 5, last: 8) -// case 8...15: -// return SeedInterval(first: 5, last: 8) -// } return SeedInterval(first: 9, last: 16) case 16...23: return SeedInterval(first: 17, last: 24) @@ -230,7 +223,7 @@ class Tournament : ModelObject, Storable { for (index, seed) in availableSeeds.enumerated() { seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: false) } - } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) { + } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { let spots = availableSeedOpponentSpot.shuffled() for (index, seed) in availableSeeds.enumerated() { @@ -261,7 +254,7 @@ class Tournament : ModelObject, Storable { } func rounds() -> [Round] { - Store.main.filter { $0.tournament == self.id }.sorted(by: \.index).reversed() + Store.main.filter { $0.tournament == self.id && $0.loser == nil }.sorted(by: \.index).reversed() } func sortedTeams() -> [TeamRegistration] { @@ -619,6 +612,8 @@ class Tournament : ModelObject, Storable { }) try? DataStore.shared.matches.addOrUpdate(contentOfs: matches) + + buildLoserBracket() } func deleteStructure() { diff --git a/PadelClub/Views/Round/LoserBracketView.swift b/PadelClub/Views/Round/LoserBracketView.swift new file mode 100644 index 0000000..ae25942 --- /dev/null +++ b/PadelClub/Views/Round/LoserBracketView.swift @@ -0,0 +1,53 @@ +// +// LoserBracketView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 04/04/2024. +// + +import SwiftUI + +struct LoserBracketView: View { + @EnvironmentObject var dataStore: DataStore + let loserRounds: [Round] + + @ViewBuilder + var body: some View { + if let first = loserRounds.first { + List { + ForEach(loserRounds) { loserRound in + _loserRoundView(loserRound) + let childLoserRounds = loserRound.loserRounds() + if childLoserRounds.isEmpty == false { + let uniqueChildRound = childLoserRounds.first + if childLoserRounds.count == 1, let uniqueChildRound { + _loserRoundView(uniqueChildRound) + } else if let uniqueChildRound { + NavigationLink { + LoserBracketView(loserRounds: childLoserRounds) + } label: { + Text(uniqueChildRound.roundTitle()) + } + } + } + } + } + .navigationTitle(first.roundTitle()) + } + } + + private func _loserRoundView(_ loserRound: Round) -> some View { + Section { + ForEach(loserRound.matches) { match in + MatchRowView(match: match, matchViewStyle: .standardStyle) + } + } header: { + Text(loserRound.roundTitle()) + } + } +} + +#Preview { + LoserBracketView(loserRounds: [Round.mock()]) + .environmentObject(DataStore.shared) +} diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift new file mode 100644 index 0000000..46e5609 --- /dev/null +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -0,0 +1,65 @@ +// +// LoserRoundsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 04/04/2024. +// + +import SwiftUI + +extension Int: Selectable, Identifiable { + public var id: Int { self } + func selectionLabel() -> String { + "Tour #\(self + 1)" + } + + func badgeValue() -> Int? { + nil + } +} + +struct LoserRoundsView: View { + var upperBracketRound: Round + @State private var selectedRound: Round? + let loserRounds: [Round] + + init(upperBracketRound: Round) { + self.upperBracketRound = upperBracketRound + self.loserRounds = upperBracketRound.loserRounds() + _selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound()) + } + + var body: some View { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: loserRounds, nilDestinationIsValid: true) + switch selectedRound { + case .none: + List { + } + case .some(let selectedRound): + LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index)) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +struct LoserRoundView: View { + let loserRounds: [Round] + + var body: some View { + List { + ForEach(loserRounds) { loserRound in + Section { + ForEach(loserRound.matches) { match in + MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + } + } header: { + Text(loserRound.roundTitle(.wide)) + } + } + } + .headerProminence(.increased) + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index b3ad616..d492403 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -23,14 +23,28 @@ struct RoundSettingsView: View { Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed) Section { - RowButtonView("Retirer toutes les têtes de séries") { + RowButtonView("Effacer classement", role: .destructive) { + tournament.rounds().forEach { round in + try? dataStore.rounds.delete(contentOfs: round.loserRounds()) + } + } + } + Section { + RowButtonView("Match de classement") { + tournament.rounds().forEach { round in + round.buildLoserBracket() + } + } + } + Section { + RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) } } Section { if let lastRound = tournament.rounds().first { // first is final, last round - RowButtonView("Supprimer " + lastRound.roundTitle()) { + RowButtonView("Supprimer " + lastRound.roundTitle(), role: .destructive) { try? dataStore.rounds.delete(instance: lastRound) } } @@ -47,6 +61,7 @@ struct RoundSettingsView: View { } try? dataStore.rounds.addOrUpdate(instance: round) try? dataStore.matches.addOrUpdate(contentOfs: matches) + round.buildLoserBracket() } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index d018085..bd01ac7 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -12,6 +12,18 @@ struct RoundView: View { var body: some View { List { + let loserRounds = round.loserRounds() + if loserRounds.isEmpty == false, let first = loserRounds.first { + Section { + NavigationLink { + LoserRoundsView(upperBracketRound: round) + .navigationTitle(first.roundTitle()) + } label: { + Text(first.roundTitle()) + } + } + } + ForEach(round.matches) { match in Section { MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) From 29cc0df4a283dddac47d036451c046a5b58b13cb Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 5 Apr 2024 18:19:37 +0200 Subject: [PATCH 4/7] in progress --- PadelClub/Data/Match.swift | 67 ++++++-- PadelClub/Data/Round.swift | 147 ++++++++++++++++-- PadelClub/Data/Tournament.swift | 8 +- PadelClub/ViewModel/MatchDescriptor.swift | 8 +- PadelClub/Views/Match/MatchSummaryView.swift | 4 +- PadelClub/Views/Match/PlayerBlockView.swift | 16 +- PadelClub/Views/Round/LoserBracketView.swift | 2 +- PadelClub/Views/Round/LoserRoundsView.swift | 21 ++- PadelClub/Views/Round/RoundSettingsView.swift | 2 +- PadelClub/Views/Round/RoundView.swift | 2 +- 10 files changed, 220 insertions(+), 57 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index d1ced08..79bdd00 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -47,7 +47,7 @@ class Match: ModelObject, Storable { func indexInRound() -> Int { if groupStage != nil { return index - } else if let index = roundObject?.matches.firstIndex(where: { $0.id == id }) { + } else if let index = roundObject?.playedMatches().firstIndex(where: { $0.id == id }) { return index } return RoundRule.matchIndexWithinRound(fromMatchIndex: index) @@ -317,33 +317,66 @@ class Match: ModelObject, Storable { }) } + func isBye() -> Bool { + guard let roundObject else { return false } + return (roundObject.upperBracketMatches(ofMatch: self) + roundObject.previousRoundMatches(ofMatch: self)).anySatisfy({ $0.disabled }) + } + + func upperBracketTopMatch() -> Match? { + guard let roundObject else { return nil } + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) + if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { + return upperBracketTopMatch + } + return nil + } + + func upperBracketBottomMatch() -> Match? { + guard let roundObject else { return nil } + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) + if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { + return upperBracketBottomMatch + } + return nil + } + func roundProjectedTeam(_ team: TeamData) -> TeamRegistration? { guard let roundObject else { return nil } + return roundObject.roundProjectedTeam(team, inMatch: self) + if roundObject.isLoserBracket() == false, let seed = seed(team) { return seed } - let indexInRound = indexInRound() + switch team { case .one: - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let loser = parentRound.matches.first(where: { parentRound.indexOfMatch($0) == indexInRound * 2 })?.losingTeamId { + if let loser = upperBracketTopMatch()?.losingTeamId { return Store.main.findById(loser) - } else if let teamId = topPreviousRoundMatch()?.winningTeamId { - return Store.main.findById(teamId) + } else if let match = topPreviousRoundMatch() { + if let teamId = match.winningTeamId { + return Store.main.findById(teamId) + } else if match.isBye() { + return match.teams().first + } } case .two: - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let loser = parentRound.matches.first(where: { parentRound.indexOfMatch($0) == indexInRound * 2 + 1 })?.losingTeamId { + if let loser = upperBracketBottomMatch()?.losingTeamId { return Store.main.findById(loser) - } else if let teamId = bottomPreviousRoundMatch()?.winningTeamId { - return Store.main.findById(teamId) + } else if let match = bottomPreviousRoundMatch() { + if let teamId = match.winningTeamId { + return Store.main.findById(teamId) + } else if match.isBye() { + return match.teams().first + } } } return nil } - func teamWon(_ team: TeamData) -> Bool { + func teamWon(_ team: TeamRegistration?) -> Bool { guard let winningTeamId else { return false } - return winningTeamId == self.team(team)?.id + return winningTeamId == team?.id } func team(_ team: TeamData) -> TeamRegistration? { @@ -364,18 +397,22 @@ class Match: ModelObject, Storable { } } - func teamNames(_ team: TeamData) -> [String]? { - self.team(team)?.players().map { $0.playerLabel() } + func teamNames(_ team: TeamRegistration?) -> [String]? { + team?.players().map { $0.playerLabel() } } - func teamWalkOut(_ team: TeamData) -> Bool { - teamScore(team)?.isWalkOut() == true + func teamWalkOut(_ team: TeamRegistration?) -> Bool { + teamScore(ofTeam: team)?.isWalkOut() == true } func teamScore(_ team: TeamData) -> TeamScore? { - scores().first(where: { $0.teamRegistration == self.team(team)?.id }) + teamScore(ofTeam: self.team(team)) } + func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? { + scores().first(where: { $0.teamRegistration == team?.id }) + } + func isRunning() -> Bool { // at least a match has started hasStarted() && hasEnded() == false } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 05b2db6..0092d4f 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -36,18 +36,113 @@ class Round: ModelObject, Storable { func hasStarted() -> Bool { - matches.anySatisfy({ $0.hasStarted() }) + playedMatches().anySatisfy({ $0.hasStarted() }) } func hasEnded() -> Bool { - matches.allSatisfy({ $0.hasEnded() }) + playedMatches().allSatisfy({ $0.hasEnded() }) } func tournamentObject() -> Tournament? { Store.main.findById(tournament) } - var matches: [Match] { + private func _matches() -> [Match] { + Store.main.filter { $0.round == self.id } + } + + func team(_ team: TeamData, inMatch match: Match) -> TeamRegistration? { + switch team { + case .one: + return roundProjectedTeam(.one, inMatch: match) + case .two: + return roundProjectedTeam(.two, inMatch: match) + } + } + + func seed(_ team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? { + return Store.main.filter(isIncluded: { + $0.tournament == tournament && $0.bracketPosition != nil + }).first(where: { + ($0.bracketPosition! / 2) == matchIndex + && ($0.bracketPosition! % 2) == team.rawValue + }) + } + + func roundProjectedTeam(_ team: TeamData, inMatch match: Match) -> TeamRegistration? { + if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) { + return seed + } + + switch team { + case .one: + if let loser = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId { + return Store.main.findById(loser) + } else if let previousMatch = topPreviousRoundMatch(ofMatch: match) { + if let teamId = previousMatch.winningTeamId { + return Store.main.findById(teamId) + } else if previousMatch.isBye() { + return previousMatch.teams().first + } + } + case .two: + if let loser = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId { + return Store.main.findById(loser) + } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) { + if let teamId = previousMatch.winningTeamId { + return Store.main.findById(teamId) + } else if previousMatch.isBye() { + return previousMatch.teams().first + } + } + } + + return nil + } + +// func isMatchBye(_ match: Match) -> Bool { +// return (upperBracketMatches(ofMatch: match) + previousRoundMatches(ofMatch: match)).anySatisfy({ $0.disabled }) +// } + + func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? { + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) + if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { + return upperBracketTopMatch + } + return nil + } + + func upperBracketBottomMatch(ofMatchIndex matchIndex: Int) -> Match? { + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) + if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { + return upperBracketBottomMatch + } + return nil + } + + func topPreviousRoundMatch(ofMatch match: Match) -> Match? { + guard let previousRound = previousRound() else { return nil } + return Store.main.filter { + $0.index == match.topPreviousRoundMatchIndex() && $0.round == previousRound.id + }.sorted(by: \.index).first + } + + func bottomPreviousRoundMatch(ofMatch match: Match) -> Match? { + guard let previousRound = previousRound() else { return nil } + return Store.main.filter { + $0.index == match.bottomPreviousRoundMatchIndex() && $0.round == previousRound.id + }.sorted(by: \.index).first + } + + + func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? { + _matches().first(where: { + let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index) + return index == matchIndexInRound + }) + } + + func playedMatches() -> [Match] { Store.main.filter { $0.round == self.id && $0.disabled == false } } @@ -63,13 +158,17 @@ class Round: ModelObject, Storable { return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount) } + func isDisabled() -> Bool { + playedMatches().allSatisfy({ $0.disabled || $0.isBye() }) + } + func getActiveLoserRound() -> Round? { let rounds = loserRounds() - return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false }) } var cumulativeMatchCount: Int { - var totalMatches = matches.count + var totalMatches = playedMatches().count if let parent = parentRound { totalMatches += parent.cumulativeMatchCount } @@ -86,10 +185,10 @@ class Round: ModelObject, Storable { func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { if let parentRound, let initialRound = parentRound.initialRound() { - let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.matches.count + let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count print("initialRound", initialRound.roundTitle()) - if let initialRoundNextRound = initialRound.nextRound()?.matches { - return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + matches.count * 2).localizedLabel(displayStyle) + if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() { + return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + playedMatches().count * 2).localizedLabel(displayStyle) } } return RoundRule.roundName(fromRoundIndex: index) @@ -102,11 +201,29 @@ class Round: ModelObject, Storable { return "à démarrer" } } +// +// func indexOfMatch(_ match: Match) -> Int? { +// playedMatches().firstIndex(where: { $0.id == match.id }) +// } - func indexOfMatch(_ match: Match) -> Int? { - matches.firstIndex(where: { $0.id == match.id }) + func previousRoundMatches(ofMatch match: Match) -> [Match] { + return Store.main.filter { + $0.round == previousRound()?.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) + } } - + + func upperBracketMatches(ofMatch match: Match) -> [Match] { + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index) + if isLoserBracket(), previousRound() == nil, let parentRound { + let upperBracketMatches = parentRound._matches().filter({ + let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index) + return index == indexInRound * 2 || index == indexInRound * 2 + 1 + }) + return upperBracketMatches + } + return [] + } + func loserRounds() -> [Round] { return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed() } @@ -122,7 +239,7 @@ class Round: ModelObject, Storable { func buildLoserBracket() { guard loserRounds().isEmpty else { return } - let currentRoundMatchCount = matches.count + let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) guard currentRoundMatchCount > 1 else { return } let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat @@ -159,7 +276,7 @@ class Round: ModelObject, Storable { } override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.matches) + try Store.main.deleteDependencies(items: _matches()) try Store.main.deleteDependencies(items: loserRoundsAndChildren()) } @@ -183,9 +300,9 @@ extension Round: Selectable { func badgeValue() -> Int? { if let parentRound { - return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.matches }.filter({ $0.isRunning() }).count + return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count } else { - return matches.filter({ $0.isRunning() }).count + return playedMatches().filter({ $0.isRunning() }).count } } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 12fe20a..9211fdc 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -156,11 +156,11 @@ class Tournament : ModelObject, Storable { } func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { - getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 0 } ?? [] + getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 0 } ?? [] } func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { - getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 1 } ?? [] + getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? [] } func availableSeedGroups() -> [SeedInterval] { @@ -613,7 +613,9 @@ class Tournament : ModelObject, Storable { try? DataStore.shared.matches.addOrUpdate(contentOfs: matches) - buildLoserBracket() + self.rounds().forEach { round in + round.buildLoserBracket() + } } func deleteStructure() { diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index b31024c..ba4c135 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -27,10 +27,12 @@ class MatchDescriptor: ObservableObject { self.matchFormat = format self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)] } - self.teamLabelOne = match?.team(.one)?.teamLabel() ?? "" - self.teamLabelTwo = match?.team(.two)?.teamLabel() ?? "" + let teamOne = match?.team(.one) + let teamTwo = match?.team(.two) + self.teamLabelOne = teamOne?.teamLabel() ?? "" + self.teamLabelTwo = teamTwo?.teamLabel() ?? "" - if let match, let scoresTeamOne = match.teamScore(.one)?.score, let scoresTeamTwo = match.teamScore(.two)?.score { + if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.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) diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index e312219..5bd927c 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -84,14 +84,14 @@ struct MatchSummaryView: View { if matchViewStyle != .feedStyle { HStack(spacing: 0) { VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 0) { - PlayerBlockView(match: match, team: .one, color: color, width: width) + PlayerBlockView(match: match, whichTeam: .one, color: color, width: width) .padding(matchViewStyle == .plainStyle ? 0 : 8) if width == 1 { Divider() } else { Divider().frame(height: width).overlay(color) } - PlayerBlockView(match: match, team: .two, color: color, width: width) + PlayerBlockView(match: match, whichTeam: .two, color: color, width: width) .padding(matchViewStyle == .plainStyle ? 0 : 8) } } diff --git a/PadelClub/Views/Match/PlayerBlockView.swift b/PadelClub/Views/Match/PlayerBlockView.swift index c8802bf..405b30c 100644 --- a/PadelClub/Views/Match/PlayerBlockView.swift +++ b/PadelClub/Views/Match/PlayerBlockView.swift @@ -9,11 +9,19 @@ import SwiftUI struct PlayerBlockView: View { var match: Match - - let team: TeamData + let whichTeam: TeamData + let team: TeamRegistration? let color: Color let width: CGFloat + init(match: Match, whichTeam: TeamData, color: Color, width: CGFloat) { + self.match = match + self.whichTeam = whichTeam + self.team = match.team(whichTeam) + self.color = color + self.width = width + } + var names: [String]? { match.teamNames(team) } @@ -31,11 +39,11 @@ struct PlayerBlockView: View { } var scores: [String] { - match.teamScore(team)?.score?.components(separatedBy: ",") ?? [] + match.teamScore(ofTeam: team)?.score?.components(separatedBy: ",") ?? [] } private func _defaultLabel() -> String { - team.localizedLabel() + whichTeam.localizedLabel() } var body: some View { diff --git a/PadelClub/Views/Round/LoserBracketView.swift b/PadelClub/Views/Round/LoserBracketView.swift index ae25942..470c033 100644 --- a/PadelClub/Views/Round/LoserBracketView.swift +++ b/PadelClub/Views/Round/LoserBracketView.swift @@ -38,7 +38,7 @@ struct LoserBracketView: View { private func _loserRoundView(_ loserRound: Round) -> some View { Section { - ForEach(loserRound.matches) { match in + ForEach(loserRound.playedMatches()) { match in MatchRowView(match: match, matchViewStyle: .standardStyle) } } header: { diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 46e5609..0e9b0ca 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -7,17 +7,6 @@ import SwiftUI -extension Int: Selectable, Identifiable { - public var id: Int { self } - func selectionLabel() -> String { - "Tour #\(self + 1)" - } - - func badgeValue() -> Int? { - nil - } -} - struct LoserRoundsView: View { var upperBracketRound: Round @State private var selectedRound: Round? @@ -52,8 +41,16 @@ struct LoserRoundView: View { List { ForEach(loserRounds) { loserRound in Section { - ForEach(loserRound.matches) { match in + ForEach(loserRound.playedMatches()) { match in MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + .overlay { + if match.isBye() { + Image(systemName: "pencil.slash") + .resizable() + .scaledToFit() + .opacity(0.3) + } + } } } header: { Text(loserRound.roundTitle(.wide)) diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index d492403..ed45818 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -88,7 +88,7 @@ struct RoundSettingsView: View { // seeds.prefix(1).first?.bracketPosition = lastIndex * 2 + 1 //TS 1 branche du bas du dernier match // seeds.prefix(2).dropFirst().first?.bracketPosition = startIndex * 2 //TS 2 branche du haut du premier match - if let matches = tournament.getRound(atRoundIndex: roundIndex)?.matches { + if let matches = tournament.getRound(atRoundIndex: roundIndex)?.playedMatches() { if let lastMatch = matches.last { seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, upperBranch: 1, opposingSeeding: false) } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index bd01ac7..8ab1b88 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -24,7 +24,7 @@ struct RoundView: View { } } - ForEach(round.matches) { match in + ForEach(round.playedMatches()) { match in Section { MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) } header: { From 82384e30495031e699f26a66f80b5184e7b700da Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 5 Apr 2024 22:13:09 +0200 Subject: [PATCH 5/7] in progress loser bracket --- PadelClub/Data/Match.swift | 26 +++++- PadelClub/Data/Round.swift | 40 +++++++- PadelClub/Data/TeamRegistration.swift | 1 + PadelClub/Data/Tournament.swift | 4 + PadelClub/Views/Match/MatchSetupView.swift | 1 + PadelClub/Views/Round/LoserRoundsView.swift | 7 +- PadelClub/Views/Round/RoundSettingsView.swift | 92 ++++++++++--------- PadelClub/Views/Round/RoundView.swift | 2 +- 8 files changed, 117 insertions(+), 56 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 79bdd00..f272626 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -26,7 +26,7 @@ class Match: ModelObject, Storable { var broadcasted: Bool var name: String? var order: Int - private(set) var disabled: Bool = false + var disabled: Bool = false internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) { self.round = round @@ -74,8 +74,26 @@ class Match: ModelObject, Storable { _toggleMatchDisableState(false) } + func _toggleLoserMatchDisableState(_ state: Bool) { + if isLoserBracket == false { + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) + if let loserMatch = roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) { + print("disabling first loserround", state, loserMatch.matchTitle(.wide)) + loserMatch.disabled = state + try? DataStore.shared.matches.addOrUpdate(instance: loserMatch) + loserMatch._toggleLoserMatchDisableState(state) + } + } else { + roundObject?.loserRounds().forEach({ round in + print("disabling", state, round.roundTitle()) + round.disableLoserRound(state) + }) + } + } + fileprivate func _toggleMatchDisableState(_ state: Bool) { disabled = state + _toggleLoserMatchDisableState(state) topPreviousRoundMatch()?._toggleMatchDisableState(state) bottomPreviousRoundMatch()?._toggleMatchDisableState(state) try? DataStore.shared.matches.addOrUpdate(instance: self) @@ -319,7 +337,7 @@ class Match: ModelObject, Storable { func isBye() -> Bool { guard let roundObject else { return false } - return (roundObject.upperBracketMatches(ofMatch: self) + roundObject.previousRoundMatches(ofMatch: self)).anySatisfy({ $0.disabled }) + return topPreviousRoundMatch()?.disabled == true || bottomPreviousRoundMatch()?.disabled == true } func upperBracketTopMatch() -> Match? { @@ -355,7 +373,7 @@ class Match: ModelObject, Storable { } else if let match = topPreviousRoundMatch() { if let teamId = match.winningTeamId { return Store.main.findById(teamId) - } else if match.isBye() { + } else if match.disabled { return match.teams().first } } @@ -365,7 +383,7 @@ class Match: ModelObject, Storable { } else if let match = bottomPreviousRoundMatch() { if let teamId = match.winningTeamId { return Store.main.findById(teamId) - } else if match.isBye() { + } else if match.disabled { return match.teams().first } } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 0092d4f..b7d5f47 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -81,7 +81,7 @@ class Round: ModelObject, Storable { } else if let previousMatch = topPreviousRoundMatch(ofMatch: match) { if let teamId = previousMatch.winningTeamId { return Store.main.findById(teamId) - } else if previousMatch.isBye() { + } else if previousMatch.disabled { return previousMatch.teams().first } } @@ -91,7 +91,7 @@ class Round: ModelObject, Storable { } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) { if let teamId = previousMatch.winningTeamId { return Store.main.findById(teamId) - } else if previousMatch.isBye() { + } else if previousMatch.disabled { return previousMatch.teams().first } } @@ -143,7 +143,11 @@ class Round: ModelObject, Storable { } func playedMatches() -> [Match] { - Store.main.filter { $0.round == self.id && $0.disabled == false } + if loser == nil { + Store.main.filter { $0.round == self.id && $0.disabled == false } + } else { + Store.main.filter { $0.round == self.id } + } } func previousRound() -> Round? { @@ -159,7 +163,7 @@ class Round: ModelObject, Storable { } func isDisabled() -> Bool { - playedMatches().allSatisfy({ $0.disabled || $0.isBye() }) + _matches().allSatisfy({ $0.disabled }) } func getActiveLoserRound() -> Round? { @@ -167,6 +171,32 @@ class Round: ModelObject, Storable { return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false }) } + func enableRound() { + let _matches = _matches() + _matches.forEach { match in + match.disabled = false + match.losingTeamId = nil + match.winningTeamId = nil + match.endDate = nil + match.court = nil + match.servingTeamId = nil + try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores) + } + try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) + } + + func disableLoserRound(_ disable: Bool) { + let _matches = _matches() + _matches.forEach { match in + match.disabled = match.topPreviousRoundMatch()?.disabled == disable || match.bottomPreviousRoundMatch()?.disabled == disable + } + + try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) + loserRounds().forEach { round in + round.disableLoserRound(disable) + } + } + var cumulativeMatchCount: Int { var totalMatches = playedMatches().count if let parent = parentRound { @@ -188,7 +218,7 @@ class Round: ModelObject, Storable { let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count print("initialRound", initialRound.roundTitle()) if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() { - return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + playedMatches().count * 2).localizedLabel(displayStyle) + return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle) } } return RoundRule.roundName(fromRoundIndex: index) diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 7bef636..37484e1 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -63,6 +63,7 @@ class TeamRegistration: ModelObject, Storable { teamPosition = upperBranch ?? (isUpper ? 1 : 0) } match.previousMatch(teamPosition)?.disableMatch() + match._toggleLoserMatchDisableState(false) bracketPosition = matchIndex * 2 + teamPosition } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 9211fdc..50200bd 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -253,6 +253,10 @@ class Tournament : ModelObject, Storable { return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first } + func allRounds() -> [Round] { + Store.main.filter { $0.tournament == self.id } + } + func rounds() -> [Round] { Store.main.filter { $0.tournament == self.id && $0.loser == nil }.sorted(by: \.index).reversed() } diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 8309546..8e35ea3 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -25,6 +25,7 @@ struct MatchSetupView: View { .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button(role: .cancel) { team.bracketPosition = nil + match._toggleLoserMatchDisableState(false) try? dataStore.teamRegistrations.addOrUpdate(instance: team) } label: { Label("retirer", systemImage: "xmark") diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 0e9b0ca..60c647d 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -44,13 +44,14 @@ struct LoserRoundView: View { ForEach(loserRound.playedMatches()) { match in MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) .overlay { - if match.isBye() { - Image(systemName: "pencil.slash") + if match.disabled { + Image(systemName: "xmark") .resizable() .scaledToFit() - .opacity(0.3) + .opacity(0.8) } } + .disabled(match.disabled) } } header: { Text(loserRound.roundTitle(.wide)) diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index ed45818..8004418 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -21,50 +21,7 @@ struct RoundSettingsView: View { var body: some View { List { Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed) - - Section { - RowButtonView("Effacer classement", role: .destructive) { - tournament.rounds().forEach { round in - try? dataStore.rounds.delete(contentOfs: round.loserRounds()) - } - } - } - Section { - RowButtonView("Match de classement") { - tournament.rounds().forEach { round in - round.buildLoserBracket() - } - } - } - Section { - RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { - tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) - } - } - Section { - if let lastRound = tournament.rounds().first { // first is final, last round - RowButtonView("Supprimer " + lastRound.roundTitle(), role: .destructive) { - try? dataStore.rounds.delete(instance: lastRound) - } - } - } - - Section { - let roundIndex = tournament.rounds().count - 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) - let matches = (0.. Date: Sat, 6 Apr 2024 11:38:08 +0200 Subject: [PATCH 6/7] loser bracket system --- PadelClub/Data/GroupStage.swift | 8 +- PadelClub/Data/Match.swift | 146 +++++++++--------- PadelClub/Data/Round.swift | 60 +++---- PadelClub/Data/TeamRegistration.swift | 10 +- PadelClub/Data/TeamScore.swift | 4 +- PadelClub/Data/Tournament.swift | 12 +- PadelClub/Manager/PadelRule.swift | 8 +- PadelClub/ViewModel/MatchDescriptor.swift | 2 +- PadelClub/ViewModel/SetDescriptor.swift | 2 +- PadelClub/Views/Match/MatchRowView.swift | 4 +- PadelClub/Views/Match/MatchSetupView.swift | 110 ++++++++----- PadelClub/Views/Match/MatchSummaryView.swift | 4 +- PadelClub/Views/Match/PlayerBlockView.swift | 18 ++- PadelClub/Views/Round/LoserRoundsView.swift | 41 +++++ PadelClub/Views/Round/RoundSettingsView.swift | 46 +++--- PadelClub/Views/Round/RoundView.swift | 29 +++- PadelClub/Views/Round/RoundsView.swift | 8 +- PadelClub/Views/Score/EditScoreView.swift | 2 +- PadelClub/Views/Team/TeamPickerView.swift | 24 ++- PadelClub/Views/Team/TeamRowView.swift | 6 +- 20 files changed, 326 insertions(+), 218 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index ed50a94..04e6388 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -172,7 +172,7 @@ class GroupStage: ModelObject, Storable { } } - func team(whichTeam team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? { + func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { let _teams = _teams(for: matchIndex) switch team { case .one: @@ -202,11 +202,11 @@ class GroupStage: ModelObject, Storable { 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() + fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { + let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. Bool { + guard let bracketPosition = team.bracketPosition else { return false } + return index * 2 + teamPosition.rawValue == bracketPosition + } + + func resetMatch() { + losingTeamId = nil + winningTeamId = nil + endDate = nil + court = nil + servingTeamId = nil + } + + func teamWillBeWalkOut(_ team: TeamRegistration) { + resetMatch() + let previousScores = teamScores.filter({ $0.luckyLoser != nil }) + try? DataStore.shared.teamScores.delete(contentOfs: previousScores) + + if let existingTeamScore = teamScore(ofTeam: team) { + try? DataStore.shared.teamScores.delete(instance: existingTeamScore) + } + + let teamScoreWalkout = TeamScore(match: id, teamRegistration: team.id) + teamScoreWalkout.walkOut = 1 + try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreWalkout) + } + + func luckyLosers() -> [TeamRegistration] { + roundObject?.previousRound()?.losers() ?? [] + } + + func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool { + teamScore(teamPosition)?.walkOut == 1 + } + + func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) { + resetMatch() + let previousScores = teamScores.filter({ $0.luckyLoser != nil }) + try? DataStore.shared.teamScores.delete(contentOfs: previousScores) + + if let existingTeamScore = teamScore(ofTeam: team) { + try? DataStore.shared.teamScores.delete(instance: existingTeamScore) + } + + let matchIndex = index + let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) + let position = matchIndex * 2 + teamPosition.rawValue + let teamScoreLuckyLoser = TeamScore(match: id, teamRegistration: team.id) + teamScoreLuckyLoser.luckyLoser = position + try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) + } + func disableMatch() { _toggleMatchDisableState(true) } @@ -121,8 +173,16 @@ class Match: ModelObject, Storable { }.sorted(by: \.index).first } - func previousMatch(_ teamPosition: Int) -> Match? { - if teamPosition == 0 { + func upperBracketMatch(_ teamPosition: TeamPosition) -> Match? { + if teamPosition == .one { + return roundObject?.upperBracketTopMatch(ofMatchIndex: index) + } else { + return roundObject?.upperBracketBottomMatch(ofMatchIndex: index) + } + } + + func previousMatch(_ teamPosition: TeamPosition) -> Match? { + if teamPosition == .one { return topPreviousRoundMatch() } else { return bottomPreviousRoundMatch() @@ -146,10 +206,10 @@ class Match: ModelObject, Storable { } } - func setWalkOut(_ whichTeam: TeamData) { - let teamScoreWalkout = teamScore(whichTeam) ?? TeamScore(match: id, teamRegistration: team(whichTeam)?.id) + func setWalkOut(_ teamPosition: TeamPosition) { + let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, teamRegistration: team(teamPosition)?.id) teamScoreWalkout.walkOut = 0 - let teamScoreWinning = teamScore(whichTeam.otherTeam) ?? TeamScore(match: id, teamRegistration: team(whichTeam.otherTeam)?.id) + let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, teamRegistration: team(teamPosition.otherTeam)?.id) teamScoreWinning.walkOut = nil try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) @@ -301,10 +361,10 @@ class Match: ModelObject, Storable { return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } } - func scoreDifference(_ whichTeam: Int) -> (set: Int, game: Int)? { + func scoreDifference(_ teamPosition: 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 { + if teamPosition == team(.two)?.groupStagePosition { reverseValue = -1 } let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) @@ -320,76 +380,14 @@ class Match: ModelObject, Storable { return (setDifference * reverseValue, gameDifference * reverseValue) } - func groupStageProjectedTeam(_ team: TeamData) -> TeamRegistration? { + func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let groupStageObject else { return nil } - return groupStageObject.team(whichTeam: team, inMatchIndex: index) - } - - func seed(_ team: TeamData) -> TeamRegistration? { - guard let roundObject else { return nil } - return Store.main.filter(isIncluded: { - $0.tournament == roundObject.tournament && $0.bracketPosition != nil - }).first(where: { - ($0.bracketPosition! / 2) == self.index - && ($0.bracketPosition! % 2) == team.rawValue - }) - } - - func isBye() -> Bool { - guard let roundObject else { return false } - return topPreviousRoundMatch()?.disabled == true || bottomPreviousRoundMatch()?.disabled == true + return groupStageObject.team(teamPosition: team, inMatchIndex: index) } - func upperBracketTopMatch() -> Match? { - guard let roundObject else { return nil } - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { - return upperBracketTopMatch - } - return nil - } - - func upperBracketBottomMatch() -> Match? { - guard let roundObject else { return nil } - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { - return upperBracketBottomMatch - } - return nil - } - - func roundProjectedTeam(_ team: TeamData) -> TeamRegistration? { + func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let roundObject else { return nil } return roundObject.roundProjectedTeam(team, inMatch: self) - - if roundObject.isLoserBracket() == false, let seed = seed(team) { - return seed - } - - switch team { - case .one: - if let loser = upperBracketTopMatch()?.losingTeamId { - return Store.main.findById(loser) - } else if let match = topPreviousRoundMatch() { - if let teamId = match.winningTeamId { - return Store.main.findById(teamId) - } else if match.disabled { - return match.teams().first - } - } - case .two: - if let loser = upperBracketBottomMatch()?.losingTeamId { - return Store.main.findById(loser) - } else if let match = bottomPreviousRoundMatch() { - if let teamId = match.winningTeamId { - return Store.main.findById(teamId) - } else if match.disabled { - return match.teams().first - } - } - } - - return nil } func teamWon(_ team: TeamRegistration?) -> Bool { @@ -397,7 +395,7 @@ class Match: ModelObject, Storable { return winningTeamId == team?.id } - func team(_ team: TeamData) -> TeamRegistration? { + func team(_ team: TeamPosition) -> TeamRegistration? { if groupStage != nil { switch team { case .one: @@ -423,7 +421,7 @@ class Match: ModelObject, Storable { teamScore(ofTeam: team)?.isWalkOut() == true } - func teamScore(_ team: TeamData) -> TeamScore? { + func teamScore(_ team: TeamPosition) -> TeamScore? { teamScore(ofTeam: self.team(team)) } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index b7d5f47..68a4378 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -51,7 +51,7 @@ class Round: ModelObject, Storable { Store.main.filter { $0.round == self.id } } - func team(_ team: TeamData, inMatch match: Match) -> TeamRegistration? { + func team(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? { switch team { case .one: return roundProjectedTeam(.one, inMatch: match) @@ -60,7 +60,7 @@ class Round: ModelObject, Storable { } } - func seed(_ team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? { + func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.bracketPosition != nil }).first(where: { @@ -69,14 +69,20 @@ class Round: ModelObject, Storable { }) } - func roundProjectedTeam(_ team: TeamData, inMatch match: Match) -> TeamRegistration? { + func losers() -> [TeamRegistration] { + _matches().compactMap { $0.losingTeamId }.compactMap { Store.main.findById($0) } + } + + func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? { if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) { return seed } switch team { case .one: - if let loser = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId { + if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 }) { + return luckyLoser.team + } else if let loser = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId { return Store.main.findById(loser) } else if let previousMatch = topPreviousRoundMatch(ofMatch: match) { if let teamId = previousMatch.winningTeamId { @@ -86,7 +92,9 @@ class Round: ModelObject, Storable { } } case .two: - if let loser = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId { + if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { + return luckyLoser.team + } else if let loser = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId { return Store.main.findById(loser) } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) { if let teamId = previousMatch.winningTeamId { @@ -99,10 +107,6 @@ class Round: ModelObject, Storable { return nil } - -// func isMatchBye(_ match: Match) -> Bool { -// return (upperBracketMatches(ofMatch: match) + previousRoundMatches(ofMatch: match)).anySatisfy({ $0.disabled }) -// } func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? { let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) @@ -175,11 +179,7 @@ class Round: ModelObject, Storable { let _matches = _matches() _matches.forEach { match in match.disabled = false - match.losingTeamId = nil - match.winningTeamId = nil - match.endDate = nil - match.court = nil - match.servingTeamId = nil + match.resetMatch() try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores) } try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) @@ -188,7 +188,15 @@ class Round: ModelObject, Storable { func disableLoserRound(_ disable: Bool) { let _matches = _matches() _matches.forEach { match in - match.disabled = match.topPreviousRoundMatch()?.disabled == disable || match.bottomPreviousRoundMatch()?.disabled == disable + if disable { + if upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == true || upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == true { + match.disabled = true + } + } else { + if upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == false && upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == false { + match.disabled = false + } + } } try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) @@ -231,28 +239,6 @@ class Round: ModelObject, Storable { return "à démarrer" } } -// -// func indexOfMatch(_ match: Match) -> Int? { -// playedMatches().firstIndex(where: { $0.id == match.id }) -// } - - func previousRoundMatches(ofMatch match: Match) -> [Match] { - return Store.main.filter { - $0.round == previousRound()?.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) - } - } - - func upperBracketMatches(ofMatch match: Match) -> [Match] { - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index) - if isLoserBracket(), previousRound() == nil, let parentRound { - let upperBracketMatches = parentRound._matches().filter({ - let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index) - return index == indexInRound * 2 || index == indexInRound * 2 + 1 - }) - return upperBracketMatches - } - return [] - } func loserRounds() -> [Round] { return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed() diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 37484e1..1891628 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -53,18 +53,18 @@ class TeamRegistration: ModelObject, Storable { bracketPosition == nil && groupStage == nil } - func setSeedPosition(inSpot match: Match, upperBranch: Int?, opposingSeeding: Bool) { + func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { let matchIndex = match.index let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) - var teamPosition = upperBranch ?? (isUpper ? 0 : 1) + var teamPosition = slot ?? (isUpper ? .one : .two) if opposingSeeding { - teamPosition = upperBranch ?? (isUpper ? 1 : 0) + teamPosition = slot ?? (isUpper ? .two : .one) } + match.enableMatch() match.previousMatch(teamPosition)?.disableMatch() - match._toggleLoserMatchDisableState(false) - bracketPosition = matchIndex * 2 + teamPosition + bracketPosition = matchIndex * 2 + teamPosition.rawValue } var initialWeight: Int { diff --git a/PadelClub/Data/TeamScore.swift b/PadelClub/Data/TeamScore.swift index 7bc103a..46654aa 100644 --- a/PadelClub/Data/TeamScore.swift +++ b/PadelClub/Data/TeamScore.swift @@ -19,9 +19,9 @@ class TeamScore: ModelObject, Storable { var playerRegistrations: [String]? var score: String? var walkOut: Int? - var luckyLoser: Bool + var luckyLoser: Int? - internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Bool = false) { + internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Int? = nil) { self.match = match self.teamRegistration = teamRegistration self.playerRegistrations = playerRegistrations diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 50200bd..8c65c16 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -221,13 +221,13 @@ class Tournament : ModelObject, Storable { if availableSeeds.count <= availableSeedSpot.count { let spots = availableSeedSpot.shuffled() for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: false) + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) } } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { let spots = availableSeedOpponentSpot.shuffled() for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: true) + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) } } else if let chunk = seedGroup.chunk() { setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) @@ -506,6 +506,10 @@ class Tournament : ModelObject, Storable { } + func availableQualifiedTeams() -> [TeamRegistration] { + unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil }) + } + func qualifiedTeams() -> [TeamRegistration] { unsortedTeams().filter({ $0.qualifiedFromGroupStage() }) } @@ -526,9 +530,11 @@ class Tournament : ModelObject, Storable { } func groupStagesAreOver() -> Bool { - guard groupStages().isEmpty == false else { + let groupStages = groupStages() + guard groupStages.isEmpty == false else { return true } + return groupStages.allSatisfy({ $0.hasEnded() }) return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index 8cbf94f..cd48f06 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -794,11 +794,11 @@ enum TournamentType: Int, Hashable, Codable, CaseIterable, Identifiable { } } -enum TeamData: Int, Hashable, Codable, CaseIterable { +enum TeamPosition: Int, Hashable, Codable, CaseIterable { case one case two - var otherTeam: TeamData { + var otherTeam: TeamPosition { switch self { case .one: return .two @@ -841,7 +841,7 @@ enum SetFormat: Int, Hashable, Codable { } } - func winner(teamOne: Int, teamTwo: Int) -> TeamData { + func winner(teamOne: Int, teamTwo: Int) -> TeamPosition { return teamOne >= teamTwo ? .one : .two } @@ -1029,7 +1029,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie] } - func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamData { + func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { scoreTeamOne >= scoreTeamTwo ? .one : .two } diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index ba4c135..1d2d86a 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -61,7 +61,7 @@ class MatchDescriptor: ObservableObject { } } - var winner: TeamData { + var winner: TeamPosition { matchFormat.winner(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo) } diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index 42c65e1..2811e4e 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -23,7 +23,7 @@ struct SetDescriptor: Identifiable, Equatable { } } - var winner: TeamData? { + var winner: TeamPosition? { if let valueTeamTwo, let valueTeamOne { return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo) } else { diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 32c244c..79e1e3b 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -10,11 +10,11 @@ import SwiftUI struct MatchRowView: View { var match: Match let matchViewStyle: MatchViewStyle - @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed + @Environment(\.editMode) private var editMode @ViewBuilder var body: some View { - if isEditingTournamentSeed && match.isGroupStage() == false { + if editMode?.wrappedValue.isEditing == true && match.isGroupStage() == false && match.isLoserBracket == false { MatchSetupView(match: match) } else { NavigationLink { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 8e35ea3..9b30547 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -14,54 +14,90 @@ struct MatchSetupView: View { @ViewBuilder var body: some View { - _teamView(match.team(.one), teamPosition: 0) - _teamView(match.team(.two), teamPosition: 1) + _teamView(inTeamPosition: .one) + _teamView(inTeamPosition: .two) } @ViewBuilder - func _teamView(_ team: TeamRegistration?, teamPosition: Int) -> some View { - if let team { - TeamRowView(team: team, teamPosition: teamPosition) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button(role: .cancel) { - team.bracketPosition = nil - match._toggleLoserMatchDisableState(false) - try? dataStore.teamRegistrations.addOrUpdate(instance: team) - } label: { - Label("retirer", systemImage: "xmark") - } + func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View { + let team = match.team(teamPosition) + let teamScore = match.teamScore(ofTeam: team) + if let team, teamScore?.walkOut == nil { + VStack(alignment: .leading, spacing: 0) { + if let teamScore, teamScore.luckyLoser != nil { + Text("Repêchée").italic().font(.caption) } + TeamRowView(team: team, teamPosition: teamPosition) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .cancel) { + if match.isSeededBy(team: team, inTeamPosition: teamPosition) { + team.bracketPosition = nil + match.enableMatch() + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } else { + match.teamWillBeWalkOut(team) + try? dataStore.matches.addOrUpdate(instance: match) + } + } label: { + Label("retirer", systemImage: "xmark") + } + } + } } else { - HStack { - TeamPickerView(teamPicked: { team in - print(team.pasteData()) - team.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) - try? dataStore.matches.addOrUpdate(instance: match) - try? dataStore.teamRegistrations.addOrUpdate(instance: team) - }) - if let tournament = match.currentTournament() { - let availableSeedGroups = tournament.availableSeedGroups() - Menu { - ForEach(availableSeedGroups, id: \.self) { seedGroup in - Button { - if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) { - randomTeam.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) - try? dataStore.matches.addOrUpdate(instance: match) - try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam) + VStack(alignment: .leading) { + if let team { + TeamRowView(team: team, teamPosition: teamPosition) + .strikethrough() + } + HStack { + let walkOutSpot = match.isWalkOutSpot(teamPosition) + let luckyLosers = walkOutSpot ? match.luckyLosers() : [] + TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in + print(team.pasteData()) + if walkOutSpot { + match.setLuckyLoser(team: team, teamPosition: teamPosition) + try? dataStore.matches.addOrUpdate(instance: match) + } else { + team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false) + try? dataStore.matches.addOrUpdate(instance: match) + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } + }) + if let tournament = match.currentTournament() { + let availableSeedGroups = tournament.availableSeedGroups() + Menu { + if walkOutSpot, luckyLosers.isEmpty == false { + Button { + if let randomTeam = luckyLosers.randomElement() { + match.setLuckyLoser(team: randomTeam, teamPosition: teamPosition) + try? dataStore.matches.addOrUpdate(instance: match) + } + } label: { + Label("Repêchage", systemImage: "dice") } - } label: { - Label(seedGroup.localizedLabel(), systemImage: "dice") } + ForEach(availableSeedGroups, id: \.self) { seedGroup in + Button { + if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) { + randomTeam.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false) + try? dataStore.matches.addOrUpdate(instance: match) + try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam) + } + } label: { + Label(seedGroup.localizedLabel(), systemImage: "dice") + } + } + } label: { + Text("Tirage").tag(nil as SeedInterval?) } - } label: { - Text("Tirage").tag(nil as SeedInterval?) + .disabled(availableSeedGroups.isEmpty && walkOutSpot == false) } - .disabled(availableSeedGroups.isEmpty) } + .fixedSize(horizontal: false, vertical: true) + .buttonBorderShape(.capsule) + .buttonStyle(.borderedProminent) + } - .fixedSize(horizontal: false, vertical: true) - .buttonBorderShape(.capsule) - .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 5bd927c..c7cefca 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -84,14 +84,14 @@ struct MatchSummaryView: View { if matchViewStyle != .feedStyle { HStack(spacing: 0) { VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 0) { - PlayerBlockView(match: match, whichTeam: .one, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .one, color: color, width: width) .padding(matchViewStyle == .plainStyle ? 0 : 8) if width == 1 { Divider() } else { Divider().frame(height: width).overlay(color) } - PlayerBlockView(match: match, whichTeam: .two, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .two, color: color, width: width) .padding(matchViewStyle == .plainStyle ? 0 : 8) } } diff --git a/PadelClub/Views/Match/PlayerBlockView.swift b/PadelClub/Views/Match/PlayerBlockView.swift index 405b30c..3b9aa15 100644 --- a/PadelClub/Views/Match/PlayerBlockView.swift +++ b/PadelClub/Views/Match/PlayerBlockView.swift @@ -9,15 +9,15 @@ import SwiftUI struct PlayerBlockView: View { var match: Match - let whichTeam: TeamData + let teamPosition: TeamPosition let team: TeamRegistration? let color: Color let width: CGFloat - init(match: Match, whichTeam: TeamData, color: Color, width: CGFloat) { + init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat) { self.match = match - self.whichTeam = whichTeam - self.team = match.team(whichTeam) + self.teamPosition = teamPosition + self.team = match.team(teamPosition) self.color = color self.width = width } @@ -43,13 +43,21 @@ struct PlayerBlockView: View { } private func _defaultLabel() -> String { - whichTeam.localizedLabel() + if match.upperBracketMatch(teamPosition)?.disabled == true { + return "Bye" + } + + return teamPosition.localizedLabel() } var body: some View { HStack { VStack(alignment: .leading) { if let names { + if let teamScore = match.teamScore(ofTeam: team), teamScore.luckyLoser != nil { + Text("Repêchée").italic().font(.caption) + } + ForEach(names, id: \.self) { name in Text(name).lineLimit(1) } diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 60c647d..f4e6063 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -24,6 +24,9 @@ struct LoserRoundsView: View { switch selectedRound { case .none: List { + RowButtonView("Effacer", role: .destructive) { + + } } case .some(let selectedRound): LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index)) @@ -35,10 +38,20 @@ struct LoserRoundsView: View { } struct LoserRoundView: View { + @EnvironmentObject var dataStore: DataStore let loserRounds: [Round] + @Environment(\.editMode) private var editMode + + private func _roundDisabled() -> Bool { + loserRounds.allSatisfy({ $0.isDisabled() }) + } var body: some View { List { + if editMode?.wrappedValue.isEditing == true { + _editingView() + } + ForEach(loserRounds) { loserRound in Section { ForEach(loserRound.playedMatches()) { match in @@ -59,5 +72,33 @@ struct LoserRoundView: View { } } .headerProminence(.increased) + .toolbar { + EditButton() + } + } + + private func _editingView() -> some View { + if _roundDisabled() { + RowButtonView("Jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + let matches = round.playedMatches() + matches.forEach { match in + if round.upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == false && round.upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == false { + match.disabled = false + } + } + + try? dataStore.matches.addOrUpdate(contentOfs: matches) + } + } + } else { + RowButtonView("Ne pas jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + round.playedMatches().forEach { match in + match.disabled = true + } + } + } + } } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 8004418..daabeae 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -9,8 +9,8 @@ import SwiftUI struct RoundSettingsView: View { @EnvironmentObject var dataStore: DataStore + @Environment(\.editMode) private var editMode @Environment(Tournament.self) var tournament: Tournament - @Binding var isEditingTournamentSeed: Bool @State private var roundIndex: Int? var round: Round? { @@ -20,8 +20,6 @@ struct RoundSettingsView: View { var body: some View { List { - Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed) - if let availableSeedGroup = tournament.availableSeedGroup() { Section { @@ -47,10 +45,10 @@ struct RoundSettingsView: View { if let matches = tournament.getRound(atRoundIndex: roundIndex)?.playedMatches() { if let lastMatch = matches.last { - seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, upperBranch: 1, opposingSeeding: false) + seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false) } if let firstMatch = matches.first { - seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, upperBranch: 0, opposingSeeding: false) + seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false) } } try? dataStore.teamRegistrations.addOrUpdate(contentOfs: seeds) @@ -58,6 +56,10 @@ struct RoundSettingsView: View { tournament.setSeeds(inRoundIndex: roundIndex, inSeedGroup: availableSeedGroup) try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) } + + if tournament.availableSeeds().isEmpty { + editMode?.wrappedValue = .inactive + } } } @@ -65,22 +67,22 @@ struct RoundSettingsView: View { Text("Placement des têtes de série") } } - - Section { - RowButtonView("Effacer classement", role: .destructive) { - tournament.rounds().forEach { round in - try? dataStore.rounds.delete(contentOfs: round.loserRounds()) - } - } - } - - Section { - RowButtonView("Match de classement") { - tournament.rounds().forEach { round in - round.buildLoserBracket() - } - } - } +// +// Section { +// RowButtonView("Effacer classement", role: .destructive) { +// tournament.rounds().forEach { round in +// try? dataStore.rounds.delete(contentOfs: round.loserRounds()) +// } +// } +// } +// +// Section { +// RowButtonView("Match de classement") { +// tournament.rounds().forEach { round in +// round.buildLoserBracket() +// } +// } +// } Section { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) @@ -119,7 +121,7 @@ struct RoundSettingsView: View { } #Preview { - RoundSettingsView(isEditingTournamentSeed: .constant(true)) + RoundSettingsView() .environment(Tournament.mock()) .environmentObject(DataStore.shared) } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 03e2f2c..58f7d62 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -8,18 +8,25 @@ import SwiftUI struct RoundView: View { + @Environment(\.editMode) private var editMode + @Environment(Tournament.self) var tournament: Tournament + var round: Round var body: some View { List { - let loserRounds = round.loserRounds() - if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { - Section { - NavigationLink { - LoserRoundsView(upperBracketRound: round) - .navigationTitle(first.roundTitle()) - } label: { - Text(first.roundTitle()) + + if editMode?.wrappedValue.isEditing == false { + let loserRounds = round.loserRounds() + if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { + Section { + NavigationLink { + LoserRoundsView(upperBracketRound: round) + .environment(tournament) + .navigationTitle(first.roundTitle()) + } label: { + Text(first.roundTitle()) + } } } } @@ -33,9 +40,15 @@ struct RoundView: View { } } .headerProminence(.increased) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + EditButton() + } + } } } #Preview { RoundView(round: Round.mock()) + .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index 7d028b3..1134fed 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -10,13 +10,13 @@ import SwiftUI struct RoundsView: View { var tournament: Tournament @State private var selectedRound: Round? - @State private var isEditingTournamentSeed = false + @State var editMode: EditMode = .inactive init(tournament: Tournament) { self.tournament = tournament _selectedRound = State(wrappedValue: tournament.getActiveRound()) if tournament.availableSeeds().isEmpty == false { - _isEditingTournamentSeed = State(wrappedValue: true) + _editMode = .init(wrappedValue: .active) } } @@ -25,14 +25,14 @@ struct RoundsView: View { GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: tournament.rounds(), nilDestinationIsValid: true) switch selectedRound { case .none: - RoundSettingsView(isEditingTournamentSeed: $isEditingTournamentSeed) + RoundSettingsView() .navigationTitle("Réglages") case .some(let selectedRound): RoundView(round: selectedRound) .navigationTitle(selectedRound.roundTitle()) - .editTournamentSeed(isEditingTournamentSeed) } } + .environment(\.editMode, $editMode) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 4e5bd6c..3c0cd33 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -12,7 +12,7 @@ struct EditScoreView: View { @ObservedObject var matchDescriptor: MatchDescriptor @Environment(\.dismiss) private var dismiss - func walkout(_ team: TeamData) { + func walkout(_ team: TeamPosition) { matchDescriptor.match?.setWalkOut(team) save() dismiss() diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index efc9aba..7747d5f 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -13,16 +13,34 @@ struct TeamPickerView: View { @Environment(\.dismiss) private var dismiss @State private var presentTeamPickerView: Bool = false @State private var searchField: String = "" + var luckyLosers: [TeamRegistration] = [] let teamPicked: ((TeamRegistration) -> (Void)) var body: some View { - Button("Choisir") { - presentTeamPickerView = true - } + Button("Choisir") { + presentTeamPickerView = true + } .sheet(isPresented: $presentTeamPickerView) { NavigationStack { List { let teams = tournament.sortedTeams() + if luckyLosers.isEmpty == false { + Section { + _teamListView(luckyLosers.sorted(by: \.weight)) + } header: { + Text("Repêchage") + } + } + + let qualified = tournament.availableQualifiedTeams() + if qualified.isEmpty == false { + Section { + _teamListView(qualified.sorted(by: \.weight)) + } header: { + Text("Qualifiées entrants") + } + } + Section { _teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed()) } header: { diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index e24a93c..4ae5b05 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -9,12 +9,12 @@ import SwiftUI struct TeamRowView: View { var team: TeamRegistration - var teamPosition: Int? = nil + var teamPosition: TeamPosition? = nil var body: some View { LabeledContent { VStack(alignment: .trailing, spacing: 0) { - if teamPosition == 0 || teamPosition == nil { + if teamPosition == .one || teamPosition == nil { Text(team.weight.formatted()) .font(.caption) } @@ -22,7 +22,7 @@ struct TeamRowView: View { Text("#" + (index + 1).formatted()) .font(.title) } - if teamPosition == 1 { + if teamPosition == .two { Text(team.weight.formatted()) .font(.caption) From e5086fac19b6496115b4a7425311d530be992ff6 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 6 Apr 2024 18:05:12 +0200 Subject: [PATCH 7/7] still fixing stuff on loserbracket management --- PadelClub/Data/Match.swift | 6 +- PadelClub/Data/Round.swift | 49 ++++++++--- PadelClub/Data/TeamRegistration.swift | 1 - PadelClub/Data/Tournament.swift | 75 ++++++++++++---- PadelClub/ViewModel/SeedInterval.swift | 9 +- PadelClub/Views/Match/PlayerBlockView.swift | 1 - PadelClub/Views/Round/LoserRoundsView.swift | 14 +-- PadelClub/Views/Round/RoundSettingsView.swift | 87 ++----------------- PadelClub/Views/Round/RoundView.swift | 11 +++ 9 files changed, 124 insertions(+), 129 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 7877f00..2e4432c 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -126,19 +126,17 @@ class Match: ModelObject, Storable { _toggleMatchDisableState(false) } - func _toggleLoserMatchDisableState(_ state: Bool) { + private func _toggleLoserMatchDisableState(_ state: Bool) { if isLoserBracket == false { let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) if let loserMatch = roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) { - print("disabling first loserround", state, loserMatch.matchTitle(.wide)) loserMatch.disabled = state try? DataStore.shared.matches.addOrUpdate(instance: loserMatch) loserMatch._toggleLoserMatchDisableState(state) } } else { roundObject?.loserRounds().forEach({ round in - print("disabling", state, round.roundTitle()) - round.disableLoserRound(state) + round.handleLoserRoundState() }) } } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 68a4378..b7090f2 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -140,10 +140,10 @@ class Round: ModelObject, Storable { func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? { - _matches().first(where: { + Store.main.filter(isIncluded: { let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index) - return index == matchIndexInRound - }) + return $0.round == id && index == matchIndexInRound + }).first } func playedMatches() -> [Match] { @@ -176,32 +176,53 @@ class Round: ModelObject, Storable { } func enableRound() { + _toggleRound(disable: false) + } + + func disableRound() { + _toggleRound(disable: true) + } + + private func _toggleRound(disable: Bool) { let _matches = _matches() _matches.forEach { match in - match.disabled = false + match.disabled = disable match.resetMatch() try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores) } try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) } - func disableLoserRound(_ disable: Bool) { + func handleLoserRoundState() { let _matches = _matches() _matches.forEach { match in - if disable { - if upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == true || upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == true { - match.disabled = true - } - } else { - if upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == false && upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == false { - match.disabled = false - } + let previousRound = self.previousRound() + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index) + var parentMatches = [Match]() + if isLoserBracket(), previousRound == nil, let parentRound = parentRound { + let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) + let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) + parentMatches = [upperBracketTopMatch, upperBracketBottomMatch].compactMap({ $0 }) + } else if let previousRound { + let previousRoundTopMatch : Match? = Store.main.filter { + $0.round == previousRound.id && $0.index == match.topPreviousRoundMatchIndex() + }.first + let previousRoundBottomMatch : Match? = Store.main.filter { + $0.round == previousRound.id && $0.index == match.bottomPreviousRoundMatchIndex() + }.first + parentMatches = [previousRoundTopMatch, previousRoundBottomMatch].compactMap({ $0 }) + } + + if parentMatches.anySatisfy({ $0.disabled }) { + match.disabled = true + } else if parentMatches.allSatisfy({ $0.disabled == false }) { + match.disabled = false } } try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) loserRounds().forEach { round in - round.disableLoserRound(disable) + round.handleLoserRoundState() } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 1891628..d945a78 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -62,7 +62,6 @@ class TeamRegistration: ModelObject, Storable { if opposingSeeding { teamPosition = slot ?? (isUpper ? .two : .one) } - match.enableMatch() match.previousMatch(teamPosition)?.disableMatch() bracketPosition = matchIndex * 2 + teamPosition.rawValue } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 8c65c16..b93c344 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -143,11 +143,11 @@ class Tournament : ModelObject, Storable { return seeds().filter { $0.isSeedable() } } - func lastSeedRound() -> Int? { + func lastSeedRound() -> Int { if let last = seeds().filter({ $0.bracketPosition != nil }).last { return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2) } else { - return nil + return 0 } } @@ -213,24 +213,67 @@ class Tournament : ModelObject, Storable { return availableSeeds } - func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) { - let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) - let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) - let availableSeeds = seeds(inSeedGroup: seedGroup) + func seedGroupAvailable(atRoundIndex roundIndex: Int) -> SeedInterval? { + if let availableSeedGroup = availableSeedGroup() { + return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: availableSeedGroup) + } else { + return nil + } + } + + func seedGroupAvailable(atRoundIndex roundIndex: Int, availableSeedGroup: SeedInterval) -> SeedInterval? { + + if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() { + + if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup } + + let availableSeeds = seeds(inSeedGroup: availableSeedGroup) + let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) + let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) + + if availableSeeds.count == availableSeedSpot.count { + return availableSeedGroup + } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) { + return availableSeedGroup + } else if let chunk = availableSeedGroup.chunk() { + return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) + } + } - if availableSeeds.count <= availableSeedSpot.count { - let spots = availableSeedSpot.shuffled() - for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) + return nil + } + + func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) { + if seedGroup == SeedInterval(first: 1, last: 2) { + let seeds = seeds() + if let matches = getRound(atRoundIndex: roundIndex)?.playedMatches() { + if let lastMatch = matches.last { + seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false) + } + if let firstMatch = matches.first { + seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false) + } } - } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { + } else { - let spots = availableSeedOpponentSpot.shuffled() - for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) + let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) + let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) + let availableSeeds = seeds(inSeedGroup: seedGroup) + + if availableSeeds.count <= availableSeedSpot.count { + let spots = availableSeedSpot.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) + } + } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { + + let spots = availableSeedOpponentSpot.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) + } + } else if let chunk = seedGroup.chunk() { + setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) } - } else if let chunk = seedGroup.chunk() { - setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) } } diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index 506be68..d0a82c0 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -16,11 +16,12 @@ struct SeedInterval: Hashable, Comparable { } func chunk() -> SeedInterval? { - if last - (last - first) / 2 > first { - return SeedInterval(first: first, last: last - (last - first) / 2) - } else { - return nil + if (last - first) / 2 > 0 { + if last - (last - first) / 2 > first { + return SeedInterval(first: first, last: last - (last - first) / 2) + } } + return nil } } diff --git a/PadelClub/Views/Match/PlayerBlockView.swift b/PadelClub/Views/Match/PlayerBlockView.swift index 3b9aa15..68ee799 100644 --- a/PadelClub/Views/Match/PlayerBlockView.swift +++ b/PadelClub/Views/Match/PlayerBlockView.swift @@ -46,7 +46,6 @@ struct PlayerBlockView: View { if match.upperBracketMatch(teamPosition)?.disabled == true { return "Bye" } - return teamPosition.localizedLabel() } diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index f4e6063..fd9dd32 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -81,22 +81,14 @@ struct LoserRoundView: View { if _roundDisabled() { RowButtonView("Jouer ce tour", role: .destructive) { loserRounds.forEach { round in - let matches = round.playedMatches() - matches.forEach { match in - if round.upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == false && round.upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == false { - match.disabled = false - } - } - - try? dataStore.matches.addOrUpdate(contentOfs: matches) + round.enableRound() + round.handleLoserRoundState() } } } else { RowButtonView("Ne pas jouer ce tour", role: .destructive) { loserRounds.forEach { round in - round.playedMatches().forEach { match in - match.disabled = true - } + round.disableRound() } } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index daabeae..8a412a0 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -11,93 +11,17 @@ struct RoundSettingsView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.editMode) private var editMode @Environment(Tournament.self) var tournament: Tournament - @State private var roundIndex: Int? - - var round: Round? { - guard let roundIndex else { return nil } - return tournament.rounds()[roundIndex] - } var body: some View { List { - if let availableSeedGroup = tournament.availableSeedGroup() { - Section { - - Picker(selection: $roundIndex) { - Text("choisir de la manche").tag(nil as Int?) - ForEach(tournament.rounds()) { round in - Text(round.roundTitle()).tag(round.index as Int?) - } - } label: { - Text(availableSeedGroup.localizedLabel()) - } - - if let roundIndex { - - RowButtonView("Valider") { - if availableSeedGroup == SeedInterval(first: 1, last: 2) { - let seeds = tournament.seeds() -// let startIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) -// let numberOfMatchInRound = RoundRule.numberOfMatches(forRoundIndex: roundIndex) -// let lastIndex = startIndex + numberOfMatchInRound - 1 -// seeds.prefix(1).first?.bracketPosition = lastIndex * 2 + 1 //TS 1 branche du bas du dernier match -// seeds.prefix(2).dropFirst().first?.bracketPosition = startIndex * 2 //TS 2 branche du haut du premier match - - if let matches = tournament.getRound(atRoundIndex: roundIndex)?.playedMatches() { - if let lastMatch = matches.last { - seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false) - } - if let firstMatch = matches.first { - seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false) - } - } - try? dataStore.teamRegistrations.addOrUpdate(contentOfs: seeds) - } else { - tournament.setSeeds(inRoundIndex: roundIndex, inSeedGroup: availableSeedGroup) - try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) - } - - if tournament.availableSeeds().isEmpty { - editMode?.wrappedValue = .inactive - } - } - } - - } header: { - Text("Placement des têtes de série") - } - } -// -// Section { -// RowButtonView("Effacer classement", role: .destructive) { -// tournament.rounds().forEach { round in -// try? dataStore.rounds.delete(contentOfs: round.loserRounds()) -// } -// } -// } -// -// Section { -// RowButtonView("Match de classement") { -// tournament.rounds().forEach { round in -// round.buildLoserBracket() -// } -// } -// } Section { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) tournament.allRounds().forEach({ round in round.enableRound() - }) - } - } - - Section { - if let lastRound = tournament.rounds().first { // first is final, last round - RowButtonView("Supprimer " + lastRound.roundTitle(), role: .destructive) { - try? dataStore.rounds.delete(instance: lastRound) - } + }) + editMode?.wrappedValue = .active } } @@ -116,6 +40,13 @@ struct RoundSettingsView: View { } } + Section { + if let lastRound = tournament.rounds().first { // first is final, last round + RowButtonView("Supprimer " + lastRound.roundTitle(), role: .destructive) { + try? dataStore.rounds.delete(instance: lastRound) + } + } + } } } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 58f7d62..cc1cb11 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -10,6 +10,7 @@ import SwiftUI struct RoundView: View { @Environment(\.editMode) private var editMode @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore var round: Round @@ -29,6 +30,16 @@ struct RoundView: View { } } } + } else if let availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) { + + RowButtonView("Placer \(availableSeedGroup.localizedLabel())") { + tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup) + try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) + + if tournament.availableSeeds().isEmpty { + editMode?.wrappedValue = .inactive + } + } } ForEach(round.playedMatches()) { match in