diff --git a/.DS_Store b/.DS_Store index 4854eab..8b9a045 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/PadelClubData.xcodeproj/xcshareddata/xcschemes/PadelClubData.xcscheme b/PadelClubData.xcodeproj/xcshareddata/xcschemes/PadelClubData.xcscheme new file mode 100644 index 0000000..d71c984 --- /dev/null +++ b/PadelClubData.xcodeproj/xcshareddata/xcschemes/PadelClubData.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PadelClubData/.DS_Store b/PadelClubData/.DS_Store index ca047a2..2fa4ede 100644 Binary files a/PadelClubData/.DS_Store and b/PadelClubData/.DS_Store differ diff --git a/PadelClubData/Business/OnlineRegistrationStatus.swift b/PadelClubData/Business.swift similarity index 56% rename from PadelClubData/Business/OnlineRegistrationStatus.swift rename to PadelClubData/Business.swift index 19f3bca..422ba55 100644 --- a/PadelClubData/Business/OnlineRegistrationStatus.swift +++ b/PadelClubData/Business.swift @@ -1,12 +1,36 @@ // -// OnlineRegistrationStatus.swift +// Business.swift // PadelClubData // -// Created by Laurent Morvillier on 15/04/2025. +// Created by Laurent Morvillier on 30/04/2025. // import Foundation +public protocol SpinDrawable { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] +} + +public enum DayPeriod: Int, CaseIterable, Identifiable, Codable { + + public var id: Int { self.rawValue } + + case all + case weekend + case week + + public func localizedDayPeriodLabel() -> String { + switch self { + case .all: + return "n'importe" + case .week: + return "la semaine" + case .weekend: + return "le week-end" + } + } +} + public enum OnlineRegistrationStatus: Int { case open = 1 case notEnabled = 2 @@ -59,3 +83,32 @@ public enum OnlineRegistrationStatus: Int { } } } + +public enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable { + case all = -1 + case male = 1 + case female = 0 + + public var id: Int { rawValue } + + public func icon() -> String { + switch self { + case .all: + return "Tous" + case .male: + return "Homme" + case .female: + return "Femme" + } + } + + public var localizedPlayerLabel: String { + switch self { + case .female: + return "joueuse" + default: + return "joueur" + } + } + +} diff --git a/PadelClubData/Business/DayPeriod.swift b/PadelClubData/Business/DayPeriod.swift deleted file mode 100644 index b418ff7..0000000 --- a/PadelClubData/Business/DayPeriod.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// DayPeriod.swift -// PadelClubData -// -// Created by Laurent Morvillier on 15/04/2025. -// - -import Foundation - -public enum DayPeriod: Int, CaseIterable, Identifiable, Codable { - public var id: Int { self.rawValue } - - case all - case weekend - case week - - public func localizedDayPeriodLabel() -> String { - switch self { - case .all: - return "n'importe" - case .week: - return "la semaine" - case .weekend: - return "le week-end" - } - } -} diff --git a/PadelClubData/Business/SpinDrawable.swift b/PadelClubData/Business/SpinDrawable.swift deleted file mode 100644 index 7888d2c..0000000 --- a/PadelClubData/Business/SpinDrawable.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// SpinDrawable.swift -// PadelClubData -// -// Created by Laurent Morvillier on 15/04/2025. -// - -import Foundation - -public protocol SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] -} diff --git a/PadelClubData/ContactManager.swift b/PadelClubData/ContactManager.swift deleted file mode 100644 index 4cf957f..0000000 --- a/PadelClubData/ContactManager.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// ContactManager.swift -// Padel Tournament -// -// Created by Razmig Sarkissian on 19/09/2023. -// - -import Foundation -import SwiftUI -import MessageUI -import LeStorage - -public enum ContactManagerError: LocalizedError { - case mailFailed - case mailNotSent //no network no error - case messageFailed - case messageNotSent //no network no error - case calendarAccessDenied - case calendarEventSaveFailed - case noCalendarAvailable - case uncalledTeams([TeamRegistration]) - - var localizedDescription: String { - switch self { - case .mailFailed: - return "Le mail n'a pas été envoyé" - case .mailNotSent: - return "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer." - case .messageFailed: - return "Le SMS n'a pas été envoyé" - case .messageNotSent: - return "Le SMS n'a pas été envoyé" - case .uncalledTeams(let array): - let verb = array.count > 1 ? "peuvent" : "peut" - return "Attention, \(array.count) équipe\(array.count.pluralSuffix) ne \(verb) pas être contacté par la méthode choisie" - case .calendarAccessDenied: - return "Padel Club n'a pas accès à votre calendrier" - case .calendarEventSaveFailed: - return "Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier" - case .noCalendarAvailable: - return "Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi" - } - } - - public static func getNetworkErrorMessage(sentError: ContactManagerError?, networkMonitorConnected: Bool) -> String { - var errors: [String] = [] - - if networkMonitorConnected == false { - errors.append("L'appareil n'est pas connecté à internet.") - } - if let sentError { - errors.append(sentError.localizedDescription) - } - return errors.joined(separator: "\n") - } -} - -public enum ContactType: Identifiable { - case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?) - case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?) - - public var id: Int { - switch self { - case .message: return 0 - case .mail: return 1 - } - } - - public static let defaultAvailablePaymentMethods: String = "Règlement possible par chèque ou espèces." - -} diff --git a/PadelClubData/Data/.DS_Store b/PadelClubData/Data/.DS_Store index 5fc72cc..652a0a0 100644 Binary files a/PadelClubData/Data/.DS_Store and b/PadelClubData/Data/.DS_Store differ diff --git a/PadelClubData/Data/AppSettings.swift b/PadelClubData/Data/AppSettings.swift index f2fad36..1703b03 100644 --- a/PadelClubData/Data/AppSettings.swift +++ b/PadelClubData/Data/AppSettings.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -public final class AppSettings: MicroStorable { +final public class AppSettings: MicroStorable { public var lastDataSource: String? = nil public var didCreateAccount: Bool = false diff --git a/PadelClubData/Data/Club.swift b/PadelClubData/Data/Club.swift index 2370b6a..74bd667 100644 --- a/PadelClubData/Data/Club.swift +++ b/PadelClubData/Data/Club.swift @@ -10,10 +10,8 @@ import SwiftUI import LeStorage @Observable -public final class Club: BaseClub { - - static var copyServerResponse: Bool { return true } - +final public class Club: BaseClub { + public func clubTitle(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide, .title: diff --git a/PadelClubData/Data/Court.swift b/PadelClubData/Data/Court.swift index 437847d..a2d6c39 100644 --- a/PadelClubData/Data/Court.swift +++ b/PadelClubData/Data/Court.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -public final class Court: BaseCourt { +final public class Court: BaseCourt { static func == (lhs: Court, rhs: Court) -> Bool { lhs.id == rhs.id diff --git a/PadelClubData/Data/CustomUser.swift b/PadelClubData/Data/CustomUser.swift index a7154e6..20e9203 100644 --- a/PadelClubData/Data/CustomUser.swift +++ b/PadelClubData/Data/CustomUser.swift @@ -8,12 +8,48 @@ import Foundation import LeStorage -enum UserRight: Int, Codable { +public enum UserRight: Int, Codable { case none = 0 case edition = 1 case creation = 2 } +public enum RegistrationPaymentMode: Int, Codable { + case disabled = 0 + case corporate = 1 + case noFee = 2 + case stripe = 3 + + public static let stripeFixedFee = 0.25 // Fixed fee in euros + public static let stripePercentageFee = 0.014 // 1.4% + + public func canEnableOnlinePayment() -> Bool { + switch self { + case .disabled: + return false + case .corporate: + return true + case .noFee: + return true + case .stripe: + return true + } + } + + public func requiresStripe() -> Bool { + switch self { + case .disabled: + return false + case .corporate: + return true + case .noFee: + return true + case .stripe: + return true + } + } +} + @Observable public class CustomUser: BaseCustomUser, UserBase { @@ -48,7 +84,7 @@ public class CustomUser: BaseCustomUser, UserBase { // // var deviceId: String? - init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) { + public init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) { super.init(username: username, email: email, firstName: firstName, lastName: lastName, phone: phone, country: country, loserBracketMode: loserBracketMode) @@ -129,7 +165,10 @@ public class CustomUser: BaseCustomUser, UserBase { } } -// enum CodingKeys: String, CodingKey { + public func canEnableOnlinePayment() -> Bool { + registrationPaymentMode.canEnableOnlinePayment() + } +// public enum CodingKeys: String, CodingKey { // case _id = "id" // case _lastUpdate = "lastUpdate" // case _username = "username" diff --git a/PadelClubData/Data/DataStore.swift b/PadelClubData/Data/DataStore.swift index 67f36f3..17229cc 100644 --- a/PadelClubData/Data/DataStore.swift +++ b/PadelClubData/Data/DataStore.swift @@ -39,7 +39,7 @@ public class DataStore: ObservableObject { public fileprivate(set) var dateIntervals: SyncedCollection public fileprivate(set) var purchases: SyncedCollection - fileprivate var userStorage: StoredSingleton + public var userStorage: StoredSingleton fileprivate var _temporaryLocalUser: OptionalStorage = OptionalStorage(fileName: "tmp_local_user.json") public fileprivate(set) var appSettingsStorage: MicroStorage = MicroStorage(fileName: "appsettings.json") @@ -282,8 +282,16 @@ public class DataStore: ObservableObject { // MARK: - Convenience + private var _lastRunningCheckDate: Date? = nil + private var _cachedRunningMatches: [Match]? = nil + public func runningMatches() -> [Match] { let dateNow : Date = Date() + if let lastCheck = _lastRunningCheckDate, + let cachedMatches = _cachedRunningMatches, + dateNow.timeIntervalSince(lastCheck) < 5 { + return cachedMatches + } let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] @@ -295,11 +303,22 @@ public class DataStore: ObservableObject { runningMatches.append(contentsOf: matches) } } - return runningMatches + _lastRunningCheckDate = dateNow + _cachedRunningMatches = runningMatches + return _cachedRunningMatches! } + private var _lastRunningAndNextCheckDate: Date? = nil + private var _cachedRunningAndNextMatches: [Match]? = nil + public func runningAndNextMatches() -> [Match] { let dateNow : Date = Date() + if let lastCheck = _lastRunningAndNextCheckDate, + let cachedMatches = _cachedRunningAndNextMatches, + dateNow.timeIntervalSince(lastCheck) < 5 { + return cachedMatches + } + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] @@ -310,11 +329,23 @@ public class DataStore: ObservableObject { runningMatches.append(contentsOf: matches) } } - return runningMatches + _lastRunningAndNextCheckDate = dateNow + _cachedRunningAndNextMatches = runningMatches + return _cachedRunningAndNextMatches! } + private var _lastEndCheckDate: Date? = nil + private var _cachedEndMatches: [Match]? = nil + public func endMatches() -> [Match] { let dateNow : Date = Date() + + if let lastCheck = _lastEndCheckDate, + let cachedMatches = _cachedEndMatches, + dateNow.timeIntervalSince(lastCheck) < 5 { + return cachedMatches + } + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] @@ -325,7 +356,18 @@ public class DataStore: ObservableObject { runningMatches.append(contentsOf: matches) } } - return runningMatches.sorted(by: \.endDate!, order: .descending) + + _lastEndCheckDate = dateNow + _cachedEndMatches = runningMatches.sorted(by: \.endDate!, order: .descending) + return _cachedEndMatches! + } + + public func subscriptionUnitlyPayedTournaments(after date: Date) -> Int { + return DataStore.shared.tournaments.count(where: { tournament in + tournament.creationDate > date && + tournament.payment == .subscriptionUnit && + tournament.isCanceled == false && + tournament.isDeleted == false }) } } diff --git a/PadelClubData/Data/DateInterval.swift b/PadelClubData/Data/DateInterval.swift index 6352641..b7cb277 100644 --- a/PadelClubData/Data/DateInterval.swift +++ b/PadelClubData/Data/DateInterval.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -public final class DateInterval: BaseDateInterval { +final public class DateInterval: BaseDateInterval { // static func resourceName() -> String { return "date-intervals" } // static func tokenExemptedMethods() -> [HTTPMethod] { return [] } @@ -45,15 +45,15 @@ public final class DateInterval: BaseDateInterval { startDate.. Bool { + public func isSingleDay() -> Bool { Calendar.current.isDate(startDate, inSameDayAs: endDate) } - func isDateInside(_ date: Date) -> Bool { + public func isDateInside(_ date: Date) -> Bool { date >= startDate && date <= endDate } - func isDateOutside(_ date: Date) -> Bool { + public func isDateOutside(_ date: Date) -> Bool { date <= startDate && date <= endDate && date >= startDate && date >= endDate } diff --git a/PadelClubData/Data/DrawLog.swift b/PadelClubData/Data/DrawLog.swift index e663219..4130f73 100644 --- a/PadelClubData/Data/DrawLog.swift +++ b/PadelClubData/Data/DrawLog.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -public final class DrawLog: BaseDrawLog, SideStorable { +final public class DrawLog: BaseDrawLog, SideStorable { public func tournamentObject() -> Tournament? { Store.main.findById(self.tournament) @@ -70,7 +70,7 @@ public final class DrawLog: BaseDrawLog, SideStorable { return drawMatch()?.matchTitle() ?? "" } - public var tournamentStore: TournamentStore? { + var tournamentStore: TournamentStore? { return TournamentLibrary.shared.store(tournamentId: self.tournament) } @@ -84,7 +84,7 @@ public enum DrawType: Int, Codable { case groupStage case court - func localizedDrawType() -> String { + public func localizedDrawType() -> String { switch self { case .seed: return "Tête de série" diff --git a/PadelClubData/Data/Event.swift b/PadelClubData/Data/Event.swift index de202f5..210c5f7 100644 --- a/PadelClubData/Data/Event.swift +++ b/PadelClubData/Data/Event.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -public final class Event: BaseEvent { +final public class Event: BaseEvent { public init(creator: String? = nil, club: String? = nil, name: String? = nil, tenupId: String? = nil) { super.init(creator: creator, club: club, name: name, tenupId: tenupId) diff --git a/PadelClubData/Data/Gen/BaseClub.swift b/PadelClubData/Data/Gen/BaseClub.swift index c98952d..5262305 100644 --- a/PadelClubData/Data/Gen/BaseClub.swift +++ b/PadelClubData/Data/Gen/BaseClub.swift @@ -10,6 +10,7 @@ public class BaseClub: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "clubs" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = true public var id: String = Store.randomId() public var creator: String? = nil diff --git a/PadelClubData/Data/Gen/BaseCourt.swift b/PadelClubData/Data/Gen/BaseCourt.swift index e799249..e4ca668 100644 --- a/PadelClubData/Data/Gen/BaseCourt.swift +++ b/PadelClubData/Data/Gen/BaseCourt.swift @@ -10,6 +10,7 @@ public class BaseCourt: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "courts" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var index: Int = 0 diff --git a/PadelClubData/Data/Gen/BaseCustomUser.swift b/PadelClubData/Data/Gen/BaseCustomUser.swift index 81f7660..5abcf9e 100644 --- a/PadelClubData/Data/Gen/BaseCustomUser.swift +++ b/PadelClubData/Data/Gen/BaseCustomUser.swift @@ -10,6 +10,7 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "users" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var username: String = "" @@ -32,8 +33,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { public var groupStageMatchFormatPreference: MatchFormat? = nil public var loserBracketMatchFormatPreference: MatchFormat? = nil public var loserBracketMode: LoserBracketMode = .automatic + public var disableRankingFederalRuling: Bool = false public var deviceId: String? = nil public var agents: [String] = [] + public var userRole: Int? = nil + public var registrationPaymentMode: RegistrationPaymentMode = RegistrationPaymentMode.disabled + public var umpireCustomMail: String? = nil + public var umpireCustomContact: String? = nil + public var umpireCustomPhone: String? = nil + public var hideUmpireMail: Bool = false + public var hideUmpirePhone: Bool = true public init( id: String = Store.randomId(), @@ -57,8 +66,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { groupStageMatchFormatPreference: MatchFormat? = nil, loserBracketMatchFormatPreference: MatchFormat? = nil, loserBracketMode: LoserBracketMode = .automatic, + disableRankingFederalRuling: Bool = false, deviceId: String? = nil, - agents: [String] = [] + agents: [String] = [], + userRole: Int? = nil, + registrationPaymentMode: RegistrationPaymentMode = RegistrationPaymentMode.disabled, + umpireCustomMail: String? = nil, + umpireCustomContact: String? = nil, + umpireCustomPhone: String? = nil, + hideUmpireMail: Bool = false, + hideUmpirePhone: Bool = true ) { super.init() self.id = id @@ -82,8 +99,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { self.groupStageMatchFormatPreference = groupStageMatchFormatPreference self.loserBracketMatchFormatPreference = loserBracketMatchFormatPreference self.loserBracketMode = loserBracketMode + self.disableRankingFederalRuling = disableRankingFederalRuling self.deviceId = deviceId self.agents = agents + self.userRole = userRole + self.registrationPaymentMode = registrationPaymentMode + self.umpireCustomMail = umpireCustomMail + self.umpireCustomContact = umpireCustomContact + self.umpireCustomPhone = umpireCustomPhone + self.hideUmpireMail = hideUmpireMail + self.hideUmpirePhone = hideUmpirePhone } required public override init() { super.init() @@ -111,8 +136,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" case _loserBracketMode = "loserBracketMode" + case _disableRankingFederalRuling = "disableRankingFederalRuling" case _deviceId = "deviceId" case _agents = "agents" + case _userRole = "userRole" + case _registrationPaymentMode = "registrationPaymentMode" + case _umpireCustomMail = "umpireCustomMail" + case _umpireCustomContact = "umpireCustomContact" + case _umpireCustomPhone = "umpireCustomPhone" + case _hideUmpireMail = "hideUmpireMail" + case _hideUmpirePhone = "hideUmpirePhone" } required init(from decoder: Decoder) throws { @@ -138,8 +171,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { self.groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference) ?? nil self.loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) ?? nil self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic + self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false self.deviceId = try container.decodeIfPresent(String.self, forKey: ._deviceId) ?? nil self.agents = try container.decodeIfPresent([String].self, forKey: ._agents) ?? [] + self.userRole = try container.decodeIfPresent(Int.self, forKey: ._userRole) ?? nil + self.registrationPaymentMode = try container.decodeIfPresent(RegistrationPaymentMode.self, forKey: ._registrationPaymentMode) ?? RegistrationPaymentMode.disabled + self.umpireCustomMail = try container.decodeIfPresent(String.self, forKey: ._umpireCustomMail) ?? nil + self.umpireCustomContact = try container.decodeIfPresent(String.self, forKey: ._umpireCustomContact) ?? nil + self.umpireCustomPhone = try container.decodeIfPresent(String.self, forKey: ._umpireCustomPhone) ?? nil + self.hideUmpireMail = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpireMail) ?? false + self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true try super.init(from: decoder) } @@ -166,8 +207,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { try container.encode(self.groupStageMatchFormatPreference, forKey: ._groupStageMatchFormatPreference) try container.encode(self.loserBracketMatchFormatPreference, forKey: ._loserBracketMatchFormatPreference) try container.encode(self.loserBracketMode, forKey: ._loserBracketMode) + try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling) try container.encode(self.deviceId, forKey: ._deviceId) try container.encode(self.agents, forKey: ._agents) + try container.encode(self.userRole, forKey: ._userRole) + try container.encode(self.registrationPaymentMode, forKey: ._registrationPaymentMode) + try container.encode(self.umpireCustomMail, forKey: ._umpireCustomMail) + try container.encode(self.umpireCustomContact, forKey: ._umpireCustomContact) + try container.encode(self.umpireCustomPhone, forKey: ._umpireCustomPhone) + try container.encode(self.hideUmpireMail, forKey: ._hideUmpireMail) + try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone) try super.encode(to: encoder) } @@ -194,8 +243,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable { self.groupStageMatchFormatPreference = customuser.groupStageMatchFormatPreference self.loserBracketMatchFormatPreference = customuser.loserBracketMatchFormatPreference self.loserBracketMode = customuser.loserBracketMode + self.disableRankingFederalRuling = customuser.disableRankingFederalRuling self.deviceId = customuser.deviceId self.agents = customuser.agents + self.userRole = customuser.userRole + self.registrationPaymentMode = customuser.registrationPaymentMode + self.umpireCustomMail = customuser.umpireCustomMail + self.umpireCustomContact = customuser.umpireCustomContact + self.umpireCustomPhone = customuser.umpireCustomPhone + self.hideUmpireMail = customuser.hideUmpireMail + self.hideUmpirePhone = customuser.hideUmpirePhone } public static func relationships() -> [Relationship] { diff --git a/PadelClubData/Data/Gen/BaseDateInterval.swift b/PadelClubData/Data/Gen/BaseDateInterval.swift index caadaa3..79e788c 100644 --- a/PadelClubData/Data/Gen/BaseDateInterval.swift +++ b/PadelClubData/Data/Gen/BaseDateInterval.swift @@ -10,6 +10,7 @@ public class BaseDateInterval: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "date-intervals" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var event: String = "" diff --git a/PadelClubData/Data/Gen/BaseDrawLog.swift b/PadelClubData/Data/Gen/BaseDrawLog.swift index 2b619b1..12f0a12 100644 --- a/PadelClubData/Data/Gen/BaseDrawLog.swift +++ b/PadelClubData/Data/Gen/BaseDrawLog.swift @@ -10,6 +10,7 @@ public class BaseDrawLog: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "draw-logs" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var tournament: String = "" diff --git a/PadelClubData/Data/Gen/BaseEvent.swift b/PadelClubData/Data/Gen/BaseEvent.swift index 6b7bcc2..0d2c634 100644 --- a/PadelClubData/Data/Gen/BaseEvent.swift +++ b/PadelClubData/Data/Gen/BaseEvent.swift @@ -10,6 +10,7 @@ public class BaseEvent: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "events" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var creator: String? = nil diff --git a/PadelClubData/Data/Gen/BaseGroupStage.swift b/PadelClubData/Data/Gen/BaseGroupStage.swift index a4f11cb..b727585 100644 --- a/PadelClubData/Data/Gen/BaseGroupStage.swift +++ b/PadelClubData/Data/Gen/BaseGroupStage.swift @@ -10,6 +10,7 @@ public class BaseGroupStage: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "group-stages" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var tournament: String = "" diff --git a/PadelClubData/Data/Gen/BaseMatch.swift b/PadelClubData/Data/Gen/BaseMatch.swift index 22848f3..2665072 100644 --- a/PadelClubData/Data/Gen/BaseMatch.swift +++ b/PadelClubData/Data/Gen/BaseMatch.swift @@ -10,6 +10,7 @@ public class BaseMatch: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "matches" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var round: String? = nil diff --git a/PadelClubData/Data/Gen/BaseMatchScheduler.swift b/PadelClubData/Data/Gen/BaseMatchScheduler.swift index e0af3c6..8e86c9d 100644 --- a/PadelClubData/Data/Gen/BaseMatchScheduler.swift +++ b/PadelClubData/Data/Gen/BaseMatchScheduler.swift @@ -10,6 +10,7 @@ public class BaseMatchScheduler: BaseModelObject, Storable { public static func resourceName() -> String { return "match-scheduler" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var tournament: String = "" diff --git a/PadelClubData/Data/Gen/BaseMonthData.swift b/PadelClubData/Data/Gen/BaseMonthData.swift index 38f7451..9ffe47e 100644 --- a/PadelClubData/Data/Gen/BaseMonthData.swift +++ b/PadelClubData/Data/Gen/BaseMonthData.swift @@ -10,6 +10,7 @@ public class BaseMonthData: BaseModelObject, Storable { public static func resourceName() -> String { return "month-data" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var monthKey: String = "" diff --git a/PadelClubData/Data/Gen/BasePlayerRegistration.swift b/PadelClubData/Data/Gen/BasePlayerRegistration.swift index 00cd033..9cdfcef 100644 --- a/PadelClubData/Data/Gen/BasePlayerRegistration.swift +++ b/PadelClubData/Data/Gen/BasePlayerRegistration.swift @@ -10,6 +10,7 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "player-registrations" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var teamRegistration: String? = nil @@ -33,6 +34,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { public var coach: Bool = false public var captain: Bool = false public var registeredOnline: Bool = false + public var timeToConfirm: Date? = nil + public var registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting + public var paymentId: String? = nil public init( id: String = Store.randomId(), @@ -56,7 +60,10 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { hasArrived: Bool = false, coach: Bool = false, captain: Bool = false, - registeredOnline: Bool = false + registeredOnline: Bool = false, + timeToConfirm: Date? = nil, + registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting, + paymentId: String? = nil ) { super.init() self.id = id @@ -81,6 +88,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.coach = coach self.captain = captain self.registeredOnline = registeredOnline + self.timeToConfirm = timeToConfirm + self.registrationStatus = registrationStatus + self.paymentId = paymentId } required public override init() { super.init() @@ -109,6 +119,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { case _coach = "coach" case _captain = "captain" case _registeredOnline = "registeredOnline" + case _timeToConfirm = "timeToConfirm" + case _registrationStatus = "registrationStatus" + case _paymentId = "paymentId" } required init(from decoder: Decoder) throws { @@ -135,6 +148,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.coach = try container.decodeIfPresent(Bool.self, forKey: ._coach) ?? false self.captain = try container.decodeIfPresent(Bool.self, forKey: ._captain) ?? false self.registeredOnline = try container.decodeIfPresent(Bool.self, forKey: ._registeredOnline) ?? false + self.timeToConfirm = try container.decodeIfPresent(Date.self, forKey: ._timeToConfirm) ?? nil + self.registrationStatus = try container.decodeIfPresent(PlayerRegistration.RegistrationStatus.self, forKey: ._registrationStatus) ?? PlayerRegistration.RegistrationStatus.waiting + self.paymentId = try container.decodeIfPresent(String.self, forKey: ._paymentId) ?? nil try super.init(from: decoder) } @@ -162,6 +178,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { try container.encode(self.coach, forKey: ._coach) try container.encode(self.captain, forKey: ._captain) try container.encode(self.registeredOnline, forKey: ._registeredOnline) + try container.encode(self.timeToConfirm, forKey: ._timeToConfirm) + try container.encode(self.registrationStatus, forKey: ._registrationStatus) + try container.encode(self.paymentId, forKey: ._paymentId) try super.encode(to: encoder) } @@ -194,6 +213,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.coach = playerregistration.coach self.captain = playerregistration.captain self.registeredOnline = playerregistration.registeredOnline + self.timeToConfirm = playerregistration.timeToConfirm + self.registrationStatus = playerregistration.registrationStatus + self.paymentId = playerregistration.paymentId } public static func relationships() -> [Relationship] { diff --git a/PadelClubData/Data/Gen/BasePurchase.swift b/PadelClubData/Data/Gen/BasePurchase.swift index cd4acf3..c1f3736 100644 --- a/PadelClubData/Data/Gen/BasePurchase.swift +++ b/PadelClubData/Data/Gen/BasePurchase.swift @@ -8,6 +8,7 @@ public class BasePurchase: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "purchases" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: UInt64 = 0 public var user: String = "" diff --git a/PadelClubData/Data/Gen/BaseRound.swift b/PadelClubData/Data/Gen/BaseRound.swift index fd35fda..b06a950 100644 --- a/PadelClubData/Data/Gen/BaseRound.swift +++ b/PadelClubData/Data/Gen/BaseRound.swift @@ -10,6 +10,7 @@ public class BaseRound: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "rounds" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var tournament: String = "" diff --git a/PadelClubData/Data/Gen/BaseTeamRegistration.swift b/PadelClubData/Data/Gen/BaseTeamRegistration.swift index 7517b1a..b98b267 100644 --- a/PadelClubData/Data/Gen/BaseTeamRegistration.swift +++ b/PadelClubData/Data/Gen/BaseTeamRegistration.swift @@ -10,6 +10,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "team-registrations" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var tournament: String = "" diff --git a/PadelClubData/Data/Gen/BaseTeamScore.swift b/PadelClubData/Data/Gen/BaseTeamScore.swift index 6b97720..025495c 100644 --- a/PadelClubData/Data/Gen/BaseTeamScore.swift +++ b/PadelClubData/Data/Gen/BaseTeamScore.swift @@ -10,6 +10,7 @@ public class BaseTeamScore: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "team-scores" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var match: String = "" diff --git a/PadelClubData/Data/Gen/BaseTournament.swift b/PadelClubData/Data/Gen/BaseTournament.swift index 4deebf1..68ea612 100644 --- a/PadelClubData/Data/Gen/BaseTournament.swift +++ b/PadelClubData/Data/Gen/BaseTournament.swift @@ -10,6 +10,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { public static func resourceName() -> String { return "tournaments" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static var copyServerResponse: Bool = false public var id: String = Store.randomId() public var event: String? = nil @@ -22,20 +23,20 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { public var roundFormat: MatchFormat? = nil public var loserRoundFormat: MatchFormat? = nil public var groupStageSortMode: GroupStageOrderingMode = GroupStageOrderingMode.snake - public var groupStageCount: Int = 0 + public var groupStageCount: Int = 4 public var rankSourceDate: Date? = nil - public var dayDuration: Int = 0 - public var teamCount: Int = 0 + public var dayDuration: Int = 1 + public var teamCount: Int = 24 public var teamSorting: TeamSortingType = TeamSortingType.inscriptionDate public var federalCategory: TournamentCategory = TournamentCategory.men - public var federalLevelCategory: TournamentLevel = TournamentLevel.unlisted - public var federalAgeCategory: FederalTournamentAge = FederalTournamentAge.unlisted + public var federalLevelCategory: TournamentLevel = TournamentLevel.p100 + public var federalAgeCategory: FederalTournamentAge = FederalTournamentAge.senior public var closedRegistrationDate: Date? = nil public var groupStageAdditionalQualified: Int = 0 public var courtCount: Int = 2 public var prioritizeClubMembers: Bool = false - public var qualifiedPerGroupStage: Int = 0 - public var teamsPerGroupStage: Int = 0 + public var qualifiedPerGroupStage: Int = 1 + public var teamsPerGroupStage: Int = 4 public var entryFee: Double? = nil public var payment: TournamentPayment? = nil public var additionalEstimationDuration: Int = 0 @@ -70,6 +71,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { public var hideUmpirePhone: Bool = true public var disableRankingFederalRuling: Bool = false public var teamCountLimit: Bool = true + public var enableOnlinePayment: Bool = false + public var onlinePaymentIsMandatory: Bool = false + public var enableOnlinePaymentRefund: Bool = false + public var refundDateLimit: Date? = nil + public var stripeAccountId: String? = nil + public var enableTimeToConfirm: Bool = false + public var isCorporateTournament: Bool = false + public var isTemplate: Bool = false public init( id: String = Store.randomId(), @@ -83,20 +92,20 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode = GroupStageOrderingMode.snake, - groupStageCount: Int = 0, + groupStageCount: Int = 4, rankSourceDate: Date? = nil, - dayDuration: Int = 0, - teamCount: Int = 0, + dayDuration: Int = 1, + teamCount: Int = 24, teamSorting: TeamSortingType = TeamSortingType.inscriptionDate, federalCategory: TournamentCategory = TournamentCategory.men, - federalLevelCategory: TournamentLevel = TournamentLevel.unlisted, - federalAgeCategory: FederalTournamentAge = FederalTournamentAge.unlisted, + federalLevelCategory: TournamentLevel = TournamentLevel.p100, + federalAgeCategory: FederalTournamentAge = FederalTournamentAge.senior, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, - qualifiedPerGroupStage: Int = 0, - teamsPerGroupStage: Int = 0, + qualifiedPerGroupStage: Int = 1, + teamsPerGroupStage: Int = 4, entryFee: Double? = nil, payment: TournamentPayment? = nil, additionalEstimationDuration: Int = 0, @@ -130,7 +139,15 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { hideUmpireMail: Bool = false, hideUmpirePhone: Bool = true, disableRankingFederalRuling: Bool = false, - teamCountLimit: Bool = true + teamCountLimit: Bool = true, + enableOnlinePayment: Bool = false, + onlinePaymentIsMandatory: Bool = false, + enableOnlinePaymentRefund: Bool = false, + refundDateLimit: Date? = nil, + stripeAccountId: String? = nil, + enableTimeToConfirm: Bool = false, + isCorporateTournament: Bool = false, + isTemplate: Bool = false ) { super.init() self.id = id @@ -192,6 +209,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { self.hideUmpirePhone = hideUmpirePhone self.disableRankingFederalRuling = disableRankingFederalRuling self.teamCountLimit = teamCountLimit + self.enableOnlinePayment = enableOnlinePayment + self.onlinePaymentIsMandatory = onlinePaymentIsMandatory + self.enableOnlinePaymentRefund = enableOnlinePaymentRefund + self.refundDateLimit = refundDateLimit + self.stripeAccountId = stripeAccountId + self.enableTimeToConfirm = enableTimeToConfirm + self.isCorporateTournament = isCorporateTournament + self.isTemplate = isTemplate } required public override init() { super.init() @@ -259,6 +284,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { case _hideUmpirePhone = "hideUmpirePhone" case _disableRankingFederalRuling = "disableRankingFederalRuling" case _teamCountLimit = "teamCountLimit" + case _enableOnlinePayment = "enableOnlinePayment" + case _onlinePaymentIsMandatory = "onlinePaymentIsMandatory" + case _enableOnlinePaymentRefund = "enableOnlinePaymentRefund" + case _refundDateLimit = "refundDateLimit" + case _stripeAccountId = "stripeAccountId" + case _enableTimeToConfirm = "enableTimeToConfirm" + case _isCorporateTournament = "isCorporateTournament" + case _isTemplate = "isTemplate" } private static func _decodePayment(container: KeyedDecodingContainer) throws -> TournamentPayment? { @@ -341,20 +374,20 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { self.roundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._roundFormat) ?? nil self.loserRoundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserRoundFormat) ?? nil self.groupStageSortMode = try container.decodeIfPresent(GroupStageOrderingMode.self, forKey: ._groupStageSortMode) ?? GroupStageOrderingMode.snake - self.groupStageCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageCount) ?? 0 + self.groupStageCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageCount) ?? 4 self.rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate) ?? nil - self.dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) ?? 0 - self.teamCount = try container.decodeIfPresent(Int.self, forKey: ._teamCount) ?? 0 + self.dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) ?? 1 + self.teamCount = try container.decodeIfPresent(Int.self, forKey: ._teamCount) ?? 24 self.teamSorting = try container.decodeIfPresent(TeamSortingType.self, forKey: ._teamSorting) ?? TeamSortingType.inscriptionDate self.federalCategory = try container.decodeIfPresent(TournamentCategory.self, forKey: ._federalCategory) ?? TournamentCategory.men - self.federalLevelCategory = try container.decodeIfPresent(TournamentLevel.self, forKey: ._federalLevelCategory) ?? TournamentLevel.unlisted - self.federalAgeCategory = try container.decodeIfPresent(FederalTournamentAge.self, forKey: ._federalAgeCategory) ?? FederalTournamentAge.unlisted + self.federalLevelCategory = try container.decodeIfPresent(TournamentLevel.self, forKey: ._federalLevelCategory) ?? TournamentLevel.p100 + self.federalAgeCategory = try container.decodeIfPresent(FederalTournamentAge.self, forKey: ._federalAgeCategory) ?? FederalTournamentAge.senior self.closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate) ?? nil self.groupStageAdditionalQualified = try container.decodeIfPresent(Int.self, forKey: ._groupStageAdditionalQualified) ?? 0 self.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2 self.prioritizeClubMembers = try container.decodeIfPresent(Bool.self, forKey: ._prioritizeClubMembers) ?? false - self.qualifiedPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._qualifiedPerGroupStage) ?? 0 - self.teamsPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._teamsPerGroupStage) ?? 0 + self.qualifiedPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._qualifiedPerGroupStage) ?? 1 + self.teamsPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._teamsPerGroupStage) ?? 4 self.entryFee = try container.decodeIfPresent(Double.self, forKey: ._entryFee) ?? nil self.payment = try Self._decodePayment(container: container) self.additionalEstimationDuration = try container.decodeIfPresent(Int.self, forKey: ._additionalEstimationDuration) ?? 0 @@ -389,6 +422,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false self.teamCountLimit = try container.decodeIfPresent(Bool.self, forKey: ._teamCountLimit) ?? true + self.enableOnlinePayment = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlinePayment) ?? false + self.onlinePaymentIsMandatory = try container.decodeIfPresent(Bool.self, forKey: ._onlinePaymentIsMandatory) ?? false + self.enableOnlinePaymentRefund = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlinePaymentRefund) ?? false + self.refundDateLimit = try container.decodeIfPresent(Date.self, forKey: ._refundDateLimit) ?? nil + self.stripeAccountId = try container.decodeIfPresent(String.self, forKey: ._stripeAccountId) ?? nil + self.enableTimeToConfirm = try container.decodeIfPresent(Bool.self, forKey: ._enableTimeToConfirm) ?? false + self.isCorporateTournament = try container.decodeIfPresent(Bool.self, forKey: ._isCorporateTournament) ?? false + self.isTemplate = try container.decodeIfPresent(Bool.self, forKey: ._isTemplate) ?? false try super.init(from: decoder) } @@ -453,6 +494,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone) try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling) try container.encode(self.teamCountLimit, forKey: ._teamCountLimit) + try container.encode(self.enableOnlinePayment, forKey: ._enableOnlinePayment) + try container.encode(self.onlinePaymentIsMandatory, forKey: ._onlinePaymentIsMandatory) + try container.encode(self.enableOnlinePaymentRefund, forKey: ._enableOnlinePaymentRefund) + try container.encode(self.refundDateLimit, forKey: ._refundDateLimit) + try container.encode(self.stripeAccountId, forKey: ._stripeAccountId) + try container.encode(self.enableTimeToConfirm, forKey: ._enableTimeToConfirm) + try container.encode(self.isCorporateTournament, forKey: ._isCorporateTournament) + try container.encode(self.isTemplate, forKey: ._isTemplate) try super.encode(to: encoder) } @@ -522,6 +571,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable { self.hideUmpirePhone = tournament.hideUmpirePhone self.disableRankingFederalRuling = tournament.disableRankingFederalRuling self.teamCountLimit = tournament.teamCountLimit + self.enableOnlinePayment = tournament.enableOnlinePayment + self.onlinePaymentIsMandatory = tournament.onlinePaymentIsMandatory + self.enableOnlinePaymentRefund = tournament.enableOnlinePaymentRefund + self.refundDateLimit = tournament.refundDateLimit + self.stripeAccountId = tournament.stripeAccountId + self.enableTimeToConfirm = tournament.enableTimeToConfirm + self.isCorporateTournament = tournament.isCorporateTournament + self.isTemplate = tournament.isTemplate } public static func relationships() -> [Relationship] { diff --git a/PadelClubData/Data/Gen/Club.json b/PadelClubData/Data/Gen/Club.json index 14a9fc7..cdaf5e2 100644 --- a/PadelClubData/Data/Gen/Club.json +++ b/PadelClubData/Data/Gen/Club.json @@ -4,6 +4,7 @@ "name": "Club", "synchronizable": true, "observable": true, + "copy_server_response": "true", "properties": [ { "name": "id", diff --git a/PadelClubData/Data/Gen/CustomUser.json b/PadelClubData/Data/Gen/CustomUser.json index ac32c3c..763d975 100644 --- a/PadelClubData/Data/Gen/CustomUser.json +++ b/PadelClubData/Data/Gen/CustomUser.json @@ -119,6 +119,11 @@ "type": "LoserBracketMode", "defaultValue": ".automatic" }, + { + "name": "disableRankingFederalRuling", + "type": "Bool", + "defaultValue": "false" + }, { "name": "deviceId", "type": "String", @@ -129,6 +134,42 @@ "name": "agents", "type": "[String]", "defaultValue": "[]" + }, + { + "name": "userRole", + "type": "Int", + "optional": true, + "defaultValue": "nil" + }, + { + "name": "registrationPaymentMode", + "type": "RegistrationPaymentMode", + "defaultValue": "RegistrationPaymentMode.disabled" + }, + { + "name": "umpireCustomMail", + "type": "String", + "optional": true + }, + { + "name": "umpireCustomContact", + "type": "String", + "optional": true + }, + { + "name": "umpireCustomPhone", + "type": "String", + "optional": true + }, + { + "name": "hideUmpireMail", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "hideUmpirePhone", + "type": "Bool", + "defaultValue": "true" } ] } diff --git a/PadelClubData/Data/Gen/PlayerRegistration.json b/PadelClubData/Data/Gen/PlayerRegistration.json index 1f5eb98..72dc484 100644 --- a/PadelClubData/Data/Gen/PlayerRegistration.json +++ b/PadelClubData/Data/Gen/PlayerRegistration.json @@ -115,6 +115,22 @@ "name": "registeredOnline", "type": "Bool", "defaultValue": "false" + }, + { + "name": "timeToConfirm", + "type": "Date", + "optional": true + }, + { + "name": "registrationStatus", + "type": "PlayerRegistration.RegistrationStatus", + "choices": "PlayerRegistration.RegistrationStatus", + "defaultValue": "PlayerRegistration.RegistrationStatus.waiting" + }, + { + "name": "paymentId", + "type": "String", + "optional": true } ] } diff --git a/PadelClubData/Data/Gen/Tournament.json b/PadelClubData/Data/Gen/Tournament.json index 1262af0..ce4f01c 100644 --- a/PadelClubData/Data/Gen/Tournament.json +++ b/PadelClubData/Data/Gen/Tournament.json @@ -66,7 +66,8 @@ }, { "name": "groupStageCount", - "type": "Int" + "type": "Int", + "defaultValue": "4" }, { "name": "rankSourceDate", @@ -75,11 +76,13 @@ }, { "name": "dayDuration", - "type": "Int" + "type": "Int", + "defaultValue": "1" }, { "name": "teamCount", - "type": "Int" + "type": "Int", + "defaultValue": "24" }, { "name": "teamSorting", @@ -94,12 +97,12 @@ { "name": "federalLevelCategory", "type": "TournamentLevel", - "defaultValue": "TournamentLevel.unlisted" + "defaultValue": "TournamentLevel.p100" }, { "name": "federalAgeCategory", "type": "FederalTournamentAge", - "defaultValue": "FederalTournamentAge.unlisted" + "defaultValue": "FederalTournamentAge.senior" }, { "name": "closedRegistrationDate", @@ -108,7 +111,8 @@ }, { "name": "groupStageAdditionalQualified", - "type": "Int" + "type": "Int", + "defaultValue": "0" }, { "name": "courtCount", @@ -117,15 +121,18 @@ }, { "name": "prioritizeClubMembers", - "type": "Bool" + "type": "Bool", + "defaultValue": "false" }, { "name": "qualifiedPerGroupStage", - "type": "Int" + "type": "Int", + "defaultValue": "1" }, { "name": "teamsPerGroupStage", - "type": "Int" + "type": "Int", + "defaultValue": "4" }, { "name": "entryFee", @@ -255,12 +262,12 @@ { "name": "minimumPlayerPerTeam", "type": "Int", - "defaultValue": 2 + "defaultValue": "2" }, { "name": "maximumPlayerPerTeam", "type": "Int", - "defaultValue": 2 + "defaultValue": "2" }, { "name": "information", @@ -295,14 +302,52 @@ { "name": "disableRankingFederalRuling", "type": "Bool", - "defaultValue": "false", - "optional": false + "defaultValue": "false" }, { "name": "teamCountLimit", "type": "Bool", - "defaultValue": "true", - "optional": false + "defaultValue": "true" + }, + { + "name": "enableOnlinePayment", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "onlinePaymentIsMandatory", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "enableOnlinePaymentRefund", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "refundDateLimit", + "type": "Date", + "optional": true + }, + { + "name": "stripeAccountId", + "type": "String", + "optional": true + }, + { + "name": "enableTimeToConfirm", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "isCorporateTournament", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "isTemplate", + "type": "Bool", + "defaultValue": "false" } ] } diff --git a/PadelClubData/Data/Gen/generator.py b/PadelClubData/Data/Gen/generator.py index ec8f2fe..2b8f9b8 100644 --- a/PadelClubData/Data/Gen/generator.py +++ b/PadelClubData/Data/Gen/generator.py @@ -24,6 +24,7 @@ class SwiftModelGenerator: resource = self.make_resource_name(model_name) resource_name = model_data.get("resource_name", resource) token_exempted = model_data.get("tokenExemptedMethods", []) + copy_server_response = model_data.get("copy_server_response", "false") lines = ["// Generated by SwiftModelGenerator", "// Do not modify this file manually", ""] @@ -43,7 +44,7 @@ class SwiftModelGenerator: lines.append("") # Add SyncedStorable protocol requirements - lines.extend(self._generate_protocol_requirements(resource_name, token_exempted)) + lines.extend(self._generate_protocol_requirements(resource_name, token_exempted, copy_server_response)) lines.append("") # Properties @@ -371,7 +372,7 @@ class SwiftModelGenerator: lines.append(" }") return lines - def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str]) -> List[str]: + def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str], copy_server_response: str) -> List[str]: """Generate the static functions required by SyncedStorable protocol.""" # Convert HTTP methods to proper format formatted_methods = [f".{method.lower()}" for method in token_exempted] @@ -380,6 +381,8 @@ class SwiftModelGenerator: return [ f" public static func resourceName() -> String {{ return \"{resource_name}\" }}", f" public static func tokenExemptedMethods() -> [HTTPMethod] {{ return [{methods_str}] }}", + f" public static var copyServerResponse: Bool = {copy_server_response}", + ] def _generate_relationships(self, model_name, properties: List[Dict[str, Any]]) -> List[str]: diff --git a/PadelClubData/Data/GroupStage.swift b/PadelClubData/Data/GroupStage.swift index bf655cb..67f7d18 100644 --- a/PadelClubData/Data/GroupStage.swift +++ b/PadelClubData/Data/GroupStage.swift @@ -11,7 +11,7 @@ internal import Algorithms import SwiftUI @Observable -public final class GroupStage: BaseGroupStage, SideStorable { +final public class GroupStage: BaseGroupStage, SideStorable { public var matchFormat: MatchFormat { get { @@ -22,7 +22,7 @@ public final class GroupStage: BaseGroupStage, SideStorable { } } - var tournamentStore: TournamentStore? { + public var tournamentStore: TournamentStore? { return TournamentLibrary.shared.store(tournamentId: self.tournament) } @@ -65,11 +65,11 @@ public final class GroupStage: BaseGroupStage, SideStorable { } } - var computedOrder: Int { + public var computedOrder: Int { index + step * 100 } - func isRunning() -> Bool { // at least a match has started + public func isRunning() -> Bool { // at least a match has started _matches().anySatisfy({ $0.isRunning() }) } @@ -224,7 +224,7 @@ public final class GroupStage: BaseGroupStage, SideStorable { } } -// func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { +// public func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { // guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } // let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) // if matches.isEmpty && nilIfEmpty { return nil } @@ -250,7 +250,7 @@ public final class GroupStage: BaseGroupStage, SideStorable { matchIndexes.append(index) } } - return _matches().filter { matchIndexes.contains($0.index % matchCount) } + return _matches().filter { matchIndexes.contains($0.index%matchCount) } } public func initialStartDate(forTeam team: TeamRegistration) -> Date? { @@ -338,16 +338,16 @@ public final class GroupStage: BaseGroupStage, SideStorable { } - func indexOf(_ matchIndex: Int) -> Int { + public func indexOf(_ matchIndex: Int) -> Int { _matchOrder().firstIndex(of: matchIndex) ?? matchIndex } - func _matchUp(for matchIndex: Int) -> [Int] { + public func _matchUp(for matchIndex: Int) -> [Int] { let combinations = Array((0.. String { + public func returnMatchesSuffix(for matchIndex: Int) -> String { if matchCount > 0 { let count = _matches().count if count > matchCount * 2 { @@ -362,7 +362,7 @@ public final class GroupStage: BaseGroupStage, SideStorable { return "" } - func localizedMatchUpLabel(for matchIndex: Int) -> String { + public func localizedMatchUpLabel(for matchIndex: Int) -> String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { return "#\(index + 1) vs #\(index2 + 1)" + returnMatchesSuffix(for: matchIndex) @@ -371,11 +371,11 @@ public final class GroupStage: BaseGroupStage, SideStorable { } } - var matchCount: Int { + public var matchCount: Int { (size * (size - 1)) / 2 } - func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { + public func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { let _teams = _teams(for: matchIndex) switch team { case .one: @@ -455,7 +455,7 @@ public final class GroupStage: BaseGroupStage, SideStorable { } - var scoreCache: [Int: TeamGroupStageScore] = [:] + public var scoreCache: [Int: TeamGroupStageScore] = [:] public func computedScore(forTeam team: TeamRegistration, step: Int = 0) -> TeamGroupStageScore? { guard let groupStagePositionAtStep = team.groupStagePositionAtStep(step) else { @@ -528,11 +528,11 @@ public final class GroupStage: BaseGroupStage, SideStorable { } // Clear the cache if necessary, for example when starting a new step or when matches update - func clearScoreCache() { + public func clearScoreCache() { scoreCache.removeAll() } -// func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { +// public func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { // if sortedByScore { // return unsortedTeams().compactMap({ team in // scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) @@ -622,8 +622,8 @@ public final class GroupStage: BaseGroupStage, SideStorable { // step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0 // } // -// func encode(to encoder: Encoder) throws { -// var container = encoder.container(keyedBy: CodingKeys.self) +// public func encode(to encoder: Encoder) throws { +// public var container = encoder.container(keyedBy: CodingKeys.self) // // try container.encode(id, forKey: ._id) // try container.encode(storeId, forKey: ._storeId) diff --git a/PadelClubData/Data/Match.swift b/PadelClubData/Data/Match.swift index 919ad8b..8a37997 100644 --- a/PadelClubData/Data/Match.swift +++ b/PadelClubData/Data/Match.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -public final class Match: BaseMatch, SideStorable { +final public class Match: BaseMatch, SideStorable { static func == (lhs: Match, rhs: Match) -> Bool { lhs.id == rhs.id && lhs.startDate == rhs.startDate @@ -20,7 +20,7 @@ public final class Match: BaseMatch, SideStorable { return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted() } - var byeState: Bool = false + public var byeState: Bool = false public init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) { @@ -88,7 +88,9 @@ defer { #endif if groupStage != nil { return index - } else if let index = (matches ?? roundObject?.playedMatches().sorted(by: \.index))?.firstIndex(where: { $0.id == id }) { + } else if let matches, let index = matches.firstIndex(where: { $0.id == id }) { + return index + } else if let roundObject, roundObject.isUpperBracket(), let index = roundObject.playedMatches().firstIndex(where: { $0.id == id }) { return index } return RoundRule.matchIndexWithinRound(fromMatchIndex: index) @@ -278,7 +280,7 @@ defer { return self.tournamentStore?.matches.first(where: { $0.round == roundObjectNextRound.id && $0.index == nextIndex }) } - func _toggleForwardMatchDisableState(_ state: Bool) { + public func _toggleForwardMatchDisableState(_ state: Bool) { guard let roundObject else { return } guard roundObject.parent != nil else { return } guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } @@ -320,7 +322,7 @@ defer { // forwardMatch._toggleByeState(state) } - func isSeededBy(team: TeamRegistration) -> Bool { + public func isSeededBy(team: TeamRegistration) -> Bool { isSeededBy(team: team, inTeamPosition: .one) || isSeededBy(team: team, inTeamPosition: .two) } @@ -337,36 +339,28 @@ defer { public func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { //if disabled == state { return } + let tournamentStore = self.tournamentStore let currentState = disabled disabled = state if disabled != currentState { - do { - try self.tournamentStore?.teamScores.delete(contentOfs: teamScores) - } catch { - Logger.error(error) - } + tournamentStore?.teamScores.delete(contentOfs: teamScores) } if state == true, state != currentState { let teams = teams() for team in teams { if isSeededBy(team: team) { team.bracketPosition = nil - self.tournamentStore?.teamRegistrations.addOrUpdate(instance: team) + tournamentStore?.teamRegistrations.addOrUpdate(instance: team) } } } //byeState = false - if state != currentState { - roundObject?._cachedSeedInterval = nil + roundObject?.invalidateCache() name = nil - do { - try self.tournamentStore?.matches.addOrUpdate(instance: self) - } catch { - Logger.error(error) - } + tournamentStore?.matches.addOrUpdate(instance: self) } if single == false { @@ -413,15 +407,15 @@ defer { [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ") } - func topPreviousRoundMatchIndex() -> Int { + public func topPreviousRoundMatchIndex() -> Int { return index * 2 + 1 } - func bottomPreviousRoundMatchIndex() -> Int { + public func bottomPreviousRoundMatchIndex() -> Int { return (index + 1) * 2 } - func topPreviousRoundMatch() -> Match? { + public func topPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } let topPreviousRoundMatchIndex = topPreviousRoundMatchIndex() let roundObjectPreviousRoundId = roundObject.previousRound()?.id @@ -430,7 +424,7 @@ defer { }) } - func bottomPreviousRoundMatch() -> Match? { + public func bottomPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } let bottomPreviousRoundMatchIndex = bottomPreviousRoundMatchIndex() let roundObjectPreviousRoundId = roundObject.previousRound()?.id @@ -466,10 +460,10 @@ defer { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) } guard let roundObject else { return index } - return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + indexInRound() + return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + RoundRule.matchIndexWithinRound(fromMatchIndex: index) } - func previousMatches() -> [Match] { + public func previousMatches() -> [Match] { guard let roundObject else { return [] } guard let tournamentStore = self.tournamentStore else { return [] } @@ -613,7 +607,7 @@ defer { } } - func createTeamScores() -> [TeamScore] { + public func createTeamScores() -> [TeamScore] { let teamOne = team(.one) let teamTwo = team(.two) let teams = [teamOne, teamTwo].compactMap({ $0 }).map { TeamScore(match: id, team: $0) } @@ -688,16 +682,16 @@ defer { return currentTournament()?.courtCount ?? 1 } - func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool { + public func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool { let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? [] return courtUsed.contains(courtIndex) == false } - func courtIsPreferred(_ courtIndex: Int) -> Bool { + public func courtIsPreferred(_ courtIndex: Int) -> Bool { return false } - func allCourts() -> [Int] { + public func allCourts() -> [Int] { let availableCourts = Array(0.. [TeamRegistration] { + public func walkoutTeam() -> [TeamRegistration] { //walkout 0 means real walkout, walkout 1 means lucky loser situation return scores().filter({ $0.walkOut == 0 }).compactMap { $0.team } } @@ -779,7 +773,7 @@ defer { return groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject() } - func tournamentId() -> String? { + public func tournamentId() -> String? { return groupStageObject?.tournament ?? roundObject?.tournament } @@ -807,7 +801,7 @@ defer { // return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } } - func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? { + public func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? { guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil } var reverseValue = 1 if teamPosition == team(.two)?.groupStagePositionAtStep(step) { @@ -842,12 +836,12 @@ defer { } } - func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { + public func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let groupStageObject else { return nil } return groupStageObject.team(teamPosition: team, inMatchIndex: index) } - func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { + public func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let roundObject else { return nil } let previousRound = roundObject.previousRound() return roundObject.roundProjectedTeam(team, inMatch: self, previousRound: previousRound) @@ -995,7 +989,7 @@ defer { public typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date) - func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] { + public func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] { guard let tournament = currentTournament() else { return [] } let startDate = Date().withoutSeconds() if runningMatches.isEmpty { diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index a1f6cb7..e2b2466 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -public final class MatchScheduler: BaseMatchScheduler, SideStorable { +final public class MatchScheduler: BaseMatchScheduler, SideStorable { // // init(tournament: String, // timeDifferenceLimit: Int = 5, @@ -63,7 +63,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { // TournamentStore.instance(tournamentId: self.tournament) } - func tournamentObject() -> Tournament? { + public func tournamentObject() -> Tournament? { return Store.main.findById(tournament) } @@ -148,7 +148,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { return lastDate } - func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { + public func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { let _groupStages = groupStages @@ -272,7 +272,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { } - func rotationDifference(loserBracket: Bool) -> Int { + public func rotationDifference(loserBracket: Bool) -> Int { if loserBracket { return loserBracketRotationDifference } else { @@ -280,7 +280,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { } } - func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { + public func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { print("Evaluating match: \(match.roundAndMatchTitle()) in round: \(roundObject.roundTitle()) with index: \(match.index)") if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate { @@ -372,11 +372,11 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { } - func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { + public func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() } - func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] { + public func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] { let byCourt = Dictionary(grouping: slots, by: { $0.courtIndex }) return (byCourt.keys.flatMap { courtIndex in let matchesByCourt = byCourt[courtIndex]?.sorted(by: \.startDate) @@ -390,7 +390,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { ) } - func getAvailableCourts(from matches: [Match]) -> [(Int, Date)] { + public func getAvailableCourts(from matches: [Match]) -> [(Int, Date)] { let validMatches = matches.filter({ $0.courtIndex != nil && $0.startDate != nil }) let byCourt = Dictionary(grouping: validMatches, by: { $0.courtIndex! }) return (byCourt.keys.flatMap { court in @@ -405,7 +405,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { ) } - func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { + public func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { var slots = [TimeMatch]() var _startDate: Date? var rotationIndex = 0 @@ -551,7 +551,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable { return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) } - func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date { + public func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date { var matchPerRound = [String: Int]() var minimumTargetedEndDate = rotationStartDate @@ -833,7 +833,7 @@ struct GroupStageTimeMatch { let groupIndex: Int } -struct TimeMatch { +public struct TimeMatch { let matchID: String let rotationIndex: Int var courtIndex: Int @@ -841,7 +841,7 @@ struct TimeMatch { var durationLeft: Int //in minutes var minimumBreakTime: Int //in minutes - func estimatedEndDate(includeBreakTime: Bool) -> Date { + public func estimatedEndDate(includeBreakTime: Bool) -> Date { let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) return startDate.addingTimeInterval(minutesToAdd * 60.0) } @@ -851,14 +851,14 @@ struct TimeMatch { } } -struct GroupStageMatchDispatcher { +public struct GroupStageMatchDispatcher { let timedMatches: [GroupStageTimeMatch] let freeCourtPerRotation: [Int: [Int]] let rotationCount: Int let groupLastRotation: [Int: Int] } -struct MatchDispatcher { +public struct MatchDispatcher { let timedMatches: [TimeMatch] let freeCourtPerRotation: [Int: [Int]] let rotationCount: Int @@ -866,7 +866,7 @@ struct MatchDispatcher { } extension Match { - func teamIds() -> [String] { + public func teamIds() -> [String] { return teams().map { $0.id } } @@ -878,7 +878,7 @@ extension Match { matchUp().contains(id) } - func matchUp() -> [String] { + public func matchUp() -> [String] { guard let groupStageObject else { return [] } diff --git a/PadelClubData/Data/MonthData.swift b/PadelClubData/Data/MonthData.swift index 55cca06..c12e235 100644 --- a/PadelClubData/Data/MonthData.swift +++ b/PadelClubData/Data/MonthData.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -public final class MonthData: BaseMonthData { +final public class MonthData: BaseMonthData { public init(monthKey: String) { super.init() diff --git a/PadelClubData/Data/PlayerRegistration.swift b/PadelClubData/Data/PlayerRegistration.swift index 23ef454..5451fef 100644 --- a/PadelClubData/Data/PlayerRegistration.swift +++ b/PadelClubData/Data/PlayerRegistration.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -public final class PlayerRegistration: BasePlayerRegistration, SideStorable { +final public class PlayerRegistration: BasePlayerRegistration, SideStorable { public func localizedSourceLabel() -> String { switch source { @@ -26,7 +26,7 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable { } } - public init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false) { + public init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false, coach: Bool = false, captain: Bool = false, registeredOnline: Bool = false, timeToConfirm: Date? = nil, registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting, paymentId: String? = nil) { super.init() self.teamRegistration = teamRegistration self.firstName = firstName @@ -46,6 +46,12 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable { self.computedRank = computedRank self.source = source self.hasArrived = hasArrived + self.coach = coach + self.captain = captain + self.registeredOnline = registeredOnline + self.timeToConfirm = timeToConfirm + self.registrationStatus = registrationStatus + self.paymentId = paymentId } required init(from decoder: any Decoder) throws { @@ -89,7 +95,7 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable { public func pasteData(_ exportFormat: ExportFormat = .rawText) -> String { switch exportFormat { case .rawText: - return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator()) + return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator()) case .csv: return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator()) } @@ -187,7 +193,7 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable { return "non classé" + (isMalePlayer() ? "" : "e") } } - + public func setComputedRank(in tournament: Tournament) { let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000 switch tournament.tournamentCategory { @@ -212,11 +218,45 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable { } } + public func hasPaidOnline() -> Bool { + registrationStatus == .confirmed && paymentId != nil && paymentType == .creditCard + } + + public func hasConfirmed() -> Bool { + registrationStatus == .confirmed + } + + public func confirmRegistration() { + registrationStatus = .confirmed + } + public enum PlayerDataSource: Int, Codable { case frenchFederation = 0 case beachPadel = 1 } + public enum RegistrationStatus: Int, Codable, CaseIterable, Identifiable { + case waiting = 0 + case pending = 1 + case confirmed = 2 + case canceled = 3 + + public var id: Int { self.rawValue } + + public func localizedRegistrationStatus() -> String { + switch self { + case .waiting: + return "En attente" + case .pending: + return "En cours" + case .confirmed: + return "Confirmé" + case .canceled: + return "Annulé" + } + } + } + public static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { switch playerRank { case 0: return 0 diff --git a/PadelClubData/Data/Purchase.swift b/PadelClubData/Data/Purchase.swift index 89c069d..fd0e6c1 100644 --- a/PadelClubData/Data/Purchase.swift +++ b/PadelClubData/Data/Purchase.swift @@ -56,7 +56,7 @@ public class Purchase: BasePurchase { // case expirationDate // } - func isValid() -> Bool { + public func isValid() -> Bool { guard self.revocationDate == nil else { return false } @@ -66,7 +66,7 @@ public class Purchase: BasePurchase { return expiration > Date() } -// func encode(to encoder: Encoder) throws { +// public func encode(to encoder: Encoder) throws { // var container = encoder.container(keyedBy: CodingKeys.self) // // try container.encode(self.id, forKey: .id) diff --git a/PadelClubData/Data/Round.swift b/PadelClubData/Data/Round.swift index fb24760..12c0ccf 100644 --- a/PadelClubData/Data/Round.swift +++ b/PadelClubData/Data/Round.swift @@ -10,10 +10,12 @@ import LeStorage import SwiftUI @Observable -public final class Round: BaseRound, SideStorable { - - var _cachedSeedInterval: SeedInterval? +final public class Round: BaseRound, SideStorable { + private var _cachedSeedInterval: SeedInterval? + private var _cachedLoserRounds: [Round]? + private var _cachedLoserRoundsAndChildren: [Round]? + public init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { super.init(tournament: tournament, index: index, parent: parent, format: matchFormat, startDate: startDate, groupStageLoserBracket: groupStageLoserBracket, loserBracketMode: loserBracketMode) @@ -45,7 +47,16 @@ public final class Round: BaseRound, SideStorable { public func tournamentObject() -> Tournament? { return Store.main.findById(tournament) } - + + public func _unsortedMatches(includeDisabled: Bool) -> [Match] { + guard let tournamentStore = self.tournamentStore else { return [] } + if includeDisabled { + return tournamentStore.matches.filter { $0.round == self.id } + } else { + return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false } + } + } + public func _matches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter { $0.round == self.id }.sorted(by: \.index) @@ -78,7 +89,20 @@ public final class Round: BaseRound, SideStorable { return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false } } + + public func upperMatches(upperRound: Round, match: Match) -> [Match] { + let matchIndex = match.index + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) + return [upperRound.getMatch(atMatchIndexInRound: indexInRound * 2), upperRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 }) + } + public func previousMatches(previousRound: Round, match: Match) -> [Match] { + guard let tournamentStore = self.tournamentStore else { return [] } + return tournamentStore.matches.filter { + $0.round == previousRound.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) + } + } + public func upperMatches(ofMatch match: Match) -> [Match] { if parent != nil, previousRound() == nil, let parentRound { let matchIndex = match.index @@ -164,12 +188,12 @@ public final class Round: BaseRound, SideStorable { public func losers() -> [TeamRegistration] { - let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } + let teamIds: [String] = self._unsortedMatches(includeDisabled: false).compactMap { $0.losingTeamId } return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) } } public func winners() -> [TeamRegistration] { - let teamIds: [String] = self._matches().compactMap { $0.winningTeamId } + let teamIds: [String] = self._unsortedMatches(includeDisabled: false).compactMap { $0.winningTeamId } return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) } } @@ -302,10 +326,10 @@ defer { public func enabledMatches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } - return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false }.sorted(by: \.index) + return tournamentStore.matches.filter { $0.disabled == false && $0.round == self.id }.sorted(by: \.index) } -// func displayableMatches() -> [Match] { +// public func displayableMatches() -> [Match] { //#if _DEBUG_TIME //DEBUGING TIME // let start = Date() // defer { @@ -353,17 +377,21 @@ defer { public func loserRounds(forRoundIndex roundIndex: Int, loserRoundsAndChildren: [Round]) -> [Round] { return loserRoundsAndChildren.filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount) } + + public func isEnabled() -> Bool { + return _unsortedMatches(includeDisabled: false).isEmpty == false + } public func isDisabled() -> Bool { - return _matches().allSatisfy({ $0.disabled }) + return _unsortedMatches(includeDisabled: true).allSatisfy({ $0.disabled }) } public func isRankDisabled() -> Bool { - return _matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty }) + return _unsortedMatches(includeDisabled: true).allSatisfy({ $0.disabled && $0.teamScores.isEmpty }) } - func resetFromRoundAllMatchesStartDate() { - _matches().forEach({ + public func resetFromRoundAllMatchesStartDate() { + _unsortedMatches(includeDisabled: false).forEach({ $0.startDate = nil }) loserRoundsAndChildren().forEach { round in @@ -372,8 +400,8 @@ defer { nextRound()?.resetFromRoundAllMatchesStartDate() } - func resetFromRoundAllMatchesStartDate(from match: Match) { - let matches = _matches() + public func resetFromRoundAllMatchesStartDate(from match: Match) { + let matches = _unsortedMatches(includeDisabled: false) if let index = matches.firstIndex(where: { $0.id == match.id }) { matches[index...].forEach { match in match.startDate = nil @@ -386,10 +414,36 @@ defer { } public func getActiveLoserRound() -> Round? { - let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed() - return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first + // Get all loser rounds once + let allLoserRounds = loserRounds() + var lowestIndexRound: Round? = nil + let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) + let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) + + for currentIndex in 0..100 rounds + // Find non-disabled round with current index + let roundAtIndex = allLoserRounds.first(where: { $0.index == currentIndex && $0.isEnabled() }) + + // No round at this index, we've checked all available rounds + if roundAtIndex == nil { + break + } + + // Save the first non-disabled round we find (should be index 0) + if lowestIndexRound == nil { + lowestIndexRound = roundAtIndex + } + + // If this round is active, return it immediately + if roundAtIndex!.hasStarted() && !roundAtIndex!.hasEnded() { + return roundAtIndex + } + } + + // If no active round found, return the one with lowest index + return lowestIndexRound } - + public func enableRound() { _toggleRound(disable: false) } @@ -399,7 +453,7 @@ defer { } private func _toggleRound(disable: Bool) { - let _matches = _matches() + let _matches = _unsortedMatches(includeDisabled: true) _matches.forEach { match in match.disabled = disable match.resetMatch() @@ -414,7 +468,7 @@ defer { } public var cumulativeMatchCount: Int { - var totalMatches = playedMatches().count + var totalMatches = _unsortedMatches(includeDisabled: false).count if let parentRound { totalMatches += parentRound.cumulativeMatchCount } @@ -443,11 +497,12 @@ defer { } public func disabledMatches() -> [Match] { - return _matches().filter({ $0.disabled }) + guard let tournamentStore = self.tournamentStore else { return [] } + return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true } } public func allLoserRoundMatches() -> [Match] { - loserRoundsAndChildren().flatMap({ $0._matches() }) + loserRoundsAndChildren().flatMap({ $0._unsortedMatches(includeDisabled: false) }) } public var theoryCumulativeMatchCount: Int { @@ -496,7 +551,7 @@ defer { return seedInterval.localizedLabel(displayStyle) } - func hasNextRound() -> Bool { + public func hasNextRound() -> Bool { return nextRound()?.isRankDisabled() == false } @@ -520,7 +575,7 @@ defer { let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - print("func seedInterval(initialMode)", initialMode, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + print("func seedInterval(initialMode)", id, index, initialMode, _cachedSeedInterval, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif @@ -560,15 +615,19 @@ defer { if let previousRound = previousRound() { if (previousRound.enabledMatches().isEmpty == false || initialMode) { - return previousRound.seedInterval(initialMode: initialMode)?.chunks()?.first + _cachedSeedInterval = previousRound.seedInterval(initialMode: initialMode)?.chunks()?.first + return _cachedSeedInterval } else { - return previousRound.seedInterval(initialMode: initialMode) + _cachedSeedInterval = previousRound.seedInterval(initialMode: initialMode) + return _cachedSeedInterval } } else if let parentRound { if parentRound.isUpperBracket() { - return parentRound.seedInterval(initialMode: initialMode) + _cachedSeedInterval = parentRound.seedInterval(initialMode: initialMode) + return _cachedSeedInterval } - return parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last + _cachedSeedInterval = parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last + return _cachedSeedInterval } return nil @@ -599,18 +658,24 @@ defer { tournamentObject?.updateTournamentState() } - public func roundStatus() -> String { - let hasEnded = hasEnded() - if hasStarted() && hasEnded == false { + public func roundStatus(playedMatches: [Match]) -> String { + let hasEnded = playedMatches.anySatisfy({ $0.hasEnded() == false }) == false + let hasStarted = playedMatches.anySatisfy({ $0.hasStarted() }) + if hasStarted && hasEnded == false { return "en cours" } else if hasEnded { return "terminée" + } else if let tournamentObject = tournamentObject(), tournamentObject.groupStagesAreOver() == false { + return "en attente" } else { return "à démarrer" } } public func loserRounds() -> [Round] { + if let _cachedLoserRounds { + return _cachedLoserRounds + } guard let tournamentStore = self.tournamentStore else { return [] } #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -620,12 +685,57 @@ defer { } #endif - return tournamentStore.rounds.filter( { $0.parent == id }).sorted(by: \.index).reversed() + // Filter first to reduce sorting work + let filteredRounds = tournamentStore.rounds.filter { $0.parent == id } + + // Return empty array early if no rounds match + if filteredRounds.isEmpty { + return [] + } + + // Sort directly in descending order to avoid the separate reversed() call + _cachedLoserRounds = filteredRounds.sorted { $0.index > $1.index } + return _cachedLoserRounds! } public func loserRoundsAndChildren() -> [Round] { - let loserRounds = loserRounds() - return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() }) + #if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func loserRoundsAndChildren: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + + // Return cached result if available + if let cached = _cachedLoserRoundsAndChildren { + return cached + } + + // Calculate result if cache is invalid or unavailable + let direct = loserRounds() + + // Return quickly if there are no direct loser rounds + if direct.isEmpty { + // Update cache with empty result + _cachedLoserRoundsAndChildren = [] + return [] + } + + // Pre-allocate capacity to avoid reallocations (estimate based on typical tournament structure) + var allRounds = direct + let estimatedChildrenCount = direct.count * 2 // Rough estimate + allRounds.reserveCapacity(estimatedChildrenCount) + + // Collect all children rounds in one pass + for round in direct { + allRounds.append(contentsOf: round.loserRoundsAndChildren()) + } + + // Store result in cache + _cachedLoserRoundsAndChildren = allRounds + + return allRounds } public func isUpperBracket() -> Bool { @@ -637,43 +747,106 @@ defer { } public func deleteLoserBracket() { - let loserRounds = loserRounds() - self.tournamentStore?.rounds.delete(contentOfs: loserRounds) +#if DEBUG //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func deleteLoserBracket: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } +#endif + self.tournamentStore?.rounds.delete(contentOfs: self.loserRounds()) + self.invalidateCache() } public func buildLoserBracket() { +#if DEBUG //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func buildLoserBracket: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } +#endif guard loserRounds().isEmpty else { return } + self.invalidateCache() let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) guard currentRoundMatchCount > 1 else { return } + guard let tournamentStore else { return } let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) - - var loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat + let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat // if let parentRound { // loserBracketMatchFormat = tournamentObject()?.loserBracketSmartMatchFormat(parentRound.index) // } - + + var titles = [String: String]() + let rounds = (0.. 0 { + match.disabled = true + if upperRound.isUpperBracket(), prmc == 1 { + match.byeState = true + } + } else { + match.disabled = false + } + } + } + tournamentStore?.matches.addOrUpdate(contentOfs: m) + + loserRounds().forEach { loserRound in + loserRound.disableUnplayedLoserBracketMatches() + } + } + public var parentRound: Round? { guard let parent = parent else { return nil } return self.tournamentStore?.rounds.findById(parent) @@ -704,12 +877,18 @@ defer { } public func updateMatchFormatOfAllMatches(_ updatedMatchFormat: MatchFormat) { - let playedMatches = _matches() + let playedMatches = _unsortedMatches(includeDisabled: true) playedMatches.forEach { match in match.matchFormat = updatedMatchFormat } self.tournamentStore?.matches.addOrUpdate(contentOfs: playedMatches) } + + public func invalidateCache() { + _cachedLoserRounds = nil + _cachedSeedInterval = nil + _cachedLoserRoundsAndChildren = nil + } public override func deleteDependencies(shouldBeSynchronized: Bool) { let matches = self._matches() @@ -726,6 +905,7 @@ defer { self.tournamentStore?.rounds.deleteDependencies(loserRounds, shouldBeSynchronized: shouldBeSynchronized) } + // enum CodingKeys: String, CodingKey { // case _id = "id" @@ -754,7 +934,7 @@ defer { // loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic // } // -// func encode(to encoder: Encoder) throws { +// public func encode(to encoder: Encoder) throws { // var container = encoder.container(keyedBy: CodingKeys.self) // // try container.encode(id, forKey: ._id) @@ -773,7 +953,7 @@ defer { func insertOnServer() { self.tournamentStore?.rounds.writeChangeAndInsertOnServer(instance: self) - for match in self._matches() { + for match in self._unsortedMatches(includeDisabled: true) { match.insertOnServer() } } diff --git a/PadelClubData/Data/TeamRegistration.swift b/PadelClubData/Data/TeamRegistration.swift index 7632ef0..32cfc11 100644 --- a/PadelClubData/Data/TeamRegistration.swift +++ b/PadelClubData/Data/TeamRegistration.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -public final class TeamRegistration: BaseTeamRegistration, SideStorable { +final public class TeamRegistration: BaseTeamRegistration, SideStorable { // static func resourceName() -> String { "team-registrations" } // static func tokenExemptedMethods() -> [HTTPMethod] { return [] } @@ -79,6 +79,20 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable { players().anySatisfy({ $0.registeredOnline }) } + public func hasPaidOnline() -> Bool { + players().anySatisfy({ $0.hasPaidOnline() }) + } + + public func hasConfirmed() -> Bool { + players().allSatisfy({ $0.hasConfirmed() }) + } + + public func confirmRegistration() { + let players = players() + players.forEach({ $0.confirmRegistration() }) + tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) + } + public func unrankedOrUnknown() -> Bool { players().anySatisfy({ $0.source == nil }) } @@ -129,7 +143,7 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable { } self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized) } - + public func hasArrived(isHere: Bool = false) { let unsortedPlayers = unsortedPlayers() unsortedPlayers.forEach({ $0.hasArrived = !isHere }) @@ -231,26 +245,26 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable { return currentMatch() != nil } - func currentMatch() -> Match? { + public func currentMatch() -> Match? { return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() }) } - func teamScores() -> [TeamScore] { + public func teamScores() -> [TeamScore] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.teamScores.filter({ $0.teamRegistration == id }) } - func wins() -> [Match] { + public func wins() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter({ $0.winningTeamId == id }) } - func loses() -> [Match] { + public func loses() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter({ $0.losingTeamId == id }) } - func matches() -> [Match] { + public func matches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id }) } @@ -264,7 +278,7 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable { players().map { $0.canonicalName }.joined(separator: " ") } - func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool { + public func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool { guard let codeClubOrClubName else { return true } return unsortedPlayers().anySatisfy({ $0.clubName?.contains(codeClubOrClubName) == true @@ -523,16 +537,25 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable { } } - func coaches() -> [PlayerRegistration] { + public func coaches() -> [PlayerRegistration] { guard let store = self.tournamentStore else { return [] } return store.playerRegistrations.filter { $0.coach } } - func significantPlayerCount() -> Int { + public func setWeight( + from players: [PlayerRegistration], + inTournamentCategory tournamentCategory: TournamentCategory + ) { + let significantPlayerCount = significantPlayerCount() + let sortedPlayers = players.sorted(by: \.computedRank, order: .ascending) + weight = (sortedPlayers.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+) + } + + public func significantPlayerCount() -> Int { return tournamentObject()?.significantPlayerCount() ?? 2 } - func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] { + public func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] { let players = unsortedPlayers() if players.count >= 2 { return [] } let s = players.compactMap { $0.sex?.rawValue } @@ -545,7 +568,7 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable { return missing } - func unrankValue(for malePlayer: Bool) -> Int { + public func unrankValue(for malePlayer: Bool) -> Int { return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000 } @@ -691,19 +714,6 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable { playerRegistration.insertOnServer() } } - - // MARK: - Refacto - - public func setWeight( - from players: [PlayerRegistration], - inTournamentCategory tournamentCategory: TournamentCategory - ) { - - let significantPlayerCount = significantPlayerCount() - let sortedPlayers = players.sorted(by: \.computedRank, order: .ascending) - weight = (sortedPlayers.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+) - } - } diff --git a/PadelClubData/Data/TeamScore.swift b/PadelClubData/Data/TeamScore.swift index 7c0f7f5..c5fb7c7 100644 --- a/PadelClubData/Data/TeamScore.swift +++ b/PadelClubData/Data/TeamScore.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -public final class TeamScore: BaseTeamScore, SideStorable { +final public class TeamScore: BaseTeamScore, SideStorable { // static func resourceName() -> String { "team-scores" } @@ -68,7 +68,7 @@ public final class TeamScore: BaseTeamScore, SideStorable { // MARK: - Computed dependencies - func matchObject() -> Match? { + public func matchObject() -> Match? { return self.tournamentStore?.matches.findById(self.match) } @@ -97,7 +97,7 @@ public final class TeamScore: BaseTeamScore, SideStorable { // case _luckyLoser = "luckyLoser" // } // -// func encode(to encoder: Encoder) throws { +// public func encode(to encoder: Encoder) throws { // var container = encoder.container(keyedBy: CodingKeys.self) // // try container.encode(id, forKey: ._id) diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index fb5499e..dcc9090 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -10,14 +10,14 @@ import LeStorage import SwiftUI @Observable -public final class Tournament: BaseTournament { +final public class Tournament: BaseTournament { //local variable public var refreshInProgress: Bool = false public var lastTeamRefresh: Date? public var refreshRanking: Bool = false - func shouldRefreshTeams(forced: Bool) -> Bool { + public func shouldRefreshTeams(forced: Bool) -> Bool { if forced { return true } @@ -27,98 +27,6 @@ public final class Tournament: BaseTournament { @ObservationIgnored public var navigationPath: [Screen] = [] - -// internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0) { -// super.init() -// } - - - public init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil, - umpireCustomMail: String? = nil, - umpireCustomContact: String? = nil, - umpireCustomPhone: String? = nil, - hideUmpireMail: Bool = false, - hideUmpirePhone: Bool = true, - disableRankingFederalRuling: Bool = false, - teamCountLimit: Bool = true - ) { - super.init() - self.event = event - self.name = name - self.startDate = startDate - self.endDate = endDate - self.creationDate = creationDate -#if DEBUG - self.isPrivate = false -#else - self.isPrivate = isPrivate -#endif - self.groupStageFormat = groupStageFormat - self.roundFormat = roundFormat - self.loserRoundFormat = loserRoundFormat - self.groupStageSortMode = groupStageSortMode - self.groupStageCount = groupStageCount - self.rankSourceDate = rankSourceDate - self.dayDuration = dayDuration - self.teamCount = teamCount - self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType - self.federalCategory = federalCategory - self.federalLevelCategory = federalLevelCategory - self.federalAgeCategory = federalAgeCategory - self.closedRegistrationDate = closedRegistrationDate - self.groupStageAdditionalQualified = groupStageAdditionalQualified - self.courtCount = courtCount - self.prioritizeClubMembers = prioritizeClubMembers - self.qualifiedPerGroupStage = qualifiedPerGroupStage - self.teamsPerGroupStage = teamsPerGroupStage - self.entryFee = entryFee - self.additionalEstimationDuration = additionalEstimationDuration - self.isDeleted = isDeleted -#if DEBUG - self.publishTeams = true - self.publishSummons = true - self.publishBrackets = true - self.publishGroupStages = true - self.publishRankings = true - self.publishTournament = true -#else - self.publishTeams = publishTeams - self.publishSummons = publishSummons - self.publishBrackets = publishBrackets - self.publishGroupStages = publishGroupStages - self.publishRankings = publishRankings - self.publishTournament = publishTournament -#endif - self.shouldVerifyBracket = shouldVerifyBracket - self.shouldVerifyGroupStage = shouldVerifyGroupStage - self.hideTeamsWeight = hideTeamsWeight - self.hidePointsEarned = hidePointsEarned - self.loserBracketMode = loserBracketMode - self.initialSeedRound = initialSeedRound - self.initialSeedCount = initialSeedCount - self.enableOnlineRegistration = enableOnlineRegistration - self.registrationDateLimit = registrationDateLimit - self.openingRegistrationDate = openingRegistrationDate - self.waitingListLimit = waitingListLimit - - self.accountIsRequired = accountIsRequired - self.licenseIsRequired = licenseIsRequired - self.minimumPlayerPerTeam = minimumPlayerPerTeam - self.maximumPlayerPerTeam = maximumPlayerPerTeam - self.information = information - self.umpireCustomMail = umpireCustomMail - self.umpireCustomContact = umpireCustomContact - self.disableRankingFederalRuling = disableRankingFederalRuling - self.teamCountLimit = teamCountLimit - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - required public init() { - super.init() - } public var tournamentStore: TournamentStore? { return TournamentLibrary.shared.store(tournamentId: self.id) @@ -155,6 +63,7 @@ public final class Tournament: BaseTournament { } + // MARK: - Computed Dependencies public func unsortedTeams() -> [TeamRegistration] { @@ -208,7 +117,7 @@ public final class Tournament: BaseTournament { return self.startDate } - func canBePublished() -> Bool { + public func canBePublished() -> Bool { switch state() { case .build, .finished, .running: return unsortedTeams().count > 3 @@ -286,7 +195,7 @@ public final class Tournament: BaseTournament { return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path) } - func courtUsed(runningMatches: [Match]) -> [Int] { + public func courtUsed(runningMatches: [Match]) -> [Int] { #if _DEBUGING_TIME //DEBUGING TIME let start = Date() defer { @@ -392,7 +301,7 @@ defer { return seeds().filter { $0.isSeedable() } } - func lastSeedRound() -> Int { + public func lastSeedRound() -> Int { if let last = seeds().filter({ $0.bracketPosition != nil }).last { return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2) } else { @@ -400,16 +309,16 @@ defer { } } - func getRound(atRoundIndex roundIndex: Int) -> Round? { + public func getRound(atRoundIndex roundIndex: Int) -> Round? { return self.tournamentStore?.rounds.first(where: { $0.index == roundIndex }) // return Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first } - func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { + public func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? [] } - func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { + public func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? [] } public func availableSeedGroups(includeAll: Bool = false) -> [SeedInterval] { @@ -596,28 +505,55 @@ defer { return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first } - func matchesWithSpace() -> [Match] { + public func matchesWithSpace() -> [Match] { getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? [] } public func getActiveRound(withSeeds: Bool = false) -> Round? { let rounds: [Round] = self.rounds() - let unfinishedRounds: [Round] = rounds.filter { $0.hasStarted() && $0.hasEnded() == false } - let sortedRounds: [Round] = unfinishedRounds.sorted(by: \.index).reversed() - let round = sortedRounds.first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first + for round in rounds { + let playedMatches = round.playedMatches() - if withSeeds { - if round?.seeds().isEmpty == false { + // Optimization: If no matches have started in this round, return nil immediately + if !playedMatches.contains(where: { $0.hasStarted() }) { return round - } else { - return nil } - } else { - return round + + if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) { + if withSeeds { + if !round.seeds().isEmpty { + return round + } else { + return nil + } + } else { + return round + } + } } + + return nil } + + public func getActiveRoundAndStatus() -> (Round, String)? { + let rounds: [Round] = self.rounds() + + for round in rounds { + let playedMatches = round.playedMatches() + // Optimization: If no matches have started in this round, return nil immediately + if !playedMatches.contains(where: { $0.hasStarted() }) { + return (round, round.roundStatus(playedMatches: playedMatches)) + } + + if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) { + return (round, round.roundStatus(playedMatches: playedMatches)) + } + } + return nil + } + public func getPlayedMatchDateIntervals(in event: Event) -> [DateInterval] { let allMatches: [Match] = self.allMatches().filter { $0.courtIndex != nil && $0.startDate != nil } return allMatches.map { match in @@ -634,7 +570,7 @@ defer { return tournamentStore.matches.filter { $0.disabled == false } } - func _allMatchesIncludingDisabled() -> [Match] { + public func _allMatchesIncludingDisabled() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return Array(tournamentStore.matches) } @@ -642,7 +578,7 @@ defer { public func rounds() -> [Round] { guard let tournamentStore = self.tournamentStore else { return [] } let rounds: [Round] = tournamentStore.rounds.filter { $0.isUpperBracket() } - return rounds.sorted(by: \.index).reversed() + return rounds.sorted { $0.index > $1.index } } public func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { @@ -655,6 +591,7 @@ defer { return waitingListTeams(in: teams, includingWalkOuts: false) } + public func selectedSortedTeams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -765,7 +702,7 @@ defer { public func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] { var duplicates = [PlayerRegistration]() Set(players.compactMap({ $0.licenceId })).forEach { licenceId in - let found = players.filter({ $0.licenceId == licenceId }) + let found = players.filter({ $0.licenceId?.strippedLicense == licenceId.strippedLicense }) if found.count > 1 { duplicates.append(found.first!) } @@ -828,12 +765,6 @@ defer { } } - - public func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { - guard let seedIndex else { return nil } - return selectedSortedTeams()[safe: seedIndex]?.callDate - } - public func maximumCourtsPerGroupSage() -> Int { if teamsPerGroupStage > 1 { return min(teamsPerGroupStage / 2, courtCount) @@ -841,7 +772,7 @@ defer { return max(1, courtCount) } } - + public func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool { guard let summonDate = team.callDate else { return true } let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate @@ -900,6 +831,10 @@ defer { return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) } + public func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { + guard let seedIndex else { return nil } + return selectedSortedTeams()[safe: seedIndex]?.callDate + } public static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME @@ -1031,7 +966,7 @@ defer { let groupStages = groupStages() var baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - if disableRankingFederalRuling == false { + if disableRankingFederalRuling == false, baseRank > 0 { baseRank += qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - 1 } let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) @@ -1096,6 +1031,19 @@ defer { return rankings } + + public func refreshPointsEarned(assimilationLevel: TournamentLevel? = nil) { + guard let tournamentStore = self.tournamentStore else { return } + let tournamentLevel = assimilationLevel ?? tournamentLevel + let unsortedTeams = unsortedTeams() + unsortedTeams.forEach { team in + if let finalRanking = team.finalRanking { + team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: finalRanking - 1, count: teamCount) + } + } + tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams) + } + public func lockRegistration() { closedRegistrationDate = Date() @@ -1119,6 +1067,17 @@ defer { self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) } + public func updateWeights() { + let teams = self.unsortedTeams() + teams.forEach { team in + let players = team.unsortedPlayers() + players.forEach { $0.setComputedRank(in: self) } + team.setWeight(from: players, inTournamentCategory: tournamentCategory) + self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) + } + self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) + } + public func missingUnrankedValue() -> Bool { return maleUnrankedValue == nil || femaleUnrankedValue == nil } @@ -1136,9 +1095,9 @@ defer { } } let displayStyleCategory = hideSenior ? .short : displayStyle - var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle)] + var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedCategoryLabel(displayStyle, ageCategory: federalAgeCategory)] if displayStyle == .short { - levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedLabel(displayStyle)] + levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedCategoryLabel(displayStyle, ageCategory: federalAgeCategory)] } let array = levelCategory + [federalTournamentAge.localizedFederalAgeLabel(displayStyleCategory)] let title: String = array.filter({ $0.isEmpty == false }).joined(separator: " ") @@ -1151,10 +1110,10 @@ defer { public func localizedTournamentType() -> String { switch tournamentLevel { - case .unlisted: + case .unlisted, .championship: return tournamentLevel.localizedLevelLabel(.short) default: - return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedLabel(.short) + return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedCategoryLabel(.short, ageCategory: federalAgeCategory) } } @@ -1185,6 +1144,7 @@ defer { return groupStageCount * qualifiedPerGroupStage } + public func availableQualifiedTeams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -1225,7 +1185,7 @@ defer { //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } - func groupStageLoserBracketAreOver() -> Bool { + public func groupStageLoserBracketAreOver() -> Bool { guard let groupStageLoserBracket = groupStageLoserBracket() else { return true } @@ -1337,8 +1297,8 @@ defer { cut = TeamRegistration.TeamRange(availableSeeds.first, availableSeeds.last) } - if let round = getActiveRound() { - return ([round.roundTitle(.short), round.roundStatus()].joined(separator: " ").lowercased(), description, cut) + if let roundAndStatus = getActiveRoundAndStatus() { + return ([roundAndStatus.0.roundTitle(.short), roundAndStatus.1].joined(separator: " ").lowercased(), description, cut) } else { return ("", description, nil) } @@ -1353,15 +1313,16 @@ defer { let cut : TeamRegistration.TeamRange? = isAnimation() ? nil : TeamRegistration.TeamRange(groupStageTeams.first, groupStageTeams.last) - let runningGroupStages = groupStages().filter({ $0.isRunning() }) if groupStagesAreOver() { return ("terminées", cut) } + let groupStages = groupStages() + let runningGroupStages = groupStages.filter({ $0.isRunning() }) if runningGroupStages.isEmpty { let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }) if ongoingGroupStages.isEmpty == false { return ("Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut) } - return (groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix, cut) + return (groupStages.count.formatted() + " poule" + groupStages.count.pluralSuffix, cut) } else { return ("Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut) } @@ -1393,6 +1354,23 @@ defer { } } + public func addEmptyTeamRegistration(_ count: Int) { + + guard let tournamentStore = self.tournamentStore else { return } + + let teams = (0.. Date? { + guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } + + let daysOffset = type.daysOffset(level: tournamentLevel) + if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) { + let startOfDay = Calendar.current.startOfDay(for: date) + return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) + } + return nil + } + public func setupDefaultPrivateSettings(templateTournament: Tournament?) { +#if DEBUG + self.isPrivate = false + self.publishTeams = true + self.publishSummons = true + self.publishBrackets = true + self.publishGroupStages = true + self.publishRankings = true + self.publishTournament = true +#else + var shouldBePrivate = templateTournament?.isPrivate ?? true + + if Guard.main.currentPlan == .monthlyUnlimited { + shouldBePrivate = false + } else if Guard.main.purchasedTransactions.isEmpty == false { + shouldBePrivate = false + } + + self.isPrivate = shouldBePrivate +#endif + } + + public func setupUmpireSettings(defaultTournament: Tournament? = nil) { + if let defaultTournament { + self.umpireCustomMail = defaultTournament.umpireCustomMail + self.umpireCustomPhone = defaultTournament.umpireCustomPhone + self.umpireCustomContact = defaultTournament.umpireCustomContact + self.hideUmpireMail = defaultTournament.hideUmpireMail + self.hideUmpirePhone = defaultTournament.hideUmpirePhone + self.disableRankingFederalRuling = defaultTournament.disableRankingFederalRuling + self.loserBracketMode = defaultTournament.loserBracketMode + } else { + let user = DataStore.shared.user + self.umpireCustomMail = user.umpireCustomMail + self.umpireCustomPhone = user.umpireCustomPhone + self.umpireCustomContact = user.umpireCustomContact + self.hideUmpireMail = user.hideUmpireMail + self.hideUmpirePhone = user.hideUmpirePhone + self.disableRankingFederalRuling = user.disableRankingFederalRuling + self.loserBracketMode = user.loserBracketMode + } + } + + public func setupRegistrationSettings(templateTournament: Tournament) { + self.enableOnlineRegistration = templateTournament.enableOnlineRegistration + self.accountIsRequired = templateTournament.accountIsRequired + self.licenseIsRequired = templateTournament.licenseIsRequired + self.minimumPlayerPerTeam = templateTournament.minimumPlayerPerTeam + self.maximumPlayerPerTeam = templateTournament.maximumPlayerPerTeam + self.waitingListLimit = templateTournament.waitingListLimit + self.teamCountLimit = templateTournament.teamCountLimit + self.enableOnlinePayment = templateTournament.enableOnlinePayment + self.onlinePaymentIsMandatory = templateTournament.onlinePaymentIsMandatory + self.enableOnlinePaymentRefund = templateTournament.enableOnlinePaymentRefund + self.stripeAccountId = templateTournament.stripeAccountId + self.enableTimeToConfirm = templateTournament.enableTimeToConfirm + self.isCorporateTournament = templateTournament.isCorporateTournament + + if self.registrationDateLimit == nil, templateTournament.registrationDateLimit != nil { + self.registrationDateLimit = startDate.truncateMinutesAndSeconds() + } + self.openingRegistrationDate = templateTournament.openingRegistrationDate != nil ? creationDate.truncateMinutesAndSeconds() : nil + self.refundDateLimit = templateTournament.enableOnlinePaymentRefund ? startDate.truncateMinutesAndSeconds() : nil + } + public func onlineRegistrationCanBeEnabled() -> Bool { - isAnimation() == false + true +// isAnimation() == false } public func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { @@ -1662,7 +1753,7 @@ defer { } } - func isSameBuild(_ build: any TournamentBuildHolder) -> Bool { + public func isSameBuild(_ build: any TournamentBuildHolder) -> Bool { tournamentLevel == build.level && tournamentCategory == build.category && federalTournamentAge == build.age @@ -1824,7 +1915,7 @@ defer { } public func allLoserRoundMatches() -> [Match] { - rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } + rounds().flatMap { $0.allLoserRoundMatches() } } public func seedsCount() -> Int { @@ -1914,47 +2005,44 @@ defer { } public func addNewRound(_ roundIndex: Int) async { - let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat) - let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) - let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) - let nextRound = round.nextRound() - var currentIndex = 0 - let matches = (0.. Bool { - return false + if hasEnded() { + return true + } + if hasStarted() == false { + return false + } + if hasStarted(), self.startDate.timeIntervalSinceNow > -3600*24 { + return false + } if tournamentStore?.store.fileCollectionsAllLoaded() == false { return false } -#if _DEBUGING_TIME //DEBUGING TIME +#if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) @@ -2057,6 +2153,10 @@ defer { unsortedTeams().filter({ $0.hasRegisteredOnline() }) } + public func paidOnlineTeams() -> [TeamRegistration] { + unsortedTeams().filter({ $0.hasPaidOnline() }) + } + public func shouldWarnOnlineRegistrationUpdates() -> Bool { enableOnlineRegistration && onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false } @@ -2064,12 +2164,17 @@ defer { public func refreshTeamList(forced: Bool) async { guard StoreCenter.main.isAuthenticated else { return } guard tournamentStore?.store.fileCollectionsAllLoaded() == true else { return } - guard shouldRefreshTeams(forced: forced), refreshInProgress == false, enableOnlineRegistration, hasEnded() == false else { return } + guard shouldRefreshTeams(forced: forced), refreshInProgress == false else { return } + if forced == false { + guard enableOnlineRegistration, hasEnded() == false else { + return + } + } refreshInProgress = true do { -// try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true) -// try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true) -// try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true) + try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true) + //try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true) + try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true) refreshInProgress = false lastTeamRefresh = Date() } catch { @@ -2079,6 +2184,11 @@ defer { } } + public func mailSubject() -> String { + let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), clubName].compactMap({ $0 }).joined(separator: " | ") + return subject + } + // MARK: - func insertOnServer() throws { @@ -2104,42 +2214,12 @@ defer { } - // MARK: - Payments & Crypto public enum PaymentError: Error { case cantPayTournament } - // MARK: - Refacto - - - public var tournamentCategory: TournamentCategory { - get { - federalCategory - } - set { - if federalCategory != newValue { - federalCategory = newValue - updateWeights() - } else { - federalCategory = newValue - } - } - } - - func updateWeights() { - let teams = self.unsortedTeams() - teams.forEach { team in - let players = team.unsortedPlayers() - players.forEach { $0.setComputedRank(in: self) } - team.setWeight(from: players, inTournamentCategory: tournamentCategory) - self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) - } - self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) - } - - } extension Bool { @@ -2151,7 +2231,7 @@ extension Bool { return Int.random(in: (5...9)) } } - static func decodeInt(_ int: Int) -> Bool { + public static func decodeInt(_ int: Int) -> Bool { switch int { case (0...4): return true @@ -2197,6 +2277,17 @@ extension Bool { // } //} +public extension Tournament { + + static func getTemplateTournament() -> Tournament? { + return DataStore.shared.tournaments.filter { $0.isTemplate && $0.isDeleted == false }.sorted(by: \.startDate, order: .descending).first + } + + static func fake() -> Tournament { + return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil) + } + +} /// Warning: if the enum has more than 10 cases, the payment algo is broken public enum TournamentPayment: Int, CaseIterable { diff --git a/PadelClubData/Data/TournamentLibrary.swift b/PadelClubData/Data/TournamentLibrary.swift index 062083f..50f2618 100644 --- a/PadelClubData/Data/TournamentLibrary.swift +++ b/PadelClubData/Data/TournamentLibrary.swift @@ -8,7 +8,7 @@ import Foundation import LeStorage -class TournamentLibrary { +public class TournamentLibrary { static let shared: TournamentLibrary = TournamentLibrary() diff --git a/PadelClubData/Extensions/Array+Extensions.swift b/PadelClubData/Extensions/Array+Extensions.swift index 8669783..0c5a1f0 100644 --- a/PadelClubData/Extensions/Array+Extensions.swift +++ b/PadelClubData/Extensions/Array+Extensions.swift @@ -30,8 +30,19 @@ public extension Array { } } +public extension Array where Element: Equatable { + + /// Remove first collection element that is equal to the given `object` or `element`: + mutating func remove(elements: [Element]) { + elements.forEach { + if let index = firstIndex(of: $0) { + remove(at: index) + } + } + } +} -extension Array where Element: CustomStringConvertible { +public extension Array where Element: CustomStringConvertible { func customJoined(separator: String, lastSeparator: String) -> String { switch count { case 0: @@ -48,7 +59,8 @@ extension Array where Element: CustomStringConvertible { } } -extension Dictionary where Key == Int, Value == [String] { + +public extension Dictionary where Key == Int, Value == [String] { mutating func setOrAppend(_ element: String?, at key: Int) { // Check if the element is nil; do nothing if it is guard let element = element else { diff --git a/PadelClubData/Extensions/Color+Extensions.swift b/PadelClubData/Extensions/Color+Extensions.swift new file mode 100644 index 0000000..324efcd --- /dev/null +++ b/PadelClubData/Extensions/Color+Extensions.swift @@ -0,0 +1,69 @@ +// +// Color+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 27/03/2024. +// + +import SwiftUI +import UIKit + +public extension Color { + + func variation(withHueOffset hueOffset: Double = 0, saturationFactor: Double = 0.4, brightnessFactor: Double = 0.8, opacity: Double = 0.5) -> Color { + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + + UIColor(self).getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + + // Apply adjustments + hue += CGFloat(hueOffset) + saturation *= CGFloat(saturationFactor) + brightness *= CGFloat(brightnessFactor) + alpha *= CGFloat(opacity) + + // Clamp values + hue = max(0, min(hue, 1)) + saturation = max(0, min(saturation, 1)) + brightness = max(0, min(brightness, 1)) + alpha = max(0, min(alpha, 1)) + + return Color(hue: Double(hue), saturation: Double(saturation), brightness: Double(brightness), opacity: Double(alpha)) + } + +} + +public extension UIColor { + /// Initialises NSColor from a hexadecimal string. Color is clear if string is invalid. + /// - Parameter fromHex: supported formats are "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA", with or without the # character + convenience init(fromHex:String) { + var r = 0, g = 0, b = 0, a = 255 + let offset = fromHex.hasPrefix("#") ? 1 : 0 + let ch = fromHex.map{$0} + switch(ch.count - offset) { + case 8: + a = 16 * (ch[offset+6].hexDigitValue ?? 0) + (ch[offset+7].hexDigitValue ?? 0) + fallthrough + case 6: + r = 16 * (ch[offset+0].hexDigitValue ?? 0) + (ch[offset+1].hexDigitValue ?? 0) + g = 16 * (ch[offset+2].hexDigitValue ?? 0) + (ch[offset+3].hexDigitValue ?? 0) + b = 16 * (ch[offset+4].hexDigitValue ?? 0) + (ch[offset+5].hexDigitValue ?? 0) + break + case 4: + a = 16 * (ch[offset+3].hexDigitValue ?? 0) + (ch[offset+3].hexDigitValue ?? 0) + fallthrough + case 3: // Three digit #0D3 is the same as six digit #00DD33 + r = 16 * (ch[offset+0].hexDigitValue ?? 0) + (ch[offset+0].hexDigitValue ?? 0) + g = 16 * (ch[offset+1].hexDigitValue ?? 0) + (ch[offset+1].hexDigitValue ?? 0) + b = 16 * (ch[offset+2].hexDigitValue ?? 0) + (ch[offset+2].hexDigitValue ?? 0) + break + default: + a = 0 + break + } + self.init(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: CGFloat(a)/255) + + } +} diff --git a/PadelClubData/Extensions/Date+Extensions.swift b/PadelClubData/Extensions/Date+Extensions.swift index c991acd..286b8c5 100644 --- a/PadelClubData/Extensions/Date+Extensions.swift +++ b/PadelClubData/Extensions/Date+Extensions.swift @@ -6,6 +6,7 @@ // import Foundation + public enum TimeOfDay { case morning case noon @@ -56,6 +57,10 @@ public extension Date { formatted(.dateTime.weekday().day(.twoDigits).month().year()) } + var dateFormatted: String { + formatted(.dateTime.day(.twoDigits).month(.twoDigits).year(.twoDigits)) + } + var monthYearFormatted: String { formatted(.dateTime.month(.wide).year(.defaultDigits)) } @@ -110,7 +115,7 @@ public extension Date { } static var firstDayOfWeek = Calendar.current.firstWeekday - static var capitalizedFirstLettersOfWeekdays: [String] { + static var capitalizedFirstLettersOfWeekdays: [String] = { let calendar = Calendar.current // let weekdays = calendar.shortWeekdaySymbols @@ -129,9 +134,9 @@ public extension Date { } } return weekdays.map { $0.capitalized } - } + }() - static var fullMonthNames: [String] { + static var fullMonthNames: [String] = { let dateFormatter = DateFormatter() dateFormatter.locale = Locale.current @@ -140,7 +145,7 @@ public extension Date { let date = Calendar.current.date(from: DateComponents(year: 2000, month: month, day: 1)) return date.map { dateFormatter.string(from: $0) } } - } + }() var startOfMonth: Date { Calendar.current.dateInterval(of: .month, for: self)!.start diff --git a/PadelClubData/Utils/MySortDescriptor.swift b/PadelClubData/Extensions/MySortDescriptor.swift similarity index 100% rename from PadelClubData/Utils/MySortDescriptor.swift rename to PadelClubData/Extensions/MySortDescriptor.swift diff --git a/PadelClubData/Extensions/NumberFormatter+Extensions.swift b/PadelClubData/Extensions/NumberFormatter+Extensions.swift index 1711cf9..68b231b 100644 --- a/PadelClubData/Extensions/NumberFormatter+Extensions.swift +++ b/PadelClubData/Extensions/NumberFormatter+Extensions.swift @@ -8,13 +8,13 @@ import Foundation public extension NumberFormatter { - static var ordinal: NumberFormatter { + static var ordinal: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .ordinal return formatter - } + }() - static var standard: NumberFormatter { + static var standard: NumberFormatter = { return NumberFormatter() - } + }() } diff --git a/PadelClubData/Extensions/Sequence+Extensions.swift b/PadelClubData/Extensions/Sequence+Extensions.swift index 70ef0c1..db0603b 100644 --- a/PadelClubData/Extensions/Sequence+Extensions.swift +++ b/PadelClubData/Extensions/Sequence+Extensions.swift @@ -29,7 +29,6 @@ public extension Sequence { } public extension Sequence { - func concurrentForEach( _ operation: @escaping (Element) async throws -> Void ) async throws { diff --git a/PadelClubData/Extensions/String+Crypto.swift b/PadelClubData/Extensions/String+Crypto.swift index 903b588..28d6662 100644 --- a/PadelClubData/Extensions/String+Crypto.swift +++ b/PadelClubData/Extensions/String+Crypto.swift @@ -8,14 +8,14 @@ import Foundation import CryptoKit -enum CryptoError: Error { +public enum CryptoError: Error { case invalidUTF8 case cantConvertUTF8 case invalidBase64String case nilSeal } -extension Data { +public extension Data { func encrypt(pass: String) throws -> Data { let key = try self._createSymmetricKey(fromString: pass) @@ -44,3 +44,4 @@ extension Data { } } + diff --git a/PadelClubData/Extensions/String+Extensions.swift b/PadelClubData/Extensions/String+Extensions.swift index c8ea2a6..97986d2 100644 --- a/PadelClubData/Extensions/String+Extensions.swift +++ b/PadelClubData/Extensions/String+Extensions.swift @@ -163,8 +163,50 @@ public extension String { } func licencesFound() -> [String] { - let matches = self.matches(of: /[1-9][0-9]{5,7}/) - return matches.map { String(self[$0.range]) } + // First try to find licenses with format: 5-8 digits followed by optional letter + let precisePattern = /[1-9][0-9]{5,7}[ ]?[A-Za-z]?/ + let preciseMatches = self.matches(of: precisePattern) + let preciseResults = preciseMatches.map { String(self[$0.range]).trimmingCharacters(in: .whitespaces) } + + // If we find potential licenses with the precise pattern + if !preciseResults.isEmpty { + // Filter to only include those with trailing letters + let licensesWithLetters = preciseResults.filter { + let lastChar = $0.last + return lastChar != nil && lastChar!.isLetter + } + + print("🎫 Found \(preciseResults.count) potential licenses, filtering to \(licensesWithLetters.count) with trailing letters") + + // If we have licenses with letters, validate them + if !licensesWithLetters.isEmpty { + let validLicenses = licensesWithLetters.filter { $0.isLicenseNumber } + + // If we have valid licenses, return the numeric part of each + if !validLicenses.isEmpty { + let numericLicenses = validLicenses.map { license -> String in + // Extract just the numeric part (all characters except the last letter) + if let lastChar = license.last, lastChar.isLetter { + return String(license.dropLast()) + } + return license + } + + if numericLicenses.isEmpty == false { + print("🎫 Found valid licenses: \(validLicenses), returning numeric parts: \(numericLicenses)") + return numericLicenses + } + } + } + } + + // Fallback to just number pattern if we didn't find good matches + let numberPattern = /[1-9][0-9]{5,7}/ + let numberMatches = self.matches(of: numberPattern) + let numberResults = numberMatches.map { String(self[$0.range]) } + + print("🎫 Falling back to number-only pattern, found: \(numberResults)") + return numberResults } } @@ -261,7 +303,7 @@ public extension String { /// Formats the birthdate string into "DD/MM/YYYY". /// - Returns: A formatted birthdate string, or the original string if parsing fails. - public func formattedAsBirthdate() -> String { + func formattedAsBirthdate() -> String { if let parsedDate = self.parseAsBirthdate() { let outputFormatter = DateFormatter() outputFormatter.dateFormat = "dd/MM/yyyy" // Desired output format diff --git a/PadelClubData/Extensions/Tournament+Extensions.swift b/PadelClubData/Extensions/Tournament+Extensions.swift deleted file mode 100644 index 907bc6a..0000000 --- a/PadelClubData/Extensions/Tournament+Extensions.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Tournament+Extensions.swift -// PadelClubData -// -// Created by Laurent Morvillier on 17/04/2025. -// - -import Foundation - -extension Tournament { - - public static func fake() -> Tournament { - return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil) - } - -} diff --git a/PadelClubData/Extensions/URL+Extensions.swift b/PadelClubData/Extensions/URL+Extensions.swift index 6afb0c2..4d33dbe 100644 --- a/PadelClubData/Extensions/URL+Extensions.swift +++ b/PadelClubData/Extensions/URL+Extensions.swift @@ -9,13 +9,13 @@ import Foundation public extension URL { - public static var savedDateFormatter: DateFormatter = { + static var savedDateFormatter: DateFormatter = { let df = DateFormatter() df.dateFormat = "DD/MM/yyyy" return df }() - public static var importDateFormatter: DateFormatter = { + static var importDateFormatter: DateFormatter = { let df = DateFormatter() df.dateFormat = "MM-yyyy" return df @@ -89,7 +89,7 @@ public extension URL { return nil } - public func fftImportingMaleUnrankValue() -> Int? { + func fftImportingMaleUnrankValue() -> Int? { // Read the contents of the file guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { return nil @@ -107,7 +107,7 @@ public extension URL { return nil } - public func fileModelIdentifier() -> String? { + func fileModelIdentifier() -> String? { // Read the contents of the file guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { return nil @@ -125,7 +125,7 @@ public extension URL { return nil } - public func fftImportingUncomplete() -> Int? { + func fftImportingUncomplete() -> Int? { // Read the contents of the file guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { return nil diff --git a/PadelClubData/Subscriptions/Guard.swift b/PadelClubData/Subscriptions/Guard.swift index 1e13d4b..06b0aed 100644 --- a/PadelClubData/Subscriptions/Guard.swift +++ b/PadelClubData/Subscriptions/Guard.swift @@ -8,6 +8,7 @@ import Foundation import StoreKit import LeStorage +import Combine @available(iOS 15, *) @objc public class Guard: NSObject { @@ -20,7 +21,7 @@ import LeStorage var updateListenerTask: Task? = nil - fileprivate let _freeTournaments: Int = 3 + public let freeTournaments: Int = 3 override init() { @@ -34,6 +35,7 @@ import LeStorage } catch { Logger.error(error) } + Logger.log("plan = \(String(describing: currentBestPurchase?.productId))") } NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil) @@ -263,8 +265,8 @@ import LeStorage return TournamentPayment.unlimited case .fivePerMonth: if let purchaseDate = self.currentBestPurchase?.purchaseDate { - let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > purchaseDate && $0.payment == .subscriptionUnit && $0.isCanceled == false } - if tournaments.count < StoreItem.five { + let count = DataStore.shared.subscriptionUnitlyPayedTournaments(after: purchaseDate) + if count < StoreItem.five { return TournamentPayment.subscriptionUnit } } @@ -277,7 +279,7 @@ import LeStorage fileprivate func _paymentWithoutSubscription() -> TournamentPayment? { let freelyPayed: Int = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count - if freelyPayed < self._freeTournaments { + if freelyPayed < self.freeTournaments { return TournamentPayment.free } let tournamentCreditCount: Int = self._purchasedTournamentCount() @@ -291,12 +293,14 @@ import LeStorage public var remainingTournaments: Int { let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == TournamentPayment.unit }.count let tournamentCreditCount = self._purchasedTournamentCount() -// let notDeletedTournamentCount = DataStore.shared.tournaments.filter { $0.isDeleted == false }.count - -// Logger.log("unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ") return tournamentCreditCount - unitlyPayed } + public var remainingFreeTournaments: Int { + let freelyPayed = DataStore.shared.tournaments.filter { $0.payment == TournamentPayment.free }.count + return self.freeTournaments - freelyPayed + } + func disconnect() { let purchases = DataStore.shared.purchases purchases.reset() @@ -305,20 +309,21 @@ import LeStorage } public struct PurchaseRow: Identifiable { - public var id: UInt64 - public var name: String public var item: StoreItem public var quantity: Int? + public var expirationDate: Date? + public var remainingCount: Int? = nil - public init(id: UInt64, name: String, item: StoreItem, quantity: Int? = nil) { + public init(id: UInt64, name: String, item: StoreItem, quantity: Int? = nil, expirationDate: Date? = nil, remainingCount: Int? = nil) { self.id = id self.name = name self.item = item self.quantity = quantity + self.expirationDate = expirationDate + self.remainingCount = remainingCount } - } fileprivate extension StoreKit.Transaction { diff --git a/PadelClubData/Subscriptions/StoreItem.swift b/PadelClubData/Subscriptions/StoreItem.swift index 233a721..868d28c 100644 --- a/PadelClubData/Subscriptions/StoreItem.swift +++ b/PadelClubData/Subscriptions/StoreItem.swift @@ -13,9 +13,9 @@ public enum StoreItem: String, Identifiable, CaseIterable { case unit = "app.padelclub.tournament.unit" #if DEBUG - static let five: Int = 2 + public static let five: Int = 2 #else - static let five: Int = 5 + public static let five: Int = 5 #endif public var id: String { return self.rawValue } diff --git a/PadelClubData/Utils/ContactManager.swift b/PadelClubData/Utils/ContactManager.swift new file mode 100644 index 0000000..2422b4c --- /dev/null +++ b/PadelClubData/Utils/ContactManager.swift @@ -0,0 +1,141 @@ +// +// ContactManager.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 19/09/2023. +// + +import Foundation +import SwiftUI +import MessageUI +import LeStorage + +public enum ContactManagerError: LocalizedError { + case mailFailed + case mailNotSent //no network no error + case messageFailed + case messageNotSent //no network no error + case calendarAccessDenied + case calendarEventSaveFailed + case noCalendarAvailable + case uncalledTeams([TeamRegistration]) + + public var localizedDescription: String { + switch self { + case .mailFailed: + return "Le mail n'a pas été envoyé" + case .mailNotSent: + return "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer." + case .messageFailed: + return "Le SMS n'a pas été envoyé" + case .messageNotSent: + return "Le SMS n'a pas été envoyé" + case .uncalledTeams(let array): + let verb = array.count > 1 ? "peuvent" : "peut" + return "Attention, \(array.count) équipe\(array.count.pluralSuffix) ne \(verb) pas être contacté par la méthode choisie" + case .calendarAccessDenied: + return "Padel Club n'a pas accès à votre calendrier" + case .calendarEventSaveFailed: + return "Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier" + case .noCalendarAvailable: + return "Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi" + } + } + + public static func getNetworkErrorMessage(sentError: ContactManagerError?, networkMonitorConnected: Bool) -> String { + var errors: [String] = [] + + if networkMonitorConnected == false { + errors.append("L'appareil n'est pas connecté à internet.") + } + if let sentError { + errors.append(sentError.localizedDescription) + } + return errors.joined(separator: "\n") + } +} + +public enum ContactType: Identifiable { + case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?) + case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?) + + public var id: Int { + switch self { + case .message: return 0 + case .mail: return 1 + } + } +} + +public extension ContactType { + static let defaultCustomMessage: String = +""" +Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me confirmer votre présence avec votre nom et de prévenir votre partenaire. +""" + static let defaultAvailablePaymentMethods: String = "Règlement possible par chèque ou espèces." + + static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { + let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage + let clubName = tournament?.clubName ?? "" + + var text = tournamentCustomMessage + let date = startDate ?? tournament?.startDate ?? Date() + + if let tournament { + text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.title, hideSenior: true)) + text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage) + } + + text = text.replacingOccurrences(of: "#club", with: clubName) + text = text.replacingOccurrences(of: "#manche", with: roundLabel.lowercased()) + text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") + text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))") + + let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament) + + text = text.replacingOccurrences(of: "#signature", with: signature) + return text + } + + static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, reSummon: Bool = false) -> String { + + let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage + + if useFullCustomMessage { + return callingCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel) + } + + let date = startDate ?? tournament?.startDate ?? Date() + + let clubName = tournament?.clubName ?? "" + let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage + let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament) + + let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s" + + var entryFeeMessage: String? { + (DataStore.shared.user.summonsDisplayEntryFee) ? tournament?.entryFeeMessage : nil + } + + var linkMessage: String? { + if let tournament, tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString { + return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink) + } else { + return nil + } + } + + var computedMessage: String { + [entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n") + } + + let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" + + if let tournament { + return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)" + } else { + return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)" + } + } +} + diff --git a/PadelClubData/Business/DisplayContext.swift b/PadelClubData/Utils/DisplayContext.swift similarity index 99% rename from PadelClubData/Business/DisplayContext.swift rename to PadelClubData/Utils/DisplayContext.swift index becb38a..03ad2aa 100644 --- a/PadelClubData/Business/DisplayContext.swift +++ b/PadelClubData/Utils/DisplayContext.swift @@ -27,6 +27,7 @@ public enum SummoningDisplayContext { } public struct DeviceHelper { + public static func isBigScreen() -> Bool { switch UIDevice.current.userInterfaceIdiom { case .pad: // iPads diff --git a/PadelClubData/Business/ExportFormat.swift b/PadelClubData/Utils/ExportFormat.swift similarity index 86% rename from PadelClubData/Business/ExportFormat.swift rename to PadelClubData/Utils/ExportFormat.swift index e60460a..bea2a1d 100644 --- a/PadelClubData/Business/ExportFormat.swift +++ b/PadelClubData/Utils/ExportFormat.swift @@ -13,7 +13,7 @@ public enum ExportFormat: Int, Identifiable, CaseIterable { case rawText case csv - var suffix: String { + public var suffix: String { switch self { case .rawText: return "txt" @@ -31,7 +31,7 @@ public enum ExportFormat: Int, Identifiable, CaseIterable { } } - func newLineSeparator(_ count: Int = 1) -> String { + public func newLineSeparator(_ count: Int = 1) -> String { return Array(repeating: "\n", count: count).joined() } } diff --git a/PadelClubData/Utils/NetworkManager.swift b/PadelClubData/Utils/NetworkManager.swift index 0f32b96..f353427 100644 --- a/PadelClubData/Utils/NetworkManager.swift +++ b/PadelClubData/Utils/NetworkManager.swift @@ -7,10 +7,10 @@ import Foundation -class NetworkManager { - static let shared: NetworkManager = NetworkManager() +public class NetworkManager { + public static let shared: NetworkManager = NetworkManager() - func removeRankingData(lastDateString: String, fileName: String) { + public func removeRankingData(lastDateString: String, fileName: String) { let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv" let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)! @@ -35,7 +35,7 @@ class NetworkManager { // } // } // - func formatDateForHTTPHeader(_ date: Date) -> String { + public func formatDateForHTTPHeader(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss 'GMT'" formatter.locale = Locale(identifier: "en_US_POSIX") @@ -45,7 +45,7 @@ class NetworkManager { } @discardableResult - func downloadRankingData(lastDateString: String, fileName: String) async throws -> Int? { + public func downloadRankingData(lastDateString: String, fileName: String) async throws -> Int? { let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv" @@ -93,7 +93,7 @@ class NetworkManager { return nil } - func checkFileCreationDate(filePath: String) throws -> Date? { + public func checkFileCreationDate(filePath: String) throws -> Date? { let fileManager = FileManager.default let attributes = try fileManager.attributesOfItem(atPath: filePath) return attributes[.creationDate] as? Date diff --git a/PadelClubData/Utils/NetworkManagerError.swift b/PadelClubData/Utils/NetworkManagerError.swift index 970af60..eb91408 100644 --- a/PadelClubData/Utils/NetworkManagerError.swift +++ b/PadelClubData/Utils/NetworkManagerError.swift @@ -7,7 +7,7 @@ import Foundation -enum NetworkManagerError: LocalizedError { +public enum NetworkManagerError: LocalizedError { case maintenance case fileNotYetAvailable case mailFailed @@ -17,7 +17,7 @@ enum NetworkManagerError: LocalizedError { case fileNotModified case fileNotDownloaded(Int) - var errorDescription: String? { + public var errorDescription: String? { switch self { case .maintenance: return "Le site de la FFT est en maintenance" diff --git a/PadelClubData/Utils/PListReader.swift b/PadelClubData/Utils/PListReader.swift index 1e5af5a..e6095fb 100644 --- a/PadelClubData/Utils/PListReader.swift +++ b/PadelClubData/Utils/PListReader.swift @@ -25,7 +25,7 @@ public class PListReader { print("Failed to read plist file at path: \(plistPath)") } } else { - print("Plist file '\(plist)' not found in bundle") + print("Plist file 'Data.plist' not found in bundle") } return nil diff --git a/PadelClubData/Patcher.swift b/PadelClubData/Utils/Patcher.swift similarity index 98% rename from PadelClubData/Patcher.swift rename to PadelClubData/Utils/Patcher.swift index db2efe8..babf700 100644 --- a/PadelClubData/Patcher.swift +++ b/PadelClubData/Utils/Patcher.swift @@ -11,7 +11,7 @@ import LeStorage public enum ManualPatch: String { case disconnect - var id: String { + public var id: String { return "padelclub.app.manual.patch.\(self.rawValue)" } } @@ -63,7 +63,7 @@ enum Patch: String, CaseIterable { public class AutomaticPatcher { - static func applyAllWhenApplicable() { + public static func applyAllWhenApplicable() { for patch in Patch.allCases { self.patchIfPossible(patch) } diff --git a/PadelClubData/SourceFileManager.swift b/PadelClubData/Utils/SourceFileManager.swift similarity index 96% rename from PadelClubData/SourceFileManager.swift rename to PadelClubData/Utils/SourceFileManager.swift index 227629e..7cda9ce 100644 --- a/PadelClubData/SourceFileManager.swift +++ b/PadelClubData/Utils/SourceFileManager.swift @@ -11,7 +11,7 @@ import LeStorage public class SourceFileManager { public static let shared = SourceFileManager() - init() { + public init() { createDirectoryIfNeeded(directoryURL: rankingSourceDirectory) #if targetEnvironment(simulator) createDirectoryIfNeeded(directoryURL: anonymousSourceDirectory) @@ -21,7 +21,7 @@ public class SourceFileManager { public let rankingSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "rankings") public let anonymousSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "anonymous") - func createDirectoryIfNeeded(directoryURL: URL) { + public func createDirectoryIfNeeded(directoryURL: URL) { let fileManager = FileManager.default do { // Check if the directory exists @@ -37,7 +37,7 @@ public class SourceFileManager { } } - var lastDataSource: String? { + public var lastDataSource: String? { DataStore.shared.appSettings.lastDataSource } @@ -54,14 +54,14 @@ public class SourceFileManager { // } } - func _removeAllData(fromDate current: Date) { + public func _removeAllData(fromDate current: Date) { let lastStringDate = URL.importDateFormatter.string(from: current) let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"] files.forEach { fileName in NetworkManager.shared.removeRankingData(lastDateString: lastStringDate, fileName: fileName) } } - + actor SourceFileDownloadTracker { var _downloadedFileStatus : Int? = nil @@ -134,7 +134,7 @@ public class SourceFileManager { } } - func monthsBetweenDates(startDateString: String, endDateString: String) -> [String] { + public func monthsBetweenDates(startDateString: String, endDateString: String) -> [String] { let dateFormatter = URL.importDateFormatter guard let startDate = dateFormatter.date(from: startDateString), @@ -207,7 +207,7 @@ public class SourceFileManager { return allFiles(isManPlayer) } - static func isDateAfterUrlImportDate(date: Date, dateString: String) -> Bool { + public static func isDateAfterUrlImportDate(date: Date, dateString: String) -> Bool { guard let importDate = URL.importDateFormatter.date(from: dateString) else { return false } diff --git a/PadelClubData/URLs.swift b/PadelClubData/Utils/URLs.swift similarity index 99% rename from PadelClubData/URLs.swift rename to PadelClubData/Utils/URLs.swift index e4c1185..67531ba 100644 --- a/PadelClubData/URLs.swift +++ b/PadelClubData/Utils/URLs.swift @@ -71,7 +71,7 @@ public enum PageLink: String, Identifiable, CaseIterable { rawValue } - var path: String { + public var path: String { switch self { case .matches: return "" diff --git a/PadelClubData/Business/MatchDescriptor.swift b/PadelClubData/ViewModel/MatchDescriptor.swift similarity index 99% rename from PadelClubData/Business/MatchDescriptor.swift rename to PadelClubData/ViewModel/MatchDescriptor.swift index 6a48240..a93de28 100644 --- a/PadelClubData/Business/MatchDescriptor.swift +++ b/PadelClubData/ViewModel/MatchDescriptor.swift @@ -10,7 +10,6 @@ import SwiftUI import Combine public class MatchDescriptor: ObservableObject { - @Published public var matchFormat: MatchFormat @Published public var setDescriptors: [SetDescriptor] public var court: Int = 1 @@ -22,29 +21,6 @@ public class MatchDescriptor: ObservableObject { public let colorTeamOne: Color = .teal public let colorTeamTwo: Color = .indigo - public init(match: Match? = nil) { - self.match = match - if let groupStage = match?.groupStageObject { - self.matchFormat = groupStage.matchFormat - self.setDescriptors = [SetDescriptor(setFormat: groupStage.matchFormat.setFormat)] - } else { - let format = match?.matchFormat ?? match?.currentTournament()?.matchFormat ?? .defaultFormatForMatchType(.groupStage) - self.matchFormat = format - self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)] - } - let teamOne = match?.team(.one) - let teamTwo = match?.team(.two) - self.teamLabelOne = teamOne?.teamLabel(.wide, twoLines: true) ?? "" - self.teamLabelTwo = teamTwo?.teamLabel(.wide, twoLines: true) ?? "" - - if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score { - - self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in - SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat) - }) - } - } - public var teamOneSetupIsActive: Bool { if hasEnded && showSetInputView == false && showTieBreakInputView == false { return false @@ -95,6 +71,29 @@ public class MatchDescriptor: ObservableObject { return setDescriptors.anySatisfy({ $0.showTieBreakInputView }) } + public init(match: Match? = nil) { + self.match = match + if let groupStage = match?.groupStageObject { + self.matchFormat = groupStage.matchFormat + self.setDescriptors = [SetDescriptor(setFormat: groupStage.matchFormat.setFormat)] + } else { + let format = match?.matchFormat ?? match?.currentTournament()?.matchFormat ?? .defaultFormatForMatchType(.groupStage) + self.matchFormat = format + self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)] + } + let teamOne = match?.team(.one) + let teamTwo = match?.team(.two) + self.teamLabelOne = teamOne?.teamLabel(.wide, twoLines: true) ?? "" + self.teamLabelTwo = teamTwo?.teamLabel(.wide, twoLines: true) ?? "" + + if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score { + + self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in + SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat) + }) + } + } + public var teamOneScores: [String] { setDescriptors.compactMap { $0.getValue(teamPosition: .one) } } diff --git a/PadelClubData/Business/MatchSpot.swift b/PadelClubData/ViewModel/MatchSpot.swift similarity index 100% rename from PadelClubData/Business/MatchSpot.swift rename to PadelClubData/ViewModel/MatchSpot.swift diff --git a/PadelClubData/Business/PadelRule.swift b/PadelClubData/ViewModel/PadelRule.swift similarity index 95% rename from PadelClubData/Business/PadelRule.swift rename to PadelClubData/ViewModel/PadelRule.swift index 7ee1bd1..0dd3e1a 100644 --- a/PadelClubData/Business/PadelRule.swift +++ b/PadelClubData/ViewModel/PadelRule.swift @@ -34,7 +34,6 @@ public protocol TournamentBuildHolder: Identifiable { } public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { - public var uniqueId: String = Store.randomId() public var id: String { uniqueId } public let category: TournamentCategory @@ -55,8 +54,8 @@ public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identif computedLabel(displayStyle) } - var identifier: String { - level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel() + public var identifier: String { + level.localizedLevelLabel()+":"+category.localizedCategoryLabel(ageCategory: age)+":"+age.localizedFederalAgeLabel() } func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { @@ -64,12 +63,12 @@ public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identif return localizedLabel(displayStyle) + " " + localizedAge(displayStyle) } - func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { - level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) + public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + level.localizedLevelLabel(displayStyle) + " " + category.localizedCategoryLabel(displayStyle, ageCategory: age) } - func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String { - level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) + public func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String { + level.localizedLevelLabel(displayStyle) + " " + category.localizedCategoryLabel(displayStyle, ageCategory: age) } func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { @@ -77,7 +76,7 @@ public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identif } } -public extension TournamentBuild { +extension TournamentBuild { init?(category: String, level: String, age: FederalTournamentAge = .senior) { guard let levelFound = TournamentLevel.allCases.first(where: { $0.localizedLevelLabel() == level }) else { return nil } @@ -104,7 +103,6 @@ public enum FederalTournamentType: String, Hashable, Codable, CaseIterable, Iden case championnatParPaire = "L" public var id: String { self.rawValue } - public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .tournoi: @@ -266,13 +264,13 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi case .unlisted: return displayStyle == .title ? "Aucune" : "" case .a11_12: - return "11/12 ans" + return "U12" case .a13_14: - return "13/14 ans" + return "U14" case .a15_16: - return "15/16 ans" + return "U16" case .a17_18: - return "17/18 ans" + return "U18" case .senior: return displayStyle == .short ? "" : "Senior" case .a45: @@ -307,6 +305,27 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi return age >= 55 } } + + public func isChildCategory() -> Bool { + switch self { + case .unlisted: + return false + case .a11_12: + return true + case .a13_14: + return true + case .a15_16: + return true + case .a17_18: + return true + case .senior: + return false + case .a45: + return false + case .a55: + return false + } + } } public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { @@ -325,9 +344,9 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable self.init(rawValue: value) } - public static var assimilationAllCases: [TournamentLevel] { + public static var assimilationAllCases: [TournamentLevel] = { return [.p25, .p100, .p250, .p500, .p1000, .p1500, .p2000] - } + }() public var entryFee: Double? { switch self { @@ -404,6 +423,15 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable } } + public func haveDeadlines() -> Bool { + switch self { + case .p500, .p1000, .p1500, .p2000: + return true + default: + return false + } + } + public func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge) -> Int { switch self { case .p25: @@ -504,6 +532,10 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable public var defaultTeamSortingType: TeamSortingType { switch self { + case .championship: + return .inscriptionDate + case .unlisted: + return .inscriptionDate case .p25, .p100, .p250: return .inscriptionDate default: @@ -555,7 +587,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable public var coachingIsAuthorized: Bool { switch self { - case .p500, .p1000, .p1500, .p2000: + case .p500, .p1000, .p1500, .p2000, .championship: return true default: return false @@ -564,6 +596,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable public func points(for rank: Int, count: Int) -> Int { if self == .unlisted { return 0 } + if self == .championship { return 0 } let points = points(for: count) if rank >= points.count { return points.last! @@ -913,26 +946,46 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab } } - public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + public func localizedCategoryLabel(_ displayStyle: DisplayStyle = .wide, ageCategory: FederalTournamentAge? = nil) -> String { switch self { case .unlisted: return displayStyle == .title ? "Aucune" : "" case .men: switch displayStyle { case .title: + if ageCategory?.isChildCategory() == true { + return "Garçons" + } return "Hommes" case .wide: + if ageCategory?.isChildCategory() == true { + return "Garçons" + } return "Hommes" case .short: + if ageCategory?.isChildCategory() == true { + return "G" + } + return "H" } case .women: switch displayStyle { case .title: + if ageCategory?.isChildCategory() == true { + return "Filles" + } return "Dames" case .wide: + if ageCategory?.isChildCategory() == true { + return "Filles" + } return "Dames" case .short: + if ageCategory?.isChildCategory() == true { + return "F" + } + return "D" } case .mix: @@ -959,35 +1012,6 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab } } -public enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable { - case all = -1 - case male = 1 - case female = 0 - - public var id: Int { rawValue } - - public func icon() -> String { - switch self { - case .all: - return "Tous" - case .male: - return "Homme" - case .female: - return "Femme" - } - } - - public var localizedPlayerLabel: String { - switch self { - case .female: - return "joueuse" - default: - return "joueur" - } - } - -} - public enum GroupStageOrderingMode: Int, Hashable, Codable, CaseIterable, Identifiable { case random @@ -1000,7 +1024,6 @@ public enum GroupStageOrderingMode: Int, Hashable, Codable, CaseIterable, Identi } public var id: Int { self.rawValue } - public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .random: @@ -1305,9 +1328,9 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { } } - public static var allCases: [MatchFormat] { + public static var allCases: [MatchFormat] = { [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint] - } + }() public func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { scoreTeamOne >= scoreTeamTwo ? .one : .two @@ -1384,7 +1407,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { } } - var estimatedTimeWithBreak: Int { + public var estimatedTimeWithBreak: Int { estimatedDuration + breakTime.breakTime } @@ -1472,6 +1495,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { public var format: String { shortFormat + (isFederal ? "" : " (non officiel)") } + public var shortFormat: String { switch self { case .twoSets: @@ -1585,7 +1609,6 @@ public enum Format: Int, Hashable, Codable { return "tie-break en 15" } } - public var isTiebreak: Bool { switch self { case .normal: @@ -1736,20 +1759,20 @@ public enum RoundRule { } } - static func teamsInFirstRound(forTeams teams: Int) -> Int { + public static func teamsInFirstRound(forTeams teams: Int) -> Int { Int(pow(2.0, ceil(log2(Double(teams))))) } - static func numberOfMatches(forTeams teams: Int) -> Int { + public static func numberOfMatches(forTeams teams: Int) -> Int { teamsInFirstRound(forTeams: teams) - 1 } - static func numberOfRounds(forTeams teams: Int) -> Int { + public static func numberOfRounds(forTeams teams: Int) -> Int { if teams == 0 { return 0 } return Int(log2(Double(teamsInFirstRound(forTeams: teams)))) } - static func matchIndex(fromRoundIndex roundIndex: Int) -> Int { + public static func matchIndex(fromRoundIndex roundIndex: Int) -> Int { guard roundIndex >= 0 else { return -1 // Invalid round index } @@ -1757,7 +1780,7 @@ public enum RoundRule { return (1 << roundIndex) - 1 } - static func matchIndex(fromBracketPosition: Int) -> Int { + public static func matchIndex(fromBracketPosition: Int) -> Int { roundIndex(fromMatchIndex: fromBracketPosition / 2) + fromBracketPosition%2 } diff --git a/PadelClubData/Business/Screen.swift b/PadelClubData/ViewModel/Screen.swift similarity index 100% rename from PadelClubData/Business/Screen.swift rename to PadelClubData/ViewModel/Screen.swift diff --git a/PadelClubData/Business/SeedInterval.swift b/PadelClubData/ViewModel/SeedInterval.swift similarity index 99% rename from PadelClubData/Business/SeedInterval.swift rename to PadelClubData/ViewModel/SeedInterval.swift index e9e7b0d..a5be1ce 100644 --- a/PadelClubData/Business/SeedInterval.swift +++ b/PadelClubData/ViewModel/SeedInterval.swift @@ -8,7 +8,6 @@ import Foundation public struct SeedInterval: Hashable, Comparable { - public let first: Int public let last: Int diff --git a/PadelClubData/Selectable.swift b/PadelClubData/ViewModel/Selectable.swift similarity index 95% rename from PadelClubData/Selectable.swift rename to PadelClubData/ViewModel/Selectable.swift index 19d1158..24dd1c0 100644 --- a/PadelClubData/Selectable.swift +++ b/PadelClubData/ViewModel/Selectable.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI import TipKit -protocol Selectable { +public protocol Selectable { func selectionLabel(index: Int) -> String func badgeValue() -> Int? func badgeImage() -> Badge? @@ -19,7 +19,7 @@ protocol Selectable { func associatedTip() -> (any Tip)? } -extension Selectable { +public extension Selectable { func associatedTip() -> (any Tip)? { return nil } @@ -71,7 +71,7 @@ struct SelectionTipViewModifier: ViewModifier { } } -extension View { +public extension View { func selectableTipViewModifier(selectable: Selectable, action: @escaping () -> Void) -> some View { modifier(SelectionTipViewModifier(selectable: selectable, action: action)) } diff --git a/PadelClubData/Utils/SetDescriptor.swift b/PadelClubData/ViewModel/SetDescriptor.swift similarity index 92% rename from PadelClubData/Utils/SetDescriptor.swift rename to PadelClubData/ViewModel/SetDescriptor.swift index b105ed7..756420a 100644 --- a/PadelClubData/Utils/SetDescriptor.swift +++ b/PadelClubData/ViewModel/SetDescriptor.swift @@ -17,11 +17,11 @@ public struct SetDescriptor: Identifiable, Equatable { public var showSetInputView: Bool = true public var showTieBreakInputView: Bool = false - public var isTeamOneSet: Bool { + public var isTeamOneSet: Bool { return valueTeamOne != nil || tieBreakValueTeamOne != nil } - public var hasEnded: Bool { + public var hasEnded: Bool { if let valueTeamTwo, let valueTeamOne { return setFormat.hasEnded(teamOne: valueTeamOne, teamTwo: valueTeamTwo) } else { @@ -29,7 +29,7 @@ public struct SetDescriptor: Identifiable, Equatable { } } - public var winner: TeamPosition? { + public var winner: TeamPosition? { if let valueTeamTwo, let valueTeamOne, valueTeamOne != valueTeamTwo { return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo) } else { @@ -37,7 +37,7 @@ public struct SetDescriptor: Identifiable, Equatable { } } - public var shouldTieBreak: Bool { + public var shouldTieBreak: Bool { setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0) }