club_update
Razmig Sarkissian 1 year ago
parent 82140ae210
commit 4b5c05dfa3
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 49
      PadelClub/Data/Tournament.swift
  3. 11
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  4. 3
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  5. 538
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  6. 763
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -175,6 +175,7 @@
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; }; FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; };
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; };
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; }; FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; };
FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */; };
FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; }; FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; };
FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F72BCE78C70080F940 /* CashierView.swift */; }; FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F72BCE78C70080F940 /* CashierView.swift */; };
FF9267FA2BCE78EC0080F940 /* CashierDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */; }; FF9267FA2BCE78EC0080F940 /* CashierDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */; };
@ -511,6 +512,7 @@
FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = "<group>"; }; FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = "<group>"; };
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = "<group>"; }; FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = "<group>"; };
FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = "<group>"; }; FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = "<group>"; };
FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTeamView.swift; sourceTree = "<group>"; };
FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = "<group>"; }; FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = "<group>"; };
FF9267F72BCE78C70080F940 /* CashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierView.swift; sourceTree = "<group>"; }; FF9267F72BCE78C70080F940 /* CashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierView.swift; sourceTree = "<group>"; };
FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierDetailView.swift; sourceTree = "<group>"; }; FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierDetailView.swift; sourceTree = "<group>"; };
@ -993,6 +995,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */, FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */,
FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */,
FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */, FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */,
FF8F26532BAE1E4400650388 /* TableStructureView.swift */, FF8F26532BAE1E4400650388 /* TableStructureView.swift */,
FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */, FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */,
@ -1640,6 +1643,7 @@
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */, FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */,
FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */, FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */,
FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */, FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */,
FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */,
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,

@ -1049,6 +1049,10 @@ defer {
Logger.error(error) Logger.error(error)
} }
if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty {
setGroupStageTeams(randomize: groupStageSortMode == .random)
}
} }
func maximumCourtsPerGroupSage() -> Int { func maximumCourtsPerGroupSage() -> Int {
@ -1679,33 +1683,36 @@ defer {
deleteGroupStages() deleteGroupStages()
buildGroupStages() buildGroupStages()
} else { } else {
setGroupStageTeams(randomize: randomize)
groupStages.forEach { $0.buildMatches() }
}
}
let max = groupStages.map { $0.size }.reduce(0,+) func setGroupStageTeams(randomize: Bool) {
var chunks = selectedSortedTeams().suffix(max).chunked(into: groupStageCount) let groupStages = groupStages()
for (index, _) in chunks.enumerated() { let max = groupStages.map { $0.size }.reduce(0,+)
if randomize { var chunks = selectedSortedTeams().suffix(max).chunked(into: groupStageCount)
chunks[index].shuffle() for (index, _) in chunks.enumerated() {
} else if index % 2 != 0 { if randomize {
chunks[index].reverse() chunks[index].shuffle()
} } else if index % 2 != 0 {
chunks[index].reverse()
print("Equipes \(chunks[index].map { $0.weight })")
for (jIndex, _) in chunks[index].enumerated() {
print("Position \(index + 1) Poule \(groupStages[jIndex].index)")
chunks[index][jIndex].groupStage = groupStages[jIndex].id
chunks[index][jIndex].groupStagePosition = index
}
} }
do { print("Equipes \(chunks[index].map { $0.weight })")
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) for (jIndex, _) in chunks[index].enumerated() {
} catch { print("Position \(index + 1) Poule \(groupStages[jIndex].index)")
Logger.error(error) chunks[index][jIndex].groupStage = groupStages[jIndex].id
chunks[index][jIndex].groupStagePosition = index
} }
groupStages.forEach { $0.buildMatches() }
} }
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
}
func isFree() -> Bool { func isFree() -> Bool {
return entryFee == nil || entryFee == 0 return entryFee == nil || entryFee == 0

@ -107,9 +107,16 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
} }
.onAppear { .onAppear {
if let selectedDestination { if let selectedDestination {
proxy.scrollTo(selectedDestination.id) proxy.scrollTo(selectedDestination.id, anchor: .trailing)
} else { } else {
proxy.scrollTo("settings") proxy.scrollTo("settings", anchor: .leading)
}
}
.onChange(of: selectedDestination) {
if let selectedDestination {
proxy.scrollTo(selectedDestination.id, anchor: .trailing)
} else {
proxy.scrollTo("settings", anchor: .leading)
} }
} }
} }

