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.
302 lines
14 KiB
302 lines
14 KiB
//
|
|
// 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/'
|
|
var urlRequest = try service._baseRequest(servicePath: "fft/all-tournaments?\(queryString)", method: .get, requiresToken: true)
|
|
urlRequest.timeoutInterval = 180
|
|
|
|
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)/"
|
|
var urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false)
|
|
urlRequest.timeoutInterval = 120.0
|
|
|
|
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)")
|
|
}
|
|
}
|
|
}
|
|
|