fix issue with ios 26

sync3
Razmig Sarkissian 2 months ago
parent cb4e2c5ed6
commit 3af1de6ff9
  1. 16
      PadelClub.xcodeproj/project.pbxproj
  2. 104
      PadelClub/Data/Federal/FederalTournament.swift
  3. 3
      PadelClub/PadelClubApp.swift
  4. 74
      PadelClub/Utils/Network/FederalDataService.swift
  5. 2
      PadelClub/Utils/Network/NetworkFederalService.swift
  6. 1
      PadelClub/ViewModel/FederalDataViewModel.swift
  7. 117
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  8. 11
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  9. 75
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  10. 94
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  11. 99
      PadelClub/Views/Navigation/MainView.swift
  12. 4
      PadelClub/Views/Navigation/OnboardingView.swift
  13. 1
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  14. 14
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

@ -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",

@ -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
}
}

@ -248,7 +248,8 @@ struct DownloadNewVersionView: View {
}.padding().background(.logoYellow)
.clipShape(.buttonBorder)
}.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
.foregroundStyle(.logoBackground)
.fontWeight(.medium)
.multilineTextAlignment(.center)

@ -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)")
}
}
}

@ -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")

@ -70,6 +70,7 @@ class FederalDataViewModel {
selectedClubs.removeAll()
dayPeriod = .all
dayDuration = nil
weekdays.removeAll()
id = UUID()
}

@ -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)
}

@ -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)
}

@ -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..<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
// Split into batches of 100
let batchSize = 100
let batches = idIndexPairs.chunked(into: batchSize)
print("Processing \(idIndexPairs.count) tournaments in \(batches.count) batches of \(batchSize)")
// Process each batch sequentially
for (batchIndex, batch) in batches.enumerated() {
print("Starting batch \(batchIndex + 1) of \(batches.count) (\(batch.count) tournaments)")
await withTaskGroup(of: (Int, String?).self) { group in
for (index, tournamentID) in batch {
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
// 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("Completed batch \(batchIndex + 1) of \(batches.count)")
}
print(".....Done")
}
}
@ -271,7 +291,6 @@ struct TournamentLookUpView: View {
federalDataViewModel.searchedFederalTournaments = []
searching = true
requestedToGetAllPages = false
federalDataViewModel.weekdays = dataStore.appSettings.weekdays
federalDataViewModel.searchAttemptCount += 1
federalDataViewModel.dayPeriod = dataStore.appSettings.dayPeriod
federalDataViewModel.dayDuration = dataStore.appSettings.dayDuration
@ -339,7 +358,7 @@ struct TournamentLookUpView: View {
print("count", count, total, tournaments.count, page)
total = count
if tournaments.count < count && page < total / 30 {
if total - tournaments.count > 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))
}
}

@ -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<Content: View>(
@ViewBuilder transform: (Self) -> Content
) -> some View {
if #available(iOS 26.0, *) {
transform(self)
} else {
self
}
}
}

@ -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<Content: View>(
@ViewBuilder content: () -> Content
) -> some View {
if #available(iOS 26.0, *) {
self.tabViewBottomAccessory {
content()
}
} else {
self
}
}
}

@ -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

@ -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")
}

@ -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))

Loading…
Cancel
Save