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.
 
 
PadelClub/PadelClub/Views/Tournament/Screen/AddTeamView.swift

512 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
enum FocusField: Hashable {
case pasteField
}
@FocusState private var focusedField: FocusField?
@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
@State private var editableTextField: String = ""
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: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption))
_autoSelect = .init(wrappedValue: true)
_editableTextField = .init(wrappedValue: pasteString)
cancelShouldDismiss = true
}
}
var body: some View {
if pasteString != nil, fetchPlayers.isEmpty == false {
computedBody
.searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher dans les résultats"))
} else {
computedBody
}
}
var computedBody: some 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
createdPlayerIds.insert(player.license!)
}
autoSelect = false
}
}
.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)
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
if pasteString == nil {
ToolbarItem(placement: .bottomBar) {
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
handlePasteString(first)
}
.foregroundStyle(.master)
.labelStyle(.titleAndIcon)
.buttonBorderShape(.capsule)
}
}
}
.navigationBarBackButtonHidden(true)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(.visible, for: .bottomBar)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe")
.environment(\.editMode, Binding.constant(EditMode.active))
}
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
}
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
editableTextField = ""
if team.players().count > 1 {
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
editableTextField = ""
self.editedTeam = nil
if editedTeam.players().count > 1 {
dismiss()
}
}
@ViewBuilder
private func _buildingTeamView() -> some View {
if let pasteString {
Section {
TextEditor(text: $editableTextField)
.frame(minHeight: 120, maxHeight: .infinity)
.focused($focusedField, equals: .pasteField)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Fermer", role: .cancel) {
self.editableTextField = pasteString
self.focusedField = nil
}
Spacer()
Button("Chercher") {
self.handlePasteString(editableTextField)
self.focusedField = nil
}
.buttonStyle(.bordered)
}
}
} header: {
Text("Contenu du presse-papier")
} footer: {
HStack {
FooterButtonView("éditer") {
self.focusedField = .pasteField
}
Spacer()
FooterButtonView("effacer", role: .destructive) {
self.focusedField = nil
self.editableTextField = ""
self.pasteString = nil
self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll()
}
}
}
}
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))
}
}
// } else {
// Text("Préparation de l'équipe")
}
}
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
self.editableTextField = ""
}
}
} else {
_listOfPlayers(pasteString: pasteString)
}
} else {
_managementView()
}
}
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)
}
}
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
autoSelect = true
}
}
}
@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) })
Section {
ForEach(sortedPlayers) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(sortedPlayers.count.formatted() + " résultat" + sortedPlayers.count.pluralSuffix)
}
}
}