From d24576eb3e4fa0a55527f3132522813598047e2c Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 24 Apr 2025 09:41:35 +0200 Subject: [PATCH] fix slow stuff --- PadelClub/Data/DataStore.swift | 40 ++- PadelClub/Data/Match.swift | 24 +- PadelClub/Data/Round.swift | 277 +++++++++++++++--- PadelClub/Data/Tournament.swift | 130 ++++---- PadelClub/Extensions/Date+Extensions.swift | 8 +- .../NumberFormatter+Extensions.swift | 8 +- PadelClub/Utils/PadelRule.swift | 8 +- .../Views/Components/MatchListView.swift | 11 + PadelClub/Views/Match/MatchRowView.swift | 59 +--- PadelClub/Views/Match/MatchSummaryView.swift | 46 +-- .../Views/Round/LoserRoundSettingsView.swift | 37 ++- PadelClub/Views/Round/LoserRoundView.swift | 158 ++++++---- PadelClub/Views/Round/LoserRoundsView.swift | 219 ++++++++++---- PadelClub/Views/Round/RoundSettingsView.swift | 8 +- PadelClub/Views/Round/RoundView.swift | 61 ++-- PadelClub/Views/Round/RoundsView.swift | 14 +- 16 files changed, 760 insertions(+), 348 deletions(-) diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 8c73857..f496b75 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -312,8 +312,16 @@ class DataStore: ObservableObject { // MARK: - Convenience + private var _lastRunningCheckDate: Date? = nil + private var _cachedRunningMatches: [Match]? = nil + func runningMatches() -> [Match] { let dateNow : Date = Date() + if let lastCheck = _lastRunningCheckDate, + let cachedMatches = _cachedRunningMatches, + dateNow.timeIntervalSince(lastCheck) < 5 { + return cachedMatches + } let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] @@ -325,11 +333,22 @@ class DataStore: ObservableObject { runningMatches.append(contentsOf: matches) } } - return runningMatches + _lastRunningCheckDate = dateNow + _cachedRunningMatches = runningMatches + return _cachedRunningMatches! } + private var _lastRunningAndNextCheckDate: Date? = nil + private var _cachedRunningAndNextMatches: [Match]? = nil + func runningAndNextMatches() -> [Match] { let dateNow : Date = Date() + if let lastCheck = _lastRunningAndNextCheckDate, + let cachedMatches = _cachedRunningAndNextMatches, + dateNow.timeIntervalSince(lastCheck) < 5 { + return cachedMatches + } + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] @@ -340,11 +359,23 @@ class DataStore: ObservableObject { runningMatches.append(contentsOf: matches) } } - return runningMatches + _lastRunningAndNextCheckDate = dateNow + _cachedRunningAndNextMatches = runningMatches + return _cachedRunningAndNextMatches! } + private var _lastEndCheckDate: Date? = nil + private var _cachedEndMatches: [Match]? = nil + func endMatches() -> [Match] { let dateNow : Date = Date() + + if let lastCheck = _lastEndCheckDate, + let cachedMatches = _cachedEndMatches, + dateNow.timeIntervalSince(lastCheck) < 5 { + return cachedMatches + } + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] @@ -355,7 +386,10 @@ class DataStore: ObservableObject { runningMatches.append(contentsOf: matches) } } - return runningMatches.sorted(by: \.endDate!, order: .descending) + + _lastEndCheckDate = dateNow + _cachedEndMatches = runningMatches.sorted(by: \.endDate!, order: .descending) + return _cachedEndMatches! } } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index e747bea..f4132a8 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -88,7 +88,9 @@ defer { #endif if groupStage != nil { return index - } else if let index = (matches ?? roundObject?.playedMatches().sorted(by: \.index))?.firstIndex(where: { $0.id == id }) { + } else if let matches, let index = matches.firstIndex(where: { $0.id == id }) { + return index + } else if let roundObject, roundObject.isUpperBracket(), let index = roundObject.playedMatches().firstIndex(where: { $0.id == id }) { return index } return RoundRule.matchIndexWithinRound(fromMatchIndex: index) @@ -337,36 +339,28 @@ defer { func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { //if disabled == state { return } + let tournamentStore = self.tournamentStore let currentState = disabled disabled = state if disabled != currentState { - do { - try self.tournamentStore?.teamScores.delete(contentOfs: teamScores) - } catch { - Logger.error(error) - } + tournamentStore?.teamScores.delete(contentOfs: teamScores) } if state == true, state != currentState { let teams = teams() for team in teams { if isSeededBy(team: team) { team.bracketPosition = nil - self.tournamentStore?.teamRegistrations.addOrUpdate(instance: team) + tournamentStore?.teamRegistrations.addOrUpdate(instance: team) } } } //byeState = false - if state != currentState { - roundObject?._cachedSeedInterval = nil + roundObject?.invalidateCache() name = nil - do { - try self.tournamentStore?.matches.addOrUpdate(instance: self) - } catch { - Logger.error(error) - } + tournamentStore?.matches.addOrUpdate(instance: self) } if single == false { @@ -466,7 +460,7 @@ defer { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) } guard let roundObject else { return index } - return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + indexInRound() + return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + RoundRule.matchIndexWithinRound(fromMatchIndex: index) } func previousMatches() -> [Match] { diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 4551aca..f64f991 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -12,8 +12,10 @@ import SwiftUI @Observable final class Round: BaseRound, SideStorable { - var _cachedSeedInterval: SeedInterval? - + private var _cachedSeedInterval: SeedInterval? + private var _cachedLoserRounds: [Round]? + private var _cachedLoserRoundsAndChildren: [Round]? + internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { super.init(tournament: tournament, index: index, parent: parent, format: matchFormat, startDate: startDate, groupStageLoserBracket: groupStageLoserBracket, loserBracketMode: loserBracketMode) @@ -45,7 +47,16 @@ final class Round: BaseRound, SideStorable { func tournamentObject() -> Tournament? { return Store.main.findById(tournament) } - + + func _unsortedMatches(includeDisabled: Bool) -> [Match] { + guard let tournamentStore = self.tournamentStore else { return [] } + if includeDisabled { + return tournamentStore.matches.filter { $0.round == self.id } + } else { + return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false } + } + } + func _matches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter { $0.round == self.id }.sorted(by: \.index) @@ -78,7 +89,20 @@ final class Round: BaseRound, SideStorable { return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false } } + + func upperMatches(upperRound: Round, match: Match) -> [Match] { + let matchIndex = match.index + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) + return [upperRound.getMatch(atMatchIndexInRound: indexInRound * 2), upperRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 }) + } + func previousMatches(previousRound: Round, match: Match) -> [Match] { + guard let tournamentStore = self.tournamentStore else { return [] } + return tournamentStore.matches.filter { + $0.round == previousRound.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) + } + } + func upperMatches(ofMatch match: Match) -> [Match] { if parent != nil, previousRound() == nil, let parentRound { let matchIndex = match.index @@ -164,12 +188,12 @@ final class Round: BaseRound, SideStorable { func losers() -> [TeamRegistration] { - let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } + let teamIds: [String] = self._unsortedMatches(includeDisabled: false).compactMap { $0.losingTeamId } return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) } } func winners() -> [TeamRegistration] { - let teamIds: [String] = self._matches().compactMap { $0.winningTeamId } + let teamIds: [String] = self._unsortedMatches(includeDisabled: false).compactMap { $0.winningTeamId } return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) } } @@ -302,7 +326,7 @@ defer { func enabledMatches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } - return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false }.sorted(by: \.index) + return tournamentStore.matches.filter { $0.disabled == false && $0.round == self.id }.sorted(by: \.index) } // func displayableMatches() -> [Match] { @@ -353,17 +377,21 @@ defer { func loserRounds(forRoundIndex roundIndex: Int, loserRoundsAndChildren: [Round]) -> [Round] { return loserRoundsAndChildren.filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount) } + + func isEnabled() -> Bool { + return _unsortedMatches(includeDisabled: false).isEmpty == false + } func isDisabled() -> Bool { - return _matches().allSatisfy({ $0.disabled }) + return _unsortedMatches(includeDisabled: true).allSatisfy({ $0.disabled }) } func isRankDisabled() -> Bool { - return _matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty }) + return _unsortedMatches(includeDisabled: true).allSatisfy({ $0.disabled && $0.teamScores.isEmpty }) } func resetFromRoundAllMatchesStartDate() { - _matches().forEach({ + _unsortedMatches(includeDisabled: false).forEach({ $0.startDate = nil }) loserRoundsAndChildren().forEach { round in @@ -373,7 +401,7 @@ defer { } func resetFromRoundAllMatchesStartDate(from match: Match) { - let matches = _matches() + let matches = _unsortedMatches(includeDisabled: false) if let index = matches.firstIndex(where: { $0.id == match.id }) { matches[index...].forEach { match in match.startDate = nil @@ -386,10 +414,36 @@ defer { } func getActiveLoserRound() -> Round? { - let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed() - return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first + // Get all loser rounds once + let allLoserRounds = loserRounds() + var lowestIndexRound: Round? = nil + let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) + let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) + + for currentIndex in 0..100 rounds + // Find non-disabled round with current index + let roundAtIndex = allLoserRounds.first(where: { $0.index == currentIndex && $0.isEnabled() }) + + // No round at this index, we've checked all available rounds + if roundAtIndex == nil { + break + } + + // Save the first non-disabled round we find (should be index 0) + if lowestIndexRound == nil { + lowestIndexRound = roundAtIndex + } + + // If this round is active, return it immediately + if roundAtIndex!.hasStarted() && !roundAtIndex!.hasEnded() { + return roundAtIndex + } + } + + // If no active round found, return the one with lowest index + return lowestIndexRound } - + func enableRound() { _toggleRound(disable: false) } @@ -399,7 +453,7 @@ defer { } private func _toggleRound(disable: Bool) { - let _matches = _matches() + let _matches = _unsortedMatches(includeDisabled: true) _matches.forEach { match in match.disabled = disable match.resetMatch() @@ -414,7 +468,7 @@ defer { } var cumulativeMatchCount: Int { - var totalMatches = playedMatches().count + var totalMatches = _unsortedMatches(includeDisabled: false).count if let parentRound { totalMatches += parentRound.cumulativeMatchCount } @@ -443,11 +497,12 @@ defer { } func disabledMatches() -> [Match] { - return _matches().filter({ $0.disabled }) + guard let tournamentStore = self.tournamentStore else { return [] } + return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true } } func allLoserRoundMatches() -> [Match] { - loserRoundsAndChildren().flatMap({ $0._matches() }) + loserRoundsAndChildren().flatMap({ $0._unsortedMatches(includeDisabled: false) }) } var theoryCumulativeMatchCount: Int { @@ -520,7 +575,7 @@ defer { let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - print("func seedInterval(initialMode)", initialMode, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + print("func seedInterval(initialMode)", id, index, initialMode, _cachedSeedInterval, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif @@ -560,15 +615,19 @@ defer { if let previousRound = previousRound() { if (previousRound.enabledMatches().isEmpty == false || initialMode) { - return previousRound.seedInterval(initialMode: initialMode)?.chunks()?.first + _cachedSeedInterval = previousRound.seedInterval(initialMode: initialMode)?.chunks()?.first + return _cachedSeedInterval } else { - return previousRound.seedInterval(initialMode: initialMode) + _cachedSeedInterval = previousRound.seedInterval(initialMode: initialMode) + return _cachedSeedInterval } } else if let parentRound { if parentRound.isUpperBracket() { - return parentRound.seedInterval(initialMode: initialMode) + _cachedSeedInterval = parentRound.seedInterval(initialMode: initialMode) + return _cachedSeedInterval } - return parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last + _cachedSeedInterval = parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last + return _cachedSeedInterval } return nil @@ -599,9 +658,10 @@ defer { tournamentObject?.updateTournamentState() } - func roundStatus() -> String { - let hasEnded = hasEnded() - if hasStarted() && hasEnded == false { + func roundStatus(playedMatches: [Match]) -> String { + let hasEnded = playedMatches.anySatisfy({ $0.hasEnded() == false }) == false + let hasStarted = playedMatches.anySatisfy({ $0.hasStarted() }) + if hasStarted && hasEnded == false { return "en cours" } else if hasEnded { return "terminée" @@ -613,6 +673,9 @@ defer { } func loserRounds() -> [Round] { + if let _cachedLoserRounds { + return _cachedLoserRounds + } guard let tournamentStore = self.tournamentStore else { return [] } #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -622,12 +685,57 @@ defer { } #endif - return tournamentStore.rounds.filter( { $0.parent == id }).sorted(by: \.index).reversed() + // Filter first to reduce sorting work + let filteredRounds = tournamentStore.rounds.filter { $0.parent == id } + + // Return empty array early if no rounds match + if filteredRounds.isEmpty { + return [] + } + + // Sort directly in descending order to avoid the separate reversed() call + _cachedLoserRounds = filteredRounds.sorted { $0.index > $1.index } + return _cachedLoserRounds! } func loserRoundsAndChildren() -> [Round] { - let loserRounds = loserRounds() - return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() }) + #if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func loserRoundsAndChildren: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + + // Return cached result if available + if let cached = _cachedLoserRoundsAndChildren { + return cached + } + + // Calculate result if cache is invalid or unavailable + let direct = loserRounds() + + // Return quickly if there are no direct loser rounds + if direct.isEmpty { + // Update cache with empty result + _cachedLoserRoundsAndChildren = [] + return [] + } + + // Pre-allocate capacity to avoid reallocations (estimate based on typical tournament structure) + var allRounds = direct + let estimatedChildrenCount = direct.count * 2 // Rough estimate + allRounds.reserveCapacity(estimatedChildrenCount) + + // Collect all children rounds in one pass + for round in direct { + allRounds.append(contentsOf: round.loserRoundsAndChildren()) + } + + // Store result in cache + _cachedLoserRoundsAndChildren = allRounds + + return allRounds } func isUpperBracket() -> Bool { @@ -639,43 +747,106 @@ defer { } func deleteLoserBracket() { - let loserRounds = loserRounds() - self.tournamentStore?.rounds.delete(contentOfs: loserRounds) +#if DEBUG //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func deleteLoserBracket: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } +#endif + self.tournamentStore?.rounds.delete(contentOfs: self.loserRounds()) + self.invalidateCache() } func buildLoserBracket() { +#if DEBUG //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func buildLoserBracket: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } +#endif guard loserRounds().isEmpty else { return } + self.invalidateCache() let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) guard currentRoundMatchCount > 1 else { return } + guard let tournamentStore else { return } let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) - - var loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat + let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat // if let parentRound { // loserBracketMatchFormat = tournamentObject()?.loserBracketSmartMatchFormat(parentRound.index) // } - + + var titles = [String: String]() + let rounds = (0.. 0 { + match.disabled = true + if upperRound.isUpperBracket(), prmc == 1 { + match.byeState = true + } + } else { + match.disabled = false + } + } + } + tournamentStore?.matches.addOrUpdate(contentOfs: m) + + loserRounds().forEach { loserRound in + loserRound.disableUnplayedLoserBracketMatches() + } + } + var parentRound: Round? { guard let parent = parent else { return nil } return self.tournamentStore?.rounds.findById(parent) @@ -706,15 +877,41 @@ defer { } func updateMatchFormatOfAllMatches(_ updatedMatchFormat: MatchFormat) { - let playedMatches = _matches() + let playedMatches = _unsortedMatches(includeDisabled: true) playedMatches.forEach { match in match.matchFormat = updatedMatchFormat } self.tournamentStore?.matches.addOrUpdate(contentOfs: playedMatches) } + + func loserBracketTurns() -> [LoserRound] { +#if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func loserBracketTurns()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } +#endif + var rounds = [LoserRound]() + let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) + let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) + + for index in 0.. Round? { let rounds: [Round] = self.rounds() - let unfinishedRounds: [Round] = rounds.filter { $0.hasStarted() && $0.hasEnded() == false } - let sortedRounds: [Round] = unfinishedRounds.sorted(by: \.index).reversed() - let round = sortedRounds.first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first + for round in rounds { + let playedMatches = round.playedMatches() - if withSeeds { - if round?.seeds().isEmpty == false { + // Optimization: If no matches have started in this round, return nil immediately + if !playedMatches.contains(where: { $0.hasStarted() }) { return round - } else { - return nil } - } else { - return round + + if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) { + if withSeeds { + if !round.seeds().isEmpty { + return round + } else { + return nil + } + } else { + return round + } + } } + + return nil } + + func getActiveRoundAndStatus() -> (Round, String)? { + let rounds: [Round] = self.rounds() + + for round in rounds { + let playedMatches = round.playedMatches() + // Optimization: If no matches have started in this round, return nil immediately + if !playedMatches.contains(where: { $0.hasStarted() }) { + return (round, round.roundStatus(playedMatches: playedMatches)) + } + + if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) { + return (round, round.roundStatus(playedMatches: playedMatches)) + } + } + return nil + } + func getPlayedMatchDateIntervals(in event: Event) -> [DateInterval] { let allMatches: [Match] = self.allMatches().filter { $0.courtIndex != nil && $0.startDate != nil } return allMatches.map { match in @@ -550,7 +577,7 @@ defer { func rounds() -> [Round] { guard let tournamentStore = self.tournamentStore else { return [] } let rounds: [Round] = tournamentStore.rounds.filter { $0.isUpperBracket() } - return rounds.sorted(by: \.index).reversed() + return rounds.sorted { $0.index > $1.index } } func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { @@ -1478,8 +1505,8 @@ defer { cut = TeamRegistration.TeamRange(availableSeeds.first, availableSeeds.last) } - if let round = getActiveRound() { - return ([round.roundTitle(.short), round.roundStatus()].joined(separator: " ").lowercased(), description, cut) + if let roundAndStatus = getActiveRoundAndStatus() { + return ([roundAndStatus.0.roundTitle(.short), roundAndStatus.1].joined(separator: " ").lowercased(), description, cut) } else { return ("", description, nil) } @@ -1494,15 +1521,16 @@ defer { let cut : TeamRegistration.TeamRange? = isAnimation() ? nil : TeamRegistration.TeamRange(groupStageTeams.first, groupStageTeams.last) - let runningGroupStages = groupStages().filter({ $0.isRunning() }) if groupStagesAreOver() { return ("terminées", cut) } + let groupStages = groupStages() + let runningGroupStages = groupStages.filter({ $0.isRunning() }) if runningGroupStages.isEmpty { let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }) if ongoingGroupStages.isEmpty == false { return ("Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut) } - return (groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix, cut) + return (groupStages.count.formatted() + " poule" + groupStages.count.pluralSuffix, cut) } else { return ("Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut) } @@ -2146,7 +2174,7 @@ defer { } func allLoserRoundMatches() -> [Match] { - rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } + rounds().flatMap { $0.allLoserRoundMatches() } } func seedsCount() -> Int { @@ -2236,47 +2264,44 @@ defer { } func addNewRound(_ roundIndex: Int) async { - let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat) - let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) - let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) - let nextRound = round.nextRound() - var currentIndex = 0 - let matches = (0.. Bool { - return false if tournamentStore?.store.fileCollectionsAllLoaded() == false { return false } -#if _DEBUGING_TIME //DEBUGING TIME +#if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 8214bb7..291f6ba 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -110,7 +110,7 @@ extension Date { } static var firstDayOfWeek = Calendar.current.firstWeekday - static var capitalizedFirstLettersOfWeekdays: [String] { + static var capitalizedFirstLettersOfWeekdays: [String] = { let calendar = Calendar.current // let weekdays = calendar.shortWeekdaySymbols @@ -129,9 +129,9 @@ extension Date { } } return weekdays.map { $0.capitalized } - } + }() - static var fullMonthNames: [String] { + static var fullMonthNames: [String] = { let dateFormatter = DateFormatter() dateFormatter.locale = Locale.current @@ -140,7 +140,7 @@ extension Date { let date = Calendar.current.date(from: DateComponents(year: 2000, month: month, day: 1)) return date.map { dateFormatter.string(from: $0) } } - } + }() var startOfMonth: Date { Calendar.current.dateInterval(of: .month, for: self)!.start diff --git a/PadelClub/Extensions/NumberFormatter+Extensions.swift b/PadelClub/Extensions/NumberFormatter+Extensions.swift index 691a7f5..7225048 100644 --- a/PadelClub/Extensions/NumberFormatter+Extensions.swift +++ b/PadelClub/Extensions/NumberFormatter+Extensions.swift @@ -8,13 +8,13 @@ import Foundation extension NumberFormatter { - static var ordinal: NumberFormatter { + static var ordinal: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .ordinal return formatter - } + }() - static var standard: NumberFormatter { + static var standard: NumberFormatter = { return NumberFormatter() - } + }() } diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index cd4a2af..0e4fe1d 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -316,9 +316,9 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { self.init(rawValue: value) } - static var assimilationAllCases: [TournamentLevel] { + static var assimilationAllCases: [TournamentLevel] = { return [.p25, .p100, .p250, .p500, .p1000, .p1500, .p2000] - } + }() var entryFee: Double? { switch self { @@ -1267,9 +1267,9 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { } } - static var allCases: [MatchFormat] { + static var allCases: [MatchFormat] = { [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint] - } + }() func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { scoreTeamOne >= scoreTeamTwo ? .one : .two diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index 0f98f01..e5e96a4 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -18,6 +18,17 @@ struct MatchListView: View { @State var isExpanded: Bool = true + init(section: String, matches: [Match]?, hideWhenEmpty: Bool = false, isExpanded: Bool = true) { + self.section = section + self.matches = matches + self.hideWhenEmpty = hideWhenEmpty + if let matches, matches.count > 10 { + _isExpanded = .init(wrappedValue: false) + } else { + _isExpanded = .init(wrappedValue: isExpanded) + } + } + private func _shouldHide() -> Bool { if matches != nil && matches!.isEmpty && hideWhenEmpty == true { return true diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index e74bd44..7d8d4fb 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -23,65 +23,13 @@ struct MatchRowView: View { if isEditingTournamentSeed.wrappedValue == true && match.isGroupStage() == false && match.disabled == false { MatchSetupView(match: match) } else { -// MatchSummaryView(match: match, matchViewStyle: matchViewStyle) -// .overlay { -// if match.disabled { -// Image(systemName: "xmark") -// .resizable() -// .scaledToFit() -// .opacity(0.8) -// } -// } -// .contextMenu(menuItems: { -// Text("index: \(match.index)") -// Text("bye state : \(match.byeState)") -// Text("disable state : \(match.disabled)") -// Button("enable") { -// match._toggleMatchDisableState(false) -// } -// Button("disable") { -// match._toggleMatchDisableState(true) -// } -// Button("bye") { -// match.byeState = true -// } -// Button("not bye") { -// match.byeState = false -// } -// Button("solo toggle") { -// match.disabled.toggle() -// } -// Button("toggle fwrd match") { -// match._toggleForwardMatchDisableState(true) -// } -// Button("toggle loser match") { -// match._toggleLoserMatchDisableState(true) -// } -// }) - NavigationLink { - MatchDetailView(match: match, updatedField: updatedField) + if match.disabled == false { + MatchDetailView(match: match, updatedField: updatedField) + } } label: { MatchSummaryView(match: match, title: title, updatedField: updatedField) .contextMenu { - Section { - ForEach(match.teams().flatMap({ $0.players() })) { player in - Button { - player.hasArrived.toggle() - do { - try player.tournamentStore?.playerRegistrations.addOrUpdate(instance: player) - } catch { - Logger.error(error) - } - } label: { - Label(player.playerLabel(), systemImage: player.hasArrived ? "checkmark" : "xmark") - } - } - } header: { - Text("Présence") - } - - Divider() NavigationLink { EditSharingView(match: match) } label: { @@ -89,7 +37,6 @@ struct MatchRowView: View { } } } - //.modifier(BroadcastViewModifier(isBroadcasted: match.isBroadcasted())) } } } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 51942d3..d0aa674 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -23,28 +23,38 @@ struct MatchSummaryView: View { self.match = match self.updatedField = updatedField - let runningMatches = DataStore.shared.runningMatches() - - let currentAvailableCourts = match.availableCourts(runningMatches: runningMatches) - self.availableCourts = currentAvailableCourts - - if let groupStage = match.groupStageObject { - self.roundTitle = groupStage.groupStageTitle(.title) - } else if let round = match.roundObject { - self.roundTitle = round.roundTitle(.short) + if match.disabled == false { + let runningMatches = DataStore.shared.runningMatches() + //let runningMatches = [Match]() + + let currentAvailableCourts = match.availableCourts(runningMatches: runningMatches) + self.availableCourts = currentAvailableCourts + + if let groupStage = match.groupStageObject { + self.roundTitle = groupStage.groupStageTitle(.title) + } else if let round = match.roundObject { + self.roundTitle = round.roundTitle(.short) + } else { + self.roundTitle = nil + } + + self.matchTitle = title ?? match.matchTitle(.short) + + if match.hasEnded() == false, let court = match.courtName() { + self.courtName = court + } else { + self.courtName = nil + } + self.estimatedStartDate = match.estimatedStartDate(availableCourts: currentAvailableCourts, runningMatches: runningMatches) + self.canBePlayedInSpecifiedCourt = match.canBePlayedInSpecifiedCourt(runningMatches: runningMatches) } else { + self.matchTitle = "" + self.availableCourts = [] + self.canBePlayedInSpecifiedCourt = true self.roundTitle = nil - } - - self.matchTitle = title ?? match.matchTitle(.short) - - if match.hasEnded() == false, let court = match.courtName() { - self.courtName = court - } else { self.courtName = nil + self.estimatedStartDate = nil } - self.estimatedStartDate = match.estimatedStartDate(availableCourts: currentAvailableCourts, runningMatches: runningMatches) - self.canBePlayedInSpecifiedCourt = match.canBePlayedInSpecifiedCourt(runningMatches: runningMatches) } var spacing: CGFloat { diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index 5026317..5f1ad8a 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -73,7 +73,11 @@ struct LoserRoundSettingsView: View { Section { RowButtonView("Effacer les matchs de classements", role: .destructive) { - upperBracketRound.round.deleteLoserBracket() + await _deleteLoserBracketMatches() + } + } footer: { + if upperBracketRound.round.index > 4 { + Text("Cela peut prendre une ou deux minutes.") } } .disabled(upperBracketRound.round.loserRounds().isEmpty) @@ -82,6 +86,10 @@ struct LoserRoundSettingsView: View { RowButtonView("Créer les matchs de classements", role: .destructive) { await _addLoserBracketMatches() } + } footer: { + if upperBracketRound.round.index > 4 { + Text("Cela peut prendre une ou deux minutes.") + } } .disabled(upperBracketRound.round.loserRounds().isEmpty == false) @@ -138,20 +146,25 @@ struct LoserRoundSettingsView: View { + Text(" Modifier quand même ?").foregroundStyle(.red) } + + private func _deleteLoserBracketMatches() async { + await MainActor.run { + self.upperBracketRound.round.deleteLoserBracket() + self.upperBracketRound.loserRounds = [] + isEditingTournamentSeed.wrappedValue.toggle() + } + } private func _addLoserBracketMatches() async { - upperBracketRound.round.buildLoserBracket() - upperBracketRound.round.disabledMatches().forEach { match in - match._toggleLoserMatchDisableState(true) + await MainActor.run { + self.upperBracketRound.round.buildLoserBracket() + self.upperBracketRound.round.loserRounds().forEach { loserRound in + loserRound.disableUnplayedLoserBracketMatches() + } } - do { - try self.tournament.tournamentStore?.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) - } catch { - Logger.error(error) + self.upperBracketRound.loserRounds = await self.upperBracketRound._prepareLoserRounds() + await MainActor.run { + isEditingTournamentSeed.wrappedValue.toggle() } } } - -//#Preview { -// LoserRoundSettingsView() -//} diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index dd4cd71..ec8b126 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -11,36 +11,28 @@ import LeStorage struct LoserRoundView: View { @Environment(Tournament.self) var tournament: Tournament @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed - let loserBracket: LoserRound + + // State to hold the fetched and filtered matches + @State private var matchesForRounds: [String: [Match]] = [:] - private func _roundDisabled() -> Bool { -#if _DEBUG_TIME //DEBUGING TIME - let start = Date() - defer { - let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - print("func _roundDisabled", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) - } -#endif - return loserBracket.allMatches.allSatisfy({ $0.disabled == true }) - } - - private func _matches(loserRoundId: String?) -> [Match] { - return loserBracket.allMatches.filter { $0.round == loserRoundId && (isEditingTournamentSeed.wrappedValue == true || (isEditingTournamentSeed.wrappedValue == false && $0.disabled == false)) }.sorted(by: \.index) + // Derive rounds from loserBracket to avoid re-accessing property + private var rounds: [Round] { + loserBracket.rounds } - + var body: some View { List { if isEditingTournamentSeed.wrappedValue == true { _editingView() } - - ForEach(loserBracket.rounds) { loserRound in - let matches = _matches(loserRoundId: loserRound.id) - if matches.isEmpty == false { + + ForEach(rounds.indices, id: \.self) { loserRoundIndex in + let loserRound = rounds[loserRoundIndex] + if let matches = matchesForRounds[loserRound.id], !matches.isEmpty, (matches.anySatisfy({ $0.disabled == false }) || isEditingTournamentSeed.wrappedValue) { Section { ForEach(matches) { match in - MatchRowView(match: match) + MatchRowView(match: match, title: "") .matchViewStyle(.sectionedStandardStyle) .overlay { if match.disabled && isEditingTournamentSeed.wrappedValue == true { @@ -60,24 +52,36 @@ struct LoserRoundView: View { } } header: { HStack { - if let seedInterval = loserRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { + if isEditingTournamentSeed.wrappedValue == true { + let base = self.loserBracket.upperBracketRound.index + let turnIndex = self.loserBracket.turnIndex + let roundIndex = loserRoundIndex + let seedInterval = getLoserBracketPlacementInterval(base: base, turnIndex: turnIndex, loserRoundIndex: roundIndex) Text(seedInterval.localizedLabel(.wide)) let seedIntervalPointRange = seedInterval.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournament.teamCount) Spacer() Text(seedIntervalPointRange) .font(.caption) } else { - if let previousRound = loserRound.previousRound() { - if let seedInterval = previousRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { - Text(seedInterval.localizedLabel()) - } else { - Text("seedInterval is missing (previous round)") - } - } else if let parentRound = loserRound.parentRound { - if let seedInterval = parentRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { - Text(seedInterval.localizedLabel()) - } else { - Text("seedInterval is missing (parent round)") + if let seedInterval = loserRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { + Text(seedInterval.localizedLabel(.wide)) + let seedIntervalPointRange = seedInterval.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournament.teamCount) + Spacer() + Text(seedIntervalPointRange) + .font(.caption) + } else { + if let previousRound = loserRound.previousRound() { + if let seedInterval = previousRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { + Text(seedInterval.localizedLabel()) + } else { + Text("seedInterval is missing (previous round)") + } + } else if let parentRound = loserRound.parentRound { + if let seedInterval = parentRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { + Text(seedInterval.localizedLabel()) + } else { + Text("seedInterval is missing (parent round)") + } } } } @@ -85,17 +89,21 @@ struct LoserRoundView: View { } } } - - /* - let shouldDisplayLoserRounds : Bool = isEditingTournamentSeed.wrappedValue == true ? true : (allMatches.first(where: { $0.disabled == false }) != nil) - - if shouldDisplayLoserRounds { - } else { - Section { - ContentUnavailableView("Aucun match joué", systemImage: "tennisball", description: Text("Il n'y aucun match à jouer dans ce tour de match de classement.")) + } + .task { + if loserBracket.allMatches.isEmpty { + let allMatches = await _prepareLoserRoundMatches() + loserBracket.allMatches = allMatches + if isEditingTournamentSeed.wrappedValue { + updateDisplayedMatches() } } - */ + } + .onAppear(perform: { + updateDisplayedMatches() + }) + .onChange(of: isEditingTournamentSeed.wrappedValue) { + updateDisplayedMatches() } .headerProminence(.increased) .toolbar { @@ -110,27 +118,75 @@ struct LoserRoundView: View { } } } - + + func getLoserBracketPlacementInterval(base: Int, turnIndex: Int, loserRoundIndex: Int) -> SeedInterval { + // Number of teams that lost in the upper bracket round 'base' + let numLosers = Int(pow(2.0, Double(base))) //128 + let group = numLosers / Int(pow(2.0, Double(turnIndex))) //128 / 64 / 32 / 16 / 8 / 2 + + //print(base, turnIndex, loserRoundIndex, numLosers, group) + return SeedInterval(first: numLosers + group * loserRoundIndex + 1, last: numLosers + group * (loserRoundIndex + 1)) + } + + private func updateDisplayedMatches() { +#if _DEBUG_TIME //DEBUGING TIME +let start = Date() +defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func updateDisplayedMatches", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) +} +#endif + + if isEditingTournamentSeed.wrappedValue == true { + matchesForRounds = Dictionary(grouping: loserBracket.allMatches, by: \.round!) + } else { + matchesForRounds = Dictionary(grouping: loserBracket.enabledMatches, by: \.round!) + } + } + private func _editingView() -> some View { if _roundDisabled() { RowButtonView("Jouer ce tour", role: .destructive) { - loserBracket.rounds.forEach { round in - round.enableRound() - } + rounds.forEach { $0.enableRound() } } } else { RowButtonView("Ne pas jouer ce tour", role: .destructive) { - loserBracket.rounds.forEach { round in - round.disableRound() - } + rounds.forEach { $0.disableRound() } } } } - + + private func _roundDisabled() -> Bool { + #if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func _roundDisabled", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return loserBracket.enabledMatches.isEmpty + } + + private func _prepareLoserRoundMatches() async -> [Match] { + return await Task.detached(priority: .background) { + #if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func _prepareLoserRoundMatches", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + let matches = await MainActor.run { + let m = self.loserBracket.gatherAllMatches() + return m + } + return matches + }.value + } + private func _refreshNames() { DispatchQueue.global(qos: .background).async { - - let allRoundMatches = loserBracket.allMatches.filter({ $0.name == nil }) + let allRoundMatches = loserBracket.enabledMatches.filter({ $0.name == nil }) allRoundMatches.forEach({ $0.setMatchName($0.roundTitle()) }) do { try self.tournament.tournamentStore?.matches.addOrUpdate(contentOfs: allRoundMatches) diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index f1ac8bb..94eea97 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -10,25 +10,67 @@ import SwiftUI class UpperRound: Identifiable, Selectable { var id: String { round.id } let round: Round - var loserRounds: [LoserRound] { - LoserRound.updateDestinations(fromLoserRounds: round.loserRounds(), inUpperBracketRound: round) - } + var loserRounds: [LoserRound] = [] let title: String let playedMatches: [Match] + var correspondingLoserRoundTitle: String init(round: Round) { self.round = round - self.title = round.roundTitle(.short) + let title = round.roundTitle(.short) + self.title = title self.playedMatches = round.playedMatches() + self.correspondingLoserRoundTitle = "Match de classement \(title)" } - func loserMatches() -> [Match] { - loserRounds.flatMap({ $0.allMatches }).filter({ $0.disabled == false }) + func _prepareLoserRounds() async -> [LoserRound] { + return await Task.detached(priority: .background) { + return self.round.loserBracketTurns() + }.value + } + + static func getActiveUpperRound(in upperRounds: [UpperRound]) -> UpperRound? { + for round in upperRounds { + let playedMatches = round.playedMatches + + // Optimization: If no matches have started in this round, return nil immediately + if !playedMatches.contains(where: { $0.hasStarted() }) { + return round + } + + if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) { + return round + } + } + return nil } - func status() -> (Int, Int) { - let loserMatches = loserMatches() - return (loserMatches.filter { $0.hasEnded() }.count, loserMatches.count) + func hasStarted() -> Bool { + return playedMatches.anySatisfy({ $0.hasStarted() }) + } + + func loserMatches() -> [Match] { + // Early return if no loser rounds + guard let store = round.tournamentStore else { return [] } + + // Combine all round IDs from all loser rounds into one Set for efficiency + // Since each loserRound already has a roundIds Set, we merge them + let allRoundIds = self.round.loserRoundsAndChildren().map { $0.id } + + // Filter matches directly from the store in one pass + return store.matches.filter { match in + // Only include non-disabled matches from loser rounds + match.disabled == false && + match.round != nil && + allRoundIds.contains(match.round!) + } + } + + func status() async -> (Int, Int) { + return await Task.detached(priority: .background) { + let loserMatches = self.loserMatches() + return (loserMatches.filter { $0.hasEnded() }.count, loserMatches.count) + }.value } } @@ -69,58 +111,81 @@ extension UpperRound: Equatable { } -struct LoserRound: Identifiable, Selectable { +class LoserRound: Identifiable, Selectable { let turnIndex: Int - let rounds: [Round] - let allMatches: [Match] - - init(turnIndex: Int, rounds: [Round]) { - self.turnIndex = turnIndex - self.rounds = rounds - self.allMatches = rounds.flatMap { $0.playedMatches() } - } - - var id: Int { - return turnIndex - } - - var shouldBeDisplayed: Bool { + let upperBracketRound: Round + var rounds: [Round] = [] + var roundIDs: Set = Set() + var enabledMatches: [Match] = [] + var allMatches: [Match] = [] + + init(roundIndex: Int, turnIndex: Int, upperBracketRound: Round) { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - print("func shouldBeDisplayed loserRound", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + print("init LoserRound()", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - - return allMatches.first(where: { $0.disabled == false }) != nil + self.turnIndex = turnIndex + self.upperBracketRound = upperBracketRound + let rounds = upperBracketRound.loserRounds(forRoundIndex: roundIndex) + let roundIDs = Set(rounds.map { $0.id }) + self.rounds = rounds + self.roundIDs = roundIDs + self.enabledMatches = upperBracketRound.tournamentStore?.matches.filter { match in + // Match must be disabled and its round ID must be in our rounds collection + match.disabled == false && + match.round != nil && + roundIDs.contains(match.round!) + } ?? [] + } + + var tournamentStore: TournamentStore? { + return upperBracketRound.tournamentStore } - static func updateDestinations(fromLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [LoserRound] { + func gatherAllMatches() -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - print("func updateDestinations(fromLoserRounds", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + print("func allMatches", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } -#endif - var rounds = [LoserRound]() - let allLoserRounds = upperBracketRound.loserRoundsAndChildren() - for (index, round) in loserRounds.enumerated() { - rounds.append(LoserRound(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index, loserRoundsAndChildren: allLoserRounds))) +#endif + guard let store = self.tournamentStore else { return [] } + let roundIDs = roundIDs + // Create and return the filtered matches + return store.matches.filter { match in + // Match must be disabled and its round ID must be in our rounds collection + match.round != nil && + roundIDs.contains(match.round!) } - - return rounds } - static func enabledLoserRounds(inLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [Round] { - let allLoserRounds = upperBracketRound.loserRoundsAndChildren() - return loserRounds.filter { loserRound in - upperBracketRound.loserRounds(forRoundIndex: loserRound.index, loserRoundsAndChildren: allLoserRounds).anySatisfy({ $0.isDisabled() == false }) + var shouldBeDisplayed: Bool { +#if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func shouldBeDisplayed turnIndex", turnIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } +#endif + guard let store = self.tournamentStore else { return false } + let roundIDs = roundIDs + + // Create and return the filtered matches + return store.matches.first(where: { match in + // Match must be disabled and its round ID must be in our rounds collection + match.round != nil && + roundIDs.contains(match.round!) && + match.disabled == false + }) != nil } - + var id: Int { + return turnIndex + } } extension LoserRound: Equatable { @@ -137,7 +202,14 @@ extension LoserRound: Equatable { } func badgeValue() -> Int? { - let runningMatches: [Match] = allMatches.filter { $0.disabled == false && $0.isRunning() } +#if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func badgeValue turnIndex", turnIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } +#endif + let runningMatches: [Match] = enabledMatches.filter { $0.isRunning() } return runningMatches.count } @@ -150,45 +222,70 @@ extension LoserRound: Equatable { let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - print("func badgeImage loserRound", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + print("func badgeImage turnIndex", turnIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter { $0.disabled == false }.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil + return enabledMatches.isEmpty == false && enabledMatches.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil } } struct LoserRoundsView: View { - var upperBracketRound: UpperRound - @State private var selectedRound: LoserRound? + let upperBracketRound: UpperRound + @State private var selectedRound: LoserRound? = nil @State private var isEditingTournamentSeed = false - + @State private var displayedLoserRounds: [LoserRound] = [] // Initialize as empty + @State private var isLoading: Bool = false + init(upperBracketRound: UpperRound) { self.upperBracketRound = upperBracketRound - _selectedRound = State(wrappedValue: upperBracketRound.loserRounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? upperBracketRound.loserRounds.first(where: { $0.shouldBeDisplayed })) - - if upperBracketRound.loserRounds.allSatisfy({ $0.shouldBeDisplayed == false }) { - _isEditingTournamentSeed = .init(wrappedValue: true) - } } - var destinations: [LoserRound] { - isEditingTournamentSeed ? upperBracketRound.loserRounds : upperBracketRound.loserRounds.filter({ $0.shouldBeDisplayed }) + func updateDisplayedRounds() { + displayedLoserRounds = isEditingTournamentSeed ? upperBracketRound.loserRounds : upperBracketRound.loserRounds.filter({ $0.shouldBeDisplayed }) + + if displayedLoserRounds.isEmpty { + selectedRound = nil + } + + if let selectedRound, displayedLoserRounds.contains(where: { $0.id == selectedRound.id }) == false { + self.selectedRound = nil + } } - + var body: some View { VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: true) - switch selectedRound { - case .some(let selectedRound): - LoserRoundView(loserBracket: selectedRound) - default: - LoserRoundSettingsView(upperBracketRound: upperBracketRound) + if isLoading { + ProgressView() + } else { + GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: displayedLoserRounds, nilDestinationIsValid: true) + switch selectedRound { + case .some(let selectedRound): + LoserRoundView(loserBracket: selectedRound).id(selectedRound.id) + default: + LoserRoundSettingsView(upperBracketRound: upperBracketRound) + } + } + } + .task { + if displayedLoserRounds.isEmpty { + self.isLoading = true + self.upperBracketRound.loserRounds = await self.upperBracketRound._prepareLoserRounds() + self.selectedRound = self.upperBracketRound.loserRounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? self.upperBracketRound.loserRounds.first(where: { $0.shouldBeDisplayed }) + if self.upperBracketRound.loserRounds.anySatisfy({ $0.shouldBeDisplayed == true }) == false { + isEditingTournamentSeed = true + } + self.updateDisplayedRounds() + self.isLoading = false } } + .onChange(of: isEditingTournamentSeed) { + updateDisplayedRounds() + } .environment(\.isEditingTournamentSeed, $isEditingTournamentSeed) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle(upperBracketRound.correspondingLoserRoundTitle) } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index ed2ca61..5216ff4 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -159,15 +159,13 @@ struct RoundSettingsView: View { } private func _removeRound(_ lastRound: Round) async { - do { + await MainActor.run { let teams = lastRound.seeds() teams.forEach { team in team.resetBracketPosition() } - try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) - try tournamentStore?.rounds.delete(instance: lastRound) - } catch { - Logger.error(error) + tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) + tournamentStore?.rounds.delete(instance: lastRound) } } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index e710353..c85cadf 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -19,15 +19,19 @@ struct RoundView: View { @State private var selectedSeedGroup: SeedInterval? @State private var showPrintScreen: Bool = false @State private var hideNames: Bool = true + @State private var disabledMatchesCount: Int? + @State private var loserBracketStatus: (Int, Int)? + @State private var correspondingLoserRoundTitle: String? + var upperRound: UpperRound init(upperRound: UpperRound) { self.upperRound = upperRound - let seeds = upperRound.round.seeds() - SlideToDeleteSeedTip.seeds = seeds.count - PrintTip.seeds = seeds.count - BracketEditTip.matchesHidden = upperRound.round.getDisabledMatches().count +// let seeds = upperRound.round.seeds() +// SlideToDeleteSeedTip.seeds = seeds.count +// PrintTip.seeds = seeds.count +// BracketEditTip.matchesHidden = upperRound.round.getDisabledMatches().count } var tournamentStore: TournamentStore? { @@ -35,14 +39,14 @@ struct RoundView: View { } private var spaceLeft: [Match] { - let displayableMatches: [Match] = self.upperRound.round.playedMatches() + let displayableMatches: [Match] = self.upperRound.playedMatches return displayableMatches.filter { match in match.teamScores.count == 1 }.filter({ $0.isValidSpot() }) } private var seedSpaceLeft: [Match] { - let displayableMatches: [Match] = self.upperRound.round.playedMatches() + let displayableMatches: [Match] = self.upperRound.playedMatches return displayableMatches.filter { match in match.teamScores.count == 0 }.filter({ $0.isValidSpot() }) @@ -63,21 +67,20 @@ struct RoundView: View { var body: some View { List { - let displayableMatches = upperRound.round.playedMatches().sorted(by: \.index) + let displayableMatches = upperRound.playedMatches if displayableMatches.isEmpty { Section { ContentUnavailableView("Aucun match dans cette manche", systemImage: "tennisball") } } - let disabledMatchesCount = BracketEditTip.matchesHidden - if disabledMatchesCount > 0 { + if let disabledMatchesCount, disabledMatchesCount > 0 { let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) TipView(bracketTip).tipStyle(tint: .green, asSection: true) let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) - if upperRound.round.hasStarted() == false, leftToPlay >= 0 { + if upperRound.hasStarted() == false, leftToPlay >= 0 { Section { LabeledContent { Text(leftToPlay.formatted()) @@ -99,26 +102,31 @@ struct RoundView: View { .tipStyle(tint: .master, asSection: true) if upperRound.round.index > 0 { - let correspondingLoserRoundTitle = upperRound.round.correspondingLoserRoundTitle() Section { NavigationLink { LoserRoundsView(upperBracketRound: upperRound) .environment(tournament) - .navigationTitle(correspondingLoserRoundTitle) } label: { LabeledContent { - let status = upperRound.status() - if status.0 == status.1 { - if status.0 == 0 { - Text("aucun match") + if let loserBracketStatus { + if loserBracketStatus.0 == loserBracketStatus.1 { + if loserBracketStatus.0 == 0 { + Text("aucun match") + } else { + Image(systemName: "checkmark").foregroundStyle(.green) + } } else { - Image(systemName: "checkmark").foregroundStyle(.green) + Text("\(loserBracketStatus.0) terminé\(loserBracketStatus.0.pluralSuffix) sur \(loserBracketStatus.1)") } } else { - Text("\(status.0) terminé\(status.0.pluralSuffix) sur \(status.1)") + ProgressView() } } label: { - Text(correspondingLoserRoundTitle) + if let correspondingLoserRoundTitle { + Text(correspondingLoserRoundTitle) + } else { + Text(upperRound.correspondingLoserRoundTitle) + } } } } header: { @@ -243,6 +251,21 @@ struct RoundView: View { } } } + .task { + await MainActor.run { + let seeds = self.upperRound.round.seeds() + SlideToDeleteSeedTip.seeds = seeds.count + PrintTip.seeds = seeds.count + self.disabledMatchesCount = self.upperRound.round.getDisabledMatches().count + BracketEditTip.matchesHidden = self.disabledMatchesCount ?? 0 + } + await MainActor.run { + let correspondingLoserRoundTitle = self.upperRound.round.correspondingLoserRoundTitle() + self.correspondingLoserRoundTitle = correspondingLoserRoundTitle + self.upperRound.correspondingLoserRoundTitle = correspondingLoserRoundTitle + } + self.loserBracketStatus = await self.upperRound.status() + } .navigationDestination(isPresented: $showPrintScreen) { PrintSettingsView(tournament: tournament) } diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index db02f53..fa49dc2 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -15,15 +15,23 @@ struct RoundsView: View { let destinations: [UpperRound] init(tournament: Tournament) { +#if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func RoundsView", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } +#endif self.tournament = tournament - let _destinations = tournament.rounds().map { UpperRound(round: $0) } + let rounds = tournament.rounds() + let _destinations = rounds.map { UpperRound(round: $0) } self.destinations = _destinations - let availableSeeds = tournament.availableSeeds() if tournament.shouldVerifyBracket { _selectedRound = State(wrappedValue: nil) } else { - _selectedRound = State(wrappedValue: _destinations.first(where: { $0.id == tournament.getActiveRound()?.id })) + _selectedRound = State(wrappedValue: _destinations.first(where: { $0.id == UpperRound.getActiveUpperRound(in: _destinations)?.id })) } + let availableSeeds = tournament.availableSeeds() if availableSeeds.isEmpty == false || tournament.availableQualifiedTeams().isEmpty == false { _isEditingTournamentSeed = State(wrappedValue: true) }