diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5b67a94..8cf08d1 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -77,6 +77,7 @@ FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; + FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -407,6 +408,7 @@ FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = ""; }; FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; + FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserGroupStageSettingsView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -1146,6 +1148,7 @@ FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */, FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */, + FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */, FF9AC3932BE3625D00C2E883 /* Components */, FF9AC3922BE3625200C2E883 /* Shared */, ); @@ -1532,6 +1535,7 @@ FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */, FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */, + FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */, FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */, FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */, FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */, @@ -1886,13 +1890,14 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 76; + CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; ENABLE_MODULE_VERIFIER = YES; ENABLE_PREVIEWS = YES; + GCC_OPTIMIZATION_LEVEL = fast; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -1910,10 +1915,12 @@ ); MARKETING_VERSION = 0.1; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - OTHER_SWIFT_FLAGS = ""; + OTHER_SWIFT_FLAGS = "-Onone"; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1926,7 +1933,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 76; + CURRENT_PROJECT_VERSION = 78; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -1952,6 +1959,7 @@ OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50"; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/PadelClub/Data/AppSettings.swift b/PadelClub/Data/AppSettings.swift index 89a5763..6f08933 100644 --- a/PadelClub/Data/AppSettings.swift +++ b/PadelClub/Data/AppSettings.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -class AppSettings: MicroStorable { +final class AppSettings: MicroStorable { var lastDataSource: String? = nil var didCreateAccount: Bool = false diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index fd52b4c..3c78021 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -class Club : ModelObject, Storable, Hashable { +final class Club : ModelObject, Storable, Hashable { static func resourceName() -> String { return "clubs" } static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] } diff --git a/PadelClub/Data/Court.swift b/PadelClub/Data/Court.swift index c786f38..22f29a0 100644 --- a/PadelClub/Data/Court.swift +++ b/PadelClub/Data/Court.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -class Court : ModelObject, Storable, Hashable { +final class Court : ModelObject, Storable, Hashable { static func resourceName() -> String { return "courts" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } diff --git a/PadelClub/Data/DateInterval.swift b/PadelClub/Data/DateInterval.swift index 69978f2..23438bd 100644 --- a/PadelClub/Data/DateInterval.swift +++ b/PadelClub/Data/DateInterval.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -class DateInterval: ModelObject, Storable { +final class DateInterval: ModelObject, Storable { static func resourceName() -> String { return "date-intervals" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } diff --git a/PadelClub/Data/Event.swift b/PadelClub/Data/Event.swift index 0c60f8b..85baf07 100644 --- a/PadelClub/Data/Event.swift +++ b/PadelClub/Data/Event.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -class Event: ModelObject, Storable { +final class Event: ModelObject, Storable { static func resourceName() -> String { return "events" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 5594cfb..26d44a0 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -11,7 +11,7 @@ import Algorithms import SwiftUI @Observable -class GroupStage: ModelObject, Storable { +final class GroupStage: ModelObject, Storable { static func resourceName() -> String { "group-stages" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index a8af519..738d2d6 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -class Match: ModelObject, Storable { +final class Match: ModelObject, Storable { static func resourceName() -> String { "matches" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } @@ -220,25 +220,10 @@ defer { func teamWillBeWalkOut(_ team: TeamRegistration) { resetMatch() - let previousScores = teamScores.filter({ $0.luckyLoser != nil }) + let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) + existingTeamScore.walkOut = 1 do { - try self.tournamentStore.teamScores.delete(contentOfs: previousScores) - } catch { - Logger.error(error) - } - - if let existingTeamScore = teamScore(ofTeam: team) { - do { - try self.tournamentStore.teamScores.delete(instance: existingTeamScore) - } catch { - Logger.error(error) - } - } - - let teamScoreWalkout = TeamScore(match: id, team: team) - teamScoreWalkout.walkOut = 1 - do { - try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreWalkout) + try self.tournamentStore.teamScores.addOrUpdate(instance: existingTeamScore) } catch { Logger.error(error) } @@ -254,24 +239,18 @@ defer { func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) { resetMatch() - let previousScores = teamScores.filter({ $0.luckyLoser != nil }) + + let matchIndex = index + let position = matchIndex * 2 + teamPosition.rawValue + + let previousScores = teamScores.filter({ $0.luckyLoser == position }) do { try self.tournamentStore.teamScores.delete(contentOfs: previousScores) } catch { Logger.error(error) } - - if let existingTeamScore = teamScore(ofTeam: team) { - do { - try self.tournamentStore.teamScores.delete(instance: existingTeamScore) - } catch { - Logger.error(error) - } - } - - let matchIndex = index - let position = matchIndex * 2 + teamPosition.rawValue - let teamScoreLuckyLoser = TeamScore(match: id, team: team) + + let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) teamScoreLuckyLoser.luckyLoser = position do { try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 02ab172..f655cd5 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -class MatchScheduler : ModelObject, Storable { +final class MatchScheduler : ModelObject, Storable { static func resourceName() -> String { return "match-scheduler" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } diff --git a/PadelClub/Data/MonthData.swift b/PadelClub/Data/MonthData.swift index 9db06f5..5c79558 100644 --- a/PadelClub/Data/MonthData.swift +++ b/PadelClub/Data/MonthData.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -class MonthData : ModelObject, Storable { +final class MonthData : ModelObject, Storable { static func resourceName() -> String { return "month-data" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 4c6d28c..6865a77 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -class PlayerRegistration: ModelObject, Storable { +final class PlayerRegistration: ModelObject, Storable { static func resourceName() -> String { "player-registrations" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } @@ -75,13 +75,21 @@ class PlayerRegistration: ModelObject, Storable { self.source = .frenchFederation } - internal init(federalData: [String], sex: Int, sexUnknown: Bool) { - lastName = federalData[0].trimmed.uppercased() - firstName = federalData[1].trimmed.capitalized + internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { + let _lastName = federalData[0].trimmed.uppercased() + let _firstName = federalData[1].trimmed.capitalized + if _lastName.isEmpty && _firstName.isEmpty { return nil } + lastName = _lastName + firstName = _firstName birthdate = federalData[2] licenceId = federalData[3] clubName = federalData[4] - rank = Int(federalData[5]) + let stringRank = federalData[5] + if stringRank.isEmpty { + rank = nil + } else { + rank = Int(stringRank) + } let _email = federalData[6] if _email.isEmpty == false { self.email = _email diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 58eb8f1..6d402c9 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -class Round: ModelObject, Storable { +final class Round: ModelObject, Storable { static func resourceName() -> String { "rounds" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } @@ -582,6 +582,29 @@ defer { loserRounds().forEach { round in round.buildLoserBracket() } + + /* + return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat) + } + + do { + try DataStore.shared.matches.addOrUpdate(contentOfs: matches) + } catch { + Logger.error(error) + } + + matches.forEach { + $0.name = $0.roundObject?.roundTitle() + } + + do { + try DataStore.shared.matches.addOrUpdate(contentOfs: matches) + } catch { + Logger.error(error) + } + + + */ } var parentRound: Round? { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 84fbde1..3d81383 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -class TeamRegistration: ModelObject, Storable { +final class TeamRegistration: ModelObject, Storable { static func resourceName() -> String { "team-registrations" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } @@ -207,8 +207,12 @@ class TeamRegistration: ModelObject, Storable { } func includes(players: [PlayerRegistration]) -> Bool { + let unsortedPlayers = unsortedPlayers() + guard players.count == unsortedPlayers.count else { return false } return players.allSatisfy { player in - includes(player: player) + unsortedPlayers.anySatisfy { _player in + _player.isSameAs(player) + } } } diff --git a/PadelClub/Data/TeamScore.swift b/PadelClub/Data/TeamScore.swift index 09caa48..8948d2d 100644 --- a/PadelClub/Data/TeamScore.swift +++ b/PadelClub/Data/TeamScore.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -class TeamScore: ModelObject, Storable { +final class TeamScore: ModelObject, Storable { static func resourceName() -> String { "team-scores" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 20b1825..f1f6dd5 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -class Tournament : ModelObject, Storable { +final class Tournament : ModelObject, Storable { static func resourceName() -> String { "tournaments" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } @@ -55,7 +55,8 @@ class Tournament : ModelObject, Storable { var hideTeamsWeight: Bool = false var publishTournament: Bool = false var hidePointsEarned: Bool = false - + var publishRankings: Bool = false + @ObservationIgnored var navigationPath: [Screen] = [] @@ -102,9 +103,10 @@ class Tournament : ModelObject, Storable { case _hideTeamsWeight = "hideTeamsWeight" case _publishTournament = "publishTournament" case _hidePointsEarned = "hidePointsEarned" + case _publishRankings = "publishRankings" } - internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false) { + internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false) { self.event = event self.name = name self.startDate = startDate @@ -141,6 +143,7 @@ class Tournament : ModelObject, Storable { self.hideTeamsWeight = hideTeamsWeight self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned + self.publishRankings = publishRankings } required init(from decoder: Decoder) throws { @@ -184,6 +187,7 @@ class Tournament : ModelObject, Storable { hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false + publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false } fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() @@ -298,6 +302,7 @@ class Tournament : ModelObject, Storable { try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight) try container.encode(publishTournament, forKey: ._publishTournament) try container.encode(hidePointsEarned, forKey: ._hidePointsEarned) + try container.encode(publishRankings, forKey: ._publishRankings) } fileprivate func _encodePayment(container: inout KeyedEncodingContainer) throws { @@ -487,8 +492,7 @@ class Tournament : ModelObject, Storable { } func courtUsed() -> [Int] { - -#if DEBUG_TIME //DEBUGING TIME +#if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) @@ -1104,6 +1108,17 @@ defer { return selected.sorted(by: \.finalRanking!, order: .ascending) } + private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) { + for key in dictionary.keys { + if var stringArray = dictionary[key] { + // Remove all instances of each string in stringsToRemove + stringArray.removeAll { stringsToRemove.contains($0) } + dictionary[key] = stringArray + } + } + } + + func finalRanking() async -> [Int: [String]] { var teams: [Int: [String]] = [:] var ids: Set = Set() @@ -1119,6 +1134,14 @@ defer { } let others: [Round] = rounds.flatMap { round in + let losers = round.losers() + let minimumFinalPosition = round.seedInterval()?.last ?? teamCount + if teams[minimumFinalPosition] == nil { + teams[minimumFinalPosition] = losers.map { $0.id } + } else { + teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id }) + } + print("round", round.roundTitle()) let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } print(rounds.count, rounds.map { $0.roundTitle() }) @@ -1137,28 +1160,40 @@ defer { print("losers", losers.count) if winners.isEmpty { let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) - teams[interval.computedLast] = disabledIds - let teamNames : [String] = disabledIds.compactMap { - let t : TeamRegistration? = Store.main.findById($0) - return t - }.map { $0.canonicalName } - print("winners.isEmpty", "\(interval.computedLast) : ", teamNames) - disabledIds.forEach { ids.insert($0) } + if disabledIds.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: disabledIds) + teams[interval.computedLast] = disabledIds + let teamNames : [String] = disabledIds.compactMap { + let t : TeamRegistration? = Store.main.findById($0) + return t + }.map { $0.canonicalName } + print("winners.isEmpty", "\(interval.computedLast) : ", teamNames) + disabledIds.forEach { + ids.insert($0) + } + } } else { - teams[interval.computedFirst + winners.count - 1] = winners - let teamNames : [String] = winners.compactMap { - let t: TeamRegistration? = Store.main.findById($0) - return t - }.map { $0.canonicalName } - print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames) - winners.forEach { ids.insert($0) } - teams[interval.computedLast] = losers - let loserTeamNames : [String] = losers.compactMap { - let t: TeamRegistration? = Store.main.findById($0) - return t - }.map { $0.canonicalName } - print("losers", "\(interval.computedLast) : ", loserTeamNames) - losers.forEach { ids.insert($0) } + if winners.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: winners) + teams[interval.computedFirst + winners.count - 1] = winners + let teamNames : [String] = winners.compactMap { + let t: TeamRegistration? = Store.main.findById($0) + return t + }.map { $0.canonicalName } + print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames) + winners.forEach { ids.insert($0) } + } + + if losers.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: losers) + teams[interval.computedLast] = losers + let loserTeamNames : [String] = losers.compactMap { + let t: TeamRegistration? = Store.main.findById($0) + return t + }.map { $0.canonicalName } + print("losers", "\(interval.computedLast) : ", loserTeamNames) + losers.forEach { ids.insert($0) } + } } } } @@ -1882,7 +1917,7 @@ defer { let selected = selectedSortedTeams() let allTeams = unsortedTeams() let seedCount = max(selected.count - groupStageSpots(), 0) - let newGroup = selected.prefix(seedCount) + let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified }) let currentGroup = allTeams.filter({ $0.bracketPosition != nil }) let selectedIds = newGroup.map { $0.id } let groupIds = currentGroup.map { $0.id } diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index b27ee9c..9686e04 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -268,13 +268,17 @@ class FileImportManager { case .mix: return 1 } } - if tournamentCategory == tournament.tournamentCategory { + if tournamentCategory == tournament.tournamentCategory { let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) - playerOne.setComputedRank(in: tournament) + playerOne?.setComputedRank(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) - playerTwo.setComputedRank(in: tournament) - let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament) - results.append(team) + playerTwo?.setComputedRank(in: tournament) + + let players = [playerOne, playerTwo].compactMap({ $0 }) + if players.isEmpty == false { + let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam(players), tournament: tournament) + results.append(team) + } } } } @@ -320,12 +324,15 @@ class FileImportManager { } let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) - playerOne.setComputedRank(in: tournament) + playerOne?.setComputedRank(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) - playerTwo.setComputedRank(in: tournament) + playerTwo?.setComputedRank(in: tournament) - let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament) - results.append(team) + let players = [playerOne, playerTwo].compactMap({ $0 }) + if players.isEmpty == false { + let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam(players), tournament: tournament) + results.append(team) + } } } } diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift index 29682f1..af72534 100644 --- a/PadelClub/Utils/HtmlGenerator.swift +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -31,10 +31,19 @@ class HtmlGenerator: ObservableObject { @Published var height: CGFloat = 0 private var webView: WKWebView = WKWebView() private var groupStageDone: Int = 0 + @Published var landscape: Bool = false + var baseWidth: CGFloat { + landscape ? 842 : 595 + } + + var baseHeight: CGFloat { + landscape ? 595 : 842 + } + var estimatedPageCount: Int { if let zoomLevel { - let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) + let pageSize = CGSize(width: baseWidth * (1 + zoomLevel), height: baseHeight * (1 + zoomLevel)) let numberOfPageInWidth = Int(width / pageSize.width) + 1 let numberOfPageInHeight = Int(height / pageSize.height) + 1 return numberOfPageInWidth * numberOfPageInHeight @@ -50,17 +59,19 @@ class HtmlGenerator: ObservableObject { func generateWebView(webView: WKWebView) { self.webView = webView self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in + print("evaluateJavaScript", "readystage", complete, error) if complete != nil { self.webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in + print("evaluateJavaScript", "height", height, error) self.height = height as! CGFloat + self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in + print("evaluateJavaScript", "width", width, error) + self.width = width as! CGFloat + if self.completionHandler != nil { + self.buildPDF() + } + }) }) - self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in - self.width = width as! CGFloat - }) - } - - if self.completionHandler != nil { - self.buildPDF() } }) } @@ -75,7 +86,7 @@ class HtmlGenerator: ObservableObject { print("bracket", width, height) let config = WKPDFConfiguration() - config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(width))) + config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(height))) webView.createPDF(configuration: config){ result in switch result{ case .success(let data): @@ -111,7 +122,7 @@ class HtmlGenerator: ObservableObject { try? FileManager.default.removeItem(at: pdfURL!) print("buildPDF", width, height, zoomLevel ?? 0) if let zoomLevel { - let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) + let pageSize = CGSize(width: baseWidth * (1 + zoomLevel), height: baseHeight * (1 + zoomLevel)) let numberOfPageInWidth = Int(width / pageSize.width) + 1 let numberOfPageInHeight = Int(height / pageSize.height) + 1 for w in 0.. String { + //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() + HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withScore: false) + } + var pdfURL: URL? { guard let pdfFolderURL = getFilePath() else { return nil diff --git a/PadelClub/Utils/HtmlService.swift b/PadelClub/Utils/HtmlService.swift index c083d34..33145b5 100644 --- a/PadelClub/Utils/HtmlService.swift +++ b/PadelClub/Utils/HtmlService.swift @@ -10,7 +10,8 @@ import Foundation enum HtmlService { case template(tournament: Tournament) - case bracket(tournament: Tournament, roundIndex: Int) + case bracket(round: Round) + case loserBracket(upperRound: Round) case match(match: Match) case player(entrant: TeamRegistration) case hiddenPlayer @@ -26,7 +27,7 @@ enum HtmlService { var fileName: String { switch self { - case .template: + case .template, .loserBracket: return "tournament-template" case .bracket: return "bracket-template" @@ -191,23 +192,42 @@ enum HtmlService { } template = template.replacingOccurrences(of: "{{matchDescription}}", with: "") return template - case .bracket(let tournament, let roundIndex): + case .bracket(let round): var template = "" var bracket = "" - if let round = tournament.rounds().first(where: { $0.index == roundIndex }) { - 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) - bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle()) + 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) + bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle()) return bracket + case .loserBracket(let upperRound): + var template = html + template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle()) + var brackets = "" + for round in upperRound.loserRounds() { + brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore)) + } + var winnerName = "" + let winner = """ +
    +
  •  
  • +
  • \(winnerName)
  • +
  •  
  • +
