From 3af1de6ff9759bb15c35bde6b59c38dc44b82dbe Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 25 Sep 2025 18:17:44 +0200 Subject: [PATCH] fix issue with ios 26 --- PadelClub.xcodeproj/project.pbxproj | 16 ++- .../Data/Federal/FederalTournament.swift | 104 +++++++++++++--- PadelClub/PadelClubApp.swift | 3 +- .../Utils/Network/FederalDataService.swift | 74 +---------- .../Utils/Network/NetworkFederalService.swift | 2 +- .../ViewModel/FederalDataViewModel.swift | 1 + .../Navigation/Agenda/ActivityView.swift | 117 ++++++++++++------ .../Navigation/Agenda/CalendarView.swift | 11 +- .../Agenda/TournamentLookUpView.swift | 75 +++++++---- .../Agenda/TournamentSubscriptionView.swift | 94 +++++++++----- PadelClub/Views/Navigation/MainView.swift | 99 ++++++++++++++- .../Views/Navigation/OnboardingView.swift | 4 + .../Navigation/Toolbox/ToolboxView.swift | 1 + .../Shared/TournamentCellView.swift | 14 ++- 14 files changed, 420 insertions(+), 195 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 57ea32a..e40ee47 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3139,6 +3139,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3155,7 +3156,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3185,6 +3186,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3201,7 +3203,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3304,6 +3306,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; @@ -3320,7 +3323,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3349,6 +3352,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; @@ -3365,7 +3369,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3409,7 +3413,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3451,7 +3455,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index cb214d4..9a3c99b 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -10,7 +10,7 @@ import PadelClubData // MARK: - FederalTournament -struct FederalTournament: Identifiable, Codable { +struct FederalTournament: Identifiable, Codable, Hashable { func getEvent() -> Event { let club = DataStore.shared.user.clubsObjects().first(where: { $0.code == codeClub }) @@ -313,7 +313,7 @@ extension FederalTournament: FederalTournamentHolder { } // MARK: - CategorieAge -struct CategorieAge: Codable { +struct CategorieAge: Codable, Hashable { var ageJoueurMin, ageMin, ageJoueurMax, ageRechercheMax: Int? var categoriesAgeTypePratique: [CategoriesAgeTypePratique]? var ageMax: Int? @@ -335,18 +335,18 @@ struct CategorieAge: Codable { } // MARK: - CategoriesAgeTypePratique -struct CategoriesAgeTypePratique: Codable { +struct CategoriesAgeTypePratique: Codable, Hashable { var id: ID? } // MARK: - ID -struct ID: Codable { +struct ID: Codable, Hashable { var typePratique: String? var idCategorieAge: Int? } // MARK: - CategorieTournoi -struct CategorieTournoi: Codable { +struct CategorieTournoi: Codable, Hashable { var code, codeTaxe: String? var compteurGda: CompteurGda? var libelle, niveauHierarchique: String? @@ -354,14 +354,14 @@ struct CategorieTournoi: Codable { } // MARK: - CompteurGda -struct CompteurGda: Codable { +struct CompteurGda: Codable, Hashable { var classementMax: Classement? var libelle: String? var classementMin: Classement? } // MARK: - Classement -struct Classement: Codable { +struct Classement: Codable, Hashable { var nature, libelle: String? var serie: Serie? var sexe: String? @@ -371,7 +371,7 @@ struct Classement: Codable { } // MARK: - Serie -struct Serie: Codable { +struct Serie: Codable, Hashable { var code, libelle: String? var valide: Bool? var sexe: String? @@ -382,7 +382,7 @@ struct Serie: Codable { } // MARK: - Epreuve -struct Epreuve: Codable { +struct Epreuve: Codable, Hashable { var inscriptionEnLigneEnCours: Bool? var categorieAge: CategorieAge? var typeEpreuve: TypeEpreuve? @@ -419,7 +419,7 @@ struct Epreuve: Codable { } // MARK: - TypeEpreuve -struct TypeEpreuve: Codable { +struct TypeEpreuve: Codable, Hashable { let code: String? let delai: Int? let libelle: String? @@ -437,12 +437,12 @@ struct TypeEpreuve: Codable { } // MARK: - BorneAnneesNaissance -struct BorneAnneesNaissance: Codable { +struct BorneAnneesNaissance: Codable, Hashable { var min, max: Int? } // MARK: - Installation -struct Installation: Codable { +struct Installation: Codable, Hashable { var ville: String? var lng: Double? var surfaces: [JSONAny]? @@ -457,7 +457,7 @@ struct Installation: Codable { } // MARK: - JugeArbitre -struct JugeArbitre: Codable { +struct JugeArbitre: Codable, Hashable { var idCRM, id: Int? var nom, prenom: String? @@ -468,7 +468,7 @@ struct JugeArbitre: Codable { } // MARK: - ModeleDeBalle -struct ModeleDeBalle: Codable { +struct ModeleDeBalle: Codable, Hashable { var libelle: String? var marqueDeBalle: MarqueDeBalle? var id: Int? @@ -476,7 +476,7 @@ struct ModeleDeBalle: Codable { } // MARK: - MarqueDeBalle -struct MarqueDeBalle: Codable { +struct MarqueDeBalle: Codable, Hashable { var id: Int? var valide: Bool? var marque: String? @@ -529,9 +529,13 @@ class JSONCodingKey: CodingKey { } } -class JSONAny: Codable { +class JSONAny: Codable, Hashable, Equatable { - let value: Any + var value: Any + + init() { + self.value = () + } static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") @@ -722,4 +726,70 @@ class JSONAny: Codable { try JSONAny.encode(to: &container, value: self.value) } } + + public static func == (lhs: JSONAny, rhs: JSONAny) -> Bool { + switch (lhs.value, rhs.value) { + case (let l as Bool, let r as Bool): return l == r + case (let l as Int64, let r as Int64): return l == r + case (let l as Double, let r as Double): return l == r + case (let l as String, let r as String): return l == r + case (let l as JSONNull, let r as JSONNull): return true + case (let l as [Any], let r as [Any]): + guard l.count == r.count else { return false } + return zip(l, r).allSatisfy { (a, b) in + // Recursively wrap in JSONAny for comparison + JSONAny(value: a) == JSONAny(value: b) + } + case (let l as [String: Any], let r as [String: Any]): + guard l.count == r.count else { return false } + for (key, lVal) in l { + guard let rVal = r[key], JSONAny(value: lVal) == JSONAny(value: rVal) else { return false } + } + return true + default: + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch value { + case let v as Bool: + hasher.combine(0) + hasher.combine(v) + case let v as Int64: + hasher.combine(1) + hasher.combine(v) + case let v as Double: + hasher.combine(2) + hasher.combine(v) + case let v as String: + hasher.combine(3) + hasher.combine(v) + case is JSONNull: + hasher.combine(4) + case let v as [Any]: + hasher.combine(5) + for elem in v { + JSONAny(value: elem).hash(into: &hasher) + } + case let v as [String: Any]: + hasher.combine(6) + // Order of hashing dictionary keys shouldn't matter + for key in v.keys.sorted() { + hasher.combine(key) + if let val = v[key] { + JSONAny(value: val).hash(into: &hasher) + } + } + default: + hasher.combine(-1) + } + } + + // Helper init for internal use + convenience init(value: Any) { + self.init() + self.value = value + } } + diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 5ec6606..182c68d 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -248,7 +248,8 @@ struct DownloadNewVersionView: View { }.padding().background(.logoYellow) .clipShape(.buttonBorder) - }.frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity) .foregroundStyle(.logoBackground) .fontWeight(.medium) .multilineTextAlignment(.center) diff --git a/PadelClub/Utils/Network/FederalDataService.swift b/PadelClub/Utils/Network/FederalDataService.swift index 9cf9bf0..97442eb 100644 --- a/PadelClub/Utils/Network/FederalDataService.swift +++ b/PadelClub/Utils/Network/FederalDataService.swift @@ -240,7 +240,8 @@ class FederalDataService { 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) + 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) @@ -275,7 +276,8 @@ class FederalDataService { // 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) + 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) @@ -297,72 +299,4 @@ class FederalDataService { 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 2d09be2..7f76fc5 100644 --- a/PadelClub/Utils/Network/NetworkFederalService.swift +++ b/PadelClub/Utils/Network/NetworkFederalService.swift @@ -93,7 +93,7 @@ class NetworkFederalService { //"geocoding%5Bcountry%5D=fr&geocoding%5Bville%5D=13%20Avenue%20Emile%20Bodin%2013260%20Cassis&geocoding%5Brayon%5D=15&geocoding%5BuserPosition%5D%5Blat%5D=43.22278594081477&geocoding%5BuserPosition%5D%5Blng%5D=5.556953900769194&geocoding%5BuserPosition%5D%5BshowDistance%5D=true&nombreResultat=0&diplomeEtatOption=false&galaxieOption=false&fauteuilOption=false&tennisSanteOption=false" let postData = parameters.data(using: .utf8) - var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/clubs/ajax")!,timeoutInterval: Double.infinity) + var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/clubs/ajax")!) request.addValue("application/json, text/plain, */*", 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") diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 4297c96..30d5f74 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -70,6 +70,7 @@ class FederalDataViewModel { selectedClubs.removeAll() dayPeriod = .all dayDuration = nil + weekdays.removeAll() id = UUID() } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 25573f3..8b444ae 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -224,6 +224,12 @@ struct ActivityView: View { .navigationDestination(for: Tournament.self) { tournament in TournamentView(tournament: tournament) } + .navigationDestination(for: SubScreen.self) { build in + switch build { + case .subscription(let federalTournament, let build): + TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) + } + } // .onDisappear(perform: { // pasteButtonIsDisplayed = nil // print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed) @@ -231,15 +237,26 @@ struct ActivityView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { if #available(iOS 26.0, *) { - Button("Vue calendrier", systemImage: "calendar") { - switch viewStyle { - case .list: - viewStyle = .calendar - case .calendar: - viewStyle = .list + if viewStyle == .calendar { + Button("Vue calendrier", systemImage: "calendar") { + switch viewStyle { + case .list: + viewStyle = .calendar + case .calendar: + viewStyle = .list + } + } + .buttonStyle(.borderedProminent) + } else { + Button("Vue calendrier", systemImage: "calendar") { + switch viewStyle { + case .list: + viewStyle = .calendar + case .calendar: + viewStyle = .list + } } } - .symbolVariant(viewStyle == .calendar ? .fill : .none) } else { Button { switch viewStyle { @@ -265,11 +282,16 @@ struct ActivityView: View { ToolbarItem(placement: .topBarLeading) { if #available(iOS 26.0, *) { - - Button("Filtre", systemImage: "line.3.horizontal.decrease") { - presentFilterView.toggle() + if federalDataViewModel.areFiltersEnabled() { + Button("Filtre", systemImage: "line.3.horizontal.decrease") { + presentFilterView.toggle() + } + .buttonStyle(.borderedProminent) + } else { + Button("Filtre", systemImage: "line.3.horizontal.decrease") { + presentFilterView.toggle() + } } - .symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) } else { Button { presentFilterView.toggle() @@ -308,35 +330,10 @@ struct ActivityView: View { } } - if tournaments.isEmpty == false, federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around { - ToolbarItemGroup(placement: .bottomBar) { - VStack(spacing: 0) { - let searchStatus = _searchStatus() - if searchStatus.isEmpty == false { - Text(_searchStatus()) - .font(.footnote) - .foregroundStyle(.secondary) - } - - HStack { - if navigation.agendaDestination == .around { - FooterButtonView("modifier votre recherche") { - displaySearchView = true - } - - if federalDataViewModel.areFiltersEnabled() { - Text("ou") - } - } - - if federalDataViewModel.areFiltersEnabled() { - FooterButtonView(_filterButtonTitle()) { - presentFilterView = true - } - - } - } - .padding(.bottom, 8) + if #unavailable(iOS 26.0) { + if _shouldDisplaySearchStatus() { + ToolbarItemGroup(placement: .bottomBar) { + _searchBoxView() } } } @@ -446,6 +443,41 @@ struct ActivityView: View { } } + private func _shouldDisplaySearchStatus() -> Bool { + tournaments.isEmpty == false && (federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around) + } + + private func _searchBoxView() -> some View { + VStack(spacing: 0) { + let searchStatus = _searchStatus() + if searchStatus.isEmpty == false { + Text(_searchStatus()) + .font(.footnote) + .foregroundStyle(.secondary) + } + + HStack { + if navigation.agendaDestination == .around { + FooterButtonView("modifier votre recherche") { + displaySearchView = true + } + + if federalDataViewModel.areFiltersEnabled() { + Text("ou") + } + } + + if federalDataViewModel.areFiltersEnabled() { + FooterButtonView(_filterButtonTitle()) { + presentFilterView = true + } + + } + } + .padding(.bottom, 8) + } + } + private func _searchStatus() -> String { var searchStatus : [String] = [] if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { @@ -623,3 +655,8 @@ struct ActivityView: View { //#Preview { // ActivityView() //} + +enum SubScreen: Hashable { + case subscription(FederalTournament, TournamentBuild) +} + diff --git a/PadelClub/Views/Navigation/Agenda/CalendarView.swift b/PadelClub/Views/Navigation/Agenda/CalendarView.swift index a3a7d81..cf96a94 100644 --- a/PadelClub/Views/Navigation/Agenda/CalendarView.swift +++ b/PadelClub/Views/Navigation/Agenda/CalendarView.swift @@ -95,10 +95,15 @@ struct CalendarView: View { if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) { if navigation.agendaDestination == .around { - NavigationLink(build.buildHolderTitle(.wide)) { - TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user) + + if #available(iOS 26.0, *) { + NavigationLink(build.buildHolderTitle(.wide), value: SubScreen.subscription(tournament, build as! TournamentBuild)) + } else { + NavigationLink(build.buildHolderTitle(.wide)) { + TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user) + } } - } else { + } else { Button(build.buildHolderTitle(.wide)) { _createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build) } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 102f900..df86047 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -173,8 +173,7 @@ struct TournamentLookUpView: View { 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") + Text("\(total) tournois en cours de récupération") .font(.caption) } Spacer() @@ -211,6 +210,7 @@ struct TournamentLookUpView: View { revealSearchParameters = true federalDataViewModel.searchedFederalTournaments = [] federalDataViewModel.searchAttemptCount = 0 + federalDataViewModel.removeFilters() } label: { Text("Ré-initialiser la recherche") } @@ -239,26 +239,46 @@ struct TournamentLookUpView: View { private func _gatherNumbers() { Task { print("Doing.....") + let tournamentsToFetch = tournaments.enumerated().filter { (idx, tournament) in + tournament.japPhoneNumber == nil || tournament.japPhoneNumber?.isEmpty == true + } + let idIndexPairs: [(Int, String)] = tournamentsToFetch.map { ($0.offset, $0.element.id) } + let tournamentIDs: [String] = idIndexPairs.map { $0.1 } + guard !tournamentIDs.isEmpty else { + print("All numbers already gathered.") + return + } - await withTaskGroup(of: (Int, String?).self) { group in - for i in 0.. count / 50 && page < total / 30 { if total < 200 || requestedToGetAllPages { page += 1 await getNewPage() @@ -395,8 +414,18 @@ struct TournamentLookUpView: View { appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth.nextDay.endOfMonth } } - DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date) - DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date) + DatePicker(selection: $appSettings.startDate, displayedComponents: .date) { + Text("Début") + .onTapGesture(count: 2) { + appSettings.startDate = appSettings.startDate.startOfCurrentMonth + } + } + DatePicker(selection: $appSettings.endDate, displayedComponents: .date) { + Text("Fin") + .onTapGesture(count: 2) { + appSettings.endDate = appSettings.endDate.nextDay.endOfMonth + } + } Picker(selection: $appSettings.dayDuration) { Text("Aucune").tag(nil as Int?) Text(1.formatted()).tag(1 as Int?) @@ -406,7 +435,8 @@ struct TournamentLookUpView: View { Text("Durée souhaitée (en jours)") } - WeekdayselectionView(weekdays: $appSettings.weekdays) + @Bindable var federalDataViewModel = federalDataViewModel + WeekdayselectionView(weekdays: $federalDataViewModel.weekdays) Picker(selection: $appSettings.dayPeriod) { ForEach(DayPeriod.allCases) { @@ -449,7 +479,6 @@ struct TournamentLookUpView: View { } .symbolVariant(.fill) .foregroundColor (Color.white) - .cornerRadius (20) .font(.system(size: 12)) } } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index d57b02a..1c27a96 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -22,6 +22,7 @@ struct TournamentSubscriptionView: View { @State private var didSendMessage: Bool = false @State private var didSaveInCalendar: Bool = false @State private var phoneNumber: String? = nil + @State private var errorWhenGatheringPhone: Bool = false init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: CustomUser) { self.federalTournament = federalTournament @@ -111,9 +112,13 @@ struct TournamentSubscriptionView: View { Text(federalTournament.phoneLabel()) } - if let phoneNumber { - LabeledContent("Téléphone JAP") { + LabeledContent("Téléphone JAP") { + if let phoneNumber { Text(phoneNumber) + } else if errorWhenGatheringPhone == false { + ProgressView() + } else { + Image(systemName: "exclamationmark.triangle") } } } header: { @@ -163,8 +168,15 @@ struct TournamentSubscriptionView: View { CopyPasteButtonView(pasteValue: messageBody) } } + .ifAvailableiOS26 { view in + view.toolbar(.hidden, for: .tabBar) + } .task { - self.phoneNumber = try? await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id).phone + do { + self.phoneNumber = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id).phone + } catch { + self.errorWhenGatheringPhone = true + } } .toolbarBackground(.visible, for: .bottomBar) .toolbarBackground(.visible, for: .navigationBar) @@ -176,51 +188,61 @@ struct TournamentSubscriptionView: View { } } .toolbar(content: { - ToolbarItem(placement: .status) { + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .bottomBar) + } + ToolbarItem(placement: .bottomBar) { Menu { - if let courrielEngagement = federalTournament.courrielEngagement { - Section { - RowButtonView("S'inscrire par email", systemImage: "envelope") { + Menu { + if let courrielEngagement = federalTournament.courrielEngagement { + Button("Email", systemImage: "envelope") { contactType = .mail(date: nil, recipients: [courrielEngagement], bccRecipients: nil, body: messageBody, subject: messageSubject, tournamentBuild: build as? TournamentBuild) } } - } - - if let telephone = phoneNumber { - if telephone.isMobileNumber() { - Section { - RowButtonView("S'inscrire par message", systemImage: "message") { + + if let telephone = phoneNumber { + if telephone.isMobileNumber() { + Button("Message", systemImage: "message") { contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild) } } - } - let number = telephone.replacingOccurrences(of: " ", with: "") - if let url = URL(string: "tel:\(number)") { - Link(destination: url) { - Label("Appeler le JAP", systemImage: "phone") + let number = telephone.replacingOccurrences(of: " ", with: "") + if let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler le JAP", systemImage: "phone") + } } } + } label: { + Label("Inscription", systemImage: "pencil.and.list.clipboard") } - if let installation = federalTournament.installation, let telephone = installation.telephone { - Section { - RowButtonView("Contacter le club", systemImage: "house.and.flag") { + Menu { + if let installation = federalTournament.installation, let telephone = installation.telephone { + Button("Email", systemImage: "envelope") { contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild) } - } - let number = telephone.replacingOccurrences(of: " ", with: "") - if let url = URL(string: "tel:\(number)") { - Link(destination: url) { - Label("Appeler le club", systemImage: "phone") + let number = telephone.replacingOccurrences(of: " ", with: "") + if let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler", systemImage: "phone") + } } } + } label: { + Label("Contacter le club", systemImage: "house.and.flag") } - + } label: { - Text("Contact et inscription") + Text("S'inscrire") + .foregroundStyle(.white) + .frame(maxWidth: .infinity) } .menuStyle(.button) .buttonStyle(.borderedProminent) - .offset(y:-2) + } + + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .bottomBar) } ToolbarItem(placement: .topBarTrailing) { @@ -361,3 +383,17 @@ struct TournamentSubscriptionView: View { } } + +extension View { + /// Runs a transform only on iOS 26+, otherwise returns self + @ViewBuilder + func ifAvailableiOS26( + @ViewBuilder transform: (Self) -> Content + ) -> some View { + if #available(iOS 26.0, *) { + transform(self) + } else { + self + } + } +} diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 209a81f..003a22a 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -17,11 +17,14 @@ struct MainView: View { @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(ImportObserver.self) private var importObserver: ImportObserver + @State private var federalDataViewModel: FederalDataViewModel = FederalDataViewModel.shared @State private var mainViewId: UUID = UUID() @State private var presentOnboarding: Bool = false @State private var canPresentOnboarding: Bool = false - + @State private var presentFilterView: Bool = false + @State private var displaySearchView: Bool = false + @AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false var lastDataSource: String? { @@ -94,6 +97,23 @@ struct MainView: View { // PadelClubView() // .tabItem(for: .padelClub) } + .applyTabViewBottomAccessory(content: { + if (navigation.selectedTab == .activity || navigation.selectedTab == nil) && _shouldDisplaySearchStatus() { + _searchBoxView() + } + }) + .sheet(isPresented: $presentFilterView) { + TournamentFilterView(federalDataViewModel: federalDataViewModel) + .environment(navigation) + .tint(.master) + } + .sheet(isPresented: $displaySearchView) { + NavigationStack { + TournamentLookUpView() + .environment(federalDataViewModel) + .environment(navigation) + } + } .onAppear { if canPresentOnboarding || StoreCenter.main.userId != nil { if didSeeOnboarding == false { @@ -264,8 +284,85 @@ struct MainView: View { } } } + + private func _searchStatus() -> String { + var searchStatus : [String] = [] + if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { + let filteredSearchedFederalTournaments = federalDataViewModel.filteredSearchedFederalTournaments + + let status : String = filteredSearchedFederalTournaments.count.formatted() + " tournoi" + filteredSearchedFederalTournaments.count.pluralSuffix + searchStatus.append(status) + } + + if federalDataViewModel.areFiltersEnabled() { + searchStatus.append(federalDataViewModel.filterStatus()) + } + + return searchStatus.joined(separator: " ") + } + + + private func _shouldDisplaySearchStatus() -> Bool { + guard navigation.path.count == 0 else { return false } + return federalDataViewModel.areFiltersEnabled() || (navigation.agendaDestination == .around && federalDataViewModel.searchedFederalTournaments.isEmpty == false) + } + + private func _searchBoxView() -> some View { + VStack(spacing: 0) { + let searchStatus = _searchStatus() + if searchStatus.isEmpty == false { + Text(_searchStatus()) + .font(.footnote) + .foregroundStyle(.secondary) + } + + HStack { + if navigation.agendaDestination == .around { + FooterButtonView("modifier votre recherche") { + displaySearchView = true + } + + if federalDataViewModel.areFiltersEnabled() { + Text("ou") + } + } + + if federalDataViewModel.areFiltersEnabled() { + FooterButtonView(_filterButtonTitle()) { + presentFilterView = true + } + + } + } + } + } + + private func _filterButtonTitle() -> String { + var prefix = "modifier " + if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { + prefix = "" + } + return prefix + "vos filtres" + } + + } //#Preview { // MainView() //} + +fileprivate extension View { + @ViewBuilder + func applyTabViewBottomAccessory( + @ViewBuilder content: () -> Content + ) -> some View { + if #available(iOS 26.0, *) { + self.tabViewBottomAccessory { + content() + } + } else { + self + } + } +} diff --git a/PadelClub/Views/Navigation/OnboardingView.swift b/PadelClub/Views/Navigation/OnboardingView.swift index b167b9c..d6d7869 100644 --- a/PadelClub/Views/Navigation/OnboardingView.swift +++ b/PadelClub/Views/Navigation/OnboardingView.swift @@ -59,6 +59,10 @@ struct OnboardingView: View { dismiss() navigation.agendaDestination = .around }), + ("Accès au classement mensuel", { + dismiss() + navigation.selectedTab = .toolbox + }), ("Calculateur de points", { dismiss() navigation.selectedTab = .toolbox diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index c867526..df94d1c 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -65,6 +65,7 @@ struct ToolboxView: View { Section { NavigationLink { SelectablePlayerListView(isPresented: false, lastDataSource: true) + .toolbar(.hidden, for: .tabBar) } label: { Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index fe260cc..ae86dda 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -28,10 +28,16 @@ struct TournamentCellView: View { if let federalTournament = tournament as? FederalTournament { if FederalDataViewModel.shared.isFederalTournamentValidForFilters(federalTournament, build: build) { if navigation.agendaDestination == .around { - NavigationLink { - TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) - } label: { - _buildView(build, existingTournament: event?.existingBuild(build)) + if #available(iOS 26.0, *) { + NavigationLink(value: SubScreen.subscription(federalTournament, build as! TournamentBuild)) { + _buildView(build, existingTournament: event?.existingBuild(build)) + } + } else { + NavigationLink { + TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) + } label: { + _buildView(build, existingTournament: event?.existingBuild(build)) + } } } else { _buildView(build, existingTournament: event?.existingBuild(build))