diff --git a/PadelClubData/Business.swift b/PadelClubData/Business.swift index 422ba55..de7193f 100644 --- a/PadelClubData/Business.swift +++ b/PadelClubData/Business.swift @@ -84,6 +84,36 @@ public enum OnlineRegistrationStatus: Int { } } +public enum PaymentStatus { + case notEnabled + case notConfigured // Online payment not set up (no Stripe account) + case optionalPayment // Online payment is optional + case mandatoryRefundEnabled // Online payment is mandatory, and refunds are enabled within the refund period + case mandatoryRefundEnded // Online payment is mandatory, refunds were enabled but the refund period has ended + case mandatoryNoRefund // Online payment is mandatory, but refunds are not enabled + + /** + Returns a localized string representation of the payment status. + This method allows for easy display of the status in the UI. + */ + public func statusLocalized() -> String { + switch self { + case .notEnabled: + return "Paiement en ligne désactivé" + case .notConfigured: + return "Non configuré" + case .optionalPayment: + return "Facultatif" + case .mandatoryRefundEnabled: + return "Obligatoire avec remboursement" + case .mandatoryRefundEnded: + return "Obligatoire, remboursement terminé" + case .mandatoryNoRefund: + return "Obligatoire sans remboursement" + } + } +} + public enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable { case all = -1 case male = 1 diff --git a/PadelClubData/Data/DataStore.swift b/PadelClubData/Data/DataStore.swift index 97fa7d8..f31c850 100644 --- a/PadelClubData/Data/DataStore.swift +++ b/PadelClubData/Data/DataStore.swift @@ -106,6 +106,7 @@ public class DataStore: ObservableObject { if Store.main.fileCollectionsAllLoaded() { AutomaticPatcher.applyAllWhenApplicable() + self.resetOngoingCache() } } @@ -188,12 +189,20 @@ public class DataStore: ObservableObject { } - public func deleteTournament(_ tournament: Tournament) { + public func deleteTournament(_ tournament: Tournament, noSync: Bool = false) { let event = tournament.eventObject() let isLastTournament = event?.tournaments.count == 1 - self.tournaments.delete(instance: tournament) - if let event, isLastTournament { - self.events.delete(instance: event) + + if noSync { + self.tournaments.deleteNoSync(instance: tournament, cascading: true) + if let event, isLastTournament { + self.events.deleteNoSync(instance: event, cascading: true) + } + } else { + self.tournaments.delete(instance: tournament) + if let event, isLastTournament { + self.events.delete(instance: event) + } } StoreCenter.main.destroyStore(identifier: tournament.id) } @@ -222,6 +231,7 @@ public class DataStore: ObservableObject { self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder() self.user.clubs.removeAll() + self.user.licenceId = nil } @@ -315,7 +325,13 @@ public class DataStore: ObservableObject { private var _lastRunningAndNextCheckDate: Date? = nil private var _cachedRunningAndNextMatches: [Match]? = nil - + + public func resetOngoingCache() { + _lastEndCheckDate = nil + _lastRunningCheckDate = nil + _lastRunningAndNextCheckDate = nil + } + public func runningAndNextMatches() -> [Match] { let dateNow : Date = Date() if let lastCheck = _lastRunningAndNextCheckDate, diff --git a/PadelClubData/Data/Gen/BasePlayerRegistration.swift b/PadelClubData/Data/Gen/BasePlayerRegistration.swift index 41da11f..9972e21 100644 --- a/PadelClubData/Data/Gen/BasePlayerRegistration.swift +++ b/PadelClubData/Data/Gen/BasePlayerRegistration.swift @@ -40,6 +40,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { public var paymentId: String? = nil public var clubCode: String? = nil public var clubMember: Bool = false + public var contactName: String? = nil + public var contactPhoneNumber: String? = nil + public var contactEmail: String? = nil public init( id: String = Store.randomId(), @@ -68,7 +71,10 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting, paymentId: String? = nil, clubCode: String? = nil, - clubMember: Bool = false + clubMember: Bool = false, + contactName: String? = nil, + contactPhoneNumber: String? = nil, + contactEmail: String? = nil ) { super.init() self.id = id @@ -98,6 +104,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.paymentId = paymentId self.clubCode = clubCode self.clubMember = clubMember + self.contactName = contactName + self.contactPhoneNumber = contactPhoneNumber + self.contactEmail = contactEmail } required public override init() { super.init() @@ -131,6 +140,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { case _paymentId = "paymentId" case _clubCode = "clubCode" case _clubMember = "clubMember" + case _contactName = "contactName" + case _contactPhoneNumber = "contactPhoneNumber" + case _contactEmail = "contactEmail" } required init(from decoder: Decoder) throws { @@ -162,6 +174,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.paymentId = try container.decodeIfPresent(String.self, forKey: ._paymentId) ?? nil self.clubCode = try container.decodeIfPresent(String.self, forKey: ._clubCode) ?? nil self.clubMember = try container.decodeIfPresent(Bool.self, forKey: ._clubMember) ?? false + self.contactName = try container.decodeIfPresent(String.self, forKey: ._contactName) ?? nil + self.contactPhoneNumber = try container.decodeIfPresent(String.self, forKey: ._contactPhoneNumber) ?? nil + self.contactEmail = try container.decodeIfPresent(String.self, forKey: ._contactEmail) ?? nil try super.init(from: decoder) } @@ -194,6 +209,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { try container.encode(self.paymentId, forKey: ._paymentId) try container.encode(self.clubCode, forKey: ._clubCode) try container.encode(self.clubMember, forKey: ._clubMember) + try container.encode(self.contactName, forKey: ._contactName) + try container.encode(self.contactPhoneNumber, forKey: ._contactPhoneNumber) + try container.encode(self.contactEmail, forKey: ._contactEmail) try super.encode(to: encoder) } @@ -231,6 +249,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.paymentId = playerregistration.paymentId self.clubCode = playerregistration.clubCode self.clubMember = playerregistration.clubMember + self.contactName = playerregistration.contactName + self.contactPhoneNumber = playerregistration.contactPhoneNumber + self.contactEmail = playerregistration.contactEmail } public static func parentRelationships() -> [Relationship] { diff --git a/PadelClubData/Data/Gen/BaseTournament.swift b/PadelClubData/Data/Gen/BaseTournament.swift index 3c8dc43..0c4d0b4 100644 --- a/PadelClubData/Data/Gen/BaseTournament.swift +++ b/PadelClubData/Data/Gen/BaseTournament.swift @@ -84,6 +84,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { public var showTeamsInProg: Bool = false public var clubMemberFeeDeduction: Double? = nil public var unregisterDeltaInHours: Int = 24 + public var currencyCode: String? = nil public init( id: String = Store.randomId(), @@ -156,7 +157,8 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { publishProg: Bool = false, showTeamsInProg: Bool = false, clubMemberFeeDeduction: Double? = nil, - unregisterDeltaInHours: Int = 24 + unregisterDeltaInHours: Int = 24, + currencyCode: String? = nil ) { super.init() self.id = id @@ -230,6 +232,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { self.showTeamsInProg = showTeamsInProg self.clubMemberFeeDeduction = clubMemberFeeDeduction self.unregisterDeltaInHours = unregisterDeltaInHours + self.currencyCode = currencyCode } required public override init() { super.init() @@ -309,6 +312,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { case _showTeamsInProg = "showTeamsInProg" case _clubMemberFeeDeduction = "clubMemberFeeDeduction" case _unregisterDeltaInHours = "unregisterDeltaInHours" + case _currencyCode = "currencyCode" } private static func _decodePayment(container: KeyedDecodingContainer) throws -> TournamentPayment? { @@ -451,6 +455,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { self.showTeamsInProg = try container.decodeIfPresent(Bool.self, forKey: ._showTeamsInProg) ?? false self.clubMemberFeeDeduction = try container.decodeIfPresent(Double.self, forKey: ._clubMemberFeeDeduction) ?? nil self.unregisterDeltaInHours = try container.decodeIfPresent(Int.self, forKey: ._unregisterDeltaInHours) ?? 24 + self.currencyCode = try container.decodeIfPresent(String.self, forKey: ._currencyCode) ?? nil try super.init(from: decoder) } @@ -527,6 +532,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { try container.encode(self.showTeamsInProg, forKey: ._showTeamsInProg) try container.encode(self.clubMemberFeeDeduction, forKey: ._clubMemberFeeDeduction) try container.encode(self.unregisterDeltaInHours, forKey: ._unregisterDeltaInHours) + try container.encode(self.currencyCode, forKey: ._currencyCode) try super.encode(to: encoder) } @@ -608,6 +614,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { self.showTeamsInProg = tournament.showTeamsInProg self.clubMemberFeeDeduction = tournament.clubMemberFeeDeduction self.unregisterDeltaInHours = tournament.unregisterDeltaInHours + self.currencyCode = tournament.currencyCode } public static func parentRelationships() -> [Relationship] { diff --git a/PadelClubData/Data/Gen/PlayerRegistration.json b/PadelClubData/Data/Gen/PlayerRegistration.json index 089f4f0..97a591c 100644 --- a/PadelClubData/Data/Gen/PlayerRegistration.json +++ b/PadelClubData/Data/Gen/PlayerRegistration.json @@ -140,6 +140,21 @@ "name": "clubMember", "type": "Bool", "defaultValue": "false" + }, + { + "name": "contactName", + "type": "String", + "optional": true + }, + { + "name": "contactPhoneNumber", + "type": "String", + "optional": true + }, + { + "name": "contactEmail", + "type": "String", + "optional": true } ] } diff --git a/PadelClubData/Data/Gen/Tournament.json b/PadelClubData/Data/Gen/Tournament.json index 0f64cf9..d7324a0 100644 --- a/PadelClubData/Data/Gen/Tournament.json +++ b/PadelClubData/Data/Gen/Tournament.json @@ -367,6 +367,11 @@ "name": "unregisterDeltaInHours", "type": "Int", "defaultValue": "24" + }, + { + "name": "currencyCode", + "type": "String", + "optional": true } ] } diff --git a/PadelClubData/Data/Match.swift b/PadelClubData/Data/Match.swift index f28bc46..c8c9798 100644 --- a/PadelClubData/Data/Match.swift +++ b/PadelClubData/Data/Match.swift @@ -50,6 +50,15 @@ final public class Match: BaseMatch, SideStorable { plannedStartDate = startDate } + public func updateStartDate(_ date: Date?, keepPlannedStartDate: Bool) { + DataStore.shared.resetOngoingCache() + let cachedPlannedStartDate = self.plannedStartDate + self.startDate = date + if keepPlannedStartDate { + self.plannedStartDate = cachedPlannedStartDate + } + } + // MARK: - public func setMatchName(_ serverName: String?) { @@ -210,7 +219,7 @@ defer { } public func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { - startDate = targetStartDate ?? startDate + updateStartDate(targetStartDate, keepPlannedStartDate: true) confirmed = false endDate = nil followingMatch()?.cleanScheduleAndSave(nil) @@ -555,7 +564,7 @@ defer { updateFollowingMatchTeamScore() } - public func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { + public func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor, walkoutPosition: TeamPosition?) { updateScore(fromMatchDescriptor: matchDescriptor) if endDate == nil { endDate = Date() @@ -565,7 +574,41 @@ defer { } else if let startDate, let endDate, startDate >= endDate { self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60)) } + + if let walkoutPosition { + let teamOne = team(walkoutPosition.otherTeam) + let teamTwo = team(walkoutPosition) + + teamOne?.hasArrived() + teamTwo?.hasArrived() + teamOne?.resetRestingTime() + teamTwo?.resetRestingTime() + + winningTeamId = teamOne?.id + losingTeamId = teamTwo?.id + let teamScoreWalkout = teamScore(walkoutPosition) ?? TeamScore(match: id, team: team(walkoutPosition)) + teamScoreWalkout.walkOut = 0 + let teamScoreWinning = teamScore(walkoutPosition.otherTeam) ?? TeamScore(match: id, team: team(walkoutPosition.otherTeam)) + teamScoreWinning.walkOut = nil + self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) + } + + confirmed = true + + groupStageObject?.updateGroupStageState() + roundObject?.updateTournamentState() + if let tournament = currentTournament(), let endDate, let startDate { + if endDate.isEarlierThan(tournament.startDate) { + tournament.startDate = startDate + } + do { + try DataStore.shared.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + updateFollowingMatchTeamScore() } public func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { @@ -609,8 +652,10 @@ defer { public func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, team: team(.one)) + teamScoreOne.walkOut = nil teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",") let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two)) + teamScoreTwo.walkOut = nil teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",") if matchDescriptor.teamOneScores.last?.contains("-") == true && matchDescriptor.teamTwoScores.last?.contains("-") == false { @@ -668,7 +713,7 @@ defer { public func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) { if hasEnded() == false { - startDate = fromStartDate + updateStartDate(fromStartDate, keepPlannedStartDate: true) switch fieldSetup { case .fullRandom: @@ -685,7 +730,7 @@ defer { } } else { - startDate = fromStartDate + updateStartDate(fromStartDate, keepPlannedStartDate: true) endDate = toEndDate } diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index 8999fa0..5f16a7d 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -138,7 +138,6 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { } public func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { - let _groupStages = groupStages // Get the maximum count of matches in any group @@ -839,7 +838,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { } - if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || errorFormat) { + if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) { bracketStartDate = lastDate.tomorrowAtNine } diff --git a/PadelClubData/Data/PlayerPaymentType.swift b/PadelClubData/Data/PlayerPaymentType.swift index 298213f..f3c2d8e 100644 --- a/PadelClubData/Data/PlayerPaymentType.swift +++ b/PadelClubData/Data/PlayerPaymentType.swift @@ -35,9 +35,9 @@ public enum PlayerPaymentType: Int, CaseIterable, Identifiable, Codable { case .cash: return "Cash" case .lydia: - return "Lydia" + return "SumUp" case .paylib: - return "Paylib" + return "Wero" case .bankTransfer: return "Virement" case .clubHouse: diff --git a/PadelClubData/Data/PlayerRegistration.swift b/PadelClubData/Data/PlayerRegistration.swift index 880e9c6..9d4511f 100644 --- a/PadelClubData/Data/PlayerRegistration.swift +++ b/PadelClubData/Data/PlayerRegistration.swift @@ -119,7 +119,9 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable { public func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { - case .wide, .title: + case .title: + return firstName.trimmed.capitalized + " " + lastName.trimmed.capitalized + case .wide: return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized case .short: let names = lastName.components(separatedBy: .whitespaces) @@ -170,7 +172,7 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable { let currentRank = rank ?? maleUnranked switch tournament.tournamentCategory { case .men: - let addon = PlayerRegistration.addon(for: currentRank, manMax: maleUnranked, womanMax: femaleUnranked) + let addon = tournament.addon(for: currentRank, manMax: maleUnranked, womanMax: femaleUnranked) computedRank = isMalePlayer() ? currentRank : currentRank + addon default: computedRank = currentRank @@ -279,16 +281,6 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable { } } - public static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { - switch playerRank { - case 0: return 0 - case womanMax: return manMax - womanMax - case manMax: return 0 - default: - return TournamentCategory.femaleInMaleAssimilationAddition(playerRank) - } - } - func insertOnServer() { self.tournamentStore?.playerRegistrations.writeChangeAndInsertOnServer(instance: self) } diff --git a/PadelClubData/Data/TeamRegistration.swift b/PadelClubData/Data/TeamRegistration.swift index 30e2ef2..75a13dd 100644 --- a/PadelClubData/Data/TeamRegistration.swift +++ b/PadelClubData/Data/TeamRegistration.swift @@ -183,11 +183,11 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable { } public func getPhoneNumbers() -> [String] { - return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false }) + return players().flatMap { [$0.phoneNumber, $0.contactPhoneNumber].compacted() }.filter({ $0.isEmpty == false }) } public func getMail() -> [String] { - let mails = players().compactMap({ $0.email }) + let mails = players().flatMap({ [$0.email, $0.contactEmail].compacted() }) return mails } @@ -255,10 +255,10 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable { separator: twoLines ? "\n" : " \(separator) ") } - public func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String { + public func teamLabelRanked(displayStyle: DisplayStyle = .wide, displayRank: Bool, displayTeamName: Bool) -> String { [ displayTeamName ? name : nil, displayRank ? seedIndex() : nil, - displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel(), + displayTeamName ? (name == nil ? teamLabel(displayStyle) : name) : teamLabel(displayStyle), ].compactMap({ $0 }).joined(separator: " ") } diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index 9956cd8..a9a2442 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -1236,7 +1236,7 @@ defer { public var entryFeeMessage: String { if let entryFee { - let message: String = "Inscription : \(entryFee.formatted(.currency(code: Locale.defaultCurrency()))) par joueur." + let message: String = "Inscription : \(entryFee.formatted(.currency(code: self.defaultCurrency()))) par joueur." return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n") } else { return "Inscription : gratuite." @@ -2154,6 +2154,45 @@ defer { return .open } + + public func getPaymentStatus() -> PaymentStatus { + if enableOnlinePayment == false { + return .notEnabled + } + + // 1. Check if Stripe account is configured. This is the most fundamental requirement. + if (stripeAccountId == nil || stripeAccountId!.isEmpty) && isCorporateTournament == false { + return .notConfigured + } + + // 2. Check if online payment is mandatory. This determines the main branch of statuses. + if onlinePaymentIsMandatory { + // Payment is mandatory. Now check refund options. + if enableOnlinePaymentRefund { + let now = Date() + if let refundDate = refundDateLimit { + // Refund is enabled and a date limit is set. Check if the date has passed. + if now < refundDate { + return .mandatoryRefundEnabled // Mandatory, refund is currently possible + } else { + return .mandatoryRefundEnded // Mandatory, refund period has ended + } + } else { + // Refund is enabled but no specific date limit is set. Assume it's currently enabled. + return .mandatoryRefundEnabled + } + } else { + // Payment is mandatory, but refunds are explicitly not enabled. + return .mandatoryNoRefund + } + } else { + // Payment is not mandatory, meaning it's optional. + // Features like `clubMemberFeeDeduction` or `enableTimeToConfirm` + // would apply to this optional payment, but don't change its + // overall 'optionalPayment' status for this summary function. + return .optionalPayment + } + } // MARK: - Status public func shouldTournamentBeOver() async -> Bool { @@ -2332,6 +2371,54 @@ defer { groupStages().sorted(by: \.computedStartDateForSorting).first?.startDate } + public func defaultCurrency() -> String { + if let currencyCode = self.currencyCode { + return currencyCode + } else { + return Locale.defaultCurrency() + } + } + + public func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { + switch playerRank { + case 0: return 0 + case womanMax: return manMax - womanMax + case manMax: return 0 + default: + return TournamentCategory.femaleInMaleAssimilationAddition(playerRank, seasonYear: self.startDate.seasonYear()) + } + } + + public func coachingIsAuthorized() -> Bool { + switch startDate.seasonYear() { + case 2026: + return true + default: + return tournamentLevel.coachingIsAuthorized + } + } + + public func minimumNumberOfTeams() -> Int { + return federalTournamentAge.minimumNumberOfTeams(inCategory: tournamentCategory, andInLevel: tournamentLevel) + } + + public func removeAllDates() { + let allMatches = allMatches() + allMatches.forEach({ + $0.startDate = nil + $0.confirmed = false + }) + self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches) + + let allGroupStages = groupStages() + allGroupStages.forEach({ $0.startDate = nil }) + self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages) + + let allRounds = allRounds() + allRounds.forEach({ $0.startDate = nil }) + self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds) + } + // MARK: - func insertOnServer() throws { diff --git a/PadelClubData/Extensions/Date+Extensions.swift b/PadelClubData/Extensions/Date+Extensions.swift index 286b8c5..a9878f0 100644 --- a/PadelClubData/Extensions/Date+Extensions.swift +++ b/PadelClubData/Extensions/Date+Extensions.swift @@ -37,6 +37,14 @@ public enum TimeOfDay { public extension Date { + func seasonYear() -> Int { + if self.monthInt >= 9 { + return self.yearInt + 1 + } + return self.yearInt + } + + func withoutSeconds() -> Date { let calendar = Calendar.current return calendar.date(bySettingHour: calendar.component(.hour, from: self), @@ -206,6 +214,10 @@ public extension Date { Calendar.current.component(.day, from: self) } + var hourInt: Int { + Calendar.current.component(.hour, from: self) + } + var startOfDay: Date { Calendar.current.startOfDay(for: self) } @@ -265,3 +277,15 @@ public extension Date { return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds() } } + +public extension Date { + var startOfCurrentMonth: Date { + let calendar = Calendar.current + return calendar.dateInterval(of: .month, for: self)?.start ?? self + } + + func addingMonths(_ months: Int) -> Date { + let calendar = Calendar.current + return calendar.date(byAdding: .month, value: months, to: self) ?? self + } +} diff --git a/PadelClubData/Extensions/URL+Extensions.swift b/PadelClubData/Extensions/URL+Extensions.swift index 4d33dbe..233b5db 100644 --- a/PadelClubData/Extensions/URL+Extensions.swift +++ b/PadelClubData/Extensions/URL+Extensions.swift @@ -143,6 +143,24 @@ public extension URL { return nil } + func fftImportingAnonymous() -> Int? { + // Read the contents of the file + guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { + return nil + } + + // Split the contents by newline characters + let lines = fileContents.components(separatedBy: .newlines) + + if let line = lines.first(where: { + $0.hasPrefix("anonymous-players:") + }) { + return Int(line.replacingOccurrences(of: "anonymous-players:", with: "")) + } + + return nil + } + func getUnrankedValue() -> Int? { // Read the contents of the file guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { diff --git a/PadelClubData/Utils/NetworkManagerError.swift b/PadelClubData/Utils/NetworkManagerError.swift index eb91408..ae5a766 100644 --- a/PadelClubData/Utils/NetworkManagerError.swift +++ b/PadelClubData/Utils/NetworkManagerError.swift @@ -16,13 +16,43 @@ public enum NetworkManagerError: LocalizedError { case messageNotSent //no network no error case fileNotModified case fileNotDownloaded(Int) - + case noDataReceived // New: If data is empty or nil + case htmlDecodingFailed // New: If String(data: data, encoding: .utf8) fails + case formBuildIdPrefixNotFound // New: If the prefix regex doesn't match + case formBuildIdSuffixNotFound // New: If the suffix regex doesn't match + case formBuildIdExtractionFailed // New: General parsing failure if other specific errors don't cover it + case apiError(String) // ADDED: General API error with a descriptive message + public var errorDescription: String? { switch self { case .maintenance: - return "Le site de la FFT est en maintenance" - default: - return String(describing: self) + return "Le site de la FFT est en maintenance." + case .fileNotYetAvailable: + return "Le fichier n'est pas encore disponible." + case .mailFailed: + return "L'envoi de l'e-mail a échoué." + case .mailNotSent: + return "L'e-mail n'a pas été envoyé (pas de réseau ou autre)." + case .messageFailed: + return "L'envoi du message a échoué." + case .messageNotSent: + return "Le message n'a pas été envoyé (pas de réseau ou autre)." + case .fileNotModified: + return "Le fichier n'a pas été modifié." + case .fileNotDownloaded(let statusCode): + return "Le fichier n'a pas pu être téléchargé. Code d'état : \(statusCode)." + case .noDataReceived: + return "Aucune donnée n'a été reçue du serveur." + case .htmlDecodingFailed: + return "Échec du décodage de la réponse HTML." + case .formBuildIdPrefixNotFound: + return "Impossible de trouver le début de l'ID du formulaire (form_build_id) dans la page." + case .formBuildIdSuffixNotFound: + return "Impossible de trouver la fin de l'ID du formulaire (form_build_id) dans la page." + case .formBuildIdExtractionFailed: + return "Échec général de l'extraction de l'ID du formulaire (form_build_id)." + case .apiError(let message): + return "Erreur API: \(message)" } } } diff --git a/PadelClubData/ViewModel/PadelRule.swift b/PadelClubData/ViewModel/PadelRule.swift index 8cc6a51..2c8a0cb 100644 --- a/PadelClubData/ViewModel/PadelRule.swift +++ b/PadelClubData/ViewModel/PadelRule.swift @@ -166,6 +166,7 @@ public enum TournamentDifficulty { public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case unlisted = 0 + case a09_10 = 100 case a11_12 = 120 case a13_14 = 140 case a15_16 = 160 @@ -184,6 +185,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi switch self { case .unlisted: return (nil, nil) + case .a09_10: + return (year - 10, year - 9) case .a11_12: return (year - 12, year - 11) case .a13_14: @@ -205,6 +208,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi switch self { case .unlisted: return "Animation" + case .a09_10: + return "09/10 ans" case .a11_12: return "11/12 ans" case .a13_14: @@ -241,6 +246,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi public var order: Int { switch self { case .unlisted: + return 8 + case .a09_10: return 7 case .a11_12: return 6 @@ -263,6 +270,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi switch self { case .unlisted: return displayStyle == .title ? "Aucune" : "" + case .a09_10: + return "U10" case .a11_12: return "U12" case .a13_14: @@ -289,14 +298,16 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi switch self { case .unlisted: return true + case .a09_10: + return age < 11 case .a11_12: return age < 13 case .a13_14: return age < 15 case .a15_16: - return age < 17 + return age < 17 && age >= 11 case .a17_18: - return age < 19 + return age < 19 && age >= 11 case .senior: return age >= 11 case .a45: @@ -310,6 +321,8 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi switch self { case .unlisted: return false + case .a09_10: + return true case .a11_12: return true case .a13_14: @@ -326,6 +339,42 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi return false } } + + func minimumNumberOfTeams(inCategory category: TournamentCategory, andInLevel level: TournamentLevel) -> Int { + + if self.isChildCategory() { + if level == .p250, category == .men { + return 8 + } + return 4 + } else { + switch level { + case .p25, .p100, .p250: + if category == .women { + return 4 + } + if level == .p100 { + return 8 + } + return 12 + case .p500: + if category == .women { + return 8 + } + return 16 + case .p1000: + if category == .women { + return 16 + } + return 24 + + case .p1500, .p2000: + return 24 + default: + return -1 + } + } + } } public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { @@ -432,7 +481,12 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable } } - public func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge) -> Int { + public func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge, seasonYear: Int) -> Int { + + if seasonYear == 2026, category == .mix { + return 0 + } + switch self { case .p25: switch ageCategory { @@ -876,7 +930,28 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab } } - public static func femaleInMaleAssimilationAddition(_ rank: Int) -> Int { + public static func femaleInMaleAssimilationAddition(_ rank: Int, seasonYear: Int?) -> Int { + switch seasonYear { + case .some(let year): + if year < 2026 { + switch rank { + case 1...10: return 400 + case 11...30: return 1000 + case 31...60: return 2000 + case 61...100: return 3500 + case 101...200: return 10000 + case 201...500: return 15000 + case 501...1000: return 25000 + case 1001...2000: return 35000 + case 2001...3000: return 45000 + default: + return 50000 + } + } + case .none: + break + } + switch rank { case 1...10: return 400 case 11...30: return 1000 @@ -887,8 +962,10 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab case 501...1000: return 25000 case 1001...2000: return 35000 case 2001...3000: return 45000 + case 3001...5000: return 55000 + case 5001...10000: return 70000 default: - return 50000 + return 90000 } } @@ -1499,7 +1576,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { public var isFederal: Bool { switch self { - case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint: return false default: return true @@ -1525,11 +1602,11 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { case .twoSetsOfSuperTie: return "G" case .megaTie: - return "F" + return "H" case .singleSet: - return "H1" + return "I1" case .singleSetDecisivePoint: - return "H2" + return "I2" case .twoSetsDecisivePoint: return "A2" case .twoSetsDecisivePointSuperTie: @@ -1539,9 +1616,9 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { case .nineGamesDecisivePoint: return "D2" case .singleSetOfFourGames: - return "I1" + return "F1" case .singleSetOfFourGamesDecisivePoint: - return "I2" + return "F2" } }