From cc52f6285c977bf8c5265c2c75630c6b0582652f Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 6 May 2025 21:16:54 +0200 Subject: [PATCH 1/2] add the ability to get umpire data from tenup --- .../Data/Federal/FederalTournament.swift | 2 +- .../Utils/Network/NetworkFederalService.swift | 46 +++++++ .../Views/Cashier/Event/EventLinksView.swift | 9 ++ .../Cashier/Event/EventSettingsView.swift | 8 ++ .../Navigation/Agenda/EventListView.swift | 125 ++++++++++++++---- .../Agenda/TournamentSubscriptionView.swift | 30 ++++- .../Views/Navigation/Umpire/UmpireView.swift | 2 +- .../TournamentGeneralSettingsView.swift | 2 +- .../Shared/TournamentCellView.swift | 31 ++++- 9 files changed, 221 insertions(+), 34 deletions(-) diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index debddba..5feb4e1 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -253,7 +253,7 @@ struct FederalTournament: Identifiable, Codable { } func umpireLabel() -> String { - [jugeArbitre?.nom, jugeArbitre?.prenom].compactMap({$0}).joined(separator: " ") + [jugeArbitre?.nom, jugeArbitre?.prenom].compactMap({$0}).map({ $0.lowercased().capitalized }).joined(separator: " ") } func phoneLabel() -> String { diff --git a/PadelClub/Utils/Network/NetworkFederalService.swift b/PadelClub/Utils/Network/NetworkFederalService.swift index 526190a..66d013d 100644 --- a/PadelClub/Utils/Network/NetworkFederalService.swift +++ b/PadelClub/Utils/Network/NetworkFederalService.swift @@ -275,4 +275,50 @@ recherche_type=\(searchType)&ville%5Bautocomplete%5D%5Bcountry%5D=fr&ville%5Baut return try await runTenupTask(request: request) } + 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) + } + } diff --git a/PadelClub/Views/Cashier/Event/EventLinksView.swift b/PadelClub/Views/Cashier/Event/EventLinksView.swift index 7128e11..71143d5 100644 --- a/PadelClub/Views/Cashier/Event/EventLinksView.swift +++ b/PadelClub/Views/Cashier/Event/EventLinksView.swift @@ -31,6 +31,15 @@ struct EventLinksView: View { var body: some View { List { + + if let tenupId = event.tenupId { + Section { + Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(tenupId)")!) { + Label("Voir sur Tenup", systemImage: "tennisball") + } + } + } + Section { let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings] Picker(selection: $pageLink) { diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index 876262c..49dd216 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -135,6 +135,14 @@ struct EventSettingsView: View { }) .toolbarBackground(.visible, for: .navigationBar) .toolbar { + if let tenupId = event.tenupId { + ToolbarItem(placement: .topBarTrailing) { + Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(tenupId)")!) { + Text("Tenup") + } + } + } + if focusedField != nil { ToolbarItem(placement: .keyboard) { HStack { diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index b2a20f5..837d6ae 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -19,6 +19,7 @@ struct EventListView: View { let sortAscending: Bool @State var showUserSearch: Bool = false + @State private var sectionImporting: Int? = nil var lastDataSource: Date? { guard let _lastDataSource = dataStore.appSettings.lastDataSource else { return nil } @@ -49,9 +50,19 @@ struct EventListView: View { if let pcTournaments = _tournaments as? [Tournament] { _menuOptions(pcTournaments) } else if let federalTournaments = _tournaments as? [FederalTournament], navigation.agendaDestination == .tenup { - FooterButtonView("Tout récupérer", role: .destructive) { - federalTournaments.forEach { federalTournament in - _importFederalTournamentBatch(federalTournament: federalTournament) + HStack { + FooterButtonView("Tout récupérer", role: .destructive) { + Task { + sectionImporting = sectionIndex + for federalTournament in federalTournaments { + await _importFederalTournamentBatch(federalTournament: federalTournament) + } + sectionImporting = nil + } + } + if sectionImporting == sectionIndex { + Spacer() + ProgressView() } } } @@ -293,6 +304,32 @@ struct EventListView: View { } label: { Text("Utiliser les réglages par défaut") } + + Button { + Task { + await pcTournaments.concurrentForEach { tournament in + if let tenupId = tournament.eventObject()?.tenupId { + let umpireData = try? await NetworkFederalService.shared.getUmpireData(idTournament: tenupId) + if let email = umpireData?.email { + tournament.umpireCustomMail = email + } + if let name = umpireData?.name { + tournament.umpireCustomContact = name.lowercased().capitalized + } + if let phone = umpireData?.phone { + tournament.umpireCustomPhone = phone + } + } + } + + await MainActor.run { + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } + } + } label: { + Text("Récuperer via Tenup") + } + } label: { Text("Informations de contact Juge-Arbitre") } @@ -406,32 +443,72 @@ struct EventListView: View { return dataStore.events.first(where: { $0.tenupId == tournament.id.string }) } - private func _importFederalTournamentBatch(federalTournament: FederalTournament) { + private func _importFederalTournamentBatch(federalTournament: FederalTournament) async { let templateTournament = Tournament.getTemplateTournament() - let newTournaments = federalTournament.tournaments.compactMap { tournament in - _create(federalTournament: federalTournament, existingTournament: _event(of: federalTournament)?.existingBuild(tournament), build: tournament, templateTournament: templateTournament) + let newTournaments = await withTaskGroup(of: Tournament?.self) { group in + var tournaments: [Tournament] = [] + + for tournament in federalTournament.tournaments { + group.addTask { + await self._create( + federalTournament: federalTournament, + existingTournament: self._event(of: federalTournament)?.existingBuild(tournament), + build: tournament, + templateTournament: templateTournament + ) + } + } + + for await tournament in group { + if let tournament = tournament { + tournaments.append(tournament) + } + } + + return tournaments } + dataStore.tournaments.addOrUpdate(contentOfs: newTournaments) } - - private func _create(federalTournament: FederalTournament, existingTournament: Tournament?, build: any TournamentBuildHolder, templateTournament: Tournament?) -> Tournament? { - if existingTournament == nil { - let event = federalTournament.getEvent() - let newTournament = Tournament.newEmptyInstance() - newTournament.event = event.id - //todo - //newTournament.umpireMail() - //newTournament.jsonData = jsonData - newTournament.tournamentLevel = build.level - newTournament.tournamentCategory = build.category - newTournament.federalTournamentAge = build.age - newTournament.dayDuration = federalTournament.dayDuration - newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) - newTournament.initSettings(templateTournament: templateTournament) - return newTournament - } else { - return nil + + private func _create(federalTournament: FederalTournament, existingTournament: Tournament?, build: any TournamentBuildHolder, templateTournament: Tournament?) async -> Tournament? { + guard existingTournament == nil else { return nil } + + let event = federalTournament.getEvent() + let newTournament = Tournament.newEmptyInstance() + newTournament.event = event.id + //todo + //newTournament.jsonData = jsonData + newTournament.tournamentLevel = build.level + newTournament.tournamentCategory = build.category + newTournament.federalTournamentAge = build.age + newTournament.dayDuration = federalTournament.dayDuration + newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) + newTournament.initSettings(templateTournament: templateTournament) + + if federalTournament.umpireLabel().isEmpty == false { + newTournament.umpireCustomContact = federalTournament.umpireLabel() + } + if federalTournament.mailLabel().isEmpty == false { + newTournament.umpireCustomMail = federalTournament.mailLabel() } + + do { + let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id) + if let email = umpireData.email { + newTournament.umpireCustomMail = email + } + if let name = umpireData.name { + newTournament.umpireCustomContact = name.lowercased().capitalized + } + if let phone = umpireData.phone { + newTournament.umpireCustomPhone = phone + } + } catch { + Logger.error(error) + } + + return newTournament } } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index a07a145..d57b02a 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -21,6 +21,7 @@ struct TournamentSubscriptionView: View { @State private var sentError: ContactManagerError? = nil @State private var didSendMessage: Bool = false @State private var didSaveInCalendar: Bool = false + @State private var phoneNumber: String? = nil init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: CustomUser) { self.federalTournament = federalTournament @@ -106,9 +107,15 @@ struct TournamentSubscriptionView: View { LabeledContent("Mail") { Text(federalTournament.mailLabel()) } - LabeledContent("Téléphone") { + LabeledContent("Téléphone Club") { Text(federalTournament.phoneLabel()) } + + if let phoneNumber { + LabeledContent("Téléphone JAP") { + Text(phoneNumber) + } + } } header: { Text("Informations") } @@ -156,6 +163,9 @@ struct TournamentSubscriptionView: View { CopyPasteButtonView(pasteValue: messageBody) } } + .task { + self.phoneNumber = try? await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id).phone + } .toolbarBackground(.visible, for: .bottomBar) .toolbarBackground(.visible, for: .navigationBar) .overlay(alignment: .bottom) { @@ -176,7 +186,7 @@ struct TournamentSubscriptionView: View { } } - if let installation = federalTournament.installation, let telephone = installation.telephone { + if let telephone = phoneNumber { if telephone.isMobileNumber() { Section { RowButtonView("S'inscrire par message", systemImage: "message") { @@ -187,10 +197,24 @@ struct TournamentSubscriptionView: View { let number = telephone.replacingOccurrences(of: " ", with: "") if let url = URL(string: "tel:\(number)") { Link(destination: url) { - Label("Appeler", systemImage: "phone") + Label("Appeler le JAP", systemImage: "phone") + } + } + } + if let installation = federalTournament.installation, let telephone = installation.telephone { + Section { + RowButtonView("Contacter le club", systemImage: "house.and.flag") { + 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") } } } + } label: { Text("Contact et inscription") } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index cdc6e93..ff0348e 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -499,7 +499,7 @@ struct UmpireView: View { } header: { Text("Juge-arbitre") } footer: { - Text("Ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") + Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index ae55917..0c40db0 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -358,7 +358,7 @@ struct TournamentGeneralSettingsView: View { } header: { Text("Juge-arbitre") } footer: { - Text("Ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") + Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") } } } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index d979a24..fe260cc 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -200,10 +200,33 @@ struct TournamentCellView: View { newTournament.dayDuration = federalTournament.dayDuration newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) newTournament.initSettings(templateTournament: Tournament.getTemplateTournament()) - do { - try dataStore.tournaments.addOrUpdate(instance: newTournament) - } catch { - Logger.error(error) + + if federalTournament.umpireLabel().isEmpty == false { + newTournament.umpireCustomContact = federalTournament.umpireLabel() + } + if federalTournament.mailLabel().isEmpty == false { + newTournament.umpireCustomMail = federalTournament.mailLabel() + } + + Task { + do { + let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id) + if let email = umpireData.email { + newTournament.umpireCustomMail = email + } + if let name = umpireData.name { + newTournament.umpireCustomContact = name.lowercased().capitalized + } + if let phone = umpireData.phone { + newTournament.umpireCustomPhone = phone + } + + await MainActor.run { + dataStore.tournaments.addOrUpdate(instance: newTournament) + } + } catch { + Logger.error(error) + } } } } From 1b00a5cf97febb8abd4a9a39fe1dcf18a1d2eb7a Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 6 May 2025 21:32:01 +0200 Subject: [PATCH 2/2] v1.2.22 b2 --- PadelClub.xcodeproj/project.pbxproj | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index e8a87ae..ece8d23 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3086,7 +3086,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3133,7 +3133,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3251,7 +3251,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3277,7 +3277,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.21; + MARKETING_VERSION = 1.2.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3297,7 +3297,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3322,7 +3322,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.21; + MARKETING_VERSION = 1.2.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3343,7 +3343,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3366,7 +3366,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.21; + MARKETING_VERSION = 1.2.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3386,7 +3386,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3408,7 +3408,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.21; + MARKETING_VERSION = 1.2.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";