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/Navigation/Agenda/TournamentLookUpView.swift

488 lines
19 KiB

//
// TournamentLookUpView.swift
// PadelClub
//
// Created by razmig on 08/09/2024.
//
import SwiftUI
import CoreLocation
import CoreLocationUI
struct TournamentLookUpView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss
@State private var searchField: String = ""
@State var page: Int = 0
@State var total: Int = 0
@State private var searching: Bool = false
@State private var requestedToGetAllPages: Bool = false
@State private var revealSearchParameters: Bool = true
@State private var presentAlert: Bool = false
@State private var confirmSearch: Bool = false
var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments
}
var showLastError: Binding<Bool> {
Binding {
locationManager.lastError != nil
} set: { value in
}
}
var body: some View {
List {
searchParametersView
}
.alert(isPresented: showLastError, error: locationManager.lastError as? LocationManager.LocationError, actions: {
Button("Annuler", role: .cancel) {
}
})
.confirmationDialog("Attention", isPresented: $confirmSearch, titleVisibility: .visible) {
Button("Cherchez quand même") {
requestedToGetAllPages = true
runSearch()
}
Button("Annuler", role: .cancel) {
}
} message: {
Text("Aucune ville n'a été indiqué, il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.")
}
.alert("Attention", isPresented: $presentAlert, actions: {
Button {
presentAlert = false
requestedToGetAllPages = true
page += 1
searching = true
Task {
await getNewPage()
searching = false
dismiss()
}
} label: {
Label("Tout voir", systemImage: "arrow.down.circle")
}
Button("Annuler") {
revealSearchParameters = true
presentAlert = false
}
}, message: {
if dataStore.appSettings.city.isEmpty {
Text("Il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.")
} else {
Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.")
}
})
.toolbarBackground(.visible, for: .bottomBar, .navigationBar)
.navigationTitle("Chercher un tournoi")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: locationManager.city) {
if let newValue = locationManager.city, dataStore.appSettings.city.isEmpty {
dataStore.appSettings.city = newValue
}
}
.toolbarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .bottomBar) {
if revealSearchParameters {
FooterButtonView("Lancer la recherche") {
if dataStore.appSettings.city.isEmpty {
confirmSearch = true
} else {
runSearch()
}
}
.disabled(searching)
} else if searching {
HStack(spacing: 20) {
Spacer()
ProgressView()
if total > 0 {
let percent = Double(tournaments.count) / Double(total)
Text(percent.formatted(.percent.precision(.significantDigits(1...3))) + " en récupération de Tenup")
.font(.caption)
}
Spacer()
}
}
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
#if DEBUG
if tournaments.isEmpty == false {
Section {
ShareLink(item: pastedTournaments) {
Label("Par texte", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
ShareLink(item: japList) {
Label("JAP liste", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
} header: {
Text("Partager les résultats")
}
}
Divider()
#endif
Button(role: .destructive) {
dataStore.appSettings.resetSearch()
locationManager.location = nil
locationManager.city = nil
revealSearchParameters = true
federalDataViewModel.searchedFederalTournaments = []
federalDataViewModel.searchAttemptCount = 0
} label: {
Text("Ré-initialiser la recherche")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
}
var pastedTournaments: String {
tournaments.map { $0.shareMessage }.joined()
}
var japList: String {
Set(tournaments.map { $0.japMessage }).joined(separator: "\n")
}
private var clubsFound: [String] {
Set(tournaments.compactMap { $0.nomClub }).sorted()
}
private var liguesFound: [String] {
Set(tournaments.compactMap { $0.nomLigue }).sorted()
}
private func runSearch() {
dataStore.appSettingsStorage.write()
revealSearchParameters = false
total = 0
page = 0
federalDataViewModel.searchedFederalTournaments = []
searching = true
requestedToGetAllPages = false
federalDataViewModel.searchAttemptCount += 1
federalDataViewModel.dayPeriod = dataStore.appSettings.dayPeriod
federalDataViewModel.dayDuration = dataStore.appSettings.dayDuration
federalDataViewModel.levels = Set(levels)
federalDataViewModel.categories = Set(categories)
federalDataViewModel.ageCategories = Set(ages)
federalDataViewModel.lastError = nil
Task {
await getNewPage()
searching = false
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false {
presentAlert = true
} else {
dismiss()
}
}
}
private var distanceLimit: Measurement<UnitLength> {
distanceLimit(distance: dataStore.appSettings.distance)
}
private func distanceLimit(distance: Double) -> Measurement<UnitLength> {
Measurement(value: distance, unit: .kilometers)
}
private var categories: [TournamentCategory] {
dataStore.appSettings.tournamentCategories.compactMap { TournamentCategory(rawValue: $0) }
}
private var levels: [TournamentLevel] {
dataStore.appSettings.tournamentLevels.compactMap { TournamentLevel(rawValue: $0) }
}
private var ages: [FederalTournamentAge] {
dataStore.appSettings.tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) }
}
private var types: [FederalTournamentType] {
dataStore.appSettings.tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) }
}
func getNewPage() async {
do {
if NetworkFederalService.shared.formId.isEmpty {
await getNewBuildForm()
} else {
let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup)
if commands.anySatisfy({ $0.command == "alert" }) {
federalDataViewModel.lastError = .maintenance
}
let resultCommand = commands.first(where: { $0.results != nil })
if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in
// let isValid = ft.tournaments.anySatisfy({ build in
// let ageValid = ages.isEmpty ? true : ages.contains(build.age)
// let levelValid = levels.isEmpty ? true : levels.contains(build.level)
// let categoryValid = categories.isEmpty ? true : categories.contains(build.category)
// return ageValid && levelValid && categoryValid
// })
if tournaments.contains(where: { $0.id == ft.id }) == false {
federalDataViewModel.searchedFederalTournaments.append(ft)
}
}
}
if let count = resultCommand?.results?.nb_results {
print("count", count, total, tournaments.count, page)
total = count
if tournaments.count < count && page < total / 30 {
if total < 200 || requestedToGetAllPages {
page += 1
await getNewPage()
}
} else {
print("finished")
}
} else {
print("total results not found")
}
}
} catch {
print("getNewPage", error)
await getNewBuildForm()
}
}
func getNewBuildForm() async {
do {
try await NetworkFederalService.shared.getNewBuildForm()
await getNewPage()
} catch {
print("getNewBuildForm", error)
}
}
@ViewBuilder
var searchParametersView: some View {
@Bindable var appSettings = dataStore.appSettings
Section {
DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date)
DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date)
Picker(selection: $appSettings.dayDuration) {
Text("Aucune").tag(nil as Int?)
Text(1.formatted()).tag(1 as Int?)
Text(2.formatted()).tag(2 as Int?)
Text(3.formatted()).tag(3 as Int?)
} label: {
Text("Durée souhaitée (en jours)")
}
Picker(selection: $appSettings.dayPeriod) {
ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel().capitalized).tag($0)
}
} label: {
Text("En semaine ou week-end")
}
HStack {
TextField("Ville", text: $appSettings.city)
if let city = locationManager.city {
Divider()
Text(city).italic()
}
if locationManager.requestStarted {
ProgressView()
} else {
LocationButton {
locationManager.requestLocation()
}
.symbolVariant(.fill)
.foregroundColor (Color.white)
.cornerRadius (20)
.font(.system(size: 12))
}
}
Picker(selection: $appSettings.distance) {
Text(distanceLimit(distance:30).formatted()).tag(30.0)
Text(distanceLimit(distance:50).formatted()).tag(50.0)
Text(distanceLimit(distance:60).formatted()).tag(60.0)
Text(distanceLimit(distance:90).formatted()).tag(90.0)
Text(distanceLimit(distance:150).formatted()).tag(150.0)
Text(distanceLimit(distance:200).formatted()).tag(200.0)
Text(distanceLimit(distance:400).formatted()).tag(400.0)
Text("Aucune").tag(3000.0)
} label: {
Text("Distance max")
}
Picker(selection: $appSettings.sortingOption) {
Text("Distance").tag("_DIST_")
Text("Date de début").tag("dateDebut+asc")
Text("Date de fin").tag("dateFin+asc")
} label: {
Text("Trier par")
}
NavigationLink {
List([TournamentCategory.men, TournamentCategory.women, TournamentCategory.mix], selection: $appSettings.tournamentCategories) { type in
Text(type.localizedLabel())
}
.navigationTitle("Catégories")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Catégorie")
Spacer()
categoriesLabel
.foregroundStyle(.secondary)
}
}
NavigationLink {
List([TournamentLevel.p25, TournamentLevel.p100, TournamentLevel.p250, TournamentLevel.p500, TournamentLevel.p1000, TournamentLevel.p1500, TournamentLevel.p2000], selection: $appSettings.tournamentLevels) { type in
Text(type.localizedLevelLabel())
}
.navigationTitle("Niveaux")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Niveau")
Spacer()
levelsLabel
.foregroundStyle(.secondary)
}
}
NavigationLink {
List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in
Text(type.localizedFederalAgeLabel())
}
.navigationTitle("Limites d'âge")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Limite d'âge")
Spacer()
if dataStore.appSettings.tournamentAges.isEmpty || dataStore.appSettings.tournamentAges.count == FederalTournamentAge.allCases.count {
Text("Tous les âges")
.foregroundStyle(.secondary)
} else {
Text(ages.map({ $0.localizedFederalAgeLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
NavigationLink {
List(FederalTournamentType.allCases, selection: $appSettings.tournamentTypes) { type in
Text(type.localizedLabel())
}
.navigationTitle("Types de tournoi")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Type de tournoi")
Spacer()
if dataStore.appSettings.tournamentTypes.isEmpty || dataStore.appSettings.tournamentTypes.count == FederalTournamentType.allCases.count {
Text("Tous les types")
.foregroundStyle(.secondary)
} else {
Text(types.map({ $0.localizedLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
Picker(selection: $appSettings.nationalCup) {
Text("N'importe").tag(false)
Text("Uniquement").tag(true)
} label: {
Text("Circuit National Padel Cup")
}
} header: {
Text("Critères de recherche")
}
.headerProminence(.increased)
.disabled(searching)
}
var categoriesLabel: some View {
if dataStore.appSettings.tournamentCategories.isEmpty || dataStore.appSettings.tournamentCategories.count == TournamentCategory.allCases.count {
Text("Toutes les catégories")
} else {
Text(categories.map({ $0.localizedLabel() }).joined(separator: ", "))
}
}
var levelsLabel: some View {
if dataStore.appSettings.tournamentLevels.isEmpty || dataStore.appSettings.tournamentLevels.count == TournamentLevel.allCases.count {
Text("Tous les niveaux")
} else {
Text(levels.map({ $0.localizedLevelLabel() }).joined(separator: ", "))
}
}
@ViewBuilder
var searchParametersSummaryView: some View {
VStack(alignment: .leading) {
HStack {
Text("Lieu")
Spacer()
Text(dataStore.appSettings.city)
if dataStore.appSettings.distance >= 3000 {
Text("sans limite de distance")
} else {
Text("à moins de " + distanceLimit.formatted())
}
}
HStack {
Text("Période")
Spacer()
Text("Du")
Text(dataStore.appSettings.startDate.twoDigitsYearFormatted)
Text("Au")
Text(dataStore.appSettings.endDate.twoDigitsYearFormatted)
}
HStack {
Text("Niveau")
Spacer()
levelsLabel
}
HStack {
Text("Catégorie")
Spacer()
categoriesLabel
}
HStack {
Text("Tri")
Spacer()
Text(sortingOptionLabel)
}
}
}
var sortingOptionLabel: String {
switch dataStore.appSettings.sortingOption {
case "_DIST_": return "Distance"
case "dateDebut+asc": return "Date de début"
case "dateFin+asc": return "Date de fin"
default:
return "Distance"
}
}
}