// // TableStructureView.swift // Padel Tournament // // Created by Razmig Sarkissian on 04/10/2023. // import SwiftUI struct TableStructureView: View { @Environment(Tournament.self) private 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 = Set() @FocusState private var stepperFieldIsFocused: Bool 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") + " meilleur\(groupStageAdditionalQualified.pluralSuffix) " + (qualifiedPerGroupStage + 1).ordinalFormatted() } var maxGroupStages: Int { teamCount / max(1, teamsPerGroupStage) } @ViewBuilder var body: some View { List { Section { LabeledContent { StepperView(count: $teamCount, minimum: 4, maximum: 128) } label: { Text("Nombre d'équipes") } LabeledContent { StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) } label: { Text("Nombre de poules") } } if groupStageCount > 0 { if (teamCount / groupStageCount) > 1 { Section { LabeledContent { StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount)) } label: { Text("Équipes par poule") } LabeledContent { StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) } label: { Text("Qualifiés par poule") } if qualifiedPerGroupStage < teamsPerGroupStage - 1 { LabeledContent { StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) } label: { Text("Qualifiés supplémentaires") Text(moreQualifiedLabel) } .onChange(of: groupStageAdditionalQualified) { if groupStageAdditionalQualified == groupStageCount { qualifiedPerGroupStage += 1 groupStageAdditionalQualified -= groupStageCount } } } if groupStageCount > 0 && teamsPerGroupStage > 0 { LabeledContent { let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 Text(mp.formatted()) } label: { Text("Matchs à jouer par poule") } } } } else { ContentUnavailableView("Erreur", systemImage: "divide.circle.fill", description: Text("Il y n'y a pas assez d'équipe pour ce nombre de poule.")) } } Section { let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0) if groupStageCount > 0 { LabeledContent { Text(teamsFromGroupStages.formatted()) } label: { Text("Équipes en poule") } LabeledContent { Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted()) } label: { Text("Équipes qualifiées de poule") } } LabeledContent { let tsPure = max(teamCount - groupStageCount * teamsPerGroupStage, 0) Text(tsPure.formatted()) } label: { Text("Nombre de têtes de série") } LabeledContent { Text(tf.formatted()) } label: { Text("Équipes en tableau final") } } } .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) .onAppear { teamCount = tournament.teamCount groupStageCount = tournament.groupStageCount teamsPerGroupStage = tournament.teamsPerGroupStage qualifiedPerGroupStage = tournament.qualifiedPerGroupStage groupStageAdditionalQualified = tournament.groupStageAdditionalQualified } .onChange(of: teamCount) { if teamCount != tournament.teamCount { updatedElements.insert(.teamCount) } else { updatedElements.remove(.teamCount) } } .onChange(of: groupStageCount) { if groupStageCount != tournament.groupStageCount { updatedElements.insert(.groupStageCount) } else { updatedElements.remove(.groupStageCount) } } .onChange(of: teamsPerGroupStage) { if teamsPerGroupStage != tournament.teamsPerGroupStage { updatedElements.insert(.teamsPerGroupStage) } else { updatedElements.remove(.teamsPerGroupStage) } } .onChange(of: qualifiedPerGroupStage) { if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage { updatedElements.insert(.qualifiedPerGroupStage) } else { updatedElements.remove(.qualifiedPerGroupStage) } } .onChange(of: groupStageAdditionalQualified) { if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified { updatedElements.insert(.groupStageAdditionalQualified) } else { updatedElements.remove(.groupStageAdditionalQualified) } } .toolbar { ToolbarItem(placement: .keyboard) { Button("Confirmer") { stepperFieldIsFocused = false _verifyValueIntegrity() } } ToolbarItem(placement: .confirmationAction) { if tournament.state() == .initial { ButtonValidateView { _save(rebuildEverything: true) } } else { let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) ButtonValidateView(role: .destructive) { if requirements.isEmpty { _save(rebuildEverything: false) } else { presentRefreshStructureWarning = true } } .confirmationDialog("Refaire la structure", isPresented: $presentRefreshStructureWarning, actions: { if requirements.allSatisfy({ $0 == .groupStage }) { Button("Refaire les poules") { _save(rebuildEverything: false) } } Button("Tout refaire", role: .destructive) { _save(rebuildEverything: true) } }, message: { ForEach(Array(requirements)) { requirement in Text(requirement.rebuildingRequirementMessage) } }) } } } .navigationTitle("Structure") .navigationBarTitleDisplayMode(.inline) } private func _save(rebuildEverything: Bool = false) { _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 == false && requirements.contains(.all)) || rebuildEverything { tournament.deleteAndBuildEverything() } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() tournament.buildGroupStages() } 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 _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 < 1 { qualifiedPerGroupStage = 1 } if groupStageAdditionalQualified < 0 { groupStageAdditionalQualified = 0 } } } } 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) // } //}