You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Views/Tournament/FileImportView.swift

325 lines
13 KiB

//
// FileImportView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/03/2024.
//
import SwiftUI
import TipKit
import LeStorage
struct FileImportView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss
let notFoundAreWalkOutTip = NotFoundAreWalkOutTip()
@State private var fileContent: String?
@State private var teams: [FileImportManager.TeamHolder] = []
@State private var isShowing = false
@State private var didImport = false
@State private var convertingFile = false
@State private var errorMessage: String? = nil
@State private var forceRankUpdate: Bool = false
@State private var selectedOptions: Set<TeamImportStrategy> = Set()
@State private var fileProvider: FileImportManager.FileProvider = .frenchFederation
private var filteredTeams: [FileImportManager.TeamHolder] {
return teams.filter { $0.tournamentCategory == tournament.tournamentCategory }.sorted(by: \.weight)
}
var body: some View {
List {
if teams.isEmpty {
Section {
RowButtonView("Choisir le fichier", systemImage: "square.and.arrow.down") {
convertingFile = false
isShowing.toggle()
}
}
Section {
Picker(selection: $fileProvider) {
ForEach(FileImportManager.FileProvider.allCases) {
Text($0.localizedLabel).tag($0)
}
} label: {
Text("Source du fichier")
}
RowButtonView("Démarrer l'importation") {
if let fileContent {
await _startImport(fileContent: fileContent)
}
}
.disabled(fileContent == nil)
} footer: {
if fileProvider == .frenchFederation {
let footerString = "Fichier provenant de [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))"
Text(.init(footerString))
}
}
}
// if filteredTeams.isEmpty == false && tournament.unsortedTeams().isEmpty == false {
// Section {
// ForEach(TeamImportStrategy.allCases, id: \.self) { strategy in
// LabeledContent {
// Toggle(isOn: .init(get: {
// selectedOptions.contains(strategy)
// }, set: { selected in
// if selected {
// selectedOptions.insert(strategy)
// } else {
// selectedOptions.remove(strategy)
//
// }
// })) {}
// } label: {
// Text(strategy.titleLabel())
// Text(strategy.descriptionLabel())
// }
//
// }
// } header: {
// Text("Stratégie d'importation")
// }
// }
if convertingFile {
Section {
LabeledContent {
ProgressView()
} label: {
Text("Importation en cours")
}
}
}
if let errorMessage {
Section {
Text(errorMessage)
} header: {
Text("Erreur")
}
}
// if tournament.entriesCount > 0 {
// Section {
// if tournament.inscriptionClosed {
// Label("Les inscriptions clôturées", systemImage: "lock")
// Text("Si le poids des équipes a changé, aucun déplacement entre les poules et le tableau n'est possible. Par contre, le classement sera mis à jour au sein des poules et du tableau, respectivement, en fonction de leur nouveau poids.")
// Toggle(isOn: $forceRankUpdate) {
// Text("Ne pas en tenir compte")
// }
// } else {
// Label("Les inscriptions sont ouvertes", systemImage: "lock.open")
// Text("Si le poids des équipes a changé, le classement de toutes les équipes sera mis à jour en fonction de leur nouveau poids.")
// }
// }
// }
if filteredTeams.isEmpty && teams.isEmpty == false {
@Bindable var tournament = tournament
Section {
Text("Aucune équipe \(tournament.tournamentCategory.importingRawValue) détectée mais \(teams.count) équipes sont dans le fichier")
Picker(selection: $tournament.tournamentCategory) {
ForEach(TournamentCategory.allCases) { category in
Text(category.importingRawValue).tag(category)
}
} label: {
Text("Modifier la catégorie du tournoi ?")
}
.onChange(of: tournament.tournamentCategory) {
_save()
}
}
} else if teams.isEmpty && didImport == true {
Section {
ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash")
}
} else if didImport {
let _filteredTeams = filteredTeams
let previousTeams = tournament.sortedTeams()
if previousTeams.isEmpty == false {
Section {
TipView(notFoundAreWalkOutTip)
.tipStyle(tint: nil)
}
}
Section {
LabeledContent {
Text(_filteredTeams.count.formatted())
} label: {
Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)")
}
}
ForEach(_filteredTeams) { team in
_teamView(team: team, inTeams: _filteredTeams, previousTeams: previousTeams)
}
}
}
.fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in
switch results {
case .success(let fileurls):
if let selectedFile = fileurls.first {
if selectedFile.startAccessingSecurityScopedResource() {
convertingFile = true
errorMessage = nil
teams.removeAll()
Task {
do {
if selectedFile.lastPathComponent.hasSuffix("xls") {
fileContent = try await CloudConvert.manager.uploadFile(selectedFile)
} else {
fileContent = try String(contentsOf: selectedFile)
}
selectedFile.stopAccessingSecurityScopedResource()
} catch {
errorMessage = error.localizedDescription
}
}
} else {
// Handle denied access
}
}
case .failure(let error):
errorMessage = error.localizedDescription
}
})
.onOpenURL { url in
do {
fileContent = try String(contentsOf: url)
} catch {
errorMessage = error.localizedDescription
}
}
.navigationTitle("Importation")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView {
// if false { //selectedOptions.contains(.deleteBeforeImport)
// try? dataStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams())
// }
if true { //selectedOptions.contains(.notFoundAreWalkOut)
let previousTeams = filteredTeams.compactMap({ $0.previousTeam })
let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams))
unfound.forEach { team in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = true
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: unfound)
}
tournament.importTeams(filteredTeams)
dismiss()
}
.disabled(teams.isEmpty)
}
}
}
func _startImport(fileContent: String) async {
await MainActor.run {
convertingFile = true
errorMessage = nil
teams.removeAll()
}
if let rankSourceDate = tournament.rankSourceDate, tournament.unrankValue(for: false) == nil || tournament.unrankValue(for: true) == nil {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: rankSourceDate)
}
self.teams = await FileImportManager.shared.createTeams(from: fileContent, tournament: tournament, fileProvider: fileProvider)
await MainActor.run {
convertingFile = false
didImport = true
}
}
@ViewBuilder
private func _teamView(team: FileImportManager.TeamHolder, inTeams teams: [FileImportManager.TeamHolder], previousTeams: [TeamRegistration]) -> some View {
let newIndex = team.index(in: teams)
Section {
HStack {
VStack(alignment: .leading) {
ForEach(team.players.sorted(by: \.computedRank)) {
Text($0.playerLabel())
}
}
Spacer()
HStack {
if let previousTeam = team.previousTeam {
Text(previousTeam.formattedSeed(in: previousTeams))
Image(systemName: "arrowshape.forward.fill")
}
Text(team.formattedSeedIndex(index: newIndex))
}
}
if let callDate = team.previousTeam?.callDate, let newDate = tournament.getStartDate(ofSeedIndex: newIndex), callDate != newDate {
Text("Attention, cette paire a déjà été convoquée à \(callDate.localizedDate())")
.foregroundStyle(.red)
.italic()
.font(.caption)
}
}
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
#Preview {
FileImportView()
.environment(Tournament.mock())
}
enum TeamImportStrategy: CaseIterable {
case notFoundAreWalkOut
case deleteBeforeImport
func titleLabel() -> String {
switch self {
case .notFoundAreWalkOut:
"Mettre les équipes manquantes WO"
case .deleteBeforeImport:
"Effacer avant d'importer"
}
}
func descriptionLabel() -> String {
switch self {
case .notFoundAreWalkOut:
"Si une équipe déjà présente n'est pas dans la nouvelle liste, elle sera mise à WO"
case .deleteBeforeImport:
"Supprime toutes les équipes avant d'importer"
// case .lockWeight:
// "Permets de déplacer les équipes avec leur nouveaux classements sans les déplacer entre les blocs (p500+)"
}
}
}