parent
40fc73e6bf
commit
708e0aa481
@ -0,0 +1,637 @@ |
||||
// |
||||
// TournamentLookUpView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by razmig on 08/09/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
import CoreLocation |
||||
import CoreLocationUI |
||||
|
||||
struct TournamentLookUpView: View { |
||||
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel |
||||
@StateObject var locationManager = LocationManager() |
||||
@Environment(\.dismiss) private var dismiss |
||||
|
||||
@State private var searchField: String = "" |
||||
@State private var sectionedTournaments: [String: [FederalTournament]] = [:] |
||||
@State private var dayPeriod: DayPeriod = .all |
||||
@State private var duration: Int = 3 |
||||
|
||||
@State var page: Int = 0 |
||||
@State var total: Int = 0 |
||||
|
||||
@State private var tournamentCategories = Set<TournamentCategory.ID>() |
||||
@State private var tournamentLevels = Set<TournamentLevel.ID>() |
||||
@State private var tournamentAges = Set<FederalTournamentAge.ID>() |
||||
@State private var tournamentTypes = Set<FederalTournamentType.ID>() |
||||
@State private var searching: Bool = false |
||||
@State private var startDate: Date = Date() |
||||
@State private var endDate: Date = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||
@AppStorage("lastCity") private var city: String = "" |
||||
@State private var ligue: String = "" |
||||
@AppStorage("lastDistance") private var distance: Double = 30 |
||||
@AppStorage("lastSortingOption") private var sortingOption: String = "_DIST_" |
||||
@State private var requestedToGetAllPages: Bool = false |
||||
@AppStorage("lastNationalCup") private var nationalCup: Bool = false |
||||
@State private var revealSearchParameters: Bool = true |
||||
@State private var searchScope = FederalTournamentSearchScope.all |
||||
|
||||
var tournaments: [FederalTournament] { |
||||
federalDataViewModel.searchedFederalTournaments |
||||
} |
||||
|
||||
func canShowTournament(_ tournament: FederalTournament) -> Bool { |
||||
guard tournament.dayDuration <= duration else { return false } |
||||
guard (tournament.dayPeriod == dayPeriod && dayPeriod != .all) || dayPeriod == .all else { return false } |
||||
if searchField.isEmpty { |
||||
return true |
||||
} else { |
||||
return tournament.validForSearch(searchField, scope: searchScope) |
||||
} |
||||
} |
||||
|
||||
var body: some View { |
||||
List { |
||||
searchParametersView |
||||
|
||||
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false { |
||||
Section { |
||||
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.") |
||||
Button { |
||||
requestedToGetAllPages = true |
||||
page += 1 |
||||
searching = true |
||||
Task { |
||||
await getNewPage() |
||||
searching = false |
||||
buildSectionedData() |
||||
} |
||||
} label: { |
||||
Label("Tout voir", systemImage: "arrow.down.circle") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.toolbarBackground(.visible, for: .bottomBar, .navigationBar) |
||||
.navigationTitle("Chercher un tournoi") |
||||
.navigationBarTitleDisplayMode(.inline) |
||||
.onChange(of: locationManager.city, perform: { newValue in |
||||
if let newValue, city.isEmpty { |
||||
city = newValue |
||||
} |
||||
}) |
||||
.toolbarTitleDisplayMode(.large) |
||||
.toolbar { |
||||
ToolbarItem(placement: .bottomBar) { |
||||
if revealSearchParameters { |
||||
FooterButtonView("Lancer la recherche") { |
||||
runSearch() |
||||
} |
||||
.disabled(searching) |
||||
} else if searchField.isEmpty == false && searchScope != .all { |
||||
let count = _totalVisibleEpreuves().count |
||||
VStack { |
||||
Text(searchField) |
||||
.foregroundStyle(.secondary) |
||||
Text(count.formatted() + " tournoi" + count.pluralSuffix) |
||||
} |
||||
.font(.caption) |
||||
} 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() |
||||
} |
||||
} else { |
||||
let count = _totalVisibleEpreuves().count |
||||
Text(count.formatted() + " tournoi" + count.pluralSuffix) |
||||
.font(.caption) |
||||
} |
||||
} |
||||
ToolbarItem(placement: .topBarTrailing) { |
||||
Menu { |
||||
if tournaments.isEmpty == false { |
||||
Section { |
||||
let preview = SharePreview(Text("Ma recherche de tournois"), icon: Image("PadelClub_logo_fondclair_transparent")) |
||||
ShareLink(item: renderedImage ?? Image(systemName: "photo"), preview: preview) { |
||||
if renderedImage == nil { |
||||
ProgressView() |
||||
} else { |
||||
Label("Par image (20max)", systemImage: "square.and.arrow.up") |
||||
.labelStyle(.titleAndIcon) |
||||
} |
||||
} |
||||
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() |
||||
Button { |
||||
tournamentLevels = Set() |
||||
tournamentCategories = Set() |
||||
city = "" |
||||
locationManager.location = nil |
||||
locationManager.city = nil |
||||
distance = 30 |
||||
startDate = Date() |
||||
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||
sortingOption = "_DIST_" |
||||
revealSearchParameters = true |
||||
} 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 func isTypeLookedAfter(_ type: any TournamentBuildHolder) -> Bool { |
||||
if levels.contains(where: { level in |
||||
type.level == level |
||||
}) || levels.isEmpty { |
||||
if categories.contains(where: { category in |
||||
type.category == category |
||||
}) || categories.isEmpty { |
||||
|
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
|
||||
@Environment(\.displayScale) var displayScale |
||||
@State private var renderedImage: Image? |
||||
|
||||
@MainActor |
||||
func render() { |
||||
let renderer = ImageRenderer(content: tournamentsView) |
||||
renderer.scale = displayScale |
||||
renderer.isOpaque = true |
||||
if let uiImage = renderer.uiImage { |
||||
renderedImage = Image(uiImage: uiImage) |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private var tournamentsView: some View { |
||||
let tournaments = tournaments.prefix(20) |
||||
VStack { |
||||
ForEach(tournaments.indices, id: \.self) { tournamentIndex in |
||||
let tournament = tournaments[tournamentIndex] |
||||
HStack(alignment: .center) { |
||||
VStack(alignment: .leading) { |
||||
Text(tournament.libelle ?? "unknown").font(.headline) |
||||
if let club = tournament.nomClub { |
||||
Text(club) |
||||
.font(.footnote) |
||||
.lineLimit(1) |
||||
} |
||||
} |
||||
Spacer() |
||||
VStack(alignment: .trailing) { |
||||
if let startDate = tournament.dateDebut { |
||||
Text(startDate.monthYearFormatted) |
||||
HStack { |
||||
Text(startDate.formatted(.dateTime.weekday())) |
||||
Text(startDate.formatted(.dateTime.day())).font(.largeTitle) |
||||
} |
||||
} |
||||
if let distance = tournament.distanceEnMetres { |
||||
let measurement = Measurement(value: distance / 1000, unit: UnitLength.kilometers) |
||||
Text(measurement.formatted()).font(.caption) |
||||
} |
||||
} |
||||
} |
||||
.padding() |
||||
.foregroundColor(Color.black) |
||||
.background { |
||||
tournamentIndex%2 == 0 ? Color.mint : Color.cyan |
||||
} |
||||
} |
||||
} |
||||
.padding() |
||||
} |
||||
|
||||
|
||||
private var clubsFound: [String] { |
||||
Set(tournaments.compactMap { $0.nomClub }).sorted() |
||||
} |
||||
|
||||
private var liguesFound: [String] { |
||||
Set(tournaments.compactMap { $0.nomLigue }).sorted() |
||||
} |
||||
|
||||
private func runSearch() { |
||||
revealSearchParameters = false |
||||
total = 0 |
||||
page = 0 |
||||
federalDataViewModel.searchedFederalTournaments = [] |
||||
searching = true |
||||
requestedToGetAllPages = false |
||||
renderedImage = nil |
||||
federalDataViewModel.searchAttemptCount += 1 |
||||
Task { |
||||
await getNewPage() |
||||
searching = false |
||||
dismiss() |
||||
} |
||||
} |
||||
|
||||
func buildSectionedData() { |
||||
sectionedTournaments = FederalTournament.sectionedData(from: tournaments) |
||||
} |
||||
|
||||
private var distanceLimit: Measurement<UnitLength> { |
||||
distanceLimit(distance: distance) |
||||
} |
||||
|
||||
private func distanceLimit(distance: Double) -> Measurement<UnitLength> { |
||||
Measurement(value: distance, unit: .kilometers) |
||||
} |
||||
|
||||
private var categories: [TournamentCategory] { |
||||
tournamentCategories.compactMap { TournamentCategory(rawValue: $0) } |
||||
} |
||||
|
||||
private var levels: [TournamentLevel] { |
||||
tournamentLevels.compactMap { TournamentLevel(rawValue: $0) } |
||||
} |
||||
|
||||
private var ages: [FederalTournamentAge] { |
||||
tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) } |
||||
} |
||||
|
||||
private var types: [FederalTournamentType] { |
||||
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: sortingOption, page: page, startDate: startDate, endDate: endDate, city: city, distance: 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: 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 renderedImage == nil { |
||||
render() |
||||
} |
||||
if tournaments.count < count && page < total / 30 { |
||||
if total < 200 || requestedToGetAllPages { |
||||
page += 1 |
||||
await MainActor.run() { |
||||
buildSectionedData() |
||||
} |
||||
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 { |
||||
tournamentLevels = Set() |
||||
tournamentCategories = Set() |
||||
city = "" |
||||
locationManager.location = nil |
||||
locationManager.city = nil |
||||
dayPeriod = .all |
||||
duration = 3 |
||||
distance = 30 |
||||
startDate = Date() |
||||
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||
sortingOption = "_DIST_" |
||||
revealSearchParameters = true |
||||
} label: { |
||||
Label("Ré-initialiser la recherche", systemImage: "xmark.circle") |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
var searchParametersView: some View { |
||||
Section { |
||||
DatePicker("Début", selection: $startDate, displayedComponents: .date) |
||||
DatePicker("Fin", selection: $endDate, displayedComponents: .date) |
||||
|
||||
Picker(selection: $duration) { |
||||
Text("Aucune").tag(7) |
||||
Text(1.formatted()).tag(1) |
||||
Text(2.formatted()).tag(2) |
||||
Text(3.formatted()).tag(3) |
||||
} label: { |
||||
Text("Durée max (en jours)") |
||||
} |
||||
|
||||
Picker(selection: $dayPeriod) { |
||||
Text("N'importe").tag(DayPeriod.all) |
||||
Text("le weekend").tag(DayPeriod.weekend) |
||||
Text("la semaine").tag(DayPeriod.week) |
||||
} label: { |
||||
Text("En semaine ou week-end") |
||||
} |
||||
|
||||
HStack { |
||||
TextField("Ville", text: $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: $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: $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.allCases, selection: $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.allCases, selection: $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.allCases, selection: $tournamentAges) { type in |
||||
Text(type.localizedLabel()) |
||||
} |
||||
.navigationTitle("Limites d'âge") |
||||
.environment(\.editMode, Binding.constant(EditMode.active)) |
||||
} label: { |
||||
HStack { |
||||
Text("Limite d'âge") |
||||
Spacer() |
||||
if tournamentAges.isEmpty || 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: $tournamentTypes) { type in |
||||
Text(type.localizedLabel()) |
||||
} |
||||
.navigationTitle("Types de tournoi") |
||||
.environment(\.editMode, Binding.constant(EditMode.active)) |
||||
} label: { |
||||
HStack { |
||||
Text("Type de tournoi") |
||||
Spacer() |
||||
if tournamentTypes.isEmpty || tournamentTypes.count == FederalTournamentType.allCases.count { |
||||
Text("Tous les types") |
||||
.foregroundStyle(.secondary) |
||||
} else { |
||||
Text(types.map({ $0.localizedLabel()}).joined(separator: ", ")) |
||||
.foregroundStyle(.secondary) |
||||
} |
||||
} |
||||
} |
||||
|
||||
Picker(selection: $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 tournamentCategories.isEmpty || tournamentCategories.count == TournamentCategory.allCases.count { |
||||
Text("Toutes les catégories") |
||||
} else { |
||||
Text(categories.map({ $0.localizedLabel() }).joined(separator: ", ")) |
||||
} |
||||
} |
||||
|
||||
var levelsLabel: some View { |
||||
if tournamentLevels.isEmpty || 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(city) |
||||
if distance >= 3000 { |
||||
Text("sans limite de distance") |
||||
} else { |
||||
Text("à moins de " + distanceLimit.formatted()) |
||||
} |
||||
} |
||||
HStack { |
||||
Text("Période") |
||||
Spacer() |
||||
Text("Du") |
||||
Text(startDate.twoDigitsYearFormatted) |
||||
Text("Au") |
||||
Text(endDate.twoDigitsYearFormatted) |
||||
} |
||||
HStack { |
||||
Text("Niveau") |
||||
Spacer() |
||||
levelsLabel |
||||
} |
||||
HStack { |
||||
Text("Catégorie") |
||||
Spacer() |
||||
categoriesLabel |
||||
} |
||||
|
||||
HStack { |
||||
Text("Tri") |
||||
Spacer() |
||||
Text(sortingOptionLabel) |
||||
} |
||||
} |
||||
} |
||||
|
||||
var sortingOptionLabel: String { |
||||
switch sortingOption { |
||||
case "_DIST_": return "Distance" |
||||
case "dateDebut+asc": return "Date de début" |
||||
case "dateFin+asc": return "Date de fin" |
||||
default: |
||||
return "Distance" |
||||
} |
||||
} |
||||
|
||||
|
||||
func _totalVisibleTournaments(_ date: Date? = nil) -> [FederalTournament] { |
||||
if let date { |
||||
if let tournaments = sectionedTournaments[URL.importDateFormatter.string(from: date)] { |
||||
let allTournaments = tournaments.filter({ canShowTournament($0) }).filter({ tournament in |
||||
if tournament.tournaments.count > 1 { |
||||
return tournament.tournaments.anySatisfy { isTypeLookedAfter($0) } |
||||
} else { |
||||
return true |
||||
} |
||||
}) |
||||
return allTournaments |
||||
} else { |
||||
return [] |
||||
} |
||||
} else { |
||||
let allTournaments = sectionedTournaments.values.flatMap({ $0 }).filter({ canShowTournament($0) }).filter({ tournament in |
||||
if tournament.tournaments.count > 1 { |
||||
return tournament.tournaments.anySatisfy { isTypeLookedAfter($0) } |
||||
} else { |
||||
return true |
||||
} |
||||
}) |
||||
return allTournaments |
||||
} |
||||
} |
||||
|
||||
func _totalVisibleEpreuves(_ date: Date? = nil) -> [any TournamentBuildHolder] { |
||||
if let date { |
||||
if let tournaments = sectionedTournaments[URL.importDateFormatter.string(from: date)] { |
||||
let allTournaments = tournaments |
||||
.filter({ canShowTournament($0) }) |
||||
.compactMap({ $0.tournaments }) |
||||
.flatMap({ $0 }) |
||||
.filter({ isTypeLookedAfter($0) }) |
||||
return allTournaments |
||||
} else { |
||||
return [] |
||||
} |
||||
} else { |
||||
let allTournaments = sectionedTournaments.values.flatMap({ $0 }) |
||||
.filter({ canShowTournament($0) }) |
||||
.compactMap({ $0.tournaments }) |
||||
.flatMap({ $0 }) |
||||
.filter({ isTypeLookedAfter($0) }) |
||||
return allTournaments |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue