fix issue with fft search

newoffer2025
Razmig Sarkissian 4 months ago
parent ae58efd2f7
commit 908edea494
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 9
      PadelClub/Data/Federal/FederalTournament.swift
  3. 368
      PadelClub/Utils/Network/FederalDataService.swift
  4. 205
      PadelClub/Utils/Network/NetworkFederalService.swift
  5. 4
      PadelClub/ViewModel/FederalDataViewModel.swift
  6. 23
      PadelClub/Views/Club/ClubSearchView.swift
  7. 85
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift

@ -753,6 +753,9 @@
FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; };
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */; };
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; };
FFD883792E1E63880004D7DD /* FederalDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD883782E1E63880004D7DD /* FederalDataService.swift */; };
FFD8837A2E1E63880004D7DD /* FederalDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD883782E1E63880004D7DD /* FederalDataService.swift */; };
FFD8837B2E1E63880004D7DD /* FederalDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD883782E1E63880004D7DD /* FederalDataService.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFE103082C353B7600684FC9 /* EventClubSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */; };
FFE103102C366DCD00684FC9 /* EditSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE1030F2C366DCD00684FC9 /* EditSharingView.swift */; };
@ -1144,6 +1147,7 @@
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.swift; sourceTree = "<group>"; };
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = "<group>"; };
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
FFD883782E1E63880004D7DD /* FederalDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalDataService.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EventClubSettingsView.swift; path = PadelClub/Views/Tournament/Screen/Components/EventClubSettingsView.swift; sourceTree = SOURCE_ROOT; };
FFE1030F2C366DCD00684FC9 /* EditSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSharingView.swift; sourceTree = "<group>"; };
@ -1743,6 +1747,7 @@
FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */,
FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */,
FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */,
FFD883782E1E63880004D7DD /* FederalDataService.swift */,
);
path = Network;
sourceTree = "<group>";
@ -2359,6 +2364,7 @@
FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */,
FFD8837A2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
@ -2623,6 +2629,7 @@
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD883792E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */,
FF4CBFDF2C996C0600151637 /* FederalTournamentSearchScope.swift in Sources */,
@ -2865,6 +2872,7 @@
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FFD8837B2E1E63880004D7DD /* FederalDataService.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */,
FF70FB5E2C90584900129CC2 /* FederalTournamentSearchScope.swift in Sources */,
@ -3137,7 +3145,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.44;
MARKETING_VERSION = 1.2.45;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3183,7 +3191,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.44;
MARKETING_VERSION = 1.2.45;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -341,17 +341,10 @@ struct CategoriesAgeTypePratique: Codable {
// MARK: - ID
struct ID: Codable {
var typePratique: TypePratique?
var typePratique: String?
var idCategorieAge: Int?
}
enum TypePratique: String, Codable {
case beach = "BEACH"
case padel = "PADEL"
case tennis = "TENNIS"
case pickle = "PICKLE"
}
// MARK: - CategorieTournoi
struct CategorieTournoi: Codable {
var code, codeTaxe: String?

@ -0,0 +1,368 @@
//
// FederalDataService.swift
// PadelClub
//
// Created by Razmig Sarkissian on 09/07/2025.
//
import Foundation
import CoreLocation
import LeStorage
import PadelClubData
struct UmpireContactInfo: Codable {
let name: String?
let email: String?
let phone: String?
}
/// Response model for the batch umpire data endpoint
struct UmpireDataResponse: Codable {
let results: [String: UmpireContactInfo]
}
// New struct for the response from get_fft_club_tournaments and get_fft_all_tournaments
struct TournamentsAPIResponse: Codable {
let success: Bool
let tournaments: [FederalTournament]
let totalResults: Int
let currentCount: Int
let pagesScraped: Int? // Optional, as it might not always be present or relevant
let page: Int? // Optional, as it might not always be present or relevant
let umpireDataIncluded: Bool? // Only for get_fft_club_tournaments_with_umpire_data
let message: String
private enum CodingKeys: String, CodingKey {
case success
case tournaments
case totalResults = "total_results"
case currentCount = "current_count"
case pagesScraped = "pages_scraped"
case page
case umpireDataIncluded = "umpire_data_included"
case message
}
}
// MARK: - FederalDataService
/// `FederalDataService` handles all API calls related to federal data (clubs, tournaments, umpire info).
/// All direct interactions with `tenup.fft.fr` are now assumed to be handled by your backend.
class FederalDataService {
static let shared: FederalDataService = FederalDataService()
// The 'formId', 'tenupJsonDecoder', 'runTenupTask', and 'getNewBuildForm'
// from the legacy NetworkFederalService are removed as their logic is now
// handled server-side.
/// Fetches federal clubs based on geographic criteria.
/// - Parameters:
/// - country: The country code (e.g., "fr").
/// - city: The city name or address for search.
/// - radius: The search radius in kilometers.
/// - location: Optional `CLLocation` for user's precise position to calculate distance.
/// - Returns: A `FederalClubResponse` object containing a list of clubs and total count.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func federalClubs(country: String = "fr", city: String, radius: Double, location: CLLocation? = nil) async throws -> FederalClubResponse {
let service = try StoreCenter.main.service()
// Construct query parameters for your backend API
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "country", value: country),
URLQueryItem(name: "city", value: city),
URLQueryItem(name: "radius", value: String(Int(radius)))
]
if let location = location {
queryItems.append(URLQueryItem(name: "lat", value: location.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us")))))
queryItems.append(URLQueryItem(name: "lng", value: location.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us")))))
}
// Build the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let queryString = urlComponents.query ?? ""
// The servicePath now points to your backend's endpoint for federal clubs: 'fft/federal-clubs/'
let urlRequest = try service._baseRequest(servicePath: "fft/federal-clubs?\(queryString)", method: .get, requiresToken: false)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse) // Keep URLError for generic network issues
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
return try JSONDecoder().decode(FederalClubResponse.self, from: data)
} catch {
print("Decoding error for FederalClubResponse: \(error)")
// Map decoding error to a generic API error
throw NetworkManagerError.apiError("Failed to decode FederalClubResponse: \(error.localizedDescription)")
}
}
/// Fetches federal tournaments for a specific club.
/// This function now calls your backend, which in turn handles the `form_build_id` and pagination.
/// The `tournaments` parameter is maintained for signature compatibility but is not used for server-side fetching.
/// Client-side accumulation of results from multiple pages should be handled by the caller.
/// - Parameters:
/// - page: The current page number for pagination.
/// - tournaments: An array of already gathered tournaments (for signature compatibility; not used internally for fetching).
/// - club: The name of the club.
/// - codeClub: The unique code of the club.
/// - startDate: Optional start date for filtering tournaments.
/// - endDate: Optional end date for filtering tournaments.
/// - Returns: An array of `FederalTournament` objects for the requested page.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil, endDate: Date? = nil) async throws -> TournamentsAPIResponse {
let service = try StoreCenter.main.service()
// Construct query parameters for your backend API
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "club_code", value: codeClub),
URLQueryItem(name: "club_name", value: club),
URLQueryItem(name: "page", value: String(page))
]
if let startDate = startDate {
queryItems.append(URLQueryItem(name: "start_date", value: startDate.twoDigitsYearFormatted))
}
if let endDate = endDate {
queryItems.append(URLQueryItem(name: "end_date", value: endDate.twoDigitsYearFormatted))
}
// Build the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let queryString = urlComponents.query ?? ""
// The servicePath now points to your backend's endpoint for club tournaments: 'fft/club-tournaments/'
let urlRequest = try service._baseRequest(servicePath: "fft/club-tournaments?\(queryString)", method: .get, requiresToken: false)
print(urlRequest.url?.absoluteString)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
// Your backend should return a direct array of FederalTournament for the requested page
let federalTournaments = try JSONDecoder().decode(TournamentsAPIResponse.self, from: data)
return federalTournaments
} catch {
print("Decoding error for FederalTournament array: \(error)")
throw NetworkManagerError.apiError("Failed to decode FederalTournament array: \(error.localizedDescription)")
}
}
/// Fetches all federal tournaments based on various filtering options.
/// This function now calls your backend, which handles the complex filtering and data retrieval.
/// The return type `[HttpCommand]` is maintained for signature compatibility,
/// wrapping the actual `[FederalTournament]` data within an `HttpCommand` structure.
/// - Parameters:
/// - sortingOption: How to sort the results (e.g., "dateDebut asc").
/// - page: The current page number for pagination.
/// - startDate: The start date for the tournament search.
/// - endDate: The end date for the tournament search.
/// - city: The city to search within.
/// - distance: The search distance from the city.
/// - categories: An array of `TournamentCategory` to filter by.
/// - levels: An array of `TournamentLevel` to filter by.
/// - lat: Optional latitude for precise location search.
/// - lng: Optional longitude for precise location search.
/// - ages: An array of `FederalTournamentAge` to filter by.
/// - types: An array of `FederalTournamentType` to filter by.
/// - nationalCup: A boolean indicating if national cup tournaments should be included.
/// - Returns: An array of `HttpCommand` objects, containing the `FederalTournament` data.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getAllFederalTournaments(
sortingOption: String,
page: Int,
startDate: Date,
endDate: Date,
city: String,
distance: Double,
categories: [TournamentCategory],
levels: [TournamentLevel],
lat: String?,
lng: String?,
ages: [FederalTournamentAge],
types: [FederalTournamentType],
nationalCup: Bool
) async throws -> TournamentsAPIResponse {
let service = try StoreCenter.main.service()
// Construct query parameters for your backend API
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "sort", value: sortingOption),
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "start_date", value: startDate.twoDigitsYearFormatted),
URLQueryItem(name: "end_date", value: endDate.twoDigitsYearFormatted),
URLQueryItem(name: "city", value: city),
URLQueryItem(name: "distance", value: String(Int(distance))),
URLQueryItem(name: "national_cup", value: nationalCup ? "true" : "false")
]
if let lat = lat, !lat.isEmpty {
queryItems.append(URLQueryItem(name: "lat", value: lat))
}
if let lng = lng, !lng.isEmpty {
queryItems.append(URLQueryItem(name: "lng", value: lng))
}
// Add array parameters (assuming your backend can handle comma-separated or multiple query params)
if !categories.isEmpty {
queryItems.append(URLQueryItem(name: "categories", value: categories.map { String($0.rawValue) }.joined(separator: ",")))
}
if !levels.isEmpty {
queryItems.append(URLQueryItem(name: "levels", value: levels.map { String($0.rawValue) }.joined(separator: ",")))
}
if !ages.isEmpty {
queryItems.append(URLQueryItem(name: "ages", value: ages.map { String($0.rawValue) }.joined(separator: ",")))
}
if !types.isEmpty {
queryItems.append(URLQueryItem(name: "types", value: types.map { $0.rawValue }.joined(separator: ",")))
}
// Build the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let queryString = urlComponents.query ?? ""
// The servicePath now points to your backend's endpoint for all tournaments: 'fft/all-tournaments/'
let urlRequest = try service._baseRequest(servicePath: "fft/all-tournaments?\(queryString)", method: .get, requiresToken: true)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
print(urlRequest.url?.absoluteString ?? "No URL")
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
// Your backend should return a direct array of FederalTournament
let federalTournaments = try JSONDecoder().decode(TournamentsAPIResponse.self, from: data)
return federalTournaments
} catch {
print("Decoding error for FederalTournament array in getAllFederalTournaments: \(error)")
throw NetworkManagerError.apiError("Failed to decode FederalTournament array: \(error.localizedDescription)")
}
}
/// Fetches umpire contact data for a given tournament ID.
/// This function now calls your backend, which performs the HTML scraping.
/// The return type is maintained for signature compatibility, mapping `UmpireContactInfo` to a tuple.
/// - Parameter idTournament: The ID of the tournament.
/// - Returns: A tuple `(name: String?, email: String?, phone: String?)` containing the umpire's contact info.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getUmpireData(idTournament: String) async throws -> (name: String?, email: String?, phone: String?) {
let service = try StoreCenter.main.service()
// The servicePath now points to your backend's endpoint for umpire data: 'fft/umpire/{tournament_id}/'
let servicePath = "fft/umpire/\(idTournament)/"
let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
do {
let umpireInfo = try JSONDecoder().decode(UmpireContactInfo.self, from: data)
// Map the decoded struct to the tuple required by the legacy signature
print(umpireInfo)
return (name: umpireInfo.name, email: umpireInfo.email, phone: umpireInfo.phone)
} catch {
print("Decoding error for UmpireContactInfo: \(error)")
throw NetworkManagerError.apiError("Failed to decode UmpireContactInfo: \(error.localizedDescription)")
}
}
/// Fetches umpire contact data for multiple tournament IDs.
/// This function calls your backend endpoint that handles multiple tournament IDs via query parameters.
/// - Parameter tournamentIds: An array of tournament ID strings.
/// - Returns: A dictionary mapping tournament IDs to tuples `(name: String?, email: String?, phone: String?)` containing the umpire's contact info.
/// - Throws: An error if the network request fails or decoding the response is unsuccessful.
func getUmpiresData(tournamentIds: [String]) async throws -> [String: (name: String?, email: String?, phone: String?)] {
let service = try StoreCenter.main.service()
// Validate input
guard !tournamentIds.isEmpty else {
throw NetworkManagerError.apiError("Tournament IDs array cannot be empty")
}
// Create the base service path
let basePath = "fft/umpires/"
// Build query parameters - join tournament IDs with commas
let tournamentIdsParam = tournamentIds.joined(separator: ",")
let queryItems = [URLQueryItem(name: "tournament_ids", value: tournamentIdsParam)]
// Create the URL with query parameters
var urlComponents = URLComponents()
urlComponents.queryItems = queryItems
let servicePath = basePath + (urlComponents.url?.query.map { "?\($0)" } ?? "")
let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard !data.isEmpty else {
throw NetworkManagerError.noDataReceived
}
// Check for HTTP errors
guard httpResponse.statusCode == 200 else {
if let errorData = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let message = errorData["message"] as? String {
throw NetworkManagerError.apiError("Server error: \(message)")
}
throw NetworkManagerError.apiError("HTTP error: \(httpResponse.statusCode)")
}
do {
let umpireResponse = try JSONDecoder().decode(UmpireDataResponse.self, from: data)
// Convert the results to the expected return format
var resultDict: [String: (name: String?, email: String?, phone: String?)] = [:]
for (tournamentId, umpireInfo) in umpireResponse.results {
resultDict[tournamentId] = (name: umpireInfo.name, email: umpireInfo.email, phone: umpireInfo.phone)
}
print("Umpire data fetched for \(resultDict.count) tournaments")
return resultDict
} catch {
print("Decoding error for UmpireDataResponse: \(error)")
throw NetworkManagerError.apiError("Failed to decode UmpireDataResponse: \(error.localizedDescription)")
}
}
}

