From f83cb5f2510e0c63854a5a16665016608580afb3 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 24 Mar 2025 19:26:06 +0100 Subject: [PATCH] fix issue with search --- PadelClub/ViewModel/SearchViewModel.swift | 493 ++++++++++++------ .../Views/Tournament/Screen/AddTeamView.swift | 4 +- 2 files changed, 344 insertions(+), 153 deletions(-) diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 8857997..c9a14df 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -14,13 +14,13 @@ class DebouncableViewModel: ObservableObject { class SearchViewModel: ObservableObject, Identifiable { let id: UUID = UUID() - var allowSelection : Int = 0 + var allowSelection: Int = 0 var codeClub: String? = nil var clubName: String? = nil var ligueName: String? = nil var showFemaleInMaleAssimilation: Bool = false var hidePlayers: [String]? - + @Published var debouncableText: String = "" @Published var searchText: String = "" @Published var task: DispatchWorkItem? @@ -37,91 +37,103 @@ class SearchViewModel: ObservableObject, Identifiable { @Published var isPresented: Bool = false @Published var selectedAgeCategory: FederalTournamentAge = .unlisted @Published var mostRecentDate: Date? = nil - + var selectionIsOver: Bool { if allowSingleSelection && selectedPlayers.count == 1 { return true } else if allowMultipleSelection && selectedPlayers.count == allowSelection { return true } - + return false } - + var allowMultipleSelection: Bool { allowSelection > 1 || allowSelection == -1 } - + var allowSingleSelection: Bool { allowSelection == 1 } - + var debounceTrigger: Double { (dataSet == .national || dataSet == .ligue) ? 0.4 : 0.1 } - + var throttleTrigger: Double { (dataSet == .national || dataSet == .ligue) ? 0.15 : 0.1 } - + var contentUnavailableMessage: String { var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] if tokens.isEmpty { - message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") + message.append( + "Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver." + ) } return message.joined(separator: "\n") } - + func codeClubs() -> [String] { let clubs: [Club] = DataStore.shared.user.clubsObjects() return clubs.compactMap { $0.code } } - + func getCodeClub() -> String? { if let codeClub { return codeClub } - if let userCodeClub = DataStore.shared.user.currentPlayerData()?.clubCode { return userCodeClub } + if let userCodeClub = DataStore.shared.user.currentPlayerData()?.clubCode { + return userCodeClub + } return nil } - + func getLigueName() -> String? { if let ligueName { return ligueName } - if let userLigueName = DataStore.shared.user.currentPlayerData()?.ligueName { return userLigueName } + if let userLigueName = DataStore.shared.user.currentPlayerData()?.ligueName { + return userLigueName + } return nil } - + func shouldIncludeSearchTextPredicate() -> Bool { if allowMultipleSelection { return true } - + if allowSingleSelection { return true } - + if tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted { return true } - - return dataSet == .national && searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) + + return dataSet == .national && searchText.isEmpty == false + && (tokens.isEmpty == true && hideAssimilation == false + && selectedAgeCategory == .unlisted) } - + func showIndex() -> Bool { if dataSet == .national { - if searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) { + if searchText.isEmpty == false + && (tokens.isEmpty == true && hideAssimilation == false + && selectedAgeCategory == .unlisted) + { return false } else { return isFiltering() } } - if (dataSet == .ligue) { return isFiltering() } + if dataSet == .ligue { return isFiltering() } if filterOption == .all { return isFiltering() } return true } - + func isFiltering() -> Bool { - searchText.isEmpty == false || tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted + searchText.isEmpty == false || tokens.isEmpty == false || hideAssimilation + || selectedAgeCategory != .unlisted } - + func prompt(forDataSet: DataSet) -> String { switch forDataSet { case .national: @@ -138,7 +150,7 @@ class SearchViewModel: ObservableObject, Identifiable { return "dans mes favoris" } } - + func label(forDataSet: DataSet) -> String { switch forDataSet { case .national: @@ -155,102 +167,201 @@ class SearchViewModel: ObservableObject, Identifiable { } func words() -> [String] { - return searchText.canonicalVersionWithPunctuation.trimmed.components(separatedBy: .whitespaces) + return searchText.canonicalVersionWithPunctuation.trimmed.components( + separatedBy: .whitespaces) } func wordsPredicates() -> NSPredicate? { let words = words().filter({ $0.isEmpty == false }) + + // Handle special case of hyphenated words + let hyphenatedWords = searchText.components(separatedBy: .whitespaces) + .filter { $0.contains("-") } + + var predicates: [NSPredicate] = [] + + // Add predicates for hyphenated words + for word in hyphenatedWords { + predicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", word)) + let parts = word.components(separatedBy: "-") + for part in parts where part.count > 1 { + predicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", part)) + } + } + + // Regular words processing switch words.count { case 2: - let predicates = [ - NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[0], words[1]), - NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[1], words[0]), - ] - return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) + predicates.append(contentsOf: [ + NSPredicate( + format: + "canonicalLastName CONTAINS[cd] %@ AND canonicalFirstName CONTAINS[cd] %@", + words[0], words[1]), + NSPredicate( + format: + "canonicalLastName CONTAINS[cd] %@ AND canonicalFirstName CONTAINS[cd] %@", + words[1], words[0]), + // For multi-word first names, try the two words as a first name + NSPredicate( + format: "canonicalFirstName CONTAINS[cd] %@", words.joined(separator: " ")), + ]) + case 3: + // Handle potential cases like "Jean Christophe CROS" + predicates.append(contentsOf: [ + // First two words as first name, last as last name + NSPredicate( + format: + "canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@", + words[0] + " " + words[1], words[2]), + // First as first name, last two as last name + NSPredicate( + format: + "canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@", + words[0], words[1] + " " + words[2]), + // Last as first name, first two as last name + NSPredicate( + format: + "canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@", + words[2], words[0] + " " + words[1]), + ]) default: - return nil + if words.count > 0 { + // For single word or many words, try matching against full name + predicates.append( + NSPredicate( + format: "canonicalFullName CONTAINS[cd] %@", + words.joined(separator: " "))) + } } + + return predicates.isEmpty + ? nil : NSCompoundPredicate(orPredicateWithSubpredicates: predicates) } func searchTextPredicate() -> NSPredicate? { - var predicates : [NSPredicate] = [] - let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) - let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed + var predicates: [NSPredicate] = [] + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces).union( + CharacterSet(charactersIn: "-")) + let canonicalVersionWithoutPunctuation = searchText.canonicalVersion + .components(separatedBy: allowedCharacterSet.inverted) + .joined() + .trimmed if canonicalVersionWithoutPunctuation.isEmpty == false { let wordsPredicates = wordsPredicates() if let wordsPredicates { predicates.append(wordsPredicates) } else { - predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) + predicates.append( + NSPredicate( + format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) } - predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation)) + + // Add match for full name + predicates.append( + NSPredicate( + format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation) + ) + + // Add pattern match for more flexible matching let components = canonicalVersionWithoutPunctuation.split(separator: " ") let pattern = components.joined(separator: ".*") - let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) - predicates.append(predicate) + predicates.append(NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)) + + // Look for exact matches on first or last name + let words = canonicalVersionWithoutPunctuation.components(separatedBy: .whitespaces) + for word in words where word.count > 2 { + predicates.append( + NSPredicate( + format: "firstName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@", word, word) + ) + } } + if predicates.isEmpty { return nil } return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) } - + func orPredicate() -> NSPredicate? { - var predicates : [NSPredicate] = [] - let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) - let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed + var predicates: [NSPredicate] = [] + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces).union( + CharacterSet(charactersIn: "-")) + let canonicalVersionWithoutPunctuation = searchText.canonicalVersion + .components(separatedBy: allowedCharacterSet.inverted) + .joined() + .trimmed let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed if tokens.isEmpty { - if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false { + if shouldIncludeSearchTextPredicate(), + canonicalVersionWithoutPunctuation.isEmpty == false + { if let searchTextPredicate = searchTextPredicate() { predicates.append(searchTextPredicate) } } } + + // Process tokens for token in tokens { switch token { case .ligue: if canonicalVersionWithoutPunctuation.isEmpty { predicates.append(NSPredicate(format: "ligueName == nil")) } else { - predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation)) + predicates.append( + NSPredicate( + format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation) + ) } case .club: if canonicalVersionWithoutPunctuation.isEmpty { predicates.append(NSPredicate(format: "clubName == nil")) } else { - predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation)) + predicates.append( + NSPredicate( + format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation)) } case .rankMoreThan: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + if canonicalVersionWithoutPunctuation.isEmpty + || Int(canonicalVersionWithoutPunctuation) == 0 + { predicates.append(NSPredicate(format: "rank == 0")) } else { - predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation)) + predicates.append( + NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation)) } case .rankLessThan: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + if canonicalVersionWithoutPunctuation.isEmpty + || Int(canonicalVersionWithoutPunctuation) == 0 + { predicates.append(NSPredicate(format: "rank == 0")) } else { - predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation)) + predicates.append( + NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation)) } case .rankBetween: let values = canonicalVersionWithPunctuation.components(separatedBy: ",") if canonicalVersionWithPunctuation.isEmpty || values.count != 2 { predicates.append(NSPredicate(format: "rank == 0")) } else { - predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) + predicates.append( + NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) } case .age: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + if canonicalVersionWithoutPunctuation.isEmpty + || Int(canonicalVersionWithoutPunctuation) == 0 + { predicates.append(NSPredicate(format: "birthYear == 0")) } else if let birthYear = Int(canonicalVersionWithoutPunctuation) { - predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) + predicates.append( + NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) } - } } + if predicates.isEmpty { return nil } @@ -258,35 +369,33 @@ class SearchViewModel: ObservableObject, Identifiable { } func predicate() -> NSPredicate? { - var predicates : [NSPredicate?] = [ + var predicates: [NSPredicate?] = [ orPredicate(), - filterOption == .male ? - NSPredicate(format: "male == YES") : - nil, - filterOption == .female ? - NSPredicate(format: "male == NO") : - nil, + filterOption == .male ? NSPredicate(format: "male == YES") : nil, + filterOption == .female ? NSPredicate(format: "male == NO") : nil, ] - + if let mostRecentDate { predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } - + if hideAssimilation { predicates.append(NSPredicate(format: "assimilation == %@", "Non")) } - + if selectedAgeCategory != .unlisted { let computedBirthYear = selectedAgeCategory.computedBirthYear() if let left = computedBirthYear.0 { - predicates.append(NSPredicate(format: "birthYear >= %@", left.formattedAsRawString())) + predicates.append( + NSPredicate(format: "birthYear >= %@", left.formattedAsRawString())) } if let right = computedBirthYear.1 { - predicates.append(NSPredicate(format: "birthYear <= %@", right.formattedAsRawString())) + predicates.append( + NSPredicate(format: "birthYear <= %@", right.formattedAsRawString())) } } - + switch dataSet { case .national: break @@ -312,23 +421,22 @@ class SearchViewModel: ObservableObject, Identifiable { if hidePlayers?.isEmpty == false { predicates.append(NSPredicate(format: "NOT (license IN %@)", hidePlayers!)) } - + return NSCompoundPredicate(andPredicateWithSubpredicates: predicates.compactMap({ $0 })) } - func sortDescriptors() -> [SortDescriptor] { sortOption.sortDescriptors(ascending, dataSet: dataSet) } - + func nsSortDescriptors() -> [NSSortDescriptor] { sortDescriptors().map { NSSortDescriptor($0) } } - + static func getSpecialSlashPredicate(inputString: String) -> NSPredicate? { // Define a regular expression to find slashes between alphabetic characters (not digits) print(inputString) - + let pattern = /(\b[A-Za-z]+)\s*\/\s*([A-Za-z]+\b)/ // Find matches in the input string @@ -336,7 +444,7 @@ class SearchViewModel: ObservableObject, Identifiable { print("No valid name pairs found") return nil } - + let lastName1 = match.output.1.trimmingCharacters(in: .whitespacesAndNewlines) let lastName2 = match.output.2.trimmingCharacters(in: .whitespacesAndNewlines) @@ -345,20 +453,23 @@ class SearchViewModel: ObservableObject, Identifiable { print("One or both names are empty") return nil } - + // Create the NSPredicate for searching in the `lastName` field - let predicate = NSPredicate(format: "lastName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@", lastName1, lastName2) - + let predicate = NSPredicate( + format: "lastName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@", lastName1, lastName2) + // Output the result //print("Generated Predicate: \(predicate)") return predicate } - - static func pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { + static func pastePredicate( + pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption + ) -> NSPredicate? { var andPredicates = [NSPredicate]() var orPredicates = [NSPredicate]() + // Check for license numbers let matches = pasteField.licencesFound() let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } orPredicates = licensesPredicates @@ -367,59 +478,110 @@ class SearchViewModel: ObservableObject, Identifiable { return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates) } - let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) - - // Remove all characters that are not in the allowedCharacterSet - var text = pasteField.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmedMultiline - // Define the regex pattern to match digits - let digitPattern = /\b\w*\d\w*\b/ - - // Replace all occurrences of the pattern (digits) with an empty string - text = text.replacing(digitPattern, with: "").trimmingCharacters(in: .whitespacesAndNewlines) - - 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 }) - - //self.wordsCount = nameComponents.count + // Add gender filter if specified if filterOption == .female { andPredicates.append(NSPredicate(format: "male == NO")) + } else if filterOption == .male { + andPredicates.append(NSPredicate(format: "male == YES")) } - + + // Add date filter if specified if let mostRecentDate { andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } - + // Check for slashes (representing alternatives) if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) { orPredicates.append(slashPredicate) } - print("nameComponents", nameComponents.count) - - if nameComponents.count < 50 { - if nameComponents.count > 1 { - orPredicates.append(contentsOf: nameComponents.pairs().map { - return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }) - } else { - orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) + // Prepare text for processing - preserve hyphens but remove digits + var text = + pasteField + .replacingOccurrences(of: "/", with: " ") // Replace slashes with spaces + .trimmingCharacters(in: .whitespacesAndNewlines) + + // Remove digits + let digitPattern = /\b\w*\d\w*\b/ + text = text.replacing(digitPattern, with: "").trimmingCharacters( + in: .whitespacesAndNewlines) + + // Split text by whitespace to get potential name components + let textComponents = text.components(separatedBy: .whitespacesAndNewlines) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { + !$0.isEmpty && $0.count > 1 && !["de", "la", "le", "du"].contains($0.lowercased()) + } + + if textComponents.count < 50 { + // Handle exact fullname match + let fullName = textComponents.joined(separator: " ") + if !fullName.isEmpty { + orPredicates.append( + NSPredicate(format: "canonicalFullName CONTAINS[cd] %@", fullName)) } + + // Handle hyphenated last names + let hyphenatedComponents = textComponents.filter { $0.contains("-") } + for component in hyphenatedComponents { + orPredicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", component)) + + // Also search for each part of the hyphenated name + let parts = component.components(separatedBy: "-") + for part in parts { + if part.count > 1 { + orPredicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", part)) + } + } + } + + // Try different combinations for first/last name + if textComponents.count > 1 { + // Try each pair of components as first+last and last+first + for i in 0.. String { switch self { case .club: @@ -485,7 +652,7 @@ enum SearchToken: String, CaseIterable, Identifiable { return "Année de naissance" } } - + var shortLocalizedLabel: String { switch self { case .club: @@ -502,7 +669,7 @@ enum SearchToken: String, CaseIterable, Identifiable { return "Né(e) en" } } - + func icon() -> String { switch self { case .club: @@ -519,7 +686,7 @@ enum SearchToken: String, CaseIterable, Identifiable { return "figure.racquetball" } } - + var systemImage: String { switch self { case .club: @@ -544,9 +711,9 @@ enum DataSet: Int, Identifiable { case club case favoriteClubs case favoritePlayers - - static let allCases : [DataSet] = [.national, .ligue, .club, .favoriteClubs] - + + static let allCases: [DataSet] = [.national, .ligue, .club, .favoriteClubs] + var id: Int { rawValue } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { @@ -560,9 +727,9 @@ enum DataSet: Int, Identifiable { return "Favori" } } - + var tokens: [SearchToken] { - var _tokens : [SearchToken] = [] + var _tokens: [SearchToken] = [] switch self { case .national: _tokens = [.club, .ligue, .rankMoreThan, .rankLessThan, .rankBetween] @@ -573,7 +740,7 @@ enum DataSet: Int, Identifiable { case .favoritePlayers, .favoriteClubs: _tokens = [.rankMoreThan, .rankLessThan, .rankBetween] } - + _tokens.append(.age) return _tokens } @@ -585,7 +752,7 @@ enum SortOption: Int, CaseIterable, Identifiable { case tournamentCount case points case progression - + var id: Int { self.rawValue } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { @@ -601,23 +768,45 @@ enum SortOption: Int, CaseIterable, Identifiable { return "Progression" } } - + func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor] { switch self { case .name: - return [SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation)] + return [ + SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse), + SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), + ] case .rank: - if (dataSet == .national || dataSet == .ligue) { - return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)] + if dataSet == .national || dataSet == .ligue { + return [ + SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse) + ] } else { - return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] + return [ + SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse), + SortDescriptor(\ImportedPlayer.assimilation), + SortDescriptor(\ImportedPlayer.lastName), + ] } case .tournamentCount: - return [SortDescriptor(\ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] + return [ + SortDescriptor( + \ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse), + SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), + SortDescriptor(\ImportedPlayer.lastName), + ] case .points: - return [SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] + return [ + SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse), + SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), + SortDescriptor(\ImportedPlayer.lastName), + ] case .progression: - return [SortDescriptor(\ImportedPlayer.progression, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] + return [ + SortDescriptor(\ImportedPlayer.progression, order: ascending ? .forward : .reverse), + SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), + SortDescriptor(\ImportedPlayer.lastName), + ] } } } @@ -626,20 +815,20 @@ enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable { case all = -1 case male = 1 case female = 0 - + var id: Int { rawValue } func icon() -> String { switch self { - case .all: - return "Tous" - case .male: - return "Homme" - case .female: - return "Femme" + case .all: + return "Tous" + case .male: + return "Homme" + case .female: + return "Femme" } } - + var localizedPlayerLabel: String { switch self { case .female: diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 4e9fc2e..62c265c 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -214,7 +214,9 @@ struct AddTeamView: View { ToolbarItem(placement: .bottomBar) { PasteButton(payloadType: String.self) { strings in let first = strings.first ?? "" - handlePasteString(first) + DispatchQueue.main.async { + self.handlePasteString(first) + } } .disabled(_limitPlayerCount()) .foregroundStyle(.master)