You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
888 lines
37 KiB
888 lines
37 KiB
//
|
|
// TableStructureView.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by Razmig Sarkissian on 04/10/2023.
|
|
//
|
|
|
|
import SwiftUI
|
|
import LeStorage
|
|
import PadelClubData
|
|
|
|
struct TableStructureView: View {
|
|
var tournament: Tournament
|
|
@EnvironmentObject private var dataStore: DataStore
|
|
@Environment(\.dismiss) var dismiss
|
|
@State private var presentRefreshStructureWarning: Bool = false
|
|
@State private var teamCount: Int = 0
|
|
@State private var groupStageCount: Int = 0
|
|
@State private var teamsPerGroupStage: Int = 0
|
|
@State private var qualifiedPerGroupStage: Int = 0
|
|
@State private var groupStageAdditionalQualified: Int = 0
|
|
@State private var updatedElements: Set<StructureElement> = Set()
|
|
@State private var structurePreset: PadelTournamentStructurePreset = .manual
|
|
@State private var buildWildcards: Bool = true
|
|
@FocusState private var stepperFieldIsFocused: Bool
|
|
@State private var confirmReset: Bool = false
|
|
@State private var selectedTournament: Tournament?
|
|
@State private var initialSeedCount: Int = 0
|
|
@State private var initialSeedRound: Int = 0
|
|
@State private var showSeedRepartition: Bool = false
|
|
@State private var seedRepartition: [Int] = []
|
|
|
|
func displayWarning() -> Bool {
|
|
let unsortedTeamsCount = tournament.unsortedTeamsCount()
|
|
return tournament.shouldWarnOnlineRegistrationUpdates() && teamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount)
|
|
}
|
|
|
|
var qualifiedFromGroupStage: Int {
|
|
groupStageCount * qualifiedPerGroupStage
|
|
}
|
|
|
|
var teamsFromGroupStages: Int {
|
|
groupStageCount * teamsPerGroupStage
|
|
}
|
|
|
|
var maxMoreQualified: Int {
|
|
if teamsPerGroupStage - qualifiedPerGroupStage > 1 {
|
|
return groupStageCount
|
|
} else if teamsPerGroupStage - qualifiedPerGroupStage == 1 {
|
|
return groupStageCount - 1
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
var moreQualifiedLabel: String {
|
|
if groupStageAdditionalQualified == 0 { return "Aucun" }
|
|
return (groupStageAdditionalQualified > 1 ? "les \(groupStageAdditionalQualified)" : "le") + " " + (qualifiedPerGroupStage + 1).ordinalFormatted()
|
|
}
|
|
|
|
var maxGroupStages: Int {
|
|
teamCount / max(1, teamsPerGroupStage)
|
|
}
|
|
|
|
var tsPure: Int {
|
|
max(teamCount - groupStageCount * teamsPerGroupStage, 0)
|
|
}
|
|
|
|
var tf: Int {
|
|
max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
|
|
}
|
|
|
|
init(tournament: Tournament) {
|
|
self.tournament = tournament
|
|
_teamCount = .init(wrappedValue: tournament.teamCount)
|
|
_groupStageCount = .init(wrappedValue: tournament.groupStageCount)
|
|
_teamsPerGroupStage = .init(wrappedValue: tournament.teamsPerGroupStage)
|
|
_qualifiedPerGroupStage = .init(wrappedValue: tournament.qualifiedPerGroupStage)
|
|
_groupStageAdditionalQualified = .init(wrappedValue: tournament.groupStageAdditionalQualified)
|
|
_initialSeedCount = .init(wrappedValue: tournament.initialSeedCount)
|
|
_initialSeedRound = .init(wrappedValue: tournament.initialSeedRound)
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
if displayWarning() {
|
|
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.")
|
|
.foregroundStyle(.logoRed)
|
|
}
|
|
|
|
if tournament.state() != .build {
|
|
Section {
|
|
Picker(selection: $structurePreset) {
|
|
ForEach(PadelTournamentStructurePreset.allCases) { preset in
|
|
Text(preset.localizedStructurePresetTitle()).tag(preset)
|
|
}
|
|
} label: {
|
|
Text("Préréglage")
|
|
}
|
|
.disabled(selectedTournament != nil)
|
|
} footer: {
|
|
Text(structurePreset.localizedDescriptionStructurePresetTitle())
|
|
}
|
|
.onChange(of: structurePreset) {
|
|
_updatePreset()
|
|
}
|
|
|
|
Section {
|
|
NavigationLink {
|
|
TournamentSelectorView(selectedTournament: $selectedTournament)
|
|
.environment(tournament)
|
|
} label: {
|
|
if let selectedTournament {
|
|
TournamentCellView(tournament: selectedTournament, displayContext: .selection)
|
|
} else {
|
|
Text("À partir d'un tournoi existant")
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: selectedTournament) {
|
|
_updatePreset()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
LabeledContent {
|
|
StepperView(count: $teamCount, minimum: 4, maximum: 128) {
|
|
|
|
} submitFollowUpAction: {
|
|
_verifyValueIntegrity()
|
|
}
|
|
} label: {
|
|
Text("Nombre d'équipes")
|
|
|
|
let minimumNumberOfTeams = tournament.minimumNumberOfTeams()
|
|
if minimumNumberOfTeams > 0 {
|
|
Text("Minimum pour homologation : \(minimumNumberOfTeams)")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
LabeledContent {
|
|
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) {
|
|
|
|
} submitFollowUpAction: {
|
|
_verifyValueIntegrity()
|
|
}
|
|
} label: {
|
|
Text("Nombre de poules")
|
|
}
|
|
} footer: {
|
|
if groupStageCount > 0 {
|
|
Text("Vous pourrez modifier la taille de vos poules de manière spécifique dans l'écran des poules.")
|
|
}
|
|
}
|
|
|
|
if groupStageCount > 0 {
|
|
if (teamCount / groupStageCount) > 1 {
|
|
Section {
|
|
LabeledContent {
|
|
StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount)) {
|
|
|
|
} submitFollowUpAction: {
|
|
_verifyValueIntegrity()
|
|
}
|
|
} label: {
|
|
Text("Équipes par poule")
|
|
}
|
|
|
|
if structurePreset != .doubleGroupStage {
|
|
LabeledContent {
|
|
StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) {
|
|
|
|
} submitFollowUpAction: {
|
|
_verifyValueIntegrity()
|
|
}
|
|
} label: {
|
|
Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule")
|
|
}
|
|
|
|
if qualifiedPerGroupStage < teamsPerGroupStage - 1 {
|
|
LabeledContent {
|
|
StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) {
|
|
|
|
} submitFollowUpAction: {
|
|
_verifyValueIntegrity()
|
|
}
|
|
} label: {
|
|
Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires")
|
|
Text(moreQualifiedLabel)
|
|
}
|
|
.onChange(of: groupStageAdditionalQualified) {
|
|
if groupStageAdditionalQualified == groupStageCount {
|
|
qualifiedPerGroupStage += 1
|
|
groupStageAdditionalQualified -= groupStageCount
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if groupStageCount > 0 && teamsPerGroupStage > 0 {
|
|
if structurePreset != .doubleGroupStage {
|
|
LabeledContent {
|
|
let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2
|
|
Text(mp.formatted())
|
|
} label: {
|
|
Text("Matchs à jouer par poule")
|
|
}
|
|
} else {
|
|
LabeledContent {
|
|
let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2
|
|
Text(mp.formatted())
|
|
} label: {
|
|
Text("Matchs à jouer par poule")
|
|
Text("Première phase")
|
|
}
|
|
|
|
LabeledContent {
|
|
let mp = (groupStageCount * (groupStageCount - 1) / 2)
|
|
Text(mp.formatted())
|
|
} label: {
|
|
Text("Matchs à jouer par poule")
|
|
Text("Deuxième phase")
|
|
}
|
|
|
|
LabeledContent {
|
|
let mp = groupStageCount - 1 + teamsPerGroupStage - 1
|
|
Text(mp.formatted())
|
|
} label: {
|
|
Text("Matchs à jouer par équipe")
|
|
Text("Total")
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ContentUnavailableView("Erreur", systemImage: "divide.circle.fill", description: Text("Il y n'y a pas assez d'équipe pour ce nombre de poule."))
|
|
}
|
|
}
|
|
|
|
Section {
|
|
if groupStageCount > 0 {
|
|
if structurePreset != .doubleGroupStage {
|
|
LabeledContent {
|
|
Text(teamsFromGroupStages.formatted())
|
|
} label: {
|
|
Text("Équipes en poule")
|
|
}
|
|
|
|
|
|
LabeledContent {
|
|
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
|
|
} label: {
|
|
Text("Équipes qualifiées de poule")
|
|
}
|
|
}
|
|
}
|
|
|
|
if structurePreset != .doubleGroupStage {
|
|
LabeledContent {
|
|
Text(tsPure.formatted())
|
|
} label: {
|
|
Text("Équipes à placer en tableau")
|
|
|
|
if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
|
|
Text("Attention !").foregroundStyle(.red)
|
|
}
|
|
}
|
|
|
|
// if groupStageCount > 0 {
|
|
// LabeledContent {
|
|
// Text(tf.formatted())
|
|
// } label: {
|
|
// Text("Effectif tableau")
|
|
// }
|
|
// }
|
|
} else {
|
|
LabeledContent {
|
|
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
|
|
let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage
|
|
Text((mp1 + mp2).formatted())
|
|
} label: {
|
|
Text("Total de matchs")
|
|
}
|
|
}
|
|
|
|
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() {
|
|
Section {
|
|
Toggle("Avec wildcards", isOn: $buildWildcards)
|
|
} footer: {
|
|
Text("Padel Club réservera des places pour eux dans votre liste d'inscription.")
|
|
}
|
|
}
|
|
|
|
if tournament.rounds().isEmpty && tournament.state() == .build {
|
|
Section {
|
|
RowButtonView("Ajouter un tableau", role: .destructive) {
|
|
tournament.buildBracket(minimalBracketTeamCount: 4)
|
|
}
|
|
} footer: {
|
|
Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.")
|
|
}
|
|
}
|
|
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Reconstruire les poules", role:.destructive) {
|
|
await _save(rebuildEverything: false)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Tout refaire", role: .destructive) {
|
|
await _save(rebuildEverything: true)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Remise-à-zéro", role: .destructive) {
|
|
_reset()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Retirer toutes les équipes de poules", role: .destructive) {
|
|
tournament.unsortedTeams().forEach {
|
|
$0.resetGroupeStagePosition()
|
|
}
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Retirer toutes les équipes du tableau", role: .destructive) {
|
|
tournament.unsortedTeams().forEach {
|
|
$0.resetBracketPosition()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.focused($stepperFieldIsFocused)
|
|
.onChange(of: stepperFieldIsFocused) {
|
|
if stepperFieldIsFocused {
|
|
DispatchQueue.main.async {
|
|
UIApplication.shared.sendAction(#selector(UIResponder.selectAll(_:)), to: nil, from: nil, for: nil)
|
|
}
|
|
}
|
|
}
|
|
.toolbarBackground(.visible, for: .navigationBar)
|
|
.sheet(isPresented: $showSeedRepartition, content: {
|
|
NavigationStack {
|
|
HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in
|
|
self.seedRepartition = seedRepartition
|
|
}
|
|
}
|
|
})
|
|
.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)
|
|
} else {
|
|
updatedElements.remove(.groupStageCount)
|
|
}
|
|
|
|
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 {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Remise-à-zéro", systemImage: "trash", role: .destructive) {
|
|
confirmReset.toggle()
|
|
}
|
|
.confirmationDialog("Remise-à-zéro", isPresented: $confirmReset, titleVisibility: .visible, actions: {
|
|
Button("Tout effacer") {
|
|
_reset()
|
|
}
|
|
|
|
Button("Annuler") {
|
|
|
|
}
|
|
}, message: {
|
|
Text("Vous êtes sur le point d'effacer le tableau et les poules déjà créés et perdre les matchs et scores correspondant.")
|
|
})
|
|
}
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
if tournament.state() == .initial {
|
|
ButtonValidateView {
|
|
Task {
|
|
await _save(rebuildEverything: true)
|
|
}
|
|
}
|
|
} else {
|
|
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
|
|
ButtonValidateView(role: .destructive) {
|
|
if requirements.isEmpty {
|
|
Task {
|
|
await _save(rebuildEverything: false)
|
|
}
|
|
} else {
|
|
presentRefreshStructureWarning = true
|
|
}
|
|
}
|
|
.confirmationDialog("Refaire la structure", isPresented: $presentRefreshStructureWarning, actions: {
|
|
Button("Sauver sans reconstuire l'existant") {
|
|
_saveWithoutRebuild()
|
|
}
|
|
|
|
Button("Reconstruire les poules") {
|
|
Task {
|
|
await _save(rebuildEverything: false)
|
|
}
|
|
}
|
|
|
|
Button("Tout refaire", role: .destructive) {
|
|
Task {
|
|
await _save(rebuildEverything: true)
|
|
}
|
|
}
|
|
}, message: {
|
|
ForEach(Array(requirements)) { requirement in
|
|
Text(requirement.rebuildingRequirementMessage)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Structure")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.ifAvailableiOS26 {
|
|
if #available(iOS 26.0, *) {
|
|
$0.navigationSubtitle(tournament.tournamentTitle())
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
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.unsortedTeams().forEach {
|
|
$0.resetPositions()
|
|
}
|
|
tournament.removeWildCards()
|
|
tournament.deleteGroupStages()
|
|
tournament.deleteStructure()
|
|
|
|
if structurePreset != .manual {
|
|
structurePreset = PadelTournamentStructurePreset.manual
|
|
} else {
|
|
_updatePreset()
|
|
}
|
|
|
|
tournament.teamCount = teamCount
|
|
tournament.groupStageCount = groupStageCount
|
|
tournament.teamsPerGroupStage = teamsPerGroupStage
|
|
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
|
|
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
|
|
|
|
dataStore.tournaments.addOrUpdate(instance: tournament)
|
|
}
|
|
|
|
private func _saveWithoutRebuild() {
|
|
tournament.teamCount = teamCount
|
|
tournament.groupStageCount = groupStageCount
|
|
let updateGroupStageState = teamsPerGroupStage > tournament.teamsPerGroupStage
|
|
tournament.teamsPerGroupStage = teamsPerGroupStage
|
|
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
|
|
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
|
|
if updateGroupStageState {
|
|
tournament.groupStages().forEach { groupStage in
|
|
groupStage.updateGroupStageState()
|
|
}
|
|
}
|
|
|
|
_checkGroupStagesTeams()
|
|
|
|
do {
|
|
try dataStore.tournaments.addOrUpdate(instance: tournament)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
dismiss()
|
|
}
|
|
|
|
private func _checkGroupStagesTeams() {
|
|
if groupStageCount == 0 {
|
|
let teams = tournament.unsortedTeams().filter({ $0.inGroupStage() })
|
|
teams.forEach { team in
|
|
team.groupStagePosition = nil
|
|
team.groupStage = nil
|
|
}
|
|
do {
|
|
try tournament.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func _save(rebuildEverything: Bool = false) async {
|
|
_verifyValueIntegrity()
|
|
|
|
do {
|
|
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
|
|
|
|
tournament.teamCount = teamCount
|
|
tournament.groupStageCount = groupStageCount
|
|
tournament.teamsPerGroupStage = teamsPerGroupStage
|
|
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
|
|
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
|
|
|
|
if rebuildEverything {
|
|
if let selectedTournament {
|
|
tournament.matchFormat = selectedTournament.matchFormat
|
|
tournament.groupStageMatchFormat = selectedTournament.groupStageMatchFormat
|
|
tournament.loserBracketMatchFormat = selectedTournament.loserBracketMatchFormat
|
|
tournament.initialSeedRound = selectedTournament.initialSeedRound
|
|
tournament.initialSeedCount = selectedTournament.initialSeedCount
|
|
} else {
|
|
tournament.initialSeedRound = seedRepartition.firstIndex(where: { $0 > 0 }) ?? 0
|
|
tournament.initialSeedCount = seedRepartition.first(where: { $0 > 0 }) ?? 0
|
|
}
|
|
tournament.removeWildCards()
|
|
if structurePreset.hasWildcards(), buildWildcards {
|
|
tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket)
|
|
tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage)
|
|
}
|
|
tournament.deleteAndBuildEverything(preset: structurePreset)
|
|
|
|
if let selectedTournament {
|
|
let oldTournamentStart = selectedTournament.startDate
|
|
let newTournamentStart = tournament.startDate
|
|
let calendar = Calendar.current
|
|
let oldComponents = calendar.dateComponents([.hour, .minute, .second], from: oldTournamentStart)
|
|
var newComponents = calendar.dateComponents([.year, .month, .day], from: newTournamentStart)
|
|
|
|
newComponents.hour = oldComponents.hour
|
|
newComponents.minute = oldComponents.minute
|
|
newComponents.second = oldComponents.second ?? 0
|
|
|
|
if let updatedStartDate = calendar.date(from: newComponents) {
|
|
tournament.startDate = updatedStartDate
|
|
}
|
|
let allRounds = selectedTournament.rounds()
|
|
let allRoundsNew = tournament.rounds()
|
|
allRoundsNew.forEach { round in
|
|
if let pRound = allRounds.first(where: { r in
|
|
round.index == r.index
|
|
}) {
|
|
round.setData(from: pRound, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
|
|
}
|
|
}
|
|
|
|
let allGroupStages = selectedTournament.allGroupStages()
|
|
let allGroupStagesNew = tournament.allGroupStages()
|
|
allGroupStagesNew.forEach { groupStage in
|
|
if let pGroupStage = allGroupStages.first(where: { gs in
|
|
groupStage.index == gs.index
|
|
}) {
|
|
groupStage.setData(from: pGroupStage, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart)
|
|
}
|
|
}
|
|
|
|
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled())
|
|
}
|
|
|
|
if seedRepartition.count > 0 {
|
|
await _handleSeedRepartition()
|
|
}
|
|
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
|
|
tournament.deleteGroupStages()
|
|
tournament.buildGroupStages()
|
|
}
|
|
|
|
_checkGroupStagesTeams()
|
|
|
|
try dataStore.tournaments.addOrUpdate(instance: tournament)
|
|
|
|
dismiss()
|
|
} catch {
|
|
// Replace this implementation with code to handle the error appropriately.
|
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
|
let nsError = error as NSError
|
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
if playedMatches.count == numberOfMatches && seedCount == numberOfMatches * 2 {
|
|
continue
|
|
}
|
|
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
|
|
qualifiedPerGroupStage = selectedTournament.qualifiedPerGroupStage
|
|
groupStageAdditionalQualified = selectedTournament.groupStageAdditionalQualified
|
|
buildWildcards = tournament.level.wildcardArePossible()
|
|
} else {
|
|
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount()
|
|
groupStageCount = structurePreset.groupStageCount()
|
|
teamsPerGroupStage = structurePreset.teamsPerGroupStage()
|
|
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
|
|
groupStageAdditionalQualified = 0
|
|
buildWildcards = tournament.level.wildcardArePossible()
|
|
}
|
|
_verifyValueIntegrity()
|
|
}
|
|
|
|
private func _verifyValueIntegrity() {
|
|
if teamCount > 128 {
|
|
teamCount = 128
|
|
}
|
|
|
|
if groupStageCount > maxGroupStages {
|
|
groupStageCount = maxGroupStages
|
|
}
|
|
|
|
if teamCount < 4 {
|
|
teamCount = 4
|
|
}
|
|
|
|
if groupStageCount < 0 {
|
|
groupStageCount = 0
|
|
}
|
|
|
|
if groupStageCount > 0 {
|
|
if teamsPerGroupStage > (teamCount / groupStageCount) {
|
|
teamsPerGroupStage = (teamCount / groupStageCount)
|
|
}
|
|
|
|
if qualifiedPerGroupStage > (teamsPerGroupStage-1) {
|
|
qualifiedPerGroupStage = (teamsPerGroupStage-1)
|
|
}
|
|
|
|
if groupStageAdditionalQualified > maxMoreQualified {
|
|
groupStageAdditionalQualified = maxMoreQualified
|
|
}
|
|
|
|
if teamsPerGroupStage < 2 {
|
|
teamsPerGroupStage = 2
|
|
}
|
|
|
|
if qualifiedPerGroupStage < 0 {
|
|
qualifiedPerGroupStage = 0
|
|
}
|
|
|
|
if groupStageAdditionalQualified < 0 {
|
|
groupStageAdditionalQualified = 0
|
|
}
|
|
}
|
|
|
|
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
|
|
}
|
|
}
|
|
|
|
extension TableStructureView {
|
|
|
|
enum StructureElement: Int, Identifiable {
|
|
case teamCount
|
|
case groupStageCount
|
|
case teamsPerGroupStage
|
|
case qualifiedPerGroupStage
|
|
case groupStageAdditionalQualified
|
|
var id: Int { self.rawValue }
|
|
|
|
var requiresRebuilding: RebuildingRequirement? {
|
|
switch self {
|
|
case .teamCount:
|
|
return .all
|
|
case .groupStageCount:
|
|
return .groupStage
|
|
case .teamsPerGroupStage:
|
|
return .groupStage
|
|
case .qualifiedPerGroupStage:
|
|
return nil
|
|
case .groupStageAdditionalQualified:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
enum RebuildingRequirement: Int, Identifiable {
|
|
case groupStage
|
|
case all
|
|
|
|
var id: Int { self.rawValue }
|
|
|
|
var rebuildingRequirementMessage: String {
|
|
switch self {
|
|
case .groupStage:
|
|
return "Si vous le souhaitez, seulement les poules seront refaites. Le tableau ne sera pas modifié."
|
|
case .all:
|
|
return "Tous les matchs seront re-générés. La position des têtes de série sera remise à zéro et les poules seront reconstruites."
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//#Preview {
|
|
// NavigationStack {
|
|
// TableStructureView()
|
|
// .environment(Tournament.mock())
|
|
// .environmentObject(DataStore.shared)
|
|
// }
|
|
//}
|
|
func frenchUmpireOrder(for matches: [Int]) -> [Int] {
|
|
if matches.count <= 1 { return matches }
|
|
if matches.count == 2 { return [matches[1], matches[0]] }
|
|
|
|
var result: [Int] = []
|
|
|
|
// Step 1: Take last, then first
|
|
result.append(matches.last!)
|
|
result.append(matches.first!)
|
|
|
|
// Step 2: Get remainder (everything except first and last)
|
|
let remainder = Array(matches[1..<matches.count-1])
|
|
|
|
if remainder.isEmpty { return result }
|
|
|
|
// Step 3: Split remainder in half
|
|
let mid = remainder.count / 2
|
|
let firstHalf = Array(remainder[0..<mid])
|
|
let secondHalf = Array(remainder[mid..<remainder.count])
|
|
|
|
// Step 4: Take first of 2nd half, then last of 1st half
|
|
if !secondHalf.isEmpty {
|
|
result.append(secondHalf.first!)
|
|
}
|
|
if !firstHalf.isEmpty {
|
|
result.append(firstHalf.last!)
|
|
}
|
|
|
|
// Step 5: Build new remainder from what's left and recurse
|
|
let newRemainder = Array(firstHalf.dropLast()) + Array(secondHalf.dropFirst())
|
|
result.append(contentsOf: frenchUmpireOrder(for: newRemainder))
|
|
|
|
return result
|
|
}
|
|
|
|
/// Convenience wrapper
|
|
func frenchUmpireOrder(for matchCount: Int) -> [Int] {
|
|
let m = frenchUmpireOrder(for: Array(0..<matchCount))
|
|
return m + m.reversed()
|
|
}
|
|
|
|
|