parent
c89c212c11
commit
38d2f7d005
@ -0,0 +1,374 @@ |
|||||||
|
// |
||||||
|
// 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").foregroundStyle(.secondary).font(.caption) |
||||||
|
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 { |
||||||
|
Button("Valider") { |
||||||
|
_save(rebuildEverything: true) |
||||||
|
dismiss() |
||||||
|
} |
||||||
|
.clipShape(Capsule()) |
||||||
|
.buttonStyle(.bordered) |
||||||
|
.disabled(updatedElements.isEmpty) |
||||||
|
} else { |
||||||
|
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) |
||||||
|
|
||||||
|
Button("Valider", role: .destructive) { |
||||||
|
if requirements.isEmpty { |
||||||
|
_save(rebuildEverything: false) |
||||||
|
dismiss() |
||||||
|
} else { |
||||||
|
presentRefreshStructureWarning = true |
||||||
|
} |
||||||
|
} |
||||||
|
.clipShape(Capsule()) |
||||||
|
.buttonStyle(.bordered) |
||||||
|
.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) |
||||||
|
dismiss() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Button("Tout mettre à jour", role: .destructive) { |
||||||
|
_save(rebuildEverything: true) |
||||||
|
dismiss() |
||||||
|
} |
||||||
|
|
||||||
|
}, message: { |
||||||
|
ForEach(Array(requirements)) { requirement in |
||||||
|
Text(requirement.rebuildingRequirementMessage) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.navigationTitle("Structure") |
||||||
|
.navigationBarTitleDisplayMode(.inline) |
||||||
|
} |
||||||
|
|
||||||
|
private func _save(rebuildEverything: Bool = false) { |
||||||
|
do { |
||||||
|
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) |
||||||
|
|
||||||
|
if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything { |
||||||
|
// if let matches = tournament.matchs { |
||||||
|
// tournament.removeFromMatchs(matches) |
||||||
|
// } |
||||||
|
// tournament.additionalRounds = 0 |
||||||
|
// tournament.orderedEntries.forEach { entrant in |
||||||
|
// entrant.initialPosition = 0 |
||||||
|
// } |
||||||
|
// tournament.hiddenRounds = nil |
||||||
|
} |
||||||
|
|
||||||
|
tournament.teamCount = teamCount |
||||||
|
tournament.groupStageCount = groupStageCount |
||||||
|
tournament.teamsPerGroupStage = teamsPerGroupStage |
||||||
|
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage |
||||||
|
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified |
||||||
|
|
||||||
|
if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything { |
||||||
|
// tournament.build() |
||||||
|
} else if (rebuildEverything == false && requirements.contains(.groupStage)) { |
||||||
|
// tournament.buildGroupStages() |
||||||
|
} |
||||||
|
|
||||||
|
try dataStore.tournaments.addOrUpdate(instance: tournament) |
||||||
|
|
||||||
|
} 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) |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue