add final ranking view

multistore
Razmig Sarkissian 2 years ago
parent 83a2b2a565
commit d30e93c6f1
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 6
      PadelClub/Data/Round.swift
  3. 50
      PadelClub/Data/Tournament.swift
  4. 2
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  5. 1
      PadelClub/Views/Team/Components/TeamWeightView.swift
  6. 1
      PadelClub/Views/Tournament/Screen/Screen.swift
  7. 106
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  8. 5
      PadelClub/Views/Tournament/TournamentView.swift

@ -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 = "<group>"; };
FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; };
FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalDataViewModel.swift; sourceTree = "<group>"; };
FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentRankView.swift; sourceTree = "<group>"; };
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreSheetView.swift; sourceTree = "<group>"; };
FF5D0D732BB41DF8005CB568 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
FF5D0D752BB428B2005CB568 /* ListRowViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViewModifier.swift; sourceTree = "<group>"; };
@ -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 */,

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

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

@ -22,7 +22,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
} label: {
Image(systemName: "wrench.and.screwdriver")
}
.foregroundStyle(selectedDestination == nil ? .white : .black)
.foregroundStyle(selectedDestination == nil ? .primary : .secondary)
.padding()
.background {
Circle()

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

@ -16,4 +16,5 @@ enum Screen: String, Codable {
case schedule
case cashier
case call
case rankings
}

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

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

Loading…
Cancel
Save