diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 7af5681..e245548 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.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) diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 942fdd9..3707917 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -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? { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b2bcbd5..cbf4b03 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -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) 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 = Set() @@ -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) } + } } } } diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index 5f00aee..dd36e02 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -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 } diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index beea497..230feb9 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -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 { 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 diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 08071b8..f983ad4 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -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) diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 024c4b6..3983879 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -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: { diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index f7db5b3..d2176ca 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -91,7 +91,6 @@ struct MainView: View { } .environmentObject(dataStore) .task { - await self._checkSourceFileAvailability() if Store.main.hasToken() { do { try await dataStore.clubs.loadDataFromServerIfAllowed() diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 10733b2..37680cb 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -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 { diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 948d2ea..453dff3 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -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) diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index 8b7b578..445a1b6 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -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 { + 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.") +// } } } } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 2abf66d..5c7866d 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -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 diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 08c655b..f47bb12 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -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 { diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index b443928..1353b44 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -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) diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index f042080..27baa28 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -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 {