@ -67,6 +67,7 @@ class NetworkFederalService {
func federalClubs(country: String = "fr", city: String, radius: Double, location: CLLocation? = nil) async throws -> FederalClubResponse {
return try await FederalDataService.shared.federalClubs(country: country, city: city, radius: radius, location: location)
/*
{
"geocoding[country]": "fr",
@ -114,211 +115,11 @@ class NetworkFederalService {
func getClubFederalTournaments(page: Int, tournaments: [FederalTournament], club: String, codeClub: String, startDate: Date? = nil, endDate: Date? = nil) async throws -> [FederalTournament] {
if formId.isEmpty {
do {
try await getNewBuildForm()
} catch {
print("getClubFederalTournaments", error)
}
}
var dateComponent = ""
if let startDate, let endDate {
dateComponent = "&date[start]=\(startDate.twoDigitsYearFormatted)&date[end]=\(endDate.endOfMonth.twoDigitsYearFormatted)"
} else if let startDate {
dateComponent = "&date[start]=\(startDate.twoDigitsYearFormatted)&date[end]=\(Calendar.current.date(byAdding: .month, value: 3, to: startDate)!.endOfMonth.twoDigitsYearFormatted)"
}
let parameters = """
recherche_type=club&club[autocomplete][value_container][value_field]=\(codeClub.replaceCharactersFromSet(characterSet: .whitespaces))&club[autocomplete][value_container][label_field]=\(club.replaceCharactersFromSet(characterSet: .whitespaces, replacementString: "+"))&pratique=PADEL\(dateComponent)&page=\(page)&sort=dateDebut+asc&form_build_id=\(formId)&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page
"""
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/system/ajax")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.addValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.httpMethod = "POST"
request.httpBody = postData
let commands : [HttpCommand] = try await runTenupTask(request: request)
if commands.anySatisfy({ $0.command == "alert" }) {
throw NetworkManagerError.maintenance
}
let resultCommand = commands.first(where: { $0.results != nil })
if let gatheredTournaments = resultCommand?.results?.items {
var finalTournaments = tournaments + gatheredTournaments
if let count = resultCommand?.results?.nb_results {
if finalTournaments.count < count {
let newTournaments = try await getClubFederalTournaments(page: page+1, tournaments: finalTournaments, club: club, codeClub: codeClub)
finalTournaments = finalTournaments + newTournaments
}
}
return finalTournaments
}
// do {
// } catch {
// print("getClubFederalTournaments", error)
// }
//
return []
}
func getNewBuildForm() async throws {
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/tournois")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.addValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.addValue("trailers", forHTTPHeaderField: "TE")
request.httpMethod = "GET"
let task = try await URLSession.shared.data(for: request)
if let stringData = String(data: task.0, encoding: .utf8) {
let stringDataFolded = stringData.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
let prefix = "form_build_id\"value=\"form-"
var finalData = ""
if let lab = stringDataFolded.matches(of: try! Regex("\(prefix)")).last {
finalData = String(stringDataFolded[lab.range.upperBound...])
}
let suffix = "\"/><inputtype=\"hidden\"name=\"form_id\"value=\"recherche_tournois_form"
if let suff = finalData.firstMatch(of: try! Regex("\(suffix)")) {
finalData = String(finalData[..<suff.range.lowerBound])
}
print(finalData)
formId = "form-\(finalData)"
} else {
print("no data found in html")
}
}
func getAllFederalTournaments(sortingOption: String, page: Int, startDate: Date, endDate: Date, city: String, distance: Double, categories: [TournamentCategory], levels: [TournamentLevel], lat: String?, lng: String?, ages: [FederalTournamentAge], types: [FederalTournamentType], nationalCup: Bool) async throws -> [HttpCommand] {
var cityParameter = ""
var searchType = "ligue"
if city.trimmed.isEmpty == false {
searchType = "ville"
cityParameter = city
}
var levelsParameter = ""
if levels.isEmpty == false {
levelsParameter = levels.map { "categorie_tournoi[\($0.searchRawValue())]=\($0.searchRawValue())" }.joined(separator: "&") + "&"
}
var categoriesParameter = ""
if categories.isEmpty == false {
categoriesParameter = categories.map { "epreuve[\($0.requestLabel)]=\($0.requestLabel)" }.joined(separator: "&") + "&"
}
var agesParameter = ""
if ages.isEmpty == false {
agesParameter = ages.map { "categorie_age[\($0.rawValue)]=\($0.rawValue)" }.joined(separator: "&") + "&"
}
var typesParameter = ""
if types.isEmpty == false {
typesParameter = types.map { "type[\($0.rawValue.capitalized)]=\($0.rawValue.capitalized)" }.joined(separator: "&") + "&"
}
var npc = ""
if nationalCup {
npc = "&tournoi_npc=1"
}
let parameters = """
recherche_type=\(searchType)&ville%5Bautocomplete%5D%5Bcountry%5D=fr&ville%5Bautocomplete%5D%5Btextfield%5D=&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blat_field%5D=\(lat ?? "")&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blng_field%5D=\(lng ?? "")&ville%5Bdistance%5D%5Bvalue_field%5D=\(Int(distance))&club%5Bautocomplete%5D%5Btextfield%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=&pratique=PADEL&date%5Bstart%5D=\(startDate.twoDigitsYearFormatted)&date%5Bend%5D=\(endDate.twoDigitsYearFormatted)&\(categoriesParameter)\(levelsParameter)\(agesParameter)\(typesParameter)\(npc)&page=\(page)&sort=\(sortingOption)&form_build_id=\(formId)&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page
"""
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/system/ajax")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.addValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.httpMethod = "POST"
request.httpBody = postData
return try await runTenupTask(request: request)
return try await FederalDataService.shared.getClubFederalTournaments(page: page, tournaments: tournaments, club: club, codeClub: codeClub, startDate: startDate, endDate: endDate).tournaments
}
func getUmpireData(idTournament: String) async throws -> (name: String?, email: String?, phone: String?) {
guard let url = URL(string: "https://tenup.fft.fr/tournoi/\(idTournament)") else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
guard let htmlString = String(data: data, encoding: .utf8) else {
throw URLError(.cannotDecodeContentData)
}
let namePattern = "tournoi-detail-page-inscription-responsable-title\">\\s*([^<]+)\\s*<"
let nameRegex = try? NSRegularExpression(pattern: namePattern)
let nameMatch = nameRegex?.firstMatch(in: htmlString, range: NSRange(htmlString.startIndex..., in: htmlString))
let name = nameMatch.flatMap { match in
Range(match.range(at: 1), in: htmlString)
}.map { range in
String(htmlString[range]).trimmingCharacters(in: .whitespacesAndNewlines)
}
// Extract email using regex
let emailPattern = "mailto:([^\"]+)\""
let emailRegex = try? NSRegularExpression(pattern: emailPattern)
let emailMatch = emailRegex?.firstMatch(in: htmlString, range: NSRange(htmlString.startIndex..., in: htmlString))
let email = emailMatch.flatMap { match in
Range(match.range(at: 1), in: htmlString)
}.map { range in
String(htmlString[range])
}
let pattern = "<div class=\"details-bloc\">\\s*(\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2})\\s*</div>"
var phoneNumber: String? = nil
// Look for the specific div and its content
if let range = htmlString.range(of: pattern, options: [.regularExpression, .caseInsensitive]) {
let match = String(htmlString[range])
let phonePattern = "\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}"
if let phoneRange = match.range(of: phonePattern, options: .regularExpression) {
phoneNumber = String(match[phoneRange])
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
return (name, email, phoneNumber)
return try await FederalDataService.shared.getUmpireData(idTournament: idTournament)
}
}

@ -176,7 +176,7 @@ class FederalDataViewModel {
let collector = TournamentCollector()
try await clubs.filter { $0.code != nil }.concurrentForEach { club in
let newTournaments = try await NetworkFederalService.shared.getClubFederalTournaments(
let newTournaments = try await FederalDataService.shared.getClubFederalTournaments(
page: 0,
tournaments: [],
club: club.name,
@ -186,7 +186,7 @@ class FederalDataViewModel {
)
// Safely add to collector
await collector.add(tournaments: newTournaments)
await collector.add(tournaments: newTournaments.tournaments)
}
// Get all collected tournaments

@ -414,18 +414,25 @@ struct FederalClubResponse: Codable {
}
}
enum Pratique: String, Codable {
case beach = "BEACH"
case padel = "PADEL"
case tennis = "TENNIS"
case pickle = "PICKLE"
}
//enum Pratique: String, Codable {
// case beach = "BEACH"
// case padel = "PADEL"
// case tennis = "TENNIS"
// case pickle = "PICKLE"
//
// // Additional cases for the combined values
// case tennisPadel = "tennis-padel"
// case tennisPicklePadel = "tennis-pickle-padel"
// case tennisPadelBeach = "tennis-padel-beach"
// case padelOnly = "padel" // lowercase padel
//}
//
// MARK: - ClubMarker
struct ClubMarker: Codable, Hashable, Identifiable {
let nom, clubID, ville, distance: String
let terrainPratiqueLibelle: String
let pratiques: [Pratique]
let pratiques: [String]
let lat, lng: Double
// Method to get the number of courts for a specific sport
@ -449,7 +456,7 @@ struct ClubMarker: Codable, Hashable, Identifiable {
return courts
}
}
} else if pratiques.count == 1 && pratiques.first?.rawValue.lowercased() == sport.lowercased() {
} else if pratiques.count == 1 && pratiques.first?.lowercased() == sport.lowercased() {
// Handle cases where only the number of courts is provided (e.g., "2 terrains")
if let courtsNumber = trimmedComponent.split(separator: " ").first,
let courts = Int(courtsNumber) {

@ -187,13 +187,26 @@ struct TournamentLookUpView: View {
private func _gatherNumbers() {
Task {
print("Doing.....")
for i in 0..<tournaments.count {
print(i, "/", tournaments.count)
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournaments[i].id).phone
federalDataViewModel.searchedFederalTournaments[i].updateJapPhoneNumber(phone: phone)
print(federalDataViewModel.searchedFederalTournaments[i].japMessage)
await withTaskGroup(of: (Int, String?).self) { group in
for i in 0..<tournaments.count {
let tournamentID = tournaments[i].id
let index = i // Capture index for use in the child task
group.addTask {
print("Starting task for tournament \(index) / \(self.tournaments.count)")
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournamentID).phone
return (index, phone) // Return the index along with the phone number
}
}
// Process results as they complete
for await (index, phone) in group {
var tournamentData = federalDataViewModel.searchedFederalTournaments[index] // Get a mutable copy
tournamentData.updateJapPhoneNumber(phone: phone) // Mutate the copy
federalDataViewModel.searchedFederalTournaments[index] = tournamentData // Assign back
}
}
print(".....Done")
}
}
@ -251,57 +264,39 @@ struct TournamentLookUpView: View {
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 commands = try await FederalDataService.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.success == false {
federalDataViewModel.lastError = .maintenance
}
commands.tournaments.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 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")
}
let count = commands.totalResults
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")
}
} catch {
print("getNewPage", error)
await getNewBuildForm()
}
}
func getNewBuildForm() async {
do {
try await NetworkFederalService.shared.getNewBuildForm()
await getNewPage()
} catch {
print("getNewBuildForm", error)
}
}

Loading…
Cancel
Save