add contact info

newoffer2025
Razmig Sarkissian 4 months ago
parent 69c0163ccb
commit 331103c4c0
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 59
      PadelClub/Data/Federal/FederalPlayer.swift
  3. 16
      PadelClub/Extensions/MonthData+Extensions.swift
  4. 11
      PadelClub/Extensions/TeamRegistration+Extensions.swift
  5. 59
      PadelClub/Utils/PhoneNumbersUtils.swift
  6. 18
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  7. 20
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  8. 113
      PadelClub/Views/Player/PlayerDetailView.swift
  9. 6
      PadelClubTests/ServerDataTests.swift

@ -650,6 +650,9 @@
FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; };
FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; };
FF8044AC2C8F676D00A49A52 /* TournamentSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */; }; FF8044AC2C8F676D00A49A52 /* TournamentSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */; };
FF81F1BC2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */; };
FF81F1BD2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */; };
FF81F1BE2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */; };
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; }; FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; };
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; }; FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; };
FF8E1CE62C006E0200184680 /* Alphabet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8E1CE52C006E0200184680 /* Alphabet.swift */; }; FF8E1CE62C006E0200184680 /* Alphabet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8E1CE52C006E0200184680 /* Alphabet.swift */; };
@ -1058,6 +1061,7 @@
FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageDatePickingView.swift; sourceTree = "<group>"; }; FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageDatePickingView.swift; sourceTree = "<group>"; };
FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = "<group>"; }; FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = "<group>"; };
FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = "<group>"; }; FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = "<group>"; };
FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumbersUtils.swift; sourceTree = "<group>"; };
FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = "<group>"; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = "<group>"; };
FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; }; FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
FF8E1CE52C006E0200184680 /* Alphabet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alphabet.swift; sourceTree = "<group>"; }; FF8E1CE52C006E0200184680 /* Alphabet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alphabet.swift; sourceTree = "<group>"; };
@ -1935,6 +1939,7 @@
FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */,
FF1DC5582BAB767000FD8220 /* Tips.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */,
C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */, C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */,
FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */,
); );
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2432,6 +2437,7 @@
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FFCB74172C480411008384D0 /* CopyPasteButtonView.swift in Sources */, FFCB74172C480411008384D0 /* CopyPasteButtonView.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF81F1BD2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */,
FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */, FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */,
FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */, FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */,
FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */, FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */,
@ -2695,6 +2701,7 @@
FF4CC0252C996C0600151637 /* Labels.swift in Sources */, FF4CC0252C996C0600151637 /* Labels.swift in Sources */,
FF4CC0262C996C0600151637 /* CopyPasteButtonView.swift in Sources */, FF4CC0262C996C0600151637 /* CopyPasteButtonView.swift in Sources */,
FF4CC0272C996C0600151637 /* PlayerSexPickerView.swift in Sources */, FF4CC0272C996C0600151637 /* PlayerSexPickerView.swift in Sources */,
FF81F1BE2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */,
FF4CC0282C996C0600151637 /* TournamentInscriptionView.swift in Sources */, FF4CC0282C996C0600151637 /* TournamentInscriptionView.swift in Sources */,
FF4CC0292C996C0600151637 /* CallSettingsView.swift in Sources */, FF4CC0292C996C0600151637 /* CallSettingsView.swift in Sources */,
FF4CC02A2C996C0600151637 /* FooterButtonView.swift in Sources */, FF4CC02A2C996C0600151637 /* FooterButtonView.swift in Sources */,
@ -2936,6 +2943,7 @@
FF70FBA42C90584900129CC2 /* Labels.swift in Sources */, FF70FBA42C90584900129CC2 /* Labels.swift in Sources */,
FF70FBA52C90584900129CC2 /* CopyPasteButtonView.swift in Sources */, FF70FBA52C90584900129CC2 /* CopyPasteButtonView.swift in Sources */,
FF70FBA62C90584900129CC2 /* PlayerSexPickerView.swift in Sources */, FF70FBA62C90584900129CC2 /* PlayerSexPickerView.swift in Sources */,
FF81F1BC2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */,
FF70FBA72C90584900129CC2 /* TournamentInscriptionView.swift in Sources */, FF70FBA72C90584900129CC2 /* TournamentInscriptionView.swift in Sources */,
FF70FBA82C90584900129CC2 /* CallSettingsView.swift in Sources */, FF70FBA82C90584900129CC2 /* CallSettingsView.swift in Sources */,
FF70FBA92C90584900129CC2 /* FooterButtonView.swift in Sources */, FF70FBA92C90584900129CC2 /* FooterButtonView.swift in Sources */,
@ -3129,7 +3137,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.40; MARKETING_VERSION = 1.2.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3175,7 +3183,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.40; MARKETING_VERSION = 1.2.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -31,54 +31,70 @@ class FederalPlayer: Decodable {
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
/*
"classement": 9,
"evolution": 2,
"nom": "PEREZ LE TIEC",
"prenom": "Pierre",
"meilleurClassement": null,
"nationalite": "FRA",
"ageSportif": 30,
"points": 14210,
"nombreTournoisJoues": 24,
"ligue": "ILE DE FRANCE",
"assimilation": false
*/
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case nom case nom
case prenom case prenom
case licence case licence
case meilleurClassement case meilleurClassement
case nationnalite case nationalite
case anneeNaissance
case codeClub case codeClub
case nomClub case nomClub
case nomLigue case ligue
case rang case classement
case progression case evolution
case points case points
case nombreDeTournois case nombreTournoisJoues
case assimile case assimilation
case ageSportif
} }
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
isMale = (decoder.userInfo[.maleData] as? Bool) == true isMale = (decoder.userInfo[.maleData] as? Bool) == true
let _lastName = try container.decode(String.self, forKey: .nom) let _lastName = try container.decodeIfPresent(String.self, forKey: .nom)
let _firstName = try container.decode(String.self, forKey: .prenom) let _firstName = try container.decodeIfPresent(String.self, forKey: .prenom)
lastName = _lastName lastName = _lastName ?? ""
firstName = _firstName firstName = _firstName ?? ""
if let lic = try? container.decodeIfPresent(Int.self, forKey: .licence) { if let lic = try? container.decodeIfPresent(Int.self, forKey: .licence) {
license = String(lic) license = String(lic)
} else { } else {
license = "" license = ""
} }
let nationnalite = try container.decode(Nationnalite.self, forKey: .nationnalite) country = try container.decodeIfPresent(String.self, forKey: .nationalite) ?? ""
country = nationnalite.code
bestRank = try container.decodeIfPresent(Int.self, forKey: .meilleurClassement) bestRank = try container.decodeIfPresent(Int.self, forKey: .meilleurClassement)
birthYear = try container.decodeIfPresent(Int.self, forKey: .anneeNaissance)
clubCode = try container.decode(String.self, forKey: .codeClub) let ageSportif = try container.decodeIfPresent(Int.self, forKey: .ageSportif)
club = try container.decode(String.self, forKey: .nomClub) if let ageSportif {
ligue = try container.decode(String.self, forKey: .nomLigue) birthYear = Calendar.current.component(.year, from: Date()) - ageSportif
rank = try container.decode(Int.self, forKey: .rang) }
progression = (try? container.decodeIfPresent(Int.self, forKey: .progression)) ?? 0 clubCode = try container.decodeIfPresent(String.self, forKey: .codeClub) ?? ""
club = try container.decodeIfPresent(String.self, forKey: .nomClub) ?? ""
ligue = try container.decodeIfPresent(String.self, forKey: .ligue) ?? ""
rank = try container.decode(Int.self, forKey: .classement)
progression = (try? container.decodeIfPresent(Int.self, forKey: .evolution)) ?? 0
let pointsAsInt = try? container.decodeIfPresent(Int.self, forKey: .points) let pointsAsInt = try? container.decodeIfPresent(Int.self, forKey: .points)
if let pointsAsInt { if let pointsAsInt {
points = Double(pointsAsInt) points = Double(pointsAsInt)
} else { } else {
points = nil points = nil
} }
tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois) tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreTournoisJoues)
let assimile = try container.decode(Bool.self, forKey: .assimile) let assimile = try container.decode(Bool.self, forKey: .assimilation)
assimilation = assimile ? "Oui" : "Non" assimilation = assimile ? "Oui" : "Non"
} }
@ -92,6 +108,7 @@ class FederalPlayer: Decodable {
} }
func formatNumbers(_ input: String) -> String { func formatNumbers(_ input: String) -> String {
if input.isEmpty { return input }
// Insert spaces at appropriate positions // Insert spaces at appropriate positions
let formattedString = insertSeparator(input, separator: " ", every: [2, 4]) let formattedString = insertSeparator(input, separator: " ", every: [2, 4])
return formattedString return formattedString

@ -15,13 +15,22 @@ extension MonthData {
let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == fromDate && $0.index == 0 }) let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == fromDate && $0.index == 0 })
print("calculateCurrentUnrankedValues", fromDate.monthYearFormatted, fileURL?.path()) print("calculateCurrentUnrankedValues", fromDate.monthYearFormatted, fileURL?.path())
let fftImportingUncomplete = fileURL?.fftImportingUncomplete() let fftImportingUncomplete = fileURL?.fftImportingUncomplete()
var fftImportingAnonymous = fileURL?.fftImportingAnonymous()
let fftImportingMaleUnrankValue = fileURL?.fftImportingMaleUnrankValue() let fftImportingMaleUnrankValue = fileURL?.fftImportingMaleUnrankValue()
let femaleFileURL = SourceFileManager.shared.allFiles(false).first(where: { $0.dateFromPath == fromDate && $0.index == 0 })
let femaleFftImportingMaleUnrankValue = femaleFileURL?.fftImportingMaleUnrankValue()
let femaleFftImportingUncomplete = femaleFileURL?.fftImportingUncomplete()
let incompleteMode = fftImportingUncomplete != nil let incompleteMode = fftImportingUncomplete != nil
let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: true) let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: true)
let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: false) let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: false)
let anonymousCount = await FederalPlayer.anonymousCount(mostRecentDateAvailable: fromDate) if fftImportingAnonymous == nil {
fftImportingAnonymous = await FederalPlayer.anonymousCount(mostRecentDateAvailable: fromDate)
}
let anonymousCount: Int? = fftImportingAnonymous
await MainActor.run { await MainActor.run {
let lastDataSource = URL.importDateFormatter.string(from: fromDate) let lastDataSource = URL.importDateFormatter.string(from: fromDate)
let currentMonthData : MonthData = DataStore.shared.monthData.first(where: { $0.monthKey == lastDataSource }) ?? MonthData(monthKey: lastDataSource) let currentMonthData : MonthData = DataStore.shared.monthData.first(where: { $0.monthKey == lastDataSource }) ?? MonthData(monthKey: lastDataSource)
@ -30,11 +39,10 @@ extension MonthData {
currentMonthData.maleUnrankedValue = incompleteMode ? fftImportingMaleUnrankValue : lastDataSourceMaleUnranked?.0 currentMonthData.maleUnrankedValue = incompleteMode ? fftImportingMaleUnrankValue : lastDataSourceMaleUnranked?.0
currentMonthData.incompleteMode = incompleteMode currentMonthData.incompleteMode = incompleteMode
currentMonthData.maleCount = incompleteMode ? fftImportingUncomplete : lastDataSourceMaleUnranked?.1 currentMonthData.maleCount = incompleteMode ? fftImportingUncomplete : lastDataSourceMaleUnranked?.1
currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0 currentMonthData.femaleUnrankedValue = incompleteMode ? femaleFftImportingMaleUnrankValue : lastDataSourceFemaleUnranked?.0
currentMonthData.femaleCount = lastDataSourceFemaleUnranked?.1 currentMonthData.femaleCount = incompleteMode ? femaleFftImportingUncomplete : lastDataSourceFemaleUnranked?.1
currentMonthData.anonymousCount = anonymousCount currentMonthData.anonymousCount = anonymousCount
DataStore.shared.monthData.addOrUpdate(instance: currentMonthData) DataStore.shared.monthData.addOrUpdate(instance: currentMonthData)
} }
} }

