fix searching players issues

sync2
Raz 1 year ago
parent 13be596b26
commit b2f38febc8
  1. 3
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  2. 36
      PadelClub/Data/Tournament.swift
  3. 4
      PadelClub/Extensions/Calendar+Extensions.swift
  4. 22
      PadelClub/Utils/PadelRule.swift
  5. 86
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  6. 173
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  7. 19
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  8. 4
      PadelClubTests/ServerDataTests.swift

@ -74,7 +74,8 @@ extension ImportedPlayer: PlayerHolder {
firstName?.localizedCaseInsensitiveContains(searchField) == true || lastName?.localizedCaseInsensitiveContains(searchField) == true
}
func hitForSearch(_ searchText: String) -> Int {
func hitForSearch(_ searchText: String?) -> Int {
guard let searchText else { return 0 }
var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current)
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ")
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .symbols, replacementString: " ")

@ -1009,15 +1009,39 @@ defer {
return []
}
return players.filter { player in
if player.rank == nil { return false }
if player.computedRank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
return true
} else {
return false
}
return isPlayerRankInadequate(player: player)
}
}
func isPlayerRankInadequate(player: PlayerHolder) -> Bool {
guard let rank = player.getRank() else { return false }
let _rank = player.male ? rank : rank + PlayerRegistration.addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0)
if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
return true
} else {
return false
}
}
func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
if startDate.isInCurrentYear() == false {
return []
}
return players.filter { player in
return isPlayerAgeInadequate(player: player)
}
}
func isPlayerAgeInadequate(player: PlayerHolder) -> Bool {
guard let computedAge = player.computedAge else { return false }
if federalTournamentAge.isAgeValid(age: computedAge) == false {
return true
} else {
return false
}
}
func mandatoryRegistrationCloseDate() -> Date? {
switch tournamentLevel {
case .p500, .p1000, .p1500, .p2000:

@ -30,8 +30,8 @@ extension Calendar {
let currentYear = component(.year, from: currentDate)
// Define the date components for 1st September and 31st December of the current year
var septemberFirstComponents = DateComponents(year: currentYear, month: 9, day: 1)
var decemberThirtyFirstComponents = DateComponents(year: currentYear, month: 12, day: 31)
let septemberFirstComponents = DateComponents(year: currentYear, month: 9, day: 1)
let decemberThirtyFirstComponents = DateComponents(year: currentYear, month: 12, day: 31)
// Get the actual dates for 1st September and 31st December
let septemberFirst = date(from: septemberFirstComponents)!

@ -276,6 +276,28 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
var tournamentDescriptionLabel: String {
return localizedLabel()
}
func isAgeValid(age: Int?) -> Bool {
guard let age else { return true }
switch self {
case .unlisted:
return true
case .a11_12:
return age < 13
case .a13_14:
return age < 15
case .a15_16:
return age < 17
case .a17_18:
return age < 19
case .senior:
return age >= 11
case .a45:
return age >= 45
case .a55:
return age >= 55
}
}
}
enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {

@ -23,9 +23,10 @@ struct ActivityView: View {
@State private var presentClubSearchView: Bool = false
@State private var quickAccessScreen: QuickAccessScreen? = nil
@State private var displaySearchView: Bool = false
@State private var pasteString: String? = nil
enum QuickAccessScreen : Identifiable, Hashable {
case inscription(pasteString: String)
case inscription
var id: String {
switch self {
@ -75,13 +76,29 @@ struct ActivityView: View {
@ViewBuilder
private func _pasteView() -> some View {
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
quickAccessScreen = .inscription(pasteString: first)
Button {
quickAccessScreen = .inscription
} label: {
Image(systemName: "person.crop.circle.badge.plus")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.foregroundStyle(.master)
.labelStyle(.iconOnly)
.buttonBorderShape(.capsule)
.accessibilityLabel("Ajouter une équipe")
// if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true {
// PasteButton(payloadType: String.self) { strings in
// let first = strings.first ?? "aucun texte"
// quickAccessScreen = .inscription(pasteString: first)
// }
// .foregroundStyle(.master)
// .labelStyle(.iconOnly)
// .buttonBorderShape(.capsule)
// .onAppear {
// pasteButtonIsDisplayed = true
// }
// } else if let pasteButtonIsDisplayed, pasteButtonIsDisplayed == false {
// }
}
var body: some View {
@ -189,6 +206,10 @@ struct ActivityView: View {
.navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament)
}
// .onDisappear(perform: {
// pasteButtonIsDisplayed = nil
// print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed)
// })
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button {
@ -291,28 +312,41 @@ struct ActivityView: View {
}
.sheet(item: $quickAccessScreen) { screen in
switch screen {
case .inscription(let pasteString):
case .inscription:
NavigationStack {
List {
Section {
Text(pasteString)
} header: {
Text("Contenu du presse-papier")
if let pasteString {
Section {
Text(pasteString)
.frame(maxWidth: .infinity)
.overlay {
if pasteString.isEmpty {
Text("Le presse-papier est vide")
.foregroundStyle(.secondary)
.italic()
}
}
} header: {
Text("Contenu du presse-papier")
}
}
Section {
ForEach(getRunningTournaments()) { tournament in
NavigationLink {
AddTeamView(tournament: tournament, pasteString: pasteString, editedTeam: nil)
} label: {
VStack(alignment: .leading) {
LabeledContent {
Text(tournament.unsortedTeamsWithoutWO().count.formatted())
} label: {
Text(tournament.tournamentTitle())
Text(tournament.formattedDate()).foregroundStyle(.secondary)
Text(tournament.formattedDate())
}
}
}
} header: {
Text("À coller dans la liste d'inscription")
Text("Ajouter à la liste d'inscription")
}
}
.toolbar {
@ -321,6 +355,26 @@ struct ActivityView: View {
self.quickAccessScreen = nil
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
pasteString = UIPasteboard.general.string ?? ""
} label: {
Label("Coller", systemImage: "doc.on.clipboard").labelStyle(.iconOnly)
}
.foregroundStyle(.master)
.labelStyle(.iconOnly)
.buttonBorderShape(.capsule)
}
ToolbarItem(placement: .bottomBar) {
PasteButton(payloadType: String.self) { strings in
pasteString = strings.first ?? ""
}
.foregroundStyle(.master)
.labelStyle(.titleAndIcon)
.buttonBorderShape(.capsule)
}
}
.navigationTitle("Choix du tournoi")
.navigationBarTitleDisplayMode(.inline)

@ -42,7 +42,13 @@ struct AddTeamView: View {
@State private var confirmHomonym: Bool = false
@State private var editableTextField: String = ""
@State private var textHeight: CGFloat = 100 // Default height
@State private var hitsForSearch: [Int: Int] = [:]
@State private var searchForHit: Int = 0
@State private var displayWarningNotEnoughCharacter: Bool = false
@State private var testMessageIndex: Int = 0
let filterLimit : Int = 1000
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
@ -74,7 +80,7 @@ struct AddTeamView: View {
}
var body: some View {
if pasteString != nil, fetchPlayers.isEmpty == false {
if let pasteString, pasteString.isEmpty == false, fetchPlayers.isEmpty == false {
computedBody
.searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher dans les résultats"))
} else {
@ -86,14 +92,27 @@ struct AddTeamView: View {
List(selection: $createdPlayerIds) {
_buildingTeamView()
}
.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
.onReceive(fetchPlayers.publisher.count()) { receivedCount in // <-- here
if receivedCount < filterLimit, let pasteString, pasteString.isEmpty == false, count == 2, autoSelect == true {
fetchPlayers.filter { hitForSearch($0, pasteString) >= hitTarget }.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
autoSelect = false
}
}
.overlay(alignment: .bottom) {
if displayWarningNotEnoughCharacter {
Text("2 lettres mininum")
.toastFormatted()
.animation(.easeInOut(duration: 2.0), value: displayWarningNotEnoughCharacter)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
displayWarningNotEnoughCharacter = false
}
}
}
}
.alert("Présence d'homonyme", isPresented: $confirmHomonym) {
Button("Créer l'équipe quand même") {
_createTeam(checkDuplicates: false, checkHomonym: false)
@ -157,7 +176,7 @@ struct AddTeamView: View {
if pasteString == nil {
ToolbarItem(placement: .bottomBar) {
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
let first = strings.first ?? ""
handlePasteString(first)
}
.foregroundStyle(.master)
@ -165,6 +184,26 @@ struct AddTeamView: View {
.buttonBorderShape(.capsule)
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
let generalString = UIPasteboard.general.string ?? ""
#if targetEnvironment(simulator)
let s = testMessages[testMessageIndex % testMessages.count]
handlePasteString(s)
testMessageIndex += 1
#else
handlePasteString(generalString)
#endif
} label: {
Label("Coller", systemImage: "doc.on.clipboard").labelStyle(.iconOnly)
}
.foregroundStyle(.master)
.labelStyle(.iconOnly)
.buttonBorderShape(.capsule)
}
}
.navigationBarBackButtonHidden(true)
.toolbarBackground(.visible, for: .navigationBar)
@ -365,8 +404,12 @@ struct AddTeamView: View {
}
Spacer()
Button("Chercher") {
self.handlePasteString(editableTextField)
self.focusedField = nil
if editableTextField.count > 1 {
self.handlePasteString(editableTextField)
self.focusedField = nil
} else {
self.displayWarningNotEnoughCharacter = true
}
}
.buttonStyle(.bordered)
}
@ -393,7 +436,11 @@ struct AddTeamView: View {
if let p = createdPlayers.first(where: { $0.id == id }) {
VStack(alignment: .leading, spacing: 0) {
if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == false {
Text("Déjà inscrit !!").foregroundStyle(.logoRed).bold()
Text("Déjà inscrit !").foregroundStyle(.logoRed).bold()
} else if tournament.isPlayerAgeInadequate(player: p) {
Text("Âge invalide !").foregroundStyle(.logoRed).bold()
} else if tournament.isPlayerRankInadequate(player: p) {
Text("Trop bien classé !").foregroundStyle(.logoRed).bold()
}
PlayerView(player: p).tag(p.id)
.environment(tournament)
@ -401,8 +448,12 @@ struct AddTeamView: View {
}
if let p = fetchPlayers.first(where: { $0.license == id }) {
VStack(alignment: .leading, spacing: 0) {
if pasteString != nil, unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil {
if let pasteString, pasteString.isEmpty == false, unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil {
Text("Déjà inscrit !").foregroundStyle(.logoRed).bold()
} else if tournament.isPlayerAgeInadequate(player: p) {
Text("Âge invalide !").foregroundStyle(.logoRed).bold()
} else if tournament.isPlayerRankInadequate(player: p) {
Text("Trop bien classé !").foregroundStyle(.logoRed).bold()
}
ImportedPlayerView(player: p).tag(p.license!)
}
@ -454,8 +505,8 @@ struct AddTeamView: View {
}
if let pasteString {
let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty })
if let pasteString, pasteString.isEmpty == false {
let sortedPlayers = _searchFilteredPlayers()
if sortedPlayers.isEmpty {
ContentUnavailableView {
@ -478,20 +529,44 @@ struct AddTeamView: View {
}
} else {
_listOfPlayers(pasteString: pasteString)
_listOfPlayers(searchFilteredPlayers: sortedPlayers, pasteString: pasteString)
}
} else {
_managementView()
}
}
@MainActor
func hitForSearch(_ ip: ImportedPlayer, _ pasteString: String?) -> Int {
guard let pasteString else { return 0 }
let _searchForHit = pasteString.hashValue
if searchForHit != _searchForHit {
DispatchQueue.main.async {
searchForHit = _searchForHit
hitsForSearch = [:]
}
}
let value = hitsForSearch[ip.id.hashValue]
if let value {
return value
} else {
let hit = ip.hitForSearch(pasteString)
DispatchQueue.main.async {
hitsForSearch[ip.id.hashValue] = hit
}
return hit
}
}
private var count: Int {
return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count
return fetchPlayers.filter { hitForSearch($0, 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 }
if fetchPlayers.filter({ hitForSearch($0, pasteString) == 100 }).count == 2 { return 100 }
} else {
return 2
}
@ -506,24 +581,22 @@ struct AddTeamView: View {
}
}
@MainActor
private func handlePasteString(_ first: String) {
Task {
await MainActor.run {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first
editableTextField = first
textHeight = Self._calculateHeight(text: first)
autoSelect = true
}
if first.isEmpty == false {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
autoSelect = true
}
pasteString = first
editableTextField = first
textHeight = Self._calculateHeight(text: first)
}
@ViewBuilder
private func _listOfPlayers(pasteString: String) -> some View {
let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty }).sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })
private func _listOfPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> some View {
let sortedPlayers = _sortedPlayers(searchFilteredPlayers: searchFilteredPlayers, pasteString: pasteString)
Section {
ForEach(sortedPlayers) { player in
@ -535,4 +608,50 @@ struct AddTeamView: View {
}
}
private func _searchFilteredPlayers() -> [ImportedPlayer] {
if searchField.isEmpty {
return Array(fetchPlayers)
} else {
return fetchPlayers.filter({ $0.contains(searchField) })
}
}
private func _sortedPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> [ImportedPlayer] {
if searchFilteredPlayers.count < filterLimit {
return searchFilteredPlayers.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) })
} else {
return searchFilteredPlayers
}
}
}
let testMessages = [
"Anthony dovetta ( 3620578 K )et christophe capeau ( 4666443v)",
"""
ok merci, il s'agit de :
Olivier Seguin - licence 5033439
JPascal Bondierlange - licence :
6508359 С
Cordialement
""",
"""
Bonsoir Lise, peux tu nous inscrire pour le 250 hommes du 15 au 17 novembre ?
Paires DESCHAMPS/PARDO. En te remerciant. Bonne soirée
Franck
""",
"""
Coucou inscription pour le tournoi du 11 /
12 octobre
Dumoutier/ Liagre Charlotte
Merci de ta confirmation"
""",
"""
Anthony Contet 6081758f
Tullou Benjamin 8990867f
""",
"""
Sms Julien La Croix +33622886688
Salut Raz, c'est ! Ju Lacroix J'espère que tu vas bien depuis le temps! Est-ce que tu peux nous inscrire au 1000 de Bandol avec Derek Gerson stp?
"""
]

@ -18,6 +18,7 @@ struct InscriptionInfoView: View {
@State private var duplicates : [PlayerRegistration] = []
@State private var problematicPlayers : [PlayerRegistration] = []
@State private var inadequatePlayers : [PlayerRegistration] = []
@State private var ageInadequatePlayers : [PlayerRegistration] = []
@State private var playersWithoutValidLicense : [PlayerRegistration] = []
@State private var entriesFromBeachPadel : [TeamRegistration] = []
@State private var playersMissing : [TeamRegistration] = []
@ -177,6 +178,23 @@ struct InscriptionInfoView: View {
Text("Il s'agit des joueurs ou joueuses dont le rang est inférieur à la limite fédérale.")
}
Section {
DisclosureGroup {
ForEach(ageInadequatePlayers) { player in
ImportedPlayerView(player: player)
}
} label: {
LabeledContent {
Text(ageInadequatePlayers.count.formatted())
} label: {
Text("Joueurs trop jeunes ou trop âgés")
}
}
.listRowView(color: .logoRed)
} footer: {
Text("Il s'agit des joueurs ou joueuses dont l'âge sportif est inférieur ou supérieur à la limite fédérale.")
}
Section {
DisclosureGroup {
ForEach(playersWithoutValidLicense) {
@ -228,6 +246,7 @@ struct InscriptionInfoView: View {
homonyms = tournament.homonyms(in: players)
problematicPlayers = players.filter({ $0.sex == nil })
inadequatePlayers = tournament.inadequatePlayers(in: players)
ageInadequatePlayers = tournament.ageInadequatePlayers(in: players)
playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players)
entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() })
playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })

@ -150,7 +150,7 @@ final class ServerDataTests: XCTestCase {
return
}
let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, matchFormat: MatchFormat.nineGames, startDate: Date(), name: "Yeah!")
let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, matchFormat: MatchFormat.nineGames, startDate: Date(), name: "Yeah!", step: 1)
let gs: GroupStage = try await StoreCenter.main.service().post(groupStage)
assert(gs.tournament == groupStage.tournament)
@ -159,6 +159,8 @@ final class ServerDataTests: XCTestCase {
assert(gs.size == groupStage.size)
assert(gs.matchFormat == groupStage.matchFormat)
assert(gs.startDate != nil)
assert(gs.step == groupStage.step)
}

Loading…
Cancel
Save