diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 094c04d..1464dc8 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3278,7 +3278,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3302,7 +3302,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.32; + MARKETING_VERSION = 1.0.33; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3323,7 +3323,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3346,7 +3346,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.32; + MARKETING_VERSION = 1.0.33; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/AppSettings.swift b/PadelClub/Data/AppSettings.swift index 60be3e3..4dbc2db 100644 --- a/PadelClub/Data/AppSettings.swift +++ b/PadelClub/Data/AppSettings.swift @@ -29,6 +29,18 @@ final class AppSettings: MicroStorable { var nationalCup: Bool var dayDuration: Int? var dayPeriod: DayPeriod + + func lastDataSourceDate() -> Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + + func localizedLastDataSource() -> String? { + guard let lastDataSource else { return nil } + guard let date = URL.importDateFormatter.date(from: lastDataSource) else { return nil } + + return date.monthYearFormatted + } func resetSearch() { tournamentAges = Set() diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 4738c2c..75e6946 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -203,6 +203,10 @@ final class GroupStage: ModelObject, Storable { } } + func orderedIndexOfMatch(_ match: Match) -> Int { + _matchOrder()[safe: match.index] ?? match.index + } + func updateGroupStageState() { clearScoreCache() @@ -368,7 +372,7 @@ final class GroupStage: ModelObject, Storable { _matchOrder().firstIndex(of: matchIndex) ?? matchIndex } - private func _matchUp(for matchIndex: Int) -> [Int] { + func _matchUp(for matchIndex: Int) -> [Int] { let combinations = Array((0.. Bool { let teams = teamScores guard teams.count == 2 else { - print("teams.count != 2") + //print("teams.count != 2") return false } guard hasEnded() == false else { return false } @@ -832,8 +832,8 @@ defer { if teamPosition == team(.two)?.groupStagePositionAtStep(step) { reverseValue = -1 } - let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) - let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) + let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) + let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) var setDifference : Int = 0 let zip = zip(endedSetsOne, endedSetsTwo) if matchFormat.setsToWin == 1 { diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 265e03f..f45f08f 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -32,6 +32,7 @@ final class MatchScheduler : ModelObject, Storable { var overrideCourtsUnavailability: Bool = false var shouldTryToFillUpCourtsAvailable: Bool = true var courtsAvailable: Set = Set() + var simultaneousStart: Bool = true init(tournament: String, timeDifferenceLimit: Int = 5, @@ -46,7 +47,8 @@ final class MatchScheduler : ModelObject, Storable { groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = true, - courtsAvailable: Set = Set()) { + courtsAvailable: Set = Set(), + simultaneousStart: Bool = true) { self.tournament = tournament self.timeDifferenceLimit = timeDifferenceLimit self.loserBracketRotationDifference = loserBracketRotationDifference @@ -61,6 +63,7 @@ final class MatchScheduler : ModelObject, Storable { self.overrideCourtsUnavailability = overrideCourtsUnavailability self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable self.courtsAvailable = courtsAvailable + self.simultaneousStart = simultaneousStart } enum CodingKeys: String, CodingKey { @@ -79,6 +82,7 @@ final class MatchScheduler : ModelObject, Storable { case _overrideCourtsUnavailability = "overrideCourtsUnavailability" case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" case _courtsAvailable = "courtsAvailable" + case _simultaneousStart = "simultaneousStart" } var courtsUnavailability: [DateInterval]? { @@ -185,14 +189,18 @@ final class MatchScheduler : ModelObject, Storable { // Get the maximum count of matches in any group let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 - - // Flatten matches in a round-robin order by cycling through each group - let flattenedMatches = (0.. 0 { rotationMatches = rotationMatches.sorted(by: { if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { - return $0.groupStageObject!.index < $1.groupStageObject!.index + if simultaneousStart { + return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1) + } else { + return $0.groupStageObject!.index < $1.groupStageObject!.index + } } else { return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 } @@ -245,7 +257,7 @@ final class MatchScheduler : ModelObject, Storable { return false } - let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) }) + let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) }) if !teamsAvailable { print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation") return false @@ -259,7 +271,7 @@ final class MatchScheduler : ModelObject, Storable { print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)") slots.append(timeMatch) - teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) + teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp()) rotationMatches.removeAll(where: { $0.id == first.id }) availableMatches.removeAll(where: { $0.id == first.id }) @@ -891,4 +903,16 @@ extension Match { func containsTeamId(_ id: String) -> Bool { return teamIds().contains(id) } + + func containsTeamIndex(_ id: String) -> Bool { + matchUp().contains(id) + } + + func matchUp() -> [String] { + guard let groupStageObject else { + return [] + } + + return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" } + } } diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index dd88446..6bea89e 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -94,11 +94,11 @@ class MatchDescriptor: ObservableObject { } var teamOneScores: [String] { - setDescriptors.compactMap { $0.valueTeamOne }.map { "\($0)" } + setDescriptors.compactMap { $0.getValue(teamPosition: .one) } } var teamTwoScores: [String] { - setDescriptors.compactMap { $0.valueTeamTwo }.map { "\($0)" } + setDescriptors.compactMap { $0.getValue(teamPosition: .two) } } var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count } diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 0b066c3..be8a85c 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -36,8 +36,7 @@ class SearchViewModel: ObservableObject, Identifiable { @Published var filterSelectionEnabled: Bool = false @Published var isPresented: Bool = false @Published var selectedAgeCategory: FederalTournamentAge = .unlisted - - var mostRecentDate: Date? = nil + @Published var mostRecentDate: Date? = nil var selectionIsOver: Bool { if allowSingleSelection && selectedPlayers.count == 1 { @@ -69,9 +68,6 @@ class SearchViewModel: ObservableObject, Identifiable { var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] if tokens.isEmpty { message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") - if filterOption == .male { - message.append("Depuis août 2024, le classement fédérale disponible est limité aux 40.000 premiers joueurs. Si le joueur n'a pas encore assez de points pour être visible, Padel Club ne pourra pas non plus le trouver.") - } } return message.joined(separator: "\n") } @@ -231,7 +227,7 @@ class SearchViewModel: ObservableObject, Identifiable { ] if let mostRecentDate { - //predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } if hideAssimilation { @@ -344,7 +340,7 @@ class SearchViewModel: ObservableObject, Identifiable { } if let mostRecentDate { - //andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } if nameComponents.count > 1 { diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index 6ef5bfe..eb04555 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -40,4 +40,23 @@ struct SetDescriptor: Identifiable, Equatable { var shouldTieBreak: Bool { setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0) } + + func getValue(teamPosition: TeamPosition) -> String? { + switch teamPosition { + case .one: + if let valueTeamOne { + if let tieBreakValueTeamOne { + return "\(valueTeamOne)-\(tieBreakValueTeamOne)" + } + } + case .two: + if let valueTeamTwo { + if let tieBreakValueTeamTwo { + return "\(valueTeamTwo)-\(tieBreakValueTeamTwo)" + } + } + } + + return nil + } } diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 8c9dd36..e8fbfd0 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -129,7 +129,7 @@ struct ToolboxView: View { Section { NavigationLink { - SelectablePlayerListView(isPresented: false) + SelectablePlayerListView(isPresented: false, lastDataSource: true) } label: { Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 3edfed5..8a0d080 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -385,6 +385,14 @@ struct PlanningSettingsView: View { } footer: { Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.") } + + Section { + Toggle(isOn: $matchScheduler.simultaneousStart) { + Text("Démarrage simultané") + } + } footer: { + Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.") + } } Section { diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 9b61575..00a4836 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -24,23 +24,19 @@ struct SelectablePlayerListView: View { @StateObject private var searchViewModel: SearchViewModel @Environment(\.dismiss) var dismiss - var lastDataSource: String? { - dataStore.appSettings.lastDataSource - } - @State private var searchText: String = "" - var mostRecentDate: Date? { - guard let lastDataSource else { return nil } - return URL.importDateFormatter.date(from: lastDataSource) - } - - init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { + + init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, lastDataSource: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { self.allowSelection = allowSelection self.playerSelectionAction = playerSelectionAction self.contentUnavailableAction = contentUnavailableAction self.searchText = searchField ?? "" let searchViewModel = SearchViewModel() searchViewModel.tokens = tokens + if lastDataSource { + searchViewModel.mostRecentDate = DataStore.shared.appSettings.lastDataSourceDate() + } + searchViewModel.searchText = searchField ?? "" searchViewModel.debouncableText = searchField ?? "" searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation @@ -59,6 +55,18 @@ struct SelectablePlayerListView: View { _searchViewModel = StateObject(wrappedValue: searchViewModel) } + var enableSourceCheck: Binding { + Binding { + searchViewModel.mostRecentDate != nil + } set: { value in + if value == false { + searchViewModel.mostRecentDate = nil + } else { + searchViewModel.mostRecentDate = dataStore.appSettings.lastDataSourceDate() + } + } + } + var body: some View { VStack(spacing: 0) { if importObserver.isImportingFile() == false { @@ -73,6 +81,13 @@ struct SelectablePlayerListView: View { } .pickerStyle(.segmented) Menu { + if let lastDataSource = dataStore.appSettings.localizedLastDataSource() { + Section { + Toggle(isOn: enableSourceCheck) { + Text("Limité à \(lastDataSource)") + } + } + } Section { ForEach(SourceFileManager.getSortOption()) { option in Toggle(isOn: .init(get: { @@ -191,7 +206,6 @@ struct SelectablePlayerListView: View { } } .onAppear { - searchViewModel.mostRecentDate = mostRecentDate if searchViewModel.tokens.isEmpty && searchText.isEmpty { searchViewModel.debouncableText.removeAll() searchViewModel.searchText.removeAll()