diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index e9b9cbc..ece45dc 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3344,7 +3344,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.14; + MARKETING_VERSION = 1.1.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3389,7 +3389,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.14; + MARKETING_VERSION = 1.1.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; 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/Match.swift b/PadelClub/Data/Match.swift index a71580d..b80f645 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -410,12 +410,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 { diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 3d17346..ebb617b 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -301,21 +301,23 @@ final class PlayerRegistration: ModelObject, Storable { 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) @@ -338,9 +340,11 @@ final class PlayerRegistration: ModelObject, Storable { } } - 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 } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 2521583..e927f14 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -70,6 +70,10 @@ final class Tournament : ModelObject, Storable { var maximumPlayerPerTeam: Int = 2 var information: String? = nil + //local variable + var refreshInProgress: Bool = false + var refreshRanking: Bool = false + @ObservationIgnored var navigationPath: [Screen] = [] @@ -1513,8 +1517,8 @@ defer { } } - 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 { @@ -1549,16 +1553,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) } + chunkedParsers = try await chunkAllSources(sources: sources, size: 10000) + } - let sources = dataURLs.map { CSVParser(url: $0) } 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 } @@ -2424,12 +2454,15 @@ defer { func updateSeedsBracketPosition() async { - await removeAllSeeds() + await removeAllSeeds(saveTeamsAtTheEnd: false) let drawLogs = drawLogs().reversed() let seeds = seeds() - for (index, seed) in seeds.enumerated() { - if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) { - drawLog.updateTeamBracketPosition(seed) + + await MainActor.run { + for (index, seed) in seeds.enumerated() { + if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) { + drawLog.updateTeamBracketPosition(seed) + } } } @@ -2440,11 +2473,13 @@ defer { } } - func removeAllSeeds() async { + func removeAllSeeds(saveTeamsAtTheEnd: Bool) async { let teams = unsortedTeams() teams.forEach({ team in team.bracketPosition = nil team._cachedRestingTime = nil + team.finalRanking = nil + team.pointsEarned = nil }) let allMatches = allRoundMatches() let ts = allMatches.flatMap { match in @@ -2471,12 +2506,13 @@ defer { Logger.error(error) } - do { - try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) - } catch { - Logger.error(error) + if saveTeamsAtTheEnd { + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) + } catch { + Logger.error(error) + } } - updateTournamentState() } func addNewRound(_ roundIndex: Int) async { @@ -2608,6 +2644,27 @@ 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 refreshTeamList() async throws { + guard enableOnlineRegistration, refreshInProgress == false, hasEnded() == false else { return } + refreshInProgress = 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 + } + // MARK: - func insertOnServer() throws { 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/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 7000131..f7de342 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -17,6 +17,11 @@ struct EventListView: View { let tournaments: [FederalTournamentHolder] let sortAscending: Bool + 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 { @@ -101,6 +106,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 { @@ -135,7 +175,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 { @@ -152,7 +192,23 @@ struct EventListView: View { } } - if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { + if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) { + + Button { + Task { + do { + try await pcTournaments.concurrentForEach { tournament in + try await tournament.refreshTeamList() + } + } catch { + Logger.error(error) + } + } + } label: { + Text("Rafraîchir la liste des équipes inscrites en ligne") + } + + Button { pcTournaments.forEach { tournament in tournament.enableOnlineRegistration = false @@ -207,6 +263,13 @@ struct EventListView: View { private func _tournamentView(_ tournament: Tournament) -> some View { NavigationLink(value: tournament) { TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver()) + .task { + do { + try await tournament.refreshTeamList() + } catch { + Logger.error(error) + } + } } .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) .contextMenu { diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 2928a49..b785fec 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -158,7 +158,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/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index f72a8cc..fbbffa3 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.") + } } } diff --git a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift index ceed51f..f85eb6e 100644 --- a/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift @@ -42,24 +42,7 @@ 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 d4d875a..53f2001 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 @@ -259,6 +258,9 @@ struct InscriptionManagerView: View { } } } + .task { + await _refreshList() + } .refreshable { await _refreshList() } @@ -543,28 +545,27 @@ struct InscriptionManagerView: View { // } // private func _refreshList() async { - if refreshInProgress { 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) + + try await self.tournament.refreshTeamList() _setHash() - self.refreshResult = "la synchronization a réussi" + 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 + tournament.refreshInProgress = false } } @@ -717,7 +718,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 @@ -845,19 +846,22 @@ struct InscriptionManagerView: View { } } label: { LabeledContent { - if refreshInProgress { + if tournament.refreshInProgress { ProgressView() - } else if let refreshStatus { - if refreshStatus { - Image(systemName: "checkmark").foregroundStyle(.green).font(.headline) - } else { - Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline) - } + } else { + Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted()) + .fontWeight(.bold) } } label: { - Text("Récupérer les inscriptions en ligne") + if let refreshStatus { + Text("Inscriptions en ligne") + } else if tournament.refreshInProgress { + Text("Récupération des inscrits en ligne") + } else { + Text("Récupérer des inscrits en ligne") + } if let refreshResult { - Text(refreshResult) + Text(refreshResult).foregroundStyle(.secondary) } } } diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index 33f08f7..bc1445f 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -27,7 +27,8 @@ struct RegistrationSetupView: View { @State private var showMoreInfos: Bool = false @State private var hasChanges: Bool = false - + @State private var displayWarning: Bool = false + @Environment(\.dismiss) private var dismiss init(tournament: Tournament) { @@ -74,6 +75,11 @@ struct RegistrationSetupView: View { var body: some View { List { + if displayWarning, tournament.enableOnlineRegistration, tournament.onlineTeams().isEmpty == false { + 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) + } + Section { Toggle(isOn: $enableOnlineRegistration) { Text("Activer") @@ -249,6 +255,7 @@ struct RegistrationSetupView: View { } .onChange(of: targetTeamCount) { + displayWarning = true _hasChanged() } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index a72e616..19cfc4c 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -21,6 +21,8 @@ struct TableStructureView: View { @State private var updatedElements: Set = Set() @State private var structurePreset: PadelTournamentStructurePreset = .manual @State private var buildWildcards: Bool = true + @State private var displayWarning: Bool = false + @FocusState private var stepperFieldIsFocused: Bool var qualifiedFromGroupStage: Int { @@ -58,7 +60,11 @@ struct TableStructureView: View { @ViewBuilder var body: some View { List { - + if displayWarning, tournament.enableOnlineRegistration, tournament.onlineTeams().isEmpty == false { + 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) + } + if tournament.state() != .build { Section { Picker(selection: $structurePreset) { @@ -91,6 +97,9 @@ struct TableStructureView: View { } label: { Text("Nombre d'équipes") } + .onChange(of: teamCount) { + displayWarning = true + } LabeledContent { StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages) { diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 9be8280..08cdae2 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -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 { @@ -164,6 +166,24 @@ 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 aucune équipe inscrite en ligne)").foregroundStyle(.secondary).font(.footnote) + } else { + Text("(dont " + value.formatted() + " équipe\(value.pluralSuffix) inscrite\(value.pluralSuffix) en ligne)").foregroundStyle(.secondary).font(.footnote) + } + } + } + if tournament.refreshRanking { + Text("mise à jour des classements des joueurs").foregroundStyle(.secondary).font(.footnote) + } else if tournament.refreshInProgress { + Text("synchronisation des inscriptions en ligne").foregroundStyle(.secondary).font(.footnote) + } + } } } .font(.caption)