diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 7464d21..215bce4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -157,6 +157,7 @@ FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; }; FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */; }; FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */; }; + FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; @@ -478,6 +479,7 @@ FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = ""; }; FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemModifier.swift; sourceTree = ""; }; FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubCourtSetupView.swift; sourceTree = ""; }; + FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundSettingsView.swift; sourceTree = ""; }; FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = ""; }; FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = ""; }; @@ -1235,6 +1237,7 @@ FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */, FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, + FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, ); path = Round; sourceTree = ""; @@ -1621,6 +1624,7 @@ FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */, C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, + FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */, FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, FF1DF49B2BD8D23900822FA0 /* BarButtonView.swift in Sources */, FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */, @@ -1935,7 +1939,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 34; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -1973,7 +1977,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 34; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 9e97677..008dc39 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -79,7 +79,7 @@ class GroupStage: ModelObject, Storable { guard teams().count == size else { return false } let _matches = _matches() if _matches.isEmpty { return false } - return _matches.allSatisfy { $0.hasEnded() } + return _matches.anySatisfy { $0.hasEnded() == false } == false } func buildMatches() { @@ -178,20 +178,60 @@ class GroupStage: ModelObject, Storable { return _matches().first(where: { matchIndexes.contains($0.index) }) } - func availableToStart(playedMatches: [Match], in runningMatches: [Match]) -> [Match] { + func availableToStart(playedMatches: [Match], in runningMatches: [Match]) async -> [Match] { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) } func runningMatches(playedMatches: [Match]) -> [Match] { - playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) + } + + func asyncRunningMatches(playedMatches: [Match]) async -> [Match] { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) } - func readyMatches(playedMatches: [Match]) -> [Match] { - playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) + + func readyMatches(playedMatches: [Match]) async -> [Match] { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) } func finishedMatches(playedMatches: [Match]) -> [Match] { - playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func group stage finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() } private func _matchOrder() -> [Int] { @@ -376,7 +416,6 @@ extension GroupStage: Selectable { } func badgeValue() -> Int? { - if teams().count < size { return nil } return runningMatches(playedMatches: _matches()).count } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 33f3b28..4b4d864 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -69,10 +69,10 @@ class Match: ModelObject, Storable { try Store.main.deleteDependencies(items: self.teamScores) } - func indexInRound() -> Int { + func indexInRound(in matches: [Match]? = nil) -> Int { if groupStage != nil { return index - } else if let index = roundObject?.playedMatches().sorted(by: \.index).firstIndex(where: { $0.id == id }) { + } else if let index = (matches ?? roundObject?.playedMatches().sorted(by: \.index))?.firstIndex(where: { $0.id == id }) { return index } return RoundRule.matchIndexWithinRound(fromMatchIndex: index) @@ -86,16 +86,16 @@ class Match: ModelObject, Storable { [roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") } - func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String { + func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String { if let groupStageObject { return groupStageObject.localizedMatchUpLabel(for: index) } switch displayStyle { case .wide: - return "Match \(indexInRound() + 1)" + return "Match \(indexInRound(in: matches) + 1)" case .short: - return "#\(indexInRound() + 1)" + return "#\(indexInRound(in: matches) + 1)" } } @@ -184,15 +184,11 @@ class Match: ModelObject, Storable { } func resetScores() { - if hasEnded() == false { - teamScores.forEach({ $0.score = nil }) - do { - try DataStore.shared.teamScores.addOrUpdate(contentOfs: teamScores) - } catch { - Logger.error(error) - } - } else { - updateTeamScores() + teamScores.forEach({ $0.score = nil }) + do { + try DataStore.shared.teamScores.addOrUpdate(contentOfs: teamScores) + } catch { + Logger.error(error) } } @@ -468,6 +464,9 @@ class Match: ModelObject, Storable { if endDate == nil { endDate = Date() } + if startDate == nil { + startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) + } winningTeamId = team(matchDescriptor.winner)?.id losingTeamId = team(matchDescriptor.winner.otherTeam)?.id groupStageObject?.updateGroupStageState() @@ -617,7 +616,7 @@ class Match: ModelObject, Storable { } func isReady() -> Bool { - teamScores.count == 2 + teamScores.count >= 2 // teams().count == 2 } @@ -627,7 +626,7 @@ class Match: ModelObject, Storable { } func hasEnded() -> Bool { - endDate != nil || hasWalkoutTeam() || winningTeamId != nil + endDate != nil } func isGroupStage() -> Bool { @@ -638,8 +637,9 @@ class Match: ModelObject, Storable { round != nil } - func walkoutTeam() -> [TeamRegistration] { - scores().filter({ $0.walkOut != nil }).compactMap { $0.team } + func walkoutTeam() -> [TeamRegistration] { + //walkout 0 means real walkout, walkout 1 means lucky loser situation + scores().filter({ $0.walkOut == 0 }).compactMap { $0.team } } func hasWalkoutTeam() -> Bool { @@ -705,6 +705,13 @@ class Match: ModelObject, Storable { } func team(_ team: TeamPosition) -> TeamRegistration? { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func match get team", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif if groupStage != nil { switch team { case .one: @@ -721,7 +728,7 @@ class Match: ModelObject, Storable { } } } - + func teamNames(_ team: TeamRegistration?) -> [String]? { team?.players().map { $0.playerLabel() } } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 7373407..6926240 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -56,7 +56,7 @@ class Round: ModelObject, Storable { } func hasEnded() -> Bool { - playedMatches().allSatisfy({ $0.hasEnded() }) + playedMatches().anySatisfy({ $0.hasEnded() == false }) == false } func upperMatches(ofMatch match: Match) -> [Match] { @@ -222,9 +222,9 @@ class Round: ModelObject, Storable { func playedMatches() -> [Match] { if parent == nil { - enabledMatches() + return enabledMatches() } else { - _matches() + return _matches() } } @@ -243,6 +243,10 @@ class Round: ModelObject, Storable { func isDisabled() -> Bool { _matches().allSatisfy({ $0.disabled }) } + + func isRankDisabled() -> Bool { + _matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty }) + } func resetFromRoundAllMatchesStartDate() { _matches().forEach({ @@ -285,11 +289,12 @@ class Round: ModelObject, Storable { _matches.forEach { match in match.disabled = disable match.resetMatch() - do { - try DataStore.shared.teamScores.delete(contentOfs: match.teamScores) - } catch { - Logger.error(error) - } + //we need to keep teamscores to handle disable ranking match round stuff +// do { +// try DataStore.shared.teamScores.delete(contentOfs: match.teamScores) +// } catch { +// Logger.error(error) +// } } do { try DataStore.shared.matches.addOrUpdate(contentOfs: _matches) @@ -398,9 +403,10 @@ class Round: ModelObject, Storable { } func roundStatus() -> String { - if hasStarted() && hasEnded() == false { + let hasEnded = hasEnded() + if hasStarted() && hasEnded == false { return "en cours" - } else if hasEnded() { + } else if hasEnded { return "terminée" } else { return "à démarrer" @@ -543,12 +549,17 @@ class Round: ModelObject, Storable { } -extension Round: Selectable { +extension Round: Selectable, Equatable { + static func == (lhs: Round, rhs: Round) -> Bool { + lhs.id == rhs.id + } + + func selectionLabel() -> String { if let parentRound { return "Tour #\(parentRound.loserRounds().count - index)" } else { - return roundTitle() + return roundTitle(.short) } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 98466c3..2ec0663 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -220,12 +220,25 @@ class TeamRegistration: ModelObject, Storable { bracketPosition != nil } - func resetPositions() { + func resetGroupeStagePosition() { groupStageObject()?._matches().forEach({ $0.updateTeamScores() }) groupStage = nil groupStagePosition = nil - tournamentObject()?.resetTeamScores(in: bracketPosition) - bracketPosition = nil + } + + func resetBracketPosition() { + guard let bracketPosition else { return } + guard let tournamentObject = tournamentObject() else { return } + if let match = tournamentObject.match(for: bracketPosition) { + let teamScores = match.teamScores.filter({ $0.teamRegistration != self.id }) + tournamentObject.resetTeamScores(in: bracketPosition, outsideOf: teamScores) + } + self.bracketPosition = nil + } + + func resetPositions() { + resetGroupeStagePosition() + resetBracketPosition() } func pasteData() -> String { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b623322..14c1c9e 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -538,11 +538,11 @@ class Tournament : ModelObject, Storable { } func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { - getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 0 } ?? [] + getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? [] } func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { - getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? [] + getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? [] } func availableSeedGroups() -> [SeedInterval] { @@ -615,7 +615,7 @@ class Tournament : ModelObject, Storable { if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { return availableSeedGroup - } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.count == availableSeedOpponentSpot.count { + } else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count { return availableSeedGroup } else if let chunks = availableSeedGroup.chunks() { if let chunk = chunks.first(where: { seedInterval in @@ -727,7 +727,13 @@ class Tournament : ModelObject, Storable { } func selectedSortedTeams() -> [TeamRegistration] { - //let start = Date() + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif var _sortedTeams : [TeamRegistration] = [] let _teams = unsortedTeams().filter({ $0.walkOut == false }) @@ -757,9 +763,6 @@ class Tournament : ModelObject, Storable { let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage _sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending) } - - //let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) - //print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) return _sortedTeams } @@ -817,10 +820,6 @@ class Tournament : ModelObject, Storable { unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank) } - func femalePlayers() -> [PlayerRegistration] { - unsortedPlayers().filter({ $0.isMalePlayer() == false }) - } - func unrankValue(for malePlayer: Bool) -> Int? { switch tournamentCategory { case .men: @@ -930,7 +929,7 @@ class Tournament : ModelObject, Storable { } } - func registrationIssues() -> Int { + func registrationIssues() async -> Int { let players : [PlayerRegistration] = unsortedPlayers() let selectedTeams : [TeamRegistration] = selectedSortedTeams() let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } @@ -957,46 +956,95 @@ class Tournament : ModelObject, Storable { return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) -> [Match] { + func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) async -> [Match] { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) } + func asyncRunningMatches(_ allMatches: [Match]) async -> [Match] { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) + } + func runningMatches(_ allMatches: [Match]) -> [Match] { - allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) } - func readyMatches(_ allMatches: [Match]) -> [Match] { + func readyMatches(_ allMatches: [Match]) async -> [Match] { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) } func finishedMatches(_ allMatches: [Match], limit: Int? = nil) -> [Match] { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif let _limit = limit ?? courtCount return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit)) } func finalRanking() -> [Int: [String]] { var teams: [Int: [String]] = [:] - + var ids: Set = Set() let rounds = rounds() let final = rounds.last?.playedMatches().last if let winner = final?.winningTeamId { teams[1] = [winner] + ids.insert(winner) } if let finalist = final?.losingTeamId { teams[2] = [finalist] + ids.insert(finalist) } let others : [Round] = rounds.flatMap { round in - round.loserRoundsAndChildren().filter { $0.isDisabled() == false && $0.hasNextRound() == false } + round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } }.compactMap({ $0 }) others.forEach { round in if let interval = round.seedInterval() { - let playedMatches = round.playedMatches().filter { $0.disabled == false } - let winners = playedMatches.compactMap({ $0.winningTeamId }) - let losers = playedMatches.compactMap({ $0.losingTeamId }) - teams[interval.first + winners.count - 1] = winners - teams[interval.last] = losers + let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() } + let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false }) + let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false }) + if winners.isEmpty { + let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) + teams[interval.last] = disabledIds + disabledIds.forEach { ids.insert($0) } + } else { + teams[interval.first + winners.count - 1] = winners + winners.forEach { ids.insert($0) } + teams[interval.last] = losers + losers.forEach { ids.insert($0) } + } } } @@ -1181,7 +1229,7 @@ class Tournament : ModelObject, Storable { } typealias TournamentStatus = (label:String, completion: String) - func cashierStatus() -> TournamentStatus { + func cashierStatus() async -> TournamentStatus { let selectedPlayers = selectedPlayers() let paid = selectedPlayers.filter({ $0.hasPaid() }) let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" @@ -1190,7 +1238,7 @@ class Tournament : ModelObject, Storable { return TournamentStatus(label: label, completion: completionLabel) } - func scheduleStatus() -> TournamentStatus { + func scheduleStatus() async -> TournamentStatus { let allMatches = allMatches() let ready = allMatches.filter({ $0.startDate != nil }) let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés" @@ -1199,7 +1247,7 @@ class Tournament : ModelObject, Storable { return TournamentStatus(label: label, completion: completionLabel) } - func callStatus() -> TournamentStatus { + func callStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " convoquées au bon horaire" @@ -1208,7 +1256,7 @@ class Tournament : ModelObject, Storable { return TournamentStatus(label: label, completion: completionLabel) } - func confirmedSummonStatus() -> TournamentStatus { + func confirmedSummonStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { $0.confirmationDate != nil } let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " confirmées" @@ -1217,7 +1265,7 @@ class Tournament : ModelObject, Storable { return TournamentStatus(label: label, completion: completionLabel) } - func bracketStatus() -> String { + func bracketStatus() async -> String { let availableSeeds = availableSeeds() if availableSeeds.isEmpty == false { return "placer \(availableSeeds.count) tête\(availableSeeds.count.pluralSuffix) de série" @@ -1233,7 +1281,7 @@ class Tournament : ModelObject, Storable { } } - func groupStageStatus() -> String { + func groupStageStatus() async -> String { let groupStageTeamsCount = groupStageTeams().count if groupStageTeamsCount == 0 || groupStageTeamsCount != teamsPerGroupStage * groupStageCount { return "à faire" @@ -1343,9 +1391,9 @@ class Tournament : ModelObject, Storable { return nil } - func resetTeamScores(in matchOfBracketPosition: Int?) { + func resetTeamScores(in matchOfBracketPosition: Int?, outsideOf: [TeamScore] = []) { guard let match = match(for: matchOfBracketPosition) else { return } - match.resetTeamScores(outsideOf: []) + match.resetTeamScores(outsideOf: outsideOf) } func updateTeamScores(in matchOfBracketPosition: Int?) { @@ -1626,7 +1674,10 @@ class Tournament : ModelObject, Storable { func getGroupStageChunkValue() -> Int { if teamsPerGroupStage >= 2 { - return min(groupStageCount, courtCount / (teamsPerGroupStage / 2)) + let result = courtCount / (teamsPerGroupStage / 2) + let remainder = courtCount % (teamsPerGroupStage / 2) + let value = remainder == 0 ? result : result + 1 + return min(groupStageCount, value) } else { return 1 } diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift index d9a068d..29682f1 100644 --- a/PadelClub/Utils/HtmlGenerator.swift +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -58,6 +58,10 @@ class HtmlGenerator: ObservableObject { self.width = width as! CGFloat }) } + + if self.completionHandler != nil { + self.buildPDF() + } }) } diff --git a/PadelClub/Utils/HtmlService.swift b/PadelClub/Utils/HtmlService.swift index ea5a64b..73fc31a 100644 --- a/PadelClub/Utils/HtmlService.swift +++ b/PadelClub/Utils/HtmlService.swift @@ -183,7 +183,7 @@ enum HtmlService { var template = "" var bracket = "" if let round = tournament.rounds().first(where: { $0.index == roundIndex }) { - for (_, match) in round.playedMatches().enumerated() { + for (_, match) in round._matches().enumerated() { template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore)) } bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index 5f7c555..2a5abb4 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -412,6 +412,34 @@ struct TournamentRunningTip: Tip { } } +struct CreateAccountTip: Tip { + var title: Text { + Text("Créer votre compte Padel Club") + } + + var message: Text? { + let message = "Un compte est nécessaire pour publier le tournoi sur [Padel Club](\(URLs.main.rawValue)) et profiter de toutes du site, comme le mode TV pour transformer l'expérience de vos tournois !" + return Text(.init(message)) + } + + var image: Image? { + Image(systemName: "person.crop.circle") + } + + var actions: [Action] { + Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte") + //todo + //Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus") + Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Jeter un oeil au site Padel Club") + } + + enum ActionKey: String { + case createAccount = "createAccount" + case learnMore = "learnMore" + case accessPadelClubWebPage = "accessPadelClubWebPage" + } +} + struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index e4a43af..49480f1 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -8,8 +8,12 @@ import Foundation import SwiftUI -enum AgendaDestination: CaseIterable, Identifiable, Selectable { - var id: Self { self } +enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { + var id: Int { self.rawValue } + + static func == (lhs: AgendaDestination, rhs: AgendaDestination) -> Bool { + return lhs.id == rhs.id + } case activity case history diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index badbdd1..bc6ce97 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -7,6 +7,11 @@ import SwiftUI +class DebouncableViewModel: ObservableObject { + @Published var debouncableText: String = "" + var debounceTrigger: Double = 0.15 +} + class SearchViewModel: ObservableObject, Identifiable { let id: UUID = UUID() var allowSelection : Int = 0 diff --git a/PadelClub/ViewModel/Selectable.swift b/PadelClub/ViewModel/Selectable.swift index 201a4b9..0953678 100644 --- a/PadelClub/ViewModel/Selectable.swift +++ b/PadelClub/ViewModel/Selectable.swift @@ -13,6 +13,13 @@ protocol Selectable { func badgeValue() -> Int? func badgeImage() -> Badge? func badgeValueColor() -> Color? + func displayImageIfValueZero() -> Bool +} + +extension Selectable { + func displayImageIfValueZero() -> Bool { + return false + } } enum Badge { diff --git a/PadelClub/Views/Calling/CallSettingsView.swift b/PadelClub/Views/Calling/CallSettingsView.swift index 0e7d8ab..091305c 100644 --- a/PadelClub/Views/Calling/CallSettingsView.swift +++ b/PadelClub/Views/Calling/CallSettingsView.swift @@ -51,7 +51,7 @@ struct CallSettingsView: View { } } - #if DEBUG + // #if DEBUG Section { RowButtonView("Annuler toutes les convocations", role: .destructive) { let teams = tournament.unsortedTeams() @@ -79,7 +79,7 @@ struct CallSettingsView: View { } } } - #endif + //#endif } .sheet(isPresented: $showSendToAllView) { SendToAllView(addLink: false) diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index e3c107e..dc708e9 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -8,39 +8,62 @@ import SwiftUI import Combine -struct CashierView: View { - @EnvironmentObject var dataStore: DataStore - var tournaments : [Tournament] - var teams: [TeamRegistration] - @State private var sortOption: SortOption = .callDate - @State private var filterOption: FilterOption = .all - @State private var sortOrder: SortOrder = .ascending - @State private var searchText = "" - @State private var isSearching: Bool = false - - init(tournament: Tournament, teams: [TeamRegistration]) { - self.tournaments = [tournament] - self.teams = teams - if tournament.hasEnded(), tournament.players().anySatisfy({ $0.hasPaid() == false }) { - _filterOption = .init(wrappedValue: .didNotPay) - } - if teams.filter({ $0.callDate != nil }).isEmpty { - _sortOption = .init(wrappedValue: .teamRank) - } else { - _sortOption = .init(wrappedValue: .callDate) - } - } +struct ShareableObject { - private func _sharedData() -> String { - let players = teams.filter({ _shouldDisplayTeam($0) }) - .flatMap({ $0.players().filter({ _shouldDisplayPlayer($0) }) }) + let cashierViewModel: CashierViewModel + let teams: [TeamRegistration] + let fileName: String + + func sharedData() async -> Data? { + let players = teams.filter({ cashierViewModel._shouldDisplayTeam($0) }) + .flatMap({ $0.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) }) .map { [$0.pasteData()] .compacted() .joined(separator: "\n") } .joined(separator: "\n\n") - return players + return players.data(using: .utf8) + } +} + +extension ShareableObject: Transferable { + enum ShareError: Error { + case failed + } + + static var transferRepresentation: some TransferRepresentation { + let rep = DataRepresentation(exportedContentType: .plainText) { object in + guard let data = await object.sharedData() else { + throw ShareError.failed + } + return data + } + return rep.suggestedFileName { object in object.fileName } + } +} + + +class CashierViewModel: ObservableObject { + let id: UUID = UUID() + @Published var sortOption: SortOption = .callDate + @Published var filterOption: FilterOption = .all + @Published var sortOrder: SortOrder = .ascending + @Published var searchText: String = "" + @Published var isSearching: Bool = false + + func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool { + team.unsortedPlayers().anySatisfy({ + _shouldDisplayPlayer($0) + }) + } + + func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { + if searchText.isEmpty == false { + filterOption.shouldDisplayPlayer(player) && player.contains(searchText) + } else { + filterOption.shouldDisplayPlayer(player) + } } enum SortOption: Int, Identifiable, CaseIterable { @@ -100,28 +123,43 @@ struct CashierView: View { } } } + +} + +struct CashierView: View { + @EnvironmentObject var dataStore: DataStore + @EnvironmentObject var cashierViewModel: CashierViewModel + + var tournaments : [Tournament] + var teams: [TeamRegistration] + @State private var shareableObject: ShareableObject? + + init(tournament: Tournament, teams: [TeamRegistration]) { + self.tournaments = [tournament] + self.teams = teams + } var body: some View { List { - if isSearching == false { + if cashierViewModel.isSearching == false { Section { - Picker(selection: $filterOption) { - ForEach(FilterOption.allCases) { filterOption in + Picker(selection: $cashierViewModel.filterOption) { + ForEach(CashierViewModel.FilterOption.allCases) { filterOption in Text(filterOption.localizedLabel()).tag(filterOption) } } label: { Text("Statut du règlement") } - Picker(selection: $sortOption) { - ForEach(SortOption.allCases) { sortOption in + Picker(selection: $cashierViewModel.sortOption) { + ForEach(CashierViewModel.SortOption.allCases) { sortOption in Text(sortOption.localizedLabel()).tag(sortOption) } } label: { Text("Affichage par") } - Picker(selection: $sortOrder) { + Picker(selection: $cashierViewModel.sortOrder) { Text("Croissant").tag(SortOrder.ascending) Text("Décroissant").tag(SortOrder.descending) } label: { @@ -131,12 +169,8 @@ struct CashierView: View { Text("Options d'affichage") } } - - if _isContentUnavailable() { - _contentUnavailableView() - } - switch sortOption { + switch cashierViewModel.sortOption { case .teamRank: _byTeamRankView() case .alphabeticalLastName: @@ -151,51 +185,53 @@ struct CashierView: View { _byCallDateView() } } + .searchable(text: $cashierViewModel.searchText, isPresented: $cashierViewModel.isSearching, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher un joueur")) + .onAppear { + cashierViewModel.searchText = "" +// if tournaments.count == 1 { +// if tournaments.first!.hasEnded() == true, tournaments.first!.players().anySatisfy({ $0.hasPaid() == false }) { +// filterOption = .didNotPay +// } +// } + if cashierViewModel.sortOption == .callDate && teams.first(where: { $0.callDate != nil }) == nil { + cashierViewModel.sortOption = .teamRank + } + self.shareableObject = ShareableObject(cashierViewModel: cashierViewModel, teams: teams, fileName: "Encaissement.txt") + } .headerProminence(.increased) - .searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur")) .toolbar { ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: _sharedData().createTxtFile("bilan")) + if let shareableObject { + ShareLink( + item: shareableObject, + preview: SharePreview(shareableObject.fileName) + ) + } } } } - - @ViewBuilder - func computedPlayerView(_ player: PlayerRegistration) -> some View { - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) - } - - private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool { - team.players().anySatisfy({ - _shouldDisplayPlayer($0) - }) - } - - private func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { - if searchText.isEmpty == false { - filterOption.shouldDisplayPlayer(player) && player.contains(searchText) - } else { - filterOption.shouldDisplayPlayer(player) - } - } - + @ViewBuilder private func _byPlayer(_ players: [PlayerRegistration]) -> some View { - let _players = sortOrder == .ascending ? players : players.reversed() - ForEach(_players) { player in - Section { - computedPlayerView(player) - } header: { - HStack { - if let teamCallDate = player.team()?.callDate { - Text(teamCallDate.localizedDate()) + let _players = cashierViewModel.sortOrder == .ascending ? players : players.reversed() + if _players.isEmpty { + _contentUnavailableView() + } else { + ForEach(_players) { player in + Section { + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + } header: { + HStack { + if let teamCallDate = player.team()?.callDate { + Text(teamCallDate.localizedDate()) + } + Spacer() + Text(player.formattedRank()) + } + } footer: { + if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() { + Text(tournamentTitle) } - Spacer() - Text(player.computedRank.formatted()) - } - } footer: { - if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() { - Text(tournamentTitle) } } } @@ -203,42 +239,52 @@ struct CashierView: View { @ViewBuilder private func _byPlayerRank() -> some View { - let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.computedRank)).filter({ _shouldDisplayPlayer($0) }) + let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.computedRank)).filter({ cashierViewModel._shouldDisplayPlayer($0) }) _byPlayer(players) } @ViewBuilder private func _byPlayerAge() -> some View { - let players = teams.flatMap({ $0.players() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ _shouldDisplayPlayer($0) }) + let players = teams.flatMap({ $0.unsortedPlayers() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ cashierViewModel._shouldDisplayPlayer($0) }) _byPlayer(players) } @ViewBuilder private func _byPlayerLastName() -> some View { - let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.lastName)).filter({ _shouldDisplayPlayer($0) }) + let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.lastName)).filter({ cashierViewModel._shouldDisplayPlayer($0) }) _byPlayer(players) } @ViewBuilder private func _byPlayerFirstName() -> some View { - let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.firstName)).filter({ _shouldDisplayPlayer($0) }) + let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.firstName)).filter({ cashierViewModel._shouldDisplayPlayer($0) }) _byPlayer(players) } @ViewBuilder private func _byTeamRankView() -> some View { - let _teams = sortOrder == .ascending ? teams : teams.reversed() - ForEach(_teams) { team in - if _shouldDisplayTeam(team) { + let _teams = cashierViewModel.sortOrder == .ascending ? teams : teams.reversed() + let _filteredTeams = _teams.filter({ cashierViewModel._shouldDisplayTeam($0) }) + if _filteredTeams.isEmpty { + _contentUnavailableView() + } else { + ForEach(_filteredTeams) { team in Section { - _cashierPlayersView(team.players()) + ForEach(team.players()) { player in + if cashierViewModel._shouldDisplayPlayer(player) { + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + } + } } header: { HStack { if let callDate = team.callDate { Text(callDate.localizedDate()) } Spacer() - Text(team.weight.formatted()) + VStack(alignment: .trailing, spacing: 0) { + Text("Poids").font(.caption) + Text(team.weight.formatted()) + } } } footer: { if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { @@ -252,52 +298,40 @@ struct CashierView: View { @ViewBuilder private func _byCallDateView() -> some View { - let groupedTeams = Dictionary(grouping: teams) { team in + let _teams = teams.filter({ $0.callDate != nil && cashierViewModel._shouldDisplayTeam($0) }) + + if _teams.isEmpty { + _contentUnavailableView() + } + + let groupedTeams = Dictionary(grouping: _teams) { team in team.callDate } - let keys = sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() + let keys = cashierViewModel.sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() ForEach(keys, id: \.self) { key in if let _teams = groupedTeams[key] { ForEach(_teams) { team in - if _shouldDisplayTeam(team) { - Section { - _cashierPlayersView(team.players()) - } header: { - Text(key.localizedDate()) - } footer: { - if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { - Text(tournamentTitle) + Section { + ForEach(team.players()) { player in + if cashierViewModel._shouldDisplayPlayer(player) { + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) } } + } header: { + Text(key.localizedDate()) + } footer: { + if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Text(tournamentTitle) + } } } } } } - - @ViewBuilder - private func _cashierPlayersView(_ players: [PlayerRegistration]) -> some View { - ForEach(players) { player in - if _shouldDisplayPlayer(player) { - computedPlayerView(player) - } - } - } - - private func _isContentUnavailable() -> Bool { - switch sortOption { - case .callDate: - return teams.filter({ $0.callDate != nil && _shouldDisplayTeam($0) }).isEmpty - case .teamRank: - return teams.filter({ _shouldDisplayTeam($0) }).isEmpty - default: - return teams.flatMap({ $0.players() }).filter({ _shouldDisplayPlayer($0) }).isEmpty - } - } private func _unavailableIcon() -> String { - switch sortOption { + switch cashierViewModel.sortOption { case .teamRank, .callDate: return "person.2.slash.fill" default: @@ -307,8 +341,8 @@ struct CashierView: View { @ViewBuilder private func _contentUnavailableView() -> some View { - if isSearching { - ContentUnavailableView.search(text: searchText) + if cashierViewModel.isSearching { + ContentUnavailableView.search(text: cashierViewModel.searchText) } else { ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon()) } diff --git a/PadelClub/Views/Cashier/Event/EventCreationView.swift b/PadelClub/Views/Cashier/Event/EventCreationView.swift index 6c959d1..3e04a56 100644 --- a/PadelClub/Views/Cashier/Event/EventCreationView.swift +++ b/PadelClub/Views/Cashier/Event/EventCreationView.swift @@ -90,39 +90,6 @@ struct EventCreationView: View { case .animation: animationEditorView } - - Section { - RowButtonView("Valider") { - let event = Event(creator: Store.main.userId, name: eventName) - event.club = selectedClub?.id - tournaments.forEach { tournament in - tournament.event = event.id - } - - do { - try dataStore.events.addOrUpdate(instance: event) - } catch { - Logger.error(error) - } - - tournaments.forEach { tournament in - tournament.courtCount = selectedClub?.courtCount ?? 2 - tournament.startDate = startingDate - tournament.dayDuration = duration - tournament.setupFederalSettings() - } - - do { - try dataStore.tournaments.addOrUpdate(contentOfs: tournaments) - } catch { - Logger.error(error) - } - - dismiss() - navigation.path.append(tournaments.first!) - } - .disabled(tournaments.isEmpty) - } } .toolbar { if textFieldIsFocus { @@ -144,11 +111,10 @@ struct EventCreationView: View { } ToolbarItem(placement: .topBarTrailing) { - BarButtonView("Ajouter une épreuve", icon: "plus.circle.fill") { - let tournament = Tournament.newEmptyInstance() - self.tournaments.append(tournament) + ButtonValidateView { + _validate() } - .popoverTip(multiTournamentsEventTip) + .disabled(tournaments.isEmpty) } } .navigationTitle("Nouvel événement") @@ -162,24 +128,63 @@ struct EventCreationView: View { } } + private func _validate() { + let event = Event(creator: Store.main.userId, name: eventName) + event.club = selectedClub?.id + tournaments.forEach { tournament in + tournament.event = event.id + } + + do { + try dataStore.events.addOrUpdate(instance: event) + } catch { + Logger.error(error) + } + + tournaments.forEach { tournament in + tournament.courtCount = selectedClub?.courtCount ?? 2 + tournament.startDate = startingDate + tournament.dayDuration = duration + tournament.setupFederalSettings() + } + + do { + try dataStore.tournaments.addOrUpdate(contentOfs: tournaments) + } catch { + Logger.error(error) + } + + dismiss() + navigation.path.append(tournaments.first!) + } + @ViewBuilder private var approvedTournamentEditorView: some View { - ForEach(tournaments) { tournament in + ForEach(tournaments.indices, id: \.self) { index in + let tournament = tournaments[index] Section { TournamentConfigurationView(tournament: tournament) - } footer: { + } header: { if tournaments.count > 1 { - FooterButtonView("effacer") { - tournaments.removeAll(where: { $0 == tournament }) + HStack { + Spacer() + FooterButtonView("effacer") { + tournaments.removeAll(where: { $0 == tournament }) + } + .textCase(nil) + } + } + } footer: { + if index == tournaments.count - 1 { + HStack { + Spacer() + FooterButtonView("Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") { + let tournament = Tournament.newEmptyInstance() + self.tournaments.append(tournament) + } + .popoverTip(multiTournamentsEventTip) } } - } - } - - Section { - RowButtonView("Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") { - let tournament = Tournament.newEmptyInstance() - self.tournaments.append(tournament) } } } diff --git a/PadelClub/Views/Cashier/Event/EventView.swift b/PadelClub/Views/Cashier/Event/EventView.swift index 294d3df..c4707b9 100644 --- a/PadelClub/Views/Cashier/Event/EventView.swift +++ b/PadelClub/Views/Cashier/Event/EventView.swift @@ -8,7 +8,11 @@ import SwiftUI import LeStorage -enum EventDestination: Identifiable, Selectable { +enum EventDestination: Identifiable, Selectable, Equatable { + static func == (lhs: EventDestination, rhs: EventDestination) -> Bool { + return lhs.id == rhs.id + } + case links case tournaments(Event) case cashier diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index f94cab0..7f393d6 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -41,11 +41,6 @@ struct ClubSearchView: View { var club: Club? var selection: ((Club) -> ())? = nil - fileprivate class DebouncableViewModel: ObservableObject { - @Published var debouncableText: String = "" - var debounceTrigger: Double = 0.15 - } - private var distanceLimit: Measurement { Measurement(value: radius, unit: .kilometers) } @@ -96,9 +91,9 @@ struct ClubSearchView: View { _importClub(clubToEdit: clubToEdit, clubMarker: clubMark) } label: { clubView(clubMark) - .frame(maxWidth: .infinity) .contentShape(Rectangle()) } + .frame(maxWidth: .infinity) .buttonStyle(.plain) } } header: { diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 80cc909..2c7fc9a 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -7,75 +7,109 @@ import SwiftUI -struct GenericDestinationPickerView: View { +struct GenericDestinationPickerView: View { @EnvironmentObject var dataStore: DataStore @Binding var selectedDestination: T? let destinations: [T] let nilDestinationIsValid: Bool var body: some View { - ScrollView(.horizontal) { - HStack { - if nilDestinationIsValid { - Button { - selectedDestination = nil - } label: { - Image(systemName: "wrench.and.screwdriver") - .foregroundColor(selectedDestination == nil ? .white : .black) - } - .padding() - .background { - Circle() - .fill(selectedDestination == nil ? .master : .beige) - } - .buttonStyle(.plain) - } - - ForEach(destinations) { destination in - Button { - selectedDestination = destination - } label: { - Text(destination.selectionLabel()) - .foregroundStyle(selectedDestination?.id == destination.id ? .white : .black) - } - .padding() - .background { - Capsule() - .fill(selectedDestination?.id == destination.id ? .master : .beige) + ScrollViewReader { proxy in + ScrollView(.horizontal) { + HStack { + if nilDestinationIsValid { + Button { + selectedDestination = nil + } label: { + Image(systemName: "wrench.and.screwdriver") + .foregroundColor(selectedDestination == nil ? .white : .black) + } + .padding() + .background { + Circle() + .fill(selectedDestination == nil ? .master : .beige) + } + .buttonStyle(.plain) + .id("settings") } - .buttonStyle(.plain) - .overlay(alignment: .bottomTrailing) { - if let badge = destination.badgeImage() { - Image(systemName: badge.systemName()) - .foregroundColor(badge.color()) - .imageScale(.medium) - .background ( - Color(.systemBackground) - .clipShape(.circle) - ) - .offset(x: 3, y: 3) - } else if let count = destination.badgeValue(), count > 0 { - Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") - .foregroundColor(destination.badgeValueColor() ?? .red) - .imageScale(.medium) - .background ( - Color(.systemBackground) - .clipShape(.circle) - ) - .offset(x: 3, y: 3) + + ForEach(destinations) { destination in + Button { + selectedDestination = destination + } label: { + Text(destination.selectionLabel()) + .foregroundStyle(selectedDestination?.id == destination.id ? .white : .black) + } + .padding() + .background { + Capsule() + .fill(selectedDestination?.id == destination.id ? .master : .beige) + } + .id(destination.id) + .buttonStyle(.plain) + .overlay(alignment: .bottomTrailing) { + if destination.displayImageIfValueZero() { + let count = destination.badgeValue() + if let count, count == 0, let badge = destination.badgeImage() { + Image(systemName: badge.systemName()) + .foregroundColor(badge.color()) + .imageScale(.medium) + .background ( + Color(.systemBackground) + .clipShape(.circle) + ) + .offset(x: 3, y: 3) + } else if let count, count > 0 { + Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") + .foregroundColor(destination.badgeValueColor() ?? .red) + .imageScale(.medium) + .background ( + Color(.systemBackground) + .clipShape(.circle) + ) + .offset(x: 3, y: 3) + } + } else { + if let badge = destination.badgeImage() { + Image(systemName: badge.systemName()) + .foregroundColor(badge.color()) + .imageScale(.medium) + .background ( + Color(.systemBackground) + .clipShape(.circle) + ) + .offset(x: 3, y: 3) + } else if let count = destination.badgeValue(), count > 0 { + Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") + .foregroundColor(destination.badgeValueColor() ?? .red) + .imageScale(.medium) + .background ( + Color(.systemBackground) + .clipShape(.circle) + ) + .offset(x: 3, y: 3) + } + } } } } + .fixedSize() + .padding(8) } - .fixedSize() - .padding(8) - } - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .background(Material.ultraThinMaterial) - .overlay { - VStack(spacing: 0) { - Spacer() - Divider() + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .background(Material.ultraThinMaterial) + .overlay { + VStack(spacing: 0) { + Spacer() + Divider() + } + } + .onAppear { + if let selectedDestination { + proxy.scrollTo(selectedDestination.id) + } else { + proxy.scrollTo("settings") + } } } } diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index 8ea90a7..9ecd9cd 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -10,23 +10,38 @@ import SwiftUI struct MatchListView: View { @EnvironmentObject var dataStore: DataStore let section: String - let matches: [Match] + let matches: [Match]? var matchViewStyle: MatchViewStyle = .standardStyle + var hideWhenEmpty: Bool = false @State var isExpanded: Bool = true + private func _shouldHide() -> Bool { + if matches != nil && matches!.isEmpty && hideWhenEmpty == true { + return true + } else { + return false + } + } + @ViewBuilder var body: some View { - if matches.isEmpty == false { + if _shouldHide() == false { Section { DisclosureGroup(isExpanded: $isExpanded) { - ForEach(matches) { match in - MatchRowView(match: match, matchViewStyle: matchViewStyle) - .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) + if let matches { + ForEach(matches) { match in + MatchRowView(match: match, matchViewStyle: matchViewStyle) + .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) + } } } label: { LabeledContent { - Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) + if matches == nil { + ProgressView() + } else { + Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) + } } label: { Text(section.firstCapitalized) } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 1631bae..098ad1c 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -17,6 +17,10 @@ struct GroupStageView: View { @State private var confirmResetMatch: Bool = false let playedMatches: [Match] + @State private var runningMatches: [Match]? + @State private var readyMatches: [Match]? + @State private var availableToStart: [Match]? + init(groupStage: GroupStage) { self.groupStage = groupStage self.playedMatches = groupStage.playedMatches() @@ -44,12 +48,16 @@ struct GroupStageView: View { } .headerProminence(.increased) - let runningMatches = groupStage.runningMatches(playedMatches: playedMatches) - MatchListView(section: "disponible", matches: groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches)) - MatchListView(section: "en cours", matches: runningMatches) - MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches)) + MatchListView(section: "en cours", matches: runningMatches, hideWhenEmpty: true) + MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true) + MatchListView(section: "à lancer", matches: self.readyMatches, hideWhenEmpty: true) MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), isExpanded: false) } + .task { + self.runningMatches = await groupStage.asyncRunningMatches(playedMatches: playedMatches) + self.readyMatches = await groupStage.readyMatches(playedMatches: playedMatches) + self.availableToStart = await groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches ?? []) + } .toolbar { ToolbarItem(placement: .topBarTrailing) { _groupStageMenuView() diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 28753ac..248ce5c 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -11,7 +11,11 @@ struct GroupStagesView: View { var tournament: Tournament @State private var selectedDestination: GroupStageDestination? - enum GroupStageDestination: Selectable, Identifiable { + enum GroupStageDestination: Selectable, Identifiable, Equatable { + static func == (lhs: GroupStagesView.GroupStageDestination, rhs: GroupStagesView.GroupStageDestination) -> Bool { + lhs.id == rhs.id + } + case all case groupStage(GroupStage) @@ -47,7 +51,12 @@ struct GroupStagesView: View { } func badgeImage() -> Badge? { - nil + switch self { + case .all: + return nil + case .groupStage(let groupStage): + return groupStage.badgeImage() + } } } @@ -76,30 +85,36 @@ struct GroupStagesView: View { return allDestinations } + @State private var runningMatches: [Match]? + @State private var readyMatches: [Match]? + @State private var availableToStart: [Match]? + var body: some View { VStack(spacing: 0) { GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) switch selectedDestination { case .all: - let runningMatches = tournament.runningMatches(allMatches) - let availableToStart = tournament.availableToStart(allMatches, in: runningMatches) - let readyMatches = tournament.readyMatches(allMatches) let finishedMatches = tournament.finishedMatches(allMatches) List { MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "disponible", matches: availableToStart, matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "prêt à démarrer", matches: availableToStart, matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "à lancer", matches: readyMatches, matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) } + .task { + runningMatches = await tournament.asyncRunningMatches(allMatches) + availableToStart = await tournament.availableToStart(allMatches, in: runningMatches ?? []) + readyMatches = await tournament.readyMatches(allMatches) + } .overlay { - if availableToStart.isEmpty && runningMatches.isEmpty && readyMatches.isEmpty && finishedMatches.isEmpty { + if availableToStart?.isEmpty == true && runningMatches?.isEmpty == true && readyMatches?.isEmpty == true && finishedMatches.isEmpty == true { ContentUnavailableView("Aucun match à afficher", systemImage: "tennisball") } } .navigationTitle("Toutes les poules") case .groupStage(let groupStage): - GroupStageView(groupStage: groupStage) + GroupStageView(groupStage: groupStage).id(groupStage.id) case nil: GroupStageSettingsView() .navigationTitle("Réglages") diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index bffc314..62c86f3 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -308,6 +308,8 @@ struct MatchDetailView: View { Button(role: .destructive) { match.resetScores() + match.resetMatch() + match.confirmed = false save() } label: { Text("Supprimer les scores") diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index b45777a..11f1410 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -22,8 +22,11 @@ struct MatchSetupView: View { @ViewBuilder func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View { - let team = match.team(teamPosition) - let teamScore = match.teamScore(ofTeam: team) + let scores = match.teamScores + let team = scores.isEmpty ? nil : match.team(teamPosition) + let teamScore = (team != nil) ? scores.first(where: { $0.teamRegistration == team!.id }) : nil + let walkOutSpot = teamScore?.walkOut == 1 + if let team, teamScore?.walkOut == nil { VStack(alignment: .leading, spacing: 0) { if let teamScore, teamScore.luckyLoser != nil { @@ -67,7 +70,6 @@ struct MatchSetupView: View { .strikethrough() } HStack { - let walkOutSpot = match.isWalkOutSpot(teamPosition) let luckyLosers = walkOutSpot ? match.luckyLosers() : [] TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in print(team.pasteData()) diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 5228dd3..8efbdf4 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -44,32 +44,6 @@ struct MatchSummaryView: View { } var body: some View { - matchSummaryView -// .contextMenu { -// ForEach(match.teamScores) { entrant in -// if let team = entrant.team, team.orderedPlayers.count > 2 { -// NavigationLink { -// PlayerPickerView(match: match, team: team) -// } label: { -// if let teamTitle = team.entrant?.brand?.title { -// Text(teamTitle).foregroundStyle(.secondary) -// } else { -// let index = match.orderedEntrants.firstIndex(where: { $0 == entrant }) ?? 0 -// Text("Équipe \(index + 1)") -// } -// if match.players(from: team).isEmpty { -// Text("Choisir la paire") -// } else { -// Text("Modifier la paire") -// } -// } -// } -// } -// } - } - - @ViewBuilder - var matchSummaryView: some View { VStack(alignment: .leading) { if matchViewStyle != .plainStyle { if matchViewStyle == .feedStyle, let tournament = match.currentTournament() { diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 1539b9b..f1e080b 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -30,18 +30,17 @@ struct ActivityView: View { var endedTournaments: [Tournament] { dataStore.tournaments.filter({ $0.endDate != nil }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) }) - .sorted(using: SortDescriptor(\.startDate, order: .reverse)) - } - - func _activityStatus() -> String? { - let tournaments = tournaments - if tournaments.isEmpty && federalDataViewModel.areFiltersEnabled() == false { - return nil - } else { - let count = tournaments.map { $0.tournaments.count }.reduce(0,+) - return "\(count) tournoi" + count.pluralSuffix - } } +// +// func _activityStatus() -> String? { +// let tournaments = tournaments +// if tournaments.isEmpty && federalDataViewModel.areFiltersEnabled() == false { +// return nil +// } else { +// let count = tournaments.map { $0.tournaments.count }.reduce(0,+) +// return "\(count) tournoi" + count.pluralSuffix +// } +// } var tournaments: [FederalTournamentHolder] { switch navigation.agendaDestination! { @@ -63,11 +62,11 @@ struct ActivityView: View { List { switch navigation.agendaDestination! { case .activity: - EventListView(tournaments: runningTournaments, viewStyle: viewStyle) + EventListView(tournaments: runningTournaments, viewStyle: viewStyle, sortAscending: true) case .history: - EventListView(tournaments: endedTournaments, viewStyle: viewStyle) + EventListView(tournaments: endedTournaments, viewStyle: viewStyle, sortAscending: false) case .tenup: - EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle) + EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle, sortAscending: true) .id(uuid) } } @@ -136,19 +135,10 @@ struct ActivityView: View { } .toolbar { if presentToolbar { - let _activityStatus = _activityStatus() - if federalDataViewModel.areFiltersEnabled() || _activityStatus != nil { + //let _activityStatus = _activityStatus() + if federalDataViewModel.areFiltersEnabled() { ToolbarItem(placement: .status) { - VStack(spacing: -2) { - if federalDataViewModel.areFiltersEnabled() { - Text(federalDataViewModel.filterStatus()) - } - if let _activityStatus { - Text(_activityStatus) - .foregroundStyle(.secondary) - } - } - .font(.footnote) + Text(federalDataViewModel.filterStatus()) } } diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 3011b04..7cfcb48 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -15,13 +15,15 @@ struct EventListView: View { let tournaments: [FederalTournamentHolder] let viewStyle: AgendaDestination.ViewStyle - + let sortAscending: Bool + var body: some View { let groupedTournamentsByDate = Dictionary(grouping: navigation.agendaDestination == .tenup ? federalDataViewModel.filteredFederalTournaments : tournaments) { $0.startDate.startOfMonth } switch viewStyle { case .list: - ForEach(groupedTournamentsByDate.keys.sorted(by: <), id: \.self) { section in - if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: \.startDate) { + ForEach(groupedTournamentsByDate.keys.sorted(by: sortAscending ? { $0 < $1 } : { $0 > $1 }), id: \.self) { section in + if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate } + ) { Section { _listView(_tournaments) } header: { @@ -37,7 +39,7 @@ struct EventListView: View { } case .calendar: ForEach(_nextMonths(), id: \.self) { section in - let _tournaments = groupedTournamentsByDate[section]?.sorted(by: \.startDate) ?? [] + let _tournaments = groupedTournamentsByDate[section] ?? [] Section { CalendarView(date: section, tournaments: _tournaments).id(federalDataViewModel.id) } header: { @@ -117,5 +119,5 @@ struct EventListView: View { } #Preview { - EventListView(tournaments: [], viewStyle: .calendar) + EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true) } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 3b4daa1..35506bd 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -48,7 +48,7 @@ struct MainView: View { dataStore.matches.filter({ $0.confirmed && $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }) } - var badgeText: Text? = Store.main.userName() == nil ? Text("!").font(.headline) : nil + var badgeText: Text? = Store.main.userId == nil ? Text("!").font(.headline) : nil var body: some View { TabView(selection: selectedTabHandler) { diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 53785d0..4e4f865 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -17,7 +17,6 @@ struct OngoingView: View { var matches: [Match] { let sorting = sortByField ? fieldSorting : defaultSorting - let now = Date() return dataStore.matches.filter({ $0.confirmed && $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }).sorted(using: sorting, order: .ascending) } diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift new file mode 100644 index 0000000..e254e6a --- /dev/null +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -0,0 +1,31 @@ +// +// LoserRoundSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/06/2024. +// + +import SwiftUI +import LeStorage + +struct LoserRoundSettingsView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed + @Environment(Tournament.self) var tournament: Tournament + + var body: some View { + List { + Section { + RowButtonView(isEditingTournamentSeed.wrappedValue == true ? "Terminer l'édition" : "Éditer les tours joués") { + isEditingTournamentSeed.wrappedValue.toggle() + } + } + + //todo proposer ici l'impression des matchs de classements peut-être? + } + } +} + +#Preview { + LoserRoundSettingsView() +} diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index 89e7ca0..ceb9efa 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -25,9 +25,9 @@ struct LoserRoundView: View { } ForEach(loserRounds) { loserRound in - if isEditingTournamentSeed || loserRound.isDisabled() == false { + if true { Section { - let matches = (isEditingTournamentSeed ? loserRound.playedMatches() : loserRound.playedMatches().filter({ $0.disabled == false })).sorted(by: \.index) + let matches = loserRound.playedMatches().sorted(by: \.index) ForEach(matches) { match in MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) .overlay { diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 0039f81..b7f2a83 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -34,7 +34,12 @@ struct LoserRound: Identifiable, Selectable { } -extension LoserRound { +extension LoserRound: Equatable { + static func == (lhs: LoserRound, rhs: LoserRound) -> Bool { + lhs.id == rhs.id + } + + func selectionLabel() -> String { return "Tour #\(turnIndex + 1)" } @@ -54,7 +59,6 @@ extension LoserRound { struct LoserRoundsView: View { - @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed var upperBracketRound: Round @State private var selectedRound: LoserRound? let loserRounds: [Round] @@ -64,8 +68,7 @@ struct LoserRoundsView: View { self.upperBracketRound = upperBracketRound let _loserRounds = upperBracketRound.loserRounds() self.loserRounds = _loserRounds - let enabledLoserRounds = LoserRound.enabledLoserRounds(inLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound) - let rounds = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound) + let rounds = LoserRound.updateDestinations(fromLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound) _allDestinations = State(wrappedValue: rounds) _selectedRound = State(wrappedValue: rounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? rounds.first) @@ -78,14 +81,5 @@ struct LoserRoundsView: View { } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - .onChange(of: isEditingTournamentSeed.wrappedValue) { - _updateDestinations() - } - } - - private func _updateDestinations() { - let enabledLoserRounds = isEditingTournamentSeed.wrappedValue ? loserRounds : LoserRound.enabledLoserRounds(inLoserRounds: loserRounds, inUpperBracketRound: upperBracketRound) - - self.allDestinations = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound) } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index fbf9253..eccbf35 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -42,19 +42,7 @@ struct RoundSettingsView: View { // } Section { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { - tournament.unsortedTeams().forEach({ team in - tournament.resetTeamScores(in: team.bracketPosition) - team.bracketPosition = nil - }) - do { - try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) - } catch { - Logger.error(error) - } - tournament.allRounds().forEach({ round in - round.enableRound() - }) - self.isEditingTournamentSeed.wrappedValue = true + await _removeAllSeeds() } } @@ -94,6 +82,30 @@ struct RoundSettingsView: View { } } } + + private func _removeAllSeeds() async { + tournament.unsortedTeams().forEach({ team in + team.bracketPosition = nil + }) + let ts = tournament.allRoundMatches().flatMap { match in + match.teamScores + } + + do { + try DataStore.shared.teamScores.delete(contentOfs: ts) + } catch { + Logger.error(error) + } + do { + try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) + } catch { + Logger.error(error) + } + tournament.allRounds().forEach({ round in + round.enableRound() + }) + self.isEditingTournamentSeed.wrappedValue = true + } } #Preview { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 742396b..ea8f354 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -13,6 +13,27 @@ struct RoundView: View { @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore @State private var selectedSeedGroup: SeedInterval? + @State private var spaceLeft: [Match] = [] + @State private var seedSpaceLeft: [Match] = [] + @State private var availableSeedGroup: SeedInterval? + + private func _getAvailableSeedGroup() async { + availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) + } + + private func _getSpaceLeft() async { + spaceLeft.removeAll() + seedSpaceLeft.removeAll() + round.displayableMatches().forEach({ + let count = $0.teamScores.count + if count == 0 { + seedSpaceLeft.append($0) + } else if count == 1 { + spaceLeft.append($0) + } + }) + } + var showVisualDrawView: Binding { Binding( get: { selectedSeedGroup != nil }, set: { @@ -26,14 +47,13 @@ struct RoundView: View { var body: some View { List { - - let loserRounds = round.loserRounds() - let availableSeeds = tournament.availableSeeds() - let availableQualifiedTeams = tournament.availableQualifiedTeams() let displayableMatches = round.displayableMatches().sorted(by: \.index) - let spaceLeft = displayableMatches.filter({ $0.hasSpaceLeft() }) - let seedSpaceLeft = displayableMatches.filter({ $0.isEmpty() }) - if isEditingTournamentSeed.wrappedValue == false { + let loserRounds = round.loserRounds() + if displayableMatches.isEmpty { + Section { + ContentUnavailableView("Aucun match dans cette manche", systemImage: "tennisball") + } + } else if isEditingTournamentSeed.wrappedValue == false { //(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue }) if loserRounds.isEmpty == false { let correspondingLoserRoundTitle = round.correspondingLoserRoundTitle() @@ -48,14 +68,19 @@ struct RoundView: View { } } } else { - if let availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) { + let availableSeeds = tournament.availableSeeds() + let availableQualifiedTeams = tournament.availableQualifiedTeams() + + if availableSeeds.isEmpty == false, let availableSeedGroup { Section { RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup) - _save() + //_save() if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } + await _getSpaceLeft() + await _getAvailableSeedGroup() } } footer: { if availableSeedGroup.isFixed() == false { @@ -74,86 +99,95 @@ struct RoundView: View { } } - - if availableQualifiedTeams.isEmpty == false && spaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableQualifiedTeams) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: spaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) - } - _save() - if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { - self.isEditingTournamentSeed.wrappedValue = false + if availableQualifiedTeams.isEmpty == false { + if spaceLeft.isEmpty == false { + Section { + DisclosureGroup { + ForEach(availableQualifiedTeams) { team in + NavigationLink { + SpinDrawView(drawees: [team], segments: spaceLeft) { results in + Task { + results.forEach { drawResult in + team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) + } + await _save() + if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { + self.isEditingTournamentSeed.wrappedValue = false + } + await _getSpaceLeft() + await _getAvailableSeedGroup() } } + } label: { + TeamRowView(team: team, displayCallDate: false) } - } label: { - TeamRowView(team: team, displayCallDate: false) } + } label: { + Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) } - } label: { - Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) + } header: { + Text("Tirage au sort visuel d'un qualifié").font(.subheadline) } - } header: { - Text("Tirage au sort visuel d'un qualifié").font(.subheadline) } } - if availableSeeds.isEmpty == false && seedSpaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) - } - _save() - if availableSeeds.isEmpty && tournament.availableQualifiedTeams().isEmpty { - self.isEditingTournamentSeed.wrappedValue = false + if availableSeeds.isEmpty == false { + if seedSpaceLeft.isEmpty == false { + Section { + DisclosureGroup { + ForEach(availableSeeds) { team in + NavigationLink { + SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in + Task { + results.forEach { drawResult in + team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) + } + await _save() + if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { + self.isEditingTournamentSeed.wrappedValue = false + } + await _getSpaceLeft() + await _getAvailableSeedGroup() } } + } label: { + TeamRowView(team: team, displayCallDate: false) } - } label: { - TeamRowView(team: team, displayCallDate: false) } + } label: { + Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) + } header: { + Text("Tirage au sort visuel d'une tête de série").font(.subheadline) } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) - } - } else if availableSeeds.isEmpty == false && spaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: spaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) - } - _save() - if availableSeeds.isEmpty && tournament.availableQualifiedTeams().isEmpty { - self.isEditingTournamentSeed.wrappedValue = false + } else if spaceLeft.isEmpty == false { + Section { + DisclosureGroup { + ForEach(availableSeeds) { team in + NavigationLink { + SpinDrawView(drawees: [team], segments: spaceLeft) { results in + Task { + results.forEach { drawResult in + team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) + } + await _save() + if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { + self.isEditingTournamentSeed.wrappedValue = false + } + await _getSpaceLeft() + await _getAvailableSeedGroup() } } + } label: { + TeamRowView(team: team, displayCallDate: false) } - } label: { - TeamRowView(team: team, displayCallDate: false) } + } label: { + Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) + } header: { + Text("Tirage au sort visuel d'une tête de série").font(.subheadline) } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) } } } @@ -164,19 +198,30 @@ struct RoundView: View { HStack { Text(round.roundTitle(.wide)) if round.index > 0 { - Text(match.matchTitle(.short)) + Text(match.matchTitle(.short, inMatches: displayableMatches)) } else { let tournamentTeamCount = tournament.teamCount - if let seedIntervalPointRange = loserRounds.first?.seedInterval()?.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournamentTeamCount) { + if let seedIntervalPointRange = round.seedInterval()?.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournamentTeamCount) { Spacer() Text(seedIntervalPointRange) .font(.caption) } } + + #if DEBUG + Spacer() + + Text(match.teamScores.count.formatted()) + #endif } } } } + .onAppear { + Task { + await _prepareRound() + } + } .fullScreenCover(isPresented: showVisualDrawView) { if let availableSeedGroup = selectedSeedGroup { let seeds = tournament.seeds(inSeedGroup: availableSeedGroup) @@ -187,7 +232,7 @@ struct RoundView: View { draws.forEach { drawResult in seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: false) } - _save() + await _save() if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } @@ -201,7 +246,9 @@ struct RoundView: View { ToolbarItem(placement: .topBarTrailing) { Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { if isEditingTournamentSeed.wrappedValue { - _save() + Task { + await _save() + } } isEditingTournamentSeed.wrappedValue.toggle() } @@ -209,7 +256,7 @@ struct RoundView: View { } } - private func _save() { + private func _save() async { do { try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) } catch { @@ -221,7 +268,7 @@ struct RoundView: View { rounds.forEach { round in let matches = round.playedMatches() matches.forEach { match in - match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound()) + match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) } } let allRoundMatches = tournament.allRoundMatches() @@ -231,6 +278,15 @@ struct RoundView: View { Logger.error(error) } } + + private func _prepareRound() async { + Task { + await _getSpaceLeft() + } + Task { + await _getAvailableSeedGroup() + } + } } #Preview { diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index eac0d07..e1fbacc 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -33,7 +33,7 @@ struct RoundsView: View { RoundSettingsView() .navigationTitle("Réglages") case .some(let selectedRound): - RoundView(round: selectedRound) + RoundView(round: selectedRound).id(selectedRound.id) .navigationTitle(selectedRound.roundTitle()) } } diff --git a/PadelClub/Views/Subscription/Guard.swift b/PadelClub/Views/Subscription/Guard.swift index c74974e..ca95431 100644 --- a/PadelClub/Views/Subscription/Guard.swift +++ b/PadelClub/Views/Subscription/Guard.swift @@ -141,7 +141,7 @@ import LeStorage var currentPlan: StoreItem? { // #if DEBUG -// return .monthlyUnlimited + return .monthlyUnlimited // #else if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) { return plan diff --git a/PadelClub/Views/Team/Components/TeamHeaderView.swift b/PadelClub/Views/Team/Components/TeamHeaderView.swift index d6a0dbf..32dfb0f 100644 --- a/PadelClub/Views/Team/Components/TeamHeaderView.swift +++ b/PadelClub/Views/Team/Components/TeamHeaderView.swift @@ -13,10 +13,6 @@ struct TeamHeaderView: View { var tournament: Tournament? var body: some View { - _teamHeaderView(team, teamIndex: teamIndex) - } - - private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { HStack(spacing: 16.0) { if let teamIndex { VStack(alignment: .leading, spacing: 0) { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index c239fb5..a42b4af 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -25,6 +25,22 @@ struct EditingTeamView: View { } header: { Text("Date d'inscription") } + + Section { + RowButtonView("Retirer des poules", role: .destructive) { + team.resetGroupeStagePosition() + _save() + } + .disabled(team.inGroupStage() == false) + } + + Section { + RowButtonView("Retirer du tableau", role: .destructive) { + team.resetBracketPosition() + _save() + } + .disabled(team.inRound() == false) + } } .onChange(of: registrationDate) { team.registrationDate = registrationDate diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index 6a2459c..9f93b05 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -80,7 +80,9 @@ struct TeamPickerView: View { presentTeamPickerView = false } label: { TeamRowView(team: team) + .contentShape(Rectangle()) } + .frame(maxWidth: .infinity) .buttonStyle(.plain) } } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 530384f..5eb7799 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -27,7 +27,8 @@ struct FileImportView: View { @State private var selectedOptions: Set = Set() @State private var fileProvider: FileImportManager.FileProvider = .frenchFederation - + @State private var validationInProgress: Bool = false + private var filteredTeams: [FileImportManager.TeamHolder] { return teams.filter { $0.tournamentCategory == tournament.tournamentCategory }.sorted(by: \.weight) } @@ -104,6 +105,16 @@ struct FileImportView: View { } } + if validationInProgress { + Section { + LabeledContent { + ProgressView() + } label: { + Text("Mise à jour des équipes") + } + } + } + if let errorMessage { Section { Text(errorMessage) @@ -146,7 +157,7 @@ struct FileImportView: View { Section { ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash") } - } else if didImport { + } else if didImport && validationInProgress == false { let _filteredTeams = filteredTeams let previousTeams = tournament.sortedTeams() @@ -222,38 +233,40 @@ struct FileImportView: View { ToolbarItem(placement: .topBarTrailing) { ButtonValidateView { -// if false { //selectedOptions.contains(.deleteBeforeImport) -// try? dataStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams()) -// } - - if true { //selectedOptions.contains(.notFoundAreWalkOut) - let previousTeams = filteredTeams.compactMap({ $0.previousTeam }) - - let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams)) - unfound.forEach { team in - team.resetPositions() - team.wildCardBracket = false - team.wildCardGroupStage = false - team.walkOut = true - } - - do { - try dataStore.teamRegistrations.addOrUpdate(contentOfs: unfound) - } catch { - Logger.error(error) - } - - } - - tournament.importTeams(filteredTeams) - dismiss() + _validate() } .disabled(teams.isEmpty) } } + .interactiveDismissDisabled(validationInProgress) + .disabled(validationInProgress) + } + + private func _validate() { + validationInProgress = true + Task { + let previousTeams = filteredTeams.compactMap({ $0.previousTeam }) + + let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams)) + unfound.forEach { team in + team.resetPositions() + team.wildCardBracket = false + team.wildCardGroupStage = false + team.walkOut = true + } + + do { + try dataStore.teamRegistrations.addOrUpdate(contentOfs: unfound) + } catch { + Logger.error(error) + } + + tournament.importTeams(filteredTeams) + dismiss() + } } - func _startImport(fileContent: String) async throws { + private func _startImport(fileContent: String) async throws { await MainActor.run { errorMessage = nil teams.removeAll() diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index 7d33725..caf27f2 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -17,18 +17,40 @@ extension String : Identifiable { struct BroadcastView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament + @Environment(NavigationViewModel.self) var navigation: NavigationViewModel + let context = CIContext() let filter = CIFilter.qrCodeGenerator() @State private var urlToShow: String? @State private var tvMode: Bool = false @State private var pageLink: PageLink = .teams + let createAccountTip = CreateAccountTip() let tournamentPublishingTip = TournamentPublishingTip() let tournamentTVBroadcastTip = TournamentTVBroadcastTip() var body: some View { @Bindable var tournament = tournament List { + if Store.main.userId == nil { + Section { + TipView(createAccountTip) { action in + switch action.id { + case CreateAccountTip.ActionKey.accessPadelClubWebPage.rawValue: + UIApplication.shared.open(URLs.main.url) + case CreateAccountTip.ActionKey.createAccount.rawValue: + navigation.selectedTab = .umpire + default: + break + //todo +// case CreateAccountTip.ActionKey.learnMore.rawValue: +// UIApplication.shared.open(URLs.padelClubLandingPage.url) + } + } + .tipStyle(tint: .master) + } + } + Section { TipView(tournamentPublishingTip) { action in UIApplication.shared.open(URLs.main.url) diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index 9541109..bc0710e 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -11,20 +11,16 @@ struct InscriptionInfoView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament - var players : [PlayerRegistration] { tournament.unsortedPlayers() } - var selectedTeams : [TeamRegistration] { tournament.selectedSortedTeams() } - - var callDateIssue : [TeamRegistration] { - selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) } - } - - var waitingList : [TeamRegistration] { tournament.waitingListTeams(in: selectedTeams) } - var duplicates : [PlayerRegistration] { tournament.duplicates(in: players) } - var problematicPlayers : [PlayerRegistration] { players.filter({ $0.sex == nil }) } - var inadequatePlayers : [PlayerRegistration] { tournament.inadequatePlayers(in: players) } - var playersWithoutValidLicense : [PlayerRegistration] { tournament.playersWithoutValidLicense(in: players) } - var entriesFromBeachPadel : [TeamRegistration] { tournament.unsortedTeams().filter({ $0.isImported() }) } - var playersMissing : [TeamRegistration] { selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) } + @State private var players : [PlayerRegistration] = [] + @State private var selectedTeams : [TeamRegistration] = [] + @State private var callDateIssue : [TeamRegistration] = [] + @State private var waitingList : [TeamRegistration] = [] + @State private var duplicates : [PlayerRegistration] = [] + @State private var problematicPlayers : [PlayerRegistration] = [] + @State private var inadequatePlayers : [PlayerRegistration] = [] + @State private var playersWithoutValidLicense : [PlayerRegistration] = [] + @State private var entriesFromBeachPadel : [TeamRegistration] = [] + @State private var playersMissing : [TeamRegistration] = [] var body: some View { List { @@ -196,10 +192,28 @@ struct InscriptionInfoView: View { .listRowView(color: .pink) } } + .task { + await _getIssues() + } .navigationTitle("Synthèse") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - } + } + + private func _getIssues() async { + Task { + players = tournament.unsortedPlayers() + selectedTeams = tournament.selectedSortedTeams() + callDateIssue = selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) } + waitingList = tournament.waitingListTeams(in: selectedTeams) + duplicates = tournament.duplicates(in: players) + problematicPlayers = players.filter({ $0.sex == nil }) + inadequatePlayers = tournament.inadequatePlayers(in: players) + playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players) + entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() }) + playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) + } + } } #Preview { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 2f5f3fe..d5b27a8 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -9,12 +9,22 @@ import SwiftUI import TipKit import LeStorage +let slideToDeleteTip = SlideToDeleteTip() +let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() +let fileTip = InscriptionManagerFileInputTip() +let pasteTip = InscriptionManagerPasteInputTip() +let searchTip = InscriptionManagerSearchInputTip() +let createTip = InscriptionManagerCreateInputTip() +let rankUpdateTip = InscriptionManagerRankUpdateTip() +let padelBeachExportTip = PadelBeachExportTip() +let padelBeachImportTip = PadelBeachImportTip() + struct InscriptionManagerView: View { @EnvironmentObject var dataStore: DataStore @EnvironmentObject var networkMonitor: NetworkMonitor @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], + sortDescriptors: [], animation: .default) private var fetchPlayers: FetchedResults @@ -41,7 +51,14 @@ struct InscriptionManagerView: View { @State private var contactType: ContactType? = nil @State private var sentError: ContactManagerError? = nil @State private var showSubscriptionView: Bool = false - + @State private var registrationIssues: Int? = nil + @State private var sortedTeams: [TeamRegistration] = [] + @State private var unfilteredTeams: [TeamRegistration] = [] + @State private var walkoutTeams: [TeamRegistration] = [] + @State private var unsortedTeamsWithoutWO: [TeamRegistration] = [] + @State private var unsortedPlayers: [PlayerRegistration] = [] + @State private var teamPaste: URL? + var messageSentFailed: Binding { Binding { sentError != nil @@ -83,20 +100,9 @@ struct InscriptionManagerView: View { } } - let slideToDeleteTip = SlideToDeleteTip() - let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() - let fileTip = InscriptionManagerFileInputTip() - let pasteTip = InscriptionManagerPasteInputTip() - let searchTip = InscriptionManagerSearchInputTip() - let createTip = InscriptionManagerCreateInputTip() - let rankUpdateTip = InscriptionManagerRankUpdateTip() - let padelBeachExportTip = PadelBeachExportTip() - let padelBeachImportTip = PadelBeachImportTip() - let categoryOption: PlayerFilterOption let filterable: Bool - let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed() - + init(tournament: Tournament) { self.tournament = tournament _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) @@ -110,6 +116,16 @@ struct InscriptionManagerView: View { } } + private func _clearScreen() { + teamPaste = nil + unsortedPlayers.removeAll() + unfilteredTeams.removeAll() + walkoutTeams.removeAll() + unsortedTeamsWithoutWO.removeAll() + sortedTeams.removeAll() + registrationIssues = nil + } + // Function to create a simple hash from a list of IDs private func _simpleHash(ids: [String]) -> Int { // Combine the hash values of each string @@ -121,7 +137,44 @@ struct InscriptionManagerView: View { return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) } - + private func _setHash() async { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + let selectedSortedTeams = tournament.selectedSortedTeams() + if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { + self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) + } + } + + private func _handleHashDiff() async { + let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id }) + if let teamsHash, newHash != teamsHash { + self.teamsHash = newHash + if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false { + self.tournament.shouldVerifyBracket = true + self.tournament.shouldVerifyGroupStage = true + + let waitingList = self.tournament.waitingListTeams(in: self.tournament.selectedSortedTeams()) + waitingList.forEach { team in + if team.bracketPosition != nil || team.groupStagePosition != nil { + team.resetPositions() + } + } + + do { + try dataStore.teamRegistrations.addOrUpdate(contentOfs: waitingList) + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } + } var body: some View { VStack(spacing: 0) { @@ -130,38 +183,18 @@ struct InscriptionManagerView: View { _buildingTeamView() } else if tournament.unsortedTeams().isEmpty { _inscriptionTipsView() - } else { + } + if _isEditingTeam() == false { _teamRegisteredView() } } .onAppear { - let selectedSortedTeams = tournament.selectedSortedTeams() - if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { - self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) - } + _getTeams() } .onDisappear { - let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id }) - if let teamsHash, newHash != teamsHash { - self.teamsHash = newHash - if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false { - self.tournament.shouldVerifyBracket = true - self.tournament.shouldVerifyGroupStage = true - - let waitingList = self.tournament.waitingListTeams(in: self.tournament.selectedSortedTeams()) - waitingList.forEach { team in - if team.bracketPosition != nil || team.groupStagePosition != nil { - team.resetPositions() - } - } - - do { - try dataStore.teamRegistrations.addOrUpdate(contentOfs: waitingList) - try dataStore.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) - } - } + Task { + await _handleHashDiff() + } } .alert("Un problème est survenu", isPresented: messageSentFailed) { @@ -258,10 +291,14 @@ struct InscriptionManagerView: View { .tint(.master) } .onChange(of: tournament.prioritizeClubMembers) { + _clearScreen() _save() + _getTeams() } .onChange(of: tournament.teamSorting) { + _clearScreen() _save() + _getTeams() } .onChange(of: currentRankSourceDate) { if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate { @@ -334,8 +371,10 @@ struct InscriptionManagerView: View { Label("Clôturer", systemImage: "lock") } Divider() - ShareLink(item: tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short))) { - Label("Exporter les paires", systemImage: "square.and.arrow.up") + if let teamPaste { + ShareLink(item: teamPaste) { + Label("Exporter les paires", systemImage: "square.and.arrow.up") + } } Button { presentImportView = true @@ -373,7 +412,30 @@ struct InscriptionManagerView: View { createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil } - private func _getTeams(from sortedTeams: [TeamRegistration]) -> [TeamRegistration] { + private func _prepareStats() async { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func _prepareStats", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + + unsortedPlayers = tournament.unsortedPlayers() + walkoutTeams = tournament.walkoutTeams() + unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO() + teamPaste = tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short)) + } + + private func _prepareTeams() { + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func _prepareTeams", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + + sortedTeams = tournament.sortedTeams() + var teams = sortedTeams if filterMode == .walkOut { teams = teams.filter({ $0.walkOut }) @@ -384,17 +446,34 @@ struct InscriptionManagerView: View { } if byDecreasingOrdering { - return teams.reversed() + self.unfilteredTeams = teams.reversed() } else { - return teams + self.unfilteredTeams = teams + } + } + + private func _getIssues() async { + #if DEBUG_TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func _getIssues", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + await registrationIssues = tournament.registrationIssues() + } + + private func _getTeams() { + _prepareTeams() + Task { + await _prepareStats() + await _getIssues() + await _setHash() } } private func _teamRegisteredView() -> some View { List { - let sortedTeams = tournament.sortedTeams() - let unfilteredTeams = _getTeams(from: sortedTeams) - if presentSearch == false { _rankHandlerView() _relatedTips() @@ -418,6 +497,7 @@ struct InscriptionManagerView: View { Task { await MainActor.run() { fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) + fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] pasteString = searchField } } @@ -474,6 +554,7 @@ struct InscriptionManagerView: View { Task { await MainActor.run { fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) + fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] pasteString = first autoSelect = true } @@ -504,6 +585,8 @@ struct InscriptionManagerView: View { @ViewBuilder func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View { Section { + let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed() + Picker(selection: $currentRankSourceDate) { if currentRankSourceDate == nil { Text("inconnu").tag(nil as Date?) @@ -571,6 +654,7 @@ struct InscriptionManagerView: View { Task { await MainActor.run { fetchPlayers.nsPredicate = _pastePredicate(pasteField: paste, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) + fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] pasteString = paste autoSelect = true } @@ -621,9 +705,6 @@ struct InscriptionManagerView: View { private func _informationView(count: Int) -> some View { Section { - let walkoutTeams = tournament.walkoutTeams() - let unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO() - LabeledContent { Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle) } label: { @@ -647,7 +728,11 @@ struct InscriptionManagerView: View { .environment(tournament) } label: { LabeledContent { - Text(tournament.registrationIssues().formatted()).font(.largeTitle) + if let registrationIssues { + Text(registrationIssues.formatted()).font(.largeTitle) + } else { + ProgressView() + } } label: { Text("Problèmes détéctés") if let closedRegistrationDate = tournament.closedRegistrationDate { @@ -660,43 +745,43 @@ struct InscriptionManagerView: View { @ViewBuilder private func _relatedTips() -> some View { - if pasteString == nil - && createdPlayerIds.isEmpty - && tournament.unsortedTeams().count >= tournament.teamCount - && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty { - Section { - TipView(padelBeachExportTip) { action in - if action.id == "more-info-export" { - isLearningMore = true - } - if action.id == "padel-beach" { - UIApplication.shared.open(URLs.beachPadel.url) - } - } - .tipStyle(tint: nil) - } - Section { - TipView(padelBeachImportTip) { action in - if action.id == "more-info-import" { - presentImportView = true - } - } - .tipStyle(tint: nil) - } - } - +// if pasteString == nil +// && createdPlayerIds.isEmpty +// && tournament.unsortedTeams().count >= tournament.teamCount +// && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty { +// Section { +// TipView(padelBeachExportTip) { action in +// if action.id == "more-info-export" { +// isLearningMore = true +// } +// if action.id == "padel-beach" { +// UIApplication.shared.open(URLs.beachPadel.url) +// } +// } +// .tipStyle(tint: nil) +// } +// Section { +// TipView(padelBeachImportTip) { action in +// if action.id == "more-info-import" { +// presentImportView = true +// } +// } +// .tipStyle(tint: nil) +// } +// } +// - if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false { + if tournament.tournamentCategory == .men && unsortedPlayers.filter({ $0.isMalePlayer() == false }).isEmpty == false { Section { TipView(inscriptionManagerWomanRankTip) .tipStyle(tint: nil) } } - - Section { - TipView(slideToDeleteTip) - .tipStyle(tint: nil) - } +// +// Section { +// TipView(slideToDeleteTip) +// .tipStyle(tint: nil) +// } } private func _searchSource() -> String? { @@ -777,6 +862,9 @@ struct InscriptionManagerView: View { createdPlayers.removeAll() createdPlayerIds.removeAll() pasteString = nil + + _clearScreen() + _getTeams() } private func _updateTeam() { @@ -797,6 +885,8 @@ struct InscriptionManagerView: View { createdPlayerIds.removeAll() pasteString = nil self.editedTeam = nil + _clearScreen() + _getTeams() } private func _buildingTeamView() -> some View { @@ -873,6 +963,7 @@ struct InscriptionManagerView: View { } } } + .headerProminence(.increased) .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here if let pasteString, count == 2, autoSelect == true { fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in @@ -999,14 +1090,19 @@ struct InscriptionManagerView: View { Toggle(isOn: .init(get: { return team.wildCardBracket }, set: { value in - team.resetPositions() - team.wildCardGroupStage = false - team.walkOut = false - team.wildCardBracket = value - do { - try dataStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) + _clearScreen() + + Task { + team.resetPositions() + team.wildCardGroupStage = false + team.walkOut = false + team.wildCardBracket = value + do { + try dataStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + _getTeams() } })) { Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle") @@ -1015,14 +1111,19 @@ struct InscriptionManagerView: View { Toggle(isOn: .init(get: { return team.wildCardGroupStage }, set: { value in - team.resetPositions() - team.wildCardBracket = false - team.walkOut = false - team.wildCardGroupStage = value - do { - try dataStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) + _clearScreen() + + Task { + team.resetPositions() + team.wildCardBracket = false + team.walkOut = false + team.wildCardGroupStage = value + do { + try dataStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + _getTeams() } })) { Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle") @@ -1032,24 +1133,32 @@ struct InscriptionManagerView: View { Toggle(isOn: .init(get: { return team.walkOut }, set: { value in - team.resetPositions() - team.wildCardBracket = false - team.wildCardGroupStage = false - team.walkOut = value - do { - try dataStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) + _clearScreen() + Task { + team.resetPositions() + team.wildCardBracket = false + team.wildCardGroupStage = false + team.walkOut = value + do { + try dataStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + _getTeams() } })) { Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle") } Divider() Button(role: .destructive) { - do { - try dataStore.teamRegistrations.delete(instance: team) - } catch { - Logger.error(error) + _clearScreen() + Task { + do { + try dataStore.teamRegistrations.delete(instance: team) + } catch { + Logger.error(error) + } + _getTeams() } } label: { LabelDelete() diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift index 2f669e7..ddb8701 100644 --- a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -13,6 +13,9 @@ struct PrintSettingsView: View { @StateObject var generator: HtmlGenerator @State private var presentShareView: Bool = false @State private var prepareGroupStage: Bool = false + @State private var generationId: UUID = UUID() + @State private var generationGroupStageId: UUID = UUID() + @State private var generating: Bool = false init(tournament: Tournament) { self.tournament = tournament @@ -63,26 +66,55 @@ struct PrintSettingsView: View { } header: { Text("Tableau principal") } + + if generating == false { + RowButtonView("Générer le PDF", systemImage: "printer") { + await MainActor.run() { + self.generating = true + } + generator.preparePDF { result in + switch result { + case .success(true): + if generator.includeGroupStage && generator.groupStageIsReady == false && tournament.groupStages().isEmpty == false { + self.prepareGroupStage = true + self.generationGroupStageId = UUID() + } else { + self.presentShareView = true + self.generating = false + } + case .success(false): + print("didn't save pdf") + break + case .failure(let error): + print(error) + break + } + } + self.prepareGroupStage = false + self.generationId = UUID() + } + .disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false) + } else { + LabeledContent { + ProgressView() + } label: { + Text("Préparation du PDF") + } + .id(generationId) + } } Section { NavigationLink { - WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in - }) + WebViewPreview(bracket: true) + .environmentObject(generator) } label: { Text("Aperçu du tableau") } ForEach(tournament.groupStages()) { groupStage in NavigationLink { - WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in - if let error { - print("preparePDF", error) - } else if loaded == false { - generator.generateGroupStage(webView: webView) - } else { - print("preparePDF", "is loading") - } - }) + WebViewPreview(groupStage: groupStage) + .environmentObject(generator) } label: { Text("Aperçu de la \(groupStage.groupStageTitle())") } @@ -90,16 +122,59 @@ struct PrintSettingsView: View { } } .background { - WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in - if let error { - print("preparePDF", error) - } else if loaded == false { - generator.generateWebView(webView: webView) - } else { - print("preparePDF", "is loading") - } - }).opacity(0) - + if generating { + _backgroundGenerationWebView() + _backgroundGroupStageWebView() + } + } + .navigationTitle("Imprimer") + .toolbarBackground(.visible, for: .navigationBar) + .navigationBarTitleDisplayMode(.inline) +// .toolbar { +// ToolbarItem(placement: .topBarTrailing) { +// Menu { +// Section { +// ShareLink(item: generator.generateHtml()) { +// Text("Tableau") +// } +// +// if let groupStage = tournament.groupStages().first { +// ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) { +// Text("Poule") +// } +// } +// } header: { +// Text("Partager le code source HTML") +// } +// } label: { +// Label("Options", systemImage: "ellipsis.circle") +// } +// } +// } + .sheet(isPresented: $presentShareView) { + if let pdfURL = generator.pdfURL { + ShareSheet(urls: [pdfURL]) + } + } + } + + @ViewBuilder + private func _backgroundGenerationWebView() -> some View { + WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in + if let error { + print("preparePDF", error) + } else if loaded == false { + generator.generateWebView(webView: webView) + } else { + print("preparePDF", "is loading") + } + }) + .opacity(0) + .id(generationId) + } + + private func _backgroundGroupStageWebView() -> some View { + Group { if prepareGroupStage { ForEach(tournament.groupStages()) { groupStage in WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in @@ -114,64 +189,7 @@ struct PrintSettingsView: View { } } } - .navigationTitle("Imprimer") - .toolbarBackground(.visible, for: .navigationBar) - .toolbarBackground(.visible, for: .bottomBar) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .bottomBar) { - Button { - generator.preparePDF { result in - switch result { - case .success(true): - if generator.includeGroupStage && generator.groupStageIsReady == false { - self.prepareGroupStage = true - } else { - self.presentShareView = true - } - case .success(false): - print("didn't save pdf") - break - case .failure(let error): - print(error) - break - } - } - - self.prepareGroupStage = false - self.generator.buildPDF() - - } label: { - Text("Obtenir le PDF") - } - .disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false) - .buttonStyle(.borderedProminent) - } - ToolbarItem(placement: .topBarTrailing) { - Menu { - Section { - ShareLink(item: generator.generateHtml()) { - Text("Tableau") - } - - if let groupStage = tournament.groupStages().first { - ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) { - Text("Poule") - } - } - } header: { - Text("Partager le code source HTML") - } - } label: { - Label("Options", systemImage: "ellipsis.circle") - } - } - } - .sheet(isPresented: $presentShareView) { - if let pdfURL = generator.pdfURL { - ShareSheet(urls: [pdfURL]) - } - } + .id(generationGroupStageId) } } @@ -239,3 +257,34 @@ struct WebView: UIViewRepresentable { } } + +struct WebViewPreview: View { + @EnvironmentObject var generator: HtmlGenerator + let bracket: Bool + let groupStage: GroupStage? + + @State private var html: String? + + init(bracket: Bool = false, groupStage: GroupStage? = nil) { + self.bracket = bracket + self.groupStage = groupStage + } + + var body: some View { + Group { + if let html { + WebView(htmlRawData: html, loadStatusChanged: { loaded, error, webView in + }) + } else { + ProgressView() + .onAppear { + if let groupStage { + html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false) + } else { + html = generator.generateHtml() + } + } + } + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index 73ae79c..a768f17 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -7,7 +7,13 @@ import SwiftUI -enum CallDestination: Identifiable, Selectable { +enum CallDestination: Identifiable, Selectable, Equatable { + + static func == (lhs: CallDestination, rhs: CallDestination) -> Bool { + return lhs.id == rhs.id + } + + case seeds(Tournament) case groupStages(Tournament) diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index 1c2fdbe..f48afd9 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -7,7 +7,12 @@ import SwiftUI -enum CashierDestination: Identifiable, Selectable { +enum CashierDestination: Identifiable, Selectable, Equatable { + + static func == (lhs: CashierDestination, rhs: CashierDestination) -> Bool { + return lhs.id == rhs.id + } + case summary case groupStage(GroupStage) case bracket(Round) @@ -37,6 +42,10 @@ enum CashierDestination: Identifiable, Selectable { } } + func displayImageIfValueZero() -> Bool { + return true + } + func badgeValue() -> Int? { switch self { case .summary: @@ -46,7 +55,7 @@ enum CashierDestination: Identifiable, Selectable { case .bracket(let round): return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count case .all(let tournament): - return tournament.selectedPlayers().filter({ $0.hasPaid() == false }).count + return nil } } @@ -58,12 +67,10 @@ enum CashierDestination: Identifiable, Selectable { switch self { case .summary: return nil - case .groupStage(let groupStage): - return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil - case .bracket(let round): - return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? .checkmark : nil - case .all(let tournament): - return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil + case .all: + return nil + default: + return .checkmark } } @@ -72,7 +79,8 @@ enum CashierDestination: Identifiable, Selectable { struct TournamentCashierView: View { var tournament: Tournament @State private var selectedDestination: CashierDestination? - + @StateObject private var cashierViewModel: CashierViewModel = CashierViewModel() + func allDestinations() -> [CashierDestination] { var allDestinations : [CashierDestination] = [] let tournamentHasEnded = tournament.hasEnded() @@ -127,10 +135,13 @@ struct TournamentCashierView: View { CashierDetailView(tournament: tournament) case .groupStage(let groupStage): CashierView(tournament: tournament, teams: groupStage.teams()) + .environmentObject(cashierViewModel) case .bracket(let round): CashierView(tournament: tournament, teams: round.seeds()) + .environmentObject(cashierViewModel) case .all(let tournament): CashierView(tournament: tournament, teams: tournament.selectedSortedTeams()) + .environmentObject(cashierViewModel) } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index f3c77c9..792c87f 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -20,7 +20,11 @@ extension Schedulable { } } -enum ScheduleDestination: String, Identifiable, Selectable { +enum ScheduleDestination: String, Identifiable, Selectable, Equatable { + static func == (lhs: ScheduleDestination, rhs: ScheduleDestination) -> Bool { + return lhs.id == rhs.id + } + var id: String { self.rawValue } case planning diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index d9f0121..70ba98a 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -7,7 +7,11 @@ import SwiftUI -enum TournamentSettings: Identifiable, Selectable { +enum TournamentSettings: Identifiable, Selectable, Equatable { + static func == (lhs: TournamentSettings, rhs: TournamentSettings) -> Bool { + return lhs.id == rhs.id + } + case status case general case club(Tournament) diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index 2c88f24..1860e54 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -9,13 +9,18 @@ import SwiftUI struct TournamentBuildView: View { var tournament: Tournament + @State private var bracketStatus: String? + @State private var groupStageStatus: String? + @State private var callStatus: Tournament.TournamentStatus? + @State private var scheduleStatus: Tournament.TournamentStatus? + @State private var cashierStatus: Tournament.TournamentStatus? @ViewBuilder var body: some View { if tournament.hasEnded() { Section { NavigationLink(value: Screen.rankings) { - Text("Classement") + Text("Classement final des équipes") } } } @@ -24,8 +29,12 @@ struct TournamentBuildView: View { if tournament.groupStageCount > 0 { NavigationLink(value: Screen.groupStage) { LabeledContent { - Text(tournament.groupStageStatus()) - .multilineTextAlignment(.trailing) + if let groupStageStatus { + Text(groupStageStatus).lineLimit(1) + .multilineTextAlignment(.trailing) + } else { + ProgressView() + } } label: { Text("Poules") if tournament.shouldVerifyGroupStage { @@ -33,13 +42,20 @@ struct TournamentBuildView: View { } } } + .task { + groupStageStatus = await tournament.groupStageStatus() + } } if tournament.rounds().isEmpty == false { NavigationLink(value: Screen.round) { LabeledContent { - Text(tournament.bracketStatus()) - .multilineTextAlignment(.trailing) + if let bracketStatus { + Text(bracketStatus).lineLimit(1) + .multilineTextAlignment(.trailing) + } else { + ProgressView() + } } label: { Text("Tableau") if tournament.shouldVerifyBracket { @@ -47,42 +63,78 @@ struct TournamentBuildView: View { } } } + .task { + bracketStatus = await tournament.bracketStatus() + } } } Section { if tournament.state() != .finished { NavigationLink(value: Screen.schedule) { - let tournamentStatus = tournament.scheduleStatus() + let tournamentStatus = scheduleStatus LabeledContent { - Text(tournamentStatus.completion) + if let tournamentStatus { + Text(tournamentStatus.completion) + } else { + ProgressView() + } } label: { Text("Horaires") - Text(tournamentStatus.label) + if let tournamentStatus { + Text(tournamentStatus.label).lineLimit(1) + } else { + Text(" ") + } } } + .task { + scheduleStatus = await tournament.scheduleStatus() + } NavigationLink(value: Screen.call) { - let tournamentStatus = tournament.callStatus() + let tournamentStatus = callStatus LabeledContent { - Text(tournamentStatus.completion) + if let tournamentStatus { + Text(tournamentStatus.completion) + } else { + ProgressView() + } } label: { Text("Convocations") - Text(tournamentStatus.label) + if let tournamentStatus { + Text(tournamentStatus.label).lineLimit(1) + } else { + Text(" ") + } } } + .task { + callStatus = await tournament.callStatus() + } } if tournament.state() == .running || tournament.state() == .finished { NavigationLink(value: Screen.cashier) { - let tournamentStatus = tournament.cashierStatus() + let tournamentStatus = cashierStatus LabeledContent { - Text(tournamentStatus.completion) + if let tournamentStatus { + Text(tournamentStatus.completion) + } else { + ProgressView() + } } label: { Text("Encaissement") - Text(tournamentStatus.label) + if let tournamentStatus { + Text(tournamentStatus.label).lineLimit(1) + } else { + Text(" ") + } } } + .task { + cashierStatus = await tournament.cashierStatus() + } } } } diff --git a/PadelClub/Views/Tournament/TournamentInitView.swift b/PadelClub/Views/Tournament/TournamentInitView.swift index c96ff3f..ba3c6ef 100644 --- a/PadelClub/Views/Tournament/TournamentInitView.swift +++ b/PadelClub/Views/Tournament/TournamentInitView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import LeStorage struct TournamentInitView: View { var tournament: Tournament @@ -43,13 +44,21 @@ struct TournamentInitView: View { NavigationLink(value: Screen.broadcast) { LabeledContent { - if tournament.isPrivate { - Text("tournoi privé").foregroundStyle(.logoRed) + if Store.main.userId == nil { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.logoRed) } else { - Text("Automatique") + if tournament.isPrivate { + Text("tournoi privé").foregroundStyle(.logoRed) + } else { + Text("Automatique") + } } } label: { Text("Publication") + if Store.main.userId == nil { + Text("Un compte Padel Club est nécessaire") + } } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 48c8fbf..6b93243 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -93,6 +93,7 @@ struct TournamentView: View { TournamentRankView() case .broadcast: BroadcastView() + .environment(navigation) case .event: if let event = tournament.eventObject() { EventView(event: event) diff --git a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift index 49a9285..ec9c829 100644 --- a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift +++ b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift @@ -14,12 +14,12 @@ struct ListRowViewModifier: ViewModifier { func body(content: Content) -> some View { if isActive { content -// .listRowBackground( -// color.variation() -// .overlay(alignment: .leading, content: { -// color.frame(width: 8) -// }) -// ) + .listRowBackground( + color.variation() + .overlay(alignment: .leading, content: { + color.frame(width: 8) + }) + ) } else { content } @@ -27,7 +27,7 @@ struct ListRowViewModifier: ViewModifier { } extension View { - func listRowView(isActive: Bool = true, color: Color) -> some View { + func listRowView(isActive: Bool = false, color: Color) -> some View { modifier(ListRowViewModifier(isActive: isActive, color: color)) } }