diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 5476029..8b109da 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -517,6 +517,7 @@ defer { losingTeamId = teamScoreWalkout.teamRegistration groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() + currentTournament()?.updateTournamentState() updateFollowingMatchTeamScore() } @@ -542,6 +543,7 @@ defer { groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() + currentTournament()?.updateTournamentState() updateFollowingMatchTeamScore() } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index aa16e3a..8dd11f1 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1230,115 +1230,143 @@ defer { teams[groupStage.index * groupStage.size + 1 + teamIndex] = [groupStageTeams[teamIndex].id] } } + } else { - return teams - } - - let final = rounds.last?.playedMatches().last - if let winner = final?.winningTeamId { - teams[1] = [winner] - ids.insert(winner) - } - if let finalist = final?.losingTeamId { - teams[2] = [finalist] - ids.insert(finalist) - } - - 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 }) + let final = rounds.last?.playedMatches().last + if let winner = final?.winningTeamId { + teams[1] = [winner] + ids.insert(winner) + } + if let finalist = final?.losingTeamId { + teams[2] = [finalist] + ids.insert(finalist) } - print("round", round.roundTitle()) - let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } - print(rounds.count, rounds.map { $0.roundTitle() }) - return rounds - }.compactMap({ $0 }) - - others.forEach { round in - print("round", round.roundTitle()) - if let interval = round.seedInterval() { - print("interval", interval.localizedInterval()) - let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() } - print("playedMatches", playedMatches.count) - let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false }) - print("winners", winners.count) - let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false }) - print("losers", losers.count) - if winners.isEmpty { - let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) - if disabledIds.isEmpty == false { - _removeStrings(from: &teams, stringsToRemove: disabledIds) - teams[interval.last] = disabledIds - let teamNames : [String] = disabledIds.compactMap { - let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0) - return t - }.map { $0.canonicalName } - print("winners.isEmpty", "\(interval.last) : ", teamNames) - disabledIds.forEach { - ids.insert($0) + 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() }) + return rounds + }.compactMap({ $0 }) + + others.forEach { round in + print("round", round.roundTitle()) + if let interval = round.seedInterval() { + print("interval", interval.localizedInterval()) + let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() } + print("playedMatches", playedMatches.count) + let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false }) + print("winners", winners.count) + let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false }) + print("losers", losers.count) + if winners.isEmpty { + let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) + if disabledIds.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: disabledIds) + teams[interval.last] = disabledIds + let teamNames : [String] = disabledIds.compactMap { + let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0) + return t + }.map { $0.canonicalName } + print("winners.isEmpty", "\(interval.last) : ", teamNames) + disabledIds.forEach { + ids.insert($0) + } + } + } else { + if winners.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: winners) + teams[interval.first + winners.count - 1] = winners + let teamNames : [String] = winners.compactMap { + let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) + return t + }.map { $0.canonicalName } + print("winners", "\(interval.last + winners.count - 1) : ", teamNames) + winners.forEach { ids.insert($0) } + } + + if losers.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: losers) + teams[interval.first + winners.count] = losers + let loserTeamNames : [String] = losers.compactMap { + let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) + return t + }.map { $0.canonicalName } + print("losers", "\(interval.first + winners.count) : ", loserTeamNames) + losers.forEach { ids.insert($0) } } } - } else { - if winners.isEmpty == false { - _removeStrings(from: &teams, stringsToRemove: winners) - teams[interval.first + winners.count - 1] = winners - let teamNames : [String] = winners.compactMap { - let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) - return t - }.map { $0.canonicalName } - print("winners", "\(interval.last + winners.count - 1) : ", teamNames) - winners.forEach { ids.insert($0) } + } + } + + if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() { + groupStageLoserBracketPlayedMatches.forEach({ match in + if match.hasEnded() { + let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count + teams.setOrAppend(match.winningTeamId, at: match.index) + teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount) } - - if losers.isEmpty == false { - _removeStrings(from: &teams, stringsToRemove: losers) - teams[interval.first + winners.count] = losers - let loserTeamNames : [String] = losers.compactMap { - let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) - return t - }.map { $0.canonicalName } - print("losers", "\(interval.first + winners.count) : ", loserTeamNames) - losers.forEach { ids.insert($0) } + }) + } + + let groupStages = groupStages() + let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) + groupStages.forEach { groupStage in + let groupStageTeams = groupStage.teams(true) + for (index, team) in groupStageTeams.enumerated() { + if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { + let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) + + let _index = baseRank + groupStageWidth + 1 + if let existingTeams = teams[_index] { + teams[_index] = existingTeams + [team.id] + } else { + teams[_index] = [team.id] + } } } } } - if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() { - groupStageLoserBracketPlayedMatches.forEach({ match in - if match.hasEnded() { - let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count - teams.setOrAppend(match.winningTeamId, at: match.index) - teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount) - } - }) - } + return teams + } + + func setRankings(finalRanks: [Int: [String]]) async -> [Int: [TeamRegistration]] { + var rankings: [Int: [TeamRegistration]] = [:] - let groupStages = groupStages() - let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) - groupStages.forEach { groupStage in - let groupStageTeams = groupStage.teams(true) - for (index, team) in groupStageTeams.enumerated() { - if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { - let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) - - let _index = baseRank + groupStageWidth + 1 - if let existingTeams = teams[_index] { - teams[_index] = existingTeams + [team.id] - } else { - teams[_index] = [team.id] - } + finalRanks.keys.sorted().forEach { rank in + if let rankedTeamIds = finalRanks[rank] { + let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } + rankings[rank] = teams + } + } + + rankings.keys.sorted().forEach { rank in + if let rankedTeams = rankings[rank] { + rankedTeams.forEach { team in + team.finalRanking = rank + team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: rank - 1, count: teamCount) } } } - return teams + do { + try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + } catch { + Logger.error(error) + } + + + return rankings } func lockRegistration() { @@ -1977,6 +2005,7 @@ defer { groupStageMatchFormat = groupStageSmartMatchFormat() loserBracketMatchFormat = loserBracketSmartMatchFormat(5) matchFormat = roundSmartMatchFormat(5) + entryFee = tournamentLevel.entryFee } func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { @@ -2150,6 +2179,19 @@ defer { } + func updateTournamentState() { + Task { + if hasEnded() { + let fr = await finalRanking() + _ = await setRankings(finalRanks: fr) + } + } + } + + func allLoserRoundMatches() -> [Match] { + rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } + } + // MARK: - func insertOnServer() throws { diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index ed5e63b..fd51e73 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -11,34 +11,43 @@ import LeStorage struct CashierSettingsView: View { @EnvironmentObject var dataStore: DataStore + @State private var entryFee: Double? = nil + @Bindable var tournament: Tournament + @FocusState private var focusedField: Tournament.CodingKeys? + let priceTags: [Double] = [15.0, 20.0, 25.0] - var tournaments: [Tournament] - - init(tournaments: [Tournament]) { - self.tournaments = tournaments - } - init(tournament: Tournament) { - self.tournaments = [tournament] + self.tournament = tournament + _entryFee = State(wrappedValue: tournament.entryFee) } var body: some View { List { + Section { + LabeledContent { + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($focusedField, equals: ._entryFee) + } label: { + Text("Inscription") + } + } footer: { + Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") + } + Section { RowButtonView("Tout le monde est arrivé", role: .destructive) { - - for tournament in self.tournaments { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = true - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + player.hasArrived = true + } + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) } - } } footer: { Text("Indique tous les joueurs sont là") @@ -46,68 +55,107 @@ struct CashierSettingsView: View { Section { RowButtonView("Personne n'est là", role: .destructive) { - - for tournament in self.tournaments { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = false - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + player.hasArrived = false + } + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) } - } } footer: { Text("Indique qu'aucun joueur n'est arrivé") } - if tournaments.count > 1 || tournaments.first?.isFree() == false { - Section { - RowButtonView("Tout le monde a réglé", role: .destructive) { - - for tournament in self.tournaments { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - if player.hasPaid() == false { - player.paymentType = .gift - } - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + if player.hasPaid() == false { + player.paymentType = .gift } - } - } footer: { - Text("Passe tous les joueurs qui n'ont pas réglé en offert") + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } } - - Section { - RowButtonView("Personne n'a réglé", role: .destructive) { - for tournament in self.tournaments { - let store = tournament.tournamentStore - - let players = tournament.selectedPlayers() - players.forEach { player in - player.paymentType = nil + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") + } + + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + let store = tournament.tournamentStore + + let players = tournament.selectedPlayers() + players.forEach { player in + player.paymentType = nil + } + do { + try store.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") + } + } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + + ToolbarItem(placement: .keyboard) { + HStack { + if tournament.isFree() { + ForEach(priceTags, id: \.self) { priceTag in + Button(priceTag.formatted(.currency(code: "EUR"))) { + entryFee = priceTag + tournament.entryFee = priceTag + focusedField = nil + } + .buttonStyle(.bordered) } - do { - try store.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) + } else { + Button("Gratuit") { + entryFee = nil + tournament.entryFee = nil + focusedField = nil } + .buttonStyle(.bordered) + } + Spacer() + Button("Valider") { + tournament.entryFee = entryFee + focusedField = nil + } + .buttonStyle(.bordered) } - } footer: { - Text("Remet à zéro le type d'encaissement de tous les joueurs") } } } + .onChange(of: tournament.entryFee) { + _save() + } + } + + private func _save() { + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 235e648..f8766d1 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -105,6 +105,7 @@ struct MatchDetailView: View { RowButtonView("Saisir les résultats", systemImage: "list.clipboard") { self._editScores() } + .disabled(match.teams().count < 2) } if self.match.currentTournament()?.isFree() == false { diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index 08952fe..3b4706a 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -13,7 +13,15 @@ struct LoserRoundSettingsView: View { @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(Tournament.self) var tournament: Tournament @State var upperBracketRound: UpperRound + @State private var confirmationRequired: Bool = false + @State private var presentConfirmation: Bool = false + @State private var loserBracketMode: LoserBracketMode + init(upperBracketRound: UpperRound) { + self.upperBracketRound = upperBracketRound + _loserBracketMode = .init(wrappedValue: upperBracketRound.round.loserBracketMode) + } + var body: some View { List { Section { @@ -23,25 +31,31 @@ struct LoserRoundSettingsView: View { } Section { - @Bindable var round: Round = upperBracketRound.round - Picker(selection: $round.loserBracketMode) { + Picker(selection: $loserBracketMode) { ForEach(LoserBracketMode.allCases) { Text($0.localizedLoserBracketMode()).tag($0) } } label: { Text("Position des perdants") } - .onChange(of: round.loserBracketMode) { - do { - try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round) - } catch { - Logger.error(error) + .onChange(of: loserBracketMode) { + if upperBracketRound.round.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false { + _refreshLoserBracketMode() + } else { + confirmationRequired = true } } } header: { Text("Matchs de classement") } footer: { - Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription()) + if confirmationRequired == false { + Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription()) + } else { + _footerViewConfirmationRequired() + .onTapGesture(perform: { + presentConfirmation = true + }) + } } Section { @@ -81,7 +95,58 @@ struct LoserRoundSettingsView: View { //todo proposer ici l'impression des matchs de classements peut-être? } + .confirmationDialog("Attention", isPresented: $presentConfirmation, actions: { + Button("Confirmer", role: .destructive) { + _refreshLoserBracketMode() + confirmationRequired = false + } + + Button("Annuler", role: .cancel) { + loserBracketMode = upperBracketRound.round.loserBracketMode + } + + }) + + } + + private func _refreshLoserBracketMode() { + let matches = upperBracketRound.round.loserRoundsAndChildren().flatMap({ $0._matches() }) + matches.forEach { match in + match.resetTeamScores(outsideOf: []) + match.resetMatch() + if loserBracketMode == .automatic { + match.updateTeamScores() + } + match.confirmed = false + } + + upperBracketRound.round.loserBracketMode = loserBracketMode + + if loserBracketMode == .automatic { + matches.forEach { match in + match.updateTeamScores() + } + } + + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: matches) + } catch { + Logger.error(error) + } + + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round) + } catch { + Logger.error(error) + } } + + private func _footerViewConfirmationRequired() -> some View { + Text("Au moins un match de classement est terminé, en modifiant ce réglage, les résultats de ces matchs de classement seront perdus.") + + + Text(" Modifier quand même ?").foregroundStyle(.red) + } + } //#Preview { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 37aa4c5..540b94a 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -275,6 +275,25 @@ struct RoundView: View { } } } + + if upperRound.round.index == 0, tournament.hasEnded() { + NavigationLink(value: Screen.rankings) { + LabeledContent { + if tournament.publishRankings == false { + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.logoYellow) + } else { + Image(systemName: "checkmark") + .foregroundStyle(.green) + } + } label: { + Text("Classement final des équipes") + if tournament.publishRankings == false { + Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) + } + } + } + } } .navigationDestination(isPresented: $showPrintScreen) { PrintSettingsView(tournament: tournament) @@ -327,13 +346,20 @@ struct RoundView: View { match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) } } + + let loserMatches = self.upperRound.loserMatches() + loserMatches.forEach { match in + match.name = match.roundTitle() + } + let allRoundMatches = tournament.allRoundMatches() + do { - try self.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + try tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) } catch { Logger.error(error) } - + if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } diff --git a/PadelClub/Views/Team/Components/TeamHeaderView.swift b/PadelClub/Views/Team/Components/TeamHeaderView.swift index 6e896a1..4109b18 100644 --- a/PadelClub/Views/Team/Components/TeamHeaderView.swift +++ b/PadelClub/Views/Team/Components/TeamHeaderView.swift @@ -45,7 +45,7 @@ struct TeamHeaderView: View { let positionLabel = team.positionLabel() let cutLabel = tournament.cutLabel(index: teamIndex, teamCount: teamCount) if team.isWildCard() { - Text("wildcard").font(.caption).italic() + Text("wildcard").foregroundStyle(.red).font(.caption).italic() Text(positionLabel ?? cutLabel) } else { if let positionLabel { diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 7ee5da2..1e11da3 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -18,15 +18,21 @@ struct TeamRowView: View { TeamWeightView(team: team, teamPosition: teamPosition) } label: { VStack(alignment: .leading) { - if let groupStage = team.groupStageObject() { - HStack { - Text(groupStage.groupStageTitle(.title)) - if let finalPosition = groupStage.finalPosition(ofTeam: team) { - Text((finalPosition + 1).ordinalFormatted()) + HStack { + if let groupStage = team.groupStageObject() { + HStack { + Text(groupStage.groupStageTitle(.title)) + if let finalPosition = groupStage.finalPosition(ofTeam: team) { + Text((finalPosition + 1).ordinalFormatted()) + } } + } else if let round = team.initialRound() { + Text(round.roundTitle(.wide)) + } + + if team.isWildCard() { + Text("wildcard").italic().foregroundStyle(.red).font(.caption) } - } else if let round = team.initialRound() { - Text(round.roundTitle(.wide)) } if let name = team.name { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 1258938..f7096d1 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -14,10 +14,15 @@ struct TournamentGeneralSettingsView: View { @Bindable var tournament: Tournament @State private var tournamentName: String = "" @State private var entryFee: Double? = nil + @State private var confirmationRequired: Bool = false + @State private var presentConfirmation: Bool = false + @State private var loserBracketMode: LoserBracketMode @FocusState private var focusedField: Tournament.CodingKeys? - + let priceTags: [Double] = [15.0, 20.0, 25.0] + init(tournament: Tournament) { self.tournament = tournament + _loserBracketMode = .init(wrappedValue: tournament.loserBracketMode) _tournamentName = State(wrappedValue: tournament.name ?? "") _entryFee = State(wrappedValue: tournament.entryFee) } @@ -25,6 +30,17 @@ struct TournamentGeneralSettingsView: View { var body: some View { @Bindable var tournament = tournament Form { + + Section { + TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) + .lineLimit(2) + .frame(maxWidth: .infinity) + .keyboardType(.alphabet) + .focused($focusedField, equals: ._name) + } header: { + Text("Nom du tournoi") + } + Section { TournamentDatePickerView() TournamentDurationManagerView() @@ -37,17 +53,8 @@ struct TournamentGeneralSettingsView: View { } label: { Text("Inscription") } - - } - - Section { - TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) - .lineLimit(2) - .frame(maxWidth: .infinity) - .keyboardType(.alphabet) - .focused($focusedField, equals: ._name) - } header: { - Text("Nom du tournoi") + } footer: { + Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") } Section { @@ -55,42 +62,51 @@ struct TournamentGeneralSettingsView: View { } Section { - Picker(selection: $tournament.loserBracketMode) { + Picker(selection: $loserBracketMode) { ForEach(LoserBracketMode.allCases) { Text($0.localizedLoserBracketMode()).tag($0) } } label: { Text("Position des perdants") } - .onChange(of: tournament.loserBracketMode) { - - _save() - - let rounds = tournament.rounds() - rounds.forEach { round in - round.loserBracketMode = tournament.loserBracketMode - } - - do { - try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) - } catch { - Logger.error(error) + .onChange(of: loserBracketMode) { + if tournament.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false { + _refreshLoserBracketMode() + } else { + confirmationRequired = true } } } header: { Text("Matchs de classement") } footer: { - if dataStore.user.loserBracketMode != tournament.loserBracketMode { - _footerView() + if confirmationRequired == false { + if dataStore.user.loserBracketMode != tournament.loserBracketMode { + _footerView() + .onTapGesture(perform: { + self.dataStore.user.loserBracketMode = tournament.loserBracketMode + self.dataStore.saveUser() + }) + } else { + Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) + } + } else { + _footerViewConfirmationRequired() .onTapGesture(perform: { - self.dataStore.user.loserBracketMode = tournament.loserBracketMode - self.dataStore.saveUser() + presentConfirmation = true }) - } else { - Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) } } } + .confirmationDialog("Attention", isPresented: $presentConfirmation, actions: { + Button("Confirmer", role: .destructive) { + _refreshLoserBracketMode() + confirmationRequired = false + } + Button("Annuler", role: .cancel) { + loserBracketMode = tournament.loserBracketMode + } + + }) .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { @@ -106,6 +122,26 @@ struct TournamentGeneralSettingsView: View { if focusedField != nil { ToolbarItem(placement: .keyboard) { HStack { + if focusedField == ._entryFee { + if tournament.isFree() { + ForEach(priceTags, id: \.self) { priceTag in + Button(priceTag.formatted(.currency(code: "EUR"))) { + entryFee = priceTag + tournament.entryFee = priceTag + focusedField = nil + } + .buttonStyle(.bordered) + } + } else { + Button("Gratuit") { + entryFee = nil + tournament.entryFee = nil + focusedField = nil + } + .buttonStyle(.bordered) + + } + } Spacer() Button("Valider") { if focusedField == ._name { @@ -166,9 +202,50 @@ struct TournamentGeneralSettingsView: View { } } + private func _refreshLoserBracketMode() { + tournament.loserBracketMode = loserBracketMode + _save() + + let rounds = tournament.rounds() + rounds.forEach { round in + let matches = round.loserRoundsAndChildren().flatMap({ $0._matches() }) + matches.forEach { match in + match.resetTeamScores(outsideOf: []) + match.resetMatch() + match.confirmed = false + } + + round.loserBracketMode = tournament.loserBracketMode + + if loserBracketMode == .automatic { + matches.forEach { match in + match.updateTeamScores() + } + } + + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: matches) + } catch { + Logger.error(error) + } + } + + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) + } catch { + Logger.error(error) + } + } + private func _footerView() -> some View { Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) + Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue) } + + private func _footerViewConfirmationRequired() -> some View { + Text("Au moins un match de classement est terminé, en modifiant ce réglage, les résultats de ces matchs de classement seront perdus.") + + + Text(" Modifier quand même ?").foregroundStyle(.red) + } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index dc06df0..ba33832 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -311,26 +311,8 @@ struct TournamentRankView: View { self.rankings.removeAll() let finalRanks = await tournament.finalRanking() - finalRanks.keys.sorted().forEach { rank in - if let rankedTeamIds = finalRanks[rank] { - let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } - self.rankings[rank] = teams - } - } - - await MainActor.run { - rankings.keys.sorted().forEach { rank in - if let rankedTeams = rankings[rank] { - rankedTeams.forEach { team in - team.finalRanking = rank - team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount) - } - } - } - _save() - - calculating = false - } + self.rankings = await tournament.setRankings(finalRanks: finalRanks) + calculating = false } private func _save() {