+ + """ + brackets = brackets.appending(winner) + + template = template.replacingOccurrences(of: "{{brackets}}", with: brackets) + return template case .template(let tournament): var template = html template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short)) var brackets = "" for round in tournament.rounds() { - brackets = brackets.appending(HtmlService.bracket(tournament: tournament, roundIndex: round.index).html(headName: headName, withRank: withRank, withScore: withScore)) + brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore)) } var winnerName = "" diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index 5f00aee..dd36e02 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -14,6 +14,7 @@ enum URLs: String, Identifiable { case api = "https://xlr.alwaysdata.net/roads/" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" //case padelClub = "https://padelclub.app" + case tenup = "https://tenup.fft.fr" case padelRules = "https://fft-site.cdn.prismic.io/fft-site/ZgLn3McYqOFdyF7n_LEGUIDEDELACOMPETITIONDEPADEL-MAJDECEMBRE2023.pdf" var id: String { return self.rawValue } diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index 91adb31..525e14e 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -37,9 +37,15 @@ struct SeedInterval: Hashable, Comparable { func chunks() -> [SeedInterval]? { if dimension > 3 { let split = dimension / 2 - let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce) - let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce) - return [firstHalf, secondHalf] + if split%2 == 0 { + let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce) + let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce) + return [firstHalf, secondHalf] + } else { + let firstHalf = SeedInterval(first: first, last: first + split, reduce: reduce) + let secondHalf = SeedInterval(first: first + split + 1, last: last, reduce: reduce) + return [firstHalf, secondHalf] + } } else { return nil } diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index 03d333b..0b95b62 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -85,7 +85,7 @@ struct GroupStageCallingView: View { } } .overlay { - if groupStage.startDate == nil { + if groupStage.startDate == nil && groupStage._matches().filter({ $0.startDate != nil }).isEmpty { ContentUnavailableView { Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") } description: { diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index 75f16c3..01ebe1f 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -27,7 +27,8 @@ struct ClubSearchView: View { @State private var searchPresented: Bool = false @State private var showingSettingsAlert = false @State private var newClub: Club? - + @State private var error: Error? + var presentClubCreationView: Binding { Binding( get: { newClub != nil }, set: { isPresented in @@ -59,7 +60,7 @@ struct ClubSearchView: View { searching = false searchAttempted = true } - + error = nil clubMarkers = [] guard let city = locationManager.city else { return } let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location) @@ -70,6 +71,8 @@ struct ClubSearchView: View { } } catch { print("getclubs", error) + self.error = error + Logger.error(error) } } @@ -143,12 +146,20 @@ struct ClubSearchView: View { } else if clubMarkers.isEmpty && searching == false && searchPresented == false { ContentUnavailableView { if searchAttempted { - Label("Aucun club trouvé", systemImage: "mappin.slash") + if error != nil { + Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill") + } else { + Label("Aucun club trouvé", systemImage: "mappin.slash") + } } else { Label("Recherche de club", systemImage: "location.circle") } } description: { - Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") + if searchAttempted && error != nil { + Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.") + } else { + Text("Padel Club recherche via Tenup un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") + } } actions: { if locationManager.manager.authorizationStatus != .restricted { RowButtonView("Chercher autour de moi") { @@ -161,6 +172,13 @@ struct ClubSearchView: View { } } } + + if error != nil { + Link(destination: URLs.tenup.url) { + Text("Voir si tenup est en maintenance") + } + } + RowButtonView("Chercher une ville ou un code postal") { searchPresented = true } @@ -343,6 +361,7 @@ struct ClubSearchView: View { private func _resetSearch() { searchAttempted = false + error = nil debouncableViewModel.debouncableText = "" searchedCity = "" locationManager.city = nil diff --git a/PadelClub/Views/GroupStage/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/GroupStageSettingsView.swift index e175129..96382c6 100644 --- a/PadelClub/Views/GroupStage/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStageSettingsView.swift @@ -82,6 +82,12 @@ struct GroupStageSettingsView: View { } #endif +// NavigationLink { +// LoserGroupStageSettingsView(tournament: tournament) +// } label: { +// Text("Match de perdant de poules") +// } + if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false { Section { menuBuildAllGroupStages diff --git a/PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift b/PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift new file mode 100644 index 0000000..78ccdd0 --- /dev/null +++ b/PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift @@ -0,0 +1,85 @@ +// +// LoserGroupStageSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 29/06/2024. +// + +import SwiftUI + +extension Round { + var isGroupStageLoserBracket: Bool { + return false + } +} + +extension Tournament { + func groupStageLoserBrackets() -> [Round] { + [] + } + + func removeGroupStageLoserBrackets() { + + } +} + +struct LoserGroupStageSettingsView: View { + var tournament: Tournament + @State private var loserGroupStageBracketType: Int? = nil + @State private var losers : Set = Set() + @Environment(\.editMode) private var editMode + + var body: some View { + List(selection: $losers) { + if tournament.groupStageLoserBrackets().isEmpty == false { + //for each all rounds without parent and loserGroupStage, ability to delete them + Section { + RowButtonView("Effacer", role: .destructive) { + tournament.removeGroupStageLoserBrackets() + } + } + } + + if self.editMode?.wrappedValue == .active { + Section { + //rajouter + toolbar valider / cancel + ForEach(tournament.groupStageTeams().filter({ $0.qualified == false })) { team in + TeamRowView(team: team).tag(team) + } + } header: { + Text("Sélection des perdants de poules") + } + } else { + Section { + RowButtonView("Ajouter un match de perdant") { + self.editMode?.wrappedValue = .active + } + } footer: { + Text("Permet d'ajouter un match de perdant de poules.") + } + } + } + .toolbar { + if self.editMode?.wrappedValue == .active { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler") { + self.editMode?.wrappedValue = .inactive + } + } + + ToolbarItem(placement: .topBarTrailing) { + Button("Valider") { + self.editMode?.wrappedValue = .inactive + //tournament.createGroupStageLoserBracket() + } + } + } + } + .navigationTitle("Match de perdant de poules") + .navigationBarBackButtonHidden(self.editMode?.wrappedValue == .active) + .navigationBarTitleDisplayMode(.inline) + .toolbar(.visible, for: .navigationBar) + .headerProminence(.increased) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 6b83bd1..66a9bdf 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -80,14 +80,14 @@ struct MatchSetupView: View { let luckyLosers = walkOutSpot ? match.luckyLosers() : [] TeamPickerView(groupStagePosition: nil, luckyLosers: luckyLosers, teamPicked: { team in print(team.pasteData()) - if walkOutSpot { + if walkOutSpot || team.bracketPosition != nil { match.setLuckyLoser(team: team, teamPosition: teamPosition) do { try tournamentStore.matches.addOrUpdate(instance: match) } catch { Logger.error(error) } - } else { + } else if team.bracketPosition == nil { team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false) do { try tournamentStore.matches.addOrUpdate(instance: match) diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 024c4b6..3983879 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -102,7 +102,7 @@ struct ActivityView: View { .overlay { if let error, navigation.agendaDestination == .tenup { ContentUnavailableView { - Label("Erreur", systemImage: "exclamationmark") + Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill") } description: { Text(error.localizedDescription) } actions: { diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index eeaf717..c7f7001 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -87,7 +87,7 @@ struct MainView: View { } .environmentObject(dataStore) .task { - await self._checkSourceFileAvailability() + //await self._checkSourceFileAvailability() if StoreCenter.main.hasToken() { do { try await dataStore.clubs.loadDataFromServerIfAllowed() diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 10733b2..37680cb 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -128,7 +128,7 @@ struct CourtAvailabilitySettingsView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Créneau indisponible") - .popover(isPresented: $showingPopover) { + .sheet(isPresented: $showingPopover) { NavigationStack { Form { Section { diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index ac0ae9d..19fc8bc 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -6,9 +6,9 @@ // import SwiftUI +import LeStorage struct LoserRoundView: View { - @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @@ -22,7 +22,7 @@ struct LoserRoundView: View { print("func _roundDisabled", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return loserBracket.allMatches.allSatisfy({ $0.disabled == false }) + return loserBracket.allMatches.allSatisfy({ $0.disabled == true }) } private func _matches(loserRoundId: String?) -> [Match] { @@ -87,6 +87,16 @@ struct LoserRoundView: View { ToolbarItem(placement: .topBarTrailing) { Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { isEditingTournamentSeed.wrappedValue.toggle() + + if isEditingTournamentSeed.wrappedValue == false { + let allRoundMatches = loserBracket.allMatches + allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + } catch { + Logger.error(error) + } + } } } } diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 948d2ea..453dff3 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -177,13 +177,12 @@ struct LoserRoundsView: View { var body: some View { VStack(spacing: 0) { - GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: false) - if let selectedRound { + GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: true) + switch selectedRound { + case .some(let selectedRound): LoserRoundView(loserBracket: selectedRound) - } else { - Section { - ContentUnavailableView("Aucun tour à jouer", systemImage: "tennisball", description: Text("Il il n'y a aucun tour de match de classement prévu.")) - } + default: + LoserRoundSettingsView() } } .environment(\.isEditingTournamentSeed, $isEditingTournamentSeed) diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index 8b7b578..445a1b6 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -11,12 +11,20 @@ struct TeamPickerView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament @Environment(\.dismiss) private var dismiss + @State private var confirmTeam: TeamRegistration? @State private var presentTeamPickerView: Bool = false @State private var searchField: String = "" var groupStagePosition: Int? = nil var luckyLosers: [TeamRegistration] = [] let teamPicked: ((TeamRegistration) -> (Void)) - + + var confirmationRequest: Binding { + Binding { + confirmTeam != nil + } set: { _ in + } + } + var body: some View { Button { presentTeamPickerView = true @@ -86,12 +94,30 @@ struct TeamPickerView: View { Button { teamPicked(team) presentTeamPickerView = false +// if team.inRound() { +// confirmTeam = team +// } else { +// teamPicked(team) +// presentTeamPickerView = false +// } } label: { TeamRowView(team: team) .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) +// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) { +// Button("Retirer du tableau", role: .destructive) { +// teamPicked(confirmTeam!) +// presentTeamPickerView = false +// } +// +// Button("Annuler", role: .cancel) { +// confirmTeam = nil +// } +// } message: { +// Text("Vous êtes sur le point de retirer cette équipe du tableau pour le replacer, cela effacera les résultats des matchs déjà joués par cette équipe dans le tableau.") +// } } } } diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index aaf6d49..8555e71 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -106,9 +106,9 @@ struct InscriptionInfoView: View { } .listRowView(color: .red) } header: { - Text("Équipes non sélectionnées") + Text("Équipes ne devant plus être sélectionnées") } footer: { - Text("Il s'agit des équipes déjà placé en poule ou tableau qui sont actuellement en attente à cause de l'arrivée d'une nouvelle équipe ou une modification de classement.") + Text("Il s'agit des équipes précédement placées en poule ou tableau mais qui sont finalement maintenant en attente suite à l'arrivée d'une nouvelle équipe plus forte ou une modification de classement.") } Section { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 8fe1a3c..cb017e6 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -530,7 +530,7 @@ struct InscriptionManagerView: View { RowButtonView("Créer une équipe") { Task { - await MainActor.run() { + await MainActor.run { fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] pasteString = searchField diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift index ddb8701..a8cc7d3 100644 --- a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -28,7 +28,6 @@ struct PrintSettingsView: View { // Toggle(isOn: $generator.displayHeads, label: { // Text("Afficher les têtes de séries") // }) - Toggle(isOn: $generator.displayRank, label: { Text("Afficher le classement du joueur") }) @@ -57,6 +56,17 @@ struct PrintSettingsView: View { } label: { Text("Zoom") } + .onChange(of: generator.zoomLevel) { + if generator.zoomLevel == nil { + generator.landscape = false + } + } + + if generator.zoomLevel != nil { + Toggle(isOn: $generator.landscape, label: { + Text("Format paysage") + }) + } HStack { Text("Nombre de page A4 à imprimer") @@ -106,11 +116,23 @@ struct PrintSettingsView: View { Section { NavigationLink { - WebViewPreview(bracket: true) + WebViewPreview() .environmentObject(generator) } label: { Text("Aperçu du tableau") } +// +// ForEach(tournament.rounds()) { round in +// if round.index > 0 { +// NavigationLink { +// WebViewPreview(round: round) +// .environmentObject(generator) +// } label: { +// Text("Aperçu \(round.correspondingLoserRoundTitle())") +// } +// } +// } +// ForEach(tournament.groupStages()) { groupStage in NavigationLink { WebViewPreview(groupStage: groupStage) @@ -260,13 +282,13 @@ struct WebView: UIViewRepresentable { struct WebViewPreview: View { @EnvironmentObject var generator: HtmlGenerator - let bracket: Bool let groupStage: GroupStage? - + let round: Round? + @State private var html: String? - init(bracket: Bool = false, groupStage: GroupStage? = nil) { - self.bracket = bracket + init(groupStage: GroupStage? = nil, round: Round? = nil) { + self.round = round self.groupStage = groupStage } @@ -280,6 +302,8 @@ struct WebViewPreview: View { .onAppear { if let groupStage { html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false) + } else if let round { + html = generator.generateLoserBracketHtml(upperRound: round) } else { html = generator.generateHtml() } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 5231dfc..b9ed6b2 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -11,7 +11,8 @@ import LeStorage struct TournamentRankView: View { @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore - + @Environment(\.editMode) private var editMode + @State private var rankings: [Int: [TeamRegistration]] = [:] @State private var calculating = false @State private var selectedTeam: TeamRegistration? @@ -33,97 +34,71 @@ struct TournamentRankView: View { var body: some View { List { @Bindable var tournament = tournament - let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil }) - Section { - LabeledContent { - if let matchesLeft { - Text(matchesLeft.count.formatted()) - } else { - ProgressView() + let rankingsCalculated = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil }) + if editMode?.wrappedValue.isEditing == false { + Section { + MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false) + MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false) + + Toggle(isOn: $tournament.hidePointsEarned) { + Text("Masquer les points gagnés") } - } label: { - Text("Matchs restant") - } - - LabeledContent { - if let runningMatches { - Text(runningMatches.count.formatted()) - } else { - ProgressView() + .onChange(of: tournament.hidePointsEarned) { + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } } - } label: { - Text("Matchs en cours") - } - - LabeledContent { - if rankingPublished { - Image(systemName: "checkmark") - .foregroundStyle(.green) - } else { - Image(systemName: "xmark") - .foregroundStyle(.logoRed) + + Toggle(isOn: $tournament.publishRankings) { + Text("Publier sur Padel Club") + if let url = tournament.shareURL(.rankings) { + Link(destination: url) { + Text("Accéder à la page") + } + } } - } label: { - Text("Classement publié") - } - - Toggle(isOn: $tournament.hidePointsEarned) { - Text("Masquer les points gagnés") - } - .onChange(of: tournament.hidePointsEarned) { - do { - try dataStore.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) + .onChange(of: tournament.publishRankings) { + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } } } + } - if rankingPublished == false { - RowButtonView("Publier le classement", role: .destructive) { - _publishRankings() + if (editMode?.wrappedValue.isEditing == true || rankingsCalculated == false) && calculating == false { + Section { + RowButtonView(rankingsCalculated ? "Re-calculer le classement" : "Calculer", role: .destructive) { + await _calculateRankings() } - } else { - RowButtonView("Re-publier le classement", role: .destructive) { - _publishRankings() + } footer: { + if rankingsCalculated { + Text("Vos éditions seront perdus.") } } - } - - if rankingPublished { - Section { - RowButtonView("Supprimer le classement", role: .destructive) { - tournament.unsortedTeams().forEach { team in - team.finalRanking = nil - team.pointsEarned = nil + + if rankingsCalculated { + Section { + RowButtonView("Supprimer le classement", role: .destructive) { + tournament.unsortedTeams().forEach { team in + team.finalRanking = nil + team.pointsEarned = nil + } + _save() } - _save() } - } footer: { - Text(.init("Masque également le classement sur le site [Padel Club](\(URLs.main.rawValue))")) } } + - if rankingPublished { + let teamsRanked = tournament.teamsRanked() + if calculating == false && rankingsCalculated && teamsRanked.isEmpty == false { Section { - ForEach(tournament.teamsRanked()) { team in - let key = team.finalRanking ?? 0 - Button { - selectedTeam = team - } label: { - TeamRankCellView(team: team, key: key) - .frame(maxWidth: .infinity) - } - .contentShape(Rectangle()) - .buttonStyle(.plain) - } - } footer: { - Text("Vous pouvez appuyer sur une ligne pour éditer manuellement le classement calculé par Padel Club.") - } - } else { - let keys = rankings.keys.sorted() - ForEach(keys, id: \.self) { key in - if let rankedTeams = rankings[key] { - ForEach(rankedTeams) { team in + ForEach(teamsRanked) { team in + if let key = team.finalRanking { TeamRankCellView(team: team, key: key) } } @@ -165,12 +140,10 @@ struct TournamentRankView: View { } }) .onAppear { - let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil }) + let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil }) if rankingPublished == false { - calculating = true Task { await _calculateRankings() - calculating = false } } } @@ -179,103 +152,156 @@ struct TournamentRankView: View { .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .topBarTrailing) { - if let url = tournament.shareURL(.rankings) { - _actionForURL(url) - } + EditButton() } } } struct TeamRankCellView: View { + @Environment(\.editMode) private var editMode @Environment(Tournament.self) var tournament: Tournament - let team: TeamRegistration - let key: Int + @State private var isEditingTeam: Bool = false + @Bindable var team: TeamRegistration + @State var key: Int var body: some View { - HStack { - VStack(alignment: .trailing) { - VStack(alignment: .trailing, spacing: -8.0) { - ZStack(alignment: .trailing) { - Text(tournament.teamCount.formatted()).hidden() - Text(key.formatted()) - } - .monospacedDigit() - .font(.largeTitle) - .fontWeight(.bold) - Text(key.ordinalFormattedSuffix()).font(.caption) - } - if let index = tournament.indexOf(team: team) { - let rankingDifference = index - (key - 1) - if rankingDifference > 0 { - HStack(spacing: 0.0) { - Text(rankingDifference.formatted(.number.sign(strategy: .always()))) - .monospacedDigit() - Image(systemName: "arrowtriangle.up.fill") - .imageScale(.small) - } - .foregroundColor(.green) - } else if rankingDifference < 0 { - HStack(spacing: 0.0) { - Text(rankingDifference.formatted(.number.sign(strategy: .always()))) - .monospacedDigit() - Image(systemName: "arrowtriangle.down.fill") - .imageScale(.small) + VStack(spacing: 0) { + if editMode?.wrappedValue.isEditing == true { + if key > 1 { + Button { + key -= 1 + team.finalRanking = key + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) } - .foregroundColor(.red) - } else { - Text("--") + } label: { + Label("descendre", systemImage: "chevron.compact.up").labelStyle(.iconOnly) } + .buttonStyle(.bordered) } } - - Divider() - - VStack(alignment: .leading) { - if let name = team.name { - Text(name).foregroundStyle(.secondary) - } - - ForEach(team.players()) { player in - VStack(alignment: .leading, spacing: -4.0) { - Text(player.playerLabel()).bold() - HStack(alignment: .firstTextBaseline, spacing: 0.0) { - Text(player.rankLabel()) - if let rank = player.getRank() { - Text(rank.ordinalFormattedSuffix()) - .font(.caption) + Button { + isEditingTeam = true + } label: { + HStack { + VStack(alignment: .trailing) { + VStack(alignment: .trailing, spacing: -8.0) { + ZStack(alignment: .trailing) { + Text(tournament.teamCount.formatted()).hidden() + Text(key.formatted()) + } + .monospacedDigit() + .font(.largeTitle) + .fontWeight(.bold) + Text(key.ordinalFormattedSuffix()).font(.caption) + } + if let index = tournament.indexOf(team: team) { + let rankingDifference = index - (key - 1) + if rankingDifference > 0 { + HStack(spacing: 0.0) { + Text(rankingDifference.formatted(.number.sign(strategy: .always()))) + .monospacedDigit() + Image(systemName: "arrowtriangle.up.fill") + .imageScale(.small) + } + .foregroundColor(.green) + } else if rankingDifference < 0 { + HStack(spacing: 0.0) { + Text(rankingDifference.formatted(.number.sign(strategy: .always()))) + .monospacedDigit() + Image(systemName: "arrowtriangle.down.fill") + .imageScale(.small) + } + .foregroundColor(.red) + } else { + Text("--") + } + } + } + + + Divider() + + VStack(alignment: .leading) { + if let name = team.name { + Text(name).foregroundStyle(.secondary) + } + + ForEach(team.players()) { player in + VStack(alignment: .leading, spacing: -4.0) { + Text(player.playerLabel()).bold() + HStack(alignment: .firstTextBaseline, spacing: 0.0) { + Text(player.rankLabel()) + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()) + .font(.caption) + } + } + } + } + } + if tournament.isAnimation() == false && key > 0 { + Spacer() + VStack(alignment: .trailing) { + HStack(alignment: .lastTextBaseline, spacing: 0.0) { + Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always()))) + Text("pts").font(.caption) } } } } + .frame(maxWidth: .infinity) } - - if tournament.isAnimation() == false && key > 0 { - Spacer() - VStack(alignment: .trailing) { - HStack(alignment: .lastTextBaseline, spacing: 0.0) { - Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always()))) - Text("pts").font(.caption) + .contentShape(Rectangle()) + .buttonStyle(.plain) + + if editMode?.wrappedValue.isEditing == true { + Button { + key += 1 + team.finalRanking = key + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) } + } label: { + Label("descendre", systemImage: "chevron.compact.down").labelStyle(.iconOnly) } + .buttonStyle(.bordered) } } - } - } - - private func _publishRankings() { - rankings.keys.sorted().forEach { rank in - if let rankedTeams = rankings[rank] { - rankedTeams.forEach { team in - team.finalRanking = rank - team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount) + .alert("Position", isPresented: $isEditingTeam) { + TextField("Position", value: $team.finalRanking, format: .number) + .keyboardType(.numberPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + + Button("Valider") { + team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount) + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + + isEditingTeam = false + } + + Button("Annuler", role: .cancel) { + isEditingTeam = false } } } - _save() } - + private func _calculateRankings() async { + await MainActor.run { + calculating = true + } + let finalRanks = await tournament.finalRanking() finalRanks.keys.sorted().forEach { rank in if let rankedTeamIds = finalRanks[rank] { @@ -283,27 +309,21 @@ struct TournamentRankView: View { self.rankings[rank] = teams } } - } - - @ViewBuilder - private func _actionForURL(_ url: URL, removeSource: Bool = false) -> some View { - Menu { - Button { - UIApplication.shared.open(url) - } label: { - Label("Voir", systemImage: "safari") + + await MainActor.run { + rankings.keys.sorted().forEach { rank in + if let rankedTeams = rankings[rank] { + rankedTeams.forEach { team in + team.finalRanking = rank + team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount) + } + } } + _save() - ShareLink(item: url) { - Label("Partager le lien", systemImage: "link") - } - } label: { - Image(systemName: "square.and.arrow.up") + calculating = false } - .frame(maxWidth: .infinity) - .buttonStyle(.borderless) } - private func _save() { do { diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index b443928..1353b44 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -89,12 +89,17 @@ struct TournamentBuildView: View { Section { + #if DEBUG + NavigationLink(value: Screen.rankings) { + Text("Classement final des équipes") + } + #else if tournament.hasEnded() { NavigationLink(value: Screen.rankings) { Text("Classement final des équipes") } } - + #endif if state == .running || state == .finished { TournamentInscriptionView(tournament: tournament) TournamentBroadcastRowView(tournament: tournament) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 4223a4f..bef203d 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -78,6 +78,7 @@ struct TournamentView: View { } } } + .environment(tournament) .id(tournament.id) .toolbarBackground(.visible, for: .navigationBar) .navigationDestination(for: Screen.self, destination: { screen in diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index c69c5f9..71591b9 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -99,7 +99,7 @@ final class ServerDataTests: XCTestCase { return } - let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true) + let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true) let t = try await Store.main.service().post(tournament) assert(t.event == tournament.event) @@ -138,6 +138,7 @@ final class ServerDataTests: XCTestCase { assert(t.hideTeamsWeight == tournament.hideTeamsWeight) assert(t.publishTournament == tournament.publishTournament) assert(t.hidePointsEarned == tournament.hidePointsEarned) + assert(t.publishRankings == tournament.publishRankings) } func testGroupStage() async throws {