sync3
Laurent 6 months ago
commit 5d25f21ebb
  1. 16
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/Federal/FederalTournament.swift
  3. 46
      PadelClub/Utils/Network/NetworkFederalService.swift
  4. 9
      PadelClub/Views/Cashier/Event/EventLinksView.swift
  5. 8
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  6. 125
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  7. 30
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  8. 2
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  9. 2
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  10. 31
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

@ -3259,7 +3259,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\"";
@ -3285,7 +3285,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 = "";
@ -3305,7 +3305,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;
@ -3330,7 +3330,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 = "";
@ -3351,7 +3351,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\"";
@ -3374,7 +3374,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 = "";
@ -3394,7 +3394,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;
@ -3416,7 +3416,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 = "";

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

@ -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 = "<div class=\"details-bloc\">\\s*(\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2}\\s+\\d{2})\\s*</div>"
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)
}
}

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

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

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

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

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

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

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

Loading…
Cancel
Save