diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5cf11b1..5d34828 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -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 = ""; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = ""; }; FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + FFD883782E1E63880004D7DD /* FederalDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalDataService.swift; sourceTree = ""; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; 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 = ""; }; @@ -1743,6 +1747,7 @@ FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */, FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */, FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */, + FFD883782E1E63880004D7DD /* FederalDataService.swift */, ); path = Network; sourceTree = ""; @@ -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 = ""; diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index 0021712..cb214d4 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -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? diff --git a/PadelClub/Utils/Network/FederalDataService.swift b/PadelClub/Utils/Network/FederalDataService.swift new file mode 100644 index 0000000..9cf9bf0 --- /dev/null +++ b/PadelClub/Utils/Network/FederalDataService.swift @@ -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)") + } + } + +} diff --git a/PadelClub/Utils/Network/NetworkFederalService.swift b/PadelClub/Utils/Network/NetworkFederalService.swift index 66d013d..2d09be2 100644 --- a/PadelClub/Utils/Network/NetworkFederalService.swift +++ b/PadelClub/Utils/Network/NetworkFederalService.swift @@ -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 = "\"/> [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 = "
\\s*(\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2})\\s*
" - - 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) } } diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 9bc23a0..593c6c5 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -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 diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index 72c880b..6c54e3a 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -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) { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 5f9410f..3c5130b 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -187,13 +187,26 @@ struct TournamentLookUpView: View { private func _gatherNumbers() { Task { print("Doing.....") - for i in 0..