@ -39,6 +39,17 @@ extension TeamRegistration {
player.licenceId?.strippedLicense != nil player.licenceId?.strippedLicense != nil
{ {
player.registeredOnline = oldPlayer.registeredOnline player.registeredOnline = oldPlayer.registeredOnline
if player.email?.canonicalVersion != oldPlayer.email?.canonicalVersion {
player.contactEmail = oldPlayer.email
} else {
player.contactEmail = oldPlayer.contactEmail
}
if areFrenchPhoneNumbersSimilar(player.phoneNumber, oldPlayer.phoneNumber) == false {
player.contactPhoneNumber = oldPlayer.phoneNumber
} else {
player.contactPhoneNumber = oldPlayer.contactPhoneNumber
}
player.contactName = oldPlayer.contactName
player.coach = oldPlayer.coach player.coach = oldPlayer.coach
player.tournamentPlayed = oldPlayer.tournamentPlayed player.tournamentPlayed = oldPlayer.tournamentPlayed
player.points = oldPlayer.points player.points = oldPlayer.points

@ -0,0 +1,59 @@
import Foundation
func areFrenchPhoneNumbersSimilar(_ phoneNumber1: String?, _ phoneNumber2: String?) -> Bool {
if phoneNumber1?.canonicalVersion == phoneNumber2?.canonicalVersion {
return true
}
// Helper function to normalize a phone number, now returning an optional String
func normalizePhoneNumber(_ numberString: String?) -> String? {
// 1. Safely unwrap the input string. If it's nil or empty, return nil immediately.
guard let numberString = numberString, !numberString.isEmpty else {
return nil
}
// 2. Remove all non-digit characters
let digitsOnly = numberString.filter(\.isNumber)
// If after filtering, there are no digits, return nil.
guard !digitsOnly.isEmpty else {
return nil
}
// 3. Handle French specific prefixes and extract the relevant part
// We need at least 9 digits to get a meaningful 8-digit comparison from the end
if digitsOnly.count >= 9 {
if digitsOnly.hasPrefix("0") {
return String(digitsOnly.suffix(9))
} else if digitsOnly.hasPrefix("33") {
// Ensure there are enough digits after dropping "33"
if digitsOnly.count >= 11 { // "33" + 9 digits = 11
return String(digitsOnly.dropFirst(2).suffix(9))
} else {
return nil // Not enough digits after dropping "33"
}
} else if digitsOnly.count == 9 { // Case like 612341234
return digitsOnly
} else { // More digits but no 0 or 33 prefix, take the last 9
return String(digitsOnly.suffix(9))
}
}
return nil // If it doesn't fit the expected patterns or is too short
}
// Normalize both phone numbers. If either results in nil, we can't compare.
guard let normalizedNumber1 = normalizePhoneNumber(phoneNumber1),
let normalizedNumber2 = normalizePhoneNumber(phoneNumber2) else {
return false
}
// Ensure both normalized numbers have at least 8 digits before comparing suffixes
guard normalizedNumber1.count >= 8 && normalizedNumber2.count >= 8 else {
return false // One or both numbers are too short to have 8 comparable digits
}
// Compare the last 8 digits
return normalizedNumber1.suffix(8) == normalizedNumber2.suffix(8)
}

@ -69,6 +69,13 @@ struct MenuWarningView: View {
Label("Appeler", systemImage: "phone") Label("Appeler", systemImage: "phone")
Text(number) Text(number)
} }
if let contactPhoneNumber = player.contactPhoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(contactPhoneNumber)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
Text(contactPhoneNumber)
}
}
} else { } else {
Menu { Menu {
ForEach(players) { player in ForEach(players) { player in
@ -78,6 +85,12 @@ struct MenuWarningView: View {
Text(number) Text(number)
} }
} }
if let number = player.contactPhoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label(player.playerLabel(.short), systemImage: "phone")
Text(number)
}
}
} }
} label: { } label: {
Text("Appeler un joueur") Text("Appeler un joueur")
@ -93,12 +106,13 @@ struct MenuWarningView: View {
} }
fileprivate func _contactByMessage(players: [PlayerRegistration], privateMode: Bool) { fileprivate func _contactByMessage(players: [PlayerRegistration], privateMode: Bool) {
self.savedContactType = .message(date: date, recipients: players.compactMap({ $0.phoneNumber }), body: message, tournamentBuild: nil) self.savedContactType = .message(date: date, recipients: players.flatMap({ [$0.phoneNumber, $0.contactPhoneNumber].compacted() }), body: message, tournamentBuild: nil)
self._tryToContact() self._tryToContact()
} }
fileprivate func _contactByMail(players: [PlayerRegistration], privateMode: Bool) { fileprivate func _contactByMail(players: [PlayerRegistration], privateMode: Bool) {
self.savedContactType = .mail(date: date, recipients: privateMode ? _getUmpireMail() : players.compactMap({ $0.email }), bccRecipients: privateMode ? players.compactMap({ $0.email }) : nil, body: message, subject: subject, tournamentBuild: nil) let mails = players.flatMap({ [$0.email, $0.contactEmail].compacted() })
self.savedContactType = .mail(date: date, recipients: privateMode ? _getUmpireMail() : mails, bccRecipients: privateMode ? mails : nil, body: message, subject: subject, tournamentBuild: nil)
self._tryToContact() self._tryToContact()
} }

