// // 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 @Environment(\.editMode) private var editMode @State private var rankings: [Int: [TeamRegistration]] = [:] @State private var calculating = false @State private var selectedTeam: TeamRegistration? var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @State private var runningMatches: [Match]? @State private var matchesLeft: [Match]? var isEditingTeam: Binding { Binding { selectedTeam != nil } set: { value in } } var body: some View { List { @Bindable var tournament = tournament let rankingsCalculated = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil }) if editMode?.wrappedValue.isEditing == false { Section { MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false) Toggle(isOn: $tournament.hidePointsEarned) { Text("Masquer les points gagnés") } .onChange(of: tournament.hidePointsEarned) { do { try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } } Toggle(isOn: $tournament.publishRankings) { Text("Publier sur Padel Club") if let url = tournament.shareURL(.rankings) { Link(destination: url) { Text("Accéder à la page") } } } .onChange(of: tournament.publishRankings) { do { try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } } } } if (editMode?.wrappedValue.isEditing == true || rankingsCalculated == false) && calculating == false { Section { RowButtonView(rankingsCalculated ? "Re-calculer le classement" : "Calculer", role: .destructive) { await _calculateRankings() } } footer: { if rankingsCalculated { Text("Vos éditions seront perdus.") } } if rankingsCalculated { Section { RowButtonView("Supprimer le classement", role: .destructive) { tournament.unsortedTeams().forEach { team in team.finalRanking = nil team.pointsEarned = nil } _save() } } } } let teamsRanked = tournament.teamsRanked() if calculating == false && rankingsCalculated && teamsRanked.isEmpty == false { Section { ForEach(teamsRanked) { team in if let key = team.finalRanking { TeamRankCellView(team: team, key: key) } } } } } .task { let all = tournament.allMatches() self.runningMatches = await tournament.asyncRunningMatches(all) self.matchesLeft = await tournament.readyMatches(all) } .alert("Position", isPresented: isEditingTeam) { if let selectedTeam { @Bindable var team = selectedTeam TextField("Position", value: $team.finalRanking, format: .number) .keyboardType(.numberPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) Button("Valider") { selectedTeam.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: selectedTeam.finalRanking! - 1, count: tournament.teamCount) do { try self.tournamentStore.teamRegistrations.addOrUpdate(instance: selectedTeam) } catch { Logger.error(error) } self.selectedTeam = nil } Button("Annuler", role: .cancel) { self.selectedTeam = nil } } } .overlay(content: { if calculating { ProgressView() } }) .onAppear { let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil }) if rankingPublished == false { Task { await _calculateRankings() } } } .navigationTitle("Classement") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .topBarTrailing) { EditButton() } } } struct TeamRankCellView: View { @Environment(\.editMode) private var editMode @Environment(Tournament.self) var tournament: Tournament @State private var isEditingTeam: Bool = false @Bindable var team: TeamRegistration @State var key: Int var body: some View { VStack(spacing: 0) { if editMode?.wrappedValue.isEditing == true { if key > 1 { Button { key -= 1 team.finalRanking = key do { try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } } label: { Label("descendre", systemImage: "chevron.compact.up").labelStyle(.iconOnly) } .buttonStyle(.bordered) } } Button { isEditingTeam = true } label: { 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) { if let name = team.name { Text(name).foregroundStyle(.secondary) } 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) } } } } } if tournament.isAnimation() == false && key > 0 { 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) } } } } .frame(maxWidth: .infinity) } .contentShape(Rectangle()) .buttonStyle(.plain) if editMode?.wrappedValue.isEditing == true { Button { key += 1 team.finalRanking = key do { try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } } label: { Label("descendre", systemImage: "chevron.compact.down").labelStyle(.iconOnly) } .buttonStyle(.bordered) } } .alert("Position", isPresented: $isEditingTeam) { TextField("Position", value: $team.finalRanking, format: .number) .keyboardType(.numberPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) Button("Valider") { team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount) do { try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } isEditingTeam = false } Button("Annuler", role: .cancel) { isEditingTeam = false } } } } private func _calculateRankings() async { await MainActor.run { calculating = true } let finalRanks = await tournament.finalRanking() finalRanks.keys.sorted().forEach { rank in if let rankedTeamIds = finalRanks[rank] { let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } self.rankings[rank] = teams } } await MainActor.run { rankings.keys.sorted().forEach { rank in if let rankedTeams = rankings[rank] { rankedTeams.forEach { team in team.finalRanking = rank team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount) } } } _save() calculating = false } } private func _save() { do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) } catch { Logger.error(error) } } } //#Preview { // TournamentRankView() //}