diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index fce005c..5a66597 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3624,7 +3624,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3650,7 +3650,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.13; + MARKETING_VERSION = 1.1.20; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3670,7 +3670,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3695,7 +3695,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.13; + MARKETING_VERSION = 1.1.20; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3792,7 +3792,6 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; - GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; @@ -3821,7 +3820,6 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -3839,7 +3837,6 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; - GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; @@ -3869,7 +3866,6 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = PRODTEST; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/PadelClub/AppDelegate.swift b/PadelClub/AppDelegate.swift index 14e6dcb..c57e143 100644 --- a/PadelClub/AppDelegate.swift +++ b/PadelClub/AppDelegate.swift @@ -23,6 +23,8 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel return true } + // MARK: - Remote Notifications + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { if StoreCenter.main.hasToken() { diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 05a2984..9e21f9b 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -103,6 +103,11 @@ class DataStore: ObservableObject { NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(collectionDidUpdate), name: NSNotification.Name.CollectionDidChange, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(_willEnterForegroundNotification), + name: UIScene.willEnterForegroundNotification, + object: nil) } @@ -119,11 +124,7 @@ class DataStore: ObservableObject { } @objc func collectionDidLoad(notification: Notification) { - - DispatchQueue.main.async { - self.objectWillChange.send() - } - + if let userSingleton: StoredSingleton = notification.object as? StoredSingleton { self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder() } else if let clubsCollection: StoredCollection = notification.object as? StoredCollection { @@ -166,6 +167,12 @@ class DataStore: ObservableObject { self.objectWillChange.send() } + @objc func _willEnterForegroundNotification() { + Task { + try await self.purchases.loadDataFromServerIfAllowed(clear: true) + } + } + func disconnect() { Task { diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index 003218e..202a5d7 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -236,12 +236,12 @@ struct CategorieAge: Codable { var tournamentAge: FederalTournamentAge? { if let id { - return FederalTournamentAge(rawValue: id) + return FederalTournamentAge(rawValue: id) ?? .senior } if let libelle { - return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) + return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) ?? .senior } - return nil + return .senior } } @@ -295,7 +295,7 @@ struct Serie: Codable { var sexe: String? var tournamentCategory: TournamentCategory? { - TournamentCategory.allCases.first(where: { $0.requestLabel == code }) + TournamentCategory.allCases.first(where: { $0.requestLabel == code }) ?? .men } } @@ -348,9 +348,9 @@ struct TypeEpreuve: Codable { var tournamentLevel: TournamentLevel? { if let code, let value = Int(code.removingFirstCharacter) { - return TournamentLevel(rawValue: value) + return TournamentLevel(rawValue: value) ?? .p100 } - return nil + return .p100 } } diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 694ff47..27fe45b 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -402,7 +402,7 @@ final class GroupStage: BaseGroupStage, SideStorable { unsortedTeams().flatMap({ $0.unsortedPlayers() }) } - fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool + typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) @@ -457,20 +457,24 @@ final class GroupStage: BaseGroupStage, SideStorable { var scoreCache: [Int: TeamGroupStageScore] = [:] + func computedScore(forTeam team: TeamRegistration, step: Int = 0) -> TeamGroupStageScore? { + if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] { + return cachedScore + } else { + let score = _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) + if let score = score { + scoreCache[team.groupStagePositionAtStep(step)!] = score + } + return score + } + } + func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { if sortedByScore { return unsortedTeams().compactMap({ team in // Check cache or use provided scores, otherwise calculate and store in cache scores?.first(where: { $0.team.id == team.id }) ?? { - if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] { - return cachedScore - } else { - let score = _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) - if let score = score { - scoreCache[team.groupStagePositionAtStep(step)!] = score - } - return score - } + return computedScore(forTeam: team, step: step) }() }).sorted { (lhs, rhs) in let predicates: [TeamScoreAreInIncreasingOrder] = [ diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 055b719..554d8fd 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -354,12 +354,15 @@ defer { } } //byeState = false - roundObject?._cachedSeedInterval = nil - name = nil - do { - try self.tournamentStore?.matches.addOrUpdate(instance: self) - } catch { - Logger.error(error) + + if state != currentState { + roundObject?._cachedSeedInterval = nil + name = nil + do { + try self.tournamentStore?.matches.addOrUpdate(instance: self) + } catch { + Logger.error(error) + } } if single == false { @@ -459,7 +462,7 @@ defer { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) } guard let roundObject else { return index } - return roundObject.isLoserBracket() ? (roundObject.index + 1) * 10000 + indexInRound() : (roundObject.index + 1) * 1000 + indexInRound() + return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + indexInRound() } func previousMatches() -> [Match] { @@ -484,9 +487,15 @@ defer { func setWalkOut(_ teamPosition: TeamPosition) { let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition)) teamScoreWalkout.walkOut = 0 + teamScoreWalkout.score = matchFormat.defaultWalkOutScore(true).compactMap({ String($0) }).joined(separator: ",") let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam)) teamScoreWinning.walkOut = nil - self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) + teamScoreWinning.score = matchFormat.defaultWalkOutScore(false).compactMap({ String($0) }).joined(separator: ",") + do { + try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) + } catch { + Logger.error(error) + } if endDate == nil { endDate = Date() diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index a9c1f83..c48b8ba 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -276,21 +276,23 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { return await withTaskGroup(of: Line?.self) { group in for source in filteredSources { group.addTask { - guard !Task.isCancelled else { print("Cancelled"); return nil } + guard !Task.isCancelled else { return nil } return try? await source.first { $0.rawValue.contains(";\(license);") } } } - if let first = await group.first(where: { $0 != nil }) { - group.cancelAll() - return first + for await result in group { + if let result { + group.cancelAll() // Stop other tasks as soon as we find a match + return result + } } return nil } } func historyFromName(from sources: [CSVParser]) async throws -> Line? { - #if DEBUG_TIME + #if DEBUG let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) @@ -313,20 +315,17 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { } } - if let first = await group.first(where: { $0 != nil }) { - group.cancelAll() - return first + for await result in group { + if let result { + group.cancelAll() // Stop other tasks as soon as we find a match + return result + } } return nil } } - func setComputedRank(in tournament: Tournament) { - if tournament.isAnimation() { - computedRank = rank ?? 0 - return - } - + func setComputedRank(in tournament: Tournament) { let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000 switch tournament.tournamentCategory { case .men: diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 65a459d..850e504 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -369,11 +369,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { func initialRoundColor() -> Color? { if walkOut { return Color.logoRed } - if groupStagePosition != nil { return Color.blue } - if let initialRound = initialRound(), - let colorHex = RoundRule.colors[safe: initialRound.index] - { + if groupStagePosition != nil || wildCardGroupStage { return Color.blue } + if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] { return Color(uiColor: .init(fromHex: colorHex)) + } else if wildCardBracket { + return Color.mint } else { return nil } @@ -582,11 +582,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { inTournamentCategory tournamentCategory: TournamentCategory ) { let significantPlayerCount = significantPlayerCount() - weight = - (players.prefix(significantPlayerCount).map { $0.computedRank } - + missingPlayerType(inTournamentCategory: tournamentCategory).map { - unrankValue(for: $0 == 1 ? true : false) - }).prefix(significantPlayerCount).reduce(0, +) + 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,+) } func significantPlayerCount() -> Int { @@ -702,6 +699,14 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { func shouldDisplayRankAndWeight() -> Bool { unsortedPlayers().count > 0 } + + func bracketMatchTitleAndQualifiedStatus() -> String? { + let values = [qualified ? "Qualifié" : nil, initialMatch()?.roundAndMatchTitle()].compactMap({ $0 }) + if values.isEmpty { + return nil + } + return values.joined(separator: " -> ") + } func insertOnServer() { self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b1becf1..f2c3350 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -12,6 +12,16 @@ import SwiftUI @Observable final class Tournament: BaseTournament { + //local variable + var refreshInProgress: Bool = false + var lastTeamRefresh: Date? + var refreshRanking: Bool = false + + func shouldRefreshTeams() -> Bool { + guard let lastTeamRefresh else { return true } + return lastTeamRefresh.timeIntervalSinceNow < -60 + } + @ObservationIgnored var navigationPath: [Screen] = [] @@ -620,7 +630,7 @@ defer { let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending) let groupStageSpots: Int = self.groupStageSpots() - var bracketSeeds: Int = min(teamCount, _teams.count) - groupStageSpots - wcBracket.count + var bracketSeeds: Int = teamCount - groupStageSpots - wcBracket.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if bracketSeeds < 0 { bracketSeeds = 0 } @@ -663,9 +673,9 @@ defer { return waitings } } - - func bracketCut(teamCount: Int) -> Int { - return max(0, teamCount - groupStageCut()) + + func bracketCut(teamCount: Int, groupStageCut: Int) -> Int { + return self.teamCount - groupStageCut } func groupStageCut() -> Int { @@ -674,10 +684,12 @@ defer { func cutLabel(index: Int, teamCount: Int?) -> String { let _teamCount = teamCount ?? selectedSortedTeams().count - let bracketCut = bracketCut(teamCount: _teamCount) + let groupStageCut = groupStageCut() + let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut) + if index < bracketCut { return "Tableau" - } else if index - bracketCut < groupStageCut() && _teamCount > 0 { + } else if index - bracketCut < groupStageCut && _teamCount > 0 { return "Poule" } else { return "Attente" @@ -687,11 +699,12 @@ defer { func cutLabelColor(index: Int?, teamCount: Int?) -> Color { guard let index else { return Color.gray } let _teamCount = teamCount ?? selectedSortedTeams().count - let bracketCut = bracketCut(teamCount: _teamCount) + let groupStageCut = groupStageCut() + let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut) if index < bracketCut { return Color.mint - } else if index - bracketCut < groupStageCut() && _teamCount > 0 { - return Color.cyan + } else if index - bracketCut < groupStageCut && _teamCount > 0 { + return Color.indigo } else { return Color.gray } @@ -881,8 +894,8 @@ defer { return max(1, courtCount) } } - - func registrationIssues(selectedTeams: [TeamRegistration]) -> Int { + + func registrationIssues(selectedTeams: [TeamRegistration]) async -> Int { let players : [PlayerRegistration] = unsortedPlayers() let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let duplicates : [PlayerRegistration] = duplicates(in: players) @@ -1187,9 +1200,9 @@ defer { } self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) } - - func updateRank(to newDate: Date?) async throws { - + + func updateRank(to newDate: Date?, forceRefreshLockWeight: Bool, providedSources: [CSVParser]?) async throws { + refreshRanking = true #if DEBUG_TIME let start = Date() defer { @@ -1225,16 +1238,42 @@ defer { let lastRankMan = monthData?.maleUnrankedValue ?? 0 let lastRankWoman = monthData?.femaleUnrankedValue ?? 0 - // Fetch only the required files - let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } - guard !dataURLs.isEmpty else { return } // Early return if no files found + var chunkedParsers: [CSVParser] = [] + if let providedSources { + chunkedParsers = providedSources + } else { + // Fetch only the required files + let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } + guard !dataURLs.isEmpty else { return } // Early return if no files found - let sources = dataURLs.map { CSVParser(url: $0) } + let sources = dataURLs.map { CSVParser(url: $0) } + chunkedParsers = try await chunkAllSources(sources: sources, size: 10000) + } + let players = unsortedPlayers() try await players.concurrentForEach { player in let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan - try await player.updateRank(from: sources, lastRank: lastRank) + try await player.updateRank(from: chunkedParsers, lastRank: lastRank) + player.setComputedRank(in: self) + } + + if providedSources == nil { + try chunkedParsers.forEach { chunk in + try FileManager.default.removeItem(at: chunk.url) + } + } + + try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) + + let unsortedTeams = unsortedTeams() + unsortedTeams.forEach { team in + team.setWeight(from: team.players(), inTournamentCategory: tournamentCategory) + if forceRefreshLockWeight { + team.lockedWeight = team.weight + } } + try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams) + refreshRanking = false } @@ -1278,7 +1317,7 @@ defer { } func hideWeight() -> Bool { - return hideTeamsWeight || tournamentLevel.hideWeight() + return hideTeamsWeight } func isAnimation() -> Bool { @@ -1537,6 +1576,7 @@ defer { } team.setWeight(from: [], inTournamentCategory: self.tournamentCategory) + team.weight += 200_000 return team } @@ -1584,9 +1624,10 @@ defer { return bracketTeamCount } - func buildBracket() { + func buildBracket(minimalBracketTeamCount: Int? = nil) { guard rounds().isEmpty else { return } - let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount()) + let roundCount = RoundRule.numberOfRounds(forTeams: minimalBracketTeamCount ?? bracketTeamCount()) + let matchCount = RoundRule.numberOfMatches(forTeams: minimalBracketTeamCount ?? bracketTeamCount()) let rounds = (0.. Bool { + func shouldTournamentBeOver() async -> Bool { #if _DEBUGING_TIME //DEBUGING TIME let start = Date() defer { @@ -2263,6 +2302,45 @@ defer { return false } + func rankSourceShouldBeRefreshed() -> Date? { + if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate = rankSourceDate, currentRankSourceDate < mostRecentDate, hasEnded() == false { + return mostRecentDate + } else { + return nil + } + } + + func onlineTeams() -> [TeamRegistration] { + unsortedTeams().filter({ $0.hasRegisteredOnline() }) + } + + func shouldWarnOnlineRegistrationUpdates() -> Bool { + enableOnlineRegistration && onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false + } + + func refreshTeamList() async { + guard StoreCenter.main.hasToken() else { return } + guard shouldRefreshTeams(), refreshInProgress == false, enableOnlineRegistration, hasEnded() == false else { return } + await MainActor.run { + 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) + await MainActor.run { + refreshInProgress = false + lastTeamRefresh = Date() + } + } catch { + Logger.error(error) + await MainActor.run { + refreshInProgress = false + lastTeamRefresh = Date() + } + } + } + // MARK: - func insertOnServer() throws { diff --git a/PadelClub/Extensions/Sequence+Extensions.swift b/PadelClub/Extensions/Sequence+Extensions.swift index 02001b2..9d21624 100644 --- a/PadelClub/Extensions/Sequence+Extensions.swift +++ b/PadelClub/Extensions/Sequence+Extensions.swift @@ -32,19 +32,35 @@ extension Sequence { func concurrentForEach( _ operation: @escaping (Element) async throws -> Void ) async throws { - // A task group automatically waits for all of its - // sub-tasks to complete, while also performing those - // tasks in parallel: try await withThrowingTaskGroup(of: Void.self) { group in + // First, create all tasks for element in self { group.addTask { try await operation(element) } - - for try await _ in group {} } + + // Then wait for all tasks to complete + for try await _ in group {} } } + + func concurrentForEach( + _ operation: @escaping (Element) async -> Void + ) async { + await withTaskGroup(of: Void.self) { group in + // First, add all tasks + for element in self { + group.addTask { + await operation(element) + } + } + + // Then wait for all tasks to complete + for await _ in group {} + } + } + } enum SortOrder { diff --git a/PadelClub/InscriptionLegendView.swift b/PadelClub/InscriptionLegendView.swift index 031e206..9589fc6 100644 --- a/PadelClub/InscriptionLegendView.swift +++ b/PadelClub/InscriptionLegendView.swift @@ -36,7 +36,7 @@ struct InscriptionLegendView: View { Text("Équipe estimée en tableau") .listRowView(isActive: true, color: .mint, hideColorVariation: true, alignment: .leading) Text("Équipe estimée en poule") - .listRowView(isActive: true, color: .cyan, hideColorVariation: true, alignment: .leading) + .listRowView(isActive: true, color: .indigo, hideColorVariation: true, alignment: .leading) } diff --git a/PadelClub/OnlineRegistrationWarningView.swift b/PadelClub/OnlineRegistrationWarningView.swift new file mode 100644 index 0000000..ad68e6a --- /dev/null +++ b/PadelClub/OnlineRegistrationWarningView.swift @@ -0,0 +1,60 @@ +// +// WaitingListView.swift +// PadelClub +// +// Created by razmig on 26/02/2025. +// + +import SwiftUI + +struct WaitingListView: View { + @Environment(Tournament.self) var tournament: Tournament + let teamCount: Int + + @ViewBuilder + var body: some View { + Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Pour l'instant Padel Club ne saura pas les prévenir automatiquement, vous devrez les contacter via l'écran de gestion des inscriptions.") + .foregroundStyle(.logoRed) + let selection = tournament.selectedSortedTeams() + if teamCount > tournament.teamCount { + Section { + let teams = tournament.waitingListSortedTeams(selectedSortedTeams: selection) + .prefix(teamCount - tournament.teamCount) + .filter { $0.hasRegisteredOnline() } + + ForEach(teams) { team in + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + TeamRowView(team: team) + } + } + } header: { + Text("Équipes entrantes dans la sélection") + } footer: { + Text("Équipes inscrites en ligne à prévenir rentrant dans votre liste") + } + } + + if teamCount < tournament.teamCount { + Section { + let teams = selection.suffix(tournament.teamCount - teamCount) + .filter { $0.hasRegisteredOnline() } + + ForEach(teams) { team in + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + TeamRowView(team: team) + } + } + } header: { + Text("Équipes sortantes de la sélection") + } footer: { + Text("Équipes inscrites en ligne à prévenir retirées de votre liste") + } + } + } +} diff --git a/PadelClub/Utils/Network/NetworkManager.swift b/PadelClub/Utils/Network/NetworkManager.swift index 6a7a7e6..0f32b96 100644 --- a/PadelClub/Utils/Network/NetworkManager.swift +++ b/PadelClub/Utils/Network/NetworkManager.swift @@ -51,9 +51,9 @@ class NetworkManager { let documentsUrl: URL = SourceFileManager.shared.rankingSourceDirectory let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") - let fileURL = URL(string: "https://xlr.alwaysdata.net/static/rankings/\(dateString)") + let fileURL = URLs.main.extend(path: "static/rankings/\(dateString)") - var request = URLRequest(url:fileURL!) + var request = URLRequest(url:fileURL) request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition") if FileManager.default.fileExists(atPath: destinationFileUrl.path()), let modificationDate = destinationFileUrl.creationDate() { request.addValue(formatDateForHTTPHeader(modificationDate), forHTTPHeaderField: "If-Modified-Since") diff --git a/PadelClub/Utils/SwiftParser.swift b/PadelClub/Utils/SwiftParser.swift index de0bda2..7aab761 100644 --- a/PadelClub/Utils/SwiftParser.swift +++ b/PadelClub/Utils/SwiftParser.swift @@ -70,18 +70,18 @@ struct Line: Identifiable { struct CSVParser: AsyncSequence, AsyncIteratorProtocol { typealias Element = Line - private let url: URL + let url: URL private var lineIterator: LineIterator - private let seperator: Character + private let separator: Character private let quoteCharacter: Character = "\"" private var lineNumber = 0 private let date: Date let maleData: Bool - init(url: URL, seperator: Character = ";") { + init(url: URL, separator: Character = ";") { self.date = url.dateFromPath self.url = url - self.seperator = seperator + self.separator = separator self.lineIterator = url.lines.makeAsyncIterator() self.maleData = url.path().contains(SourceFile.messieurs.rawValue) } @@ -127,7 +127,7 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol { func makeAsyncIterator() -> CSVParser { return self } - + private func split(line: String) -> [String?] { var data = [String?]() var inQuote = false @@ -139,7 +139,7 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol { inQuote = !inQuote continue - case seperator: + case separator: if !inQuote { data.append(currentString.isEmpty ? nil : currentString) currentString = "" @@ -157,4 +157,63 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol { return data } + + /// Splits the CSV file into multiple temporary CSV files, each containing `size` lines. + /// Returns an array of new `CSVParser` instances pointing to these chunked files. + func getChunkedParser(size: Int) async throws -> [CSVParser] { + var chunkedParsers: [CSVParser] = [] + var currentChunk: [String] = [] + var iterator = self.makeAsyncIterator() + var chunkIndex = 0 + + while let line = try await iterator.next()?.rawValue { + currentChunk.append(line) + + // When the chunk reaches the desired size, write it to a file + if currentChunk.count == size { + let chunkURL = try writeChunkToFile(chunk: currentChunk, index: chunkIndex) + chunkedParsers.append(CSVParser(url: chunkURL, separator: self.separator)) + chunkIndex += 1 + currentChunk.removeAll() + } + } + + // Handle remaining lines (if any) + if !currentChunk.isEmpty { + let chunkURL = try writeChunkToFile(chunk: currentChunk, index: chunkIndex) + chunkedParsers.append(CSVParser(url: chunkURL, separator: self.separator)) + } + + return chunkedParsers + } + + /// Writes a chunk of CSV lines to a temporary file and returns its URL. + private func writeChunkToFile(chunk: [String], index: Int) throws -> URL { + let tempDirectory = FileManager.default.temporaryDirectory + let chunkURL = tempDirectory.appendingPathComponent("\(url.lastPathComponent)-\(index).csv") + + let chunkData = chunk.joined(separator: "\n") + try chunkData.write(to: chunkURL, atomically: true, encoding: .utf8) + + return chunkURL + } +} + +/// Process all large CSV files concurrently and gather all mini CSVs. +func chunkAllSources(sources: [CSVParser], size: Int) async throws -> [CSVParser] { + var allChunks: [CSVParser] = [] + + await withTaskGroup(of: [CSVParser].self) { group in + for source in sources { + group.addTask { + return (try? await source.getChunkedParser(size: size)) ?? [] + } + } + + for await miniCSVs in group { + allChunks.append(contentsOf: miniCSVs) + } + } + + return allChunks } diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 47ba573..0baae88 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -199,14 +199,16 @@ struct CallView: View { let uncalledTeams = teams.filter { $0.getPhoneNumbers().isEmpty } if networkMonitor.connected == false { - if uncalledTeams.isEmpty == false { + if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false { self.sentError = .uncalledTeams(uncalledTeams) } else { self.sentError = .messageNotSent } } else { - if uncalledTeams.isEmpty == false { + if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false { self.sentError = .uncalledTeams(uncalledTeams) + } else if uncalledTeams.isEmpty == false, calledTeams.isEmpty { + self._called(uncalledTeams, true) } self._called(calledTeams, true) } @@ -228,14 +230,16 @@ struct CallView: View { if networkMonitor.connected == false { self.contactType = nil - if uncalledTeams.isEmpty == false { + if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false { self.sentError = .uncalledTeams(uncalledTeams) } else { self.sentError = .mailNotSent } } else { - if uncalledTeams.isEmpty == false { + if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false { self.sentError = .uncalledTeams(uncalledTeams) + } else if uncalledTeams.isEmpty == false, calledTeams.isEmpty { + self._called(uncalledTeams, true) } self._called(calledTeams, true) } diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index c037ca9..ae854f7 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -128,6 +128,18 @@ struct GroupStagesSettingsView: View { } } #endif + + Section { + menuGenerateGroupStage(.random) + } footer: { + Text("Redistribue les équipes par tirage au sort par chapeau") + } + + Section { + menuGenerateGroupStage(.snake) + } footer: { + Text("Redistribue les équipes par la méthode du serpentin") + } Section { RowButtonView("Retirer tous les horaires", role: .destructive) { @@ -144,30 +156,17 @@ struct GroupStagesSettingsView: View { } } } footer: { - Text("Retire les horaires pré-définis des matchs. Utile si vous avez convoqué mais que l'ordre des matchs à lancer n'est pas important.") + Text("Retire les horaires pré-définis des matchs. Utile si vous avez convoqué mais que l'ordre des matchs à lancer n'est pas important.").foregroundStyle(.logoRed).bold() } - if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false { Section { menuBuildAllGroupStages } footer: { - Text("Efface et recréé les poules, les horaires et les résultats existants seront perdus") + Text("Efface et recréé les poules, les horaires et les résultats existants seront perdus").foregroundStyle(.logoRed).bold() } } - Section { - menuGenerateGroupStage(.random) - } footer: { - Text("Redistribue les équipes par tirage au sort par chapeau") - } - - Section { - menuGenerateGroupStage(.snake) - } footer: { - Text("Redistribue les équipes par la méthode du serpentin") - } - let groupStages = tournament.groupStages() Section { diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 05446a2..c89f7bc 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -105,6 +105,80 @@ struct GroupStagesView: View { allDestinations.append(contentsOf: groupStageDestinations) return allDestinations } + + func sortedTeams(missingQualifiedFromGroupStages: [TeamRegistration]) -> [GroupStage.TeamGroupStageScore] { + let sortedTeams = missingQualifiedFromGroupStages.compactMap({ team in + team.groupStageObject()?.computedScore(forTeam: team) + }).sorted { (lhs, rhs) in + let predicates: [GroupStage.TeamScoreAreInIncreasingOrder] = [ + { $0.wins < $1.wins }, + { $0.setDifference < $1.setDifference }, + { $0.gameDifference < $1.gameDifference}, + ] + + for predicate in predicates { + if !predicate(lhs, rhs) && !predicate(rhs, lhs) { + continue + } + + return predicate(lhs, rhs) + } + + return false + } + return sortedTeams + } + + func _sortedAdditionnalTeams(missingQualifiedFromGroupStages: [TeamRegistration]) -> some View { + Section { + let teamGroupStageScores = self.sortedTeams(missingQualifiedFromGroupStages: missingQualifiedFromGroupStages).reversed() + ForEach(teamGroupStageScores, id: \.team.id) { teamGroupStageScore in + let team = teamGroupStageScore.team + let groupStage = team.groupStageObject()! + let groupStagePosition = team.groupStagePosition! + + NavigationLink { + GroupStageTeamView(groupStage: groupStage, team: team) + .environment(self.tournament) + } label: { + HStack { + VStack(alignment: .leading) { + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName).foregroundStyle(.secondary).font(.footnote) + } + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1) + } + } + Spacer() + if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: teamGroupStageScore) { + VStack(alignment: .trailing) { + HStack(spacing: 0.0) { + Text(score.wins) + Text("/") + Text(score.losses) + }.font(.headline).monospacedDigit() + if let setsDifference = score.setsDifference { + HStack(spacing: 4.0) { + Text(setsDifference) + }.font(.footnote) + } + if let gamesDifference = score.gamesDifference { + HStack(spacing: 4.0) { + Text(gamesDifference) + }.font(.footnote) + } + } + } + } + } + } + } header: { + let name = "\((tournament.qualifiedPerGroupStage + 1).ordinalFormatted())" + Text("Meilleurs \(name) de poule") + } + + } var body: some View { VStack(spacing: 0) { @@ -116,9 +190,8 @@ struct GroupStagesView: View { List { if tournament.groupStageAdditionalQualified > 0 { let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages() + let name = "\((tournament.qualifiedPerGroupStage + 1).ordinalFormatted())" Section { - let name = "\((tournament.qualifiedPerGroupStage + 1).ordinalFormatted())" - NavigationLink { SpinDrawView(drawees: ["Qualification d'un \(name) de poule"], segments: missingQualifiedFromGroupStages) { results in results.forEach { drawResult in @@ -134,6 +207,8 @@ struct GroupStagesView: View { Text("Qualifier un \(name) de poule par tirage au sort") } .disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty) + } header: { + Text("Tirage au sort d'un \(name) de poule") } footer: { if tournament.moreQualifiedToDraw() == 0 { Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifier le paramètre dans structure.") @@ -141,6 +216,8 @@ struct GroupStagesView: View { Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.") } } + + _sortedAdditionnalTeams(missingQualifiedFromGroupStages: missingQualifiedFromGroupStages) } let runningMatches = Tournament.runningMatches(allMatches) diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 0c7ec65..c1095e3 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -30,7 +30,7 @@ struct MatchDateView: View { } var currentDate: Date { - Date().withoutSeconds() + Date() } var body: some View { diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index d4c8d73..e2a7e15 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -151,7 +151,7 @@ struct PlayerBlockView: View { } } } - } else if let team { + } else if let team, hasWon == false, isWalkOut == false { TeamWeightView(team: team, teamPosition: teamPosition) } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index a81ea94..5dd2485 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -501,7 +501,7 @@ struct MatchDetailView: View { Text("Horaire") } .onChange(of: startDateSetup) { - let date = Date().withoutSeconds() + let date = Date() switch startDateSetup { case .customDate: break diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 48bdf11..9c222ce 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -178,6 +178,12 @@ struct ActivityView: View { federalDataViewModel.federalTournaments.removeAll() NetworkFederalService.shared.formId = "" _gatherFederalTournaments() + } else if navigation.agendaDestination == .activity { + runningTournaments.forEach { t in + if let tournament = t as? Tournament { + tournament.lastTeamRefresh = nil + } + } } } .task { diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 9a33dda..56feb26 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -19,6 +19,11 @@ struct EventListView: View { @State var showUserSearch: Bool = false + var lastDataSource: Date? { + guard let _lastDataSource = dataStore.appSettings.lastDataSource else { return nil } + return URL.importDateFormatter.date(from: _lastDataSource) + } + var body: some View { let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth } switch viewStyle { @@ -103,6 +108,41 @@ struct EventListView: View { @ViewBuilder private func _options(_ pcTournaments: [Tournament]) -> some View { + if let lastDataSource, pcTournaments.anySatisfy({ $0.rankSourceShouldBeRefreshed() != nil && $0.hasEnded() == false }) { + Section { + Button { + Task { + do { + + let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == lastDataSource } + guard !dataURLs.isEmpty else { return } // Early return if no files found + + let sources = dataURLs.map { CSVParser(url: $0) } + let chunkedParsers = try await chunkAllSources(sources: sources, size: 10000) + + try await pcTournaments.concurrentForEach { tournament in + if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() { + try await tournament.updateRank(to: mostRecentDate, forceRefreshLockWeight: false, providedSources: chunkedParsers) + } + } + + try chunkedParsers.forEach { chunk in + try FileManager.default.removeItem(at: chunk.url) + } + + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } + } label: { + Text("Rafraîchir les classements") + } + } header: { + Text("Source disponible : \(lastDataSource.monthYearFormatted)") + } + Divider() + } Section { if pcTournaments.anySatisfy({ $0.isPrivate == true }) { Button { @@ -137,7 +177,7 @@ struct EventListView: View { Text("Visibilité sur Padel Club") } Divider() - if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { + if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) { Section { if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) { Button { @@ -154,7 +194,19 @@ struct EventListView: View { } } - if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { + if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) { + + Button { + Task { + await pcTournaments.concurrentForEach { tournament in + await tournament.refreshTeamList() + } + } + } label: { + Text("Rafraîchir la liste des équipes inscrites en ligne") + } + + Button { pcTournaments.forEach { tournament in tournament.enableOnlineRegistration = false @@ -208,11 +260,11 @@ struct EventListView: View { private func _tournamentView(_ tournament: Tournament) -> some View { NavigationLink(value: tournament) { - - TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver()) - .popover(isPresented: self.$showUserSearch) { - ShareModelView(instance: tournament) - } + TournamentCellView(tournament: tournament) + .id(tournament.lastTeamRefresh) + .task(priority: .background) { + await tournament.refreshTeamList() + } } .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) .contextMenu { diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index 6569682..1f5c291 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -97,7 +97,7 @@ struct EditablePlayerView: View { @ViewBuilder func computedPlayerView(_ player: PlayerRegistration) -> some View { VStack(alignment: .leading, spacing: 0.0) { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: player.tournament()?.tournamentCategory.showFemaleInMaleAssimilation ?? false) // HStack { // Text(player.isImported() ? "importé via beach padel" : "") // Text(player.formattedLicense().isLicenseNumber ? "licence valide" : "non valide") diff --git a/PadelClub/Views/Player/PlayerView.swift b/PadelClub/Views/Player/PlayerView.swift index c604fe9..7dbda7d 100644 --- a/PadelClub/Views/Player/PlayerView.swift +++ b/PadelClub/Views/Player/PlayerView.swift @@ -20,7 +20,7 @@ struct PlayerView: View { } var body: some View { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { do { diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 58ac05e..ed2ca61 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -150,7 +150,7 @@ struct RoundSettingsView: View { private func _removeAllSeeds() async { - await tournament.removeAllSeeds() + await tournament.removeAllSeeds(saveTeamsAtTheEnd: true) self.isEditingTournamentSeed.wrappedValue = true } diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 78cda7f..27a88a1 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -217,6 +217,11 @@ struct FollowUpMatchView: View { } #if DEBUG Spacer() + if let roundObject = match.roundObject { + Text(roundObject.index.formatted()) + Text(roundObject.theoryCumulativeMatchCount.formatted()) + } + Text(match.computedOrder.formatted()) FooterButtonView("copier l'id") { let pasteboard = UIPasteboard.general pasteboard.string = match.id diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 4210ef9..16b7d7e 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -427,7 +427,9 @@ struct MySearchView: View { searchViewModel.selectedPlayers.insert(player) } label: { ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + .contentShape(Rectangle()) } + .frame(maxWidth: .infinity) .buttonStyle(.plain) } } header: { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 0c79849..d5fa22c 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -20,11 +20,11 @@ struct EditingTeamView: View { @State private var sentError: ContactManagerError? = nil @State private var showSubscriptionView: Bool = false @State private var registrationDate : Date + @State private var walkOut : Bool + @State private var wildCardBracket : Bool + @State private var wildCardGroupStage : Bool @State private var name: String @FocusState private var focusedField: TeamRegistration.CodingKeys? - @State private var presentOnlineRegistrationWarning: Bool = false - @State private var currentWaitingList: TeamRegistration? - @State private var presentTeamToWarn: Bool = false var messageSentFailed: Binding { Binding { @@ -36,6 +36,22 @@ struct EditingTeamView: View { } } + var hasChanged: Binding { + Binding { + if canSaveWithoutWarning() { + return false + } + + return + registrationDate != team.registrationDate + || walkOut != team.walkOut + || wildCardBracket != team.wildCardBracket + || wildCardGroupStage != team.wildCardGroupStage + + } set: { _ in + } + } + var tournamentStore: TournamentStore? { return self.tournament.tournamentStore } @@ -44,25 +60,19 @@ struct EditingTeamView: View { self.team = team _name = .init(wrappedValue: team.name ?? "") _registrationDate = State(wrappedValue: team.registrationDate ?? Date()) + _walkOut = State(wrappedValue: team.walkOut) + _wildCardBracket = State(wrappedValue: team.wildCardBracket) + _wildCardGroupStage = State(wrappedValue: team.wildCardGroupStage) } private func _resetTeam() { - let selectedSortedTeams = tournament.selectedSortedTeams() - self.currentWaitingList = tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams).filter({ $0.hasRegisteredOnline() }).first team.resetPositions() + team.qualified = false team.wildCardGroupStage = false team.walkOut = false team.wildCardBracket = false } - private func _checkOnlineRegistrationWarning() { - guard let currentWaitingList else { return } - let selectedSortedTeams = tournament.selectedSortedTeams().map({ $0.id }) - if selectedSortedTeams.contains(currentWaitingList.id) { - presentOnlineRegistrationWarning = true - } - } - var body: some View { List { Section { @@ -92,10 +102,6 @@ struct EditingTeamView: View { .headerProminence(.increased) Section { - DatePicker(selection: $registrationDate) { - Text("Inscription") - Text(registrationDate.localizedWeekDay().capitalized) - } if let callDate = team.callDate { LabeledContent() { Text(callDate.localizedDate()) @@ -117,36 +123,23 @@ struct EditingTeamView: View { Text("Équipe sur place") } } + } + + Section { + DatePicker(selection: $registrationDate) { + Text("Inscription") + Text(registrationDate.localizedWeekDay().capitalized) + } - Toggle(isOn: .init(get: { - return team.wildCardBracket - }, set: { value in - _resetTeam() - team.wildCardBracket = value - - _save() - })) { + Toggle(isOn: $wildCardBracket) { Text("Wildcard Tableau") } - Toggle(isOn: .init(get: { - return team.wildCardGroupStage - }, set: { value in - _resetTeam() - team.wildCardGroupStage = value - _save() - })) { + Toggle(isOn: $wildCardGroupStage) { Text("Wildcard Poule") - } + }.disabled(tournament.groupStageCount == 0) - Toggle(isOn: .init(get: { - return team.walkOut - }, set: { value in - _resetTeam() - team.walkOut = value - - _save() - })) { + Toggle(isOn: $walkOut) { Text("Forfait") } } @@ -218,30 +211,24 @@ struct EditingTeamView: View { } } } - .sheet(isPresented: $presentTeamToWarn) { - if let currentWaitingList { - NavigationStack { - EditingTeamView(team: currentWaitingList) - } - .tint(.master) + .alert("Attention", isPresented: hasChanged, actions: { + Button("Confirmer") { + _resetTeam() + team.registrationDate = registrationDate + team.wildCardBracket = wildCardBracket + team.wildCardGroupStage = wildCardGroupStage + team.walkOut = walkOut + _save() } - } - .alert("Attention", isPresented: $presentOnlineRegistrationWarning, actions: { - if currentWaitingList != nil { - - Button("Voir l'équipe") { - self.presentTeamToWarn = true - } - - Button("OK") { - self.currentWaitingList = nil - self.presentOnlineRegistrationWarning = false - } + + Button("Annuler", role: .cancel) { + registrationDate = team.registrationDate ?? Date() + walkOut = team.walkOut + wildCardBracket = team.wildCardBracket + wildCardGroupStage = team.wildCardGroupStage } }, message: { - if let currentWaitingList { - Text("L'équipe \(currentWaitingList.teamLabel(separator: "/")), inscrite en ligne, rentre dans votre sélection suite à la modification que vous venez de faire, voulez-vous les prévenir ?") - } + Text("Ce changement peut entraîner l'entrée ou la sortie d'une équipe de votre sélection. Padel Club préviendra automatiquement une équipe inscrite en ligne de son nouveau statut.") }) .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { @@ -329,14 +316,29 @@ struct EditingTeamView: View { } } .onChange(of: registrationDate) { - team.registrationDate = registrationDate - _save() + if canSaveWithoutWarning() { + team.registrationDate = registrationDate + _save() + } + } + .onChange(of: [walkOut, wildCardBracket, wildCardGroupStage]) { + if canSaveWithoutWarning() { + _resetTeam() + team.walkOut = walkOut + team.wildCardBracket = wildCardBracket + team.wildCardGroupStage = wildCardGroupStage + _save() + } } .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Édition de l'équipe") .navigationBarTitleDisplayMode(.inline) } + func canSaveWithoutWarning() -> Bool { + (tournament.shouldWarnOnlineRegistrationUpdates() && tournament.teamCount <= tournament.unsortedTeamsCount()) == false + } + private var confirmationReceived: Binding { Binding { team.confirmed() @@ -371,8 +373,6 @@ struct EditingTeamView: View { } catch { Logger.error(error) } - - _checkOnlineRegistrationWarning() } private var _networkErrorMessage: String { diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index 0515283..73b8d59 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -134,11 +134,19 @@ struct TeamPickerView: View { // presentTeamPickerView = false // } } label: { - TeamRowView(team: team) - .contentShape(Rectangle()) + VStack(alignment: .leading) { + if let roundAndMatchTitle = team.bracketMatchTitleAndQualifiedStatus() { + Text(roundAndMatchTitle) + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + } + TeamRowView(team: team) + } + .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) + .id(team.id) .listRowView(isActive: matchTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true) // .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) { // Button("Retirer du tableau", role: .destructive) { diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 4a1ba04..bb32774 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -9,6 +9,8 @@ import SwiftUI struct TeamRowView: View { @EnvironmentObject var dataStore: DataStore + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed + var team: TeamRegistration var teamPosition: TeamPosition? = nil var displayCallDate: Bool = false @@ -20,7 +22,9 @@ struct TeamRowView: View { TeamWeightView(team: team, teamPosition: teamPosition, teamIndex: teamIndex) } label: { VStack(alignment: .leading) { - TeamHeadlineView(team: team) + if isEditingTournamentSeed.wrappedValue == false { + TeamHeadlineView(team: team) + } TeamView(team: team) } if displayCallDate { diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index d8c2bae..0973ba2 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -120,10 +120,10 @@ struct FileImportView: View { return teams.filter { $0.tournamentCategory == tournament.tournamentCategory && $0.tournamentAgeCategory == tournament.federalTournamentAge }.sorted(by: \.weight) } - private func _deleteTeams() async { + private func _deleteTeams(teams: [TeamRegistration]) async { await MainActor.run { do { - try tournamentStore?.teamRegistrations.delete(contentOfs: tournament.unsortedTeams()) + try tournamentStore?.teamRegistrations.delete(contentOfs: teams) } catch { Logger.error(error) } @@ -140,9 +140,18 @@ struct FileImportView: View { } } - if tournament.unsortedTeams().count > 0, tournament.enableOnlineRegistration == false { - RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) { - await _deleteTeams() + let unsortedTeams = tournament.unsortedTeams() + let onlineTeams = unsortedTeams.filter({ $0.hasRegisteredOnline() }) + if unsortedTeams.count > 0 { + Section { + RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) { + await _deleteTeams(teams: unsortedTeams) + } + .disabled(onlineTeams.isEmpty == false) + } footer: { + if onlineTeams.isEmpty == false { + Text("Ce tournoi contient des inscriptions en ligne, vous ne pouvez pas effacer toute votre liste d'inscription d'un coup.") + } } } @@ -499,21 +508,7 @@ struct FileImportView: View { private func _validate(tournament: Tournament) async { let filteredTeams = filteredTeams(tournament: tournament) - - let unfound = _getUnfound(tournament: tournament, fromTeams: filteredTeams) - unfound.forEach { team in - if team.isWildCard() == false { - team.resetPositions() - team.walkOut = true - } - } - - do { - try tournament.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unfound) - } catch { - Logger.error(error) - } - + tournament.importTeams(filteredTeams) validatedTournamentIds.insert(tournament.id) diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 6bb0074..8c71827 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -80,7 +80,15 @@ struct AddTeamView: View { fetchRequest = FetchRequest(fetchRequest: request, animation: .default) } - + + var selectionLimit: Int { + if tournament.isAnimation() { + return -1 + } else { + return tournament.significantPlayerCount() - _currentSelectionIds().count + } + } + var body: some View { if let pasteString, pasteString.isEmpty == false, fetchPlayers.isEmpty == false { computedBody @@ -163,7 +171,7 @@ struct AddTeamView: View { } .sheet(isPresented: $presentPlayerSearch) { NavigationStack { - SelectablePlayerListView(allowSelection: 2 - _currentSelectionIds().count, isPresented: true, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in + SelectablePlayerListView(allowSelection: selectionLimit, isPresented: true, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) newPlayer.setComputedRank(in: tournament) diff --git a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift index e39b33a..0b8cfb0 100644 --- a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift @@ -42,25 +42,8 @@ struct UpdateSourceRankDateView: View { updatingRank = true Task { do { - try await tournament.updateRank(to: currentRankSourceDate) - - let unsortedPlayers = tournament.unsortedPlayers() - tournament.unsortedPlayers().forEach { player in - player.setComputedRank(in: tournament) - } - - try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) - - let unsortedTeams = tournament.unsortedTeams() - unsortedTeams.forEach { team in - team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory) - if forceRefreshLockWeight { - team.lockedWeight = team.weight - } - } - - try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams) + try await tournament.updateRank(to: currentRankSourceDate, forceRefreshLockWeight: forceRefreshLockWeight, providedSources: nil) try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 44632c2..effe804 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -51,7 +51,6 @@ struct InscriptionManagerView: View { @State private var pasteString: String? @State private var registrationIssues: Int? = nil @State private var refreshResult: String? = nil - @State private var refreshInProgress: Bool = false @State private var refreshStatus: Bool? @State private var showLegendView: Bool = false @@ -193,8 +192,8 @@ struct InscriptionManagerView: View { self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) } self.registrationIssues = nil - DispatchQueue.main.async { - self.registrationIssues = tournament.registrationIssues(selectedTeams: selectedSortedTeams) + Task { + self.registrationIssues = await tournament.registrationIssues(selectedTeams: selectedSortedTeams) } } @@ -248,7 +247,7 @@ struct InscriptionManagerView: View { if tournament.enableOnlineRegistration { RowButtonView("Rafraîchir la liste", cornerRadius: 20) { - await _refreshList() + await _refreshList(forced: true) } } else if tournament.onlineRegistrationCanBeEnabled() { RowButtonView("Inscription en ligne") { @@ -259,11 +258,16 @@ struct InscriptionManagerView: View { } } } + .task(priority: .background) { + await _refreshList(forced: false) + } .refreshable { - await _refreshList() + await _refreshList(forced: true) } .onAppear { - _setHash(currentSelectedSortedTeams: selectedSortedTeams) + if tournament.enableOnlineRegistration == false || refreshStatus == true { + _setHash(currentSelectedSortedTeams: selectedSortedTeams) + } } .onDisappear { _handleHashDiff(selectedSortedTeams: selectedSortedTeams) @@ -542,29 +546,29 @@ struct InscriptionManagerView: View { // try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) // } // - private func _refreshList() async { - if refreshInProgress { return } + private func _refreshList(forced: Bool) async { + if refreshStatus == true, forced == false { return } + if tournament.enableOnlineRegistration == false { return } + if tournament.hasEnded() { return } + if tournament.refreshInProgress { return } refreshResult = nil refreshStatus = nil - 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) + await self.tournament.refreshTeamList() _setHash() - - self.refreshResult = "la synchronization a réussi" + if let lastTeamRefresh = self.tournament.lastTeamRefresh?.formatted(date: .abbreviated, time: .shortened) { + self.refreshResult = "Dernière m-à-j : \(lastTeamRefresh)" + } else { + self.refreshResult = "La synchronization a réussi" + } self.refreshStatus = true - refreshInProgress = false } catch { Logger.error(error) - - self.refreshResult = "la synchronization a échoué" + self.refreshResult = "La synchronization a échoué" self.refreshStatus = false - refreshInProgress = false } } @@ -717,7 +721,7 @@ struct InscriptionManagerView: View { @ViewBuilder private func _rankHandlerView() -> some View { - if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false { + if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() { Section { TipView(rankUpdateTip) { action in self.currentRankSourceDate = mostRecentDate @@ -839,28 +843,21 @@ struct InscriptionManagerView: View { // } if tournament.enableOnlineRegistration { - Button { - Task { - await _refreshList() - } + LabeledContent { + Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted()) + .font(.largeTitle) } label: { - LabeledContent { - if refreshInProgress { - ProgressView() - } else if let refreshStatus { - if refreshStatus { - Image(systemName: "checkmark").foregroundStyle(.green).font(.headline) - } else { - Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline) - } - } - } label: { - Text("Récupérer les inscriptions en ligne") - if let refreshResult { - Text(refreshResult) - } + Text("Inscriptions en ligne") + if let refreshResult { + Text(refreshResult).foregroundStyle(.secondary) + } else { + Text(" ") } } + + RowButtonView("Rafraîchir les inscriptions en ligne") { + await _refreshList(forced: true) + } } } header: { HStack { diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index 33f08f7..54de58a 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -27,7 +27,7 @@ struct RegistrationSetupView: View { @State private var showMoreInfos: Bool = false @State private var hasChanges: Bool = false - + @Environment(\.dismiss) private var dismiss init(tournament: Tournament) { @@ -72,8 +72,18 @@ struct RegistrationSetupView: View { } + func displayWarning() -> Bool { + let unsortedTeamsCount = tournament.unsortedTeamsCount() + return tournament.shouldWarnOnlineRegistrationUpdates() && targetTeamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || targetTeamCount <= unsortedTeamsCount) + } + var body: some View { List { + if displayWarning() { + Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.") + .foregroundStyle(.logoRed) + } + Section { Toggle(isOn: $enableOnlineRegistration) { Text("Activer") diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index a43930f..960179f 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -23,6 +23,11 @@ struct TableStructureView: View { @State private var buildWildcards: Bool = true @FocusState private var stepperFieldIsFocused: Bool + func displayWarning() -> Bool { + let unsortedTeamsCount = tournament.unsortedTeamsCount() + return tournament.shouldWarnOnlineRegistrationUpdates() && teamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount) + } + var qualifiedFromGroupStage: Int { groupStageCount * qualifiedPerGroupStage } @@ -43,7 +48,7 @@ struct TableStructureView: View { var moreQualifiedLabel: String { if groupStageAdditionalQualified == 0 { return "Aucun" } - return (groupStageAdditionalQualified > 1 ? "les \(groupStageAdditionalQualified)" : "le") + " meilleur\(groupStageAdditionalQualified.pluralSuffix) " + (qualifiedPerGroupStage + 1).ordinalFormatted() + return (groupStageAdditionalQualified > 1 ? "les \(groupStageAdditionalQualified)" : "le") + " " + (qualifiedPerGroupStage + 1).ordinalFormatted() } var maxGroupStages: Int { @@ -58,7 +63,11 @@ struct TableStructureView: View { @ViewBuilder var body: some View { List { - + if displayWarning() { + Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.") + .foregroundStyle(.logoRed) + } + if tournament.state() != .build { Section { Picker(selection: $structurePreset) { @@ -252,6 +261,16 @@ struct TableStructureView: View { } } + if tournament.rounds().isEmpty, tournament.state() == .build { + Section { + RowButtonView("Ajouter un tableau", role: .destructive) { + tournament.buildBracket(minimalBracketTeamCount: 4) + } + } footer: { + Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.") + } + } + if tournament.state() != .initial { Section { diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 4512410..9919ed6 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -15,7 +15,7 @@ struct TournamentCellView: View { let tournament: FederalTournamentHolder // let color: Color = .black var displayStyle: DisplayStyle = .wide - var shouldTournamentBeOver: Bool = false + @State var shouldTournamentBeOver: Bool = false var event: Event? { guard let federalTournament = tournament as? FederalTournament else { return nil } @@ -114,7 +114,9 @@ struct TournamentCellView: View { } Spacer() if let tournament = tournament as? Tournament, displayStyle == .wide { - if tournament.isCanceled { + if tournament.refreshInProgress || tournament.refreshRanking { + ProgressView() + } else if tournament.isCanceled { Text("Annulé".uppercased()) .capsule(foreground: .white, background: .logoRed) } else if shouldTournamentBeOver { @@ -157,6 +159,9 @@ struct TournamentCellView: View { let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted() let word = hasStarted ? "équipe" : "inscription" Text(word + teamCount.pluralSuffix) + .task(priority: .background) { + self.shouldTournamentBeOver = await tournament.shouldTournamentBeOver() + } } } } @@ -164,6 +169,17 @@ struct TournamentCellView: View { Text(build.category.localizedLabel()) Text(build.age.localizedFederalAgeLabel()) } + if displayStyle == .wide, let tournament = tournament as? Tournament { + if tournament.enableOnlineRegistration { + let value: Int = tournament.onlineTeams().count + HStack { + Spacer() + if value > 0 { + Text("(dont " + value.formatted() + " inscrite\(value.pluralSuffix) en ligne)") + } + } + } + } } } .font(.caption) diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index d44e5aa..62b72c9 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -54,7 +54,7 @@ struct TournamentBuildView: View { } } } - .task { + .task(priority: .background) { groupStageStatus = await tournament.groupStageStatus() } } @@ -105,7 +105,7 @@ struct TournamentBuildView: View { } } } - .task { + .task(priority: .background) { bracketStatus = await tournament.bracketStatus() } } @@ -157,7 +157,7 @@ struct TournamentBuildView: View { } } } - .task { + .task(priority: .background) { scheduleStatus = await tournament.scheduleStatus() } @@ -178,7 +178,7 @@ struct TournamentBuildView: View { } } } - .task { + .task(priority: .background) { callStatus = await tournament.callStatus() } @@ -199,7 +199,7 @@ struct TournamentBuildView: View { } } } - .task { + .task(priority: .background) { cashierStatus = await tournament.cashierStatus() } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index e9df55d..1efed36 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -15,7 +15,8 @@ struct TournamentView: View { @Environment(NavigationViewModel.self) var navigation: NavigationViewModel @State var tournament: Tournament @State private var showMoreInfos: Bool = false - + @State var shouldTournamentBeOver: Bool = false + var presentationContext: PresentationContext = .agenda let tournamentSelectionTip: TournamentSelectionTip = TournamentSelectionTip() @@ -106,7 +107,7 @@ struct TournamentView: View { TournamentBuildView(tournament: tournament) TournamentInitView(tournament: tournament) case .running: - if tournament.shouldTournamentBeOver() { + if shouldTournamentBeOver { Section { TipView(shouldTournamentBeOverTip) { actions in navigation.path.append(Screen.stateSettings) @@ -125,6 +126,9 @@ struct TournamentView: View { RegistrationInfoSheetView() } } + .task(priority: .background) { + self.shouldTournamentBeOver = await tournament.shouldTournamentBeOver() + } .environment(tournament) .id(tournament.id) .toolbarBackground(.visible, for: .navigationBar)