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) { func teamWillBeWalkOut(_ team: TeamRegistration) {
resetMatch() resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil }) let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
existingTeamScore.walkOut = 1
do { do {
try DataStore.shared.teamScores.delete(contentOfs: previousScores) try DataStore.shared.teamScores.addOrUpdate(instance: existingTeamScore)
} 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)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -242,24 +227,17 @@ defer {
func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) { func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) {
resetMatch() 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 { do {
try DataStore.shared.teamScores.delete(contentOfs: previousScores) try DataStore.shared.teamScores.delete(contentOfs: previousScores)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
if let existingTeamScore = teamScore(ofTeam: team) { let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: 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)
teamScoreLuckyLoser.luckyLoser = position teamScoreLuckyLoser.luckyLoser = position
do { do {
try DataStore.shared.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) try DataStore.shared.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)

@ -556,6 +556,29 @@ defer {
loserRounds().forEach { round in loserRounds().forEach { round in
round.buildLoserBracket() 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? { var parentRound: Round? {

@ -53,7 +53,8 @@ final class Tournament : ModelObject, Storable {
var hideTeamsWeight: Bool = false var hideTeamsWeight: Bool = false
var publishTournament: Bool = false var publishTournament: Bool = false
var hidePointsEarned: Bool = false var hidePointsEarned: Bool = false
var publishRankings: Bool = false
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
@ -100,9 +101,10 @@ final class Tournament : ModelObject, Storable {
case _hideTeamsWeight = "hideTeamsWeight" case _hideTeamsWeight = "hideTeamsWeight"
case _publishTournament = "publishTournament" case _publishTournament = "publishTournament"
case _hidePointsEarned = "hidePointsEarned" 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.event = event
self.name = name self.name = name
self.startDate = startDate self.startDate = startDate
@ -139,6 +141,7 @@ final class Tournament : ModelObject, Storable {
self.hideTeamsWeight = hideTeamsWeight self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -182,6 +185,7 @@ final class Tournament : ModelObject, Storable {
hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false
publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? 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() fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
@ -296,6 +300,7 @@ final class Tournament : ModelObject, Storable {
try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight) try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight)
try container.encode(publishTournament, forKey: ._publishTournament) try container.encode(publishTournament, forKey: ._publishTournament)
try container.encode(hidePointsEarned, forKey: ._hidePointsEarned) try container.encode(hidePointsEarned, forKey: ._hidePointsEarned)
try container.encode(publishRankings, forKey: ._publishRankings)
} }
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws { fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
@ -1091,6 +1096,17 @@ defer {
return selected.sorted(by: \.finalRanking!, order: .ascending) 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]] { func finalRanking() async -> [Int: [String]] {
var teams: [Int: [String]] = [:] var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>() var ids: Set<String> = Set<String>()
@ -1106,6 +1122,14 @@ defer {
} }
let others: [Round] = rounds.flatMap { round in 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()) print("round", round.roundTitle())
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
print(rounds.count, rounds.map { $0.roundTitle() }) print(rounds.count, rounds.map { $0.roundTitle() })
@ -1124,28 +1148,40 @@ defer {
print("losers", losers.count) print("losers", losers.count)
if winners.isEmpty { if winners.isEmpty {
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
teams[interval.computedLast] = disabledIds if disabledIds.isEmpty == false {
let teamNames : [String] = disabledIds.compactMap { _removeStrings(from: &teams, stringsToRemove: disabledIds)
let t : TeamRegistration? = Store.main.findById($0) teams[interval.computedLast] = disabledIds
return t let teamNames : [String] = disabledIds.compactMap {
}.map { $0.canonicalName } let t : TeamRegistration? = Store.main.findById($0)
print("winners.isEmpty", "\(interval.computedLast) : ", teamNames) return t
disabledIds.forEach { ids.insert($0) } }.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.computedLast) : ", teamNames)
disabledIds.forEach {
ids.insert($0)
}
}
} else { } else {
teams[interval.computedFirst + winners.count - 1] = winners if winners.isEmpty == false {
let teamNames : [String] = winners.compactMap { _removeStrings(from: &teams, stringsToRemove: winners)
let t: TeamRegistration? = Store.main.findById($0) teams[interval.computedFirst + winners.count - 1] = winners
return t let teamNames : [String] = winners.compactMap {
}.map { $0.canonicalName } let t: TeamRegistration? = Store.main.findById($0)
print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames) return t
winners.forEach { ids.insert($0) } }.map { $0.canonicalName }
teams[interval.computedLast] = losers print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames)
let loserTeamNames : [String] = losers.compactMap { winners.forEach { ids.insert($0) }
let t: TeamRegistration? = Store.main.findById($0) }
return t
}.map { $0.canonicalName } if losers.isEmpty == false {
print("losers", "\(interval.computedLast) : ", loserTeamNames) _removeStrings(from: &teams, stringsToRemove: losers)
losers.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) }
}
} }
} }
} }

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

@ -27,7 +27,8 @@ struct ClubSearchView: View {
@State private var searchPresented: Bool = false @State private var searchPresented: Bool = false
@State private var showingSettingsAlert = false @State private var showingSettingsAlert = false
@State private var newClub: Club? @State private var newClub: Club?
@State private var error: Error?
var presentClubCreationView: Binding<Bool> { Binding( var presentClubCreationView: Binding<Bool> { Binding(
get: { newClub != nil }, get: { newClub != nil },
set: { isPresented in set: { isPresented in
@ -59,7 +60,7 @@ struct ClubSearchView: View {
searching = false searching = false
searchAttempted = true searchAttempted = true
} }
error = nil
clubMarkers = [] clubMarkers = []
guard let city = locationManager.city else { return } guard let city = locationManager.city else { return }
let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location) let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location)
@ -70,6 +71,8 @@ struct ClubSearchView: View {
} }
} catch { } catch {
print("getclubs", error) print("getclubs", error)
self.error = error
Logger.error(error)
} }
} }
@ -143,12 +146,20 @@ struct ClubSearchView: View {
} else if clubMarkers.isEmpty && searching == false && searchPresented == false { } else if clubMarkers.isEmpty && searching == false && searchPresented == false {
ContentUnavailableView { ContentUnavailableView {
if searchAttempted { 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 { } else {
Label("Recherche de club", systemImage: "location.circle") Label("Recherche de club", systemImage: "location.circle")
} }
} description: { } 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: { } actions: {
if locationManager.manager.authorizationStatus != .restricted { if locationManager.manager.authorizationStatus != .restricted {
RowButtonView("Chercher autour de moi") { 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") { RowButtonView("Chercher une ville ou un code postal") {
searchPresented = true searchPresented = true
} }
@ -343,6 +361,7 @@ struct ClubSearchView: View {
private func _resetSearch() { private func _resetSearch() {
searchAttempted = false searchAttempted = false
error = nil
debouncableViewModel.debouncableText = "" debouncableViewModel.debouncableText = ""
searchedCity = "" searchedCity = ""
locationManager.city = nil locationManager.city = nil

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

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

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

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

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

@ -11,12 +11,20 @@ struct TeamPickerView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var confirmTeam: TeamRegistration?
@State private var presentTeamPickerView: Bool = false @State private var presentTeamPickerView: Bool = false
@State private var searchField: String = "" @State private var searchField: String = ""
var groupStagePosition: Int? = nil var groupStagePosition: Int? = nil
var luckyLosers: [TeamRegistration] = [] var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
var confirmationRequest: Binding<Bool> {
Binding {
confirmTeam != nil
} set: { _ in
}
}
var body: some View { var body: some View {
Button { Button {
presentTeamPickerView = true presentTeamPickerView = true
@ -86,12 +94,30 @@ struct TeamPickerView: View {
Button { Button {
teamPicked(team) teamPicked(team)
presentTeamPickerView = false presentTeamPickerView = false
// if team.inRound() {
// confirmTeam = team
// } else {
// teamPicked(team)
// presentTeamPickerView = false
// }
} label: { } label: {
TeamRowView(team: team) TeamRowView(team: team)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .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") { RowButtonView("Créer une équipe") {
Task { Task {
await MainActor.run() { await MainActor.run {
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = searchField pasteString = searchField

@ -11,7 +11,8 @@ import LeStorage
struct TournamentRankView: View { struct TournamentRankView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@State private var rankings: [Int: [TeamRegistration]] = [:] @State private var rankings: [Int: [TeamRegistration]] = [:]
@State private var calculating = false @State private var calculating = false
@State private var selectedTeam: TeamRegistration? @State private var selectedTeam: TeamRegistration?
@ -28,97 +29,71 @@ struct TournamentRankView: View {
var body: some View { var body: some View {
List { List {
@Bindable var tournament = tournament @Bindable var tournament = tournament
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil }) let rankingsCalculated = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
Section { if editMode?.wrappedValue.isEditing == false {
LabeledContent { Section {
if let matchesLeft { MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false)
Text(matchesLeft.count.formatted()) MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false)
} else {
ProgressView() Toggle(isOn: $tournament.hidePointsEarned) {
Text("Masquer les points gagnés")
} }
} label: { .onChange(of: tournament.hidePointsEarned) {
Text("Matchs restant") do {
} try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
LabeledContent { Logger.error(error)
if let runningMatches { }
Text(runningMatches.count.formatted())
} else {
ProgressView()
} }
} label: {
Text("Matchs en cours") Toggle(isOn: $tournament.publishRankings) {
} Text("Publier sur Padel Club")
if let url = tournament.shareURL(.rankings) {
LabeledContent { Link(destination: url) {
if rankingPublished { Text("Accéder à la page")
Image(systemName: "checkmark") }
.foregroundStyle(.green) }
} else {
Image(systemName: "xmark")
.foregroundStyle(.logoRed)
} }
} label: { .onChange(of: tournament.publishRankings) {
Text("Classement publié") do {
} try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Toggle(isOn: $tournament.hidePointsEarned) { Logger.error(error)
Text("Masquer les points gagnés") }
}
.onChange(of: tournament.hidePointsEarned) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
} }
} }
}
if rankingPublished == false { if (editMode?.wrappedValue.isEditing == true || rankingsCalculated == false) && calculating == false {
RowButtonView("Publier le classement", role: .destructive) { Section {
_publishRankings() RowButtonView(rankingsCalculated ? "Re-calculer le classement" : "Calculer", role: .destructive) {
await _calculateRankings()
} }
} else { } footer: {
RowButtonView("Re-publier le classement", role: .destructive) { if rankingsCalculated {
_publishRankings() Text("Vos éditions seront perdus.")
} }
} }
}
if rankingsCalculated {
if rankingPublished { Section {
Section { RowButtonView("Supprimer le classement", role: .destructive) {
RowButtonView("Supprimer le classement", role: .destructive) { tournament.unsortedTeams().forEach { team in
tournament.unsortedTeams().forEach { team in team.finalRanking = nil
team.finalRanking = nil team.pointsEarned = 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 { Section {
ForEach(tournament.teamsRanked()) { team in ForEach(teamsRanked) { team in
let key = team.finalRanking ?? 0 if let key = team.finalRanking {
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
TeamRankCellView(team: team, key: key) TeamRankCellView(team: team, key: key)
} }
} }
@ -130,42 +105,16 @@ struct TournamentRankView: View {
self.runningMatches = await tournament.asyncRunningMatches(all) self.runningMatches = await tournament.asyncRunningMatches(all)
self.matchesLeft = await tournament.readyMatches(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: { .overlay(content: {
if calculating { if calculating {
ProgressView() ProgressView()
} }
}) })
.onAppear { .onAppear {
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil }) let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
if rankingPublished == false { if rankingPublished == false {
calculating = true
Task { Task {
await _calculateRankings() await _calculateRankings()
calculating = false
} }
} }
} }
@ -174,130 +123,178 @@ struct TournamentRankView: View {
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
if let url = tournament.shareURL(.rankings) { EditButton()
_actionForURL(url)
}
} }
} }
} }
struct TeamRankCellView: View { struct TeamRankCellView: View {
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration @EnvironmentObject var dataStore: DataStore
let key: Int @State private var isEditingTeam: Bool = false
@Bindable var team: TeamRegistration
@State var key: Int
var body: some View { var body: some View {
HStack { VStack(spacing: 0) {
VStack(alignment: .trailing) { if editMode?.wrappedValue.isEditing == true {
VStack(alignment: .trailing, spacing: -8.0) { if key > 1 {
ZStack(alignment: .trailing) { Button {
Text(tournament.teamCount.formatted()).hidden() key -= 1
Text(key.formatted()) team.finalRanking = key
} do {
.monospacedDigit() try dataStore.teamRegistrations.addOrUpdate(instance: team)
.font(.largeTitle) } catch {
.fontWeight(.bold) Logger.error(error)
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) } label: {
} else { Label("descendre", systemImage: "chevron.compact.up").labelStyle(.iconOnly)
Text("--")
} }
.buttonStyle(.bordered)
} }
} }
Button {
Divider() isEditingTeam = true
} label: {
VStack(alignment: .leading) { HStack {
if let name = team.name { VStack(alignment: .trailing) {
Text(name).foregroundStyle(.secondary) VStack(alignment: .trailing, spacing: -8.0) {
} ZStack(alignment: .trailing) {
Text(tournament.teamCount.formatted()).hidden()
ForEach(team.players()) { player in Text(key.formatted())
VStack(alignment: .leading, spacing: -4.0) { }
Text(player.playerLabel()).bold() .monospacedDigit()
HStack(alignment: .firstTextBaseline, spacing: 0.0) { .font(.largeTitle)
Text(player.rankLabel()) .fontWeight(.bold)
if let rank = player.getRank() { Text(key.ordinalFormattedSuffix()).font(.caption)
Text(rank.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())
if tournament.isAnimation() == false && key > 0 { .buttonStyle(.plain)
Spacer()
VStack(alignment: .trailing) { if editMode?.wrappedValue.isEditing == true {
HStack(alignment: .lastTextBaseline, spacing: 0.0) { Button {
Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always()))) key += 1
Text("pts").font(.caption) 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)
} }
} }
} .alert("Position", isPresented: $isEditingTeam) {
} TextField("Position", value: $team.finalRanking, format: .number)
.keyboardType(.numberPad)
private func _publishRankings() { .multilineTextAlignment(.trailing)
rankings.keys.sorted().forEach { rank in .frame(maxWidth: .infinity)
if let rankedTeams = rankings[rank] {
rankedTeams.forEach { team in Button("Valider") {
team.finalRanking = rank team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount)
team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 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 { private func _calculateRankings() async {
await MainActor.run {
calculating = true
}
let finalRanks = await tournament.finalRanking() let finalRanks = await tournament.finalRanking()
finalRanks.keys.sorted().forEach { rank in finalRanks.keys.sorted().forEach { rank in
if let rankedTeamIds = finalRanks[rank] { if let rankedTeamIds = finalRanks[rank] {
rankings[rank] = rankedTeamIds.compactMap { Store.main.findById($0) } rankings[rank] = rankedTeamIds.compactMap { Store.main.findById($0) }
} }
} }
}
await MainActor.run {
@ViewBuilder rankings.keys.sorted().forEach { rank in
private func _actionForURL(_ url: URL, removeSource: Bool = false) -> some View { if let rankedTeams = rankings[rank] {
Menu { rankedTeams.forEach { team in
Button { team.finalRanking = rank
UIApplication.shared.open(url) team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount)
} label: { }
Label("Voir", systemImage: "safari") }
} }
_save()
ShareLink(item: url) { calculating = false
Label("Partager le lien", systemImage: "link")
}
} label: {
Image(systemName: "square.and.arrow.up")
} }
.frame(maxWidth: .infinity)
.buttonStyle(.borderless)
} }
private func _save() { private func _save() {
do { do {

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

@ -99,7 +99,7 @@ final class ServerDataTests: XCTestCase {
return 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) let t = try await Store.main.service().post(tournament)
assert(t.event == tournament.event) assert(t.event == tournament.event)
@ -138,6 +138,7 @@ final class ServerDataTests: XCTestCase {
assert(t.hideTeamsWeight == tournament.hideTeamsWeight) assert(t.hideTeamsWeight == tournament.hideTeamsWeight)
assert(t.publishTournament == tournament.publishTournament) assert(t.publishTournament == tournament.publishTournament)
assert(t.hidePointsEarned == tournament.hidePointsEarned) assert(t.hidePointsEarned == tournament.hidePointsEarned)
assert(t.publishRankings == tournament.publishRankings)
} }
func testGroupStage() async throws { func testGroupStage() async throws {

Loading…
Cancel
Save