@ -250,8 +250,7 @@ struct ActivityView: View {
Section { Section {
ForEach(getRunningTournaments()) { tournament in ForEach(getRunningTournaments()) { tournament in
NavigationLink { NavigationLink {
InscriptionManagerView(tournament: tournament, pasteString: pasteString) AddTeamView(tournament: tournament, pasteString: pasteString, editedTeam: nil)
.environment(tournament)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(tournament.tournamentTitle()) Text(tournament.tournamentTitle())

@ -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)
}
}
}

@ -40,14 +40,10 @@ struct InscriptionManagerView: View {
@State private var presentPlayerCreation: Bool = false @State private var presentPlayerCreation: Bool = false
@State private var presentImportView: Bool = false @State private var presentImportView: Bool = false
@State private var isLearningMore: Bool = false @State private var isLearningMore: Bool = false
@State private var createdPlayers: Set<PlayerRegistration> = Set()
@State private var createdPlayerIds: Set<String> = Set()
@State private var editedTeam: TeamRegistration? @State private var editedTeam: TeamRegistration?
@State private var pasteString: String?
@State private var currentRankSourceDate: Date? @State private var currentRankSourceDate: Date?
@State private var confirmUpdateRank = false @State private var confirmUpdateRank = false
@State private var selectionSearchField: String? @State private var selectionSearchField: String?
@State private var autoSelect: Bool = false
@State private var teamsHash: Int? @State private var teamsHash: Int?
@State private var presentationCount: Int = 0 @State private var presentationCount: Int = 0
@State private var filterMode: FilterMode = .all @State private var filterMode: FilterMode = .all
@ -63,7 +59,7 @@ struct InscriptionManagerView: View {
@State private var unsortedPlayers: [PlayerRegistration] = [] @State private var unsortedPlayers: [PlayerRegistration] = []
@State private var teamPaste: URL? @State private var teamPaste: URL?
@State private var confirmDuplicate: Bool = false @State private var confirmDuplicate: Bool = false
@State private var presentAddTeamView: Bool = false
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
} }
@ -100,26 +96,20 @@ struct InscriptionManagerView: View {
case walkOut case walkOut
case waiting case waiting
func localizedLabel() -> String { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self { switch self {
case .all: case .all:
return "Toutes les équipes" return displayStyle == .wide ? "Équipes inscrites / souhaitées" : "Équipes inscrites"
case .walkOut: case .walkOut:
return "Voir les WOs" return "Forfaits"
case .waiting: case .waiting:
return "Liste d'attente" return "Liste d'attente"
} }
} }
} }
init(tournament: Tournament, pasteString: String? = nil) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
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
}
_currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate)
} }
@ -184,13 +174,29 @@ struct InscriptionManagerView: View {
} }
var body: some View { var body: some View {
VStack(spacing: 0) { Group {
if _isEditingTeam() { if tournament.unsortedTeams().isEmpty == false {
_buildingTeamView()
} else if sortedTeams.isEmpty {
_inscriptionTipsView()
} else {
_teamRegisteredView() _teamRegisteredView()
} else {
List {
}
.overlay {
ContentUnavailableView {
Label("Aucune équipe", systemImage: "person.2.slash")
} description: {
Text("Vous n'avez aucune équipe dans votre liste. Complétez là ou importer un fichier.")
} actions: {
RowButtonView("Ajouter une équipe") {
presentAddTeamView = true
}
RowButtonView("Importer un fichier") {
presentImportView = true
}
}
.padding()
}
} }
} }
.onAppear { .onAppear {
@ -202,21 +208,6 @@ struct InscriptionManagerView: View {
} }
} }
.alert("Cette équipe existe déjà", isPresented: $confirmDuplicate) {
Button("Créer l'équipe quand même") {
_createTeam(checkDuplicates: false)
}
Button("Annuler", role: .cancel) {
pasteString = nil
editedTeam = nil
createdPlayers.removeAll()
createdPlayerIds.removeAll()
}
} message: {
Text("Cette équipe existe déjà dans votre liste d'inscription.")
}
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") { Button("OK") {
} }
@ -272,39 +263,10 @@ struct InscriptionManagerView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $isLearningMore) { .sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament) LearnMoreSheetView(tournament: tournament)
.tint(.master) .tint(.master)
} }
.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)
}
.sheet(isPresented: $presentImportView, onDismiss: { .sheet(isPresented: $presentImportView, onDismiss: {
_getTeams() _getTeams()
}) { }) {
@ -334,6 +296,15 @@ struct InscriptionManagerView: View {
UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament) UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament)
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $presentAddTeamView, onDismiss: {
editedTeam = nil
_getTeams()
}) {
NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam)
}
.tint(.master)
}
.onChange(of: filterMode) { .onChange(of: filterMode) {
_prepareTeams() _prepareTeams()
} }
@ -344,109 +315,83 @@ struct InscriptionManagerView: View {
_prepareTeams() _prepareTeams()
} }
.toolbar { .toolbar {
if _isEditingTeam() { ToolbarItemGroup(placement: .navigationBarTrailing) {
ToolbarItem(placement: .cancellationAction) { Menu {
Button("Annuler", role: .cancel) { Picker(selection: $filterMode) {
pasteString = nil ForEach(FilterMode.allCases) {
editedTeam = nil Text($0.localizedLabel(.short)).tag($0)
createdPlayers.removeAll()
createdPlayerIds.removeAll()
if cancelShouldDismiss {
dismiss()
} }
} label: {
} }
} Picker(selection: $sortingMode) {
} else { ForEach(SortingMode.allCases) {
ToolbarItemGroup(placement: .navigationBarTrailing) { Text($0.localizedLabel()).tag($0)
Menu {
Picker(selection: $filterMode) {
ForEach(FilterMode.allCases) {
Text($0.localizedLabel()).tag($0)
}
} label: {
} }
Picker(selection: $sortingMode) { } label: {
ForEach(SortingMode.allCases) { }
Text($0.localizedLabel()).tag($0)
} Picker(selection: $byDecreasingOrdering) {
Text("Croissant").tag(false)
Text("Décroissant").tag(true)
} label: {
}
} label: {
LabelFilter()
}
Menu {
if tournament.inscriptionClosed() == false {
Menu {
_sortingTypePickerView()
} label: { } label: {
Text("Méthode de sélection")
Text(tournament.teamSorting.localizedLabel())
} }
Divider()
rankingDateSourcePickerView(showDateInLabel: true)
Picker(selection: $byDecreasingOrdering) { Divider()
Text("Croissant").tag(false) Button {
Text("Décroissant").tag(true) tournament.lockRegistration()
_save()
} label: { } label: {
Label("Clôturer", systemImage: "lock")
} }
} label: { Divider()
LabelFilter() if let teamPaste {
} ShareLink(item: teamPaste) {
Menu { Label("Exporter les paires", systemImage: "square.and.arrow.up")
if tournament.inscriptionClosed() == false {
Menu {
_sortingTypePickerView()
} label: {
Text("Méthode de sélection")
Text(tournament.teamSorting.localizedLabel())
}
Divider()
rankingDateSourcePickerView(showDateInLabel: true)
if tournament.teamSorting == .inscriptionDate {
Divider()
//_prioritizeClubMembersButton()
Button("Bloquer une place") {
_createTeam(checkDuplicates: false)
}
}
Divider()
Button {
tournament.lockRegistration()
_save()
} label: {
Label("Clôturer", systemImage: "lock")
}
Divider()
if let teamPaste {
ShareLink(item: teamPaste) {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
}
}
Button {
presentImportView = true
} label: {
Label("Importer beach-padel", systemImage: "square.and.arrow.down")
}
Link(destination: URLs.beachPadel.url) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
}
} else {
Button {
tournament.closedRegistrationDate = nil
_save()
} label: {
Label("Ré-ouvrir", systemImage: "lock.open")
} }
} }
} label: { Button {
if tournament.inscriptionClosed() == false { presentImportView = true
LabelOptions() } label: {
} else { Label("Importer beach-padel", systemImage: "square.and.arrow.down")
Label("Clôturer", systemImage: "lock") }
Link(destination: URLs.beachPadel.url) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
} }
} else {
Button {
tournament.closedRegistrationDate = nil
_save()
} label: {
Label("Ré-ouvrir", systemImage: "lock.open")
}
}
} label: {
if tournament.inscriptionClosed() == false {
LabelOptions()
} else {
Label("Clôturer", systemImage: "lock")
} }
} }
} }
} }
.navigationBarBackButtonHidden(_isEditingTeam())
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscriptions") .navigationTitle("Inscriptions")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
private func _isEditingTeam() -> Bool {
createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil
}
private func _prepareStats() async { private func _prepareStats() async {
#if DEBUG_TIME //DEBUGING TIME #if DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
@ -517,6 +462,8 @@ struct InscriptionManagerView: View {
private func _teamRegisteredView() -> some View { private func _teamRegisteredView() -> some View {
List { List {
_informationView()
let selectedSortedTeams = tournament.selectedSortedTeams() let selectedSortedTeams = tournament.selectedSortedTeams()
if let closedRegistrationDate = tournament.closedRegistrationDate { if let closedRegistrationDate = tournament.closedRegistrationDate {
Section { Section {
@ -535,7 +482,11 @@ struct InscriptionManagerView: View {
if presentSearch == false { if presentSearch == false {
_rankHandlerView() _rankHandlerView()
_relatedTips() _relatedTips()
_informationView() Section {
RowButtonView("Compléter la liste") {
presentAddTeamView = true
}
}
} }
let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) })
@ -552,13 +503,13 @@ struct InscriptionManagerView: View {
} }
RowButtonView("Créer une équipe") { RowButtonView("Créer une équipe") {
Task { // Task {
await MainActor.run { // await MainActor.run {
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) // fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] // fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = searchField // pasteString = searchField
} // }
} // }
} }
RowButtonView("D'accord") { RowButtonView("D'accord") {
@ -567,7 +518,6 @@ struct InscriptionManagerView: View {
} }
} }
} }
ForEach(teams) { team in ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams) let teamIndex = team.index(in: sortedTeams)
Section { Section {
@ -585,34 +535,6 @@ struct InscriptionManagerView: View {
.autocorrectionDisabled() .autocorrectionDisabled()
} }
@ViewBuilder
private func _managementView() -> some View {
Button {
presentPlayerSearch = true
} label: {
Text("Rechercher dans la base fédérale")
}
Button {
presentPlayerCreation = true
} label: {
Text("Créer un non classé / non licencié")
}
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
}
}
}
}
@ViewBuilder @ViewBuilder
func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View { func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View {
Section { Section {
@ -656,72 +578,6 @@ struct InscriptionManagerView: View {
return tournament.tournamentCategory.playerFilterOption return tournament.tournamentCategory.playerFilterOption
} }
@ViewBuilder
private func _inscriptionTipsView() -> some View {
List {
if let closedRegistrationDate = tournament.closedRegistrationDate {
Section {
CloseDatePicker(closedRegistrationDate: closedRegistrationDate)
} footer: {
Text("Toutes les équipes ayant été inscrites après la date de clôture seront en liste d'attente.")
}
}
_informationView()
Section {
TipView(fileTip) { action in
if action.id == "website" {
UIApplication.shared.open(URLs.beachPadel.url)
} else if action.id == "add-team-file" {
presentImportView = true
}
}
.tipStyle(tint: nil)
}
Section {
TipView(pasteTip) { action in
if let paste = UIPasteboard.general.string {
Task {
await MainActor.run {
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: paste, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = paste
autoSelect = true
}
}
}
}
.tipStyle(tint: nil)
}
Section {
TipView(searchTip) { action in
presentPlayerSearch = true
}
.tipStyle(tint: nil)
}
Section {
TipView(createTip) { action in
presentPlayerCreation = true
}
.tipStyle(tint: nil)
}
Section {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Vous n'avez encore aucune équipe inscrite dans votre tournoi."))
}
_rankHandlerView()
}
}
@ViewBuilder @ViewBuilder
private func _rankHandlerView() -> some View { private func _rankHandlerView() -> some View {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false { if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false {
@ -737,36 +593,29 @@ struct InscriptionManagerView: View {
} }
} }
private func _teamCountForFilterMode(filterMode: FilterMode) -> String {
switch filterMode {
case .all:
return unsortedTeamsWithoutWO.count.formatted() + " / " + tournament.teamCount.formatted()
case .walkOut:
let wo = walkoutTeams.count.formatted()
return wo
case .waiting:
let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
return waiting.formatted()
}
}
@ViewBuilder @ViewBuilder
private func _informationView() -> some View { private func _informationView() -> some View {
Section { Section {
Button { ForEach(FilterMode.allCases) { filterMode in
filterMode = .all
} label: {
LabeledContent { LabeledContent {
Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()) Text(_teamCountForFilterMode(filterMode: filterMode))
} label: { } label: {
Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)") Text(filterMode.localizedLabel())
} }
} }
.buttonStyle(.plain)
HStack {
let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
FooterButtonView("\(waiting.formatted()) équipes en attente\(waiting.pluralSuffix)") {
filterMode = .waiting
}
.disabled(filterMode == .waiting)
Divider()
let wo = walkoutTeams.count
FooterButtonView("\(wo.formatted()) équipes forfait\(wo.pluralSuffix)") {
filterMode = .walkOut
}
.disabled(filterMode == .walkOut)
}
.fixedSize(horizontal: true, vertical: true)
NavigationLink { NavigationLink {
InscriptionInfoView() InscriptionInfoView()
@ -784,27 +633,77 @@ struct InscriptionManagerView: View {
} }
} header: { } header: {
Text("Statut des inscriptions") Text("Statut des inscriptions")
} footer: {
HStack {
Menu {
_managementView()
} label: {
Text("Complétez votre liste")
}
Text("ou")
FooterButtonView("Importez un fichier") {
presentImportView = true
}
}
// if filterMode != .all {
// FooterButtonView("tout afficher") {
// filterMode = .all
// }
// }
} }
.headerProminence(.increased)
} }
//
// @ViewBuilder
// private func _informationView() -> some View {
// Section {
// Button {
// filterMode = .all
// } label: {
// LabeledContent {
// Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted())
// } label: {
// Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)")
// }
// }
// .buttonStyle(.plain)
//
// HStack {
// let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
// FooterButtonView("\(waiting.formatted()) équipes en attente\(waiting.pluralSuffix)") {
// filterMode = .waiting
// }
// .disabled(filterMode == .waiting)
//
// Divider()
//
// let wo = walkoutTeams.count
// FooterButtonView("\(wo.formatted()) équipes forfait\(wo.pluralSuffix)") {
// filterMode = .walkOut
// }
// .disabled(filterMode == .walkOut)
// }
// .fixedSize(horizontal: true, vertical: true)
//
// NavigationLink {
// InscriptionInfoView()
// .environment(tournament)
// } label: {
// LabeledContent {
// if let registrationIssues {
// Text(registrationIssues.formatted())
// } else {
// ProgressView()
// }
// } label: {
// Text("Problèmes détéctés")
// }
// }
// } header: {
// Text("Statut des inscriptions")
// } footer: {
// HStack {
// Menu {
// _managementView()
// } label: {
// Text("Complétez votre liste")
// }
// Text("ou")
//
// FooterButtonView("Importez un fichier") {
// presentImportView = true
// }
// }
//// if filterMode != .all {
//// FooterButtonView("tout afficher") {
//// filterMode = .all
//// }
//// }
// }
// .headerProminence(.increased)
// }
@ViewBuilder @ViewBuilder
private func _relatedTips() -> some View { private func _relatedTips() -> some View {
@ -848,278 +747,7 @@ struct InscriptionManagerView: View {
} }
private func _searchSource() -> String? { private func _searchSource() -> String? {
selectionSearchField ?? pasteString selectionSearchField
}
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 sortedTeams.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
_clearScreen()
_getTeams()
}
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
_clearScreen()
_getTeams()
}
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))
}
}
}
}
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)
}
}
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)
}
}
}
}
.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
} }
@ViewBuilder @ViewBuilder
@ -1214,10 +842,7 @@ struct InscriptionManagerView: View {
//Divider() //Divider()
Button("Changer les joueurs") { Button("Changer les joueurs") {
editedTeam = team editedTeam = team
team.unsortedPlayers().forEach { player in presentAddTeamView = true
createdPlayers.insert(player)
createdPlayerIds.insert(player.id)
}
} }
Divider() Divider()
NavigationLink { NavigationLink {

Loading…
Cancel
Save