diff --git a/.gitignore b/.gitignore index 3a95ee3..7172565 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +PadelClubDataTests/config.plist + ## User settings xcuserdata/ .DS_Store diff --git a/PadelClubData/Data/GroupStage.swift b/PadelClubData/Data/GroupStage.swift index e50d9cb..3c00d7e 100644 --- a/PadelClubData/Data/GroupStage.swift +++ b/PadelClubData/Data/GroupStage.swift @@ -298,7 +298,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) } - public func readyMatches(playedMatches: [Match]) -> [Match] { + public func readyMatches(playedMatches: [Match], runningMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -306,7 +306,9 @@ final public class GroupStage: BaseGroupStage, SideStorable { print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) + let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id }) + + return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false }) } public func finishedMatches(playedMatches: [Match]) -> [Match] { @@ -616,6 +618,16 @@ final public class GroupStage: BaseGroupStage, SideStorable { public func computedStartDate() -> Date? { return self._matches().sorted(by: \.computedStartDateForSorting).first?.startDate } + + public func removeAllTeams() { + let teams = teams() + teams.forEach { team in + team.groupStagePosition = nil + team.groupStage = nil + self._matches().forEach({ $0.updateTeamScores() }) + } + tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) + } public override func deleteDependencies(store: Store, actionOption: ActionOption) { store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.groupStage == self.id } diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index 5f16a7d..637d21f 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -838,8 +838,10 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { } - if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) { - bracketStartDate = lastDate.tomorrowAtNine + if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) { + if tournament.groupStageCount > 0 { + bracketStartDate = lastDate.tomorrowAtNine + } } return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: bracketStartDate) @@ -894,6 +896,11 @@ extension Match { return teamIds().contains(id) } + public func containsTeamIds(_ ids: [String]) -> Bool { + let teamIds = teamIds() + return !Set(ids).isDisjoint(with: teamIds) + } + public func containsTeamIndex(_ id: String) -> Bool { matchUp().contains(id) } diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index 5b33f57..0503a97 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -104,6 +104,25 @@ final public class Tournament: BaseTournament { return self.tournamentStore?.teamRegistrations.count ?? 0 } + public func deleteGroupStage(_ groupStage: GroupStage) { + groupStage.removeAllTeams() + let index = groupStage.index + self.tournamentStore?.groupStages.delete(instance: groupStage) + self.groupStageCount -= 1 + let groupStages = self.groupStages() + groupStages.filter({ $0.index > index }).forEach { gs in + gs.index -= 1 + } + self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groupStages) + } + + public func addGroupStage() { + let groupStage = GroupStage(tournament: id, index: groupStageCount, size: teamsPerGroupStage, format: groupStageFormat) + self.tournamentStore?.groupStages.addOrUpdate(instance: groupStage) + groupStage.buildMatches(keepExistingMatches: false) + self.groupStageCount += 1 + } + public func groupStages(atStep step: Int = 0) -> [GroupStage] { guard let tournamentStore = self.tournamentStore else { return [] } let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } @@ -872,7 +891,7 @@ defer { return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) } - public static func readyMatches(_ allMatches: [Match]) -> [Match] { + public static func readyMatches(_ allMatches: [Match], runningMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -880,7 +899,10 @@ defer { print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) + + let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id }) + + return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false }).sorted(using: defaultSorting, order: .ascending) } public static func matchesLeft(_ allMatches: [Match]) -> [Match] { @@ -1698,12 +1720,14 @@ defer { } public func initSettings(templateTournament: Tournament?, overrideTeamCount: Bool = true) { + courtCount = eventObject()?.clubObject()?.courtCount ?? 2 setupDefaultPrivateSettings(templateTournament: templateTournament) setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings if let templateTournament { setupRegistrationSettings(templateTournament: templateTournament, overrideTeamCount: overrideTeamCount) } setupFederalSettings() + customizeUsingPreferences() } public func setupFederalSettings() { @@ -1719,6 +1743,23 @@ defer { } } + public func customizeUsingPreferences() { + guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in + tournament.tournamentLevel == self.tournamentLevel + && tournament.tournamentCategory == self.tournamentCategory + && tournament.federalTournamentAge == self.federalTournamentAge + && tournament.hasEnded() == true + && tournament.isCanceled == false + && tournament.isDeleted == false + }).sorted(by: \.endDate!, order: .descending).first else { + return + } + + self.entryFee = lastTournamentWithSameBuild.entryFee + self.clubMemberFeeDeduction = lastTournamentWithSameBuild.clubMemberFeeDeduction + } + + public func deadline(for type: TournamentDeadlineType) -> Date? { guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } diff --git a/PadelClubData/Extensions/Date+Extensions.swift b/PadelClubData/Extensions/Date+Extensions.swift index a9878f0..aa681c4 100644 --- a/PadelClubData/Extensions/Date+Extensions.swift +++ b/PadelClubData/Extensions/Date+Extensions.swift @@ -118,6 +118,14 @@ public extension Date { } } + var nextDay: Date { + return Calendar.current.date(byAdding: .day, value: 1, to: self)! + } + + var weekDay: Int { + Calendar.current.component(.weekday, from: self) + } + func atBeginningOfDay(hourInt: Int = 9) -> Date { Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)! } @@ -144,6 +152,28 @@ public extension Date { return weekdays.map { $0.capitalized } }() + static var weekdays: [String] = { + let calendar = Calendar.current + // let weekdays = calendar.shortWeekdaySymbols + + // return weekdays.map { weekday in + // guard let firstLetter = weekday.first else { return "" } + // return String(firstLetter).capitalized + // } + // Adjusted for the different weekday starts + var weekdays = calendar.weekdaySymbols + if firstDayOfWeek > 1 { + for _ in 1.. Task { return Task(priority: .background) { //Iterate through any transactions which didn't come from a direct call to `purchase()`. @@ -235,13 +258,34 @@ import Combine let purchases = DataStore.shared.purchases let units = purchases.filter { $0.productId == StoreItem.unit.rawValue } - return units.reduce(0) { $0 + ($1.quantity ?? 0) } + let units10Pack = purchases.filter { $0.productId == StoreItem.unit10Pack.rawValue } + + return units.reduce(0) { $0 + ($1.quantity ?? 0) } + 10 * units10Pack.reduce(0) { $0 + ($1.quantity ?? 0) } // let units = self.userFilteredPurchases().filter { $0.productID == StoreItem.unit.rawValue } // return units.reduce(0) { $0 + $1.purchasedQuantity } } - public func paymentForNewTournament() -> TournamentPayment? { + struct CanCreateResponse: Decodable { var canCreate: Bool } + + public func paymentForNewTournament() async -> TournamentPayment? { + + if let payment = self.localPaymentForNewTournament() { + return payment + } else if let services = try? StoreCenter.main.service() { + do { + let response: CanCreateResponse = try await services.run(path: "is_granted_unlimited_access/", method: .get, requiresToken: true) + if response.canCreate { + return .unlimited + } + } catch { + Logger.error(error) + } + } + return nil + } + + public func localPaymentForNewTournament() -> TournamentPayment? { switch self.currentPlan { case .monthlyUnlimited: diff --git a/PadelClubData/Subscriptions/StoreItem.swift b/PadelClubData/Subscriptions/StoreItem.swift index 868d28c..d9c4bce 100644 --- a/PadelClubData/Subscriptions/StoreItem.swift +++ b/PadelClubData/Subscriptions/StoreItem.swift @@ -11,6 +11,7 @@ public enum StoreItem: String, Identifiable, CaseIterable { case monthlyUnlimited = "app.padelclub.tournament.subscription.unlimited" case fivePerMonth = "app.padelclub.tournament.subscription.five.per.month" case unit = "app.padelclub.tournament.unit" + case unit10Pack = "app.padelclub.tournament.unit.10" #if DEBUG public static let five: Int = 2 @@ -20,18 +21,27 @@ public enum StoreItem: String, Identifiable, CaseIterable { public var id: String { return self.rawValue } + public var summarySystemImage: String { + switch self { + case .monthlyUnlimited: return "infinity.circle.fill" + case .fivePerMonth: return "star.circle.fill" + case .unit, .unit10Pack: return "tennisball.circle.fill" + } + } + public var systemImage: String { switch self { case .monthlyUnlimited: return "infinity.circle.fill" case .fivePerMonth: return "star.circle.fill" - case .unit: return "tennisball.circle.fill" + case .unit: return "1.circle.fill" + case .unit10Pack: return "10.circle.fill" } } public var isConsumable: Bool { switch self { case .monthlyUnlimited, .fivePerMonth: return false - case .unit: return true + case .unit, .unit10Pack: return true } } diff --git a/PadelClubData/Subscriptions/StoreManager.swift b/PadelClubData/Subscriptions/StoreManager.swift index 4bf853b..e34f7e5 100644 --- a/PadelClubData/Subscriptions/StoreManager.swift +++ b/PadelClubData/Subscriptions/StoreManager.swift @@ -53,7 +53,11 @@ public class StoreManager { var products: [Product] = try await Product.products(for: self._productIdentifiers()) products = products.sorted { p1, p2 in - return p2.price > p1.price + if p1.type == p2.type { + return p2.price > p1.price + } else { + return p2.type.rawValue < p1.type.rawValue + } } Logger.log("products = \(products.count)") @@ -67,12 +71,10 @@ public class StoreManager { fileprivate func _productIdentifiers() -> [String] { var items: [StoreItem] = [] switch Guard.main.currentPlan { - case .fivePerMonth: - items = [StoreItem.unit, StoreItem.monthlyUnlimited] case .monthlyUnlimited: - break + items = [StoreItem.unit, StoreItem.unit10Pack] default: - items = StoreItem.allCases + items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited] } return items.map { $0.rawValue } } diff --git a/PadelClubData/ViewModel/PadelRule.swift b/PadelClubData/ViewModel/PadelRule.swift index 2c954c8..9df6b83 100644 --- a/PadelClubData/ViewModel/PadelRule.swift +++ b/PadelClubData/ViewModel/PadelRule.swift @@ -25,7 +25,7 @@ enum RankSource: Hashable { } } -public protocol TournamentBuildHolder: Identifiable { +public protocol TournamentBuildHolder: Identifiable, Hashable, Equatable { var id: String { get } var category: TournamentCategory { get } var level: TournamentLevel { get }