// // 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 var tournaments: [FederalTournament] { federalDataViewModel.searchedFederalTournaments } var body: some View { List { searchParametersView } .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") { 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 Task { await getNewPage() searching = false if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false { presentAlert = true } else { dismiss() } } } private var distanceLimit: Measurement { distanceLimit(distance: dataStore.appSettings.distance) } private func distanceLimit(distance: Double) -> Measurement { 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) let resultCommand = commands.first(where: { $0.results != nil }) if let newTournaments = resultCommand?.results?.items { newTournaments.forEach { ft in 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 searchContollerView: some View { Section { Button { runSearch() } label: { HStack { Label("Chercher un tournoi", systemImage: "magnifyingglass") if searching { Spacer() ProgressView() } } } Button { dataStore.appSettings.resetSearch() locationManager.location = nil locationManager.city = nil revealSearchParameters = true } label: { Label("Ré-initialiser la recherche", systemImage: "xmark.circle") } } } @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.localizedLabel()) } .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.localizedLabel()) } .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.localizedLabel()}).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.localizedLabel() }).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" } } }