|
|
|
|
@ -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? |
|
|
|
|
""" |
|
|
|
|
] |
|
|
|
|
|