From 38d2f7d00566d3f0ec824251b80de7c9a3978c73 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 23 Mar 2024 08:48:03 +0100 Subject: [PATCH] add structure building --- PadelClub.xcodeproj/project.pbxproj | 4 + PadelClub/Data/Tournament.swift | 69 +++- PadelClub/Manager/PadelRule.swift | 13 + PadelClub/Views/Components/StepperView.swift | 90 +++-- .../Views/Shared/MatchFormatPickerView.swift | 16 +- .../Views/Tournament/Screen/Screen.swift | 1 + .../Screen/TableStructureView.swift | 374 ++++++++++++++++++ .../Screen/TournamentSettingsView.swift | 4 +- .../Views/Tournament/TournamentInitView.swift | 23 +- .../Views/Tournament/TournamentView.swift | 2 + 10 files changed, 526 insertions(+), 70 deletions(-) create mode 100644 PadelClub/Views/Tournament/Screen/TableStructureView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b716bbf..f8ad0e0 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */; }; FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; }; FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; + FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; @@ -218,6 +219,7 @@ FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentDatePickerView.swift; sourceTree = ""; }; FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = ""; }; FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = ""; }; + FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = ""; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; @@ -469,6 +471,7 @@ FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */, FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */, FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */, + FF8F26532BAE1E4400650388 /* TableStructureView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -809,6 +812,7 @@ C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */, FF8F263D2BAD627A00650388 /* TournamentConfiguratorView.swift in Sources */, FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */, + FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 3c45892..a2bd0d0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -9,21 +9,7 @@ import Foundation import LeStorage @Observable -class Tournament : ModelObject, Storable, Hashable { - static func == (lhs: Tournament, rhs: Tournament) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(event) - hasher.combine(creator) - hasher.combine(courtCount) - } - - @ObservationIgnored - var undoManager: Int = 0 - +class Tournament : ModelObject, Storable { static func resourceName() -> String { "tournaments" } var id: String = Store.randomId() @@ -56,6 +42,12 @@ class Tournament : ModelObject, Storable, Hashable { var teamsPerGroupStage: Int var entryFee: Double? + @ObservationIgnored + var navigationPath: [Screen] = [] + + @ObservationIgnored + var undoManager: Int = 0 + internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil) { self.event = event self.creator = creator @@ -87,9 +79,6 @@ class Tournament : ModelObject, Storable, Hashable { self.entryFee = entryFee } - @ObservationIgnored - var navigationPath: [Screen] = [] - var rounds: Int { 4 } @@ -110,6 +99,12 @@ class Tournament : ModelObject, Storable, Hashable { func settingsDescriptionLocalizedLabel() -> String { [dayDuration.formatted() + " jour\(dayDuration.pluralSuffix)", courtCount.formatted() + " terrain\(courtCount.pluralSuffix)"].joined(separator: ", ") } + + func structureDescriptionLocalizedLabel() -> String { + let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil + return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ") + } + } extension Tournament { @@ -269,3 +264,41 @@ extension Tournament { case initial } } + +extension Tournament: Hashable { + static func == (lhs: Tournament, rhs: Tournament) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(event) + hasher.combine(creator) + hasher.combine(name) + hasher.combine(startDate) + hasher.combine(endDate) + hasher.combine(creationDate) + hasher.combine(isPrivate) + hasher.combine(groupStageFormat) + hasher.combine(roundFormat) + hasher.combine(loserRoundFormat) + hasher.combine(groupStageSortMode) + hasher.combine(groupStageCount) + hasher.combine(rankSourceDate) + hasher.combine(dayDuration) + hasher.combine(teamCount) + hasher.combine(teamSorting) + hasher.combine(federalCategory) + hasher.combine(federalLevelCategory) + hasher.combine(federalAgeCategory) + hasher.combine(groupStageCourtCount) + hasher.combine(seedCount) + hasher.combine(closedRegistrationDate) + hasher.combine(groupStageAdditionalQualified) + hasher.combine(courtCount) + hasher.combine(prioritizeClubMembers) + hasher.combine(qualifiedPerGroupStage) + hasher.combine(teamsPerGroupStage) + hasher.combine(entryFee) + } +} diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index a7b0a55..4b61106 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -1029,6 +1029,19 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } } + func formattedEstimatedDuration() -> String { + Duration.seconds(estimatedDuration * 60).formatted(.units(allowed: [.minutes])) + } + + func formattedEstimatedBreakDuration() -> String { + var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes])) + if breakTime.matchCount > 1 { + label += " après \(breakTime.matchCount) match" + label += breakTime.matchCount.pluralSuffix + } + return label + } + var defaultEstimatedDuration: Int { switch self { case .twoSets: diff --git a/PadelClub/Views/Components/StepperView.swift b/PadelClub/Views/Components/StepperView.swift index 562cb2f..2e15a57 100644 --- a/PadelClub/Views/Components/StepperView.swift +++ b/PadelClub/Views/Components/StepperView.swift @@ -11,54 +11,80 @@ import SwiftUI struct StepperView: View { + var title: String? = nil @Binding var count: Int var minimum: Int? = nil - + var maximum: Int? = nil + var body: some View { - HStack { - Button(action: { - self._add() - }, label: { - Image(systemName: "plus.circle") - }) - Button(action: { - self._subtract() - }, label: { - Image(systemName: "minus.circle") - }) + VStack(spacing: 0) { + HStack(spacing: 16) { + Button(action: { + self._subtract() + }, label: { + Image(systemName: "minus.circle") + .resizable() + .scaledToFit() + .frame(width: 24) + }) + .disabled(_minusIsDisabled()) + .buttonStyle(.borderless) - }.padding(4.0) + TextField("00", value: $count, format: .number) + .keyboardType(.numberPad) + .fixedSize() + .font(.title2) + .monospacedDigit() + .onSubmit { + if let minimum, count < minimum { + count = minimum + } else if let maximum, count > maximum { + count = maximum + } + } + Button(action: { + self._add() + }, label: { + Image(systemName: "plus.circle") + .resizable() + .scaledToFit() + .frame(width: 24) + }) + .disabled(_plusIsDisabled()) + .buttonStyle(.borderless) + } + if let title { + Text(title + count.pluralSuffix).font(.caption) + } + } + .multilineTextAlignment(.trailing) } + fileprivate func _minusIsDisabled() -> Bool { + count <= (minimum ?? 0) + } + + fileprivate func _plusIsDisabled() -> Bool { + count >= (maximum ?? Int.max) + } + fileprivate func _add() { + if let maximum, self.count + 1 > maximum { + return + } self.count += 1 } fileprivate func _subtract() { - self.count -= 1 - if let minimum, self.count < minimum { - self.count = minimum + if let minimum, self.count - 1 < minimum { + return } + self.count -= 1 } } -struct StepperTestView: View { - - @State var quantity: Int = 5 - - var body: some View { - Text(quantity.formatted()) - StepperView(count: self.$quantity, minimum: 0) - } - -} - -#Preview { - StepperTestView() -} - #Preview { - StepperView(count: .constant(1)) + StepperView(title: "poule", count: .constant(1)) } diff --git a/PadelClub/Views/Shared/MatchFormatPickerView.swift b/PadelClub/Views/Shared/MatchFormatPickerView.swift index 137986d..f70c115 100644 --- a/PadelClub/Views/Shared/MatchFormatPickerView.swift +++ b/PadelClub/Views/Shared/MatchFormatPickerView.swift @@ -37,16 +37,20 @@ struct MatchFormatPickerView: View { Text("Durée").font(.caption) } HStack { - Text(matchFormat.format) + Text(matchFormat.format).font(.largeTitle) Spacer() VStack(alignment: .trailing) { - Text("~" + matchFormat.estimatedDuration.formatted() + " minutes") - Text(matchFormat.breakTime.breakTime.formatted() + " minutes de pause").foregroundStyle(.secondary) - if matchFormat.breakTime.matchCount > 1 { - Text("après \(matchFormat.breakTime.matchCount) match" + matchFormat.breakTime.matchCount.pluralSuffix).foregroundStyle(.secondary) - } + Text("~" + matchFormat.formattedEstimatedDuration()) + Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline) } } } } } + + +#Preview { + List { + MatchFormatPickerView(headerLabel: "Test", matchFormat: .constant(MatchFormat.superTie)) + } +} diff --git a/PadelClub/Views/Tournament/Screen/Screen.swift b/PadelClub/Views/Tournament/Screen/Screen.swift index 56417d5..f743966 100644 --- a/PadelClub/Views/Tournament/Screen/Screen.swift +++ b/PadelClub/Views/Tournament/Screen/Screen.swift @@ -11,4 +11,5 @@ enum Screen: String, Codable { case inscription case groupStage case settings + case structure } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift new file mode 100644 index 0000000..1f0478b --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -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 = 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) + } +} diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index 7225e29..fa35daf 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -42,13 +42,11 @@ struct TournamentSettingsView: View { Text("Nom du tournoi") } + TournamentLevelPickerView() TournamentDurationManagerView() - TournamentFieldsManagerView() - TournamentDatePickerView() TournamentFormatSelectionView() - TournamentLevelPickerView() } .navigationTitle("Réglages") .toolbarBackground(.visible, for: .navigationBar) diff --git a/PadelClub/Views/Tournament/TournamentInitView.swift b/PadelClub/Views/Tournament/TournamentInitView.swift index a9f3987..5de7349 100644 --- a/PadelClub/Views/Tournament/TournamentInitView.swift +++ b/PadelClub/Views/Tournament/TournamentInitView.swift @@ -23,17 +23,18 @@ struct TournamentInitView: View { } footer: { Text("La date, la catégorie, le niveau, le nombre de terrain, les formats, etc.") } -// -// Section { -// NavigationLink { -// TableStructureView(tournament: tournament) -// } label: { -// Label("Structure", systemImage: "hammer") -// .badge(tournament.structureDescriptionLocalizedLabel) -// } -// } footer: { -// Text("Nombre d'équipes, de poules, de qualifiés sortant, etc.") -// } + + Section { + NavigationLink(value: Screen.structure) { + LabeledContent { + Text(tournament.structureDescriptionLocalizedLabel()) + } label: { + Label("Structure", systemImage: "hammer") + } + } + } footer: { + Text("Nombre d'équipes, de poules, de qualifiés sortant, etc.") + } } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 8ca0cb5..9057883 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -27,6 +27,8 @@ struct TournamentView: View { .navigationDestination(for: Screen.self, destination: { screen in Group { switch screen { + case .structure: + TableStructureView() case .settings: TournamentSettingsView() case .inscription: