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.
502 lines
20 KiB
502 lines
20 KiB
//
|
|
// 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<ImportedPlayer>
|
|
|
|
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<PlayerRegistration> = Set()
|
|
@State private var createdPlayerIds: Set<String> = 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 presentationCount: Int = 0
|
|
@State private var confirmDuplicate: Bool = false
|
|
@State private var homonyms: [PlayerRegistration] = []
|
|
@State private var confirmHomonym: Bool = false
|
|
|
|
var tournamentStore: TournamentStore {
|
|
return self.tournament.tournamentStore
|
|
}
|
|
|
|
init(tournament: Tournament, pasteString: String? = nil, editedTeam: TeamRegistration? = nil) {
|
|
self.tournament = tournament
|
|
_editedTeam = .init(wrappedValue: editedTeam)
|
|
if let team = editedTeam {
|
|
var createdPlayers: Set<PlayerRegistration> = Set()
|
|
var createdPlayerIds: Set<String> = 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<ImportedPlayer>(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
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
_buildingTeamView()
|
|
.navigationBarBackButtonHidden(true)
|
|
.alert("Présence d'homonyme", isPresented: $confirmHomonym) {
|
|
Button("Créer l'équipe quand même") {
|
|
_createTeam(checkDuplicates: false, checkHomonym: false)
|
|
}
|
|
|
|
Button("Annuler", role: .cancel) {
|
|
confirmHomonym = false
|
|
}
|
|
|
|
} message: {
|
|
let plural : String = homonyms.count > 1 ? "nt chacun" : ""
|
|
Text(homonyms.map({ $0.playerLabel() }).joined(separator: ", ") + " possède" + plural + " au moins un homonyme dans la base fédérale, vérifiez bien leur licence.")
|
|
}
|
|
.alert("Cette équipe existe déjà", isPresented: $confirmDuplicate) {
|
|
Button("Créer l'équipe quand même") {
|
|
_createTeam(checkDuplicates: false, checkHomonym: true)
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _managementView() -> some View {
|
|
Section {
|
|
RowButtonView("Ajouter via 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 allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
|
|
|
|
// Remove all characters that are not in the allowedCharacterSet
|
|
let text = pasteField.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed
|
|
|
|
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 BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }
|
|
} else {
|
|
orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }
|
|
}
|
|
|
|
let components = text.split(separator: " ").sorted()
|
|
let pattern = components.joined(separator: ".*")
|
|
print(text, pattern)
|
|
let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
|
|
orPredicates.append(canonicalFullNamePredicate)
|
|
|
|
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<PlayerRegistration> {
|
|
var currentSelection = Set<PlayerRegistration>()
|
|
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, checkHomonym: Bool) {
|
|
if checkDuplicates && _isDuplicate() {
|
|
confirmDuplicate = true
|
|
return
|
|
}
|
|
|
|
let players = _currentSelection()
|
|
|
|
if checkHomonym {
|
|
homonyms = players.filter({ $0.hasHomonym() })
|
|
if homonyms.isEmpty == false {
|
|
confirmHomonym = true
|
|
return
|
|
}
|
|
}
|
|
|
|
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!)
|
|
}
|
|
}
|
|
}
|
|
if editedTeam == nil {
|
|
if createdPlayerIds.isEmpty {
|
|
RowButtonView("Bloquer une place") {
|
|
_createTeam(checkDuplicates: false, checkHomonym: false)
|
|
}
|
|
} else {
|
|
RowButtonView("Ajouter l'équipe") {
|
|
_createTeam(checkDuplicates: true, checkHomonym: true)
|
|
}
|
|
}
|
|
} else {
|
|
RowButtonView("Confirmer") {
|
|
_updateTeam(checkDuplicates: false)
|
|
editedTeam = nil
|
|
}
|
|
}
|
|
} 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 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. Attention, si un joueur n'a pas joué de tournoi dans les 12 derniers, Padel Club ne pourra pas le trouver.")
|
|
} actions: {
|
|
RowButtonView("Créer un joueur non classé") {
|
|
presentPlayerCreation = true
|
|
}
|
|
|
|
RowButtonView("Effacer cette recherche") {
|
|
self.pasteString = nil
|
|
}
|
|
}
|
|
|
|
} else {
|
|
Section {
|
|
let sortedPlayers = fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })
|
|
ForEach(sortedPlayers) { player in
|
|
ImportedPlayerView(player: player).tag(player.license!)
|
|
}
|
|
} header: {
|
|
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix)
|
|
}
|
|
}
|
|
} else {
|
|
_managementView()
|
|
}
|
|
}
|
|
.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)
|
|
}
|
|
}
|
|
}
|
|
|