fix refresh round title

fix refresh final rankings
fix update of loser bracket mode
sync2
Raz 1 year ago
parent d5ea4f5336
commit 8d67d7efab
  1. 2
      PadelClub/Data/Match.swift
  2. 228
      PadelClub/Data/Tournament.swift
  3. 178
      PadelClub/Views/Cashier/CashierSettingsView.swift
  4. 1
      PadelClub/Views/Match/MatchDetailView.swift
  5. 81
      PadelClub/Views/Round/LoserRoundSettingsView.swift
  6. 30
      PadelClub/Views/Round/RoundView.swift
  7. 2
      PadelClub/Views/Team/Components/TeamHeaderView.swift
  8. 20
      PadelClub/Views/Team/TeamRowView.swift
  9. 141
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  10. 22
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift

@ -517,6 +517,7 @@ defer {
losingTeamId = teamScoreWalkout.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
updateFollowingMatchTeamScore() updateFollowingMatchTeamScore()
} }
@ -542,6 +543,7 @@ defer {
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
updateFollowingMatchTeamScore() updateFollowingMatchTeamScore()
} }

@ -1230,115 +1230,143 @@ defer {
teams[groupStage.index * groupStage.size + 1 + teamIndex] = [groupStageTeams[teamIndex].id] 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]
let final = rounds.last?.playedMatches().last ids.insert(winner)
if let winner = final?.winningTeamId { }
teams[1] = [winner] if let finalist = final?.losingTeamId {
ids.insert(winner) teams[2] = [finalist]
} ids.insert(finalist)
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 })
} }
print("round", round.roundTitle()) let others: [Round] = rounds.flatMap { round in
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } let losers = round.losers()
print(rounds.count, rounds.map { $0.roundTitle() }) let minimumFinalPosition = round.seedInterval()?.last ?? teamCount
return rounds if teams[minimumFinalPosition] == nil {
}.compactMap({ $0 }) teams[minimumFinalPosition] = losers.map { $0.id }
} else {
others.forEach { round in teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id })
print("round", round.roundTitle()) }
if let interval = round.seedInterval() {
print("interval", interval.localizedInterval()) print("round", round.roundTitle())
let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() } let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
print("playedMatches", playedMatches.count) print(rounds.count, rounds.map { $0.roundTitle() })
let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false }) return rounds
print("winners", winners.count) }.compactMap({ $0 })
let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false })
print("losers", losers.count) others.forEach { round in
if winners.isEmpty { print("round", round.roundTitle())
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) if let interval = round.seedInterval() {
if disabledIds.isEmpty == false { print("interval", interval.localizedInterval())
_removeStrings(from: &teams, stringsToRemove: disabledIds) let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() }
teams[interval.last] = disabledIds print("playedMatches", playedMatches.count)
let teamNames : [String] = disabledIds.compactMap { let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false })
let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0) print("winners", winners.count)
return t let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false })
}.map { $0.canonicalName } print("losers", losers.count)
print("winners.isEmpty", "\(interval.last) : ", teamNames) if winners.isEmpty {
disabledIds.forEach { let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
ids.insert($0) 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 if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() {
let teamNames : [String] = winners.compactMap { groupStageLoserBracketPlayedMatches.forEach({ match in
let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) if match.hasEnded() {
return t let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count
}.map { $0.canonicalName } teams.setOrAppend(match.winningTeamId, at: match.index)
print("winners", "\(interval.last + winners.count - 1) : ", teamNames) teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount)
winners.forEach { ids.insert($0) }
} }
})
if losers.isEmpty == false { }
_removeStrings(from: &teams, stringsToRemove: losers)
teams[interval.first + winners.count] = losers let groupStages = groupStages()
let loserTeamNames : [String] = losers.compactMap { let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified
let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 }))
return t groupStages.forEach { groupStage in
}.map { $0.canonicalName } let groupStageTeams = groupStage.teams(true)
print("losers", "\(interval.first + winners.count) : ", loserTeamNames) for (index, team) in groupStageTeams.enumerated() {
losers.forEach { ids.insert($0) } 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() { return teams
groupStageLoserBracketPlayedMatches.forEach({ match in }
if match.hasEnded() {
let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count func setRankings(finalRanks: [Int: [String]]) async -> [Int: [TeamRegistration]] {
teams.setOrAppend(match.winningTeamId, at: match.index) var rankings: [Int: [TeamRegistration]] = [:]
teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount)
}
})
}
let groupStages = groupStages() finalRanks.keys.sorted().forEach { rank in
let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified if let rankedTeamIds = finalRanks[rank] {
let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
groupStages.forEach { groupStage in rankings[rank] = teams
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) rankings.keys.sorted().forEach { rank in
if let rankedTeams = rankings[rank] {
let _index = baseRank + groupStageWidth + 1 rankedTeams.forEach { team in
if let existingTeams = teams[_index] { team.finalRanking = rank
teams[_index] = existingTeams + [team.id] team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: rank - 1, count: teamCount)
} else {
teams[_index] = [team.id]
}
} }
} }
} }
return teams do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
return rankings
} }
func lockRegistration() { func lockRegistration() {
@ -1977,6 +2005,7 @@ defer {
groupStageMatchFormat = groupStageSmartMatchFormat() groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(5) loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
matchFormat = roundSmartMatchFormat(5) matchFormat = roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee
} }
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { 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: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {

@ -11,34 +11,43 @@ import LeStorage
struct CashierSettingsView: View { struct CashierSettingsView: View {
@EnvironmentObject var dataStore: DataStore @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) { init(tournament: Tournament) {
self.tournaments = [tournament] self.tournament = tournament
_entryFee = State(wrappedValue: tournament.entryFee)
} }
var body: some View { var body: some View {
List { 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 { Section {
RowButtonView("Tout le monde est arrivé", role: .destructive) { RowButtonView("Tout le monde est arrivé", role: .destructive) {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
for tournament in self.tournaments { players.forEach { player in
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) player.hasArrived = true
players.forEach { player in }
player.hasArrived = true do {
} try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
do { } catch {
try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) Logger.error(error)
} catch {
Logger.error(error)
}
} }
} }
} footer: { } footer: {
Text("Indique tous les joueurs sont là") Text("Indique tous les joueurs sont là")
@ -46,68 +55,107 @@ struct CashierSettingsView: View {
Section { Section {
RowButtonView("Personne n'est là", role: .destructive) { RowButtonView("Personne n'est là", role: .destructive) {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
for tournament in self.tournaments { players.forEach { player in
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) player.hasArrived = false
players.forEach { player in }
player.hasArrived = false do {
} try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
do { } catch {
try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) Logger.error(error)
} catch {
Logger.error(error)
}
} }
} }
} footer: { } footer: {
Text("Indique qu'aucun joueur n'est arrivé") Text("Indique qu'aucun joueur n'est arrivé")
} }
if tournaments.count > 1 || tournaments.first?.isFree() == false { Section {
Section { RowButtonView("Tout le monde a réglé", role: .destructive) {
RowButtonView("Tout le monde a réglé", role: .destructive) { let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in
for tournament in self.tournaments { if player.hasPaid() == false {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) player.paymentType = .gift
players.forEach { player in
if player.hasPaid() == false {
player.paymentType = .gift
}
}
do {
try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
} catch {
Logger.error(error)
}
} }
} }
} footer: { do {
Text("Passe tous les joueurs qui n'ont pas réglé en offert") try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
} catch {
Logger.error(error)
}
} }
} footer: {
Section { Text("Passe tous les joueurs qui n'ont pas réglé en offert")
RowButtonView("Personne n'a réglé", role: .destructive) { }
for tournament in self.tournaments {
let store = tournament.tournamentStore Section {
RowButtonView("Personne n'a réglé", role: .destructive) {
let players = tournament.selectedPlayers() let store = tournament.tournamentStore
players.forEach { player in
player.paymentType = nil 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 { } else {
try store.playerRegistrations.addOrUpdate(contentOfs: players) Button("Gratuit") {
} catch { entryFee = nil
Logger.error(error) 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)
}
} }
} }

@ -105,6 +105,7 @@ struct MatchDetailView: View {
RowButtonView("Saisir les résultats", systemImage: "list.clipboard") { RowButtonView("Saisir les résultats", systemImage: "list.clipboard") {
self._editScores() self._editScores()
} }
.disabled(match.teams().count < 2)
} }
if self.match.currentTournament()?.isFree() == false { if self.match.currentTournament()?.isFree() == false {

@ -13,7 +13,15 @@ struct LoserRoundSettingsView: View {
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State var upperBracketRound: UpperRound @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 { var body: some View {
List { List {
Section { Section {
@ -23,25 +31,31 @@ struct LoserRoundSettingsView: View {
} }
Section { Section {
@Bindable var round: Round = upperBracketRound.round Picker(selection: $loserBracketMode) {
Picker(selection: $round.loserBracketMode) {
ForEach(LoserBracketMode.allCases) { ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0) Text($0.localizedLoserBracketMode()).tag($0)
} }
} label: { } label: {
Text("Position des perdants") Text("Position des perdants")
} }
.onChange(of: round.loserBracketMode) { .onChange(of: loserBracketMode) {
do { if upperBracketRound.round.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false {
try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round) _refreshLoserBracketMode()
} catch { } else {
Logger.error(error) confirmationRequired = true
} }
} }
} header: { } header: {
Text("Matchs de classement") Text("Matchs de classement")
} footer: { } footer: {
Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription()) if confirmationRequired == false {
Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription())
} else {
_footerViewConfirmationRequired()
.onTapGesture(perform: {
presentConfirmation = true
})
}
} }
Section { Section {
@ -81,7 +95,58 @@ struct LoserRoundSettingsView: View {
//todo proposer ici l'impression des matchs de classements peut-être? //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 { //#Preview {

@ -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) { .navigationDestination(isPresented: $showPrintScreen) {
PrintSettingsView(tournament: tournament) PrintSettingsView(tournament: tournament)
@ -327,13 +346,20 @@ struct RoundView: View {
match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) 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() let allRoundMatches = tournament.allRoundMatches()
do { do {
try self.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) try tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false self.isEditingTournamentSeed.wrappedValue = false
} }

@ -45,7 +45,7 @@ struct TeamHeaderView: View {
let positionLabel = team.positionLabel() let positionLabel = team.positionLabel()
let cutLabel = tournament.cutLabel(index: teamIndex, teamCount: teamCount) let cutLabel = tournament.cutLabel(index: teamIndex, teamCount: teamCount)
if team.isWildCard() { if team.isWildCard() {
Text("wildcard").font(.caption).italic() Text("wildcard").foregroundStyle(.red).font(.caption).italic()
Text(positionLabel ?? cutLabel) Text(positionLabel ?? cutLabel)
} else { } else {
if let positionLabel { if let positionLabel {

@ -18,15 +18,21 @@ struct TeamRowView: View {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let groupStage = team.groupStageObject() { HStack {
HStack { if let groupStage = team.groupStageObject() {
Text(groupStage.groupStageTitle(.title)) HStack {
if let finalPosition = groupStage.finalPosition(ofTeam: team) { Text(groupStage.groupStageTitle(.title))
Text((finalPosition + 1).ordinalFormatted()) 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 { if let name = team.name {

@ -14,10 +14,15 @@ struct TournamentGeneralSettingsView: View {
@Bindable var tournament: Tournament @Bindable var tournament: Tournament
@State private var tournamentName: String = "" @State private var tournamentName: String = ""
@State private var entryFee: Double? = nil @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? @FocusState private var focusedField: Tournament.CodingKeys?
let priceTags: [Double] = [15.0, 20.0, 25.0]
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_loserBracketMode = .init(wrappedValue: tournament.loserBracketMode)
_tournamentName = State(wrappedValue: tournament.name ?? "") _tournamentName = State(wrappedValue: tournament.name ?? "")
_entryFee = State(wrappedValue: tournament.entryFee) _entryFee = State(wrappedValue: tournament.entryFee)
} }
@ -25,6 +30,17 @@ struct TournamentGeneralSettingsView: View {
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
Form { 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 { Section {
TournamentDatePickerView() TournamentDatePickerView()
TournamentDurationManagerView() TournamentDurationManagerView()
@ -37,17 +53,8 @@ struct TournamentGeneralSettingsView: View {
} label: { } label: {
Text("Inscription") 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 {
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 { Section {
@ -55,42 +62,51 @@ struct TournamentGeneralSettingsView: View {
} }
Section { Section {
Picker(selection: $tournament.loserBracketMode) { Picker(selection: $loserBracketMode) {
ForEach(LoserBracketMode.allCases) { ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0) Text($0.localizedLoserBracketMode()).tag($0)
} }
} label: { } label: {
Text("Position des perdants") Text("Position des perdants")
} }
.onChange(of: tournament.loserBracketMode) { .onChange(of: loserBracketMode) {
if tournament.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false {
_save() _refreshLoserBracketMode()
} else {
let rounds = tournament.rounds() confirmationRequired = true
rounds.forEach { round in
round.loserBracketMode = tournament.loserBracketMode
}
do {
try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds)
} catch {
Logger.error(error)
} }
} }
} header: { } header: {
Text("Matchs de classement") Text("Matchs de classement")
} footer: { } footer: {
if dataStore.user.loserBracketMode != tournament.loserBracketMode { if confirmationRequired == false {
_footerView() 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: { .onTapGesture(perform: {
self.dataStore.user.loserBracketMode = tournament.loserBracketMode presentConfirmation = true
self.dataStore.saveUser()
}) })
} 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) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .toolbar(content: {
if focusedField != nil { if focusedField != nil {
@ -106,6 +122,26 @@ struct TournamentGeneralSettingsView: View {
if focusedField != nil { if focusedField != nil {
ToolbarItem(placement: .keyboard) { ToolbarItem(placement: .keyboard) {
HStack { 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() Spacer()
Button("Valider") { Button("Valider") {
if focusedField == ._name { 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 { private func _footerView() -> some View {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
+ +
Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue) 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)
}
} }

@ -311,26 +311,8 @@ struct TournamentRankView: View {
self.rankings.removeAll() self.rankings.removeAll()
let finalRanks = await tournament.finalRanking() let finalRanks = await tournament.finalRanking()
finalRanks.keys.sorted().forEach { rank in self.rankings = await tournament.setRankings(finalRanks: finalRanks)
if let rankedTeamIds = finalRanks[rank] { calculating = false
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
}
} }
private func _save() { private func _save() {

Loading…
Cancel
Save