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.
506 lines
20 KiB
506 lines
20 KiB
//
|
|
// TableStructureView.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by Razmig Sarkissian on 04/10/2023.
|
|
//
|
|
|
|
import SwiftUI
|
|
import LeStorage
|
|
|
|
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()
|
|
@State private var structurePreset: PadelTournamentStructurePreset = .manual
|
|
@State private var buildWildcards: Bool = true
|
|
@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)
|
|
}
|
|
|
|
var tsPure: Int {
|
|
max(teamCount - groupStageCount * teamsPerGroupStage, 0)
|
|
}
|
|
|
|
|
|
@ViewBuilder
|
|
var body: some View {
|
|
List {
|
|
|
|
if tournament.state() != .build {
|
|
Section {
|
|
Picker(selection: $structurePreset) {
|
|
ForEach(PadelTournamentStructurePreset.allCases) { preset in
|
|
Text(preset.localizedStructurePresetTitle()).tag(preset)
|
|
}
|
|
} label: {
|
|
Text("Préréglage")
|
|
}
|
|
} footer: {
|
|
Text(structurePreset.localizedDescriptionStructurePresetTitle())
|
|
}
|
|
.onChange(of: structurePreset) {
|
|
teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount()
|
|
groupStageCount = structurePreset.groupStageCount()
|
|
teamsPerGroupStage = structurePreset.teamsPerGroupStage()
|
|
qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage()
|
|
groupStageAdditionalQualified = 0
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
} footer: {
|
|
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))
|
|
} label: {
|
|
Text("Équipes par poule")
|
|
}
|
|
|
|
if structurePreset != .doubleGroupStage {
|
|
LabeledContent {
|
|
StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1))
|
|
} label: {
|
|
Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule")
|
|
}
|
|
|
|
if qualifiedPerGroupStage < teamsPerGroupStage - 1 {
|
|
LabeledContent {
|
|
StepperView(count: $groupStageAdditionalQualified, minimum: 1, maximum: maxMoreQualified)
|
|
} 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 {
|
|
let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
|
|
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("Nombre de têtes de série")
|
|
|
|
if tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) {
|
|
Text("Attention !").foregroundStyle(.red)
|
|
}
|
|
}
|
|
LabeledContent {
|
|
Text(tf.formatted())
|
|
} label: {
|
|
Text("Équipes en tableau final")
|
|
}
|
|
} 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")
|
|
}
|
|
}
|
|
} footer: {
|
|
if tsPure > 0 && structurePreset != .doubleGroupStage {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if structurePreset.hasWildcards() {
|
|
Section {
|
|
Toggle("Avec wildcards", isOn: $buildWildcards)
|
|
} footer: {
|
|
Text("Padel Club réservera des places pour eux dans votre liste d'inscription.")
|
|
}
|
|
}
|
|
|
|
|
|
if tournament.state() != .initial {
|
|
Section {
|
|
RowButtonView("Sauver sans reconstuire l'existant") {
|
|
_saveWithoutRebuild()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Reconstruire les poules", role:.destructive) {
|
|
_save(rebuildEverything: false)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Tout refaire", role: .destructive) {
|
|
_save(rebuildEverything: true)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
RowButtonView("Remise-à-zéro", role: .destructive) {
|
|
tournament.deleteGroupStages()
|
|
tournament.deleteStructure()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.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: {
|
|
Button("Sauver sans reconstuire l'existant") {
|
|
_saveWithoutRebuild()
|
|
}
|
|
|
|
Button("Reconstruire 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 _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()
|
|
}
|
|
}
|
|
do {
|
|
try dataStore.tournaments.addOrUpdate(instance: tournament)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
dismiss()
|
|
}
|
|
|
|
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 {
|
|
tournament.deleteAndBuildEverything(preset: structurePreset)
|
|
if structurePreset.hasWildcards(), buildWildcards {
|
|
tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket)
|
|
tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage)
|
|
}
|
|
} 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)
|
|
// }
|
|
//}
|
|
|