diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 24c98c6..eea28e6 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -79,7 +79,6 @@ FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; - FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -147,6 +146,7 @@ FF5DA19B2BB9662200A33061 /* TournamentSeedEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */; }; FF6087EA2BE25EF1004E1E47 /* TournamentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */; }; FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; + FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; @@ -425,7 +425,6 @@ FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = ""; }; FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; - FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserGroupStageSettingsView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -493,6 +492,7 @@ FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSeedEditing.swift; sourceTree = ""; }; FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentStatusView.swift; sourceTree = ""; }; FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = ""; }; + FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = ""; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = ""; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = ""; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = ""; }; @@ -1196,7 +1196,7 @@ FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */, FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF5DA18E2BB9268800A33061 /* GroupStagesSettingsView.swift */, - FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */, + FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */, FF9AC3932BE3625D00C2E883 /* Components */, FF9AC3922BE3625200C2E883 /* Shared */, ); @@ -1594,7 +1594,6 @@ FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */, FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */, - FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */, FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */, C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */, FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */, @@ -1689,6 +1688,7 @@ FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, + FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */, FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index ac2d9c7..32e07b2 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -383,6 +383,11 @@ final class GroupStage: ModelObject, Storable { return data.joined(separator: "\n") } + + func finalPosition(ofTeam team: TeamRegistration) -> Int? { + guard hasEnded() else { return nil } + return teams(true).firstIndex(of: team) + } override func deleteDependencies() throws { let matches = self._matches() diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 148abb1..1e8c918 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -121,6 +121,10 @@ defer { print("func matchTitle", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif + + if roundObject?.groupStageLoserBracket == true { + return "\(index)\(index.ordinalFormattedSuffix()) place" + } if let groupStageObject { return groupStageObject.localizedMatchUpLabel(for: index) } @@ -361,9 +365,17 @@ defer { return false } - func _toggleMatchDisableState(_ state: Bool, forward: Bool = false) { + func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { //if disabled == state { return } disabled = state + + if disabled { + do { + try self.tournamentStore.teamScores.delete(contentOfs: teamScores) + } catch { + Logger.error(error) + } + } if state == true { let teams = teams() for team in teams { @@ -384,12 +396,14 @@ defer { Logger.error(error) } - _toggleLoserMatchDisableState(state) - if forward { - _toggleForwardMatchDisableState(state) - } else { - topPreviousRoundMatch()?._toggleMatchDisableState(state) - bottomPreviousRoundMatch()?._toggleMatchDisableState(state) + if single == false { + _toggleLoserMatchDisableState(state) + if forward { + _toggleForwardMatchDisableState(state) + } else { + topPreviousRoundMatch()?._toggleMatchDisableState(state) + bottomPreviousRoundMatch()?._toggleMatchDisableState(state) + } } } @@ -837,7 +851,22 @@ defer { } var isLoserBracket: Bool { - return roundObject?.parent != nil + if let roundObject { + if roundObject.parent != nil || roundObject.groupStageLoserBracket { + return true + } + } + return false + } + + var matchType: MatchType { + if isLoserBracket { + return .loserBracket + } else if isGroupStage() { + return .groupStage + } else { + return .bracket + } } enum CodingKeys: String, CodingKey { diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 27eee27..0cc15be 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -22,13 +22,15 @@ final class Round: ModelObject, Storable { var parent: String? private(set) var format: MatchFormat? var startDate: Date? + var groupStageLoserBracket: Bool = false - internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil) { + internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false) { self.tournament = tournament self.index = index self.parent = parent self.format = matchFormat self.startDate = startDate + self.groupStageLoserBracket = groupStageLoserBracket } // MARK: - Computed dependencies @@ -67,7 +69,7 @@ final class Round: ModelObject, Storable { } func hasEnded() -> Bool { - if parent == nil { + if isUpperBracket() { return playedMatches().anySatisfy({ $0.hasEnded() == false }) == false } else { return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false @@ -184,13 +186,13 @@ defer { case .two: if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { return luckyLoser.team - } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { + } else if groupStageLoserBracket == false, let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { if let teamId = previousMatch.winningTeamId { return self.tournamentStore.teamRegistrations.findById(teamId) } else if previousMatch.disabled { return previousMatch.teams().first } - } else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { + } else if groupStageLoserBracket == false, let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { return tournamentStore.findById(parent) } } @@ -292,7 +294,7 @@ defer { // } func playedMatches() -> [Match] { - if parent == nil { + if isUpperBracket() { return enabledMatches() } else { return _matches() @@ -477,15 +479,19 @@ defer { } #endif - if parent == nil { + if isUpperBracket() { if index == 0 { return SeedInterval(first: 1, last: 2) } let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { $0.bracketPosition != nil && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex } - let playedMatches = playedMatches() - let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) + + let playedMatches = playedMatches().count + let minimumMatches = initialMode ? RoundRule.numberOfMatches(forRoundIndex: index) : playedMatches * 2 + //print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count) + let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count) + //print(seedInterval.localizedLabel()) return seedInterval } @@ -496,7 +502,7 @@ defer { return previousRound.seedInterval(initialMode: initialMode) } } else if let parentRound { - if parentRound.parent == nil { + if parentRound.isUpperBracket() { return parentRound.seedInterval(initialMode: initialMode) } return parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last @@ -506,6 +512,10 @@ defer { } func roundTitle(_ displayStyle: DisplayStyle = .wide, initialMode: Bool = false) -> String { + if groupStageLoserBracket { + return "Classement Poules" + } + if parent != nil { if let seedInterval = seedInterval(initialMode: initialMode) { return seedInterval.localizedLabel(displayStyle) @@ -557,11 +567,11 @@ defer { } func isUpperBracket() -> Bool { - return parent == nil + return parent == nil && groupStageLoserBracket == false } func isLoserBracket() -> Bool { - return parent != nil + return parent != nil || groupStageLoserBracket } func deleteLoserBracket() { @@ -670,6 +680,18 @@ defer { case _parent = "parent" case _format = "format" case _startDate = "startDate" + case _groupStageLoserBracket = "groupStageLoserBracket" + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: ._id) + tournament = try container.decode(String.self, forKey: ._tournament) + index = try container.decode(Int.self, forKey: ._index) + parent = try container.decodeIfPresent(String.self, forKey: ._parent) + format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) + startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) + groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false } func encode(to encoder: Encoder) throws { @@ -678,7 +700,8 @@ defer { try container.encode(id, forKey: ._id) try container.encode(tournament, forKey: ._tournament) try container.encode(index, forKey: ._index) - + try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket) + if let parent = parent { try container.encode(parent, forKey: ._parent) } else { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 7ad53ff..9d9fa18 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -809,7 +809,7 @@ defer { } func rounds() -> [Round] { - let rounds: [Round] = self.tournamentStore.rounds.filter { $0.parent == nil } + let rounds: [Round] = self.tournamentStore.rounds.filter { $0.isUpperBracket() } return rounds.sorted(by: \.index).reversed() } @@ -1224,7 +1224,7 @@ defer { _removeStrings(from: &teams, stringsToRemove: disabledIds) teams[interval.last] = disabledIds let teamNames : [String] = disabledIds.compactMap { - let t : TeamRegistration? = Store.main.findById($0) + let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0) return t }.map { $0.canonicalName } print("winners.isEmpty", "\(interval.last) : ", teamNames) @@ -1237,7 +1237,7 @@ defer { _removeStrings(from: &teams, stringsToRemove: winners) teams[interval.first + winners.count - 1] = winners let teamNames : [String] = winners.compactMap { - let t: TeamRegistration? = Store.main.findById($0) + let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) return t }.map { $0.canonicalName } print("winners", "\(interval.last + winners.count - 1) : ", teamNames) @@ -1246,25 +1246,32 @@ defer { if losers.isEmpty == false { _removeStrings(from: &teams, stringsToRemove: losers) - teams[interval.last] = losers + teams[interval.first + winners.count] = losers let loserTeamNames : [String] = losers.compactMap { - let t: TeamRegistration? = Store.main.findById($0) + let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) return t }.map { $0.canonicalName } - print("losers", "\(interval.last) : ", loserTeamNames) + print("losers", "\(interval.first + winners.count) : ", loserTeamNames) losers.forEach { ids.insert($0) } } } } } + groupStageLoserBracket()?.playedMatches().forEach({ match in + if match.hasEnded() { + teams.setOrAppend(match.winningTeamId, at: match.index) + teams.setOrAppend(match.losingTeamId, at: match.index + 1) + } + }) + let groupStages = groupStages() let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - + let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) groupStages.forEach { groupStage in let groupStageTeams = groupStage.teams(true) for (index, team) in groupStageTeams.enumerated() { - if team.qualified == false { + if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) let _index = baseRank + groupStageWidth + 1 @@ -1934,10 +1941,7 @@ defer { } func tournamentWinner() -> TeamRegistration? { - let finals: Round? = self.tournamentStore.rounds.first(where: { $0.index == 0 && $0.parent == nil }) - -// let rounds: [Round] = Store.main.filter(isIncluded: { $0.index == 0 && $0.tournament == id && $0.parent == nil }) -// let final: Round? = .first + let finals: Round? = self.tournamentStore.rounds.first(where: { $0.index == 0 && $0.isUpperBracket() }) return finals?.playedMatches().first?.winner() } @@ -1998,6 +2002,14 @@ defer { return (Array(shouldBeInIt), Array(shouldNotBeInIt)) } + func groupStageLoserBracket() -> Round? { + tournamentStore.rounds.first(where: { $0.groupStageLoserBracket }) + } + + func groupStageLoserBracketsInitialPlace() -> Int { + selectedSortedTeams().filter({ $0.qualified || $0.bracketPosition != nil }).count + 1 + } + // MARK: - func insertOnServer() throws { diff --git a/PadelClub/Extensions/Array+Extensions.swift b/PadelClub/Extensions/Array+Extensions.swift index 62b8d4b..f1ab768 100644 --- a/PadelClub/Extensions/Array+Extensions.swift +++ b/PadelClub/Extensions/Array+Extensions.swift @@ -18,6 +18,16 @@ extension Array { return first(where: { p($0) }) != nil //return !self.allSatisfy { !p($0) } } + + // Check if the number of elements in the sequence is even + var isEven: Bool { + return self.count % 2 == 0 + } + + // Check if the number of elements in the sequence is odd + var isOdd: Bool { + return self.count % 2 != 0 + } } extension Array where Element: Equatable { @@ -49,3 +59,22 @@ extension Array where Element: CustomStringConvertible { } } + +extension Dictionary where Key == Int, Value == [String] { + mutating func setOrAppend(_ element: String?, at key: Int) { + // Check if the element is nil; do nothing if it is + guard let element = element else { + return + } + + // Check if the key exists in the dictionary + if var array = self[key] { + // If it exists, append the element to the array + array.append(element) + self[key] = array + } else { + // If it doesn't exist, create a new array with the element + self[key] = [element] + } + } +} diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 8c65472..9672531 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -291,6 +291,13 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { self.init(rawValue: value) } + + func pointsRange(first: Int, last: Int, teamsCount: Int) -> String { + let range = [points(for: last - 1, count: teamsCount), + points(for: first - 1, count: teamsCount)] + return range.map { $0.formatted(.number.sign(strategy: .always())) }.joined(separator: " / ") + " pts" + } + func hideWeight() -> Bool { switch self { case .unlisted: diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index 01e7519..32ab726 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -12,9 +12,7 @@ struct SeedInterval: Hashable, Comparable { let last: Int func pointsRange(tournamentLevel: TournamentLevel, teamsCount: Int) -> String { - let range = [tournamentLevel.points(for: last - 1, count: teamsCount), - tournamentLevel.points(for: first - 1, count: teamsCount)] - return range.map { $0.formatted(.number.sign(strategy: .always())) }.joined(separator: " / ") + " pts" + tournamentLevel.pointsRange(first: first, last: last, teamsCount: teamsCount) } static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool { diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index c82bdab..62c58c6 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -202,7 +202,7 @@ struct GroupStageView: View { VStack(alignment: .leading, spacing: 0) { Text("#\(index + 1)") .font(.caption) - TeamPickerView(groupStagePosition: index, teamPicked: { team in + TeamPickerView(groupStagePosition: index, matchTypeContext: .groupStage, teamPicked: { team in print(team.pasteData()) team.groupStage = groupStage.id team.groupStagePosition = index diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index 8b3770c..e0f29dc 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -72,6 +72,30 @@ struct GroupStagesSettingsView: View { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de vos poules et valider que tout est ok.") } } + + Section { + if tournament.groupStageLoserBracket() == nil { + RowButtonView("Ajouter des matchs de classements", role: .destructive) { + let round = Round(tournament: tournament.id, index: 0, matchFormat: tournament.loserRoundFormat, groupStageLoserBracket: true) + + do { + try tournamentStore.rounds.addOrUpdate(instance: round) + } catch { + Logger.error(error) + } + + } + } else if let groupStageLoserBracket = tournament.groupStageLoserBracket() { + RowButtonView("Supprimer les matchs de classements", role: .destructive) { + do { + try groupStageLoserBracket.deleteDependencies() + try tournamentStore.rounds.delete(instance: groupStageLoserBracket) + } catch { + Logger.error(error) + } + } + } + } #if DEBUG Section { @@ -81,12 +105,6 @@ struct GroupStagesSettingsView: View { } #endif -// NavigationLink { -// LoserGroupStageSettingsView(tournament: tournament) -// } label: { -// Text("Match de perdant de poules") -// } - Section { RowButtonView("Retirer tous les horaires", role: .destructive) { let matches = tournament.groupStages().flatMap({ $0._matches() }) @@ -155,6 +173,26 @@ struct GroupStagesSettingsView: View { } } } + + Section { + RowButtonView("Retirer tout le monde", role: .destructive) { + tournament.groupStages().forEach { groupStage in + let teams = groupStage.teams() + teams.forEach { team in + team.groupStagePosition = nil + team.groupStage = nil + groupStage._matches().forEach({ $0.updateTeamScores() }) + } + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) + } catch { + Logger.error(error) + } + } + } + } footer: { + Text("Toutes les équipes seront retirées et les scores des matchs seront perdus.") + } } .overlay(alignment: .bottom) { diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 2260f7d..bfc447e 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -18,12 +18,15 @@ struct GroupStagesView: View { } case all(Tournament) + case loserBracket(Round) case groupStage(GroupStage) var id: String { switch self { case .all: return "all-group-stage" + case .loserBracket(let loserBracket): + return loserBracket.id case .groupStage(let groupStage): return groupStage.id } @@ -33,6 +36,8 @@ struct GroupStagesView: View { switch self { case .all: return "Tout" + case .loserBracket: + return "Classement" case .groupStage(let groupStage): return groupStage.groupStageTitle() } @@ -42,6 +47,8 @@ struct GroupStagesView: View { switch self { case .all: return nil + case .loserBracket(let loserBracket): + return loserBracket.badgeValue() case .groupStage(let groupStage): return groupStage.badgeValue() } @@ -59,6 +66,8 @@ struct GroupStagesView: View { } else { return nil } + case .loserBracket(let loserBracket): + return loserBracket.badgeImage() case .groupStage(let groupStage): return groupStage.badgeImage() } @@ -69,9 +78,10 @@ struct GroupStagesView: View { tournament.groupStagesMatches() } + @State private var isEditingLoserBracketGroupStage: Bool + init(tournament: Tournament) { self.tournament = tournament - if tournament.shouldVerifyGroupStage { _selectedDestination = State(wrappedValue: nil) } else if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty { @@ -82,11 +92,15 @@ struct GroupStagesView: View { _selectedDestination = State(wrappedValue: .groupStage(gs)) } } + _isEditingLoserBracketGroupStage = .init(wrappedValue: tournament.groupStageLoserBracket()?._matches().isEmpty ?? false) } func allDestinations() -> [GroupStageDestination] { var allDestinations : [GroupStageDestination] = [.all(tournament)] let groupStageDestinations : [GroupStageDestination] = tournament.groupStages().map { GroupStageDestination.groupStage($0) } + if let loserBracket = tournament.groupStageLoserBracket() { + allDestinations.insert(.loserBracket(loserBracket), at: 0) + } allDestinations.append(contentsOf: groupStageDestinations) return allDestinations } @@ -142,6 +156,9 @@ struct GroupStagesView: View { .navigationTitle("Toutes les poules") case .groupStage(let groupStage): GroupStageView(groupStage: groupStage).id(groupStage.id) + case .loserBracket(let loserBracket): + LoserBracketFromGroupStageView(loserBracket: loserBracket).id(loserBracket.id) + .environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage) case nil: GroupStagesSettingsView() .navigationTitle("Réglages") diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift new file mode 100644 index 0000000..d3a4e09 --- /dev/null +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -0,0 +1,122 @@ +// +// LoserBracketFromGroupStageView.swift +// PadelClub +// +// Created by razmig on 07/09/2024. +// + +import SwiftUI +import LeStorage + +struct LoserBracketFromGroupStageView: View { + + @Environment(\.isEditingTournamentSeed) var isEditingTournamentSeed + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + + @State var loserBracket: Round + + var tournamentStore: TournamentStore { + return self.tournament.tournamentStore + } + + var body: some View { + List { + let displayableMatches = loserBracket.playedMatches().sorted(by: \.index) + + if isEditingTournamentSeed.wrappedValue == true { + Section { + RowButtonView("Ajouter un match", role: .destructive) { + let placeCount = tournament.groupStageLoserBracketsInitialPlace() + displayableMatches.count * 2 + let match = Match(round: loserBracket.id, index: placeCount, matchFormat: loserBracket.matchFormat) + match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix()) place" + do { + try tournamentStore.matches.addOrUpdate(instance: match) + } catch { + Logger.error(error) + } + } + } + } + + ForEach(displayableMatches) { match in + Section { + MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + } header: { + let tournamentTeamCount = tournament.teamCount + let seedIntervalPointRange = tournament.tournamentLevel.pointsRange(first: match.index, last: match.index + 1, teamsCount: tournamentTeamCount) + HStack { + Text(match.matchTitle(.wide)) + Spacer() + Text(seedIntervalPointRange) + .font(.caption) + } + } footer: { + if isEditingTournamentSeed.wrappedValue == true { + HStack { + if match.index > tournament.groupStageLoserBracketsInitialPlace() { + FooterButtonView("même place qu'au-dessus") { + match.index -= 2 + do { + try tournamentStore.matches.addOrUpdate(instance: match) + } catch { + Logger.error(error) + } + } + Spacer() + } + FooterButtonView("effacer", role: .destructive) { + do { + try match.deleteDependencies() + try tournamentStore.matches.delete(instance: match) + } catch { + Logger.error(error) + } + } + } + } + } + } + + Section { + if displayableMatches.isEmpty == false && isEditingTournamentSeed.wrappedValue == true { + Section { + RowButtonView("Effacer tous les matchs", role: .destructive) { + _deleteAllMatches() + } + } footer: { + Text("Efface tous les matchs de classement de poules ci-dessus.") + } + } + } + } + .headerProminence(.increased) + .navigationTitle("Classement de poules") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { + if isEditingTournamentSeed.wrappedValue == true { + isEditingTournamentSeed.wrappedValue = false + } else { + isEditingTournamentSeed.wrappedValue = true + } + } + } + } + } + + private func _deleteAllMatches() { + let displayableMatches = loserBracket.playedMatches().sorted(by: \.index) + + do { + for match in displayableMatches { + try match.deleteDependencies() + } + try tournamentStore.matches.delete(contentOfs: displayableMatches) + } catch { + Logger.error(error) + } + + } +} diff --git a/PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift b/PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift deleted file mode 100644 index 78ccdd0..0000000 --- a/PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// LoserGroupStageSettingsView.swift -// PadelClub -// -// Created by Razmig Sarkissian on 29/06/2024. -// - -import SwiftUI - -extension Round { - var isGroupStageLoserBracket: Bool { - return false - } -} - -extension Tournament { - func groupStageLoserBrackets() -> [Round] { - [] - } - - func removeGroupStageLoserBrackets() { - - } -} - -struct LoserGroupStageSettingsView: View { - var tournament: Tournament - @State private var loserGroupStageBracketType: Int? = nil - @State private var losers : Set = Set() - @Environment(\.editMode) private var editMode - - var body: some View { - List(selection: $losers) { - if tournament.groupStageLoserBrackets().isEmpty == false { - //for each all rounds without parent and loserGroupStage, ability to delete them - Section { - RowButtonView("Effacer", role: .destructive) { - tournament.removeGroupStageLoserBrackets() - } - } - } - - if self.editMode?.wrappedValue == .active { - Section { - //rajouter + toolbar valider / cancel - ForEach(tournament.groupStageTeams().filter({ $0.qualified == false })) { team in - TeamRowView(team: team).tag(team) - } - } header: { - Text("Sélection des perdants de poules") - } - } else { - Section { - RowButtonView("Ajouter un match de perdant") { - self.editMode?.wrappedValue = .active - } - } footer: { - Text("Permet d'ajouter un match de perdant de poules.") - } - } - } - .toolbar { - if self.editMode?.wrappedValue == .active { - ToolbarItem(placement: .topBarLeading) { - Button("Annuler") { - self.editMode?.wrappedValue = .inactive - } - } - - ToolbarItem(placement: .topBarTrailing) { - Button("Valider") { - self.editMode?.wrappedValue = .inactive - //tournament.createGroupStageLoserBracket() - } - } - } - } - .navigationTitle("Match de perdant de poules") - .navigationBarBackButtonHidden(self.editMode?.wrappedValue == .active) - .navigationBarTitleDisplayMode(.inline) - .toolbar(.visible, for: .navigationBar) - .headerProminence(.increased) - .toolbarBackground(.visible, for: .navigationBar) - } -} diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index cc3ea7b..444aa94 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -8,7 +8,7 @@ import SwiftUI struct PlayerBlockView: View { - var match: Match + @State var match: Match let teamPosition: TeamPosition let team: TeamRegistration? let color: Color @@ -52,7 +52,7 @@ struct PlayerBlockView: View { HStack { VStack(alignment: .leading) { if let names { - if let teamScore, teamScore.luckyLoser != nil { + if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false { Text("Repêchée").italic().font(.caption) } diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 65a98f7..2b3ce78 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -13,13 +13,17 @@ struct MatchSetupView: View { @EnvironmentObject var dataStore: DataStore - var match: Match + @State var match: Match @State private var seedGroup: SeedInterval? var tournamentStore: TournamentStore { return match.tournamentStore } + var matchTypeContext: MatchType { + match.matchType + } + @ViewBuilder var body: some View { ForEach(TeamPosition.allCases) { teamPosition in @@ -38,7 +42,7 @@ struct MatchSetupView: View { if let team, teamScore?.walkOut == nil { VStack(alignment: .leading, spacing: 0) { - if let teamScore, teamScore.luckyLoser != nil { + if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false { Text("Repêchée").italic().font(.caption) } Menu { @@ -59,9 +63,9 @@ struct MatchSetupView: View { } HStack { let luckyLosers = walkOutSpot ? match.luckyLosers() : [] - TeamPickerView(shouldConfirm: shouldConfirm, groupStagePosition: nil, luckyLosers: luckyLosers, teamPicked: { team in + TeamPickerView(shouldConfirm: shouldConfirm, groupStagePosition: nil, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in print(team.pasteData()) - if walkOutSpot || team.bracketPosition != nil { + if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket { match.setLuckyLoser(team: team, teamPosition: teamPosition) do { try tournamentStore.matches.addOrUpdate(instance: match) @@ -82,7 +86,7 @@ struct MatchSetupView: View { } } }) - if let tournament = match.currentTournament() { + if matchTypeContext == .bracket, let tournament = match.currentTournament() { let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableSeedGroups = tournament.availableSeedGroups() Text("ou") @@ -146,30 +150,33 @@ struct MatchSetupView: View { .underline() } .disabled(availableSeedGroups.isEmpty && walkOutSpot == false && availableQualifiedTeams.isEmpty) - Spacer() - if match.isSeedLocked(atTeamPosition: teamPosition) { - Button { - match.unlockSeedPosition(atTeamPosition: teamPosition) - do { - try tournamentStore.matches.addOrUpdate(instance: match) - } catch { - Logger.error(error) + + if matchTypeContext == .bracket { + Spacer() + if match.isSeedLocked(atTeamPosition: teamPosition) { + Button { + match.unlockSeedPosition(atTeamPosition: teamPosition) + do { + try tournamentStore.matches.addOrUpdate(instance: match) + } catch { + Logger.error(error) + } + } label: { + Text("Libérer") + .underline() } - } label: { - Text("Libérer") - .underline() - } - } else { - ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) { - _ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) - do { - try tournamentStore.matches.addOrUpdate(instance: match) - } catch { - Logger.error(error) + } else { + ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) { + _ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) + do { + try tournamentStore.matches.addOrUpdate(instance: match) + } catch { + Logger.error(error) + } + } label: { + Text("Réserver") + .underline() } - } label: { - Text("Réserver") - .underline() } } } diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index b8e9ba9..2e24481 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -16,11 +16,31 @@ struct LoserRoundSettingsView: View { var body: some View { List { + Section { + RowButtonView(isEditingTournamentSeed.wrappedValue == true ? "Terminer l'édition" : "Éditer les tours joués") { + isEditingTournamentSeed.wrappedValue.toggle() + } + } + + Section { + RowButtonView("Synchroniser les noms des matchs") { + let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches + }) + allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + } catch { + Logger.error(error) + } + } + } + Section { RowButtonView("Effacer les matchs de classements", role: .destructive) { upperBracketRound.round.deleteLoserBracket() } } + .disabled(upperBracketRound.round.loserRounds().isEmpty) Section { RowButtonView("Créer les matchs de classements", role: .destructive) { @@ -30,12 +50,7 @@ struct LoserRoundSettingsView: View { } } } - - Section { - RowButtonView(isEditingTournamentSeed.wrappedValue == true ? "Terminer l'édition" : "Éditer les tours joués") { - isEditingTournamentSeed.wrappedValue.toggle() - } - } + .disabled(upperBracketRound.round.loserRounds().isEmpty == false) //todo proposer ici l'impression des matchs de classements peut-être? } diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index 017db3d..eb7b2fa 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -53,13 +53,13 @@ struct LoserRoundView: View { if isEditingTournamentSeed.wrappedValue == true { RowButtonView(match.disabled ? "Jouer ce match" : "Ne pas jouer ce match", role: .destructive) { - match._toggleMatchDisableState(!match.disabled) + match._toggleMatchDisableState(!match.disabled, single: true) } } } } header: { HStack { - if let seedInterval = loserRound.seedInterval() { + if let seedInterval = loserRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { Text(seedInterval.localizedLabel(.wide)) let seedIntervalPointRange = seedInterval.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournament.teamCount) Spacer() @@ -67,13 +67,13 @@ struct LoserRoundView: View { .font(.caption) } else { if let previousRound = loserRound.previousRound() { - if let seedInterval = previousRound.seedInterval() { + if let seedInterval = previousRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { Text(seedInterval.localizedLabel()) } else { Text("no previous round") } } else if let parentRound = loserRound.parentRound { - if let seedInterval = parentRound.seedInterval() { + if let seedInterval = parentRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { Text(seedInterval.localizedLabel()) } else { Text("no parent round") diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 4e583c3..f1ac8bb 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -167,6 +167,10 @@ struct LoserRoundsView: View { 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] { diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index ee049c7..e23846f 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -92,7 +92,7 @@ struct RoundSettingsView: View { Section { let roundIndex = tournament.rounds().count - RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex)) { + RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex), role: .destructive) { let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) @@ -153,6 +153,18 @@ struct RoundSettingsView: View { } } } + + Section { + RowButtonView("Synchroniser les noms des matchs") { + let allRoundMatches = tournament.allRoundMatches() + allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + } catch { + Logger.error(error) + } + } + } } .toolbar { ToolbarItem(placement: .topBarTrailing) { diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index 4eb1980..aa891f9 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -14,8 +14,11 @@ struct TeamPickerView: View { @State private var confirmTeam: TeamRegistration? @State private var presentTeamPickerView: Bool = false @State private var searchField: String = "" + @State private var sortOrder: SortOrder = .ascending + var shouldConfirm: Bool = false var groupStagePosition: Int? = nil + var matchTypeContext: MatchType = .bracket var luckyLosers: [TeamRegistration] = [] let teamPicked: ((TeamRegistration) -> (Void)) @@ -43,40 +46,20 @@ struct TeamPickerView: View { Text("Même ligne en poule") } } - let teams = tournament.selectedSortedTeams() - if luckyLosers.isEmpty == false { - Section { - _teamListView(luckyLosers.sorted(by: \.weight)) - } header: { - Text("Repêchage") - } - } + _sectionView(luckyLosers.sorted(by: \.weight, order: sortOrder), title: "Repêchage") let qualified = tournament.availableQualifiedTeams() - if qualified.isEmpty == false { - Section { - _teamListView(qualified.sorted(by: \.weight)) - } header: { - Text("Qualifiées entrants") - } - } + _sectionView(qualified.sorted(by: \.weight, order: sortOrder), title: "Qualifiées entrants") + - Section { - _teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed()) - } header: { - Text("Disponible") - } - Section { - _teamListView(teams.filter({ $0.inGroupStage() }).sorted(by: \.groupStagePosition!).reversed()) - } header: { - Text("Déjà placée en poule") - } - Section { - _teamListView(teams.filter({ $0.inRound() }).sorted(by: \.bracketPosition!).reversed()) - } header: { - Text("Déjà placée dans le tableau") + let teams = tournament.selectedSortedTeams() + if matchTypeContext == .loserBracket { + _sectionView(teams.filter({ $0.inGroupStage() && $0.qualified == false }).sorted(by: \.weight, order: sortOrder), title: "Non qualifié de poules") } - + + _sectionView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight, order: sortOrder), title: "Disponible") + _sectionView(teams.filter({ $0.inGroupStage() }).sorted(by: \.weight, order: sortOrder), title: "Déjà placée en poule") + _sectionView(teams.filter({ $0.inRound() }).sorted(by: \.weight, order: sortOrder), title: "Déjà placée dans le tableau") } .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always)) .keyboardType(.alphabet) @@ -84,11 +67,36 @@ struct TeamPickerView: View { .navigationTitle("Choisir une équipe") .toolbarBackground(.visible, for: .navigationBar) .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Picker(selection: $sortOrder) { + Label("Poids", systemImage: "arrow.up").tag(SortOrder.ascending) + .labelStyle(.titleAndIcon) + Label("Poids", systemImage: "arrow.down").tag(SortOrder.descending) + .labelStyle(.titleAndIcon) + } label: { + Label("Trier", systemImage: "arrow.up.arrow.down") + } + .pickerStyle(.menu) + } + } + .headerProminence(.increased) } .tint(.master) } } + @ViewBuilder + private func _sectionView(_ teams: [TeamRegistration], title: String) -> some View { + if teams.isEmpty == false { + Section { + _teamListView(teams) + } header: { + Text(title) + } + } + } + private func _teamListView(_ teams: [TeamRegistration]) -> some View { ForEach(teams) { team in if searchField.isEmpty || team.contains(searchField) { diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 0b3550d..533d4bc 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -21,6 +21,17 @@ struct TeamRowView: View { Text(name).foregroundStyle(.secondary) } + if let groupStage = team.groupStageObject() { + HStack { + Text(groupStage.groupStageTitle()) + if let finalPosition = groupStage.finalPosition(ofTeam: team) { + Text((finalPosition + 1).ordinalFormatted()) + } + } + } else if let round = team.initialRound() { + Text(round.roundTitle(.wide)) + } + if team.players().isEmpty == false { ForEach(team.players()) { player in Text(player.playerLabel()) diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 88b0031..e491aec 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -192,26 +192,37 @@ struct TournamentRankView: View { .fontWeight(.bold) Text(key.ordinalFormattedSuffix()).font(.caption) } + if let index = tournament.indexOf(team: team) { - let rankingDifference = index - (key - 1) - if rankingDifference > 0 { - HStack(spacing: 0.0) { - Text(rankingDifference.formatted(.number.sign(strategy: .always()))) - .monospacedDigit() - Image(systemName: "arrowtriangle.up.fill") - .imageScale(.small) - } - .foregroundColor(.green) - } else if rankingDifference < 0 { + ZStack { HStack(spacing: 0.0) { - Text(rankingDifference.formatted(.number.sign(strategy: .always()))) + Text(tournament.teamCount.formatted(.number.sign(strategy: .always()))) .monospacedDigit() Image(systemName: "arrowtriangle.down.fill") .imageScale(.small) } - .foregroundColor(.logoRed) - } else { - Text("--") + .opacity(0) + + let rankingDifference = index - (key - 1) + if rankingDifference > 0 { + HStack(spacing: 0.0) { + Text(rankingDifference.formatted(.number.sign(strategy: .always()))) + .monospacedDigit() + Image(systemName: "arrowtriangle.up.fill") + .imageScale(.small) + } + .foregroundColor(.green) + } else if rankingDifference < 0 { + HStack(spacing: 0.0) { + Text(rankingDifference.formatted(.number.sign(strategy: .always()))) + .monospacedDigit() + Image(systemName: "arrowtriangle.down.fill") + .imageScale(.small) + } + .foregroundColor(.logoRed) + } else { + Text("--") + } } } }