|
|
|
|
@ -11,7 +11,8 @@ 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? |
|
|
|
|
@ -28,97 +29,71 @@ struct TournamentRankView: View { |
|
|
|
|
var body: some View { |
|
|
|
|
List { |
|
|
|
|
@Bindable var tournament = tournament |
|
|
|
|
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil }) |
|
|
|
|
Section { |
|
|
|
|
LabeledContent { |
|
|
|
|
if let matchesLeft { |
|
|
|
|
Text(matchesLeft.count.formatted()) |
|
|
|
|
} else { |
|
|
|
|
ProgressView() |
|
|
|
|
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") |
|
|
|
|
} |
|
|
|
|
} label: { |
|
|
|
|
Text("Matchs restant") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
LabeledContent { |
|
|
|
|
if let runningMatches { |
|
|
|
|
Text(runningMatches.count.formatted()) |
|
|
|
|
} else { |
|
|
|
|
ProgressView() |
|
|
|
|
.onChange(of: tournament.hidePointsEarned) { |
|
|
|
|
do { |
|
|
|
|
try dataStore.tournaments.addOrUpdate(instance: tournament) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} label: { |
|
|
|
|
Text("Matchs en cours") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
LabeledContent { |
|
|
|
|
if rankingPublished { |
|
|
|
|
Image(systemName: "checkmark") |
|
|
|
|
.foregroundStyle(.green) |
|
|
|
|
} else { |
|
|
|
|
Image(systemName: "xmark") |
|
|
|
|
.foregroundStyle(.logoRed) |
|
|
|
|
|
|
|
|
|
Toggle(isOn: $tournament.publishRankings) { |
|
|
|
|
Text("Publier sur Padel Club") |
|
|
|
|
if let url = tournament.shareURL(.rankings) { |
|
|
|
|
Link(destination: url) { |
|
|
|
|
Text("Accéder à la page") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} label: { |
|
|
|
|
Text("Classement publié") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
.onChange(of: tournament.publishRankings) { |
|
|
|
|
do { |
|
|
|
|
try dataStore.tournaments.addOrUpdate(instance: tournament) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if rankingPublished == false { |
|
|
|
|
RowButtonView("Publier le classement", role: .destructive) { |
|
|
|
|
_publishRankings() |
|
|
|
|
if (editMode?.wrappedValue.isEditing == true || rankingsCalculated == false) && calculating == false { |
|
|
|
|
Section { |
|
|
|
|
RowButtonView(rankingsCalculated ? "Re-calculer le classement" : "Calculer", role: .destructive) { |
|
|
|
|
await _calculateRankings() |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
RowButtonView("Re-publier le classement", role: .destructive) { |
|
|
|
|
_publishRankings() |
|
|
|
|
} footer: { |
|
|
|
|
if rankingsCalculated { |
|
|
|
|
Text("Vos éditions seront perdus.") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if rankingPublished { |
|
|
|
|
Section { |
|
|
|
|
RowButtonView("Supprimer le classement", role: .destructive) { |
|
|
|
|
tournament.unsortedTeams().forEach { team in |
|
|
|
|
team.finalRanking = nil |
|
|
|
|
team.pointsEarned = nil |
|
|
|
|
|
|
|
|
|
if rankingsCalculated { |
|
|
|
|
Section { |
|
|
|
|
RowButtonView("Supprimer le classement", role: .destructive) { |
|
|
|
|
tournament.unsortedTeams().forEach { team in |
|
|
|
|
team.finalRanking = nil |
|
|
|
|
team.pointsEarned = nil |
|
|
|
|
} |
|
|
|
|
_save() |
|
|
|
|
} |
|
|
|
|
_save() |
|
|
|
|
} |
|
|
|
|
} footer: { |
|
|
|
|
Text(.init("Masque également le classement sur le site [Padel Club](\(URLs.main.rawValue))")) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if rankingPublished { |
|
|
|
|
let teamsRanked = tournament.teamsRanked() |
|
|
|
|
if calculating == false && rankingsCalculated && teamsRanked.isEmpty == false { |
|
|
|
|
Section { |
|
|
|
|
ForEach(tournament.teamsRanked()) { team in |
|
|
|
|
let key = team.finalRanking ?? 0 |
|
|
|
|
Button { |
|
|
|
|
selectedTeam = team |
|
|
|
|
} label: { |
|
|
|
|
TeamRankCellView(team: team, key: key) |
|
|
|
|
.frame(maxWidth: .infinity) |
|
|
|
|
} |
|
|
|
|
.contentShape(Rectangle()) |
|
|
|
|
.buttonStyle(.plain) |
|
|
|
|
} |
|
|
|
|
} footer: { |
|
|
|
|
Text("Vous pouvez appuyer sur une ligne pour éditer manuellement le classement calculé par Padel Club.") |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
let keys = rankings.keys.sorted() |
|
|
|
|
ForEach(keys, id: \.self) { key in |
|
|
|
|
if let rankedTeams = rankings[key] { |
|
|
|
|
ForEach(rankedTeams) { team in |
|
|
|
|
ForEach(teamsRanked) { team in |
|
|
|
|
if let key = team.finalRanking { |
|
|
|
|
TeamRankCellView(team: team, key: key) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -130,42 +105,16 @@ struct TournamentRankView: View { |
|
|
|
|
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 dataStore.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().allSatisfy({ $0.finalRanking != nil }) |
|
|
|
|
let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil }) |
|
|
|
|
if rankingPublished == false { |
|
|
|
|
calculating = true |
|
|
|
|
Task { |
|
|
|
|
await _calculateRankings() |
|
|
|
|
calculating = false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -174,130 +123,178 @@ struct TournamentRankView: View { |
|
|
|
|
.toolbarBackground(.visible, for: .navigationBar) |
|
|
|
|
.toolbar { |
|
|
|
|
ToolbarItem(placement: .topBarTrailing) { |
|
|
|
|
if let url = tournament.shareURL(.rankings) { |
|
|
|
|
_actionForURL(url) |
|
|
|
|
} |
|
|
|
|
EditButton() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct TeamRankCellView: View { |
|
|
|
|
@Environment(\.editMode) private var editMode |
|
|
|
|
@Environment(Tournament.self) var tournament: Tournament |
|
|
|
|
let team: TeamRegistration |
|
|
|
|
let key: Int |
|
|
|
|
@EnvironmentObject var dataStore: DataStore |
|
|
|
|
@State private var isEditingTeam: Bool = false |
|
|
|
|
@Bindable var team: TeamRegistration |
|
|
|
|
@State var key: Int |
|
|
|
|
|
|
|
|
|
var body: some View { |
|
|
|
|
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) |
|
|
|
|
VStack(spacing: 0) { |
|
|
|
|
if editMode?.wrappedValue.isEditing == true { |
|
|
|
|
if key > 1 { |
|
|
|
|
Button { |
|
|
|
|
key -= 1 |
|
|
|
|
team.finalRanking = key |
|
|
|
|
do { |
|
|
|
|
try dataStore.teamRegistrations.addOrUpdate(instance: team) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
.foregroundColor(.red) |
|
|
|
|
} else { |
|
|
|
|
Text("--") |
|
|
|
|
} label: { |
|
|
|
|
Label("descendre", systemImage: "chevron.compact.up").labelStyle(.iconOnly) |
|
|
|
|
} |
|
|
|
|
.buttonStyle(.bordered) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
.contentShape(Rectangle()) |
|
|
|
|
.buttonStyle(.plain) |
|
|
|
|
|
|
|
|
|
if editMode?.wrappedValue.isEditing == true { |
|
|
|
|
Button { |
|
|
|
|
key += 1 |
|
|
|
|
team.finalRanking = key |
|
|
|
|
do { |
|
|
|
|
try dataStore.teamRegistrations.addOrUpdate(instance: team) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} label: { |
|
|
|
|
Label("descendre", systemImage: "chevron.compact.down").labelStyle(.iconOnly) |
|
|
|
|
} |
|
|
|
|
.buttonStyle(.bordered) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private func _publishRankings() { |
|
|
|
|
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) |
|
|
|
|
.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 dataStore.teamRegistrations.addOrUpdate(instance: team) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
isEditingTeam = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Button("Annuler", role: .cancel) { |
|
|
|
|
isEditingTeam = false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
_save() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] { |
|
|
|
|
rankings[rank] = rankedTeamIds.compactMap { Store.main.findById($0) } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ViewBuilder |
|
|
|
|
private func _actionForURL(_ url: URL, removeSource: Bool = false) -> some View { |
|
|
|
|
Menu { |
|
|
|
|
Button { |
|
|
|
|
UIApplication.shared.open(url) |
|
|
|
|
} label: { |
|
|
|
|
Label("Voir", systemImage: "safari") |
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
ShareLink(item: url) { |
|
|
|
|
Label("Partager le lien", systemImage: "link") |
|
|
|
|
} |
|
|
|
|
} label: { |
|
|
|
|
Image(systemName: "square.and.arrow.up") |
|
|
|
|
calculating = false |
|
|
|
|
} |
|
|
|
|
.frame(maxWidth: .infinity) |
|
|
|
|
.buttonStyle(.borderless) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private func _save() { |
|
|
|
|
do { |
|
|
|
|
|