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.
356 lines
13 KiB
356 lines
13 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
|
|
}
|
|
}
|
|
.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)
|
|
// }
|
|
//}
|
|
|