diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b0c58b3..4157b4f 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.41; 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.41; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; 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/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") }