diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 73e77a8..629c7aa 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -764,6 +764,12 @@ FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; + FFC2DB202E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; }; + FFC2DB212E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; }; + FFC2DB222E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; }; + FFC2DB242E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; }; + FFC2DB252E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; }; + FFC2DB262E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; }; FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */; }; FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; @@ -1167,6 +1173,8 @@ FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = ""; }; + FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSelectorView.swift; sourceTree = ""; }; + FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadManagerView.swift; sourceTree = ""; }; FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundView.swift; sourceTree = ""; }; FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = ""; }; FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = ""; }; @@ -1823,6 +1831,8 @@ FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */, FFE8B5BE2DAA325400BDE966 /* RefundResultsView.swift */, FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */, + FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */, + FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */, ); path = Components; sourceTree = ""; @@ -2371,6 +2381,7 @@ FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, + FFC2DB252E97DD0A00869317 /* HeadManagerView.swift in Sources */, FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */, FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, @@ -2433,6 +2444,7 @@ FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, + FFC2DB212E97D00300869317 /* TournamentSelectorView.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, @@ -2646,6 +2658,7 @@ FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */, FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF4CBFA12C996C0600151637 /* LoserRoundSettingsView.swift in Sources */, + FFC2DB262E97DD0A00869317 /* HeadManagerView.swift in Sources */, FF4CBFA22C996C0600151637 /* Persistence.swift in Sources */, FF4CBFA32C996C0600151637 /* CloseDatePicker.swift in Sources */, FF4CBFA42C996C0600151637 /* BarButtonView.swift in Sources */, @@ -2708,6 +2721,7 @@ FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */, FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */, FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */, + FFC2DB202E97D00300869317 /* TournamentSelectorView.swift in Sources */, FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */, FFE8B63B2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */, @@ -2899,6 +2913,7 @@ FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */, FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF70FB202C90584900129CC2 /* LoserRoundSettingsView.swift in Sources */, + FFC2DB242E97DD0A00869317 /* HeadManagerView.swift in Sources */, FF70FB212C90584900129CC2 /* Persistence.swift in Sources */, FF70FB222C90584900129CC2 /* CloseDatePicker.swift in Sources */, FF70FB232C90584900129CC2 /* BarButtonView.swift in Sources */, @@ -2961,6 +2976,7 @@ FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */, FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */, FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */, + FFC2DB222E97D00300869317 /* TournamentSelectorView.swift in Sources */, FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */, FFE8B63A2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */, diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index f6f7a34..c4fe24e 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -315,8 +315,9 @@ struct PlanningView: View { matchesToUpdate = matches.filter({ selectedIds.contains($0.stringId) }) } label: { Text("Modifier") + .frame(maxWidth: .infinity) } - .buttonStyle(.borderless) + .buttonStyle(.borderedProminent) .disabled(selectedIds.isEmpty) } } diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 6d10c35..ab9b3f9 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -88,6 +88,19 @@ struct EditingTeamView: View { team.uniqueRandomIndex = 0 } + var hasRegisteredOnline: Binding { + Binding { + team.hasRegisteredOnline() + } set: { hasRegisteredOnline in + let players = team.players() + players.forEach { player in + player.registeredOnline = hasRegisteredOnline + } + + tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) + } + } + var body: some View { List { Section { @@ -126,11 +139,10 @@ struct EditingTeamView: View { } .headerProminence(.increased) - if team.hasRegisteredOnline() || team.hasPaidOnline() { + if team.hasRegisteredOnline() || team.hasPaidOnline() || tournament.enableOnlineRegistration { Section { - LabeledContent { - Text(team.hasRegisteredOnline() ? "Oui" : "Non") - } label: { + + Toggle(isOn: hasRegisteredOnline) { Text("Inscrits en ligne") } diff --git a/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift b/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift index 1dd587e..bb0d8c9 100644 --- a/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift +++ b/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift @@ -101,7 +101,7 @@ struct ConsolationTournamentImportView: View { Picker(selection: $selectedTournament) { Text("Aucun tournoi").tag(nil as Tournament?) ForEach(tournaments) { tournament in - TournamentCellView(tournament: tournament).tag(tournament) + TournamentCellView(tournament: tournament, displayContext: .selection).tag(tournament) } } label: { if selectedTournament == nil { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 3686ac1..1fa1dc5 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -231,6 +231,7 @@ struct AddTeamView: View { .disabled(_limitPlayerCount()) .foregroundStyle(.master) .labelStyle(.titleAndIcon) + .frame(maxWidth: .infinity) .buttonBorderShape(.capsule) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift new file mode 100644 index 0000000..926340a --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -0,0 +1,43 @@ +// +// HeadManagerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 09/10/2025. +// + +import SwiftUI +import LeStorage +import PadelClubData + +struct HeadManagerView: View { + + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject private var dataStore: DataStore + @Environment(\.dismiss) var dismiss + @Binding var initialSeedRound: Int + @Binding var initialSeedCount: Int + + var body: some View { + List { + Section { + Picker(selection: $initialSeedRound) { + ForEach((0...10)) { + Text(RoundRule.roundName(fromRoundIndex: $0)).tag($0) + } + } label: { + Text("Premier tour") + } + .onChange(of: initialSeedRound) { + initialSeedCount = RoundRule.numberOfMatches(forRoundIndex: initialSeedRound) + } + + LabeledContent { + StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)) + } label: { + Text("Têtes de série") + } + } + } + .navigationTitle("Têtes de série") + } +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift new file mode 100644 index 0000000..bc69b46 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift @@ -0,0 +1,51 @@ +// +// TournamentSelectorView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 09/10/2025. +// + +import SwiftUI +import LeStorage +import PadelClubData + +struct TournamentSelectorView: View { + @Binding var selectedTournament: Tournament? + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject private var dataStore: DataStore + @Environment(\.dismiss) var dismiss + + @State private var allTournaments: Bool = false + + var tournaments: [Tournament] { + dataStore.tournaments.filter({ $0.isDeleted == false && $0.id != tournament.id && (allTournaments || ($0.endDate != nil && $0.level == tournament.level && $0.category == tournament.category && $0.age == tournament.age)) }).sorted(by: \.startDate, order: .descending) + } + + var body: some View { + List { + Picker(selection: $selectedTournament) { + Text("Aucun tournoi").tag(nil as Tournament?) + ForEach(tournaments) { tournament in + TournamentCellView(tournament: tournament, displayContext: .selection).tag(tournament) + } + } label: { + Text("Sélection d'un tournoi") + } + .labelsHidden() + .pickerStyle(.inline) + } + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Toggle("Tous les tournois", isOn: $allTournaments) + } label: { + Label("Filtre", systemImage: "line.3.horizontal.decrease") + } + } + }) + .navigationTitle("Sélection d'un tournoi") + .onChange(of: selectedTournament) { oldValue, newValue in + dismiss() + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 2033927..f7ff58a 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -10,7 +10,7 @@ import LeStorage import PadelClubData struct TableStructureView: View { - @Environment(Tournament.self) private var tournament: Tournament + var tournament: Tournament @EnvironmentObject private var dataStore: DataStore @Environment(\.dismiss) var dismiss @State private var presentRefreshStructureWarning: Bool = false @@ -24,7 +24,10 @@ struct TableStructureView: View { @State private var buildWildcards: Bool = true @FocusState private var stepperFieldIsFocused: Bool @State private var confirmReset: Bool = false - + @State private var selectedTournament: Tournament? + @State private var initialSeedCount: Int = 0 + @State private var initialSeedRound: Int = 0 + func displayWarning() -> Bool { let unsortedTeamsCount = tournament.unsortedTeamsCount() return tournament.shouldWarnOnlineRegistrationUpdates() && teamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount) @@ -60,9 +63,18 @@ struct TableStructureView: View { var tsPure: Int { max(teamCount - groupStageCount * teamsPerGroupStage, 0) } - - - @ViewBuilder + + init(tournament: Tournament) { + self.tournament = tournament + _teamCount = .init(wrappedValue: tournament.teamCount) + _groupStageCount = .init(wrappedValue: tournament.groupStageCount) + _teamsPerGroupStage = .init(wrappedValue: tournament.teamsPerGroupStage) + _qualifiedPerGroupStage = .init(wrappedValue: tournament.qualifiedPerGroupStage) + _groupStageAdditionalQualified = .init(wrappedValue: tournament.groupStageAdditionalQualified) + _initialSeedCount = .init(wrappedValue: tournament.initialSeedCount) + _initialSeedRound = .init(wrappedValue: tournament.initialSeedRound) + } + var body: some View { List { if displayWarning() { @@ -79,9 +91,33 @@ struct TableStructureView: View { } label: { Text("Préréglage") } + .disabled(selectedTournament != nil) + + NavigationLink { + TournamentSelectorView(selectedTournament: $selectedTournament) + .environment(tournament) + } label: { + if let selectedTournament { + TournamentCellView(tournament: selectedTournament, displayContext: .selection) + } else { + Text("À partir d'un tournoi existant") + } + } + + NavigationLink { + HeadManagerView(initialSeedRound: $initialSeedRound, initialSeedCount: $initialSeedCount) + .environment(tournament) + } label: { + Text("Configuration des têtes de série") + } + .disabled(selectedTournament != nil) + } footer: { Text(structurePreset.localizedDescriptionStructurePresetTitle()) } + .onChange(of: selectedTournament) { + _updatePreset() + } .onChange(of: structurePreset) { _updatePreset() } @@ -226,7 +262,7 @@ struct TableStructureView: View { LabeledContent { Text(tsPure.formatted()) } label: { - Text("Nombre de têtes de série") + Text("Équipes à placer en tableau") if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) { Text("Attention !").foregroundStyle(.red) @@ -235,7 +271,12 @@ struct TableStructureView: View { LabeledContent { Text(tf.formatted()) } label: { - Text("Équipes en tableau final") + Text("Effectif") + } + LabeledContent { + Text(RoundRule.teamsInFirstRound(forTeams: tf).formatted()) + } label: { + Text("Dimension") } } else { LabeledContent { @@ -266,7 +307,7 @@ struct TableStructureView: View { } } - if tournament.rounds().isEmpty { + if tournament.rounds().isEmpty && tournament.state() == .build { Section { RowButtonView("Ajouter un tableau", role: .destructive) { tournament.buildBracket(minimalBracketTeamCount: 4) @@ -312,13 +353,6 @@ struct TableStructureView: View { } } .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) @@ -490,12 +524,79 @@ struct TableStructureView: View { tournament.groupStageAdditionalQualified = groupStageAdditionalQualified if rebuildEverything { + if let selectedTournament { + tournament.matchFormat = selectedTournament.matchFormat + tournament.groupStageMatchFormat = selectedTournament.groupStageMatchFormat + tournament.loserBracketMatchFormat = selectedTournament.loserBracketMatchFormat + tournament.initialSeedRound = selectedTournament.initialSeedRound + tournament.initialSeedCount = selectedTournament.initialSeedCount + } else { + tournament.initialSeedRound = initialSeedRound + tournament.initialSeedCount = initialSeedCount + } tournament.removeWildCards() if structurePreset.hasWildcards(), buildWildcards { tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket) tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage) } tournament.deleteAndBuildEverything(preset: structurePreset) + + if let selectedTournament { + let oldTournamentStart = selectedTournament.startDate + let newTournamentStart = tournament.startDate + let calendar = Calendar.current + let oldComponents = calendar.dateComponents([.hour, .minute, .second], from: oldTournamentStart) + var newComponents = calendar.dateComponents([.year, .month, .day], from: newTournamentStart) + + newComponents.hour = oldComponents.hour + newComponents.minute = oldComponents.minute + newComponents.second = oldComponents.second ?? 0 + + if let updatedStartDate = calendar.date(from: newComponents) { + tournament.startDate = updatedStartDate + } + let allRounds = selectedTournament.rounds() + let allRoundsNew = tournament.rounds() + allRoundsNew.forEach { round in + if let pRound = allRounds.first(where: { r in + round.index == r.index + }) { + round.setData(from: pRound, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart) + } + } + + let allGroupStages = selectedTournament.allGroupStages() + let allGroupStagesNew = tournament.allGroupStages() + allGroupStagesNew.forEach { groupStage in + if let pGroupStage = allGroupStages.first(where: { gs in + groupStage.index == gs.index + }) { + groupStage.setData(from: pGroupStage, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart) + } + } + + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) + } else { + if initialSeedRound > 0 { + if let round = tournament.rounds().first(where: { $0.index == initialSeedRound }) { + let seedSorted = frenchUmpireOrder(for: RoundRule.numberOfMatches(forRoundIndex: round.index)) + print(seedSorted) + seedSorted.prefix(initialSeedCount).forEach { index in + if let match = round._matches()[safe:index] { + if match.indexInRound() < RoundRule.numberOfMatches(forRoundIndex: round.index) / 2 { + match.previousMatch(.one)?.disableMatch() + } else { + match.previousMatch(.two)?.disableMatch() + } + } + } + + if initialSeedCount > 0 { + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) + } + } + } + } } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() tournament.buildGroupStages() @@ -515,12 +616,21 @@ struct TableStructureView: View { } private func _updatePreset() { - teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount() - groupStageCount = structurePreset.groupStageCount() - teamsPerGroupStage = structurePreset.teamsPerGroupStage() - qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() - groupStageAdditionalQualified = 0 - buildWildcards = tournament.level.wildcardArePossible() + if let selectedTournament { + teamCount = selectedTournament.teamCount + groupStageCount = selectedTournament.groupStageCount + teamsPerGroupStage = selectedTournament.teamsPerGroupStage + qualifiedPerGroupStage = selectedTournament.qualifiedPerGroupStage + groupStageAdditionalQualified = selectedTournament.groupStageAdditionalQualified + buildWildcards = tournament.level.wildcardArePossible() + } else { + teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount() + groupStageCount = structurePreset.groupStageCount() + teamsPerGroupStage = structurePreset.teamsPerGroupStage() + qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() + groupStageAdditionalQualified = 0 + buildWildcards = tournament.level.wildcardArePossible() + } } private func _verifyValueIntegrity() { @@ -620,3 +730,41 @@ extension TableStructureView { // .environmentObject(DataStore.shared) // } //} + +func frenchUmpireOrder(for matches: [Int]) -> [Int] { + guard matches.count > 1 else { return matches } + + // Base case + if matches.count == 2 { + return [matches[1], matches[0]] // bottom, top + } + + let n = matches.count + let mid = n / 2 + + let topHalf = Array(matches[0.. [Int] { + return frenchUmpireOrder(for: Array(0..