You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
14 KiB
339 lines
14 KiB
//
|
|
// 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<Bool> {
|
|
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()
|
|
//}
|
|
|