diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b0c58b3..2e686ab 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -650,6 +650,9 @@ FF7DCD3A2CC330270041110C /* 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 */; }; + 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 */; }; FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.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 = ""; }; FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = ""; }; FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = ""; }; + FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumbersUtils.swift; sourceTree = ""; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = ""; }; FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; FF8E1CE52C006E0200184680 /* Alphabet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alphabet.swift; sourceTree = ""; }; @@ -1935,6 +1939,7 @@ FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */, C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */, + FF81F1BB2E0C4B5F00782CFD /* PhoneNumbersUtils.swift */, ); path = Utils; sourceTree = ""; @@ -2432,6 +2437,7 @@ FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FFCB74172C480411008384D0 /* CopyPasteButtonView.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, + FF81F1BD2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */, FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */, FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */, FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */, @@ -2695,6 +2701,7 @@ FF4CC0252C996C0600151637 /* Labels.swift in Sources */, FF4CC0262C996C0600151637 /* CopyPasteButtonView.swift in Sources */, FF4CC0272C996C0600151637 /* PlayerSexPickerView.swift in Sources */, + FF81F1BE2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */, FF4CC0282C996C0600151637 /* TournamentInscriptionView.swift in Sources */, FF4CC0292C996C0600151637 /* CallSettingsView.swift in Sources */, FF4CC02A2C996C0600151637 /* FooterButtonView.swift in Sources */, @@ -2936,6 +2943,7 @@ FF70FBA42C90584900129CC2 /* Labels.swift in Sources */, FF70FBA52C90584900129CC2 /* CopyPasteButtonView.swift in Sources */, FF70FBA62C90584900129CC2 /* PlayerSexPickerView.swift in Sources */, + FF81F1BC2E0C4B5F00782CFD /* PhoneNumbersUtils.swift in Sources */, FF70FBA72C90584900129CC2 /* TournamentInscriptionView.swift in Sources */, FF70FBA82C90584900129CC2 /* CallSettingsView.swift in Sources */, FF70FBA92C90584900129CC2 /* FooterButtonView.swift in Sources */, @@ -3129,7 +3137,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.40; + MARKETING_VERSION = 1.2.42; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3175,7 +3183,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.40; + MARKETING_VERSION = 1.2.42; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index d5a6784..3573a3e 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -31,54 +31,70 @@ class FederalPlayer: Decodable { } 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 { case nom case prenom case licence case meilleurClassement - case nationnalite - case anneeNaissance + case nationalite case codeClub case nomClub - case nomLigue - case rang - case progression + case ligue + case classement + case evolution case points - case nombreDeTournois - case assimile + case nombreTournoisJoues + case assimilation + case ageSportif } let container = try decoder.container(keyedBy: CodingKeys.self) isMale = (decoder.userInfo[.maleData] as? Bool) == true - let _lastName = try container.decode(String.self, forKey: .nom) - let _firstName = try container.decode(String.self, forKey: .prenom) - lastName = _lastName - firstName = _firstName + let _lastName = try container.decodeIfPresent(String.self, forKey: .nom) + let _firstName = try container.decodeIfPresent(String.self, forKey: .prenom) + lastName = _lastName ?? "" + firstName = _firstName ?? "" if let lic = try? container.decodeIfPresent(Int.self, forKey: .licence) { license = String(lic) } else { license = "" } - let nationnalite = try container.decode(Nationnalite.self, forKey: .nationnalite) - country = nationnalite.code + country = try container.decodeIfPresent(String.self, forKey: .nationalite) ?? "" bestRank = try container.decodeIfPresent(Int.self, forKey: .meilleurClassement) - birthYear = try container.decodeIfPresent(Int.self, forKey: .anneeNaissance) - clubCode = try container.decode(String.self, forKey: .codeClub) - club = try container.decode(String.self, forKey: .nomClub) - ligue = try container.decode(String.self, forKey: .nomLigue) - rank = try container.decode(Int.self, forKey: .rang) - progression = (try? container.decodeIfPresent(Int.self, forKey: .progression)) ?? 0 + + let ageSportif = try container.decodeIfPresent(Int.self, forKey: .ageSportif) + if let ageSportif { + birthYear = Calendar.current.component(.year, from: Date()) - ageSportif + } + 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) if let pointsAsInt { points = Double(pointsAsInt) } else { points = nil } - tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois) - let assimile = try container.decode(Bool.self, forKey: .assimile) + tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreTournoisJoues) + let assimile = try container.decode(Bool.self, forKey: .assimilation) assimilation = assimile ? "Oui" : "Non" } @@ -92,6 +108,7 @@ class FederalPlayer: Decodable { } func formatNumbers(_ input: String) -> String { + if input.isEmpty { return input } // Insert spaces at appropriate positions let formattedString = insertSeparator(input, separator: " ", every: [2, 4]) return formattedString diff --git a/PadelClub/Extensions/MonthData+Extensions.swift b/PadelClub/Extensions/MonthData+Extensions.swift index 0f6bbf6..13d8502 100644 --- a/PadelClub/Extensions/MonthData+Extensions.swift +++ b/PadelClub/Extensions/MonthData+Extensions.swift @@ -15,13 +15,22 @@ extension MonthData { let fileURL = SourceFileManager.shared.allFiles(true).first(where: { $0.dateFromPath == fromDate && $0.index == 0 }) print("calculateCurrentUnrankedValues", fromDate.monthYearFormatted, fileURL?.path()) let fftImportingUncomplete = fileURL?.fftImportingUncomplete() + var fftImportingAnonymous = fileURL?.fftImportingAnonymous() 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 lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: fromDate, man: true) 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 { let lastDataSource = URL.importDateFormatter.string(from: fromDate) 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.incompleteMode = incompleteMode currentMonthData.maleCount = incompleteMode ? fftImportingUncomplete : lastDataSourceMaleUnranked?.1 - currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0 - currentMonthData.femaleCount = lastDataSourceFemaleUnranked?.1 + currentMonthData.femaleUnrankedValue = incompleteMode ? femaleFftImportingMaleUnrankValue : lastDataSourceFemaleUnranked?.0 + currentMonthData.femaleCount = incompleteMode ? femaleFftImportingUncomplete : lastDataSourceFemaleUnranked?.1 currentMonthData.anonymousCount = anonymousCount DataStore.shared.monthData.addOrUpdate(instance: currentMonthData) - } } diff --git a/PadelClub/Extensions/TeamRegistration+Extensions.swift b/PadelClub/Extensions/TeamRegistration+Extensions.swift index c3c2140..5cdfd84 100644 --- a/PadelClub/Extensions/TeamRegistration+Extensions.swift +++ b/PadelClub/Extensions/TeamRegistration+Extensions.swift @@ -39,6 +39,17 @@ extension TeamRegistration { player.licenceId?.strippedLicense != nil { 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.tournamentPlayed = oldPlayer.tournamentPlayed player.points = oldPlayer.points diff --git a/PadelClub/Utils/PhoneNumbersUtils.swift b/PadelClub/Utils/PhoneNumbersUtils.swift new file mode 100644 index 0000000..cd446ac --- /dev/null +++ b/PadelClub/Utils/PhoneNumbersUtils.swift @@ -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) +} diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index 3f3bcc5..885b1cf 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -69,6 +69,13 @@ struct MenuWarningView: View { Label("Appeler", systemImage: "phone") 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 { Menu { ForEach(players) { player in @@ -78,6 +85,12 @@ struct MenuWarningView: View { 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: { Text("Appeler un joueur") @@ -93,12 +106,13 @@ struct MenuWarningView: View { } 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() } 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() } diff --git a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift index ef9a947..29f0145 100644 --- a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -275,6 +275,14 @@ struct PadelClubView: View { } } + struct RankingJSON: Decodable { + enum CodingKeys: String, CodingKey { + case joueurs + } + + let joueurs: [FederalPlayer] + } + private func _exportCsv() async { for fileURL in SourceFileManager.shared.jsonFiles() { let decoder = JSONDecoder() @@ -282,18 +290,16 @@ struct PadelClubView: View { do { 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 } - let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false } print("before anonymousPlayers.count", anonymousPlayers.count) - FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) - print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count) + //FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) +// 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 } - .count) +// print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count) 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) } catch { diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 8199432..1633df7 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -18,6 +18,12 @@ struct PlayerDetailView: View { @State private var licenceId: String @State private var phoneNumber: 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? var tournamentStore: TournamentStore? { @@ -29,6 +35,9 @@ struct PlayerDetailView: View { _licenceId = .init(wrappedValue: player.licenceId ?? "") _email = .init(wrappedValue: player.email ?? "") _phoneNumber = .init(wrappedValue: player.phoneNumber ?? "") + _contactName = .init(wrappedValue: player.contactName ?? "") + _contactEmail = .init(wrappedValue: player.contactEmail ?? "") + _contactPhoneNumber = .init(wrappedValue: player.contactPhoneNumber ?? "") } var body: some View { @@ -210,6 +219,18 @@ struct PlayerDetailView: View { } } label: { 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) PasteButtonView(text: $phoneNumber) } label: { @@ -231,33 +252,103 @@ struct PlayerDetailView: View { } } label: { Menu { + if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") { + Link(destination: mailURL) { + Label(mail, systemImage: "mail") + } + Divider() + } CopyPasteButtonView(pasteValue: player.email) PasteButtonView(text: $email) } label: { Text("Email") } } + } header: { + Text("Information fédérale") } Section { - if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { - if let url = URL(string: "tel:\(number)") { - Link(destination: url) { - Label("Appeler", systemImage: "phone") + LabeledContent { + TextField("Contact/tuteur", text: $contactName) + .focused($focusedField, equals: ._contactName) + .keyboardType(.alphabet) + .textContentType(nil) + .multilineTextAlignment(.trailing) + .autocorrectionDisabled() + .frame(maxWidth: .infinity) + .onSubmit(of: .text) { + player.contactName = contactName.prefixTrimmed(200) + _save() } - } - if let url = URL(string: "sms:\(number)") { - Link(destination: url) { - Label("Message", systemImage: "message") + } label: { + Text("Contact/tuteur") + } + + 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)") { - Link(destination: mailURL) { - Label("Mail", systemImage: "mail") + LabeledContent { + TextField("Email contact", text: $contactEmail) + .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 { diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index a0729e0..98735cb 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -310,7 +310,7 @@ final class ServerDataTests: XCTestCase { 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" 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.registrationStatus == playerRegistration.registrationStatus) assert(pr.paymentId == playerRegistration.paymentId) + assert(pr.contactName == playerRegistration.contactName) + assert(pr.contactEmail == playerRegistration.contactEmail) + assert(pr.contactPhoneNumber == playerRegistration.contactPhoneNumber) + } else { XCTFail("missing data") }