From d30e93c6f1c0bda25e1542e8e4e9dccb15f6c089 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 May 2024 10:14:48 +0200 Subject: [PATCH] add final ranking view --- PadelClub.xcodeproj/project.pbxproj | 4 + PadelClub/Data/Round.swift | 6 + PadelClub/Data/Tournament.swift | 50 ++++++++- .../GenericDestinationPickerView.swift | 2 +- .../Team/Components/TeamWeightView.swift | 1 - .../Views/Tournament/Screen/Screen.swift | 1 + .../Screen/TournamentRankView.swift | 106 ++++++++++++++++++ .../Views/Tournament/TournamentView.swift | 5 + 8 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 PadelClub/Views/Tournament/Screen/TournamentRankView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index e612230..75bcc61 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */; }; + FF5BAF722BE19274008B4B7E /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; }; FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */; }; FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D732BB41DF8005CB568 /* Color+Extensions.swift */; }; FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */; }; @@ -427,6 +428,7 @@ FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = ""; }; FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalDataViewModel.swift; sourceTree = ""; }; + FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentRankView.swift; sourceTree = ""; }; FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreSheetView.swift; sourceTree = ""; }; FF5D0D732BB41DF8005CB568 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViewModifier.swift; sourceTree = ""; }; @@ -928,6 +930,7 @@ FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */, FF9268062BCE94D90080F940 /* TournamentCallView.swift */, FF1162802BCF945C000C4809 /* TournamentCashierView.swift */, + FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -1586,6 +1589,7 @@ FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, + FF5BAF722BE19274008B4B7E /* TournamentRankView.swift in Sources */, FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */, FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */, FFF9645B2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift in Sources */, diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 2038e8f..82b829e 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -337,6 +337,10 @@ class Round: ModelObject, Storable { return seedInterval.localizedLabel(displayStyle) } + func hasNextRound() -> Bool { + nextRound()?.isDisabled() == false + } + func seedInterval() -> SeedInterval? { if parent == nil { let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index + 1) @@ -370,6 +374,8 @@ class Round: ModelObject, Storable { func roundStatus() -> String { if hasStarted() && hasEnded() == false { return "en cours" + } else if hasEnded() { + return "terminée" } else { return "à démarrer" } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a28ed74..ce5d580 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -322,7 +322,7 @@ class Tournament : ModelObject, Storable { func getActiveRound(withSeeds: Bool = false) -> Round? { let rounds = rounds() - let round = rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + let round = rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first if withSeeds { if round?.seeds().isEmpty == false { @@ -591,6 +591,54 @@ class Tournament : ModelObject, Storable { return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit)) } + func finalRanking() -> [Int: [String]] { + var teams: [Int: [String]] = [:] + + let rounds = rounds() + let final = rounds.last?.playedMatches().last + if let winner = final?.winningTeamId { + teams[1] = [winner] + } + if let finalist = final?.losingTeamId { + teams[2] = [finalist] + } + + let others : [Round] = rounds.flatMap { round in + round.loserRoundsAndChildren().filter { $0.isDisabled() == false && $0.hasNextRound() == false } + }.compactMap({ $0 }) + + others.forEach { round in + if let interval = round.seedInterval() { + let playedMatches = round.playedMatches().filter { $0.disabled == false } + let winners = playedMatches.compactMap({ $0.winningTeamId }) + let losers = playedMatches.compactMap({ $0.losingTeamId }) + teams[interval.first + winners.count - 1] = winners + teams[interval.last] = losers + } + } + + let groupStages = groupStages() + let baseRank = teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + + groupStages.forEach { groupStage in + let groupStageTeams = groupStage.teams(true) + for (index, team) in groupStageTeams.enumerated() { + if team.qualified == false { + let groupStageWidth = max(((index == qualifiedPerGroupStage) ? teamsPerGroupStage - groupStageAdditionalQualified : teamsPerGroupStage) * (index - qualifiedPerGroupStage), 0) + + let _index = baseRank + groupStageWidth + 1 + if let existingTeams = teams[_index] { + teams[_index] = existingTeams + [team.id] + } else { + teams[_index] = [team.id] + } + } + } + } + + + return teams + } func lockRegistration() { closedRegistrationDate = Date() diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 07edd1e..cdae4b9 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -22,7 +22,7 @@ struct GenericDestinationPickerView: View { } label: { Image(systemName: "wrench.and.screwdriver") } - .foregroundStyle(selectedDestination == nil ? .white : .black) + .foregroundStyle(selectedDestination == nil ? .primary : .secondary) .padding() .background { Circle() diff --git a/PadelClub/Views/Team/Components/TeamWeightView.swift b/PadelClub/Views/Team/Components/TeamWeightView.swift index fc48af5..72e1696 100644 --- a/PadelClub/Views/Team/Components/TeamWeightView.swift +++ b/PadelClub/Views/Team/Components/TeamWeightView.swift @@ -21,7 +21,6 @@ struct TeamWeightView: View { if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) { Text("#" + (index + 1).formatted(.number.precision(.integerLength(2...3)))) .monospacedDigit() - .font(.title) } if teamPosition == .two { Text(team.weight.formatted()) diff --git a/PadelClub/Views/Tournament/Screen/Screen.swift b/PadelClub/Views/Tournament/Screen/Screen.swift index d9ffa1e..57c0a67 100644 --- a/PadelClub/Views/Tournament/Screen/Screen.swift +++ b/PadelClub/Views/Tournament/Screen/Screen.swift @@ -16,4 +16,5 @@ enum Screen: String, Codable { case schedule case cashier case call + case rankings } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift new file mode 100644 index 0000000..8dcc0c4 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -0,0 +1,106 @@ +// +// TournamentRankView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 30/04/2024. +// + +import SwiftUI +import LeStorage + +struct TournamentRankView: View { + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + + @State private var rankings: [Int: [TeamRegistration]] = [:] + + var body: some View { + List { + let keys = rankings.keys.sorted() + ForEach(keys, id: \.self) { key in + if let rankedTeams = rankings[key] { + ForEach(rankedTeams) { team in + HStack { + VStack(alignment: .trailing) { + VStack(alignment: .trailing, spacing: -8.0) { + ZStack(alignment: .trailing) { + Text(tournament.teamCount.formatted()).hidden() + Text(key.formatted()) + } + .monospacedDigit() + .font(.largeTitle) + .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 { + HStack(spacing: 0.0) { + Text(rankingDifference.formatted(.number.sign(strategy: .always()))) + .monospacedDigit() + Image(systemName: "arrowtriangle.down.fill") + .imageScale(.small) + } + .foregroundColor(.red) + } else { + Text("--") + } + } + } + + + Divider() + + VStack(alignment: .leading) { + ForEach(team.players()) { player in + VStack(alignment: .leading, spacing: -4.0) { + Text(player.playerLabel()).bold() + HStack(alignment: .firstTextBaseline, spacing: 0.0) { + Text(player.rankLabel()) + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()) + .font(.caption) + } + } + } + } + } + + Spacer() + VStack(alignment: .trailing) { + HStack(alignment: .lastTextBaseline, spacing: 0.0) { + Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always()))) + Text("pts").font(.caption) + } + } + } + } + } + } + } + .listStyle(.grouped) + .onAppear { + let finalRanks = tournament.finalRanking() + finalRanks.keys.sorted().forEach { rank in + if let rankedTeamIds = finalRanks[rank] { + rankings[rank] = rankedTeamIds.compactMap { Store.main.findById($0) } + } + } + } + .navigationTitle("Classement") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +#Preview { + TournamentRankView() +} diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 4a8f027..7cc9690 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -85,6 +85,8 @@ struct TournamentView: View { TournamentCashierView(tournament: tournament) case .call: TournamentCallView(tournament: tournament) + case .rankings: + TournamentRankView() } } .environment(tournament) @@ -118,6 +120,9 @@ struct TournamentView: View { NavigationLink(value: Screen.structure) { LabelStructure() } + NavigationLink(value: Screen.rankings) { + Text("Classement") + } } } label: { LabelOptions()