diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index d742100..940a091 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -160,14 +160,7 @@ struct RoundSettingsView: View { } private func _removeRound(_ lastRound: Round) async { - await MainActor.run { - let teams = lastRound.seeds() - teams.forEach { team in - team.resetBracketPosition() - } - tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) - tournamentStore?.rounds.delete(instance: lastRound) - } + await tournament.removeRound(lastRound) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift index d53e10b..3fa4ed0 100644 --- a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -79,7 +79,7 @@ struct HeadManagerView: View { valueToAppend = theory } else { let lastValue = teamsPerRound.last ?? 0 - var newValueToAppend = theory + var newValueToAppend = theory == 0 ? maxAssignable / 2 : theory if theory > maxAssignable || theory < 0 { newValueToAppend = valueToAppend / 2 } @@ -102,33 +102,62 @@ struct HeadManagerView: View { Picker(selection: $selectedSeedRound) { Text("Choisir").tag(nil as Int?) ForEach(seedRepartition.indices, id: \.self) { idx in - Text(RoundRule.roundName(fromRoundIndex: idx)).tag(idx) + Text(RoundRule.roundName(fromRoundIndex: idx, displayStyle: .short)).tag(idx) } } label: { - Text("Tour contenant la meilleure tête de série") + Text("Tour de la tête de série n°1") } .onChange(of: selectedSeedRound) { seedRepartition = Self.place(heads: heads, teamsInBracket: teamsInBracket, initialSeedRound: selectedSeedRound) } - } footer: { - FooterButtonView("remise à zéro") { - selectedSeedRound = nil - } } Section { LabeledContent { - Text(teamsInBracket.formatted()) + Text(heads.formatted()) } label: { - Text("Effectif du tableau") + Text("Équipes à placer en tableau") } + if (teamsInBracket - heads) > 0 { + LabeledContent { + Text((teamsInBracket - heads).formatted()) + } label: { + Text("Qualifiés entrants") + } + } LabeledContent { Text(leftToPlace.formatted()) } label: { Text("Restant à placer") } + + LabeledContent { + let matchCount = seedRepartition.enumerated().map { (index, value) in + var result = 0 + var count = value + if count == 0, let selectedSeedRound, index < selectedSeedRound { + let t = RoundRule.numberOfMatches(forRoundIndex: index) + result = RoundRule.cumulatedNumberOfMatches(forTeams: t * 2) + } else { + if index == seedRepartition.count - 1 { + count += (teamsInBracket - heads) + } else if index == seedRepartition.count - 2 { + count += ((seedRepartition[index + 1] + (teamsInBracket - heads)) / 2) + } else { + count += (seedRepartition[index + 1]) + } + result = RoundRule.cumulatedNumberOfMatches(forTeams: count) + } + + return result + } + .reduce(0, +) + Text(matchCount.formatted()) + } label: { + Text("Matchs estimés") + } } Section { @@ -155,13 +184,13 @@ struct HeadManagerView: View { if headsLeft - maxAssignable > 0 { valueToAppend = valueToAppend - (headsLeft - maxAssignable) } - print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)") +// print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)") seedRepartition.append(valueToAppend) } } } } - .navigationTitle("Têtes de série") + .navigationTitle("Répartition") .toolbar { ToolbarItem(placement: .topBarTrailing) { ButtonValidateView(title: "Valider") { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 21f0542..f6d10ee 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -267,13 +267,13 @@ struct TableStructureView: View { } } - if groupStageCount > 0 { - LabeledContent { - Text(tf.formatted()) - } label: { - Text("Effectif") - } - } +// if groupStageCount > 0 { +// LabeledContent { +// Text(tf.formatted()) +// } label: { +// Text("Effectif tableau") +// } +// } } else { LabeledContent { let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount @@ -283,16 +283,30 @@ struct TableStructureView: View { Text("Total de matchs") } } - } footer: { - if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0 { - if tsPure > teamCount / 2 { - Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif.").foregroundStyle(.red) - } else if tsPure < teamCount / 8 { - Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif.").foregroundStyle(.red) - } else if tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified { - Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red) + + LabeledContent { + FooterButtonView("configurer") { + showSeedRepartition = true + } + } label: { + if tournament.state() == .build { + Text("Répartition des équipes") + } else if selectedTournament != nil { + Text("La configuration du tournoi séléctionné sera utilisée.") + } else { + Text(_seeds()) } } + .onAppear { + if seedRepartition.isEmpty && tournament.state() == .initial && selectedTournament == nil { + seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil) + } + } + + } footer: { + if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0, tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified { + Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red) + } } if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() { @@ -303,27 +317,6 @@ struct TableStructureView: View { } } - if tournament.state() != .build { - Section { - LabeledContent { - Image(systemName: seedRepartition.isEmpty ? "xmark" : "checkmark") - } label: { - FooterButtonView("Configuration du tableau") { - showSeedRepartition = true - } - .disabled(selectedTournament != nil) - } - } footer: { - if seedRepartition.isEmpty { - Text("Aucune répartition n'a été indiqué, vous devrez réserver ou placer les têtes de séries dans le tableau manuellement.") - } else { - FooterButtonView("Supprimer la configuration", role: .destructive) { - seedRepartition = [] - } - } - } - } - if tournament.rounds().isEmpty && tournament.state() == .build { Section { RowButtonView("Ajouter un tableau", role: .destructive) { @@ -336,6 +329,12 @@ struct TableStructureView: View { if tournament.state() != .initial { + if seedRepartition.isEmpty == false { + RowButtonView("Modifier la répartition des équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") { + await _handleSeedRepartition() + } + } + Section { RowButtonView("Sauver sans reconstuire l'existant") { _saveWithoutRebuild() @@ -370,13 +369,6 @@ struct TableStructureView: View { } } .toolbarBackground(.visible, for: .navigationBar) - .onChange(of: teamCount) { - if teamCount != tournament.teamCount { - updatedElements.insert(.teamCount) - } else { - updatedElements.remove(.teamCount) - } - } .sheet(isPresented: $showSeedRepartition, content: { NavigationStack { HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in @@ -384,6 +376,14 @@ struct TableStructureView: View { } } }) + .onChange(of: teamCount) { + if teamCount != tournament.teamCount { + updatedElements.insert(.teamCount) + } else { + updatedElements.remove(.teamCount) + } + _verifyValueIntegrity() + } .onChange(of: groupStageCount) { if groupStageCount != tournament.groupStageCount { updatedElements.insert(.groupStageCount) @@ -394,25 +394,31 @@ struct TableStructureView: View { if structurePreset.isFederalPreset(), groupStageCount == 0 { teamCount = structurePreset.tableDimension() } + _verifyValueIntegrity() } .onChange(of: teamsPerGroupStage) { if teamsPerGroupStage != tournament.teamsPerGroupStage { updatedElements.insert(.teamsPerGroupStage) } else { updatedElements.remove(.teamsPerGroupStage) - } } + } + _verifyValueIntegrity() + } .onChange(of: qualifiedPerGroupStage) { if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage { updatedElements.insert(.qualifiedPerGroupStage) } else { updatedElements.remove(.qualifiedPerGroupStage) - } } + } + _verifyValueIntegrity() + } .onChange(of: groupStageAdditionalQualified) { if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified { updatedElements.insert(.groupStageAdditionalQualified) } else { updatedElements.remove(.groupStageAdditionalQualified) } + _verifyValueIntegrity() } .toolbar { if tournament.state() != .initial { @@ -484,6 +490,19 @@ struct TableStructureView: View { } } + + private func _seeds() -> String { + if seedRepartition.isEmpty || seedRepartition.reduce(0, +) == 0 { + return "Aucune configuration" + } + return seedRepartition.enumerated().compactMap { (index, count) in + if count > 0 { + return RoundRule.roundName(fromRoundIndex: index) + " : \(count)" + } else { + return nil + } + }.joined(separator: ", ") + } private func _reset() { tournament.removeWildCards() @@ -573,12 +592,6 @@ struct TableStructureView: View { } tournament.deleteAndBuildEverything(preset: structurePreset) - if seedRepartition.count > 0 { - while tournament.rounds().count < seedRepartition.count { - await tournament.addNewRound(tournament.rounds().count) - } - } - if let selectedTournament { let oldTournamentStart = selectedTournament.startDate let newTournamentStart = tournament.startDate @@ -614,66 +627,10 @@ struct TableStructureView: View { } tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) - } else { - for (index, seedCount) in seedRepartition.enumerated() { - if let round = tournament.rounds().first(where: { $0.index == index }) { - let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index) - let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index) - let playedMatches = round.playedMatches().map { $0.index - baseIndex } - let allMatches = round._matches() - let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in - playedMatches.contains(index) - }).prefix(seedCount) - for (index, value) in seedSorted.enumerated() { - let isOpponentTurn = index >= playedMatches.count - if let match = allMatches[safe:value] { - if match.index - baseIndex < numberOfMatches / 2 { - if isOpponentTurn { - match.previousMatch(.two)?.disableMatch() - } else { - match.previousMatch(.one)?.disableMatch() - } - } else { - if isOpponentTurn { - match.previousMatch(.one)?.disableMatch() - } else { - match.previousMatch(.two)?.disableMatch() - } - } - } - } - - if seedCount > 0 { - tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches()) - tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches()) - round.deleteLoserBracket() - round.buildLoserBracket() - round.loserRounds().forEach { loserRound in - loserRound.disableUnplayedLoserBracketMatches() - } - } - } - - } -// if initialSeedRound > 0 { -// if let round = tournament.rounds().first(where: { $0.index == initialSeedRound }) { -// let seedSorted = frenchUmpireOrder(for: RoundRule.numberOfMatches(forRoundIndex: round.index)) -// print(seedSorted) -// seedSorted.prefix(initialSeedCount).forEach { index in -// if let match = round._matches()[safe:index] { -// if match.indexInRound() < RoundRule.numberOfMatches(forRoundIndex: round.index) / 2 { -// match.previousMatch(.one)?.disableMatch() -// } else { -// match.previousMatch(.two)?.disableMatch() -// } -// } -// } -// -// if initialSeedCount > 0 { -// tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) -// } -// } -// } + } + + if seedRepartition.count > 0 { + await _handleSeedRepartition() } } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() @@ -693,8 +650,63 @@ struct TableStructureView: View { } } + private func _handleSeedRepartition() async { + while tournament.rounds().count < seedRepartition.count { + await tournament.addNewRound(tournament.rounds().count) + } + + if seedRepartition.reduce(0, +) > 0 { + let rounds = tournament.rounds() + let roundsToDelete = rounds.suffix(rounds.count - seedRepartition.count) + for round in roundsToDelete { + await tournament.removeRound(round) + } + } + + for (index, seedCount) in seedRepartition.enumerated() { + if let round = tournament.rounds().first(where: { $0.index == index }) { + let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index) + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index) + let playedMatches = round.playedMatches().map { $0.index - baseIndex } + let allMatches = round._matches() + let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in + playedMatches.contains(index) + }).prefix(seedCount) + for (index, value) in seedSorted.enumerated() { + let isOpponentTurn = index >= playedMatches.count + if let match = allMatches[safe:value] { + if match.index - baseIndex < numberOfMatches / 2 { + if isOpponentTurn { + match.previousMatch(.two)?.disableMatch() + } else { + match.previousMatch(.one)?.disableMatch() + } + } else { + if isOpponentTurn { + match.previousMatch(.one)?.disableMatch() + } else { + match.previousMatch(.two)?.disableMatch() + } + } + } + } + + if seedCount > 0 { + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches()) + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches()) + round.deleteLoserBracket() + round.buildLoserBracket() + round.loserRounds().forEach { loserRound in + loserRound.disableUnplayedLoserBracketMatches() + } + } + } + } + } + private func _updatePreset() { if let selectedTournament { + seedRepartition = [] teamCount = selectedTournament.teamCount groupStageCount = selectedTournament.groupStageCount teamsPerGroupStage = selectedTournament.teamsPerGroupStage @@ -709,6 +721,7 @@ struct TableStructureView: View { groupStageAdditionalQualified = 0 buildWildcards = tournament.level.wildcardArePossible() } + _verifyValueIntegrity() } private func _verifyValueIntegrity() { @@ -754,6 +767,7 @@ struct TableStructureView: View { } } + seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil) } }