@ -275,6 +275,14 @@ struct PadelClubView: View {
} }
} }
struct RankingJSON: Decodable {
enum CodingKeys: String, CodingKey {
case joueurs
}
let joueurs: [FederalPlayer]
}
private func _exportCsv() async { private func _exportCsv() async {
for fileURL in SourceFileManager.shared.jsonFiles() { for fileURL in SourceFileManager.shared.jsonFiles() {
let decoder = JSONDecoder() let decoder = JSONDecoder()
@ -282,18 +290,16 @@ struct PadelClubView: View {
do { do {
let data = try Data(contentsOf: fileURL) let data = try Data(contentsOf: fileURL)
let players = try decoder.decode([FederalPlayer].self, from: data) let players = try decoder.decode(RankingJSON.self, from: data).joueurs
var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false }
print("before anonymousPlayers.count", anonymousPlayers.count) print("before anonymousPlayers.count", anonymousPlayers.count)
FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) //FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers)
print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count) // print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
await fetchPlayersDataSequentially(for: &anonymousPlayers) //await fetchPlayersDataSequentially(for: &anonymousPlayers)
print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } // print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
.count)
SourceFileManager.shared.exportToCSV(players: players, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) SourceFileManager.shared.exportToCSV(players: players, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
} catch { } catch {

@ -18,6 +18,12 @@ struct PlayerDetailView: View {
@State private var licenceId: String @State private var licenceId: String
@State private var phoneNumber: String @State private var phoneNumber: String
@State private var email: String @State private var email: String
@State private var contactName: String
@State private var contactPhoneNumber: String
@State private var contactEmail: String
@FocusState var focusedField: PlayerRegistration.CodingKeys? @FocusState var focusedField: PlayerRegistration.CodingKeys?
var tournamentStore: TournamentStore? { var tournamentStore: TournamentStore? {
@ -29,6 +35,9 @@ struct PlayerDetailView: View {
_licenceId = .init(wrappedValue: player.licenceId ?? "") _licenceId = .init(wrappedValue: player.licenceId ?? "")
_email = .init(wrappedValue: player.email ?? "") _email = .init(wrappedValue: player.email ?? "")
_phoneNumber = .init(wrappedValue: player.phoneNumber ?? "") _phoneNumber = .init(wrappedValue: player.phoneNumber ?? "")
_contactName = .init(wrappedValue: player.contactName ?? "")
_contactEmail = .init(wrappedValue: player.contactEmail ?? "")
_contactPhoneNumber = .init(wrappedValue: player.contactPhoneNumber ?? "")
} }
var body: some View { var body: some View {
@ -210,6 +219,18 @@ struct PlayerDetailView: View {
} }
} label: { } label: {
Menu { Menu {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("\(number)", systemImage: "phone")
}
}
if let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label("\(number)", systemImage: "message")
}
}
Divider()
}
CopyPasteButtonView(pasteValue: player.phoneNumber) CopyPasteButtonView(pasteValue: player.phoneNumber)
PasteButtonView(text: $phoneNumber) PasteButtonView(text: $phoneNumber)
} label: { } label: {
@ -231,33 +252,103 @@ struct PlayerDetailView: View {
} }
} label: { } label: {
Menu { Menu {
if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") {
Link(destination: mailURL) {
Label(mail, systemImage: "mail")
}
Divider()
}
CopyPasteButtonView(pasteValue: player.email) CopyPasteButtonView(pasteValue: player.email)
PasteButtonView(text: $email) PasteButtonView(text: $email)
} label: { } label: {
Text("Email") Text("Email")
} }
} }
} header: {
Text("Information fédérale")
} }
Section { Section {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { LabeledContent {
if let url = URL(string: "tel:\(number)") { TextField("Contact/tuteur", text: $contactName)
Link(destination: url) { .focused($focusedField, equals: ._contactName)
Label("Appeler", systemImage: "phone") .keyboardType(.alphabet)
.textContentType(nil)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.contactName = contactName.prefixTrimmed(200)
_save()
} }
} } label: {
if let url = URL(string: "sms:\(number)") { Text("Contact/tuteur")
Link(destination: url) { }
Label("Message", systemImage: "message")
LabeledContent {
TextField("Téléphone contact", text: $contactPhoneNumber)
.focused($focusedField, equals: ._contactPhoneNumber)
.keyboardType(.namePhonePad)
.textContentType(nil)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.contactPhoneNumber = contactPhoneNumber.prefixTrimmed(50)
_save()
} }
} label: {
Menu {
if let number = player.contactPhoneNumber?.replacingOccurrences(of: " ", with: "") { if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("\(number)", systemImage: "phone")
}
}
if let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label(number, systemImage: "message")
}
}
Divider()
}
CopyPasteButtonView(pasteValue: player.contactPhoneNumber)
PasteButtonView(text: $contactPhoneNumber)
} label: {
Text("Téléphone contact")
} }
} }
if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") { LabeledContent {
Link(destination: mailURL) { TextField("Email contact", text: $contactEmail)
Label("Mail", systemImage: "mail") .focused($focusedField, equals: ._contactEmail)
.keyboardType(.emailAddress)
.textContentType(nil)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.contactEmail = contactEmail.prefixTrimmed(50)
_save()
}
} label: {
Menu {
if let mail = player.contactEmail, let mailURL = URL(string: "mail:\(mail)") {
Link(destination: mailURL) {
Label(mail, systemImage: "mail")
}
Divider()
}
CopyPasteButtonView(pasteValue: player.contactEmail)
PasteButtonView(text: $contactEmail)
} label: {
Text("Email contact")
} }
} }
} header: {
Text("Information de contact")
} footer: {
Text("Ces champs vous permettent de garder les informations de contacts avec le joueur s'ils sont différents de ceux renvoyées par la base fédérale. Cela permet également de garder le contact d'un parent s'il s'agit d'un tournoi enfant.")
} }
// Section { // Section {

