From dd7207ae333eb641a057a05ae07840096d4b06ba Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 20 Oct 2025 09:44:43 +0200 Subject: [PATCH] small improvements --- PadelClubData/Data/PlayerRegistration.swift | 4 +- PadelClubData/Data/Tournament.swift | 8 +- .../Extensions/String+Extensions.swift | 81 ++++++++++++++++--- PadelClubData/Utils/ContactManager.swift | 28 ++++++- 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/PadelClubData/Data/PlayerRegistration.swift b/PadelClubData/Data/PlayerRegistration.swift index cfd13e3..392c6d2 100644 --- a/PadelClubData/Data/PlayerRegistration.swift +++ b/PadelClubData/Data/PlayerRegistration.swift @@ -231,12 +231,12 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable { } public func hasMail() -> Bool { - var mails = [email, contactEmail].compactMap({ $0 }) + let mails = [email, contactEmail].compactMap({ $0 }) return mails.isEmpty == false && mails.anySatisfy({ $0.isValidEmail() }) } public func hasMobilePhone() -> Bool { - var phones = [phoneNumber, contactPhoneNumber].compactMap({ $0 }) + let phones = [phoneNumber, contactPhoneNumber].compactMap({ $0 }) return phones.isEmpty == false && phones.anySatisfy({ $0.isPhoneNumber() }) } diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index 3662b03..ad2446c 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -125,9 +125,15 @@ final public class Tournament: BaseTournament { public func groupStages(atStep step: Int = 0) -> [GroupStage] { guard let tournamentStore = self.tournamentStore else { return [] } - let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } + let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.step == step } return groupStages.sorted(by: \.index) } + + public func hasGroupeStages() -> Bool { + if groupStageCount > 0 { return true } + guard let tournamentStore = self.tournamentStore else { return false } + return tournamentStore.groupStages.isEmpty == false + } public func allGroupStages() -> [GroupStage] { guard let tournamentStore = self.tournamentStore else { return [] } diff --git a/PadelClubData/Extensions/String+Extensions.swift b/PadelClubData/Extensions/String+Extensions.swift index 5f9aa47..4b27125 100644 --- a/PadelClubData/Extensions/String+Extensions.swift +++ b/PadelClubData/Extensions/String+Extensions.swift @@ -213,25 +213,88 @@ public extension String { // MARK: - FFT Source Importing public extension String { enum RegexStatic { - static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/ - static let phoneNumber = /^(?:\+33|0033|0)[1-9](?:[ .-]?[0-9]{2}){4}$/ + // Patterns for France only + static let phoneNumber = /^(\+33|0033|33|0)[1-9][0-9]{8}$/ + static let phoneNumberWithExtra0 = /^33[0][1-9][0-9]{8}$/ + static let mobileNumber = /^(\+33|0033|33|0)[6-7][0-9]{8}$/ + static let mobileNumberWithExtra0 = /^33[0][6-7][0-9]{8}$/ } - - func isMobileNumber() -> Bool { - firstMatch(of: RegexStatic.mobileNumber) != nil + + private func cleanedNumberForValidation() -> String { + // Keep leading '+' if present, remove all other non-digit characters + var cleaned = self.trimmingCharacters(in: .whitespacesAndNewlines) + if cleaned.hasPrefix("+") { + // Preserve '+' at start, remove all other non-digit characters + let digitsOnly = cleaned.dropFirst().components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + cleaned = "+" + digitsOnly + } else { + // Remove all non-digit characters + cleaned = cleaned.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + } + return cleaned } - - func isPhoneNumber() -> Bool { - firstMatch(of: RegexStatic.phoneNumber) != nil + + // MARK: - Phone Number Validation + + /// Validate if the string is a mobile number for the specified locale. + /// - Parameter locale: The locale to validate against. Defaults to `.current`. + /// - Returns: True if the string matches the mobile number pattern for the locale. + func isMobileNumber(locale: Locale = .current) -> Bool { + // TODO: Support additional regions/locales in the future. + switch locale.region?.identifier { + case "FR", "fr", nil: + // French logic for now + let cleaned = cleanedNumberForValidation() + if cleaned.firstMatch(of: RegexStatic.mobileNumber) != nil { + return true + } + if cleaned.firstMatch(of: RegexStatic.mobileNumberWithExtra0) != nil { + return true + } + return false + default: + // For unsupported locales, fallback to checking if the string contains at least 8 digits + // This is a generic minimum length for most countries' phone numbers + let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + return digitsOnly.count >= 8 + } } + /// Validate if the string is a phone number for the specified locale. + /// - Parameter locale: The locale to validate against. Defaults to `.current`. + /// - Returns: True if the string matches the phone number pattern for the locale. + func isPhoneNumber(locale: Locale = .current) -> Bool { + // TODO: Support additional regions/locales in the future. + switch locale.region?.identifier { + case "FR", "fr", nil: + // French logic for now + let cleaned = cleanedNumberForValidation() + if cleaned.firstMatch(of: RegexStatic.phoneNumber) != nil { + return true + } + if cleaned.firstMatch(of: RegexStatic.phoneNumberWithExtra0) != nil { + return true + } + return false + default: + // For unsupported locales, fallback to checking if the string contains at least 8 digits + // This is a generic minimum length for most countries' phone numbers + let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + return digitsOnly.count >= 8 + } + } + func normalize(_ phone: String) -> String { var normalized = phone.trimmingCharacters(in: .whitespacesAndNewlines) // Remove all non-digit characters normalized = normalized.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() // Remove leading country code for France (33) if present if normalized.hasPrefix("33") { - normalized = "0" + normalized.dropFirst(2) + if normalized.dropFirst(2).hasPrefix("0") { + // Keep as is, don't strip the zero after 33 + } else { + normalized = "0" + normalized.dropFirst(2) + } } else if normalized.hasPrefix("0033") { normalized = "0" + normalized.dropFirst(4) } diff --git a/PadelClubData/Utils/ContactManager.swift b/PadelClubData/Utils/ContactManager.swift index e8e9a18..ca353fc 100644 --- a/PadelClubData/Utils/ContactManager.swift +++ b/PadelClubData/Utils/ContactManager.swift @@ -57,13 +57,14 @@ public enum ContactManagerError: LocalizedError { public enum SummonType: Int, Identifiable { case contact + case contactWithoutSignature case summon case summonWalkoutFollowUp case summonErrorFollowUp public func isRecall() -> Bool { switch self { - case .contact: + case .contact, .contactWithoutSignature: return false case .summon: return false @@ -74,10 +75,27 @@ public enum SummonType: Int, Identifiable { } } + public func mainWord() -> String { + switch self { + case .contact: + return "Contacter" + case .contactWithoutSignature: + return "Contacter" + case .summon: + return "Convoquer" + case .summonWalkoutFollowUp: + return "Reconvoquer" + case .summonErrorFollowUp: + return "Reconvoquer" + } + } + public func caption() -> String? { switch self { case .contact: return nil + case .contactWithoutSignature: + return "Sans texte par défaut" case .summon: return nil case .summonWalkoutFollowUp: @@ -89,7 +107,7 @@ public enum SummonType: Int, Identifiable { public func shouldConfirm() -> Bool { switch self { - case .contact: + case .contact, .contactWithoutSignature: return false case .summon: return true @@ -102,6 +120,8 @@ public enum SummonType: Int, Identifiable { public func intro() -> String { switch self { + case .contactWithoutSignature: + return "" case .contact: return "Vous êtes" case .summon: @@ -161,7 +181,9 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c } static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, summonType: SummonType = .summon) -> String { - + if summonType == .contactWithoutSignature { + return "" + } let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage if useFullCustomMessage {