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