multistore
Razmig Sarkissian 1 year ago
parent a6679088df
commit f961498121
  1. 40
      PadelClub/Data/Match.swift
  2. 23
      PadelClub/Data/Round.swift
  3. 82
      PadelClub/Data/Tournament.swift
  4. 1
      PadelClub/Utils/URLs.swift
  5. 27
      PadelClub/Views/Club/ClubSearchView.swift
  6. 4
      PadelClub/Views/Match/MatchSetupView.swift
  7. 2
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  8. 1
      PadelClub/Views/Navigation/MainView.swift
  9. 2
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  10. 11
      PadelClub/Views/Round/LoserRoundsView.swift
  11. 28
      PadelClub/Views/Team/TeamPickerView.swift
  12. 2
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  13. 379
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  14. 7
      PadelClub/Views/Tournament/TournamentBuildView.swift
  15. 3
      PadelClubTests/ServerDataTests.swift

@ -208,25 +208,10 @@ defer {
func teamWillBeWalkOut(_ team: TeamRegistration) {
resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
existingTeamScore.walkOut = 1
do {
try DataStore.shared.teamScores.delete(contentOfs: previousScores)
} catch {
Logger.error(error)
}
if let existingTeamScore = teamScore(ofTeam: team) {
do {
try DataStore.shared.teamScores.delete(instance: existingTeamScore)
} catch {
Logger.error(error)
}
}
let teamScoreWalkout = TeamScore(match: id, team: team)
teamScoreWalkout.walkOut = 1
do {
try DataStore.shared.teamScores.addOrUpdate(instance: teamScoreWalkout)
try DataStore.shared.teamScores.addOrUpdate(instance: existingTeamScore)
} catch {
Logger.error(error)
}
@ -242,24 +227,17 @@ defer {
func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) {
resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
let matchIndex = index
let position = matchIndex * 2 + teamPosition.rawValue
let previousScores = teamScores.filter({ $0.luckyLoser == position })
do {
try DataStore.shared.teamScores.delete(contentOfs: previousScores)
} catch {
Logger.error(error)
}
if let existingTeamScore = teamScore(ofTeam: team) {
do {
try DataStore.shared.teamScores.delete(instance: existingTeamScore)
} catch {
Logger.error(error)
}
}
let matchIndex = index
let position = matchIndex * 2 + teamPosition.rawValue
let teamScoreLuckyLoser = TeamScore(match: id, team: team)
let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
teamScoreLuckyLoser.luckyLoser = position
do {
try DataStore.shared.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)

@ -556,6 +556,29 @@ defer {
loserRounds().forEach { round in
round.buildLoserBracket()
}
/*
return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat)
}
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
matches.forEach {
$0.name = $0.roundObject?.roundTitle()
}
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
*/
}
var parentRound: Round? {

@ -53,7 +53,8 @@ final class Tournament : ModelObject, Storable {
var hideTeamsWeight: Bool = false
var publishTournament: Bool = false
var hidePointsEarned: Bool = false
var publishRankings: Bool = false
@ObservationIgnored
var navigationPath: [Screen] = []
@ -100,9 +101,10 @@ final class Tournament : ModelObject, Storable {
case _hideTeamsWeight = "hideTeamsWeight"
case _publishTournament = "publishTournament"
case _hidePointsEarned = "hidePointsEarned"
case _publishRankings = "publishRankings"
}
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false) {
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false) {
self.event = event
self.name = name
self.startDate = startDate
@ -139,6 +141,7 @@ final class Tournament : ModelObject, Storable {
self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
}
required init(from decoder: Decoder) throws {
@ -182,6 +185,7 @@ final class Tournament : ModelObject, Storable {
hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false
publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false
publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false
}
fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
@ -296,6 +300,7 @@ final class Tournament : ModelObject, Storable {
try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight)
try container.encode(publishTournament, forKey: ._publishTournament)
try container.encode(hidePointsEarned, forKey: ._hidePointsEarned)
try container.encode(publishRankings, forKey: ._publishRankings)
}
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
@ -1091,6 +1096,17 @@ defer {
return selected.sorted(by: \.finalRanking!, order: .ascending)
}
private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) {
for key in dictionary.keys {
if var stringArray = dictionary[key] {
// Remove all instances of each string in stringsToRemove
stringArray.removeAll { stringsToRemove.contains($0) }
dictionary[key] = stringArray
}
}
}
func finalRanking() async -> [Int: [String]] {
var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>()
@ -1106,6 +1122,14 @@ defer {
}
let others: [Round] = rounds.flatMap { round in
let losers = round.losers()
let minimumFinalPosition = round.seedInterval()?.last ?? teamCount
if teams[minimumFinalPosition] == nil {
teams[minimumFinalPosition] = losers.map { $0.id }
} else {
teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id })
}
print("round", round.roundTitle())
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
print(rounds.count, rounds.map { $0.roundTitle() })
@ -1124,28 +1148,40 @@ defer {
print("losers", losers.count)
if winners.isEmpty {
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
teams[interval.computedLast] = disabledIds
let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.computedLast) : ", teamNames)
disabledIds.forEach { ids.insert($0) }
if disabledIds.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: disabledIds)
teams[interval.computedLast] = disabledIds
let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.computedLast) : ", teamNames)
disabledIds.forEach {
ids.insert($0)
}
}
} else {
teams[interval.computedFirst + winners.count - 1] = winners
let teamNames : [String] = winners.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames)
winners.forEach { ids.insert($0) }
teams[interval.computedLast] = losers
let loserTeamNames : [String] = losers.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("losers", "\(interval.computedLast) : ", loserTeamNames)
losers.forEach { ids.insert($0) }
if winners.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: winners)
teams[interval.computedFirst + winners.count - 1] = winners
let teamNames : [String] = winners.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames)
winners.forEach { ids.insert($0) }
}
if losers.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: losers)
teams[interval.computedLast] = losers
let loserTeamNames : [String] = losers.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("losers", "\(interval.computedLast) : ", loserTeamNames)
losers.forEach { ids.insert($0) }
}
}
}
}

