diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 7486b9a..0c8a95c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; }; FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */; }; FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */; }; + FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */; }; FF0EC5202BB16F680056B6D1 /* SwiftParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */; }; FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */; }; FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5382BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv */; }; @@ -98,6 +99,7 @@ FF2BE4882B85E27400592328 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; + FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */; }; FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; }; FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */; }; FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6B42B9248200002987F /* NetworkManager.swift */; }; @@ -199,11 +201,17 @@ FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; }; FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; }; FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; }; + FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; }; FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; }; FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; }; FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD52B923960008466FA /* URL+Extensions.swift */; }; FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD82B923F3C008466FA /* String+Extensions.swift */; }; FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */; }; + FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF9644F2BC25E3700EEF017 /* PlanningView.swift */; }; + FFF964532BC262B000EEF017 /* PlanningSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */; }; + FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964542BC266CF00EEF017 /* SchedulerView.swift */; }; + FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */; }; + FFF9645B2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -288,6 +296,7 @@ FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = ""; }; FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImportView.swift; sourceTree = ""; }; + FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentScheduleView.swift; sourceTree = ""; }; FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftParser.swift; sourceTree = ""; }; FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSourceRankDateView.swift; sourceTree = ""; }; FF0EC5232BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL DAMES-07-2023.csv"; sourceTree = ""; }; @@ -345,6 +354,7 @@ FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayContext.swift; sourceTree = ""; }; FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduler.swift; sourceTree = ""; }; FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = ""; }; FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestination.swift; sourceTree = ""; }; FF4AB6B42B9248200002987F /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; @@ -447,11 +457,17 @@ FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = ""; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; + FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = ""; }; FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = ""; }; FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = ""; }; FFF8ACD52B923960008466FA /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; FFF8ACD82B923F3C008466FA /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; + FFF9644F2BC25E3700EEF017 /* PlanningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningView.swift; sourceTree = ""; }; + FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningSettingsView.swift; sourceTree = ""; }; + FFF964542BC266CF00EEF017 /* SchedulerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerView.swift; sourceTree = ""; }; + FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundScheduleEditorView.swift; sourceTree = ""; }; + FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageScheduleEditorView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -600,6 +616,7 @@ FFCFC00B2BBC39A600B82851 /* Score */, FF967D072BAF3D3000A9A3BD /* Team */, FF089EB92BB011EE00F0AEC7 /* Player */, + FFF964512BC2628600EEF017 /* Planning */, FF3F74F72B919F96004CFE0E /* Tournament */, C4A47D882B7BBB5000ADC637 /* Subscription */, C4A47D852B7BA33F00ADC637 /* User */, @@ -779,6 +796,7 @@ FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */, FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */, FF8F26532BAE1E4400650388 /* TableStructureView.swift */, + FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -824,6 +842,7 @@ FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */, FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, + FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */, ); path = ViewModel; sourceTree = ""; @@ -1008,6 +1027,19 @@ path = Extensions; sourceTree = ""; }; + FFF964512BC2628600EEF017 /* Planning */ = { + isa = PBXGroup; + children = ( + FFF9644F2BC25E3700EEF017 /* PlanningView.swift */, + FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */, + FFF964542BC266CF00EEF017 /* SchedulerView.swift */, + FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */, + FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, + FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, + ); + path = Planning; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1243,6 +1275,7 @@ FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */, FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */, + FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */, FFA1B1292BB71773006CE248 /* PadelClubButtonView.swift in Sources */, FF5DA19B2BB9662200A33061 /* TournamentSeedEditing.swift in Sources */, FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, @@ -1266,16 +1299,19 @@ C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */, FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, + FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */, FF967CEC2BAECB9900A9A3BD /* Match.swift in Sources */, 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 */, + FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */, FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */, C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */, FF8F263D2BAD627A00650388 /* TournamentConfiguratorView.swift in Sources */, FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */, + FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */, FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */, FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, @@ -1319,6 +1355,7 @@ FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */, FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */, FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */, + FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, @@ -1334,6 +1371,8 @@ FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */, FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */, FF967D0D2BAF3EB300A9A3BD /* MatchDateView.swift in Sources */, + FFF964532BC262B000EEF017 /* PlanningSettingsView.swift in Sources */, + FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */, FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */, FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */, FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */, @@ -1348,6 +1387,7 @@ FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */, FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */, + FFF9645B2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift in Sources */, C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 04e6388..c703675 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -85,7 +85,7 @@ class GroupStage: ModelObject, Storable { try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) } - func matches() -> [Match] { + func playedMatches() -> [Match] { let ordered = _matches() if ordered.isEmpty == false && ordered.count == _matchOrder().count { return _matchOrder().map { @@ -127,19 +127,19 @@ class GroupStage: ModelObject, Storable { } func availableToStart() -> [Match] { - matches().filter({ $0.canBeStarted() && $0.isRunning() == false }) + playedMatches().filter({ $0.canBeStarted() && $0.isRunning() == false }) } func runningMatches() -> [Match] { - matches().filter({ $0.isRunning() }) + playedMatches().filter({ $0.isRunning() }) } func readyMatches() -> [Match] { - matches().filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) + playedMatches().filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) } func finishedMatches() -> [Match] { - matches().filter({ $0.hasEnded() }) + playedMatches().filter({ $0.hasEnded() }) } private func _matchOrder() -> [Int] { @@ -195,7 +195,7 @@ class GroupStage: ModelObject, Storable { (size * (size - 1)) / 2 } - private func _matches() -> [Match] { + func _matches() -> [Match] { Store.main.filter { $0.groupStage == self.id } } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 2e4432c..ee4cd30 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -111,7 +111,6 @@ class Match: ModelObject, Storable { } 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 @@ -149,6 +148,9 @@ class Match: ModelObject, Storable { try? DataStore.shared.matches.addOrUpdate(instance: self) } + func next() -> Match? { + Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first + } func topPreviousRoundMatchIndex() -> Int { index * 2 + 1 } @@ -187,6 +189,16 @@ class Match: ModelObject, Storable { } } + func upperMatches() -> [Match] { + guard let roundObject else { return [] } + return [roundObject.upperBracketTopMatch(ofMatchIndex: index), roundObject.upperBracketBottomMatch(ofMatchIndex: index)].compactMap({ $0 }) + } + + var computedOrder: Int { + guard let roundObject else { return index } + return roundObject.isLoserBracket() ? roundObject.index * 100 + indexInRound() : roundObject.index * 1000 + indexInRound() + } + func previousMatches() -> [Match] { guard let roundObject else { return [] } return Store.main.filter { match in @@ -327,11 +339,7 @@ class Match: ModelObject, Storable { func isBracket() -> Bool { round != nil } - - func isTournamentMatch() -> Bool { - groupStageObject?.tournament != nil - } - + func walkoutTeam() -> [TeamRegistration] { scores().filter({ $0.walkOut != nil }).compactMap { $0.team } } diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 17429d1..10bcd26 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -17,6 +17,12 @@ extension Club { } } +extension GroupStage { + static func mock() -> GroupStage { + GroupStage(tournament: "", index: 0, size: 4) + } +} + extension Round { static func mock() -> Round { Round(tournament: "", index: 0) diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index b7090f2..9050978 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -17,7 +17,8 @@ class Round: ModelObject, Storable { var index: Int var loser: String? var format: Int? - + var startDate: Date? + internal init(tournament: String, index: Int, loser: String? = nil, matchFormat: MatchFormat? = nil) { self.tournament = tournament self.index = index @@ -47,10 +48,39 @@ class Round: ModelObject, Storable { Store.main.findById(tournament) } - private func _matches() -> [Match] { + func _matches() -> [Match] { Store.main.filter { $0.round == self.id } } + func upperMatches(ofMatch match: Match) -> [Match] { + if loser != nil, previousRound() == nil, let parentRound { + let matchIndex = match.index + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) + return [parentRound.getMatch(atMatchIndexInRound: indexInRound * 2), parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 }) + } + return [] + } + + func previousMatches(ofMatch match: Match) -> [Match] { + guard let previousRound = previousRound() else { return [] } + return Store.main.filter { + ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) && $0.round == previousRound.id + } + } + + func precedentMatches(ofMatch match: Match) -> [Match] { + let upper = upperMatches(ofMatch: match) + if upper.isEmpty == false { + return upper + } + let previous : [Match] = previousMatches(ofMatch: match) + if previous.isEmpty == false && previous.allSatisfy({ $0.disabled }), let previousRound = previousRound() { + return previous.flatMap({ previousRound.precedentMatches(ofMatch: $0) }) + } else { + return previous + } + } + func team(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? { switch team { case .one: @@ -150,10 +180,10 @@ class Round: ModelObject, Storable { if loser == nil { Store.main.filter { $0.round == self.id && $0.disabled == false } } else { - Store.main.filter { $0.round == self.id } + _matches() } } - + func previousRound() -> Round? { Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index + 1 }).first } @@ -245,7 +275,7 @@ class Round: ModelObject, Storable { func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { if let parentRound, let initialRound = parentRound.initialRound() { let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count - print("initialRound", initialRound.roundTitle()) +// print("initialRound", initialRound.roundTitle()) if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() { return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle) } @@ -323,6 +353,7 @@ class Round: ModelObject, Storable { case _index = "index" case _loser = "loser" case _format = "format" + case _startDate = "startDate" } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b93c344..63ecbed 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -296,6 +296,12 @@ class Tournament : ModelObject, Storable { return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first } + func allMatches() -> [Match] { + let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } + let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() } + return matches.filter({ $0.disabled == false }) + } + func allRounds() -> [Round] { Store.main.filter { $0.tournament == self.id } } @@ -578,10 +584,14 @@ class Tournament : ModelObject, Storable { return true } return groupStages.allSatisfy({ $0.hasEnded() }) - return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified + //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified + } + + func scheduleStatus() -> String { + //todo + return "todo" } - func bracketStatus() -> String { if let round = getActiveRound() { return [round.roundTitle(), round.roundStatus()].joined(separator: " ") diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift new file mode 100644 index 0000000..5778eca --- /dev/null +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -0,0 +1,251 @@ +// +// MatchScheduler.swift +// PadelClub +// +// Created by Razmig Sarkissian on 08/04/2024. +// + +import Foundation + +struct TimeMatch { + let matchID: String + let rotationIndex: Int + var courtIndex: Int + let groupIndex: Int +} + +struct MatchDispatcher { + let timedMatches: [TimeMatch] + let freeCourtPerRotation: [Int: [Int]] + let rotationCount: Int + let groupLastRotation: [Int: Int] +} + +extension Match { + func teamIds() -> [String] { + return teams().map { $0.id } + } + + func containsTeamId(_ id: String) -> Bool { + teamIds().contains(id) + } +} + +class MatchScheduler { + static let shared = MatchScheduler() + + func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> MatchDispatcher { + + let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate } + + // Get the maximum count of matches in any group + let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 + + // Use zip and flatMap to flatten matches in the desired order + let flattenedMatches = (0.. 0 { + rotationMatches = rotationMatches.sorted(by: { + if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { + return $0.groupStageObject!.index < $1.groupStageObject!.index + } else { + return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 + } + }) + } + + (0.. Bool { + print(roundObject.roundTitle(), match.matchTitle()) + let previousMatches = roundObject.precedentMatches(ofMatch: match) + if previousMatches.isEmpty { return true } + + let previousMatchSlots = slots.filter({ slot in + previousMatches.map { $0.id }.contains(slot.matchID) + }) + + if previousMatchSlots.isEmpty { + if previousMatches.filter({ $0.disabled == false }).allSatisfy({ $0.startDate != nil }) { + return true + } + return false + } + + if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count { + if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) { + return true + } + return false + } + + let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + (roundObject.loser == nil ? 1 : 0) < rotationIndex }) + return previousMatchIsInPreviousRotation + + } + + func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0) -> MatchDispatcher { + + var slots = [TimeMatch]() + var availableMatchs = flattenedMatches + var rotationIndex = 0 + var freeCourtPerRotation = [Int: [Int]]() + var groupLastRotation = [Int: Int]() + + while slots.count < flattenedMatches.count { + freeCourtPerRotation[rotationIndex] = [] + var matchPerRound = [Int: Int]() + var availableCourt = numberOfCourtsAvailablePerRotation + if rotationIndex == 0 { + availableCourt = availableCourt - initialOccupiedCourt + } + (0.. 0, match.indexInRound() == 0, numberOfCourtsAvailablePerRotation > 1, let nextMatch = match.next() { + if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex) { + return true + } else { + return false + } + } + + if (matchPerRound[roundObject.index] ?? 0)%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == numberOfCourtsAvailablePerRotation - 1 { + return false + } + + return canBePlayed + }) { + //print(first.roundObject!.roundTitle(), first.matchTitle()) + + if first.roundObject!.loser == nil { + if let roundIndex = matchPerRound[first.roundObject!.index] { + matchPerRound[first.roundObject!.index] = roundIndex + 1 + } else { + matchPerRound[first.roundObject!.index] = 1 + } + } + slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index )) + availableMatchs.removeAll(where: { $0.id == first.id }) + if let index = first.roundObject?.index { + groupLastRotation[index] = rotationIndex + } + } else { + freeCourtPerRotation[rotationIndex]!.append(courtIndex) + } + } + + rotationIndex += 1 + } + + var organizedSlots = [TimeMatch]() + for i in 0.. some View { Menu { - if groupStage.matches().isEmpty { + if groupStage.playedMatches().isEmpty { Button { //groupStage.startGroupStage() //save() diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index cfe0778..8504db5 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -153,18 +153,14 @@ struct MatchDetailView: View { MatchSummaryView(match: match, matchViewStyle: .plainStyle) } header: { } footer: { - HStack { - if match.isTournamentMatch() { - if match.isEmpty() == false { - Button { - showDetails = true - } label: { - Text("Détails des joueurs") - } + if match.isEmpty() == false { + HStack { + Button { + showDetails = true + } label: { + Text("Détails des joueurs") } - } - Spacer() - if match.isEmpty() == false && match.isTournamentMatch() { + Spacer() Menu { //MenuWarnView(warningSender: match) } label: { @@ -204,7 +200,7 @@ struct MatchDetailView: View { // .presentationDetents([.fraction(0.66)]) // } .sheet(item: $scoreType, onDismiss: { - if match.hasEnded() && match.isTournamentMatch() { + if match.hasEnded() { dismiss() } }) { scoreType in @@ -361,11 +357,7 @@ struct MatchDetailView: View { DisclosureGroup(isExpanded: $isEditing) { startingOptionView } label: { - if match.isTournamentMatch() { - Text("Modifier l'horaire et le terrain") - } else { - Text("Horaires et terrain") - } + Text("Modifier l'horaire et le terrain") } } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index c7cefca..d4fac8f 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -60,6 +60,8 @@ struct MatchSummaryView: View { if match.isGroupStage() && matchViewStyle != .feedStyle { if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle { Text(groupStage.groupStageTitle()) + } else if let round = match.roundObject { + Text(round.roundTitle()) } Text(match.matchTitle()) } else if let currentTournament = match.currentTournament() { diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift new file mode 100644 index 0000000..b2222a3 --- /dev/null +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -0,0 +1,48 @@ +// +// GroupStageScheduleEditorView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 07/04/2024. +// + +import SwiftUI + +struct GroupStageScheduleEditorView: View { + @EnvironmentObject var dataStore: DataStore + var groupStage: GroupStage + + var body: some View { + @Bindable var groupStage = groupStage + List { + Section { + MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat) + } + + Section { + Text("Modifier l'horaire") + } + + RowButtonView("Convoquer") { + + } + + NavigationLink { + GroupStageView(groupStage: groupStage) + } label: { + Text("Voir la poule") + } + } + .onChange(of: groupStage.matchFormat) { + _save() + } + } + + + private func _save() { + try? dataStore.groupStages.addOrUpdate(instance: groupStage) + } +} + +#Preview { + GroupStageScheduleEditorView(groupStage: GroupStage.mock()) +} diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift new file mode 100644 index 0000000..10fa53f --- /dev/null +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -0,0 +1,40 @@ +// +// MatchScheduleEditorView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 10/04/2024. +// + +import SwiftUI + +struct MatchScheduleEditorView: View { + @Environment(Tournament.self) var tournament: Tournament + var match: Match + @State private var startDate: Date + + init(match: Match) { + self.match = match + self._startDate = State(wrappedValue: match.startDate ?? Date()) + } + + var body: some View { + Section { + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday())) + } + RowButtonView("Modifier") { + _updateSchedule() + } + } header: { + Text(match.matchTitle()) + } + } + + private func _updateSchedule() { + MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, randomizeCourts: true, startDate: startDate) + } +} + +#Preview { + MatchScheduleEditorView(match: Match.mock()) +} diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift new file mode 100644 index 0000000..d4a7aec --- /dev/null +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -0,0 +1,159 @@ +// +// PlanningSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 07/04/2024. +// + +import SwiftUI + +struct PlanningSettingsView: View { + @EnvironmentObject var dataStore: DataStore + var tournament: Tournament + @State private var scheduleSetup: Bool = false + @State private var randomCourtDistribution: Bool = false + @State private var groupStageCourtCount: Int + + init(tournament: Tournament) { + self.tournament = tournament + self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1) + } + + var body: some View { + @Bindable var tournament = tournament + List { + Section { + DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate) + Stepper(value: $tournament.dayDuration, in: 1...1_000) { + HStack { + Text("Durée") + Spacer() + Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) + } + } + } header: { + Text("Démarrage et durée du tournoi") + } footer: { + Text("todo: Expliquer ce que ca fait") + } + + Section { + TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) + TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount) + + NavigationLink { + + } label: { + Text("Disponibilité des terrains") + } + } + + Section { + + Toggle(isOn: $randomCourtDistribution) { + Text("Distribuer les terrains au hasard") + } + + RowButtonView("Horaire intelligent", role: .destructive) { + let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 + let groupStages = tournament.groupStages() + let numberOfCourtsAvailablePerRotation: Int = min(tournament.courtCount, groupStageCourtCount * groupStages.count) + + let matchScheduler = MatchScheduler.shared + + let matches = tournament.groupStages().flatMap({ $0._matches() }) + matches.forEach({ $0.startDate = nil }) + + var times = Set(groupStages.compactMap { $0.startDate }) + if times.isEmpty { + groupStages.forEach({ $0.startDate = tournament.startDate }) + times.insert(tournament.startDate) + try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) + } + + var lastDate : Date? = nil + times.forEach { time in + let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groupStages, startingDate: time, randomizeCourts: randomCourtDistribution) + + dispatch.timedMatches.forEach { matchSchedule in + if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { + let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 + if let startDate = match.groupStageObject?.startDate { + match.startDate = startDate.addingTimeInterval(timeIntervalToAdd) + lastDate = match.startDate?.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60) + } + match.setCourt(matchSchedule.courtIndex + 1) + } + } + } + try? dataStore.matches.addOrUpdate(contentOfs: matches) + let upperRounds = tournament.rounds() + let rounds = upperRounds.flatMap { + [$0] + $0.loserRoundsAndChildren() + } + + let flattenedMatches = rounds.flatMap { round in + round._matches().filter({ $0.disabled == false }).sorted(by: \.index) + } + + flattenedMatches.forEach({ $0.startDate = nil }) + + let roundDispatch = matchScheduler.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomCourtDistribution) + + roundDispatch.timedMatches.forEach { matchSchedule in + if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { + let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 + if let lastDate { + match.startDate = lastDate.addingTimeInterval(timeIntervalToAdd) + } + match.setCourt(matchSchedule.courtIndex + 1) + } + } + + try? dataStore.matches.addOrUpdate(contentOfs: flattenedMatches) + + + scheduleSetup = true + } + + if scheduleSetup { + HStack { + Image(systemName: "checkmark") + } + } + } + + Section { + NavigationLink { + + } label: { + Text("Modifier le message de convocation") + } + } + } + .onChange(of: groupStageCourtCount) { + tournament.groupStageCourtCount = groupStageCourtCount + _save() + } + .onChange(of: tournament.startDate) { + _save() + } + .onChange(of: tournament.courtCount) { + _save() + } + .onChange(of: tournament.groupStageCourtCount) { + _save() + } + .onChange(of: tournament.dayDuration) { + _save() + } + } + + private func _save() { + try? dataStore.tournaments.addOrUpdate(instance: tournament) + } +} + +#Preview { + PlanningSettingsView(tournament: Tournament.mock()) +} diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift new file mode 100644 index 0000000..37f83d7 --- /dev/null +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -0,0 +1,149 @@ +// +// PlanningView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 07/04/2024. +// + +import SwiftUI + +struct PlanningView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(\.editMode) private var editMode + + let matches: [Match] + @State private var timeSlots: [Date:[Match]] + @State private var days: [Date] + @State private var keys: [Date] + + init(matches: [Match]) { + self.matches = matches + let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } + _timeSlots = State(wrappedValue: timeSlots) + _days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted()) + _keys = State(wrappedValue: timeSlots.keys.sorted()) + } + + var body: some View { + List { + ForEach(days, id: \.self) { day in + Section { + ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in + if let _matches = timeSlots[key] { + if editMode?.wrappedValue.isEditing == true { + HStack { + VStack(alignment: .leading) { + let index = keys.firstIndex(of: key) + Button { + let previousKey = keys[index! - 1] + let previousMatches = timeSlots[previousKey] + previousMatches?.forEach { match in + match.startDate = key + } + _matches.forEach { match in + match.startDate = previousKey + } + _update() + } label: { + Image(systemName: "arrow.up") + } + .buttonStyle(.bordered) + .disabled(index == 0) + Button { + let nextKey = keys[index! + 1] + let nextMatches = timeSlots[nextKey] + nextMatches?.forEach { match in + match.startDate = key + } + _matches.forEach { match in + match.startDate = nextKey + } + _update() + } label: { + Image(systemName: "arrow.down") + } + .buttonStyle(.bordered) + .disabled(index == keys.count - 1) + } + VStack(alignment: .leading) { + LabeledContent { + Text(_matches.count.formatted() + " match" + _matches.count.pluralSuffix) + } label: { + Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle) + } + + ForEach(_matches) { match in + LabeledContent { + + } label: { + if let groupStage = match.groupStageObject { + Text(groupStage.groupStageTitle()) + } else if let round = match.roundObject { + Text(round.roundTitle()) + } + Text(match.matchTitle()) + } + } + } + } + } else { + DisclosureGroup { + ForEach(_matches) { match in + NavigationLink { + MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) + } label: { + if let groupStage = match.groupStageObject { + Text(groupStage.groupStageTitle()) + } else if let round = match.roundObject { + Text(round.roundTitle()) + } + Text(match.matchTitle()) + } + } + } label: { + _timeSlotView(key: key, matches: _matches) + } + } + } + } + } header: { + Text(day.formatted(.dateTime.day().weekday().month().year())) + } + .headerProminence(.increased) + } + } + .toolbar { + EditButton() + } + .onChange(of: isEditing) { old, new in + if old == true && new == false { + print("save") + try? dataStore.matches.addOrUpdate(contentOfs: matches) + } + } + .navigationTitle("Programmation") + } + + private func _update() { + let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } + self.timeSlots = timeSlots + self.days = Set(timeSlots.keys.map { $0.startOfDay }).sorted() + self.keys = timeSlots.keys.sorted() + } + + private var isEditing: Bool { + editMode?.wrappedValue.isEditing == true + } + + private func _timeSlotView(key: Date, matches: [Match]) -> some View { + LabeledContent { + Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) + } label: { + Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle) + } + } +} + +#Preview { + PlanningView(matches: []) +} diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift new file mode 100644 index 0000000..5c07bdb --- /dev/null +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -0,0 +1,58 @@ +// +// RoundScheduleEditorView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 07/04/2024. +// + +import SwiftUI + +struct RoundScheduleEditorView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(Tournament.self) var tournament: Tournament + + var round: Round + @State private var startDate: Date + + init(round: Round) { + self.round = round + self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date()) + } + + var body: some View { + @Bindable var round = round + List { + Section { + MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) + } + + Section { + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday())) + } + RowButtonView("Modifier") { + _updateSchedule() + } + } + + ForEach(round.playedMatches()) { match in + MatchScheduleEditorView(match: match) + } + } + .onChange(of: round.matchFormat) { + _save() + } + } + + private func _updateSchedule() { + MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, randomizeCourts: true, startDate: startDate) + } + + private func _save() { + try? dataStore.rounds.addOrUpdate(instance: round) + } +} + +#Preview { + RoundScheduleEditorView(round: Round.mock()) +} diff --git a/PadelClub/Views/Planning/SchedulerView.swift b/PadelClub/Views/Planning/SchedulerView.swift new file mode 100644 index 0000000..dd7be35 --- /dev/null +++ b/PadelClub/Views/Planning/SchedulerView.swift @@ -0,0 +1,66 @@ +// +// SchedulerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 07/04/2024. +// + +import SwiftUI + +extension GroupStage: Schedulable {} +extension Round: Schedulable {} + +struct SchedulerView: View { + var tournament: Tournament + + var body: some View { + List { + ForEach(tournament.groupStages()) { + _schedulerView($0) + } + ForEach(tournament.rounds()) { round in + _schedulerView(round) + ForEach(round.loserRoundsAndChildren()) { loserRound in + if round.isDisabled() == false { + _schedulerView(loserRound) + } + } + } + } + .headerProminence(.increased) + } + + func _schedulerView(_ schedulable: any Schedulable) -> some View { + Section { + NavigationLink { + Group { + if let round = schedulable as? Round { + RoundScheduleEditorView(round: round) + .environment(tournament) + } + else if let groupStage = schedulable as? GroupStage { + GroupStageScheduleEditorView(groupStage: groupStage) + } + } + .navigationTitle(schedulable.selectionLabel()) + } label: { + LabeledContent { + Text(schedulable.matchFormat.format).font(.largeTitle) + } label: { + if let startDate = schedulable.startDate { + Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle) + Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) + } else { + Text("Aucun horaire") + } + } + } + } header: { + Text(schedulable.selectionLabel()) + } + } +} + +#Preview { + SchedulerView(tournament: Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift index 0afe572..8f46f49 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift @@ -8,21 +8,21 @@ import SwiftUI struct TournamentFieldsManagerView: View { - @Environment(Tournament.self) private var tournament: Tournament - + let localizedStringKey: String + @Binding var count: Int + var body: some View { - @Bindable var tournament = tournament - - Stepper(value: $tournament.courtCount, in: 1...100_000) { + Stepper(value: $count, in: 1...1_000) { LabeledContent { - Text(tournament.courtCount.formatted()) + Text(count.formatted()) } label: { - Text("Nombre de terrains") + Text(localizedStringKey) } } - }} + } +} #Preview { - TournamentFieldsManagerView() + TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2)) .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/Screen/Screen.swift b/PadelClub/Views/Tournament/Screen/Screen.swift index bf665a4..5ac9fed 100644 --- a/PadelClub/Views/Tournament/Screen/Screen.swift +++ b/PadelClub/Views/Tournament/Screen/Screen.swift @@ -13,4 +13,5 @@ enum Screen: String, Codable { case round case settings case structure + case schedule } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift new file mode 100644 index 0000000..4fccc36 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -0,0 +1,91 @@ +// +// TournamentScheduleView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 07/04/2024. +// + +import SwiftUI + +protocol Schedulable: Selectable, Identifiable { + var startDate: Date? { get set } + var matchFormat: MatchFormat { get set } + func playedMatches() -> [Match] +} + +enum ScheduleDestination: Identifiable, Selectable { + var id: String { + switch self { + case .groupStage(let groupStage): + return groupStage.id + case .round(let round): + return round.id + default: + return String(describing: self) + } + } + + case planning + case schedule + case groupStage(GroupStage) + case round(Round) + + func selectionLabel() -> String { + switch self { + case .schedule: + return "Horaires" + case .planning: + return "Progr." + case .groupStage(let groupStage): + return groupStage.selectionLabel() + case .round(let round): + return round.selectionLabel() + } + } + + func badgeValue() -> Int? { + nil + } +} + +struct TournamentScheduleView: View { + var tournament: Tournament + @State private var selectedScheduleDestination: ScheduleDestination? = nil + let allDestinations: [ScheduleDestination] + let verticalDestinations: [ScheduleDestination] + + init(tournament: Tournament) { + self.tournament = tournament + self.verticalDestinations = tournament.groupStages().map({ .groupStage($0) }) + tournament.rounds().map({ .round($0) }) + self.allDestinations = [.schedule, .planning] + } + + var body: some View { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $selectedScheduleDestination, destinations: allDestinations, nilDestinationIsValid: true) + switch selectedScheduleDestination { + case .none: + PlanningSettingsView(tournament: tournament) + .navigationTitle("Réglages") + case .some(let selectedSchedule): + switch selectedSchedule { + case .groupStage(let groupStage): + Text("ok") + case .round(let round): + Text("ok") + case .planning: + PlanningView(matches: tournament.allMatches()) + case .schedule: + SchedulerView(tournament: tournament) + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Horaires") + } +} + +#Preview { + TournamentScheduleView(tournament: Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index 5d04840..c6c6500 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -44,7 +44,7 @@ struct TournamentSettingsView: View { TournamentLevelPickerView() TournamentDurationManagerView() - TournamentFieldsManagerView() + TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) TournamentDatePickerView() diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index d29ee90..8e4312f 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -12,6 +12,16 @@ struct TournamentRunningView: View { @ViewBuilder var body: some View { + Section { + NavigationLink(value: Screen.schedule) { + LabeledContent { + Text(tournament.scheduleStatus()) + } label: { + Text("Horaires") + } + } + } + Section { NavigationLink(value: Screen.groupStage) { LabeledContent { diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index aa200a6..73f9a29 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -21,15 +21,15 @@ struct TournamentView: View { var body: some View { List { - if tournament.missingUnrankedValue() { - Button("update NC") { - tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate) - tournament.maleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: tournament.rankSourceDate) - try? dataStore.tournaments.addOrUpdate(instance: tournament) - } - } - - +// if tournament.missingUnrankedValue() { +// Button("update NC") { +// tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate) +// tournament.maleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: tournament.rankSourceDate) +// try? dataStore.tournaments.addOrUpdate(instance: tournament) +// } +// } +// +// Section { NavigationLink(value: Screen.inscription) { LabeledContent { @@ -78,6 +78,8 @@ struct TournamentView: View { GroupStagesView(tournament: tournament) case .round: RoundsView(tournament: tournament) + case .schedule: + TournamentScheduleView(tournament: tournament) } } .environment(tournament)