diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 6fd9425..6f73258 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -23,14 +23,16 @@ final class Round: ModelObject, Storable { private(set) var format: MatchFormat? var startDate: Date? var groupStageLoserBracket: Bool = false - - internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false) { + var loserBracketMode: LoserBracketMode = .automatic + + internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { self.tournament = tournament self.index = index self.parent = parent self.format = matchFormat self.startDate = startDate self.groupStageLoserBracket = groupStageLoserBracket + self.loserBracketMode = loserBracketMode } // MARK: - Computed dependencies @@ -214,7 +216,7 @@ defer { } #endif let parentRound = parentRound - if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false { + if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic { return nil } @@ -235,7 +237,7 @@ defer { #endif let parentRound = parentRound - if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false { + if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic { return nil } @@ -435,6 +437,10 @@ defer { return _matches().filter({ $0.disabled }) } + func allLoserRoundMatches() -> [Match] { + loserRoundsAndChildren().flatMap({ $0._matches() }) + } + var theoryCumulativeMatchCount: Int { var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index) if let parentRound { @@ -591,7 +597,11 @@ defer { func deleteLoserBracket() { do { - try self.tournamentStore.rounds.delete(contentOfs: loserRounds()) + let loserRounds = loserRounds() + for loserRound in loserRounds { + try loserRound.deleteDependencies() + } + try self.tournamentStore.rounds.delete(contentOfs: loserRounds) } catch { Logger.error(error) } @@ -696,6 +706,7 @@ defer { case _format = "format" case _startDate = "startDate" case _groupStageLoserBracket = "groupStageLoserBracket" + case _loserBracketMode = "loserBracketMode" } required init(from decoder: Decoder) throws { @@ -707,6 +718,7 @@ defer { format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false + loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic } func encode(to encoder: Encoder) throws { @@ -716,7 +728,8 @@ defer { try container.encode(tournament, forKey: ._tournament) try container.encode(index, forKey: ._index) try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket) - + try container.encode(loserBracketMode, forKey: ._loserBracketMode) + if let parent = parent { try container.encode(parent, forKey: ._parent) } else { @@ -791,3 +804,29 @@ extension Round: Selectable, Equatable { return hasEnded() ? .checkmark : nil } } + + +enum LoserBracketMode: Int, CaseIterable, Identifiable, Codable { + var id: Int { self.rawValue } + + case automatic + case manual + + func localizedLoserBracketMode() -> String { + switch self { + case .automatic: + "Automatique" + case .manual: + "Manuelle" + } + } + + func localizedLoserBracketModeDescription() -> String { + switch self { + case .automatic: + "Les perdants du tableau principal sont placés à leur place prévue." + case .manual: + "Aucun placement automatique n'est fait. Vous devez choisir les perdants qui se jouent." + } + } +} diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 3f478da..14437e0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -57,8 +57,8 @@ final class Tournament : ModelObject, Storable { var publishTournament: Bool = false var hidePointsEarned: Bool = false var publishRankings: Bool = false - var automaticLoserBracket: Bool = true - + var loserBracketMode: LoserBracketMode = .automatic + @ObservationIgnored var navigationPath: [Screen] = [] @@ -106,9 +106,10 @@ final class Tournament : ModelObject, Storable { case _publishTournament = "publishTournament" case _hidePointsEarned = "hidePointsEarned" case _publishRankings = "publishRankings" + case _loserBracketMode = "loserBracketMode" } - 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) { + 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, loserBracketMode: LoserBracketMode = .automatic) { self.event = event self.name = name self.startDate = startDate @@ -146,6 +147,7 @@ final class Tournament : ModelObject, Storable { self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned self.publishRankings = publishRankings + self.loserBracketMode = loserBracketMode } required init(from decoder: Decoder) throws { @@ -190,6 +192,7 @@ final class Tournament : ModelObject, Storable { 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 + loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic } fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() @@ -1634,7 +1637,7 @@ defer { let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount()) let rounds = (0.. Tournament { diff --git a/PadelClub/Data/User.swift b/PadelClub/Data/User.swift index d40e126..59e3e1f 100644 --- a/PadelClub/Data/User.swift +++ b/PadelClub/Data/User.swift @@ -43,16 +43,18 @@ class User: ModelObject, UserBase, Storable { var bracketMatchFormatPreference: MatchFormat? var groupStageMatchFormatPreference: MatchFormat? var loserBracketMatchFormatPreference: MatchFormat? - + var loserBracketMode: LoserBracketMode = .automatic + var deviceId: String? - init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?) { + init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) { self.username = username self.firstName = firstName self.lastName = lastName self.email = email self.phone = phone self.country = country + self.loserBracketMode = loserBracketMode } public func uuid() throws -> UUID { @@ -139,8 +141,42 @@ class User: ModelObject, UserBase, Storable { case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" case _deviceId = "deviceId" + case _loserBracketMode = "loserBracketMode" } + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Required properties + id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() + username = try container.decode(String.self, forKey: ._username) + email = try container.decode(String.self, forKey: ._email) + firstName = try container.decode(String.self, forKey: ._firstName) + lastName = try container.decode(String.self, forKey: ._lastName) + + // Optional properties + clubs = try container.decodeIfPresent([String].self, forKey: ._clubs) ?? [] + umpireCode = try container.decodeIfPresent(String.self, forKey: ._umpireCode) + licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId) + phone = try container.decodeIfPresent(String.self, forKey: ._phone) + country = try container.decodeIfPresent(String.self, forKey: ._country) + + // Summons-related properties + summonsMessageBody = try container.decodeIfPresent(String.self, forKey: ._summonsMessageBody) + summonsMessageSignature = try container.decodeIfPresent(String.self, forKey: ._summonsMessageSignature) + summonsAvailablePaymentMethods = try container.decodeIfPresent(String.self, forKey: ._summonsAvailablePaymentMethods) + summonsDisplayFormat = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayFormat) ?? false + summonsDisplayEntryFee = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayEntryFee) ?? false + summonsUseFullCustomMessage = try container.decodeIfPresent(Bool.self, forKey: ._summonsUseFullCustomMessage) ?? false + + // Match-related properties + matchFormatsDefaultDuration = try container.decodeIfPresent([MatchFormat: Int].self, forKey: ._matchFormatsDefaultDuration) + bracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._bracketMatchFormatPreference) + groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference) + loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) + loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic + } + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -228,6 +264,7 @@ class User: ModelObject, UserBase, Storable { try container.encodeNil(forKey: ._deviceId) } + try container.encode(loserBracketMode, forKey: ._loserBracketMode) } static func placeHolder() -> User { diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index 0740e0b..70d778e 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -121,6 +121,22 @@ struct UmpireView: View { } + Section { + @Bindable var user = dataStore.user + Picker(selection: $user.loserBracketMode) { + ForEach(LoserBracketMode.allCases) { + Text($0.localizedLoserBracketMode()).tag($0) + } + } label: { + Text("Position des perdants") + } + .onChange(of: user.loserBracketMode) { + dataStore.saveUser() + } + } header: { + Text("Matchs de classement") + } + Section { NavigationLink { GlobalSettingsView() diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index 2e24481..08952fe 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -22,6 +22,28 @@ struct LoserRoundSettingsView: View { } } + Section { + @Bindable var round: Round = upperBracketRound.round + Picker(selection: $round.loserBracketMode) { + ForEach(LoserBracketMode.allCases) { + Text($0.localizedLoserBracketMode()).tag($0) + } + } label: { + Text("Position des perdants") + } + .onChange(of: round.loserBracketMode) { + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round) + } catch { + Logger.error(error) + } + } + } header: { + Text("Matchs de classement") + } footer: { + Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription()) + } + Section { RowButtonView("Synchroniser les noms des matchs") { let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches @@ -48,6 +70,11 @@ struct LoserRoundSettingsView: View { upperBracketRound.round.disabledMatches().forEach { match in match.disableMatch() } + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) + } catch { + Logger.error(error) + } } } .disabled(upperBracketRound.round.loserRounds().isEmpty == false) diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index b5d9a2d..7ba50a3 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -172,7 +172,7 @@ struct SelectablePlayerListView: View { } .scrollDismissesKeyboard(.immediately) .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) - .toolbarBackground(.visible, for: .bottomBar) + //.toolbarBackground(.visible, for: .bottomBar) // .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) .navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet)) @@ -358,13 +358,109 @@ struct MySearchView: View { @ViewBuilder var playersView: some View { + let showProgression = true + let showFemaleInMaleAssimilation = searchViewModel.showFemaleInMaleAssimilation if searchViewModel.allowMultipleSelection { List(selection: $searchViewModel.selectedPlayers) { if searchViewModel.filterSelectionEnabled { let array = Array(searchViewModel.selectedPlayers) Section { ForEach(array) { player in - ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + let index : Int? = nil + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) + } + } + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } .onDelete { indexSet in for index in indexSet { @@ -379,7 +475,102 @@ struct MySearchView: View { } else { Section { ForEach(players, id: \.self) { player in - ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + let index : Int? = nil + + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) + } + } + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } } header: { if players.isEmpty == false { @@ -395,10 +586,105 @@ struct MySearchView: View { if searchViewModel.allowSingleSelection { Section { ForEach(players) { player in + let index : Int? = nil + Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) + } + } + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } .buttonStyle(.plain) } @@ -410,9 +696,104 @@ struct MySearchView: View { .id(UUID()) } else { Section { - ForEach(players.indices, id: \.self) { index in - let player = players[index] - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + ForEach(players.indices, id: \.self) { playerIndex in + let player = players[playerIndex] + let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil + + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) + } + } + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } } header: { if players.isEmpty == false { @@ -423,19 +804,205 @@ struct MySearchView: View { } } else { Section { - ForEach(players.indices, id: \.self) { index in - let player = players[index] + ForEach(players.indices, id: \.self) { playerIndex in + let player = players[playerIndex] + let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil if searchViewModel.allowSingleSelection { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) - .contentShape(Rectangle()) + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) + } + } + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } .frame(maxWidth: .infinity) .buttonStyle(.plain) } else { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + VStack(alignment: .leading) { + HStack { + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } + if index == nil { + Text(player.male ? "♂︎" : "♀︎") + } + Spacer() + if let index { + HStack(alignment: .top, spacing: 0) { + Text(index.formatted()) + .foregroundStyle(.secondary) + .font(.title3) + Text(index.ordinalFormattedSuffix()) + .foregroundStyle(.secondary) + .font(.caption) + } + } + } + .font(.title3) + .lineLimit(1) + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } + } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + } + } + } + .lineLimit(1) + + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) + } + } + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") + } + } + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) + } + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) + } + } } } } header: { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 01575a4..32b0f1a 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -72,11 +72,11 @@ struct AddTeamView: View { } var body: some View { - if pasteString == nil { + if pasteString != nil, fetchPlayers.isEmpty == false { computedBody + .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher dans les résultats")) } else { computedBody - .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .automatic), prompt: Text("Chercher dans les résultats")) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 11416ea..35afb65 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -35,19 +35,42 @@ struct TournamentGeneralSettingsView: View { } Section { - Toggle(isOn: $tournament.automaticLoserBracket) { - Text("Gestion automatique des matchs de classements") + Picker(selection: $tournament.loserBracketMode) { + ForEach(LoserBracketMode.allCases) { + Text($0.localizedLoserBracketMode()).tag($0) + } + } label: { + Text("Position des perdants") + } + .onChange(of: tournament.loserBracketMode) { + + _save() + + let rounds = tournament.rounds() + rounds.forEach { round in + round.loserBracketMode = tournament.loserBracketMode + } + + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) + } catch { + Logger.error(error) + } + } + } header: { + Text("Matchs de classement") + } footer: { + if dataStore.user.loserBracketMode != tournament.loserBracketMode { + _footerView() + .onTapGesture(perform: { + self.dataStore.user.loserBracketMode = tournament.loserBracketMode + self.dataStore.saveUser() + }) + } else { + Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) } -// Picker(selection: $tournament.loserBracketMode) { -// ForEach(LoserBracketMode.allCases) { mode in -// Text(mode.loserBracketModeLocalizedLabel()).tag(mode) -// } -// } label: { -// Text("Mode") -// } } - Section { LabeledContent { TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) @@ -132,4 +155,10 @@ struct TournamentGeneralSettingsView: View { Logger.error(error) } } + + private func _footerView() -> some View { + Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) + + + Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue) + } }