@ -310,7 +310,7 @@ final class ServerDataTests: XCTestCase {
return return
} }
let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerPaymentType.cash, sex: PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true, coach: false, captain: false, registeredOnline: false, timeToConfirm: nil, registrationStatus: PlayerRegistration.RegistrationStatus.waiting, paymentId: nil) let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerPaymentType.cash, sex: PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true, coach: false, captain: false, registeredOnline: false, timeToConfirm: nil, registrationStatus: PlayerRegistration.RegistrationStatus.waiting, paymentId: nil, contactName: "coach juan", contactPhoneNumber: "4587654321", contactEmail: "juana@email.com")
playerRegistration.storeId = "123" playerRegistration.storeId = "123"
if let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration) { if let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration) {
@ -343,6 +343,10 @@ final class ServerDataTests: XCTestCase {
assert(pr.timeToConfirm == playerRegistration.timeToConfirm) assert(pr.timeToConfirm == playerRegistration.timeToConfirm)
assert(pr.registrationStatus == playerRegistration.registrationStatus) assert(pr.registrationStatus == playerRegistration.registrationStatus)
assert(pr.paymentId == playerRegistration.paymentId) assert(pr.paymentId == playerRegistration.paymentId)
assert(pr.contactName == playerRegistration.contactName)
assert(pr.contactEmail == playerRegistration.contactEmail)
assert(pr.contactPhoneNumber == playerRegistration.contactPhoneNumber)
} else { } else {
XCTFail("missing data") XCTFail("missing data")
} }

Loading…
Cancel
Save