From 8a5db5921a7a20cb2a963fdea5c350d76c61adbf Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 11 Feb 2025 18:22:32 +0100 Subject: [PATCH 01/11] v1.1.9 --- PadelClub.xcodeproj/project.pbxproj | 4 +- .../Federal/FederalTournamentHolder.swift | 6 +- PadelClub/Data/TeamRegistration.swift | 5 +- PadelClub/Data/Tournament.swift | 28 ++++++--- .../HTML Templates/bracket-template.html | 5 +- .../HTML Templates/groupstage-template.html | 1 + PadelClub/HTML Templates/player-template.html | 1 + .../HTML Templates/tournament-template.html | 7 ++- PadelClub/Utils/HtmlGenerator.swift | 7 ++- PadelClub/Utils/HtmlService.swift | 62 ++++++++++++------- .../Views/Calling/CallSettingsView.swift | 2 + .../Views/Calling/GroupStageCallingView.swift | 19 +++++- .../Views/Calling/SeedsCallingView.swift | 18 ++++++ .../Views/Calling/TeamsCallingView.swift | 18 ++++++ .../Navigation/Agenda/EventListView.swift | 40 ++++++++++++ .../TournamentGeneralSettingsView.swift | 3 +- .../Tournament/Screen/PrintSettingsView.swift | 60 +++++++++++------- 17 files changed, 218 insertions(+), 68 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index dd34dcc..e9ed9ba 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3329,7 +3329,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.8; + MARKETING_VERSION = 1.1.9; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3376,7 +3376,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.8; + MARKETING_VERSION = 1.1.9; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Federal/FederalTournamentHolder.swift b/PadelClub/Data/Federal/FederalTournamentHolder.swift index 4ee6bd6..1e5de69 100644 --- a/PadelClub/Data/Federal/FederalTournamentHolder.swift +++ b/PadelClub/Data/Federal/FederalTournamentHolder.swift @@ -19,10 +19,14 @@ protocol FederalTournamentHolder { var dayPeriod: DayPeriod { get } func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool + func togglePrivate(isPrivate: Bool) } extension FederalTournamentHolder { - + func togglePrivate(isPrivate: Bool) { + + } + func durationLabel() -> String { switch dayDuration { case 1: diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 457b6e7..cc7dd6d 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -271,8 +271,9 @@ final class TeamRegistration: ModelObject, Storable { return teams.firstIndex(where: { $0.id == id }) } - func formattedSeed(in teams: [TeamRegistration]) -> String { - if let index = index(in: teams) { + func formattedSeed(in teams: [TeamRegistration]? = nil) -> String { + let selectedSortedTeams = teams ?? tournamentObject()?.selectedSortedTeams() ?? [] + if let index = index(in: selectedSortedTeams) { return "#\(index + 1)" } else { return "###" diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 1869399..531e749 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -132,7 +132,7 @@ final class Tournament : ModelObject, Storable { } - 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, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) { + internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, 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, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) { self.event = event self.name = name self.startDate = startDate @@ -141,11 +141,7 @@ final class Tournament : ModelObject, Storable { #if DEBUG self.isPrivate = false #else - if Guard.main.currentPlan == .monthlyUnlimited { - self.isPrivate = true - } else { - self.isPrivate = Guard.main.purchasedTransactions.isEmpty - } + self.isPrivate = isPrivate #endif self.groupStageFormat = groupStageFormat self.roundFormat = roundFormat @@ -1218,7 +1214,7 @@ defer { // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - static let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)] + static let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.computedOrder)] static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME @@ -2684,7 +2680,10 @@ extension Tournament: Hashable { } extension Tournament: FederalTournamentHolder { - + func togglePrivate(isPrivate: Bool) { + self.isPrivate = isPrivate + } + func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { if isAnimation() { if let name { @@ -2779,12 +2778,21 @@ extension Tournament { } let rankSourceDate = _mostRecentDateAvailable - let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false } + let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false }.sorted(by: \.endDate!, order: .descending) + + var shouldBePrivate = tournaments.first?.isPrivate ?? true + + if Guard.main.currentPlan == .monthlyUnlimited { + shouldBePrivate = false + } else if Guard.main.purchasedTransactions.isEmpty == false { + shouldBePrivate = false + } + let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) //creator: DataStore.shared.user?.id - return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode) + return Tournament(isPrivate: shouldBePrivate, groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode) } static func fake() -> Tournament { diff --git a/PadelClub/HTML Templates/bracket-template.html b/PadelClub/HTML Templates/bracket-template.html index c344123..8c3ec91 100644 --- a/PadelClub/HTML Templates/bracket-template.html +++ b/PadelClub/HTML Templates/bracket-template.html @@ -1,4 +1,7 @@
    -
  •  {{roundLabel}}
  • +
  • +  {{roundLabel}} +
    {{formatLabel}}
    +
  • {{match-template}}
diff --git a/PadelClub/HTML Templates/groupstage-template.html b/PadelClub/HTML Templates/groupstage-template.html index 6e203b9..32de623 100644 --- a/PadelClub/HTML Templates/groupstage-template.html +++ b/PadelClub/HTML Templates/groupstage-template.html @@ -82,6 +82,7 @@ body{

{{bracketTitle}}

{{bracketStartDate}}

+

{{formatLabel}}

diff --git a/PadelClub/HTML Templates/player-template.html b/PadelClub/HTML Templates/player-template.html index 38579d9..56c92dc 100644 --- a/PadelClub/HTML Templates/player-template.html +++ b/PadelClub/HTML Templates/player-template.html @@ -1,3 +1,4 @@ +
{{teamIndex}}
{{playerOne}}{{weightOne}}
{{playerTwo}}{{weightTwo}}
diff --git a/PadelClub/HTML Templates/tournament-template.html b/PadelClub/HTML Templates/tournament-template.html index 1d6f1b9..e98ebf0 100644 --- a/PadelClub/HTML Templates/tournament-template.html +++ b/PadelClub/HTML Templates/tournament-template.html @@ -9,6 +9,7 @@ flex-direction:row; padding: 1%; } + .round{ display:flex; flex-direction:column; @@ -27,7 +28,7 @@ .round .spacer{ flex-grow:1; font-size:24px; text-align: center; - color: #bbb; + color: #000000; font-style:italic; } .round .spacer:first-child, @@ -65,7 +66,7 @@ li.game-spacer{ border-right:2px solid #4f7a38; - min-height:156px; + min-height:{{minHeight}}px; text-align: right; display : flex; justify-content: center; @@ -95,7 +96,7 @@ -

{{tournamentTitle}}

+

{{tournamentTitle}} - {{tournamentStartDate}}

{{brackets}}
diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift index 4139f67..09d5ab1 100644 --- a/PadelClub/Utils/HtmlGenerator.swift +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -24,6 +24,9 @@ class HtmlGenerator: ObservableObject { @Published var displayHeads: Bool = false @Published var groupStageIsReady: Bool = false @Published var displayRank: Bool = false + @Published var displayTeamIndex: Bool = false + @Published var displayScore: Bool = false + private var pdfDocument: PDFDocument = PDFDocument() private var rects: [CGRect] = [] private var completionHandler: ((Result) -> ())? @@ -167,12 +170,12 @@ class HtmlGenerator: ObservableObject { func generateHtml() -> String { //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() - HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false) + HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore) } func generateLoserBracketHtml(upperRound: Round) -> String { //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() - HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withScore: false) + HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore) } var pdfURL: URL? { diff --git a/PadelClub/Utils/HtmlService.swift b/PadelClub/Utils/HtmlService.swift index 33145b5..3ebd892 100644 --- a/PadelClub/Utils/HtmlService.swift +++ b/PadelClub/Utils/HtmlService.swift @@ -50,7 +50,7 @@ enum HtmlService { } } - func html(headName: Bool, withRank: Bool, withScore: Bool) -> String { + func html(headName: Bool, withRank: Bool, withTeamIndex: Bool, withScore: Bool) -> String { guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else { fatalError() } @@ -69,12 +69,12 @@ enum HtmlService { } template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle(.short)) template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle()) - + template = template.replacingOccurrences(of: "{{formatLabel}}", with: bracket.matchFormat.formatTitle()) var col = "" var row = "" bracket.teams().forEach { entrant in - col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withScore: withScore)) - row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withScore: withScore)) + col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) + row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } template = template.replacingOccurrences(of: "{{teamsCol}}", with: col) template = template.replacingOccurrences(of: "{{teamsRow}}", with: row) @@ -82,6 +82,12 @@ enum HtmlService { return template case .groupstageEntrant(let entrant): var template = html + if withTeamIndex == false { + template = template.replacingOccurrences(of: #"
{{teamIndex}}
"#, with: "") + } else { + template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.seedIndex() ?? "") + } + if let playerOne = entrant.players()[safe: 0] { template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) if withRank { @@ -108,7 +114,7 @@ enum HtmlService { return template case .groupstageRow(let entrant, let teamsPerBracket): var template = html - template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) var scores = "" (0..{{teamIndex}}"#, with: "") + } else { + template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.formattedSeed()) + } + + if let playerOne = entrant.players()[safe: 0] { template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) if withRank { @@ -164,18 +177,22 @@ enum HtmlService { } return template case .hiddenPlayer: - return html + html + var template = html + html + if withTeamIndex { + template += html + } + return template case .match(let match): var template = html if let entrantOne = match.team(.one) { - template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } else { - template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } if let entrantTwo = match.team(.two) { - template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } else { - template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } if match.disabled { template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden") @@ -196,19 +213,20 @@ enum HtmlService { var template = "" var bracket = "" for (_, match) in round._matches().enumerated() { - template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle()) + bracket = bracket.replacingOccurrences(of: "{{formatLabel}}", with: round.matchFormat.formatTitle()) 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)) + brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } - var winnerName = "" + let winnerName = "" let winner = """
  •  
  • @@ -224,15 +242,17 @@ enum HtmlService { return template case .template(let tournament): var template = html - template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short)) + template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156") + template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.title)) + template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: tournament.formattedDate()) var brackets = "" for round in tournament.rounds() { - brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore)) + brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } var winnerName = "" if let tournamentWinner = tournament.tournamentWinner() { - winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withScore: withScore) + winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore) } let winner = """
      diff --git a/PadelClub/Views/Calling/CallSettingsView.swift b/PadelClub/Views/Calling/CallSettingsView.swift index 7834e0e..54fbcb6 100644 --- a/PadelClub/Views/Calling/CallSettingsView.swift +++ b/PadelClub/Views/Calling/CallSettingsView.swift @@ -22,6 +22,8 @@ struct CallSettingsView: View { var body: some View { List { + + Section { NavigationLink { CallMessageCustomizationView(tournament: tournament) diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index cb12cd4..020d603 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -6,15 +6,32 @@ // import SwiftUI +import LeStorage struct GroupStageCallingView: View { @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore @State private var displayByTeam: Bool = false var body: some View { let groupStages = tournament.groupStages() List { - + if tournament.isPrivate { + Section { + RowButtonView("Rendre visible sur Padel Club") { + tournament.isPrivate = false + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.") + .foregroundStyle(.logoRed) + } + } + let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }) if uncalled.isEmpty == false { NavigationLink { diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index c1b899e..d13f4b3 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -6,13 +6,31 @@ // import SwiftUI +import LeStorage struct SeedsCallingView: View { + @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament @State private var displayByMatch: Bool = true var body: some View { List { + if tournament.isPrivate { + Section { + RowButtonView("Rendre visible sur Padel Club") { + tournament.isPrivate = false + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.") + .foregroundStyle(.logoRed) + } + } + let tournamentRounds = tournament.rounds() let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil }) diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 8a5d1e9..b2473a9 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -10,6 +10,7 @@ import LeStorage struct TeamsCallingView: View { @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore let teams : [TeamRegistration] @State private var hideConfirmed: Bool = false @@ -31,6 +32,23 @@ struct TeamsCallingView: View { var body: some View { List { + if tournament.isPrivate { + Section { + RowButtonView("Rendre visible sur Padel Club") { + tournament.isPrivate = false + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.") + .foregroundStyle(.logoRed) + } + } + + PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false } diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index cd20e93..f9a7e2d 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -36,6 +36,10 @@ struct EventListView: View { let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments) Text("\(count.formatted()) tournoi" + count.pluralSuffix) } + } footer: { + if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] { + _menuOptions(pcTournaments) + } } .headerProminence(.increased) } @@ -54,6 +58,10 @@ struct EventListView: View { let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments) Text("\(count.formatted()) tournoi" + count.pluralSuffix) } + } footer: { + if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] { + _menuOptions(pcTournaments) + } } .id(sectionIndex) .headerProminence(.increased) @@ -82,6 +90,38 @@ struct EventListView: View { } } + private func _menuOptions(_ pcTournaments: [Tournament]) -> some View { + Menu { + Button { + pcTournaments.forEach { tournament in + tournament.togglePrivate(isPrivate: false) + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Les afficher tous sur Padel Club") + } + Button { + pcTournaments.forEach { tournament in + tournament.togglePrivate(isPrivate: true) + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Les masquer tous sur Padel Club") + } + + } label: { + Text("Options") + } + } + private func _nextMonths() -> [Date] { let currentDate = Date().startOfMonth let uniqueDates = tournaments.map { $0.startDate.startOfMonth }.uniqued().sorted() diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 803dbc5..f2c65e8 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -93,8 +93,9 @@ struct TournamentGeneralSettingsView: View { } .frame(maxHeight: 200) .overlay { - if tournamentInformation.isEmpty { + if tournamentInformation.isEmpty, focusedField != ._information { Text("Texte visible dans l'onglet informations sur Padel Club.").italic() + .foregroundStyle(.secondary) } } } header: { diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift index a8cc7d3..f9206bf 100644 --- a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -28,10 +28,20 @@ struct PrintSettingsView: View { // Toggle(isOn: $generator.displayHeads, label: { // Text("Afficher les têtes de séries") // }) + + Toggle(isOn: $generator.displayTeamIndex, label: { + Text("Afficher le poids et le rang de l'équipe") + }) + Toggle(isOn: $generator.displayRank, label: { Text("Afficher le classement du joueur") }) - + + Toggle(isOn: $generator.displayScore, label: { + Text("Afficher le score") + Text("Affiche le score des matchs terminés") + }) + Toggle(isOn: $generator.includeBracket, label: { Text("Tableau") }) @@ -152,32 +162,34 @@ struct PrintSettingsView: View { .navigationTitle("Imprimer") .toolbarBackground(.visible, for: .navigationBar) .navigationBarTitleDisplayMode(.inline) -// .toolbar { -// ToolbarItem(placement: .topBarTrailing) { -// Menu { -// Section { -// ShareLink(item: generator.generateHtml()) { -// Text("Tableau") -// } -// -// if let groupStage = tournament.groupStages().first { -// ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) { -// Text("Poule") -// } -// } -// } header: { -// Text("Partager le code source HTML") -// } -// } label: { -// Label("Options", systemImage: "ellipsis.circle") -// } -// } -// } .sheet(isPresented: $presentShareView) { if let pdfURL = generator.pdfURL { ShareSheet(urls: [pdfURL]) } } + #if DEBUG + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Section { + ShareLink(item: generator.generateHtml()) { + Text("Tableau") + } + + if let groupStage = tournament.groupStages().first { + ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)) { + Text("Poule") + } + } + } header: { + Text("Partager le code source HTML") + } + } label: { + Label("Options", systemImage: "ellipsis.circle") + } + } + } + #endif } @ViewBuilder @@ -199,7 +211,7 @@ struct PrintSettingsView: View { Group { if prepareGroupStage { ForEach(tournament.groupStages()) { groupStage in - WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in + WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore), loadStatusChanged: { loaded, error, webView in if let error { print("preparePDF", error) } else if loaded == false { @@ -301,7 +313,7 @@ struct WebViewPreview: View { ProgressView() .onAppear { if let groupStage { - html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false) + html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore) } else if let round { html = generator.generateLoserBracketHtml(upperRound: round) } else { From 4e5dc3ea12846f96170595d117e44458ebf4c3bc Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 12 Feb 2025 14:20:59 +0100 Subject: [PATCH 02/11] fix search stuff --- PadelClub/ViewModel/SearchViewModel.swift | 146 +++++++++++------- .../Navigation/Agenda/EventListView.swift | 8 +- PadelClub/Views/Round/RoundView.swift | 2 +- .../Shared/SelectablePlayerListView.swift | 48 +++++- .../Screen/TournamentRankView.swift | 2 +- .../Tournament/TournamentBuildView.swift | 2 +- .../Views/Tournament/TournamentView.swift | 2 +- 7 files changed, 143 insertions(+), 67 deletions(-) diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index cc16ead..5167d05 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -89,8 +89,31 @@ class SearchViewModel: ObservableObject, Identifiable { return nil } + func shouldIncludeSearchTextPredicate() -> Bool { + if allowMultipleSelection { + return true + } + + if allowSingleSelection { + return true + } + + if tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted { + return true + } + + return dataSet == .national && searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) + } + func showIndex() -> Bool { - if (dataSet == .national || dataSet == .ligue) { return isFiltering() } + if dataSet == .national { + if searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) { + return false + } else { + return isFiltering() + } + } + if (dataSet == .ligue) { return isFiltering() } if filterOption == .all { return isFiltering() } return true } @@ -149,66 +172,85 @@ class SearchViewModel: ObservableObject, Identifiable { } } + func searchTextPredicate() -> NSPredicate? { + var predicates : [NSPredicate] = [] + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) + let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed + + if canonicalVersionWithoutPunctuation.isEmpty == false { + let wordsPredicates = wordsPredicates() + if let wordsPredicates { + predicates.append(wordsPredicates) + } else { + predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) + } + predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation)) + let components = canonicalVersionWithoutPunctuation.split(separator: " ") + let pattern = components.joined(separator: ".*") + let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) + predicates.append(predicate) + } + if predicates.isEmpty { + return nil + } + return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) + } + func orPredicate() -> NSPredicate? { var predicates : [NSPredicate] = [] let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed - switch tokens.first { - case .none: - if canonicalVersionWithoutPunctuation.isEmpty == false { - let wordsPredicates = wordsPredicates() - if let wordsPredicates { - predicates.append(wordsPredicates) - } else { - predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) + + if tokens.isEmpty { + if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false { + if let searchTextPredicate = searchTextPredicate() { + predicates.append(searchTextPredicate) } - predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation)) - let components = canonicalVersionWithoutPunctuation.split(separator: " ") - let pattern = components.joined(separator: ".*") - let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) - predicates.append(predicate) } - case .ligue: - if canonicalVersionWithoutPunctuation.isEmpty { - predicates.append(NSPredicate(format: "ligueName == nil")) - } else { - predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation)) - } - case .club: - if canonicalVersionWithoutPunctuation.isEmpty { - predicates.append(NSPredicate(format: "clubName == nil")) - } else { - predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation)) - } - case .rankMoreThan: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { - predicates.append(NSPredicate(format: "rank == 0")) - } else { - predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation)) - } - case .rankLessThan: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { - predicates.append(NSPredicate(format: "rank == 0")) - } else { - predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation)) - } - case .rankBetween: - let values = canonicalVersionWithPunctuation.components(separatedBy: ",") - if canonicalVersionWithPunctuation.isEmpty || values.count != 2 { - predicates.append(NSPredicate(format: "rank == 0")) - } else { - predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) - } - case .age: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { - predicates.append(NSPredicate(format: "birthYear == 0")) - } else if let birthYear = Int(canonicalVersionWithoutPunctuation) { - predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) + } + for token in tokens { + switch token { + case .ligue: + if canonicalVersionWithoutPunctuation.isEmpty { + predicates.append(NSPredicate(format: "ligueName == nil")) + } else { + predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation)) + } + case .club: + if canonicalVersionWithoutPunctuation.isEmpty { + predicates.append(NSPredicate(format: "clubName == nil")) + } else { + predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation)) + } + case .rankMoreThan: + if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation)) + } + case .rankLessThan: + if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation)) + } + case .rankBetween: + let values = canonicalVersionWithPunctuation.components(separatedBy: ",") + if canonicalVersionWithPunctuation.isEmpty || values.count != 2 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) + } + case .age: + if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + predicates.append(NSPredicate(format: "birthYear == 0")) + } else if let birthYear = Int(canonicalVersionWithoutPunctuation) { + predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) + } + } - } - if predicates.isEmpty { return nil } diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index f9a7e2d..833f228 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -102,7 +102,7 @@ struct EventListView: View { Logger.error(error) } } label: { - Text("Les afficher tous sur Padel Club") + Text("Afficher ces tournois sur Padel Club") } Button { pcTournaments.forEach { tournament in @@ -114,11 +114,13 @@ struct EventListView: View { Logger.error(error) } } label: { - Text("Les masquer tous sur Padel Club") + Text("Masquer ces tournois sur Padel Club") } } label: { - Text("Options") + Text("Gérer la visibilité sur Padel Club") + .font(.caption) + .underline() } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 23bf3d5..e5e0bd2 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -262,7 +262,7 @@ struct RoundView: View { .foregroundStyle(.green) } } label: { - Text("Classement final des équipes") + Text("Classement final") if tournament.publishRankings == false { Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) } diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 36feb3a..4210ef9 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -337,6 +337,21 @@ struct MySearchView: View { _searchViewModel = ObservedObject(wrappedValue: searchViewModel) _players = FetchRequest(sortDescriptors: searchViewModel.sortDescriptors(), predicate: searchViewModel.predicate()) } + + func searchedPlayers() -> [ImportedPlayer] { + if searchViewModel.searchText.isEmpty { + return Array(players) + } + + if let searchPredicate = searchViewModel.searchTextPredicate() { + let filteredPlayers = players.filter { player in + searchPredicate.evaluate(with: player) + } + return filteredPlayers + } + + return Array(players) + } var body: some View { playersView @@ -371,8 +386,6 @@ 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 { @@ -423,7 +436,7 @@ struct MySearchView: View { } } .id(UUID()) - } else { + } else if searchViewModel.shouldIncludeSearchTextPredicate() { Section { ForEach(players.indices, id: \.self) { index in let player = players[index] @@ -435,26 +448,45 @@ struct MySearchView: View { } } .id(UUID()) + } else { + let filteredPlayers = searchedPlayers() + + Section { + ForEach(filteredPlayers.indices, id: \.self) { index in + let player = filteredPlayers[index] + let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil + let computedIndex = realIndex != nil ? realIndex! + 1 : nil + ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + } + } header: { + if filteredPlayers.isEmpty == false { + headerView() + } + } + .id(UUID()) } } else { + let filteredPlayers = searchedPlayers() Section { - ForEach(players.indices, id: \.self) { index in - let player = players[index] + ForEach(filteredPlayers.indices, id: \.self) { index in + let player = filteredPlayers[index] + let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil + let computedIndex = realIndex != nil ? realIndex! + 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) + ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) } else { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) } } } header: { - if players.isEmpty == false { + if filteredPlayers.isEmpty == false { headerView() } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 2a8e137..b3214ca 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -71,7 +71,7 @@ struct TournamentRankView: View { } footer: { if let url = tournament.shareURL(.rankings) { Link(destination: url) { - Text("Voir la page des classements sur Padel Club") + Text("Voir les classements sur Padel Club") } } } diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index b76002c..d44e5aa 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -124,7 +124,7 @@ struct TournamentBuildView: View { .foregroundStyle(.green) } } label: { - Text("Classement final des équipes") + Text("Classement final") if tournament.publishRankings == false { Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index f2a3ec4..92fea53 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -247,7 +247,7 @@ struct TournamentView: View { .foregroundStyle(.green) } } label: { - Text("Classement final des équipes") + Text("Classement final") if tournament.publishRankings == false { Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) } From 6e31435840113eb4fbcc2557109804d85c06e5bf Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 13 Feb 2025 16:59:25 +0100 Subject: [PATCH 03/11] Adds blocking system to force app download --- PadelClub.xcodeproj/project.pbxproj | 8 ++ PadelClub/PadelClubApp.swift | 125 +++++++++++++++++------- PadelClub/Utils/URLs.swift | 7 +- PadelClub/Utils/VersionComparator.swift | 33 +++++++ 4 files changed, 137 insertions(+), 36 deletions(-) create mode 100644 PadelClub/Utils/VersionComparator.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index e9ed9ba..bb87f8d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BAE432BCA753E002EEC8A /* Purchase.swift */; }; C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */; }; C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */; }; + C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; }; + C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; }; + C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; }; C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; }; C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; }; C49EF0262BD80AE80077B5AA /* SubscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */; }; @@ -942,6 +945,7 @@ C45BAE432BCA753E002EEC8A /* Purchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = ""; }; C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICallsListView.swift; sourceTree = ""; }; C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = ""; }; + C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionComparator.swift; sourceTree = ""; }; C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = ""; }; C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = ""; }; @@ -2030,6 +2034,7 @@ FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */, C49EF01A2BD6A1E80077B5AA /* URLs.swift */, + C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */, FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */, ); path = Utils; @@ -2393,6 +2398,7 @@ FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */, FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */, + C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, @@ -2683,6 +2689,7 @@ FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */, FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */, FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */, + C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */, FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */, FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */, FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */, @@ -2952,6 +2959,7 @@ FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */, FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */, FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */, + C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */, FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */, FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */, FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */, diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 20c2389..b718dbc 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -19,6 +19,8 @@ struct PadelClubApp: App { @State private var importObserverViewModel = ImportObserver() @Environment(\.horizontalSizeClass) var horizontalSizeClass + @State var blockApp = false + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var presentError: Binding { @@ -62,54 +64,79 @@ struct PadelClubApp: App { var body: some Scene { WindowGroup { - MainView() - .environment(\.horizontalSizeClass, .compact) - .alert(isPresented: presentError, error: registrationError) { - Button("Contactez-nous") { - _openMail() - } - Button("Annuler", role: .cancel) { - registrationError = nil + + if self.blockApp { + DownloadNewVersionView() + } else { + MainView() + .environment(\.horizontalSizeClass, .compact) + .alert(isPresented: presentError, error: registrationError) { + Button("Contactez-nous") { + _openMail() + } + Button("Annuler", role: .cancel) { + registrationError = nil + } } - } - .onOpenURL { url in + .onOpenURL { url in #if targetEnvironment(simulator) #else - _handleIncomingURL(url) + _handleIncomingURL(url) #endif - } - .environmentObject(networkMonitor) - .environmentObject(dataStore) - .environment(importObserverViewModel) - .environment(navigationViewModel) - .accentColor(.master) - .onAppear { + } + .environmentObject(networkMonitor) + .environmentObject(dataStore) + .environment(importObserverViewModel) + .environment(navigationViewModel) + .accentColor(.master) + .onAppear { + self._checkVersion() #if DEBUG -print("Running in Debug mode") + print("Running in Debug mode") #elseif TESTFLIGHT -print("Running in TestFlight mode") + print("Running in TestFlight mode") #elseif PRODTEST -print("Running in ProdTest mode") + print("Running in ProdTest mode") #else -print("Running in Release mode") + print("Running in Release mode") #endif - networkMonitor.checkConnection() - self._onAppear() - print(PersistenceController.getModelVersion()) - } - .task { - - try? Tips.resetDatastore() - - try? Tips.configure([ - .displayFrequency(.immediate), - .datastoreLocation(.applicationDefault) - ]) + networkMonitor.checkConnection() + self._onAppear() + print(PersistenceController.getModelVersion()) + } + .task { + + try? Tips.resetDatastore() + + try? Tips.configure([ + .displayFrequency(.immediate), + .datastoreLocation(.applicationDefault) + ]) + } + .environment(\.managedObjectContext, persistenceController.localContainer.viewContext) + } + } + } + + fileprivate func _checkVersion() { + Task.detached(priority: .high) { + if let requiredVersion = await self._retrieveRequiredVersion() { + let cleanedRequired = requiredVersion.replacingOccurrences(of: "\n", with: "") + Logger.log(">>> VERSION = \(requiredVersion)") + if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + await MainActor.run { + self.blockApp = VersionComparator.compare(cleanedRequired, currentVersion) == 1 + } } - .environment(\.managedObjectContext, persistenceController.localContainer.viewContext) + } } } + fileprivate func _retrieveRequiredVersion() async -> String? { + let requiredVersionURL = URLs.main.extend(path: "static/misc/required-version.txt") + return try? String(contentsOf: requiredVersionURL, encoding: .utf8) + } + private func _handleIncomingURL(_ url: URL) { // Parse the URL let pathComponents = url.pathComponents @@ -173,3 +200,31 @@ print("Running in Release mode") } } } + + +struct DownloadNewVersionView: View { + + var body: some View { + + VStack { +// AngledStripesBackground() + Spacer() + Text("Veuillez télécharger la nouvelle version de Padel Club pour continuer à vous servir de l'app !") + .padding(32.0) + .background(.logoYellow) + .clipShape(.buttonBorder) + .foregroundStyle(.logoBackground) + .fontWeight(.medium) + .multilineTextAlignment(.center) + .padding(.horizontal, 64.0) + Image("logo").padding(.vertical, 50.0) + Spacer() + }.background(.logoBackground) + .onTapGesture { + UIApplication.shared.open(URLs.appStore.url) + } + + } + +} + diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index 63f4489..2b9e746 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -46,7 +46,12 @@ enum URLs: String, Identifiable { var url: URL { return URL(string: self.rawValue)! - } + } + + func extend(path: String) -> URL { + return URL(string: self.rawValue + path)! + } + } enum PageLink: String, Identifiable, CaseIterable { diff --git a/PadelClub/Utils/VersionComparator.swift b/PadelClub/Utils/VersionComparator.swift new file mode 100644 index 0000000..9c9a240 --- /dev/null +++ b/PadelClub/Utils/VersionComparator.swift @@ -0,0 +1,33 @@ +// +// VersionComparator.swift +// PadelClub +// +// Created by Laurent Morvillier on 13/02/2025. +// + +class VersionComparator { + + static func compare(_ version1: String, _ version2: String) -> Int { + // Split versions into components + let v1Components = version1.split(separator: ".").map { Int($0) ?? 0 } + let v2Components = version2.split(separator: ".").map { Int($0) ?? 0 } + + // Get the maximum length to compare + let maxLength = max(v1Components.count, v2Components.count) + + // Compare each component + for i in 0.. v2Num { + return 1 // version1 is larger + } + } + + return 0 // versions are equal + } + +} From aa0955917f87ff23f5124b7274709baa4c6d6f01 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 14 Feb 2025 14:24:44 +0100 Subject: [PATCH 04/11] fix thread issue --- PadelClub/PadelClubApp.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index b718dbc..35e88fd 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -122,7 +122,7 @@ struct PadelClubApp: App { Task.detached(priority: .high) { if let requiredVersion = await self._retrieveRequiredVersion() { let cleanedRequired = requiredVersion.replacingOccurrences(of: "\n", with: "") - Logger.log(">>> VERSION = \(requiredVersion)") + Logger.log(">>> REQUIRED VERSION = \(requiredVersion)") if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { await MainActor.run { self.blockApp = VersionComparator.compare(cleanedRequired, currentVersion) == 1 @@ -134,9 +134,16 @@ struct PadelClubApp: App { fileprivate func _retrieveRequiredVersion() async -> String? { let requiredVersionURL = URLs.main.extend(path: "static/misc/required-version.txt") - return try? String(contentsOf: requiredVersionURL, encoding: .utf8) + + do { + let (data, _) = try await URLSession.shared.data(from: requiredVersionURL) + return String(data: data, encoding: .utf8) + } catch { + Logger.log("Error fetching required version: \(error)") + return nil + } } - + private func _handleIncomingURL(_ url: URL) { // Parse the URL let pathComponents = url.pathComponents From 88a3699e467aebd1b54ed9b513c731dced4b1d2f Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 14 Feb 2025 14:28:45 +0100 Subject: [PATCH 05/11] Bumps version to 1.1.10 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index bb87f8d..bcac59a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3337,7 +3337,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.9; + MARKETING_VERSION = 1.1.10; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3384,7 +3384,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.9; + MARKETING_VERSION = 1.1.10; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 738b79bf9c975ff49bd269e61f2eb10c67dc6055 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 14 Feb 2025 16:35:58 +0100 Subject: [PATCH 06/11] fix stuff --- .../Federal/FederalTournamentHolder.swift | 5 - PadelClub/Data/Tournament.swift | 4 - PadelClub/ViewModel/SearchViewModel.swift | 44 +++++--- .../Navigation/Agenda/EventListView.swift | 103 ++++++++++++++---- .../Views/Tournament/Screen/AddTeamView.swift | 9 +- .../Screen/RegistrationSetupView.swift | 21 ++-- .../TournamentInscriptionView.swift | 4 + 7 files changed, 124 insertions(+), 66 deletions(-) diff --git a/PadelClub/Data/Federal/FederalTournamentHolder.swift b/PadelClub/Data/Federal/FederalTournamentHolder.swift index 1e5de69..594e98b 100644 --- a/PadelClub/Data/Federal/FederalTournamentHolder.swift +++ b/PadelClub/Data/Federal/FederalTournamentHolder.swift @@ -19,14 +19,9 @@ protocol FederalTournamentHolder { var dayPeriod: DayPeriod { get } func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool - func togglePrivate(isPrivate: Bool) } extension FederalTournamentHolder { - func togglePrivate(isPrivate: Bool) { - - } - func durationLabel() -> String { switch dayDuration { case 1: diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 531e749..804a089 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -2680,10 +2680,6 @@ extension Tournament: Hashable { } extension Tournament: FederalTournamentHolder { - func togglePrivate(isPrivate: Bool) { - self.isPrivate = isPrivate - } - func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { if isAnimation() { if let name { diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 5167d05..8857997 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -356,6 +356,17 @@ class SearchViewModel: ObservableObject, Identifiable { static func pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { + var andPredicates = [NSPredicate]() + var orPredicates = [NSPredicate]() + + let matches = pasteField.licencesFound() + let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } + orPredicates = licensesPredicates + + if matches.count == 2 { + return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates) + } + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) // Remove all characters that are not in the allowedCharacterSet @@ -369,14 +380,8 @@ class SearchViewModel: ObservableObject, Identifiable { let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines) let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 } let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 }) - var andPredicates = [NSPredicate]() - var orPredicates = [NSPredicate]() - //self.wordsCount = nameComponents.count - - if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) { - orPredicates.append(slashPredicate) - } + //self.wordsCount = nameComponents.count if filterOption == .female { andPredicates.append(NSPredicate(format: "male == NO")) } @@ -385,11 +390,20 @@ class SearchViewModel: ObservableObject, Identifiable { andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } - if nameComponents.count > 1 { - orPredicates.append(contentsOf: nameComponents.pairs().map { - return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }) - } else { - orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) + + if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) { + orPredicates.append(slashPredicate) + } + + print("nameComponents", nameComponents.count) + + if nameComponents.count < 50 { + if nameComponents.count > 1 { + orPredicates.append(contentsOf: nameComponents.pairs().map { + return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }) + } else { + orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) + } } let components = text.split(separator: " ") @@ -397,11 +411,7 @@ class SearchViewModel: ObservableObject, Identifiable { print(text, pattern) let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) orPredicates.append(canonicalFullNamePredicate) - - let matches = pasteField.licencesFound() - let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } - orPredicates = orPredicates + licensesPredicates - + var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) if orPredicates.isEmpty == false { diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 833f228..7000131 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -92,35 +92,83 @@ struct EventListView: View { private func _menuOptions(_ pcTournaments: [Tournament]) -> some View { Menu { - Button { - pcTournaments.forEach { tournament in - tournament.togglePrivate(isPrivate: false) + _options(pcTournaments) + } label: { + Text("Options rapides pour ce mois") + .underline() + } + } + + @ViewBuilder + private func _options(_ pcTournaments: [Tournament]) -> some View { + Section { + if pcTournaments.anySatisfy({ $0.isPrivate == true }) { + Button { + pcTournaments.forEach { tournament in + tournament.isPrivate = false + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Afficher ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club") } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) + } + + if pcTournaments.anySatisfy({ $0.isPrivate == false }) { + Button { + pcTournaments.forEach { tournament in + tournament.isPrivate = true + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Masquer ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club") } - } label: { - Text("Afficher ces tournois sur Padel Club") } - Button { - pcTournaments.forEach { tournament in - tournament.togglePrivate(isPrivate: true) + } header: { + Text("Visibilité sur Padel Club") + } + Divider() + if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { + Section { + if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) { + Button { + pcTournaments.forEach { tournament in + tournament.enableOnlineRegistration = true + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Activer l'inscription en ligne") + } } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) + + if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { + Button { + pcTournaments.forEach { tournament in + tournament.enableOnlineRegistration = false + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Désactiver l'inscription en ligne") + } } - } label: { - Text("Masquer ces tournois sur Padel Club") + } header: { + Text("Inscription en ligne") } - - } label: { - Text("Gérer la visibilité sur Padel Club") - .font(.caption) - .underline() } } @@ -160,13 +208,20 @@ struct EventListView: View { NavigationLink(value: tournament) { TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver()) } + .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) .contextMenu { if tournament.hasEnded() == false { Button { navigation.openTournamentInOrganizer(tournament) } label: { - Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + Label("Afficher dans le gestionnaire", systemImage: "line.diagonal.arrow") } + + Divider() + + _options([tournament]) + + } } #if DEBUG diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 7fd4bf8..f0dbe16 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -620,14 +620,11 @@ struct AddTeamView: View { return 1 } - @MainActor private func handlePasteString(_ first: String) { if first.isEmpty == false { - DispatchQueue.main.async { - fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - autoSelect = true - } + fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) + fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] + autoSelect = true } pasteString = first editableTextField = first diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index ea4d552..33f08f7 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -141,17 +141,18 @@ struct RegistrationSetupView: View { } Section { - Toggle(isOn: $targetTeamCountEnabled) { - Text("Activer une limite") - } - - if targetTeamCountEnabled { - StepperView(count: $targetTeamCount, minimum: 4) - } - } header: { +// Toggle(isOn: $targetTeamCountEnabled) { +// Text("Activer une limite") +// } +// +// if targetTeamCountEnabled { +// StepperView(count: $targetTeamCount, minimum: 4) +// } + StepperView(count: $targetTeamCount, minimum: 4) + } header: { Text("Paires admises") } footer: { - Text("Si une limite de paire existe, les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.") + Text("Les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.") } Section { @@ -160,7 +161,7 @@ struct RegistrationSetupView: View { } if waitingListLimitEnabled { - StepperView(count: $waitingListLimit, minimum: 1) + StepperView(count: $waitingListLimit, minimum: 0) } } header: { Text("Liste d'attente") diff --git a/PadelClub/Views/Tournament/TournamentInscriptionView.swift b/PadelClub/Views/Tournament/TournamentInscriptionView.swift index a40dee1..c06461a 100644 --- a/PadelClub/Views/Tournament/TournamentInscriptionView.swift +++ b/PadelClub/Views/Tournament/TournamentInscriptionView.swift @@ -21,6 +21,10 @@ struct TournamentInscriptionView: View { Text("Gestion des inscriptions") if let closedRegistrationDate = tournament.closedRegistrationDate { Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) + } else if tournament.enableOnlineRegistration { + Text("Inscription en ligne activée") + } else if tournament.onlineRegistrationCanBeEnabled() { + Text("Inscription en ligne désactivée") } } } From 5e1798af2a8ade335331ed9244a8979aa418aecf Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 14 Feb 2025 17:06:15 +0100 Subject: [PATCH 07/11] wip setup tournament using previous tournament preferences --- PadelClub/Data/Tournament.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 804a089..20c1a44 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -2171,6 +2171,25 @@ defer { //enableOnlineRegistration = true registrationDateLimit = deadline(for: .inscription) } + + //self.customizeUsingPreferences() + } + + func customizeUsingPreferences() { + guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in + tournament.tournamentLevel == self.tournamentLevel + && tournament.tournamentCategory == self.tournamentCategory + && tournament.federalTournamentAge == self.federalTournamentAge + && tournament.hasEnded() == true + && tournament.isCanceled == false + && tournament.isDeleted == false + }).sorted(by: \.endDate!, order: .descending).first else { + return + } + + self.dayDuration = lastTournamentWithSameBuild.dayDuration + self.teamCount = (lastTournamentWithSameBuild.teamCount / 2) * 2 + self.enableOnlineRegistration = lastTournamentWithSameBuild.enableOnlineRegistration } func onlineRegistrationCanBeEnabled() -> Bool { From a3459ba57ff213244ca711c0a16dcb1a3885ef74 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 14 Feb 2025 17:07:19 +0100 Subject: [PATCH 08/11] v1.1.11 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index bcac59a..6fbfa92 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3337,7 +3337,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3384,7 +3384,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 7c7aee398a3b75c60e09a7a0d39984d0f1555fd8 Mon Sep 17 00:00:00 2001 From: Laurent Date: Sun, 16 Feb 2025 11:57:55 +0100 Subject: [PATCH 09/11] Bumps to 1.1.12 - 2 --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 6fbfa92..03fae61 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3310,7 +3310,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3337,7 +3337,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.11; + MARKETING_VERSION = 1.1.12; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3358,7 +3358,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3384,7 +3384,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.11; + MARKETING_VERSION = 1.1.12; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 119d7e53c9825637d81f64629dcc58fe614e9c80 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 20 Feb 2025 10:59:31 +0100 Subject: [PATCH 10/11] fix wildcard management fix p500 dates and schedule add format aide memoire optimize selectedsorted teams calls --- PadelClub.xcodeproj/project.pbxproj | 12 +- PadelClub/Data/TeamRegistration.swift | 8 +- PadelClub/Data/Tournament.swift | 26 ++-- PadelClub/Utils/PadelRule.swift | 34 +++-- PadelClub/Views/Calling/SendToAllView.swift | 7 +- .../Toolbox/MatchFormatStorageView.swift | 2 +- .../Views/Planning/MatchFormatGuideView.swift | 72 +++++++++++ .../Views/Planning/PlanningSettingsView.swift | 25 +++- .../Team/Components/TeamWeightView.swift | 7 +- PadelClub/Views/Team/EditingTeamView.swift | 3 +- PadelClub/Views/Team/TeamRowView.swift | 3 +- .../Views/Tournament/FileImportView.swift | 2 +- .../TournamentMatchFormatsSettingsView.swift | 2 +- .../Screen/InscriptionManagerView.swift | 121 +++++++++--------- 14 files changed, 221 insertions(+), 103 deletions(-) create mode 100644 PadelClub/Views/Planning/MatchFormatGuideView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 6fbfa92..e1b24d3 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -798,6 +798,9 @@ FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; + FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; + FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; + FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; @@ -1201,6 +1204,7 @@ FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = ""; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = ""; }; + FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.swift; sourceTree = ""; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = ""; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = ""; }; FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = ""; }; @@ -2073,6 +2077,7 @@ FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */, + FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */, FF1162882BD0523B000C4809 /* Components */, ); path = Planning; @@ -2494,6 +2499,7 @@ FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, + FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */, @@ -2785,6 +2791,7 @@ FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, + FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */, @@ -3055,6 +3062,7 @@ FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, + FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */, @@ -3315,7 +3323,6 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; - GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3344,7 +3351,6 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -3362,7 +3368,6 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; - GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3391,7 +3396,6 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index cc7dd6d..e9337af 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -632,13 +632,17 @@ final class TeamRegistration: ModelObject, Storable { func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { if let bracketPosition, let drawMatchIndex { return drawMatchIndex != bracketPosition - } else if let bracketPosition { + } else if bracketPosition != nil { return true - } else if let drawMatchIndex { + } else if drawMatchIndex != nil { return true } return false } + + func shouldDisplayRankAndWeight() -> Bool { + unsortedPlayers().count > 0 + } enum CodingKeys: String, CodingKey { case _id = "id" diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 20c1a44..4470155 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -428,6 +428,11 @@ final class Tournament : ModelObject, Storable { return Array(self.tournamentStore.teamRegistrations) } + func unsortedTeamsCount() -> Int { + return self.tournamentStore.teamRegistrations.count + } + + func groupStages(atStep step: Int = 0) -> [GroupStage] { let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } return groupStages.sorted(by: \.index) @@ -581,7 +586,8 @@ defer { } func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { - let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams() + let _selectedSortedTeams = selectedSortedTeams() + let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams) switch exportFormat { case .rawText: return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2)) @@ -878,13 +884,13 @@ defer { return rounds.sorted(by: \.index).reversed() } - func sortedTeams() -> [TeamRegistration] { - let teams = selectedSortedTeams() + func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { + let teams = selectedSortedTeams return teams + waitingListTeams(in: teams, includingWalkOuts: true) } - func waitingListSortedTeams() -> [TeamRegistration] { - let teams = selectedSortedTeams() + func waitingListSortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { + let teams = selectedSortedTeams return waitingListTeams(in: teams, includingWalkOuts: false) } @@ -1183,9 +1189,8 @@ defer { } } - func registrationIssues() -> Int { + func registrationIssues(selectedTeams: [TeamRegistration]) -> Int { let players : [PlayerRegistration] = unsortedPlayers() - let selectedTeams : [TeamRegistration] = selectedSortedTeams() let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let duplicates : [PlayerRegistration] = duplicates(in: players) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) @@ -1852,6 +1857,7 @@ defer { team.wildCardGroupStage = true } + team.setWeight(from: [], inTournamentCategory: self.tournamentCategory) return team } @@ -1865,6 +1871,7 @@ defer { func addEmptyTeamRegistration(_ count: Int) { let teams = (0.. Date? { guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } - var daysOffset = type.daysOffset - if tournamentLevel == .p500 { - daysOffset += 7 - } + var daysOffset = type.daysOffset(level: tournamentLevel) if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) { let startOfDay = Calendar.current.startOfDay(for: date) return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index ff9b1d8..dd7cbd2 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1996,16 +1996,30 @@ enum TournamentDeadlineType: String, CaseIterable { case wildcardLicensePurchase = "Prise de licence des WC" case definitiveBroadcastList = "Publication définitive" - var daysOffset: Int { - switch self { - case .inscription: - return -13 - case .broadcastList: - return -12 - case .wildcardRequest: - return -9 - case .wildcardLicensePurchase, .definitiveBroadcastList: - return -8 + func daysOffset(level: TournamentLevel) -> Int { + if level == .p500 { + switch self { + case .inscription: + return -6 + case .broadcastList: + return -6 + case .wildcardRequest: + return -4 + case .wildcardLicensePurchase, .definitiveBroadcastList: + return -4 + } + } else { + switch self { + case .inscription: + return -13 + case .broadcastList: + return -12 + case .wildcardRequest: + return -9 + case .wildcardLicensePurchase, .definitiveBroadcastList: + return -8 + } + } } diff --git a/PadelClub/Views/Calling/SendToAllView.swift b/PadelClub/Views/Calling/SendToAllView.swift index d261f35..8a2adc2 100644 --- a/PadelClub/Views/Calling/SendToAllView.swift +++ b/PadelClub/Views/Calling/SendToAllView.swift @@ -223,13 +223,14 @@ struct SendToAllView: View { } func _teams() -> [TeamRegistration] { + let selectedSortedTeams = tournament.selectedSortedTeams() if onlyWaitingList { - return tournament.waitingListSortedTeams() + return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) } if _roundTeams().isEmpty && _groupStagesTeams().isEmpty { - return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : []) + return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : []) } - return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : []) + return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : []) } func _roundTeams() -> [TeamRegistration] { diff --git a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift index 7d90518..9565658 100644 --- a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift +++ b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift @@ -22,7 +22,7 @@ struct MatchFormatStorageView: View { var body: some View { Section { LabeledContent { - StepperView(title: "minutes", count: $estimatedDuration, step: 5) + StepperView(title: "minute", count: $estimatedDuration, step: 5) } label: { MatchFormatRowView(matchFormat: matchFormat, hideDuration: true) } diff --git a/PadelClub/Views/Planning/MatchFormatGuideView.swift b/PadelClub/Views/Planning/MatchFormatGuideView.swift new file mode 100644 index 0000000..4596a77 --- /dev/null +++ b/PadelClub/Views/Planning/MatchFormatGuideView.swift @@ -0,0 +1,72 @@ +// +// MatchFormatGuideView.swift +// PadelClub +// +// Created by razmig on 20/02/2025. +// + +import SwiftUI + +struct MatchFormatGuideView: View { + let matchCounts = Array(2...7) + let formats: [MatchFormat] = [ + .twoSets, .twoSetsDecisivePoint, + .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, + .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, + .nineGames, .nineGamesDecisivePoint, + .superTie + ] + + func getFormatDescription(for matchCount: Int) -> String { + var description = "" + + // Group formats by their behavior + let formatGroups = Dictionary(grouping: formats) { format in + format.maximumMatchPerDay(for: matchCount) + } + + // Sort by maximum matches allowed (descending) + let sortedMaxMatches = formatGroups.keys.sorted(by: >) + + for maxMatches in sortedMaxMatches { + if let formatsForMax = formatGroups[maxMatches] { + let formatStrings = formatsForMax.map { $0.format }.joined(separator: "/") + if maxMatches > 0 && maxMatches <= matchCount { + description += "Maximum \(maxMatches) matchs en format \(formatStrings)\n" + } else if maxMatches == 0 { + description += "Aucun match au format \(formatStrings)\n" + } + } + } + + if matchCount >= 7 { + description += "Format \(MatchFormat.superTie.format) principalement" + } + + return description.isEmpty ? "Aucun match possible" : description + } + + var body: some View { + List { + Section { + ForEach(matchCounts, id: \.self) { count in + VStack { + Text("\(count) matchs par jour") + .font(.headline) + Text(getFormatDescription(for: count)) + } + } + + // Special case for 7+ matches + VStack { + Text("7+ matchs par jour") + .font(.headline) + Text("Tournois P 25 uniquement (soirée/demi-journée/journée)") + } + } + } + .navigationTitle("Guide des Formats de Match") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 8a0d080..8a1d024 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -23,7 +23,8 @@ struct PlanningSettingsView: View { @State private var parallelType: Bool = false @State private var deletingDateMatchesDone: Bool = false @State private var deletingDone: Bool = false - + @State private var presentFormatHelperView: Bool = false + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -145,6 +146,28 @@ struct PlanningSettingsView: View { _smartView() } + .navigationTitle("Réglages") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + presentFormatHelperView = true + } label: { + Text("Aide-mémoire") + } + } + } + .sheet(isPresented: $presentFormatHelperView) { + NavigationStack { + MatchFormatGuideView() + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Retour", role: .cancel) { + presentFormatHelperView = false + } + } + } + } + } .headerProminence(.increased) .onAppear { do { diff --git a/PadelClub/Views/Team/Components/TeamWeightView.swift b/PadelClub/Views/Team/Components/TeamWeightView.swift index f19b6e6..0868ed3 100644 --- a/PadelClub/Views/Team/Components/TeamWeightView.swift +++ b/PadelClub/Views/Team/Components/TeamWeightView.swift @@ -12,16 +12,15 @@ struct TeamWeightView: View { let team: TeamRegistration var teamPosition: TeamPosition? = nil - var teamIndex: Int? { - team.tournamentObject()?.indexOf(team: team) - } + var teamIndex: Int? var displayWeight: Bool { - team.tournamentObject()?.hideWeight() == false + team.shouldDisplayRankAndWeight() && team.tournamentObject()?.hideWeight() == false } var body: some View { VStack(alignment: .trailing, spacing: 0) { + let displayWeight = self.displayWeight if (teamPosition == .one || teamPosition == nil) && displayWeight { Text(team.weight.formatted()) .monospacedDigit() diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 76bbe32..8f06955 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -47,7 +47,8 @@ struct EditingTeamView: View { } private func _resetTeam() { - self.currentWaitingList = tournament.waitingListSortedTeams().filter({ $0.hasRegisteredOnline() }).first + let selectedSortedTeams = tournament.selectedSortedTeams() + self.currentWaitingList = tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams).filter({ $0.hasRegisteredOnline() }).first team.resetPositions() team.wildCardGroupStage = false team.walkOut = false diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 966f903..4a1ba04 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -13,10 +13,11 @@ struct TeamRowView: View { var teamPosition: TeamPosition? = nil var displayCallDate: Bool = false var displayRestingTime: Bool = false + var teamIndex: Int? var body: some View { LabeledContent { - TeamWeightView(team: team, teamPosition: teamPosition) + TeamWeightView(team: team, teamPosition: teamPosition, teamIndex: teamIndex) } label: { VStack(alignment: .leading) { TeamHeadlineView(team: team) diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 500d7dc..f72a8cc 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -312,7 +312,7 @@ struct FileImportView: View { } } else if didImport { let _filteredTeams = filteredTeams - let previousTeams = tournament.sortedTeams() + let previousTeams = tournament.sortedTeams(selectedSortedTeams: tournament.selectedSortedTeams()) if previousTeams.isEmpty == false { Section { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift index ebf4bde..2546787 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift @@ -34,7 +34,7 @@ struct TournamentMatchFormatsSettingsView: View { Section { LabeledContent { - StepperView(title: "minutes", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10) + StepperView(title: "minute", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10) } label: { Text("Modifier les durées moyennes") } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index e95fade..d4d875a 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -180,7 +180,7 @@ struct InscriptionManagerView: View { return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) } - private func _setHash() { + private func _setHash(currentSelectedSortedTeams: [TeamRegistration]? = nil) { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -188,18 +188,17 @@ struct InscriptionManagerView: View { print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - let selectedSortedTeams = tournament.selectedSortedTeams() + let selectedSortedTeams = currentSelectedSortedTeams == nil ? tournament.selectedSortedTeams() : currentSelectedSortedTeams! if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) } self.registrationIssues = nil DispatchQueue.main.async { - self.registrationIssues = tournament.registrationIssues() + self.registrationIssues = tournament.registrationIssues(selectedTeams: selectedSortedTeams) } } - private func _handleHashDiff() { - let selectedSortedTeams = tournament.selectedSortedTeams() + private func _handleHashDiff(selectedSortedTeams: [TeamRegistration]) { let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) { self.teamsHash = newHash @@ -225,9 +224,10 @@ struct InscriptionManagerView: View { } var body: some View { - Group { - if tournament.unsortedTeams().isEmpty == false { - _teamRegisteredView() + let selectedSortedTeams = tournament.selectedSortedTeams() + return Group { + if tournament.unsortedTeamsCount() > 0 { + _teamRegisteredView(selectedSortedTeams: selectedSortedTeams) } else { List { @@ -263,10 +263,10 @@ struct InscriptionManagerView: View { await _refreshList() } .onAppear { - _setHash() + _setHash(currentSelectedSortedTeams: selectedSortedTeams) } .onDisappear { - _handleHashDiff() + _handleHashDiff(selectedSortedTeams: selectedSortedTeams) } .sheet(isPresented: $isLearningMore) { LearnMoreSheetView(tournament: tournament) @@ -490,47 +490,43 @@ struct InscriptionManagerView: View { tournament.unsortedPlayers() } - var sortedTeams: [TeamRegistration] { + func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { if filterMode == .waiting { - return tournament.waitingListSortedTeams() + return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) } else { - return tournament.sortedTeams() + return tournament.sortedTeams(selectedSortedTeams: selectedSortedTeams) } } - var filteredTeams: [TeamRegistration] { - - var teams = sortedTeams - switch filterMode { - case .wildcardBracket: - teams = teams.filter({ $0.wildCardBracket }) - case .wildcardGroupStage: - teams = teams.filter({ $0.wildCardGroupStage }) - case .walkOut: - teams = teams.filter({ $0.walkOut }) - case .bracket: - teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false }) - case .groupStage: - teams = teams.filter({ $0.inGroupStage() }) - case .notImported: - teams = teams.filter({ $0.isImported() == false }) - case .registeredLocally: - teams = teams.filter({ $0.hasRegisteredOnline() == false }) - case .registeredOnline: - teams = teams.filter({ $0.hasRegisteredOnline() == true }) - default: - break - } - - if sortingMode == .registrationDate { - teams = teams.sorted(by: \.computedRegistrationDate) + func filteredTeams(sortedTeams: [TeamRegistration]) -> [TeamRegistration] { + let filtered = sortedTeams.lazy.filter { team in + switch filterMode { + case .wildcardBracket: + return team.wildCardBracket + case .wildcardGroupStage: + return team.wildCardGroupStage + case .walkOut: + return team.walkOut + case .bracket: + return team.inRound() && !team.inGroupStage() + case .groupStage: + return team.inGroupStage() + case .notImported: + return !team.isImported() + case .registeredLocally: + return !team.hasRegisteredOnline() + case .registeredOnline: + return team.hasRegisteredOnline() + default: + return true + } } - if byDecreasingOrdering { - return teams.reversed() - } else { - return teams - } + let sorted = sortingMode == .registrationDate + ? filtered.sorted(by: { $0.computedRegistrationDate < $1.computedRegistrationDate }) + : Array(filtered) + + return byDecreasingOrdering ? sorted.reversed() : sorted } // private func _fixModel() { @@ -572,12 +568,10 @@ struct InscriptionManagerView: View { } } - private func _teamRegisteredView() -> some View { + private func _teamRegisteredView(selectedSortedTeams: [TeamRegistration]) -> some View { List { - let selectedSortedTeams = tournament.selectedSortedTeams() - if presentSearch == false { - _informationView() + _informationView(for: selectedSortedTeams) if tournament.isAnimation() == false { _rankHandlerView() @@ -585,7 +579,8 @@ struct InscriptionManagerView: View { } } - let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) + let sortedTeams = sortedTeams(selectedSortedTeams: selectedSortedTeams) + let teams = searchField.isEmpty ? filteredTeams(sortedTeams: sortedTeams) : filteredTeams(sortedTeams: sortedTeams).filter({ $0.contains(searchField.canonicalVersion) }) if teams.isEmpty && searchField.isEmpty == false { ContentUnavailableView { @@ -622,7 +617,7 @@ struct InscriptionManagerView: View { EditingTeamView(team: team) .environment(tournament) } label: { - TeamRowView(team: team) + TeamRowView(team: team, teamIndex: teamIndex) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if tournament.enableOnlineRegistration == false { @@ -735,18 +730,18 @@ struct InscriptionManagerView: View { } } - private func _teamCountForFilterMode(filterMode: FilterMode) -> String { + private func _teamCountForFilterMode(filterMode: FilterMode, in teams: [TeamRegistration]) -> String { switch filterMode { case .wildcardBracket: - return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted() + return teams.filter({ $0.wildCardBracket }).count.formatted() case .wildcardGroupStage: - return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted() + return teams.filter({ $0.wildCardGroupStage }).count.formatted() case .all: return unsortedTeamsWithoutWO.count.formatted() case .bracket: - return tournament.selectedSortedTeams().filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted() + return teams.filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted() case .groupStage: - return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted() + return teams.filter({ $0.inGroupStage() }).count.formatted() case .walkOut: let wo = walkoutTeams.count.formatted() return wo @@ -754,20 +749,20 @@ struct InscriptionManagerView: View { let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) return waiting.formatted() case .notImported: - let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count) + let notImported: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.isImported() == false }).count) return notImported.formatted() case .registeredLocally: - let registeredLocally: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() == false }).count) + let registeredLocally: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() == false }).count) return registeredLocally.formatted() case .registeredOnline: - let registeredOnline: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() }).count) + let registeredOnline: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() }).count) return registeredOnline.formatted() } } @ViewBuilder - private func _informationView() -> some View { + private func _informationView(for teams: [TeamRegistration]) -> some View { Section { HStack { // VStack(alignment: .leading, spacing: 0) { @@ -781,7 +776,7 @@ struct InscriptionManagerView: View { // } // ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in - _filterModeView(filterMode: filterMode) + _filterModeView(filterMode: filterMode, in: teams) } Button { @@ -809,7 +804,7 @@ struct InscriptionManagerView: View { .listRowSeparator(.hidden) HStack { ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in - _filterModeView(filterMode: filterMode) + _filterModeView(filterMode: filterMode, in: teams) } } .padding(.bottom, -4) @@ -883,7 +878,7 @@ struct InscriptionManagerView: View { } } - private func _filterModeView(filterMode: FilterMode) -> some View { + private func _filterModeView(filterMode: FilterMode, in teams: [TeamRegistration]) -> some View { Button { if self.filterMode == filterMode { @@ -894,7 +889,7 @@ struct InscriptionManagerView: View { } label: { VStack(alignment: .center, spacing: -2) { Text(filterMode.localizedLabel(.short)).font(.caption).padding(.horizontal, -8) - Text(_teamCountForFilterMode(filterMode: filterMode)).font(.largeTitle) + Text(_teamCountForFilterMode(filterMode: filterMode, in: teams)).font(.largeTitle) } .frame(maxWidth: .infinity) .contentShape(Rectangle()) From 0afde57d8ce4e6fd8f9d0a7415f51216f566d063 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 20 Feb 2025 11:21:33 +0100 Subject: [PATCH 11/11] v1.1.13 --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 6ad3437..c3b7e74 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3318,7 +3318,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3344,7 +3344,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.12; + MARKETING_VERSION = 1.1.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3364,7 +3364,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3389,7 +3389,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.12; + MARKETING_VERSION = 1.1.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";