@ -14,6 +14,7 @@ enum URLs: String, Identifiable {
case api = "https://xlr.alwaysdata.net/roads/"
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app"
case tenup = "https://tenup.fft.fr"
case padelRules = "https://fft-site.cdn.prismic.io/fft-site/ZgLn3McYqOFdyF7n_LEGUIDEDELACOMPETITIONDEPADEL-MAJDECEMBRE2023.pdf"
var id: String { return self.rawValue }

@ -27,7 +27,8 @@ struct ClubSearchView: View {
@State private var searchPresented: Bool = false
@State private var showingSettingsAlert = false
@State private var newClub: Club?
@State private var error: Error?
var presentClubCreationView: Binding<Bool> { Binding(
get: { newClub != nil },
set: { isPresented in
@ -59,7 +60,7 @@ struct ClubSearchView: View {
searching = false
searchAttempted = true
}
error = nil
clubMarkers = []
guard let city = locationManager.city else { return }
let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location)
@ -70,6 +71,8 @@ struct ClubSearchView: View {
}
} catch {
print("getclubs", error)
self.error = error
Logger.error(error)
}
}
@ -143,12 +146,20 @@ struct ClubSearchView: View {
} else if clubMarkers.isEmpty && searching == false && searchPresented == false {
ContentUnavailableView {
if searchAttempted {
Label("Aucun club trouvé", systemImage: "mappin.slash")
if error != nil {
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} else {
Label("Aucun club trouvé", systemImage: "mappin.slash")
}
} else {
Label("Recherche de club", systemImage: "location.circle")
}
} description: {
Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
if searchAttempted && error != nil {
Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.")
} else {
Text("Padel Club recherche via Tenup un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
}
} actions: {
if locationManager.manager.authorizationStatus != .restricted {
RowButtonView("Chercher autour de moi") {
@ -161,6 +172,13 @@ struct ClubSearchView: View {
}
}
}
if error != nil {
Link(destination: URLs.tenup.url) {
Text("Voir si tenup est en maintenance")
}
}
RowButtonView("Chercher une ville ou un code postal") {
searchPresented = true
}
@ -343,6 +361,7 @@ struct ClubSearchView: View {
private func _resetSearch() {
searchAttempted = false
error = nil
debouncableViewModel.debouncableText = ""
searchedCity = ""
locationManager.city = nil

@ -73,14 +73,14 @@ struct MatchSetupView: View {
let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(groupStagePosition: nil, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData())
if walkOutSpot {
if walkOutSpot || team.bracketPosition != nil {
match.setLuckyLoser(team: team, teamPosition: teamPosition)
do {
try dataStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
} else {
} else if team.bracketPosition == nil {
team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false)
do {
try dataStore.matches.addOrUpdate(instance: match)

@ -102,7 +102,7 @@ struct ActivityView: View {
.overlay {
if let error, navigation.agendaDestination == .tenup {
ContentUnavailableView {
Label("Erreur", systemImage: "exclamationmark")
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} description: {
Text(error.localizedDescription)
} actions: {

@ -91,7 +91,6 @@ struct MainView: View {
}
.environmentObject(dataStore)
.task {
await self._checkSourceFileAvailability()
if Store.main.hasToken() {
do {
try await dataStore.clubs.loadDataFromServerIfAllowed()

@ -128,7 +128,7 @@ struct CourtAvailabilitySettingsView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible")
.popover(isPresented: $showingPopover) {
.sheet(isPresented: $showingPopover) {
NavigationStack {
Form {
Section {

@ -177,13 +177,12 @@ struct LoserRoundsView: View {
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: false)
if let selectedRound {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: true)
switch selectedRound {
case .some(let selectedRound):
LoserRoundView(loserBracket: selectedRound)
} else {
Section {
ContentUnavailableView("Aucun tour à jouer", systemImage: "tennisball", description: Text("Il il n'y a aucun tour de match de classement prévu."))
}
default:
LoserRoundSettingsView()
}
}
.environment(\.isEditingTournamentSeed, $isEditingTournamentSeed)

@ -11,12 +11,20 @@ struct TeamPickerView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss
@State private var confirmTeam: TeamRegistration?
@State private var presentTeamPickerView: Bool = false
@State private var searchField: String = ""
var groupStagePosition: Int? = nil
var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void))
var confirmationRequest: Binding<Bool> {
Binding {
confirmTeam != nil
} set: { _ in
}
}
var body: some View {
Button {
presentTeamPickerView = true
@ -86,12 +94,30 @@ struct TeamPickerView: View {
Button {
teamPicked(team)
presentTeamPickerView = false
// if team.inRound() {
// confirmTeam = team
// } else {
// teamPicked(team)
// presentTeamPickerView = false
// }
} label: {
TeamRowView(team: team)
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) {
// Button("Retirer du tableau", role: .destructive) {
// teamPicked(confirmTeam!)
// presentTeamPickerView = false
// }
//
// Button("Annuler", role: .cancel) {
// confirmTeam = nil
// }
// } message: {
// Text("Vous êtes sur le point de retirer cette équipe du tableau pour le replacer, cela effacera les résultats des matchs déjà joués par cette équipe dans le tableau.")
// }
}
}
}

@ -524,7 +524,7 @@ struct InscriptionManagerView: View {
RowButtonView("Créer une équipe") {
Task {
await MainActor.run() {
await MainActor.run {
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = searchField

@ -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 {

@ -89,12 +89,17 @@ struct TournamentBuildView: View {
Section {
#if DEBUG
NavigationLink(value: Screen.rankings) {
Text("Classement final des équipes")
}
#else
if tournament.hasEnded() {
NavigationLink(value: Screen.rankings) {
Text("Classement final des équipes")
}
}
#endif
if state == .running || state == .finished {
TournamentInscriptionView(tournament: tournament)
TournamentBroadcastRowView(tournament: tournament)

@ -99,7 +99,7 @@ final class ServerDataTests: XCTestCase {
return
}
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true)
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true)
let t = try await Store.main.service().post(tournament)
assert(t.event == tournament.event)
@ -138,6 +138,7 @@ final class ServerDataTests: XCTestCase {
assert(t.hideTeamsWeight == tournament.hideTeamsWeight)
assert(t.publishTournament == tournament.publishTournament)
assert(t.hidePointsEarned == tournament.hidePointsEarned)
assert(t.publishRankings == tournament.publishRankings)
}
func testGroupStage() async throws {

Loading…
Cancel
Save