diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index a40ff6e..ddd9264 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ 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 */; }; + FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */; }; FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; }; FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F72BCE78C70080F940 /* CashierView.swift */; }; FF9267FA2BCE78EC0080F940 /* CashierDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */; }; @@ -511,6 +512,7 @@ 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 = ""; }; + FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTeamView.swift; sourceTree = ""; }; FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = ""; }; FF9267F72BCE78C70080F940 /* CashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierView.swift; sourceTree = ""; }; FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierDetailView.swift; sourceTree = ""; }; @@ -993,6 +995,7 @@ isa = PBXGroup; children = ( FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */, + FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */, FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */, FF8F26532BAE1E4400650388 /* TableStructureView.swift */, FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */, @@ -1640,6 +1643,7 @@ FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */, FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */, + FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */, FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index bd0f2fd..09c31c9 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1049,6 +1049,10 @@ defer { Logger.error(error) } + + if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty { + setGroupStageTeams(randomize: groupStageSortMode == .random) + } } func maximumCourtsPerGroupSage() -> Int { @@ -1679,34 +1683,37 @@ defer { deleteGroupStages() buildGroupStages() } else { - - let max = groupStages.map { $0.size }.reduce(0,+) - var chunks = selectedSortedTeams().suffix(max).chunked(into: groupStageCount) - for (index, _) in chunks.enumerated() { - if randomize { - chunks[index].shuffle() - } else if index % 2 != 0 { - chunks[index].reverse() - } - - print("Equipes \(chunks[index].map { $0.weight })") - for (jIndex, _) in chunks[index].enumerated() { - print("Position \(index + 1) Poule \(groupStages[jIndex].index)") - chunks[index][jIndex].groupStage = groupStages[jIndex].id - chunks[index][jIndex].groupStagePosition = index - } + setGroupStageTeams(randomize: randomize) + groupStages.forEach { $0.buildMatches() } + } + } + + func setGroupStageTeams(randomize: Bool) { + let groupStages = groupStages() + let max = groupStages.map { $0.size }.reduce(0,+) + var chunks = selectedSortedTeams().suffix(max).chunked(into: groupStageCount) + for (index, _) in chunks.enumerated() { + if randomize { + chunks[index].shuffle() + } else if index % 2 != 0 { + chunks[index].reverse() } - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) - } catch { - Logger.error(error) + print("Equipes \(chunks[index].map { $0.weight })") + for (jIndex, _) in chunks[index].enumerated() { + print("Position \(index + 1) Poule \(groupStages[jIndex].index)") + chunks[index][jIndex].groupStage = groupStages[jIndex].id + chunks[index][jIndex].groupStagePosition = index } - groupStages.forEach { $0.buildMatches() } + } + + do { + try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + } catch { + Logger.error(error) } } - func isFree() -> Bool { return entryFee == nil || entryFee == 0 } diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index b726306..0c1114b 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -107,9 +107,16 @@ struct GenericDestinationPickerView: } .onAppear { if let selectedDestination { - proxy.scrollTo(selectedDestination.id) + proxy.scrollTo(selectedDestination.id, anchor: .trailing) } else { - proxy.scrollTo("settings") + proxy.scrollTo("settings", anchor: .leading) + } + } + .onChange(of: selectedDestination) { + if let selectedDestination { + proxy.scrollTo(selectedDestination.id, anchor: .trailing) + } else { + proxy.scrollTo("settings", anchor: .leading) } } } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 2e55ce4..42a545d 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -250,8 +250,7 @@ struct ActivityView: View { Section { ForEach(getRunningTournaments()) { tournament in NavigationLink { - InscriptionManagerView(tournament: tournament, pasteString: pasteString) - .environment(tournament) + AddTeamView(tournament: tournament, pasteString: pasteString, editedTeam: nil) } label: { VStack(alignment: .leading) { Text(tournament.tournamentTitle()) diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift new file mode 100644 index 0000000..1242505 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -0,0 +1,538 @@ +// +// AddTeamView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 15/07/2024. +// + +import SwiftUI +import LeStorage + +struct AddTeamView: View { + + @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) var dismiss + + @FetchRequest( + sortDescriptors: [], + animation: .default) + private var fetchPlayers: FetchedResults + + var tournament: Tournament + var cancelShouldDismiss: Bool = false + + @State private var searchField: String = "" + @State private var presentSearch: Bool = false + @State private var presentPlayerSearch: Bool = false + @State private var presentPlayerCreation: Bool = false + @State private var presentImportView: Bool = false + @State private var createdPlayers: Set = Set() + @State private var createdPlayerIds: Set = Set() + @State private var editedTeam: TeamRegistration? + @State private var pasteString: String? + @State private var selectionSearchField: String? + @State private var autoSelect: Bool = false + @State private var teamsHash: Int? + @State private var presentationCount: Int = 0 + @State private var confirmDuplicate: Bool = false + + var tournamentStore: TournamentStore { + return self.tournament.tournamentStore + } + + init(tournament: Tournament, pasteString: String? = nil, editedTeam: TeamRegistration?) { + self.tournament = tournament + _editedTeam = .init(wrappedValue: editedTeam) + if let team = editedTeam { + var createdPlayers: Set = Set() + var createdPlayerIds: Set = Set() + + team.unsortedPlayers().forEach { player in + createdPlayers.insert(player) + createdPlayerIds.insert(player.id) + } + + _createdPlayers = .init(wrappedValue: createdPlayers) + _createdPlayerIds = .init(wrappedValue: createdPlayerIds) + } + + if let pasteString { + _pasteString = .init(wrappedValue: pasteString) + _fetchPlayers = FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: Self._pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption)) + _autoSelect = .init(wrappedValue: true) + cancelShouldDismiss = true + } + } + + // Function to create a simple hash from a list of IDs + private func _simpleHash(ids: [String]) -> Int { + // Combine the hash values of each string + return ids.reduce(0) { $0 ^ $1.hashValue } + } + + // Function to check if two lists of IDs produce different hashes + private func _areDifferent(ids1: [String], ids2: [String]) -> Bool { + return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) + } + + private func _setHash() async { + #if DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + let selectedSortedTeams = tournament.selectedSortedTeams() + if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { + self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) + } + } + + private func _handleHashDiff() async { + let selectedSortedTeams = tournament.selectedSortedTeams() + let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) + if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) { + self.teamsHash = newHash + if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false { + self.tournament.shouldVerifyBracket = true + self.tournament.shouldVerifyGroupStage = true + + let waitingList = self.tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true) + waitingList.forEach { team in + if team.bracketPosition != nil || team.groupStagePosition != nil { + team.resetPositions() + } + } + + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: waitingList) + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } + } + + var body: some View { + _buildingTeamView() + .navigationBarBackButtonHidden(true) + .alert("Cette équipe existe déjà", isPresented: $confirmDuplicate) { + Button("Créer l'équipe quand même") { + _createTeam(checkDuplicates: false) + } + + Button("Annuler", role: .cancel) { + confirmDuplicate = false + } + + } message: { + Text("Cette équipe existe déjà dans votre liste d'inscription.") + } + .sheet(isPresented: $presentPlayerSearch, onDismiss: { + selectionSearchField = nil + }) { + NavigationStack { + SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in + selectionSearchField = nil + players.forEach { player in + let newPlayer = PlayerRegistration(importedPlayer: player) + newPlayer.setComputedRank(in: tournament) + createdPlayers.insert(newPlayer) + createdPlayerIds.insert(newPlayer.id) + } + } contentUnavailableAction: { searchViewModel in + selectionSearchField = searchViewModel.searchText + presentPlayerSearch = false + presentPlayerCreation = true + } + } + .tint(.master) + } + .sheet(isPresented: $presentPlayerCreation) { + PlayerPopoverView(source: _searchSource(), sex: _addPlayerSex()) { p in + p.setComputedRank(in: tournament) + createdPlayers.insert(p) + createdPlayerIds.insert(p.id) + } + .tint(.master) + } + .navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Annuler", role: .cancel) { + dismiss() + } + } + + if pasteString == nil { + ToolbarItem(placement: .topBarTrailing) { + PasteButton(payloadType: String.self) { strings in + guard let first = strings.first else { return } + Task { + await MainActor.run { + fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) + fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] + pasteString = first + autoSelect = true + } + } + } + .foregroundStyle(.master) + .labelStyle(.iconOnly) + .buttonBorderShape(.capsule) + } + } + } + .navigationBarBackButtonHidden(_isEditingTeam()) + .toolbarBackground(.visible, for: .navigationBar) + .navigationBarTitleDisplayMode(.inline) + } + + private func _isEditingTeam() -> Bool { + createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil + } + + var unsortedPlayers: [PlayerRegistration] { + tournament.unsortedPlayers() + } + + private func _getTeams() { + Task { + await _setHash() + } + } + + @ViewBuilder + private func _managementView() -> some View { + Section { + RowButtonView("Rechercher dans la base fédérale") { + presentPlayerSearch = true + } + } footer: { + if let rankSourceDate = tournament.rankSourceDate { + Text("Cherchez dans la base fédérale de \(rankSourceDate.monthYearFormatted), vous y trouverez tous les joueurs ayant participé à au moins un tournoi dans les 12 derniers mois.") + } + } + + Section { + RowButtonView("Créer un non classé / non licencié") { + presentPlayerCreation = true + } + } footer: { + Text("Si le joueur n'a pas encore de licence ou n'a pas encore participé à une compétition, vous pouvez le créer vous-même.") + } + } + + private func _addPlayerSex() -> Int { + switch tournament.tournamentCategory { + case .men, .unlisted: + return 1 + case .women: + return 0 + case .mix: + return 1 + } + + } + + private func _filterOption() -> PlayerFilterOption { + return tournament.tournamentCategory.playerFilterOption + } + + private func _searchSource() -> String? { + selectionSearchField ?? pasteString + } + + static private func _pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { + let text = pasteField.canonicalVersion + + let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines) + let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 } + let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 }) + var andPredicates = [NSPredicate]() + var orPredicates = [NSPredicate]() + //self.wordsCount = nameComponents.count + + + if filterOption == .male { + andPredicates.append(NSPredicate(format: "male == YES")) + } else if filterOption == .female { + andPredicates.append(NSPredicate(format: "male == NO")) + } + + if let mostRecentDate { + andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + } + + if nameComponents.count > 1 { + orPredicates = nameComponents.pairs().map { + return NSPredicate(format: "(firstName contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) } + } else { + orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) } + } + + let matches = text.licencesFound() + let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } + orPredicates = orPredicates + licensesPredicates + + var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) + + if orPredicates.isEmpty == false { + predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)]) + } + + return predicate + } + + private func _currentSelection() -> Set { + var currentSelection = Set() + createdPlayerIds.compactMap { id in + fetchPlayers.first(where: { id == $0.license }) + }.forEach { player in + let player = PlayerRegistration(importedPlayer: player) + player.setComputedRank(in: tournament) + currentSelection.insert(player) + } + + createdPlayerIds.compactMap { id in + createdPlayers.first(where: { id == $0.id }) + }.forEach { + currentSelection.insert($0) + } + return currentSelection + } + + private func _currentSelectionIds() -> [String?] { + var currentSelection = [String?]() + createdPlayerIds.compactMap { id in + fetchPlayers.first(where: { id == $0.license }) + }.forEach { player in + currentSelection.append(player.license) + } + + createdPlayerIds.compactMap { id in + createdPlayers.first(where: { id == $0.id }) + }.forEach { + currentSelection.append($0.licenceId) + } + return currentSelection + } + + private func _isDuplicate() -> Bool { + let ids : [String?] = _currentSelectionIds() + if tournament.selectedSortedTeams().anySatisfy({ $0.containsExactlyPlayerLicenses(ids) }) { + return true + } + return false + } + + private func _createTeam(checkDuplicates: Bool) { + if checkDuplicates && _isDuplicate() { + confirmDuplicate = true + return + } + + let players = _currentSelection() + let team = tournament.addTeam(players) + do { + try self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + + createdPlayers.removeAll() + createdPlayerIds.removeAll() + pasteString = nil + dismiss() + } + + private func _updateTeam(checkDuplicates: Bool) { + guard let editedTeam else { return } + if checkDuplicates && _isDuplicate() { + confirmDuplicate = true + return + } + + let players = _currentSelection() + editedTeam.updatePlayers(players, inTournamentCategory: tournament.tournamentCategory) + do { + try self.tournamentStore.teamRegistrations.addOrUpdate(instance: editedTeam) + } catch { + Logger.error(error) + } + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + createdPlayers.removeAll() + createdPlayerIds.removeAll() + pasteString = nil + self.editedTeam = nil + dismiss() + } + + private func _buildingTeamView() -> some View { + List(selection: $createdPlayerIds) { + if let pasteString { + + Section { + Text(pasteString) + } footer: { + HStack { + Text("contenu du presse-papier") + Spacer() + Button("effacer", role: .destructive) { + self.pasteString = nil + self.createdPlayers.removeAll() + self.createdPlayerIds.removeAll() + } + .buttonStyle(.borderless) + } + } + } + + Section { + ForEach(createdPlayerIds.sorted(), id: \.self) { id in + if let p = createdPlayers.first(where: { $0.id == id }) { + VStack(alignment: .leading, spacing: 0) { + if let player = unsortedPlayers.first(where: { $0.licenceId == p.licenceId }), editedTeam?.includes(player: player) == false { + Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } + PlayerView(player: p).tag(p.id) + } + } + if let p = fetchPlayers.first(where: { $0.license == id }) { + VStack(alignment: .leading, spacing: 0) { + if unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { + Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } + ImportedPlayerView(player: p).tag(p.license!) + } + } + } + } header: { + let _currentSelection = _currentSelection() + let selectedSortedTeams = tournament.selectedSortedTeams() + let rank = _currentSelection.map { + $0.computedRank + }.reduce(0, +) + let teamIndex = selectedSortedTeams.firstIndex(where: { $0.weight >= rank }) ?? selectedSortedTeams.count + if _currentSelection.isEmpty == false, tournament.hideWeight() == false, rank > 0 { + HStack(spacing: 16.0) { + VStack(alignment: .leading, spacing: 0) { + Text("Rang").font(.caption) + Text("#" + (teamIndex + 1).formatted()) + } + + VStack(alignment: .leading, spacing: 0) { + Text("Poids").font(.caption) + Text(rank.formatted()) + } + Spacer() + VStack(alignment: .trailing, spacing: 0) { + Text("").font(.caption) + Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count)) + } + } + } + } + + + Section { + if editedTeam == nil { + if createdPlayerIds.isEmpty { + RowButtonView("Bloquer une place") { + _createTeam(checkDuplicates: false) + } + } else { + RowButtonView("Ajouter l'équipe") { + _createTeam(checkDuplicates: true) + } + } + } else { + RowButtonView("Modifier l'équipe") { + _updateTeam(checkDuplicates: false) + editedTeam = nil + } + } + } + + if let pasteString { + if fetchPlayers.isEmpty { + ContentUnavailableView { + Label("Aucun résultat", systemImage: "person.2.slash") + } description: { + Text("Aucun joueur classé n'a été trouvé dans ce message.") + } actions: { + RowButtonView("Créer un joueur non classé") { + presentPlayerCreation = true + } + + RowButtonView("Effacer cette recherche") { + self.pasteString = nil + } + } + + } else { + Section { + ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in + ImportedPlayerView(player: player).tag(player.license!) + } + } header: { + Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix) + } + } + } else { + _managementView() + } + } + .onAppear { + _getTeams() + } + .onDisappear { + Task { + await _handleHashDiff() + + } + } + .headerProminence(.increased) + .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here + if let pasteString, count == 2, autoSelect == true { + fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in + createdPlayerIds.insert(player.license!) + } + autoSelect = false + } + } + .environment(\.editMode, Binding.constant(EditMode.active)) + } + + private var count: Int { + return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count + } + + private var hitTarget: Int { + if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 { + if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 } + } else { + return 2 + } + return 1 + } + + private func _save() { + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 3337176..59bf017 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -40,14 +40,10 @@ struct InscriptionManagerView: View { @State private var presentPlayerCreation: Bool = false @State private var presentImportView: Bool = false @State private var isLearningMore: Bool = false - @State private var createdPlayers: Set = Set() - @State private var createdPlayerIds: Set = Set() @State private var editedTeam: TeamRegistration? - @State private var pasteString: String? @State private var currentRankSourceDate: Date? @State private var confirmUpdateRank = false @State private var selectionSearchField: String? - @State private var autoSelect: Bool = false @State private var teamsHash: Int? @State private var presentationCount: Int = 0 @State private var filterMode: FilterMode = .all @@ -63,7 +59,7 @@ struct InscriptionManagerView: View { @State private var unsortedPlayers: [PlayerRegistration] = [] @State private var teamPaste: URL? @State private var confirmDuplicate: Bool = false - + @State private var presentAddTeamView: Bool = false var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -100,26 +96,20 @@ struct InscriptionManagerView: View { case walkOut case waiting - func localizedLabel() -> String { + func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .all: - return "Toutes les équipes" + return displayStyle == .wide ? "Équipes inscrites / souhaitées" : "Équipes inscrites" case .walkOut: - return "Voir les WOs" + return "Forfaits" case .waiting: return "Liste d'attente" } } } - init(tournament: Tournament, pasteString: String? = nil) { + init(tournament: Tournament) { self.tournament = tournament - if let pasteString { - _pasteString = .init(wrappedValue: pasteString) - _fetchPlayers = FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: Self._pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption)) - _autoSelect = .init(wrappedValue: true) - cancelShouldDismiss = true - } _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) } @@ -184,13 +174,29 @@ struct InscriptionManagerView: View { } var body: some View { - VStack(spacing: 0) { - if _isEditingTeam() { - _buildingTeamView() - } else if sortedTeams.isEmpty { - _inscriptionTipsView() - } else { + Group { + if tournament.unsortedTeams().isEmpty == false { _teamRegisteredView() + } else { + List { + + } + .overlay { + ContentUnavailableView { + Label("Aucune équipe", systemImage: "person.2.slash") + } description: { + Text("Vous n'avez aucune équipe dans votre liste. Complétez là ou importer un fichier.") + } actions: { + RowButtonView("Ajouter une équipe") { + presentAddTeamView = true + } + + RowButtonView("Importer un fichier") { + presentImportView = true + } + } + .padding() + } } } .onAppear { @@ -202,21 +208,6 @@ struct InscriptionManagerView: View { } } - .alert("Cette équipe existe déjà", isPresented: $confirmDuplicate) { - Button("Créer l'équipe quand même") { - _createTeam(checkDuplicates: false) - } - - Button("Annuler", role: .cancel) { - pasteString = nil - editedTeam = nil - createdPlayers.removeAll() - createdPlayerIds.removeAll() - } - - } message: { - Text("Cette équipe existe déjà dans votre liste d'inscription.") - } .alert("Un problème est survenu", isPresented: messageSentFailed) { Button("OK") { } @@ -272,39 +263,10 @@ struct InscriptionManagerView: View { } .tint(.master) } - .sheet(isPresented: $isLearningMore) { LearnMoreSheetView(tournament: tournament) .tint(.master) } - .sheet(isPresented: $presentPlayerSearch, onDismiss: { - selectionSearchField = nil - }) { - NavigationStack { - SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in - selectionSearchField = nil - players.forEach { player in - let newPlayer = PlayerRegistration(importedPlayer: player) - newPlayer.setComputedRank(in: tournament) - createdPlayers.insert(newPlayer) - createdPlayerIds.insert(newPlayer.id) - } - } contentUnavailableAction: { searchViewModel in - selectionSearchField = searchViewModel.searchText - presentPlayerSearch = false - presentPlayerCreation = true - } - } - .tint(.master) - } - .sheet(isPresented: $presentPlayerCreation) { - PlayerPopoverView(source: _searchSource(), sex: _addPlayerSex()) { p in - p.setComputedRank(in: tournament) - createdPlayers.insert(p) - createdPlayerIds.insert(p.id) - } - .tint(.master) - } .sheet(isPresented: $presentImportView, onDismiss: { _getTeams() }) { @@ -334,6 +296,15 @@ struct InscriptionManagerView: View { UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament) .tint(.master) } + .sheet(isPresented: $presentAddTeamView, onDismiss: { + editedTeam = nil + _getTeams() + }) { + NavigationStack { + AddTeamView(tournament: tournament, editedTeam: editedTeam) + } + .tint(.master) + } .onChange(of: filterMode) { _prepareTeams() } @@ -344,109 +315,83 @@ struct InscriptionManagerView: View { _prepareTeams() } .toolbar { - if _isEditingTeam() { - ToolbarItem(placement: .cancellationAction) { - Button("Annuler", role: .cancel) { - pasteString = nil - editedTeam = nil - createdPlayers.removeAll() - createdPlayerIds.removeAll() - if cancelShouldDismiss { - dismiss() + ToolbarItemGroup(placement: .navigationBarTrailing) { + Menu { + Picker(selection: $filterMode) { + ForEach(FilterMode.allCases) { + Text($0.localizedLabel(.short)).tag($0) } + } label: { } - } - } else { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Menu { - Picker(selection: $filterMode) { - ForEach(FilterMode.allCases) { - Text($0.localizedLabel()).tag($0) - } - } label: { + Picker(selection: $sortingMode) { + ForEach(SortingMode.allCases) { + Text($0.localizedLabel()).tag($0) } - Picker(selection: $sortingMode) { - ForEach(SortingMode.allCases) { - Text($0.localizedLabel()).tag($0) - } + } label: { + } + + Picker(selection: $byDecreasingOrdering) { + Text("Croissant").tag(false) + Text("Décroissant").tag(true) + } label: { + } + } label: { + LabelFilter() + } + Menu { + if tournament.inscriptionClosed() == false { + Menu { + _sortingTypePickerView() } label: { + Text("Méthode de sélection") + Text(tournament.teamSorting.localizedLabel()) } + Divider() + rankingDateSourcePickerView(showDateInLabel: true) - Picker(selection: $byDecreasingOrdering) { - Text("Croissant").tag(false) - Text("Décroissant").tag(true) + Divider() + Button { + tournament.lockRegistration() + _save() } label: { + Label("Clôturer", systemImage: "lock") } - } label: { - LabelFilter() - } - Menu { - if tournament.inscriptionClosed() == false { - Menu { - _sortingTypePickerView() - } label: { - Text("Méthode de sélection") - Text(tournament.teamSorting.localizedLabel()) - } - Divider() - rankingDateSourcePickerView(showDateInLabel: true) - if tournament.teamSorting == .inscriptionDate { - Divider() - //_prioritizeClubMembersButton() - - Button("Bloquer une place") { - _createTeam(checkDuplicates: false) - } - } - Divider() - Button { - tournament.lockRegistration() - _save() - } label: { - Label("Clôturer", systemImage: "lock") - } - Divider() - if let teamPaste { - ShareLink(item: teamPaste) { - Label("Exporter les paires", systemImage: "square.and.arrow.up") - } - } - Button { - presentImportView = true - } label: { - Label("Importer beach-padel", systemImage: "square.and.arrow.down") - } - Link(destination: URLs.beachPadel.url) { - Label("beach-padel.app.fft.fr", systemImage: "safari") - } - } else { - Button { - tournament.closedRegistrationDate = nil - _save() - } label: { - Label("Ré-ouvrir", systemImage: "lock.open") + Divider() + if let teamPaste { + ShareLink(item: teamPaste) { + Label("Exporter les paires", systemImage: "square.and.arrow.up") } } - } label: { - if tournament.inscriptionClosed() == false { - LabelOptions() - } else { - Label("Clôturer", systemImage: "lock") + Button { + presentImportView = true + } label: { + Label("Importer beach-padel", systemImage: "square.and.arrow.down") + } + Link(destination: URLs.beachPadel.url) { + Label("beach-padel.app.fft.fr", systemImage: "safari") } + } else { + Button { + tournament.closedRegistrationDate = nil + _save() + } label: { + Label("Ré-ouvrir", systemImage: "lock.open") + } + } + } label: { + if tournament.inscriptionClosed() == false { + LabelOptions() + } else { + Label("Clôturer", systemImage: "lock") } } } } - .navigationBarBackButtonHidden(_isEditingTeam()) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Inscriptions") .navigationBarTitleDisplayMode(.inline) } - private func _isEditingTeam() -> Bool { - createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil - } - private func _prepareStats() async { #if DEBUG_TIME //DEBUGING TIME let start = Date() @@ -517,6 +462,8 @@ struct InscriptionManagerView: View { private func _teamRegisteredView() -> some View { List { + _informationView() + let selectedSortedTeams = tournament.selectedSortedTeams() if let closedRegistrationDate = tournament.closedRegistrationDate { Section { @@ -531,11 +478,15 @@ struct InscriptionManagerView: View { } } } - + if presentSearch == false { _rankHandlerView() _relatedTips() - _informationView() + Section { + RowButtonView("Compléter la liste") { + presentAddTeamView = true + } + } } let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) @@ -552,13 +503,13 @@ struct InscriptionManagerView: View { } RowButtonView("Créer une équipe") { - Task { - await MainActor.run { - fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - pasteString = searchField - } - } + // Task { + // await MainActor.run { + // fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) + // fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] + // pasteString = searchField + // } + // } } RowButtonView("D'accord") { @@ -567,7 +518,6 @@ struct InscriptionManagerView: View { } } } - ForEach(teams) { team in let teamIndex = team.index(in: sortedTeams) Section { @@ -585,34 +535,6 @@ struct InscriptionManagerView: View { .autocorrectionDisabled() } - @ViewBuilder - private func _managementView() -> some View { - Button { - presentPlayerSearch = true - } label: { - Text("Rechercher dans la base fédérale") - } - - Button { - presentPlayerCreation = true - } label: { - Text("Créer un non classé / non licencié") - } - - PasteButton(payloadType: String.self) { strings in - guard let first = strings.first else { return } - Task { - await MainActor.run { - fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - pasteString = first - autoSelect = true - } - } - } - } - - @ViewBuilder func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View { Section { @@ -656,72 +578,6 @@ struct InscriptionManagerView: View { return tournament.tournamentCategory.playerFilterOption } - @ViewBuilder - private func _inscriptionTipsView() -> some View { - List { - if let closedRegistrationDate = tournament.closedRegistrationDate { - Section { - CloseDatePicker(closedRegistrationDate: closedRegistrationDate) - } footer: { - Text("Toutes les équipes ayant été inscrites après la date de clôture seront en liste d'attente.") - } - } - - _informationView() - - Section { - - TipView(fileTip) { action in - if action.id == "website" { - UIApplication.shared.open(URLs.beachPadel.url) - } else if action.id == "add-team-file" { - presentImportView = true - } - } - .tipStyle(tint: nil) - } - - Section { - - TipView(pasteTip) { action in - if let paste = UIPasteboard.general.string { - Task { - await MainActor.run { - fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: paste, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - pasteString = paste - autoSelect = true - } - } - } - } - .tipStyle(tint: nil) - } - - Section { - - TipView(searchTip) { action in - presentPlayerSearch = true - } - .tipStyle(tint: nil) - } - - Section { - - TipView(createTip) { action in - presentPlayerCreation = true - } - .tipStyle(tint: nil) - } - - Section { - ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Vous n'avez encore aucune équipe inscrite dans votre tournoi.")) - } - - _rankHandlerView() - } - } - @ViewBuilder private func _rankHandlerView() -> some View { if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false { @@ -737,36 +593,29 @@ struct InscriptionManagerView: View { } } + private func _teamCountForFilterMode(filterMode: FilterMode) -> String { + switch filterMode { + case .all: + return unsortedTeamsWithoutWO.count.formatted() + " / " + tournament.teamCount.formatted() + case .walkOut: + let wo = walkoutTeams.count.formatted() + return wo + case .waiting: + let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) + return waiting.formatted() + } + } + @ViewBuilder private func _informationView() -> some View { Section { - Button { - filterMode = .all - } label: { + ForEach(FilterMode.allCases) { filterMode in LabeledContent { - Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()) + Text(_teamCountForFilterMode(filterMode: filterMode)) } label: { - Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)") + Text(filterMode.localizedLabel()) } } - .buttonStyle(.plain) - - HStack { - let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) - FooterButtonView("\(waiting.formatted()) équipes en attente\(waiting.pluralSuffix)") { - filterMode = .waiting - } - .disabled(filterMode == .waiting) - - Divider() - - let wo = walkoutTeams.count - FooterButtonView("\(wo.formatted()) équipes forfait\(wo.pluralSuffix)") { - filterMode = .walkOut - } - .disabled(filterMode == .walkOut) - } - .fixedSize(horizontal: true, vertical: true) NavigationLink { InscriptionInfoView() @@ -784,27 +633,77 @@ struct InscriptionManagerView: View { } } header: { Text("Statut des inscriptions") - } footer: { - HStack { - Menu { - _managementView() - } label: { - Text("Complétez votre liste") - } - Text("ou") - - FooterButtonView("Importez un fichier") { - presentImportView = true - } - } -// if filterMode != .all { -// FooterButtonView("tout afficher") { -// filterMode = .all -// } -// } } - .headerProminence(.increased) } +// +// @ViewBuilder +// private func _informationView() -> some View { +// Section { +// Button { +// filterMode = .all +// } label: { +// LabeledContent { +// Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()) +// } label: { +// Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)") +// } +// } +// .buttonStyle(.plain) +// +// HStack { +// let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) +// FooterButtonView("\(waiting.formatted()) équipes en attente\(waiting.pluralSuffix)") { +// filterMode = .waiting +// } +// .disabled(filterMode == .waiting) +// +// Divider() +// +// let wo = walkoutTeams.count +// FooterButtonView("\(wo.formatted()) équipes forfait\(wo.pluralSuffix)") { +// filterMode = .walkOut +// } +// .disabled(filterMode == .walkOut) +// } +// .fixedSize(horizontal: true, vertical: true) +// +// NavigationLink { +// InscriptionInfoView() +// .environment(tournament) +// } label: { +// LabeledContent { +// if let registrationIssues { +// Text(registrationIssues.formatted()) +// } else { +// ProgressView() +// } +// } label: { +// Text("Problèmes détéctés") +// } +// } +// } header: { +// Text("Statut des inscriptions") +// } footer: { +// HStack { +// Menu { +// _managementView() +// } label: { +// Text("Complétez votre liste") +// } +// Text("ou") +// +// FooterButtonView("Importez un fichier") { +// presentImportView = true +// } +// } +//// if filterMode != .all { +//// FooterButtonView("tout afficher") { +//// filterMode = .all +//// } +//// } +// } +// .headerProminence(.increased) +// } @ViewBuilder private func _relatedTips() -> some View { @@ -848,280 +747,9 @@ struct InscriptionManagerView: View { } private func _searchSource() -> String? { - selectionSearchField ?? pasteString + selectionSearchField } - static private func _pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { - let text = pasteField.canonicalVersion - - let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines) - let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 } - let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 }) - var andPredicates = [NSPredicate]() - var orPredicates = [NSPredicate]() - //self.wordsCount = nameComponents.count - - - if filterOption == .male { - andPredicates.append(NSPredicate(format: "male == YES")) - } else if filterOption == .female { - andPredicates.append(NSPredicate(format: "male == NO")) - } - - if let mostRecentDate { - andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) - } - - if nameComponents.count > 1 { - orPredicates = nameComponents.pairs().map { - return NSPredicate(format: "(firstName contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) } - } else { - orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) } - } - - let matches = text.licencesFound() - let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } - orPredicates = orPredicates + licensesPredicates - - var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) - - if orPredicates.isEmpty == false { - predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)]) - } - - return predicate - } - - private func _currentSelection() -> Set { - var currentSelection = Set() - createdPlayerIds.compactMap { id in - fetchPlayers.first(where: { id == $0.license }) - }.forEach { player in - let player = PlayerRegistration(importedPlayer: player) - player.setComputedRank(in: tournament) - currentSelection.insert(player) - } - - createdPlayerIds.compactMap { id in - createdPlayers.first(where: { id == $0.id }) - }.forEach { - currentSelection.insert($0) - } - return currentSelection - } - - private func _currentSelectionIds() -> [String?] { - var currentSelection = [String?]() - createdPlayerIds.compactMap { id in - fetchPlayers.first(where: { id == $0.license }) - }.forEach { player in - currentSelection.append(player.license) - } - - createdPlayerIds.compactMap { id in - createdPlayers.first(where: { id == $0.id }) - }.forEach { - currentSelection.append($0.licenceId) - } - return currentSelection - } - - private func _isDuplicate() -> Bool { - let ids : [String?] = _currentSelectionIds() - if sortedTeams.anySatisfy({ $0.containsExactlyPlayerLicenses(ids) }) { - return true - } - return false - } - - private func _createTeam(checkDuplicates: Bool) { - if checkDuplicates && _isDuplicate() { - confirmDuplicate = true - return - } - - let players = _currentSelection() - let team = tournament.addTeam(players) - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } - - createdPlayers.removeAll() - createdPlayerIds.removeAll() - pasteString = nil - - _clearScreen() - _getTeams() - } - - private func _updateTeam(checkDuplicates: Bool) { - guard let editedTeam else { return } - if checkDuplicates && _isDuplicate() { - confirmDuplicate = true - return - } - - let players = _currentSelection() - editedTeam.updatePlayers(players, inTournamentCategory: tournament.tournamentCategory) - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(instance: editedTeam) - } catch { - Logger.error(error) - } - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } - createdPlayers.removeAll() - createdPlayerIds.removeAll() - pasteString = nil - self.editedTeam = nil - _clearScreen() - _getTeams() - } - - private func _buildingTeamView() -> some View { - List(selection: $createdPlayerIds) { - if let pasteString { - - Section { - Text(pasteString) - } footer: { - HStack { - Text("contenu du presse-papier") - Spacer() - Button("effacer", role: .destructive) { - self.pasteString = nil - self.createdPlayers.removeAll() - self.createdPlayerIds.removeAll() - } - .buttonStyle(.borderless) - } - } - } - - Section { - ForEach(createdPlayerIds.sorted(), id: \.self) { id in - if let p = createdPlayers.first(where: { $0.id == id }) { - VStack(alignment: .leading, spacing: 0) { - if let player = unsortedPlayers.first(where: { $0.licenceId == p.licenceId }), editedTeam?.includes(player: player) == false { - Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() - } - PlayerView(player: p).tag(p.id) - } - } - if let p = fetchPlayers.first(where: { $0.license == id }) { - VStack(alignment: .leading, spacing: 0) { - if unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { - Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() - } - ImportedPlayerView(player: p).tag(p.license!) - } - } - } - } header: { - let _currentSelection = _currentSelection() - let selectedSortedTeams = tournament.selectedSortedTeams() - let rank = _currentSelection.map { - $0.computedRank - }.reduce(0, +) - let teamIndex = selectedSortedTeams.firstIndex(where: { $0.weight >= rank }) ?? selectedSortedTeams.count - if _currentSelection.isEmpty == false, tournament.hideWeight() == false, rank > 0 { - HStack(spacing: 16.0) { - VStack(alignment: .leading, spacing: 0) { - Text("Rang").font(.caption) - Text("#" + (teamIndex + 1).formatted()) - } - - VStack(alignment: .leading, spacing: 0) { - Text("Poids").font(.caption) - Text(rank.formatted()) - } - Spacer() - VStack(alignment: .trailing, spacing: 0) { - Text("").font(.caption) - Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count)) - } - } - } - } - - if editedTeam == nil { - if createdPlayerIds.isEmpty { - RowButtonView("Bloquer une place") { - _createTeam(checkDuplicates: false) - } - } else { - RowButtonView("Ajouter l'équipe") { - _createTeam(checkDuplicates: true) - } - } - } else { - RowButtonView("Modifier l'équipe") { - _updateTeam(checkDuplicates: false) - } - } - - if let pasteString { - if fetchPlayers.isEmpty { - ContentUnavailableView { - Label("Aucun résultat", systemImage: "person.2.slash") - } description: { - Text("Aucun joueur classé n'a été trouvé dans ce message.") - } actions: { - RowButtonView("Créer un joueur non classé") { - presentPlayerCreation = true - } - - RowButtonView("Effacer cette recherche") { - self.pasteString = nil - } - } - - } else { - Section { - ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in - ImportedPlayerView(player: player).tag(player.license!) - } - } header: { - Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix) - } - } - } - } - .headerProminence(.increased) - .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here - if let pasteString, count == 2, autoSelect == true { - fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in - createdPlayerIds.insert(player.license!) - } - autoSelect = false - } - } - .environment(\.editMode, Binding.constant(EditMode.active)) - } - - private var count: Int { - return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count - } - - private var hitTarget: Int { - if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 { - if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 } - } else { - return 2 - } - return 1 - } - @ViewBuilder private func _sortingTypePickerView() -> some View { @Bindable var tournament = tournament @@ -1214,10 +842,7 @@ struct InscriptionManagerView: View { //Divider() Button("Changer les joueurs") { editedTeam = team - team.unsortedPlayers().forEach { player in - createdPlayers.insert(player) - createdPlayerIds.insert(player.id) - } + presentAddTeamView = true } Divider() NavigationLink {