fix slow stuff

online_payment
Raz 7 months ago
parent 34b72b4d66
commit d24576eb3e
  1. 40
      PadelClub/Data/DataStore.swift
  2. 24
      PadelClub/Data/Match.swift
  3. 277
      PadelClub/Data/Round.swift
  4. 130
      PadelClub/Data/Tournament.swift
  5. 8
      PadelClub/Extensions/Date+Extensions.swift
  6. 8
      PadelClub/Extensions/NumberFormatter+Extensions.swift
  7. 8
      PadelClub/Utils/PadelRule.swift
  8. 11
      PadelClub/Views/Components/MatchListView.swift
  9. 59
      PadelClub/Views/Match/MatchRowView.swift
  10. 46
      PadelClub/Views/Match/MatchSummaryView.swift
  11. 37
      PadelClub/Views/Round/LoserRoundSettingsView.swift
  12. 158
      PadelClub/Views/Round/LoserRoundView.swift
  13. 219
      PadelClub/Views/Round/LoserRoundsView.swift
  14. 8
      PadelClub/Views/Round/RoundSettingsView.swift
  15. 61
      PadelClub/Views/Round/RoundView.swift
  16. 14
      PadelClub/Views/Round/RoundsView.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!
}
}

@ -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] {

@ -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..<roundCount { // Assuming no tournament has >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..<roundCount).map { //index 0 is the final
let round = Round(tournament: tournament, index: $0, matchFormat: loserBracketMatchFormat)
round.parent = id //parent
//titles[round.id] = round.roundTitle(initialMode: true)
return round
}
self.tournamentStore?.rounds.addOrUpdate(contentOfs: rounds)
tournamentStore.rounds.addOrUpdate(contentOfs: rounds)
let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount)
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, format: loserBracketMatchFormat, name: round.roundTitle(initialMode: true))
//let title = titles[round.id]
return Match(round: round.id, index: $0, format: loserBracketMatchFormat)
//initial mode let the roundTitle give a name without considering the playable match
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
tournamentStore.matches.addOrUpdate(contentOfs: matches)
loserRounds().forEach { round in
rounds.forEach { round in
round.buildLoserBracket()
}
}
func disableUnplayedLoserBracketMatches() {
let m = self._matches()
if let previousRound = self.previousRound() {
m.forEach { match in
let prmc = previousRound.previousMatches(previousRound: previousRound, match: match).filter({
$0.disabled
}).count
let byeprmc = previousRound.previousMatches(previousRound: previousRound, match: match).filter({ $0.byeState }).count
if byeprmc == 2 {
match.disabled = false
} else {
if prmc == 2 {
match.disabled = true
if byeprmc == 1 {
match.byeState = true
}
} else if prmc == 1 {
match.byeState = true
match.disabled = true
} else {
match.disabled = false
}
}
}
} else if let upperRound = self.parentRound {
m.forEach { match in
let prmc = self.upperMatches(upperRound: upperRound, match: match).filter({ $0.disabled }).count
if prmc > 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..<roundCount {
let lr = LoserRound(roundIndex: roundCount - index - 1, turnIndex: index, upperBracketRound: self)
rounds.append(lr)
}
return rounds
}
func invalidateCache() {
_cachedLoserRounds = nil
_cachedSeedInterval = nil
_cachedLoserRoundsAndChildren = nil
}
override func deleteDependencies() {
let matches = self._matches()
let matches = self._unsortedMatches(includeDisabled: true)
for match in matches {
match.deleteDependencies()
}
@ -775,7 +972,7 @@ defer {
func insertOnServer() {
self.tournamentStore?.rounds.writeChangeAndInsertOnServer(instance: self)
for match in self._matches() {
for match in self._unsortedMatches(includeDisabled: true) {
match.insertOnServer()
}
}

@ -510,22 +510,49 @@ defer {
func getActiveRound(withSeeds: Bool = false) -> 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..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, format: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore?.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
await MainActor.run {
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()
let tournamentStore = self.tournamentStore
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, format: round.matchFormat)
if let nextRound, let followingMatch = tournamentStore?.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
return match
}
tournamentStore?.rounds.addOrUpdate(instance: round)
tournamentStore?.matches.addOrUpdate(contentOfs: matches)
if round.index < 5 {
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
return match
}
do {
try tournamentStore?.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore?.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
@ -2340,11 +2365,10 @@ defer {
// MARK: - Status
func shouldTournamentBeOver() async -> 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)

@ -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

@ -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()
}
}()
}

@ -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

@ -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

@ -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()))
}
}
}

@ -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 {

@ -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()
//}

@ -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)

@ -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<String> = 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)
}
}

@ -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)
}
}
}

@ -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)
}

@ -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)
}

Loading…
Cancel
Save