You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
14 KiB
437 lines
14 KiB
//
|
|
// SearchViewModel.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by Razmig Sarkissian on 07/02/2024.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
class SearchViewModel: ObservableObject, Identifiable {
|
|
let id: UUID = UUID()
|
|
var allowSelection : Int = 0
|
|
var user: User? = nil
|
|
var codeClub: String? = nil
|
|
var clubName: String? = nil
|
|
var ligueName: String? = nil
|
|
@Published var debouncableText: String = ""
|
|
@Published var searchText: String = ""
|
|
@Published var task: DispatchWorkItem?
|
|
@Published var computedSearchText: String = ""
|
|
@Published var tokens = [SearchToken]()
|
|
@Published var suggestedTokens = [SearchToken]()
|
|
@Published var dataSet: DataSet = .national
|
|
@Published var filterOption = PlayerFilterOption.all
|
|
@Published var hideAssimilation: Bool = false
|
|
@Published var ascending: Bool = true
|
|
@Published var sortOption: SortOption = .rank
|
|
@Published var selectedPlayers: Set<ImportedPlayer> = Set()
|
|
@Published var filterSelectionEnabled: Bool = false
|
|
@Published var isPresented: Bool = false
|
|
|
|
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 ? 0.4 : 0.1
|
|
}
|
|
|
|
var throttleTrigger: Double {
|
|
dataSet == .national ? 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 pas, Padel Club ne pourra pas le trouver.")
|
|
}
|
|
return message.joined(separator: "\n")
|
|
}
|
|
|
|
func getCodeClub() -> String? {
|
|
if let codeClub { return codeClub }
|
|
// if let userCodeClub = user?.player?.codeClub { return userCodeClub }
|
|
return nil
|
|
}
|
|
|
|
func getClubName() -> String? {
|
|
if let clubName { return clubName }
|
|
// if let userClubName = user?.player?.clubName { return userClubName }
|
|
return nil
|
|
}
|
|
|
|
func showIndex() -> Bool {
|
|
if dataSet == .national { return false }
|
|
if filterOption == .all { return false }
|
|
return true
|
|
}
|
|
|
|
func prompt(forDataSet: DataSet) -> String {
|
|
switch forDataSet {
|
|
case .national:
|
|
if let mostRecentDate {
|
|
return "base fédérale \(mostRecentDate.monthYearFormatted)"
|
|
} else {
|
|
return "rechercher"
|
|
}
|
|
case .ligue:
|
|
return "dans cette ligue"
|
|
case .club:
|
|
return "dans ce club"
|
|
case .favorite:
|
|
return "dans mes favoris"
|
|
}
|
|
}
|
|
|
|
func label(forDataSet: DataSet) -> String {
|
|
switch forDataSet {
|
|
case .national:
|
|
return "National"
|
|
case .ligue:
|
|
return (ligueName)?.capitalized ?? "Ma ligue"
|
|
case .club:
|
|
return (clubName)?.capitalized ?? "Mon club"
|
|
case .favorite:
|
|
return "Mes favoris"
|
|
}
|
|
}
|
|
|
|
func words() -> [String] {
|
|
searchText.canonicalVersion.trimmed.components(separatedBy: .whitespaces)
|
|
}
|
|
|
|
func wordsPredicates() -> NSPredicate? {
|
|
let words = words().filter({ $0.isEmpty })
|
|
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)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func orPredicate() -> NSPredicate? {
|
|
var predicates : [NSPredicate] = []
|
|
let searchText = searchText.canonicalVersion
|
|
switch tokens.first {
|
|
case .none:
|
|
if searchText.isEmpty == false {
|
|
let wordsPredicates = wordsPredicates()
|
|
if let wordsPredicates {
|
|
predicates.append(wordsPredicates)
|
|
} else {
|
|
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", searchText))
|
|
predicates.append(NSPredicate(format: "license contains[cd] %@", searchText))
|
|
}
|
|
}
|
|
case .ligue:
|
|
if searchText.isEmpty {
|
|
predicates.append(NSPredicate(format: "ligueName == nil"))
|
|
} else {
|
|
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", searchText))
|
|
}
|
|
case .club:
|
|
if searchText.isEmpty {
|
|
predicates.append(NSPredicate(format: "clubName == nil"))
|
|
} else {
|
|
predicates.append(NSPredicate(format: "clubName contains[cd] %@", searchText))
|
|
}
|
|
case .rankMoreThan:
|
|
if searchText.isEmpty || Int(searchText) == 0 {
|
|
predicates.append(NSPredicate(format: "rank == 0"))
|
|
} else {
|
|
predicates.append(NSPredicate(format: "rank >= %@", searchText))
|
|
}
|
|
case .rankLessThan:
|
|
if searchText.isEmpty || Int(searchText) == 0 {
|
|
predicates.append(NSPredicate(format: "rank == 0"))
|
|
} else {
|
|
predicates.append(NSPredicate(format: "rank <= %@", searchText))
|
|
}
|
|
case .rankBetween:
|
|
let values = searchText.components(separatedBy: ",")
|
|
if searchText.isEmpty || values.count != 2 {
|
|
predicates.append(NSPredicate(format: "rank == 0"))
|
|
} else {
|
|
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
|
|
}
|
|
}
|
|
|
|
if predicates.isEmpty {
|
|
return nil
|
|
}
|
|
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
|
|
}
|
|
|
|
func predicate() -> NSPredicate? {
|
|
var predicates : [NSPredicate?] = [
|
|
orPredicate(),
|
|
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"))
|
|
}
|
|
|
|
switch dataSet {
|
|
case .national:
|
|
break
|
|
case .ligue:
|
|
if let ligueName {
|
|
predicates.append(NSPredicate(format: "ligueName == %@", ligueName))
|
|
} else {
|
|
predicates.append(NSPredicate(format: "ligueName == nil"))
|
|
}
|
|
case .club:
|
|
if let codeClub {
|
|
predicates.append(NSPredicate(format: "clubCode == %@", codeClub))
|
|
} else {
|
|
predicates.append(NSPredicate(format: "clubCode == nil"))
|
|
}
|
|
case .favorite:
|
|
predicates.append(NSPredicate(format: "license == nil"))
|
|
}
|
|
|
|
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates.compactMap({ $0 }))
|
|
}
|
|
|
|
|
|
func sortDescriptors() -> [SortDescriptor<ImportedPlayer>] {
|
|
sortOption.sortDescriptors(ascending, dataSet: dataSet)
|
|
}
|
|
|
|
func nsSortDescriptors() -> [NSSortDescriptor] {
|
|
sortDescriptors().map { NSSortDescriptor($0) }
|
|
}
|
|
}
|
|
|
|
enum SearchToken: String, CaseIterable, Identifiable {
|
|
case club = "club"
|
|
case ligue = "ligue"
|
|
case rankMoreThan = "rang >"
|
|
case rankLessThan = "rang <"
|
|
case rankBetween = "rang <>"
|
|
|
|
var id: String {
|
|
rawValue
|
|
}
|
|
|
|
var message: String {
|
|
switch self {
|
|
case .club:
|
|
return "Taper le nom d'un club pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
|
|
case .ligue:
|
|
return "Taper le nom d'une ligue pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
|
|
case .rankMoreThan:
|
|
return "Taper un nombre pour chercher les joueurs ayant un classement supérieur ou égale."
|
|
case .rankLessThan:
|
|
return "Taper un nombre pour chercher les joueurs ayant un classement inférieur ou égale."
|
|
case .rankBetween:
|
|
return "Taper deux nombres séparés par une virgule pour chercher les joueurs dans cette intervalle de classement"
|
|
}
|
|
}
|
|
|
|
var titleLabel: String {
|
|
switch self {
|
|
case .club:
|
|
return "Chercher un club"
|
|
case .ligue:
|
|
return "Chercher une ligue"
|
|
case .rankMoreThan, .rankLessThan:
|
|
return "Chercher un rang"
|
|
case .rankBetween:
|
|
return "Chercher une intervalle de classement"
|
|
}
|
|
}
|
|
|
|
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
switch self {
|
|
case .club:
|
|
return "Club"
|
|
case .ligue:
|
|
return "Ligue"
|
|
case .rankMoreThan:
|
|
return "Rang supérieur ou égale à"
|
|
case .rankLessThan:
|
|
return "Rang inférieur ou égale à"
|
|
case .rankBetween:
|
|
return "Rang entre deux valeurs"
|
|
}
|
|
}
|
|
|
|
var shortLocalizedLabel: String {
|
|
switch self {
|
|
case .club:
|
|
return "Club"
|
|
case .ligue:
|
|
return "Ligue"
|
|
case .rankMoreThan:
|
|
return "Rang ≥"
|
|
case .rankLessThan:
|
|
return "Rang ≤"
|
|
case .rankBetween:
|
|
return "Rang ≥,≤"
|
|
}
|
|
}
|
|
|
|
func icon() -> String {
|
|
switch self {
|
|
case .club:
|
|
return "house.and.flag.fill"
|
|
case .ligue:
|
|
return "house.and.flag.fill"
|
|
case .rankMoreThan:
|
|
return "figure.racquetball"
|
|
case .rankLessThan:
|
|
return "figure.racquetball"
|
|
case .rankBetween:
|
|
return "figure.racquetball"
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .club:
|
|
return "house.and.flag.fill"
|
|
case .ligue:
|
|
return "house.and.flag.fill"
|
|
case .rankMoreThan:
|
|
return "figure.racquetball"
|
|
case .rankLessThan:
|
|
return "figure.racquetball"
|
|
case .rankBetween:
|
|
return "figure.racquetball"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum DataSet: Int, CaseIterable, Identifiable {
|
|
case national
|
|
case ligue
|
|
case club
|
|
case favorite
|
|
|
|
var id: Int { rawValue }
|
|
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
switch self {
|
|
case .national:
|
|
return "National"
|
|
case .ligue:
|
|
return "Ligue"
|
|
case .club:
|
|
return "Club"
|
|
case .favorite:
|
|
return "Favori"
|
|
}
|
|
}
|
|
|
|
var tokens: [SearchToken] {
|
|
switch self {
|
|
case .national:
|
|
return [.club, .ligue, .rankMoreThan, .rankLessThan, .rankBetween]
|
|
case .ligue:
|
|
return [.club, .rankMoreThan, .rankLessThan, .rankBetween]
|
|
case .club:
|
|
return [.rankMoreThan, .rankLessThan, .rankBetween]
|
|
case .favorite:
|
|
return [.rankMoreThan, .rankLessThan, .rankBetween]
|
|
}
|
|
}
|
|
}
|
|
|
|
enum SortOption: Int, CaseIterable, Identifiable {
|
|
case name
|
|
case rank
|
|
case tournamentCount
|
|
case points
|
|
|
|
var id: Int { self.rawValue }
|
|
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
switch self {
|
|
case .name:
|
|
return "Nom"
|
|
case .rank:
|
|
return "Rang"
|
|
case .tournamentCount:
|
|
return "Tournoi"
|
|
case .points:
|
|
return "Points"
|
|
}
|
|
}
|
|
|
|
func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor<ImportedPlayer>] {
|
|
switch self {
|
|
case .name:
|
|
return [SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation)]
|
|
case .rank:
|
|
if dataSet == .national {
|
|
return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)]
|
|
} else {
|
|
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)]
|
|
case .points:
|
|
return [SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)]
|
|
}
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
var localizedPlayerLabel: String {
|
|
switch self {
|
|
case .female:
|
|
return "joueuse"
|
|
default:
|
|
return "joueur"
|
|
}
|
|
}
|
|
|
|
}
|
|
|