parent
82140ae210
commit
4b5c05dfa3
@ -0,0 +1,538 @@ |
||||
// |
||||
// 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 teamsHash: Int? |
||||
@State private var presentationCount: Int = 0 |
||||
@State private var confirmDuplicate: Bool = false |
||||
|
||||
var tournamentStore: TournamentStore { |
||||
return self.tournament.tournamentStore |
||||
} |
||||
|
||||
init(tournament: Tournament, pasteString: String? = nil, editedTeam: TeamRegistration?) { |
||||
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 |
||||
} |
||||
} |
||||
|
||||
// Function to create a simple hash from a list of IDs |
||||
private func _simpleHash(ids: [String]) -> Int { |
||||
// Combine the hash values of each string |
||||
return ids.reduce(0) { $0 ^ $1.hashValue } |
||||
} |
||||
|
||||
// Function to check if two lists of IDs produce different hashes |
||||
private func _areDifferent(ids1: [String], ids2: [String]) -> Bool { |
||||
return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) |
||||
} |
||||
|
||||
private func _setHash() async { |
||||
#if DEBUG_TIME //DEBUGING TIME |
||||
let start = Date() |
||||
defer { |
||||
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||
} |
||||
#endif |
||||
let selectedSortedTeams = tournament.selectedSortedTeams() |
||||
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { |
||||
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) |
||||
} |
||||
} |
||||
|
||||
private func _handleHashDiff() async { |
||||
let selectedSortedTeams = tournament.selectedSortedTeams() |
||||
let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) |
||||
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) { |
||||
self.teamsHash = newHash |
||||
if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false { |
||||
self.tournament.shouldVerifyBracket = true |
||||
self.tournament.shouldVerifyGroupStage = true |
||||
|
||||
let waitingList = self.tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true) |
||||
waitingList.forEach { team in |
||||
if team.bracketPosition != nil || team.groupStagePosition != nil { |
||||
team.resetPositions() |
||||
} |
||||
} |
||||
|
||||
do { |
||||
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: waitingList) |
||||
try dataStore.tournaments.addOrUpdate(instance: tournament) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
var body: some View { |
||||
_buildingTeamView() |
||||
.navigationBarBackButtonHidden(true) |
||||
.alert("Cette équipe existe déjà", isPresented: $confirmDuplicate) { |
||||
Button("Créer l'équipe quand même") { |
||||
_createTeam(checkDuplicates: false) |
||||
} |
||||
|
||||
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() |
||||
} |
||||
|
||||
private func _getTeams() { |
||||
Task { |
||||
await _setHash() |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _managementView() -> some View { |
||||
Section { |
||||
RowButtonView("Rechercher dans 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 text = pasteField.canonicalVersion |
||||
|
||||
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 contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) } |
||||
} else { |
||||
orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) } |
||||
} |
||||
|
||||
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) { |
||||
if checkDuplicates && _isDuplicate() { |
||||
confirmDuplicate = true |
||||
return |
||||
} |
||||
|
||||
let players = _currentSelection() |
||||
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!) |
||||
} |
||||
} |
||||
} |
||||
} 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)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
Section { |
||||
if editedTeam == nil { |
||||
if createdPlayerIds.isEmpty { |
||||
RowButtonView("Bloquer une place") { |
||||
_createTeam(checkDuplicates: false) |
||||
} |
||||
} else { |
||||
RowButtonView("Ajouter l'équipe") { |
||||
_createTeam(checkDuplicates: true) |
||||
} |
||||
} |
||||
} else { |
||||
RowButtonView("Modifier l'équipe") { |
||||
_updateTeam(checkDuplicates: false) |
||||
editedTeam = nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
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.") |
||||
} actions: { |
||||
RowButtonView("Créer un joueur non classé") { |
||||
presentPlayerCreation = true |
||||
} |
||||
|
||||
RowButtonView("Effacer cette recherche") { |
||||
self.pasteString = nil |
||||
} |
||||
} |
||||
|
||||
} else { |
||||
Section { |
||||
ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in |
||||
ImportedPlayerView(player: player).tag(player.license!) |
||||
} |
||||
} header: { |
||||
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix) |
||||
} |
||||
} |
||||
} else { |
||||
_managementView() |
||||
} |
||||
} |
||||
.onAppear { |
||||
_getTeams() |
||||
} |
||||
.onDisappear { |
||||
Task { |
||||
await _handleHashDiff() |
||||
|
||||
} |
||||
} |
||||
.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) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue