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.
1268 lines
48 KiB
1268 lines
48 KiB
//
|
|
// InscriptionManagerView.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Razmig Sarkissian on 29/02/2024.
|
|
//
|
|
|
|
import SwiftUI
|
|
import TipKit
|
|
import LeStorage
|
|
|
|
let slideToDeleteTip = SlideToDeleteTip()
|
|
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip()
|
|
let fileTip = InscriptionManagerFileInputTip()
|
|
let pasteTip = InscriptionManagerPasteInputTip()
|
|
let searchTip = InscriptionManagerSearchInputTip()
|
|
let createTip = InscriptionManagerCreateInputTip()
|
|
let rankUpdateTip = InscriptionManagerRankUpdateTip()
|
|
let padelBeachExportTip = PadelBeachExportTip()
|
|
let padelBeachImportTip = PadelBeachImportTip()
|
|
|
|
struct InscriptionManagerView: View {
|
|
|
|
@EnvironmentObject var dataStore: DataStore
|
|
|
|
@EnvironmentObject var networkMonitor: NetworkMonitor
|
|
@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 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 pasteString: String?
|
|
@State private var currentRankSourceDate: Date?
|
|
@State private var confirmUpdateRank = false
|
|
@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 filterMode: FilterMode = .all
|
|
@State private var sortingMode: SortingMode = .teamWeight
|
|
@State private var byDecreasingOrdering: Bool = false
|
|
@State private var contactType: ContactType? = nil
|
|
@State private var sentError: ContactManagerError? = nil
|
|
@State private var showSubscriptionView: Bool = false
|
|
@State private var registrationIssues: Int? = nil
|
|
@State private var sortedTeams: [TeamRegistration] = []
|
|
@State private var walkoutTeams: [TeamRegistration] = []
|
|
@State private var unsortedTeamsWithoutWO: [TeamRegistration] = []
|
|
@State private var unsortedPlayers: [PlayerRegistration] = []
|
|
@State private var teamPaste: URL?
|
|
@State private var confirmDuplicate: Bool = false
|
|
|
|
var tournamentStore: TournamentStore {
|
|
return self.tournament.tournamentStore
|
|
}
|
|
|
|
var messageSentFailed: Binding<Bool> {
|
|
Binding {
|
|
sentError != nil
|
|
} set: { newValue in
|
|
if newValue == false {
|
|
sentError = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
enum SortingMode: Int, Identifiable, CaseIterable {
|
|
var id: Int { self.rawValue }
|
|
case registrationDate
|
|
case teamWeight
|
|
|
|
func localizedLabel() -> String {
|
|
switch self {
|
|
case .registrationDate:
|
|
return "Date d'inscription"
|
|
case .teamWeight:
|
|
return "Poids d'équipe"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum FilterMode: Int, Identifiable, CaseIterable {
|
|
var id: Int { self.rawValue }
|
|
case all
|
|
case walkOut
|
|
|
|
func localizedLabel() -> String {
|
|
switch self {
|
|
case .all:
|
|
return "Toutes les équipes"
|
|
case .walkOut:
|
|
return "Voir les WOs"
|
|
}
|
|
}
|
|
}
|
|
|
|
init(tournament: Tournament, pasteString: String? = nil) {
|
|
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)
|
|
}
|
|
|
|
private func _clearScreen() {
|
|
teamPaste = nil
|
|
unsortedPlayers.removeAll()
|
|
walkoutTeams.removeAll()
|
|
unsortedTeamsWithoutWO.removeAll()
|
|
sortedTeams.removeAll()
|
|
registrationIssues = nil
|
|
}
|
|
|
|
// 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)
|
|
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 {
|
|
VStack(spacing: 0) {
|
|
_managementView()
|
|
if _isEditingTeam() {
|
|
_buildingTeamView()
|
|
} else if sortedTeams.isEmpty {
|
|
_inscriptionTipsView()
|
|
} else {
|
|
_teamRegisteredView()
|
|
}
|
|
}
|
|
.onAppear {
|
|
_getTeams()
|
|
}
|
|
.onDisappear {
|
|
Task {
|
|
await _handleHashDiff()
|
|
|
|
}
|
|
}
|
|
.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) {
|
|
Button("OK") {
|
|
}
|
|
} message: {
|
|
Text(_getErrorMessage())
|
|
}
|
|
.sheet(item: $contactType) { contactType in
|
|
Group {
|
|
switch contactType {
|
|
case .message(_, let recipients, let body, _):
|
|
if Guard.main.paymentForNewTournament() != nil {
|
|
MessageComposeView(recipients: recipients, body: body) { result in
|
|
switch result {
|
|
case .cancelled:
|
|
break
|
|
case .failed:
|
|
self.sentError = .messageFailed
|
|
case .sent:
|
|
if networkMonitor.connected == false {
|
|
self.sentError = .messageNotSent
|
|
}
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
|
|
}
|
|
case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
|
|
if Guard.main.paymentForNewTournament() != nil {
|
|
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
|
|
switch result {
|
|
case .cancelled, .saved:
|
|
self.contactType = nil
|
|
case .failed:
|
|
self.contactType = nil
|
|
self.sentError = .mailFailed
|
|
case .sent:
|
|
if networkMonitor.connected == false {
|
|
self.contactType = nil
|
|
self.sentError = .mailNotSent
|
|
}
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
|
|
}
|
|
}
|
|
}
|
|
.tint(.master)
|
|
}
|
|
|
|
.sheet(isPresented: $isLearningMore) {
|
|
LearnMoreSheetView(tournament: tournament)
|
|
.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: {
|
|
_getTeams()
|
|
}) {
|
|
NavigationStack {
|
|
FileImportView()
|
|
}
|
|
.tint(.master)
|
|
}
|
|
.onChange(of: tournament.prioritizeClubMembers) {
|
|
_clearScreen()
|
|
_save()
|
|
_getTeams()
|
|
}
|
|
.onChange(of: tournament.teamSorting) {
|
|
_clearScreen()
|
|
_save()
|
|
_getTeams()
|
|
}
|
|
.onChange(of: currentRankSourceDate) {
|
|
if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate {
|
|
confirmUpdateRank = true
|
|
}
|
|
}
|
|
.sheet(isPresented: $confirmUpdateRank, onDismiss: {
|
|
currentRankSourceDate = tournament.rankSourceDate
|
|
}) {
|
|
UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament)
|
|
.tint(.master)
|
|
}
|
|
.onChange(of: filterMode) {
|
|
_prepareTeams()
|
|
}
|
|
.onChange(of: sortingMode) {
|
|
_prepareTeams()
|
|
}
|
|
.onChange(of: byDecreasingOrdering) {
|
|
_prepareTeams()
|
|
}
|
|
.toolbar {
|
|
if _isEditingTeam() {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Annuler", role: .cancel) {
|
|
pasteString = nil
|
|
editedTeam = nil
|
|
createdPlayers.removeAll()
|
|
createdPlayerIds.removeAll()
|
|
if cancelShouldDismiss {
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
|
Menu {
|
|
Picker(selection: $filterMode) {
|
|
ForEach(FilterMode.allCases) {
|
|
Text($0.localizedLabel()).tag($0)
|
|
}
|
|
} label: {
|
|
}
|
|
Picker(selection: $sortingMode) {
|
|
ForEach(SortingMode.allCases) {
|
|
Text($0.localizedLabel()).tag($0)
|
|
}
|
|
} label: {
|
|
}
|
|
|
|
Picker(selection: $byDecreasingOrdering) {
|
|
Text("Croissant").tag(false)
|
|
Text("Décroissant").tag(true)
|
|
} label: {
|
|
}
|
|
} label: {
|
|
LabelFilter()
|
|
}
|
|
Menu {
|
|
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: {
|
|
if tournament.inscriptionClosed() == false {
|
|
LabelOptions()
|
|
} else {
|
|
Label("Clôturer", systemImage: "lock")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationBarBackButtonHidden(_isEditingTeam())
|
|
.toolbarBackground(.visible, for: .navigationBar)
|
|
.navigationTitle("Inscriptions")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
private func _isEditingTeam() -> Bool {
|
|
createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil
|
|
}
|
|
|
|
private func _prepareStats() async {
|
|
#if DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func _prepareStats", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
|
|
unsortedPlayers = tournament.unsortedPlayers()
|
|
walkoutTeams = tournament.walkoutTeams()
|
|
unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO()
|
|
teamPaste = tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short))
|
|
}
|
|
|
|
private func _prepareTeams() {
|
|
#if DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func _prepareTeams", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
sortedTeams = tournament.sortedTeams()
|
|
}
|
|
|
|
var filteredTeams: [TeamRegistration] {
|
|
|
|
var teams = sortedTeams
|
|
if filterMode == .walkOut {
|
|
teams = teams.filter({ $0.walkOut })
|
|
}
|
|
|
|
if sortingMode == .registrationDate {
|
|
teams = teams.sorted(by: \.computedRegistrationDate)
|
|
}
|
|
|
|
if byDecreasingOrdering {
|
|
return teams.reversed()
|
|
} else {
|
|
return teams
|
|
}
|
|
}
|
|
|
|
private func _getIssues() async {
|
|
#if DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func _getIssues", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
await registrationIssues = tournament.registrationIssues()
|
|
}
|
|
|
|
private func _getTeams() {
|
|
_prepareTeams()
|
|
Task {
|
|
await _prepareStats()
|
|
await _getIssues()
|
|
await _setHash()
|
|
}
|
|
}
|
|
|
|
private func _teamRegisteredView() -> some View {
|
|
List {
|
|
if presentSearch == false {
|
|
_rankHandlerView()
|
|
_relatedTips()
|
|
_informationView()
|
|
}
|
|
|
|
let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) })
|
|
|
|
if teams.isEmpty && searchField.isEmpty == false {
|
|
ContentUnavailableView {
|
|
Label("Aucun résultat", systemImage: "person.2.slash")
|
|
} description: {
|
|
Text("\(searchField) est introuvable dans les équipes inscrites.")
|
|
} actions: {
|
|
RowButtonView("Modifier la recherche") {
|
|
searchField = ""
|
|
presentSearch = true
|
|
}
|
|
|
|
RowButtonView("Créer une équipe") {
|
|
Task {
|
|
await MainActor.run() {
|
|
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
|
|
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
|
|
pasteString = searchField
|
|
}
|
|
}
|
|
}
|
|
|
|
RowButtonView("D'accord") {
|
|
searchField = ""
|
|
presentSearch = false
|
|
}
|
|
}
|
|
}
|
|
|
|
ForEach(teams) { team in
|
|
let teamIndex = team.index(in: sortedTeams)
|
|
Section {
|
|
TeamDetailView(team: team)
|
|
} header: {
|
|
TeamHeaderView(team: team, teamIndex: teamIndex, tournament: tournament)
|
|
} footer: {
|
|
_teamFooterView(team)
|
|
}
|
|
.headerProminence(.increased)
|
|
}
|
|
}
|
|
.searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites"))
|
|
.keyboardType(.alphabet)
|
|
.autocorrectionDisabled()
|
|
}
|
|
|
|
@MainActor
|
|
private func _managementView() -> some View {
|
|
HStack {
|
|
Button {
|
|
presentPlayerCreation = true
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "person.fill.badge.plus")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 20)
|
|
Text("Créer")
|
|
.font(.headline)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
Button {
|
|
presentPlayerSearch = true
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "person.fill.viewfinder")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 20)
|
|
Text("FFT")
|
|
.font(.headline)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.logoBackground)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.padding(16)
|
|
}
|
|
|
|
|
|
@ViewBuilder
|
|
func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View {
|
|
Section {
|
|
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
|
|
|
|
Picker(selection: $currentRankSourceDate) {
|
|
if currentRankSourceDate == nil {
|
|
Text("inconnu").tag(nil as Date?)
|
|
}
|
|
ForEach(dates, id: \.self) { date in
|
|
Text(date.monthYearFormatted).tag(date as Date?)
|
|
}
|
|
} label: {
|
|
Text("Classement utilisé")
|
|
if showDateInLabel {
|
|
if let currentRankSourceDate {
|
|
Text(currentRankSourceDate.monthYearFormatted)
|
|
} else {
|
|
Text("Choisir le mois")
|
|
}
|
|
}
|
|
}
|
|
.pickerStyle(.menu)
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _inscriptionTipsView() -> some View {
|
|
List {
|
|
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 dans votre liste d'attente."))
|
|
}
|
|
|
|
_rankHandlerView()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _rankHandlerView() -> some View {
|
|
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false {
|
|
Section {
|
|
TipView(rankUpdateTip) { action in
|
|
self.currentRankSourceDate = mostRecentDate
|
|
}
|
|
.tipStyle(tint: nil)
|
|
}
|
|
rankingDateSourcePickerView(showDateInLabel: false)
|
|
} else if tournament.rankSourceDate == nil {
|
|
rankingDateSourcePickerView(showDateInLabel: false)
|
|
}
|
|
}
|
|
|
|
private func _informationView() -> some View {
|
|
Section {
|
|
LabeledContent {
|
|
Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle)
|
|
} label: {
|
|
Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)")
|
|
}
|
|
|
|
LabeledContent {
|
|
Text(walkoutTeams.count.formatted()).font(.largeTitle)
|
|
} label: {
|
|
Text("Forfait\(walkoutTeams.count.pluralSuffix)")
|
|
}
|
|
|
|
LabeledContent {
|
|
Text(max(0, unsortedTeamsWithoutWO.count - tournament.teamCount).formatted()).font(.largeTitle)
|
|
} label: {
|
|
Text("Attente")
|
|
}
|
|
|
|
NavigationLink {
|
|
InscriptionInfoView()
|
|
.environment(tournament)
|
|
} label: {
|
|
LabeledContent {
|
|
if let registrationIssues {
|
|
Text(registrationIssues.formatted()).font(.largeTitle)
|
|
} else {
|
|
ProgressView()
|
|
}
|
|
} label: {
|
|
Text("Problèmes détéctés")
|
|
if let closedRegistrationDate = tournament.closedRegistrationDate {
|
|
Text("clôturé le " + closedRegistrationDate.formatted())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _relatedTips() -> some View {
|
|
// if pasteString == nil
|
|
// && createdPlayerIds.isEmpty
|
|
// && tournament.unsortedTeams().count >= tournament.teamCount
|
|
// && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty {
|
|
// Section {
|
|
// TipView(padelBeachExportTip) { action in
|
|
// if action.id == "more-info-export" {
|
|
// isLearningMore = true
|
|
// }
|
|
// if action.id == "padel-beach" {
|
|
// UIApplication.shared.open(URLs.beachPadel.url)
|
|
// }
|
|
// }
|
|
// .tipStyle(tint: nil)
|
|
// }
|
|
// Section {
|
|
// TipView(padelBeachImportTip) { action in
|
|
// if action.id == "more-info-import" {
|
|
// presentImportView = true
|
|
// }
|
|
// }
|
|
// .tipStyle(tint: nil)
|
|
// }
|
|
// }
|
|
//
|
|
|
|
if tournament.tournamentCategory == .men && unsortedPlayers.filter({ $0.isMalePlayer() == false }).isEmpty == false {
|
|
Section {
|
|
TipView(inscriptionManagerWomanRankTip)
|
|
.tipStyle(tint: nil)
|
|
}
|
|
}
|
|
//
|
|
// Section {
|
|
// TipView(slideToDeleteTip)
|
|
// .tipStyle(tint: nil)
|
|
// }
|
|
}
|
|
|
|
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 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!)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
private func _sortingTypePickerView() -> some View {
|
|
@Bindable var tournament = tournament
|
|
Picker(selection: $tournament.teamSorting) {
|
|
ForEach(TeamSortingType.allCases) {
|
|
Text($0.localizedLabel()).tag($0)
|
|
}
|
|
} label: {
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _prioritizeClubMembersButton() -> some View {
|
|
@Bindable var tournament = tournament
|
|
if let federalClub = tournament.club() {
|
|
Menu {
|
|
Picker(selection: $tournament.prioritizeClubMembers) {
|
|
Text("Oui").tag(true)
|
|
Text("Non").tag(false)
|
|
} label: {
|
|
|
|
}
|
|
.labelsHidden()
|
|
|
|
Divider()
|
|
NavigationLink {
|
|
ClubsView() { club in
|
|
if let event = tournament.eventObject() {
|
|
event.club = club.id
|
|
do {
|
|
try dataStore.events.addOrUpdate(instance: event)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
_save()
|
|
}
|
|
} label: {
|
|
Text("Changer de club")
|
|
}
|
|
} label: {
|
|
Text("Membres prioritaires")
|
|
Text(federalClub.acronym)
|
|
}
|
|
Divider()
|
|
} else {
|
|
NavigationLink {
|
|
ClubsView() { club in
|
|
if let event = tournament.eventObject() {
|
|
event.club = club.id
|
|
do {
|
|
try dataStore.events.addOrUpdate(instance: event)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
_save()
|
|
}
|
|
} label: {
|
|
Text("Identifier le club")
|
|
}
|
|
Divider()
|
|
}
|
|
}
|
|
|
|
private func _teamFooterView(_ team: TeamRegistration) -> some View {
|
|
HStack {
|
|
if let formattedRegistrationDate = team.formattedInscriptionDate() {
|
|
Text(formattedRegistrationDate)
|
|
}
|
|
Spacer()
|
|
_teamMenuOptionView(team)
|
|
}
|
|
}
|
|
private func _teamMenuOptionView(_ team: TeamRegistration) -> some View {
|
|
Menu {
|
|
Section {
|
|
MenuWarningView(tournament: team.tournamentObject()!, teams: [team], contactType: $contactType)
|
|
//Divider()
|
|
Button("Copier") {
|
|
let pasteboard = UIPasteboard.general
|
|
pasteboard.string = team.playersPasteData()
|
|
}
|
|
//Divider()
|
|
Button("Changer les joueurs") {
|
|
editedTeam = team
|
|
team.unsortedPlayers().forEach { player in
|
|
createdPlayers.insert(player)
|
|
createdPlayerIds.insert(player.id)
|
|
}
|
|
}
|
|
Divider()
|
|
NavigationLink {
|
|
EditingTeamView(team: team)
|
|
.environment(tournament)
|
|
} label: {
|
|
Text("Éditer une donnée de l'équipe")
|
|
}
|
|
Divider()
|
|
Toggle(isOn: .init(get: {
|
|
return team.wildCardBracket
|
|
}, set: { value in
|
|
_clearScreen()
|
|
|
|
Task {
|
|
team.resetPositions()
|
|
team.wildCardGroupStage = false
|
|
team.walkOut = false
|
|
team.wildCardBracket = value
|
|
do {
|
|
try tournamentStore.teamRegistrations.addOrUpdate(instance: team)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
_getTeams()
|
|
}
|
|
})) {
|
|
Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle")
|
|
}
|
|
|
|
Toggle(isOn: .init(get: {
|
|
return team.wildCardGroupStage
|
|
}, set: { value in
|
|
_clearScreen()
|
|
|
|
Task {
|
|
team.resetPositions()
|
|
team.wildCardBracket = false
|
|
team.walkOut = false
|
|
team.wildCardGroupStage = value
|
|
do {
|
|
try tournamentStore.teamRegistrations.addOrUpdate(instance: team)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
_getTeams()
|
|
}
|
|
})) {
|
|
Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle")
|
|
}
|
|
|
|
Divider()
|
|
Toggle(isOn: .init(get: {
|
|
return team.walkOut
|
|
}, set: { value in
|
|
_clearScreen()
|
|
Task {
|
|
team.resetPositions()
|
|
team.wildCardBracket = false
|
|
team.wildCardGroupStage = false
|
|
team.walkOut = value
|
|
do {
|
|
try tournamentStore.teamRegistrations.addOrUpdate(instance: team)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
_getTeams()
|
|
}
|
|
})) {
|
|
Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle")
|
|
}
|
|
Divider()
|
|
Button(role: .destructive) {
|
|
_clearScreen()
|
|
Task {
|
|
do {
|
|
try tournamentStore.teamRegistrations.delete(instance: team)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
_getTeams()
|
|
}
|
|
} label: {
|
|
LabelDelete()
|
|
}
|
|
|
|
// } header: {
|
|
// Text(team.teamLabel(.short))
|
|
}
|
|
} label: {
|
|
LabelOptions().labelStyle(.titleOnly)
|
|
}
|
|
}
|
|
|
|
private func _getErrorMessage() -> String {
|
|
let m1 : String? = (networkMonitor.connected == false ? "L'appareil n'est pas connecté à internet." : nil)
|
|
let m2 : String? = (sentError == .mailNotSent ? "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer." : nil)
|
|
let m3 : String? = ((sentError == .messageFailed || sentError == .messageNotSent) ? "Le SMS n'a pas été envoyé" : nil)
|
|
let m4 : String? = (sentError == .mailFailed ? "Le mail n'a pas été envoyé" : nil)
|
|
|
|
let message : String = [m1, m2, m3, m4].compacted().joined(separator: "\n")
|
|
return message
|
|
}
|
|
|
|
private func _save() {
|
|
do {
|
|
try dataStore.tournaments.addOrUpdate(instance: tournament)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
//#Preview {
|
|
// NavigationStack {
|
|
// InscriptionManagerView(tournament: Tournament.mock())
|
|
// .environment(Tournament.mock())
|
|
// }
|
|
//}
|
|
|