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.
 
 
PadelClub/PadelClub/Views/Tournament/Screen/TableStructureView.swift

360 lines
14 KiB

//
// 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<StructureElement> = 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
}
}
.disabled(updatedElements.isEmpty)
.confirmationDialog("Mise à jour de la structure", isPresented: $presentRefreshStructureWarning, actions: {
if requirements.allSatisfy({ $0 == .groupStage }) {
Button("Mettre à jour les poules") {
_save(rebuildEverything: false)
}
}
Button("Tout mettre à jour", 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 })
if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything {
tournament.deleteStructure()
}
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything {
tournament.buildStructure()
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
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 mis à jour. 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)
}
}