From 051065b453680480c6461daeebe9c2ca36454295 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 23 Sep 2024 11:31:57 +0200 Subject: [PATCH 01/41] fix search player stuff --- PadelClub/ViewModel/SearchViewModel.swift | 4 +- .../Views/Tournament/Screen/AddTeamView.swift | 65 +++++++++++++------ .../Screen/TournamentRankView.swift | 2 +- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 8d8e9e7..0b066c3 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -168,7 +168,7 @@ class SearchViewModel: ObservableObject, Identifiable { predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) } predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation)) - let components = canonicalVersionWithoutPunctuation.split(separator: " ").sorted() + let components = canonicalVersionWithoutPunctuation.split(separator: " ") let pattern = components.joined(separator: ".*") let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) predicates.append(predicate) @@ -354,7 +354,7 @@ class SearchViewModel: ObservableObject, Identifiable { orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) } - let components = text.split(separator: " ").sorted() + let components = text.split(separator: " ") let pattern = components.joined(separator: ".*") print(text, pattern) let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 32b0f1a..cd0f484 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -41,6 +41,7 @@ struct AddTeamView: View { @State private var homonyms: [PlayerRegistration] = [] @State private var confirmHomonym: Bool = false @State private var editableTextField: String = "" + @State private var textHeight: CGFloat = 100 // Default height var tournamentStore: TournamentStore { return self.tournament.tournamentStore @@ -67,6 +68,7 @@ struct AddTeamView: View { _fetchPlayers = FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption)) _autoSelect = .init(wrappedValue: true) _editableTextField = .init(wrappedValue: pasteString) + _textHeight = .init(wrappedValue: Self._calculateHeight(text: pasteString)) cancelShouldDismiss = true } } @@ -121,7 +123,7 @@ struct AddTeamView: View { selectionSearchField = nil }) { NavigationStack { - SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in + SelectablePlayerListView(allowSelection: -1, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in selectionSearchField = nil players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) @@ -147,7 +149,7 @@ struct AddTeamView: View { } .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Annuler", role: .cancel) { + Button("Terminer", role: .cancel) { dismiss() } } @@ -166,7 +168,7 @@ struct AddTeamView: View { } .navigationBarBackButtonHidden(true) .toolbarBackground(.visible, for: .navigationBar) - .toolbarBackground(.visible, for: .bottomBar) + .toolbarBackground(.automatic, for: .bottomBar) .navigationBarTitleDisplayMode(.inline) .navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe") .environment(\.editMode, Binding.constant(EditMode.active)) @@ -291,14 +293,16 @@ struct AddTeamView: View { Logger.error(error) } - createdPlayers.removeAll() - createdPlayerIds.removeAll() - pasteString = nil - editableTextField = "" + pasteString = nil + editableTextField = "" - if team.players().count > 1 { - dismiss() - } + if team.players().count > 1 { + createdPlayers.removeAll() + createdPlayerIds.removeAll() + dismiss() + } else { + editedTeam = team + } } private func _updateTeam(checkDuplicates: Bool) { @@ -320,23 +324,37 @@ struct AddTeamView: View { } catch { Logger.error(error) } - createdPlayers.removeAll() - createdPlayerIds.removeAll() + pasteString = nil editableTextField = "" - self.editedTeam = nil if editedTeam.players().count > 1 { dismiss() } } + // Calculating the height based on the content of the TextEditor + static private func _calculateHeight(text: String) -> CGFloat { + let size = CGSize(width: UIScreen.main.bounds.width - 32, height: .infinity) + let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 17)] + let boundingRect = text.boundingRect( + with: size, + options: .usesLineFragmentOrigin, + attributes: attributes, + context: nil + ) + return max(boundingRect.height + 20, 40) // Add some padding and set a minimum height + } + @ViewBuilder private func _buildingTeamView() -> some View { if let pasteString { Section { TextEditor(text: $editableTextField) - .frame(minHeight: 120, maxHeight: .infinity) + .frame(height: textHeight) + .onChange(of: editableTextField) { + textHeight = Self._calculateHeight(text: pasteString) + } .focused($focusedField, equals: .pasteField) .toolbar { ToolbarItemGroup(placement: .keyboard) { @@ -360,12 +378,10 @@ struct AddTeamView: View { self.focusedField = .pasteField } Spacer() - FooterButtonView("effacer", role: .destructive) { + FooterButtonView("effacer le texte") { self.focusedField = nil self.editableTextField = "" self.pasteString = nil - self.createdPlayers.removeAll() - self.createdPlayerIds.removeAll() } } } @@ -379,11 +395,12 @@ struct AddTeamView: View { Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() } PlayerView(player: p).tag(p.id) + .environment(tournament) } } if let p = fetchPlayers.first(where: { $0.license == id }) { VStack(alignment: .leading, spacing: 0) { - if unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { + if pasteString != nil, unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() } ImportedPlayerView(player: p).tag(p.license!) @@ -437,7 +454,9 @@ struct AddTeamView: View { if let pasteString { - if fetchPlayers.isEmpty { + let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty }) + + if sortedPlayers.isEmpty { ContentUnavailableView { Label("Aucun résultat", systemImage: "person.2.slash") } description: { @@ -446,7 +465,11 @@ struct AddTeamView: View { RowButtonView("Créer un joueur non classé") { presentPlayerCreation = true } - + + RowButtonView("Chercher dans la base") { + presentPlayerSearch = true + } + RowButtonView("Effacer cette recherche") { self.pasteString = nil self.editableTextField = "" @@ -489,6 +512,7 @@ struct AddTeamView: View { fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] pasteString = first editableTextField = first + textHeight = Self._calculateHeight(text: first) autoSelect = true } } @@ -503,6 +527,7 @@ struct AddTeamView: View { Section { ForEach(sortedPlayers) { player in ImportedPlayerView(player: player).tag(player.license!) + //Text(player.getLastName() + " " + player.getFirstName()).tag(player.license!) } } header: { Text(sortedPlayers.count.formatted() + " résultat" + sortedPlayers.count.pluralSuffix) diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index baf9600..b0ed71a 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -51,7 +51,7 @@ struct TournamentRankView: View { Logger.error(error) } } - + //affiche l'onglet sur le site, car sur le broadcast c'est dispo automatiquement de toute façon Toggle(isOn: $tournament.publishRankings) { Text("Publier sur Padel Club") if let url = tournament.shareURL(.rankings) { From 0352fffc4dfd7b526a3f648f0d40f970c8739e52 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 23 Sep 2024 11:32:51 +0200 Subject: [PATCH 02/41] v1.0.12 --- 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 329d149..bc4d6c4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3132,7 +3132,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\""; @@ -3176,7 +3176,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; From 4c4e472c0968e42670b2b80eb2c7764481e8e7bd Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 24 Sep 2024 11:35:02 +0200 Subject: [PATCH 03/41] gs step wip --- PadelClub/Data/GroupStage.swift | 30 +++- PadelClub/Data/TeamRegistration.swift | 10 ++ PadelClub/Data/Tournament.swift | 62 ++++++-- PadelClub/Utils/PadelRule.swift | 24 +++ .../Event/TournamentConfiguratorView.swift | 2 +- .../Views/GroupStage/GroupStageView.swift | 4 +- .../GroupStage/GroupStagesSettingsView.swift | 21 +++ .../Views/GroupStage/GroupStagesView.swift | 13 +- .../Screen/TableStructureView.swift | 144 +++++++++++++----- .../Tournament/TournamentBuildView.swift | 6 + 10 files changed, 258 insertions(+), 58 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 32e07b2..ee20f67 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -24,6 +24,7 @@ final class GroupStage: ModelObject, Storable { private var format: MatchFormat? var startDate: Date? var name: String? + var step: Int = 0 var matchFormat: MatchFormat { get { @@ -34,13 +35,14 @@ final class GroupStage: ModelObject, Storable { } } - internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil) { + internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil, step: Int = 0) { self.tournament = tournament self.index = index self.size = size self.format = matchFormat self.startDate = startDate self.name = name + self.step = step } var tournamentStore: TournamentStore { @@ -61,7 +63,10 @@ final class GroupStage: ModelObject, Storable { // MARK: - func teamAt(groupStagePosition: Int) -> TeamRegistration? { - teams().first(where: { $0.groupStagePosition == groupStagePosition }) + if step > 0 { + return teams().first(where: { $0.groupStagePositionAtStep(step) == groupStagePosition }) + } + return teams().first(where: { $0.groupStagePosition == groupStagePosition }) } func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String { @@ -190,7 +195,7 @@ final class GroupStage: ModelObject, Storable { } func initialStartDate(forTeam team: TeamRegistration) -> Date? { - guard let groupStagePosition = team.groupStagePosition else { return nil } + guard let groupStagePosition = team.groupStagePositionAtStep(step) else { return nil } return matches(forGroupStagePosition: groupStagePosition).compactMap({ $0.startDate }).sorted().first ?? startDate } @@ -326,6 +331,9 @@ final class GroupStage: ModelObject, Storable { } func unsortedTeams() -> [TeamRegistration] { + if step > 0 { + return self.tournamentStore.groupStages.filter({ $0.step == step - 1 }).compactMap({ $0.teams(true)[safe: index] }) + } return self.tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } } @@ -397,6 +405,19 @@ final class GroupStage: ModelObject, Storable { self.tournamentStore.matches.deleteDependencies(matches) } + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(String.self, forKey: ._id) + tournament = try container.decode(String.self, forKey: ._tournament) + index = try container.decode(Int.self, forKey: ._index) + size = try container.decode(Int.self, forKey: ._size) + format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) + startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) + name = try container.decodeIfPresent(String.self, forKey: ._name) + step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0 + } + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -422,6 +443,8 @@ final class GroupStage: ModelObject, Storable { } else { try container.encodeNil(forKey: ._name) } + + try container.encode(step, forKey: ._step) } func insertOnServer() { @@ -442,6 +465,7 @@ extension GroupStage { case _format = "format" case _startDate = "startDate" case _name = "name" + case _step = "step" } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 1c90fef..3ed4dc1 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -511,6 +511,16 @@ final class TeamRegistration: ModelObject, Storable { return Store.main.findById(tournament) } + func groupStagePositionAtStep(_ step: Int) -> Int? { + guard let groupStagePosition else { return nil } + if step == 0 { + return groupStagePosition + } else if let groupStageObject = groupStageObject(), groupStageObject.hasEnded() { + return groupStageObject.index + } + return nil + } + enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index cab2239..160911a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -380,15 +380,11 @@ final class Tournament : ModelObject, Storable { return Array(self.tournamentStore.teamRegistrations) } - func groupStages() -> [GroupStage] { - let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id } + 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) } - func allGroupStages() -> [GroupStage] { - return Array(self.tournamentStore.groupStages) - } - func allRounds() -> [Round] { return Array(self.tournamentStore.rounds) } @@ -767,8 +763,8 @@ defer { closedRegistrationDate != nil } - func getActiveGroupStage() -> GroupStage? { - let groupStages = groupStages() + func getActiveGroupStage(atStep step: Int = 0) -> GroupStage? { + let groupStages = groupStages(atStep: step) return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first } @@ -1118,8 +1114,8 @@ defer { return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame } - func groupStagesMatches() -> [Match] { - return self.tournamentStore.matches.filter { $0.groupStage != nil } + func groupStagesMatches(atStep step: Int = 0) -> [Match] { + return groupStages(atStep: step).flatMap({ $0._matches() }) // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } @@ -1191,6 +1187,19 @@ defer { var teams: [Int: [String]] = [:] var ids: Set = Set() let rounds = rounds() + let lastStep = lastStep() + if rounds.isEmpty, lastStep > 0 { + let groupStages = groupStages(atStep: lastStep) + + for groupStage in groupStages { + for (teamIndex, team) in groupStage.teams(true).enumerated() { + teams[groupStage.index + 1 + teamIndex] = [team.id] + } + } + + return teams + } + let final = rounds.last?.playedMatches().last if let winner = final?.winningTeamId { teams[1] = [winner] @@ -1600,12 +1609,23 @@ defer { return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ") } - func deleteAndBuildEverything() { + func deleteAndBuildEverything(preset: PadelTournamentStructurePreset = .manual) { resetBracketPosition() deleteStructure() deleteGroupStages() - buildGroupStages() - buildBracket() + + switch preset { + case .manual: + buildGroupStages() + buildBracket() + case .doubleGroupStage: + buildGroupStages() + addNewGroupStageStep() + + qualifiedPerGroupStage = 0 + groupStageAdditionalQualified = 0 + + } } func buildGroupStages() { @@ -2021,6 +2041,22 @@ defer { return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1 } + func addNewGroupStageStep() { + let lastStep = lastStep() + 1 + for i in 0.. Int { + self.tournamentStore.groupStages.sorted(by: \.step).last?.step ?? 0 + } + // MARK: - func insertOnServer() throws { diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 7e6f922..af5177c 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1637,3 +1637,27 @@ enum AnimationType: Int, CaseIterable, Hashable, Identifiable { } } +enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + + case manual + case doubleGroupStage + + func localizedStructurePresetTitle() -> String { + switch self { + case .manual: + return "Défaut" + case .doubleGroupStage: + return "2 phases de poules" + } + } + + func localizedDescriptionStructurePresetTitle() -> String { + switch self { + case .manual: + return "24 équipes, 4 poules de 4, 1 qualifié par poule" + case .doubleGroupStage: + return "Poules qui enchaîne sur une autre phase de poule : les premiers de chaque se retrouve ensemble, puis les 2èmes, etc." + } + } +} diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index dfffa60..65ecfc3 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -40,7 +40,7 @@ struct TournamentConfigurationView: View { } Picker(selection: $tournament.federalCategory, label: Text("Catégorie")) { ForEach(TournamentCategory.allCases) { type in - Text(type.localizedLabel(.wide)).tag(type) + Text(type.localizedLabel(.title)).tag(type) } } Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) { diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 5b04f71..467a377 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -105,7 +105,7 @@ struct GroupStageView: View { var body: some View { ForEach(0..<(groupStage.size), id: \.self) { index in - if let team = _teamAt(atIndex: index), let groupStagePosition = team.groupStagePosition { + if let team = _teamAt(atIndex: index), let groupStagePosition = team.groupStagePositionAtStep(groupStage.step) { NavigationLink { GroupStageTeamView(groupStage: groupStage, team: team) .environment(self.tournament) @@ -137,7 +137,7 @@ struct GroupStageView: View { } } Spacer() - if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: scores?.first(where: { $0.team.groupStagePosition == groupStagePosition })) { + if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: scores?.first(where: { $0.team.groupStagePositionAtStep(groupStage.step) == groupStagePosition })) { VStack(alignment: .trailing) { HStack(spacing: 0.0) { Text(score.wins) diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index e0f29dc..adf1426 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -13,6 +13,7 @@ struct GroupStagesSettingsView: View { @Environment(Tournament.self) var tournament: Tournament @State private var generationDone: Bool = false + let step: Int var tournamentStore: TournamentStore { return self.tournament.tournamentStore @@ -96,6 +97,26 @@ struct GroupStagesSettingsView: View { } } } + + if tournament.lastStep() == 0, step == 0, tournament.rounds().isEmpty { + Section { + RowButtonView("Ajouter une phase de poule", role: .destructive) { + tournament.addNewGroupStageStep() + } + } + } else if step > 0 { + Section { + RowButtonView("Supprimer cette phase de poule", role: .destructive) { + let gs = tournament.groupStages(atStep: tournament.lastStep()) + do { + try tournament.tournamentStore.groupStages.delete(contentOfs: gs) + } catch { + Logger.error(error) + } + } + } + + } #if DEBUG Section { diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 7274171..6f3355d 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -12,6 +12,7 @@ struct GroupStagesView: View { @State var tournament: Tournament @State private var selectedDestination: GroupStageDestination? @EnvironmentObject var dataStore: DataStore + let step: Int enum GroupStageDestination: Selectable, Identifiable, Equatable { static func == (lhs: GroupStagesView.GroupStageDestination, rhs: GroupStagesView.GroupStageDestination) -> Bool { @@ -77,17 +78,18 @@ struct GroupStagesView: View { } var allMatches: [Match] { - tournament.groupStagesMatches() + tournament.groupStagesMatches(atStep: step) } - init(tournament: Tournament) { + init(tournament: Tournament, step: Int = 0) { self.tournament = tournament + self.step = step if tournament.shouldVerifyGroupStage { _selectedDestination = State(wrappedValue: nil) } else if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty { _selectedDestination = State(wrappedValue: nil) } else { - let gs = tournament.getActiveGroupStage() + let gs = tournament.getActiveGroupStage(atStep: step) if let gs { _selectedDestination = State(wrappedValue: .groupStage(gs)) } @@ -96,7 +98,7 @@ struct GroupStagesView: View { func allDestinations() -> [GroupStageDestination] { var allDestinations : [GroupStageDestination] = [.all(tournament)] - let groupStageDestinations : [GroupStageDestination] = tournament.groupStages().map { GroupStageDestination.groupStage($0) } + let groupStageDestinations : [GroupStageDestination] = tournament.groupStages(atStep: step).map { GroupStageDestination.groupStage($0) } if let loserBracket = tournament.groupStageLoserBracket() { allDestinations.insert(.loserBracket(loserBracket), at: 0) } @@ -158,10 +160,11 @@ struct GroupStagesView: View { case .loserBracket(let loserBracket): LoserBracketFromGroupStageView(loserBracket: loserBracket).id(loserBracket.id) case nil: - GroupStagesSettingsView() + GroupStagesSettingsView(step: step) .navigationTitle("Réglages") } } + .environment(tournament) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 6ca1e6c..b33820c 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -19,8 +19,9 @@ struct TableStructureView: View { @State private var qualifiedPerGroupStage: Int = 0 @State private var groupStageAdditionalQualified: Int = 0 @State private var updatedElements: Set = Set() + @State private var structurePreset: PadelTournamentStructurePreset = .manual @FocusState private var stepperFieldIsFocused: Bool - + var qualifiedFromGroupStage: Int { groupStageCount * qualifiedPerGroupStage } @@ -51,6 +52,37 @@ struct TableStructureView: View { @ViewBuilder var body: some View { List { + + if tournament.state() != .build { + Section { + Picker(selection: $structurePreset) { + ForEach(PadelTournamentStructurePreset.allCases) { preset in + Text(preset.localizedStructurePresetTitle()).tag(preset) + } + } label: { + Text("Préréglage") + } + } footer: { + Text(structurePreset.localizedDescriptionStructurePresetTitle()) + } + .onChange(of: structurePreset) { + switch structurePreset { + case .manual: + teamCount = 24 + groupStageCount = 4 + teamsPerGroupStage = 4 + qualifiedPerGroupStage = 1 + groupStageAdditionalQualified = 0 + case .doubleGroupStage: + teamCount = 9 + groupStageCount = 3 + teamsPerGroupStage = 3 + qualifiedPerGroupStage = 0 + groupStageAdditionalQualified = 0 + } + } + } + Section { LabeledContent { StepperView(count: $teamCount, minimum: 4, maximum: 128) @@ -73,33 +105,62 @@ struct TableStructureView: View { Text("Équipes par poule") } - LabeledContent { - StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) - } label: { - Text("Qualifiés par poule") - } - - if qualifiedPerGroupStage < teamsPerGroupStage - 1 { + if structurePreset == .manual { LabeledContent { - StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) + StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) } label: { - Text("Qualifiés supplémentaires") - Text(moreQualifiedLabel) + Text("Qualifiés par poule") } - .onChange(of: groupStageAdditionalQualified) { - if groupStageAdditionalQualified == groupStageCount { - qualifiedPerGroupStage += 1 - groupStageAdditionalQualified -= groupStageCount + + if qualifiedPerGroupStage < teamsPerGroupStage - 1 { + LabeledContent { + StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) + } label: { + Text("Qualifiés supplémentaires") + Text(moreQualifiedLabel) + } + .onChange(of: groupStageAdditionalQualified) { + if groupStageAdditionalQualified == groupStageCount { + qualifiedPerGroupStage += 1 + groupStageAdditionalQualified -= groupStageCount + } } } } if groupStageCount > 0 && teamsPerGroupStage > 0 { - LabeledContent { - let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 - Text(mp.formatted()) - } label: { - Text("Matchs à jouer par poule") + if structurePreset == .manual { + LabeledContent { + let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 + Text(mp.formatted()) + } label: { + Text("Matchs à jouer par poule") + } + } else { + LabeledContent { + let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 + Text(mp.formatted()) + } label: { + Text("Matchs à jouer par poule") + Text("Première phase") + } + + LabeledContent { + let mp = (groupStageCount * (groupStageCount - 1) / 2) + Text(mp.formatted()) + } label: { + Text("Matchs à jouer par poule") + Text("Deuxième phase") + } + + LabeledContent { + let mp = groupStageCount - 1 + teamsPerGroupStage - 1 + Text(mp.formatted()) + } label: { + Text("Matchs à jouer par équipe") + Text("Total") + } + } } } @@ -116,22 +177,30 @@ struct TableStructureView: View { } label: { Text("Équipes en poule") } + + if structurePreset == .manual { + + LabeledContent { + Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted()) + } label: { + Text("Équipes qualifiées de poule") + } + } + } + + if structurePreset == .manual { + LabeledContent { - Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted()) + let tsPure = max(teamCount - groupStageCount * teamsPerGroupStage, 0) + Text(tsPure.formatted()) } label: { - Text("Équipes qualifiées de poule") + Text("Nombre de têtes de série") + } + LabeledContent { + Text(tf.formatted()) + } label: { + Text("Équipes en tableau final") } - } - LabeledContent { - let tsPure = max(teamCount - groupStageCount * teamsPerGroupStage, 0) - Text(tsPure.formatted()) - } label: { - Text("Nombre de têtes de série") - } - LabeledContent { - Text(tf.formatted()) - } label: { - Text("Équipes en tableau final") } } @@ -154,6 +223,13 @@ struct TableStructureView: View { _save(rebuildEverything: true) } } + + Section { + RowButtonView("Remise-à-zéro", role: .destructive) { + tournament.deleteGroupStages() + tournament.deleteStructure() + } + } } } .focused($stepperFieldIsFocused) @@ -283,7 +359,7 @@ struct TableStructureView: View { tournament.groupStageAdditionalQualified = groupStageAdditionalQualified if rebuildEverything { - tournament.deleteAndBuildEverything() + tournament.deleteAndBuildEverything(preset: structurePreset) } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() tournament.buildGroupStages() diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index a11e055..75c2cdd 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -55,6 +55,12 @@ struct TournamentBuildView: View { } } + if tournament.groupStages(atStep: 1).isEmpty == false { + NavigationLink("Step 1") { + GroupStagesView(tournament: tournament, step: 1) + } + } + if tournament.rounds().isEmpty == false { NavigationLink(value: Screen.round) { LabeledContent { From 4f3aec3e19340046a1661827813aa9e573882c43 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 24 Sep 2024 15:56:33 +0200 Subject: [PATCH 04/41] v1.0.13 b3 --- PadelClub.xcodeproj/project.pbxproj | 26 +++--- PadelClub/Data/Tournament.swift | 8 +- PadelClub/Extensions/String+Extensions.swift | 4 + PadelClub/Utils/FileImportManager.swift | 53 +++++++++-- PadelClub/Utils/PadelRule.swift | 6 +- PadelClub/Utils/Tips.swift | 2 +- .../Cashier/Event/EventSettingsView.swift | 4 +- .../Event/TournamentConfiguratorView.swift | 2 +- .../Components/GroupStageSettingsView.swift | 1 + PadelClub/Views/Team/EditingTeamView.swift | 6 +- .../Views/Tournament/FileImportView.swift | 18 +++- .../Views/Tournament/Screen/AddTeamView.swift | 3 +- .../Tournament/Screen/BroadcastView.swift | 65 ++++++-------- .../TournamentGeneralSettingsView.swift | 40 ++++----- .../Screen/InscriptionManagerView.swift | 89 +++++++++++-------- .../Screen/TableStructureView.swift | 2 +- 16 files changed, 199 insertions(+), 130 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index bc4d6c4..4ee6389 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -743,6 +743,7 @@ FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.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 */; }; FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; }; FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; }; FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; }; @@ -2671,6 +2672,7 @@ FF4CBFF82C996C0600151637 /* TabItemModifier.swift in Sources */, FF4CBFF92C996C0600151637 /* DeferredViewModifier.swift in Sources */, FF4CBFFA2C996C0600151637 /* TournamentScheduleView.swift in Sources */, + FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */, FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */, FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFD2C996C0600151637 /* User.swift in Sources */, @@ -3132,7 +3134,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\""; @@ -3155,7 +3157,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.12; + MARKETING_VERSION = 1.0.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3176,7 +3178,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; @@ -3198,7 +3200,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.12; + MARKETING_VERSION = 1.0.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3291,7 +3293,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\""; @@ -3313,7 +3315,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.10; + MARKETING_VERSION = 1.0.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3333,7 +3335,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; @@ -3354,7 +3356,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.10; + MARKETING_VERSION = 1.0.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3375,7 +3377,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3397,7 +3399,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.9; + MARKETING_VERSION = 1.0.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3417,7 +3419,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3438,7 +3440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.9; + MARKETING_VERSION = 1.0.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index cab2239..f7a6da1 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1387,8 +1387,12 @@ defer { } func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String { - if tournamentLevel == .unlisted, displayStyle == .title, let name { - return name + if tournamentLevel == .unlisted, displayStyle == .title { + if let name { + return name + } else { + return tournamentLevel.localizedLabel(.title) + } } let title: String = [tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].joined(separator: " ") if displayStyle == .wide, let name { diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 98567e7..ee29135 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -13,6 +13,10 @@ extension String { return (self.count > length) ? self.prefix(length) + trailing : self } + func prefixTrimmed(_ length: Int) -> String { + String(trimmed.prefix(length)) + } + var trimmed: String { replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines) } diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index 7ee5ced..67f4fb2 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -434,27 +434,29 @@ class FileImportManager { let fetchRequest = ImportedPlayer.fetchRequest() let federalContext = PersistenceController.shared.localContainer.viewContext - let results: [TeamHolder] = lines.chunked(into: 2).map { team in + let results: [TeamHolder] = lines.chunked(byParameterAt: 1).map { team in var teamName: String? = nil let players = team.map { player in let data = player.components(separatedBy: separator) - let lastName : String = data[safe: 2]?.trimmed ?? "" - let firstName : String = data[safe: 3]?.trimmed ?? "" + let lastName : String = data[safe: 2]?.prefixTrimmed(50) ?? "" + let firstName : String = data[safe: 3]?.prefixTrimmed(50) ?? "" let sex: PlayerRegistration.PlayerSexType = data[safe: 0] == "f" ? PlayerRegistration.PlayerSexType.female : PlayerRegistration.PlayerSexType.male if data[safe: 1]?.trimmed != nil { teamName = data[safe: 1]?.trimmed } - let phoneNumber : String? = data[safe: 4]?.trimmed - let email : String? = data[safe: 5]?.trimmed + let phoneNumber : String? = data[safe: 4]?.trimmed.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).prefixTrimmed(15) + let email : String? = data[safe: 5]?.prefixTrimmed(50) let rank : Int? = data[safe: 6]?.trimmed.toInt() - let licenceId : String? = data[safe: 7]?.trimmed - let club : String? = data[safe: 8]?.trimmed + let licenceId : String? = data[safe: 7]?.prefixTrimmed(50) + let club : String? = data[safe: 8]?.prefixTrimmed(200) let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName) fetchRequest.predicate = predicate let found = try? federalContext.fetch(fetchRequest).first if let found, autoSearch { let player = PlayerRegistration(importedPlayer: found) player.setComputedRank(in: tournament) + player.email = email + player.phoneNumber = phoneNumber return player } else { let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email) @@ -465,8 +467,43 @@ class FileImportManager { } } - return TeamHolder(players: players, tournamentCategory: .men, tournamentAgeCategory: .senior, previousTeam: nil, name: teamName, tournament: tournament) + return TeamHolder(players: players, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: nil, name: teamName, tournament: tournament) } return results } } + +extension Array where Element == String { + /// Groups the array of CSV lines based on the same value at the specified column index. + /// If no key is found, it defaults to chunking the array into groups of 2 lines. + /// - Parameter index: The index of the CSV column to group by. + /// - Returns: An array of arrays, where each inner array contains lines grouped by the CSV parameter or by default chunks of 2. + func chunked(byParameterAt index: Int) -> [[String]] { + var groups: [String: [String]] = [:] + + for line in self { + let columns = line.split(separator: ";", omittingEmptySubsequences: false).map { String($0) } + if index < columns.count { + let key = columns[index] + + if groups[key] == nil { + groups[key] = [] + } + groups[key]?.append(line) + } else { + // Handle out-of-bounds by continuing + print("Warning: Index \(index) out of bounds for line: \(line)") + return [[]] + } + } + + // If no valid groups found, chunk into groups of 2 lines + if groups.isEmpty { + return self.chunked(into: 2) + } else { + // Append groups by parameter value, converting groups.values into an array of arrays + return groups.map { $0.value } + } + } +} + diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 7e6f922..8a069d4 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -837,7 +837,7 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable { case .men: switch displayStyle { case .title: - return "DH" + return "Hommes" case .wide: return "Hommes" case .short: @@ -846,7 +846,7 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable { case .women: switch displayStyle { case .title: - return "DD" + return "Dames" case .wide: return "Dames" case .short: @@ -855,7 +855,7 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable { case .mix: switch displayStyle { case .title: - return "MX" + return "Mixte" case .wide: return "Mixte" case .short: diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index ae4c3c8..1366b39 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -430,7 +430,7 @@ struct CreateAccountTip: Tip { Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte") //todo //Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus") - Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Jeter un oeil au site Padel Club") + Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Voir le site Padel Club") } enum ActionKey: String { diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index 58b5428..d2f7d9c 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -46,12 +46,14 @@ struct EventSettingsView: View { var body: some View { Form { Section { - TextField("Description de l'événement", text: $eventName, axis: .vertical) + TextField("Nom de l'événement", text: $eventName, axis: .vertical) .lineLimit(2) .keyboardType(.alphabet) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity) .focused($textFieldIsFocus) + } header: { + Text("Nom de l'événement") } footer: { if eventName.isEmpty == false { FooterButtonView("effacer le nom") { diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index dfffa60..65ecfc3 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -40,7 +40,7 @@ struct TournamentConfigurationView: View { } Picker(selection: $tournament.federalCategory, label: Text("Catégorie")) { ForEach(TournamentCategory.allCases) { type in - Text(type.localizedLabel(.wide)).tag(type) + Text(type.localizedLabel(.title)).tag(type) } } Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 948dfa6..6713281 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -37,6 +37,7 @@ struct GroupStageSettingsView: View { Section { TextField("Nom de la poule", text: $groupStageName) .keyboardType(.alphabet) + .submitLabel(.done) .frame(maxWidth: .infinity) .onSubmit { groupStageName = groupStageName.trimmed diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 7e5f4be..782e996 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -137,7 +137,9 @@ struct EditingTeamView: View { })) { Text("Forfait") } - + } + + Section { TextField("Nom de l'équipe", text: $name) .autocorrectionDisabled() .keyboardType(.alphabet) @@ -153,6 +155,8 @@ struct EditingTeamView: View { _save() } + } header: { + Text("Nom de l'équipe") } Section { diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index c6644ff..e9ef5d5 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -85,11 +85,18 @@ struct FileImportView: View { @State private var presentFormatHelperView: Bool = false @State private var validatedTournamentIds: Set = Set() + init(defaultFileProvider: FileImportManager.FileProvider = .frenchFederation) { + _fileProvider = .init(wrappedValue: defaultFileProvider) + } + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] { + if tournament.isAnimation() { + return teams.sorted(by: \.weight) + } return teams.filter { $0.tournamentCategory == tournament.tournamentCategory && $0.tournamentAgeCategory == tournament.federalTournamentAge }.sorted(by: \.weight) } @@ -309,10 +316,14 @@ struct FileImportView: View { LabeledContent { Text(_filteredTeams.count.formatted()) } label: { - Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") + if tournament.isAnimation() { + Text("Équipe\(_filteredTeams.count.pluralSuffix) détectée\(_filteredTeams.count.pluralSuffix)") + } else { + Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue.lowercased()) détectée\(_filteredTeams.count.pluralSuffix)") + } } } footer: { - if previousTeams.isEmpty == false { + if previousTeams.isEmpty == false, tournament.isAnimation() == false { Text("La liste ci-dessous n'est qu'une indication d'évolution par rapport au seul poids d'équipe. Cela ne tient pas compte des dates d'inscriptions, WCs et autres éléments.").foregroundStyle(.logoRed) } } @@ -535,6 +546,9 @@ struct FileImportView: View { Section { HStack { VStack(alignment: .leading) { + if let teamName = team.name { + Text(teamName).foregroundStyle(.secondary) + } ForEach(team.players.sorted(by: \.computedRank)) { Text($0.playerLabel()) } diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index cd0f484..005d7e1 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -258,6 +258,7 @@ struct AddTeamView: View { } private func _isDuplicate() -> Bool { + if tournament.isAnimation() { return false } let ids : [String?] = _currentSelectionIds() if tournament.selectedSortedTeams().anySatisfy({ $0.containsExactlyPlayerLicenses(ids) }) { return true @@ -391,7 +392,7 @@ struct AddTeamView: View { ForEach(createdPlayerIds.sorted(), id: \.self) { id in if let p = createdPlayers.first(where: { $0.id == id }) { VStack(alignment: .leading, spacing: 0) { - if let player = unsortedPlayers.first(where: { $0.licenceId == p.licenceId }), editedTeam?.includes(player: player) == false { + if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == true { Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() } PlayerView(player: p).tag(p.id) diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index 661f69e..2ded795 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -42,7 +42,7 @@ struct BroadcastView: View { navigation.selectedTab = .umpire } - RowButtonView("Jeter un oeil au site Padel Club") { + RowButtonView("Voir le site Padel Club") { UIApplication.shared.open(URLs.main.url) } } @@ -104,12 +104,7 @@ struct BroadcastView: View { Section { Toggle(isOn: $tournament.isPrivate) { Text("Tournoi privé") - if (tournament.isPrivate && Guard.main.purchasedTransactions.isEmpty) { - Text("Vous devez disposer d'une offre pour rendre publique ce tournoi.") - .foregroundStyle(.logoRed) - } } - .disabled(_disablePrivateToggle()) } footer: { let verb : String = tournament.isPrivate ? "est" : "sera" let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))" @@ -273,32 +268,34 @@ struct BroadcastView: View { } } .toolbar(content: { - ToolbarItem(placement: .topBarTrailing) { - Menu { - Section { - let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings, .broadcast, .clubBroadcast] - Picker(selection: $pageLink) { - ForEach(links) { pageLink in - Text(pageLink.localizedLabel()).tag(pageLink) + if StoreCenter.main.userId != nil, tournament.isPrivate == false, tournament.club() != nil { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Section { + let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings, .broadcast, .clubBroadcast] + Picker(selection: $pageLink) { + ForEach(links) { pageLink in + Text(pageLink.localizedLabel()).tag(pageLink) + } + } label: { + Text("Choisir la page à partager") } - } label: { - Text("Choisir la page à partager") + .pickerStyle(.menu) + actionForURL(title: "Partager la page '" + pageLink.localizedLabel() + "'", url: tournament.shareURL(pageLink)) + } header: { + Text("Lien du tournoi à partager") } - .pickerStyle(.menu) - actionForURL(title: "Partager la page '" + pageLink.localizedLabel() + "'", url: tournament.shareURL(pageLink)) - } header: { - Text("Lien du tournoi à partager") - } - - Section { - let club = tournament.club() - actionForURL(title: (club == nil) ? "Aucun club indiqué pour ce tournoi" : club!.clubTitle(), description: "Page du club", url: club?.shareURL()) - actionForURL(title: "Padel Club", url: URLs.main.url) - } header: { - Text("Autres liens") + + Section { + let club = tournament.club() + actionForURL(title: (club == nil) ? "Aucun club indiqué pour ce tournoi" : club!.clubTitle(), description: "Page du club", url: club?.shareURL()) + actionForURL(title: "Padel Club", url: URLs.main.url) + } header: { + Text("Autres liens") + } + } label: { + Label("Partager les liens", systemImage: "square.and.arrow.up") } - } label: { - Label("Partager les liens", systemImage: "square.and.arrow.up") } } }) @@ -320,15 +317,7 @@ struct BroadcastView: View { _save() } } - - private func _disablePrivateToggle() -> Bool { - #if DEBUG - return false - #else - return (tournament.isPrivate && Guard.main.purchasedTransactions.isEmpty) - #endif - } - + private func _save() { do { if [tournament.publishTeams, tournament.publishSummons, tournament.publishBrackets, tournament.publishGroupStages].anySatisfy({ $0 == true }) { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 35afb65..49de144 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -28,6 +28,26 @@ struct TournamentGeneralSettingsView: View { Section { TournamentDatePickerView() TournamentDurationManagerView() + LabeledContent { + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($focusedField, equals: ._entryFee) + } label: { + Text("Inscription") + } + + } + + Section { + TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) + .lineLimit(2) + .frame(maxWidth: .infinity) + .keyboardType(.alphabet) + .focused($focusedField, equals: ._name) + } header: { + Text("Nom du tournoi") } Section { @@ -70,26 +90,6 @@ struct TournamentGeneralSettingsView: View { Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) } } - - Section { - LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) - .keyboardType(.decimalPad) - .multilineTextAlignment(.trailing) - .frame(maxWidth: .infinity) - .focused($focusedField, equals: ._entryFee) - } label: { - Text("Inscription") - } - } - - Section { - TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) - .lineLimit(2) - .frame(maxWidth: .infinity) - .keyboardType(.alphabet) - .focused($focusedField, equals: ._name) - } } .toolbarBackground(.visible, for: .navigationBar) .toolbar { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 353fd7e..16ddaf8 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -231,7 +231,7 @@ struct InscriptionManagerView: View { _setHash() }) { NavigationStack { - FileImportView() + FileImportView(defaultFileProvider: tournament.isAnimation() ? .custom : .frenchFederation) } .tint(.master) } @@ -308,44 +308,52 @@ struct InscriptionManagerView: View { .symbolVariant(filterMode == .all ? .none : .fill) } Menu { - if tournament.inscriptionClosed() == false { - Menu { - _sortingTypePickerView() - } label: { - Text("Méthode de sélection") - Text(tournament.teamSorting.localizedLabel()) - } - Divider() - rankingDateSourcePickerView(showDateInLabel: true) - - Divider() - Button { - tournament.lockRegistration() - _save() - } label: { - Label("Clôturer", systemImage: "lock") - } - Divider() - _sharingTeamsMenuView() - Button { - presentImportView = true - } label: { - Label("Importer beach-padel", systemImage: "square.and.arrow.down") - } - Link(destination: URLs.beachPadel.url) { - Label("beach-padel.app.fft.fr", systemImage: "safari") + if tournament.isAnimation() == false { + if tournament.inscriptionClosed() == false { + Menu { + _sortingTypePickerView() + } label: { + Text("Méthode de sélection") + Text(tournament.teamSorting.localizedLabel()) + } + Divider() + rankingDateSourcePickerView(showDateInLabel: true) + + Divider() + Button { + tournament.lockRegistration() + _save() + } label: { + Label("Clôturer", systemImage: "lock") + } + Divider() + _sharingTeamsMenuView() + Button { + presentImportView = true + } label: { + Label("Importer beach-padel", systemImage: "square.and.arrow.down") + } + Link(destination: URLs.beachPadel.url) { + Label("beach-padel.app.fft.fr", systemImage: "safari") + } + } else { + + _sharingTeamsMenuView() + + Divider() + + Button { + tournament.unlockRegistration() + _save() + } label: { + Label("Ré-ouvrir", systemImage: "lock.open") + } } } else { - - _sharingTeamsMenuView() - - Divider() - Button { - tournament.unlockRegistration() - _save() + presentImportView = true } label: { - Label("Ré-ouvrir", systemImage: "lock.open") + Label("Importer un fichier", systemImage: "square.and.arrow.down") } } } label: { @@ -438,8 +446,11 @@ struct InscriptionManagerView: View { if presentSearch == false { _informationView() - _rankHandlerView() - _relatedTips() + + if tournament.isAnimation() == false { + _rankHandlerView() + _relatedTips() + } } let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) @@ -650,7 +661,7 @@ struct InscriptionManagerView: View { .listRowSeparator(.hidden) let registrationIssues = tournament.registrationIssues() - if registrationIssues > 0 { + if tournament.isAnimation() == false, registrationIssues > 0 { NavigationLink { InscriptionInfoView() .environment(tournament) @@ -660,7 +671,7 @@ struct InscriptionManagerView: View { .foregroundStyle(.logoRed) .fontWeight(.bold) } label: { - Text("Problèmes détéctés") + Text("Problèmes détectés") } } } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 6ca1e6c..460fd28 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -74,7 +74,7 @@ struct TableStructureView: View { } LabeledContent { - StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) + StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) } label: { Text("Qualifiés par poule") } From af9f9a1747b765bf57a479bd62afba81aa356b00 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 24 Sep 2024 16:01:19 +0200 Subject: [PATCH 05/41] fix animation / pbl stuff --- 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 4ee6389..bf9c5ae 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From 32d81a01e803c234b64390b568def9f37786140d Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 24 Sep 2024 17:01:34 +0200 Subject: [PATCH 06/41] fix minor stuff --- PadelClub/Utils/FileImportManager.swift | 3 +-- PadelClub/Views/Tournament/FileImportView.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index 67f4fb2..6e553ef 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -444,7 +444,7 @@ class FileImportManager { if data[safe: 1]?.trimmed != nil { teamName = data[safe: 1]?.trimmed } - let phoneNumber : String? = data[safe: 4]?.trimmed.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).prefixTrimmed(15) + let phoneNumber : String? = data[safe: 4]?.trimmed.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).prefixTrimmed(50) let email : String? = data[safe: 5]?.prefixTrimmed(50) let rank : Int? = data[safe: 6]?.trimmed.toInt() let licenceId : String? = data[safe: 7]?.prefixTrimmed(50) @@ -493,7 +493,6 @@ extension Array where Element == String { } else { // Handle out-of-bounds by continuing print("Warning: Index \(index) out of bounds for line: \(line)") - return [[]] } } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index e9ef5d5..99bf6b9 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -319,7 +319,7 @@ struct FileImportView: View { if tournament.isAnimation() { Text("Équipe\(_filteredTeams.count.pluralSuffix) détectée\(_filteredTeams.count.pluralSuffix)") } else { - Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue.lowercased()) détectée\(_filteredTeams.count.pluralSuffix)") + Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue.lowercased()) détectée\(_filteredTeams.count.pluralSuffix)") } } } footer: { From 62e2b598754d3be1178643defc34c3d7ef243f71 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 24 Sep 2024 17:31:31 +0200 Subject: [PATCH 07/41] fix some stuff --- PadelClub/Data/Tournament.swift | 16 +++++++++++++--- PadelClub/Utils/FileImportManager.swift | 2 ++ .../Views/Tournament/Screen/AddTeamView.swift | 8 ++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index f7a6da1..54cac3d 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1067,6 +1067,11 @@ defer { registrationDate = previousTeamRegistrationDate } let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name) + if isAnimation() { + if newTeam.weight == 0 { + newTeam.weight = team.index(in: teams) ?? 0 + } + } teamsToImport.append(newTeam) } } @@ -1795,6 +1800,11 @@ defer { players.forEach { player in player.teamRegistration = team.id } + if isAnimation() { + if team.weight == 0 { + team.weight = unsortedTeams().count + } + } return team } @@ -1910,9 +1920,9 @@ defer { private func _defaultSorting() -> [MySortDescriptor] { switch teamSorting { case .rank: - [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!)] + [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.id)] case .inscriptionDate: - [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight)] + [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] } } @@ -1922,7 +1932,7 @@ defer { && federalTournamentAge == build.age } - private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.registrationDate!)] + private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.id)] private func _matchSchedulers() -> [MatchScheduler] { return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id } diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index 6e553ef..f61f0dd 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -462,6 +462,8 @@ class FileImportManager { let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email) if rank == nil, autoSearch { player.setComputedRank(in: tournament) + } else { + player.computedRank = rank ?? 0 } return player } diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 005d7e1..ca7ad1e 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -149,7 +149,7 @@ struct AddTeamView: View { } .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Terminer", role: .cancel) { + Button("Annuler", role: .cancel) { dismiss() } } @@ -392,8 +392,8 @@ struct AddTeamView: View { ForEach(createdPlayerIds.sorted(), id: \.self) { id in if let p = createdPlayers.first(where: { $0.id == id }) { VStack(alignment: .leading, spacing: 0) { - if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == true { - Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == false { + Text("Déjà inscrit !!").foregroundStyle(.logoRed).bold() } PlayerView(player: p).tag(p.id) .environment(tournament) @@ -421,7 +421,7 @@ struct AddTeamView: View { } else { RowButtonView("Confirmer") { _updateTeam(checkDuplicates: false) - editedTeam = nil + dismiss() } } } header: { From 0aed24a00329e4ddc85036a2149c23603af8c5ce Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 24 Sep 2024 17:49:52 +0200 Subject: [PATCH 08/41] appstore release --- 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 bf9c5ae..39b08b9 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From daec6373015bacd049852ad3705ccc727a53d0e2 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 24 Sep 2024 19:53:40 +0200 Subject: [PATCH 09/41] v1.0.13 b4 --- 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 39b08b9..06e2066 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3178,7 +3178,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 4; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From 913d3de4f5d19de0aec077ac02055f11f5016929 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 25 Sep 2024 07:39:30 +0200 Subject: [PATCH 10/41] fix textfield cancel / validate UI stuff --- PadelClub/Data/Tournament.swift | 8 ++++---- PadelClub/Utils/HtmlGenerator.swift | 2 +- PadelClub/Utils/PadelRule.swift | 10 +++++----- PadelClub/ViewModel/FederalDataViewModel.swift | 2 +- .../Calling/CallMessageCustomizationView.swift | 10 ++++++++++ .../Views/Cashier/Event/EventSettingsView.swift | 12 +++++++++++- .../Cashier/Event/TournamentConfiguratorView.swift | 2 +- PadelClub/Views/Club/ClubDetailView.swift | 10 ++++++++++ PadelClub/Views/Club/CourtView.swift | 12 ++++++++++++ .../Components/GroupStageSettingsView.swift | 14 +++++++++++++- .../Navigation/Agenda/TournamentLookUpView.swift | 4 ++-- .../Navigation/Toolbox/RankCalculatorView.swift | 4 ++-- PadelClub/Views/Player/PlayerDetailView.swift | 10 ++++++++++ PadelClub/Views/Shared/TournamentFilterView.swift | 2 +- PadelClub/Views/Team/EditingTeamView.swift | 14 +++++++++++++- .../Components/TournamentGeneralSettingsView.swift | 10 ++++++++++ .../Components/TournamentLevelPickerView.swift | 2 +- .../Tournament/Screen/TableStructureView.swift | 2 ++ .../Tournament/Shared/TournamentCellView.swift | 2 +- 19 files changed, 110 insertions(+), 22 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 54cac3d..deb4f64 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1396,10 +1396,10 @@ defer { if let name { return name } else { - return tournamentLevel.localizedLabel(.title) + return tournamentLevel.localizedLevelLabel(.title) } } - let title: String = [tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].joined(separator: " ") + let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") if displayStyle == .wide, let name { return [title, name].joined(separator: " - ") } else { @@ -1410,9 +1410,9 @@ defer { func localizedTournamentType() -> String { switch tournamentLevel { case .unlisted: - return tournamentLevel.localizedLabel(.short) + return tournamentLevel.localizedLevelLabel(.short) default: - return tournamentLevel.localizedLabel(.short) + tournamentCategory.localizedLabel(.short) + return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedLabel(.short) } } diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift index af72534..4139f67 100644 --- a/PadelClub/Utils/HtmlGenerator.swift +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -186,7 +186,7 @@ class HtmlGenerator: ObservableObject { .day() .dateSeparator(.dash)) - let name = tournament.tournamentLevel.localizedLabel() + "-" + tournament.tournamentCategory.importingRawValue + let name = tournament.tournamentLevel.localizedLevelLabel() + "-" + tournament.tournamentCategory.importingRawValue return pdfFolderURL.appendingPathComponent(stringDate + "-" + name + ".pdf") } diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 8a069d4..62a1889 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -48,7 +48,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } var identifier: String { - level.localizedLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() + level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() } var computedLabel: String { @@ -57,11 +57,11 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { - level.localizedLabel() + category.localizedLabel(.short) + level.localizedLevelLabel() + category.localizedLabel(.short) } var localizedTitle: String { - level.localizedLabel() + " " + category.localizedLabel() + level.localizedLevelLabel() + " " + category.localizedLabel() } var localizedAge: String { @@ -72,7 +72,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { extension TournamentBuild { init?(category: String, level: String, age: FederalTournamentAge = .senior) { - guard let levelFound = TournamentLevel.allCases.first(where: { $0.localizedLabel() == level }) else { return nil } + guard let levelFound = TournamentLevel.allCases.first(where: { $0.localizedLevelLabel() == level }) else { return nil } var c = category if c.hasPrefix("ME") { @@ -465,7 +465,7 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { } } - func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + func localizedLevelLabel(_ displayStyle: DisplayStyle = .wide) -> String { if self == .unlisted { return displayStyle == .title ? "Animation" : "Anim." } return String(describing: self).capitalized } diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 010b211..d4d6d24 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -25,7 +25,7 @@ class FederalDataViewModel { func filterStatus() -> String { var labels: [String] = [] - labels.append(contentsOf: levels.map { $0.localizedLabel() }.formatList()) + labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList()) labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList()) labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList()) let clubNames = selectedClubs.compactMap { codeClub in diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 93a1779..569f420 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -95,6 +95,16 @@ struct CallMessageCustomizationView: View { } .headerProminence(.increased) .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Message de convocation") .toolbar { diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index d2f7d9c..cad0f0a 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -27,7 +27,7 @@ struct EventSettingsView: View { link.append(tournaments.compactMap({ tournament in if let url = tournament.shareURL(pageLink) { var tournamentLink = [String]() - tournamentLink.append(tournament.tournamentTitle()) + tournamentLink.append(tournament.tournamentTitle(.title)) tournamentLink.append(url.absoluteString) return tournamentLink.joined(separator: "\n") } else { @@ -87,6 +87,16 @@ struct EventSettingsView: View { } } } + .navigationBarBackButtonHidden(textFieldIsFocus) + .toolbar(content: { + if textFieldIsFocus { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + textFieldIsFocus = false + } + } + } + }) .toolbar { if textFieldIsFocus { ToolbarItem(placement: .keyboard) { diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index 65ecfc3..68483c3 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -22,7 +22,7 @@ struct TournamentConfigurationView: View { var body: some View { Picker(selection: $tournament.federalLevelCategory, label: Text("Niveau")) { ForEach(TournamentLevel.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedLevelLabel(.title)).tag(type) } } .onChange(of: tournament.federalLevelCategory) { diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index cb3b23b..b04b4f0 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -213,6 +213,16 @@ struct ClubDetailView: View { } } } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) .keyboardType(.alphabet) .autocorrectionDisabled() .defaultFocus($focusedField, ._name, priority: .automatic) diff --git a/PadelClub/Views/Club/CourtView.swift b/PadelClub/Views/Club/CourtView.swift index 8986cc4..c4e89f8 100644 --- a/PadelClub/Views/Club/CourtView.swift +++ b/PadelClub/Views/Club/CourtView.swift @@ -12,6 +12,7 @@ struct CourtView: View { @EnvironmentObject var dataStore: DataStore @Bindable var court: Court @State private var name: String = "" + @FocusState var focusedField: Court.CodingKeys? init(court: Court) { self.court = court @@ -23,6 +24,7 @@ struct CourtView: View { Section { LabeledContent { TextField("Nom", text: $name) + .focused($focusedField, equals: ._name) .autocorrectionDisabled() .keyboardType(.alphabet) .multilineTextAlignment(.trailing) @@ -71,6 +73,16 @@ struct CourtView: View { Logger.error(error) } } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) .navigationTitle(court.courtTitle()) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 6713281..64abb0f 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -20,7 +20,8 @@ struct GroupStageSettingsView: View { @State private var presentConfirmationButton: Bool = false @State private var size: Int @State private var courtIndex: Int - + @FocusState var focusedField: GroupStage.CodingKeys? + init(groupStage: GroupStage) { _groupStage = Bindable(groupStage) _groupStageName = .init(wrappedValue: groupStage.name ?? "") @@ -37,6 +38,7 @@ struct GroupStageSettingsView: View { Section { TextField("Nom de la poule", text: $groupStageName) .keyboardType(.alphabet) + .focused($focusedField, equals: ._name) .submitLabel(.done) .frame(maxWidth: .infinity) .onSubmit { @@ -153,6 +155,16 @@ struct GroupStageSettingsView: View { presentConfirmationButton = true } } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) .navigationTitle("Paramètres") .toolbarBackground(.visible, for: .navigationBar) } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 000ba3f..ede4f2d 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -335,7 +335,7 @@ struct TournamentLookUpView: View { NavigationLink { List([TournamentLevel.p25, TournamentLevel.p100, TournamentLevel.p250, TournamentLevel.p500, TournamentLevel.p1000, TournamentLevel.p1500, TournamentLevel.p2000], selection: $appSettings.tournamentLevels) { type in - Text(type.localizedLabel()) + Text(type.localizedLevelLabel()) } .navigationTitle("Niveaux") .environment(\.editMode, Binding.constant(EditMode.active)) @@ -413,7 +413,7 @@ struct TournamentLookUpView: View { if dataStore.appSettings.tournamentLevels.isEmpty || dataStore.appSettings.tournamentLevels.count == TournamentLevel.allCases.count { Text("Tous les niveaux") } else { - Text(levels.map({ $0.localizedLabel() }).joined(separator: ", ")) + Text(levels.map({ $0.localizedLevelLabel() }).joined(separator: ", ")) } } diff --git a/PadelClub/Views/Navigation/Toolbox/RankCalculatorView.swift b/PadelClub/Views/Navigation/Toolbox/RankCalculatorView.swift index 0ebd1bf..28b4050 100644 --- a/PadelClub/Views/Navigation/Toolbox/RankCalculatorView.swift +++ b/PadelClub/Views/Navigation/Toolbox/RankCalculatorView.swift @@ -17,7 +17,7 @@ struct RankCalculatorView: View { Section { HStack { let ordinal = NumberFormatter.ordinal.string(from: NSNumber(value:rank))! - Text("\(ordinal) d'un \(tournamentLevel.localizedLabel()) de \(count.localizedLabel()) équipes:") + Text("\(ordinal) d'un \(tournamentLevel.localizedLevelLabel()) de \(count.localizedLabel()) équipes:") Spacer() Text(tournamentLevel.points(for: rank-1, count: count.rawValue).formatted(.number.sign(strategy: .always()))) } @@ -25,7 +25,7 @@ struct RankCalculatorView: View { Section { Picker(selection: $tournamentLevel) { ForEach(TournamentLevel.allCases) { level in - Text(level.localizedLabel()).tag(level) + Text(level.localizedLevelLabel()).tag(level) } } label: { Label("Niveau", systemImage: "gauge.medium") diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 3d65b72..e5a203c 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -179,6 +179,16 @@ struct PlayerDetailView: View { player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory) _save() } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) .headerProminence(.increased) .navigationTitle("Édition") .navigationBarTitleDisplayMode(.inline) diff --git a/PadelClub/Views/Shared/TournamentFilterView.swift b/PadelClub/Views/Shared/TournamentFilterView.swift index 8e84a0b..6af4d7c 100644 --- a/PadelClub/Views/Shared/TournamentFilterView.swift +++ b/PadelClub/Views/Shared/TournamentFilterView.swift @@ -63,7 +63,7 @@ struct TournamentFilterView: View { } } } label: { - Text(level.localizedLabel(.title)) + Text(level.localizedLevelLabel(.title)) } } } header: { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 782e996..e60c949 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -21,7 +21,8 @@ struct EditingTeamView: View { @State private var registrationDate : Date @State private var callDate : Date @State private var name: String - + @FocusState private var focusedField: TeamRegistration.CodingKeys? + var messageSentFailed: Binding { Binding { sentError != nil @@ -142,6 +143,7 @@ struct EditingTeamView: View { Section { TextField("Nom de l'équipe", text: $name) .autocorrectionDisabled() + .focused($focusedField, equals: ._name) .keyboardType(.alphabet) .frame(maxWidth: .infinity) .submitLabel(.done) @@ -186,6 +188,16 @@ struct EditingTeamView: View { } } } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) .alert("Un problème est survenu", isPresented: messageSentFailed) { Button("OK") { } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 49de144..1258938 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -91,6 +91,16 @@ struct TournamentGeneralSettingsView: View { } } } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) .toolbarBackground(.visible, for: .navigationBar) .toolbar { if focusedField != nil { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift index 5550b66..4e3bc69 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift @@ -15,7 +15,7 @@ struct TournamentLevelPickerView: View { Picker(selection: $tournament.tournamentLevel, label: Text("Niveau")) { ForEach(TournamentLevel.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedLevelLabel(.title)).tag(type) } } .onChange(of: tournament.federalLevelCategory) { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 460fd28..d3ec145 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -62,6 +62,8 @@ struct TableStructureView: View { } label: { Text("Nombre de poules") } + } footer: { + Text("Vous pourrez modifier la taille de vos poules de manière spécifique dans l'écran des poules.") } if groupStageCount > 0 { diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 1e9ab62..94a7353 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -88,7 +88,7 @@ struct TournamentCellView: View { .font(.caption) } HStack(alignment: .bottom) { - Text(build.level.localizedLabel()) + Text(build.level.localizedLevelLabel()) .fontWeight(.semibold) if displayStyle == .wide { VStack(alignment: .leading, spacing: 0) { From 0a816b20e15b4178fdaba52fa60952ec29549d0c Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 25 Sep 2024 11:27:39 +0200 Subject: [PATCH 11/41] minor updates --- PadelClub/Data/PlayerRegistration.swift | 5 ++ PadelClub/Data/Tournament.swift | 4 +- PadelClub/PadelClubApp.swift | 2 +- PadelClub/Utils/LocationManager.swift | 15 ++++- PadelClub/Utils/Tips.swift | 23 +++++++ PadelClub/ViewModel/AgendaDestination.swift | 10 ++++ PadelClub/ViewModel/Selectable.swift | 33 ++++++++++ .../GenericDestinationPickerView.swift | 4 ++ .../Agenda/TournamentLookUpView.swift | 60 +++++++++++-------- .../Views/Planning/PlanningSettingsView.swift | 2 +- .../Views/Tournament/TournamentView.swift | 2 +- 11 files changed, 128 insertions(+), 32 deletions(-) diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index da256cf..28c1cff 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -283,6 +283,11 @@ final class PlayerRegistration: ModelObject, Storable { } func setComputedRank(in tournament: Tournament) { + if tournament.isAnimation() { + computedRank = rank ?? 0 + return + } + let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 70_000 switch tournament.tournamentCategory { case .men: diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index deb4f64..bff7b30 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1430,7 +1430,9 @@ defer { func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { - case .wide, .title: + case .title: + startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year()) + case .wide: startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted) case .short: startDate.formatted(date: .numeric, time: .omitted) diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index befb5d3..ccf8660 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -100,7 +100,7 @@ print("Running in Release mode") //try? Tips.resetDatastore() try? Tips.configure([ - .displayFrequency(.immediate), + .displayFrequency(.daily), .datastoreLocation(.applicationDefault) ]) } diff --git a/PadelClub/Utils/LocationManager.swift b/PadelClub/Utils/LocationManager.swift index 3961bf6..0af0e36 100644 --- a/PadelClub/Utils/LocationManager.swift +++ b/PadelClub/Utils/LocationManager.swift @@ -16,7 +16,18 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { @Published var postalCode: String? @Published var requestStarted: Bool = false @Published var userReadableCityOrZipcode: String = "" - @Published var lastError: Error? = nil + @Published var lastError: LocalizedError? = nil + + enum LocationError: LocalizedError { + case unknownError(error: Error) + + var errorDescription: String? { + switch self { + case .unknownError(let error): + return "Padel Club n'a pas réussi à vous localiser." + } + } + } override init() { super.init() @@ -49,7 +60,7 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("locationManager didFailWithError", error) requestStarted = false - self.lastError = error + self.lastError = LocationError.unknownError(error: error) } func geocodeCity(cityOrZipcode: String, completion: @escaping (_ placemark: [CLPlacemark]?, _ error: Error?) -> Void) { diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index 1366b39..bb136dd 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -549,6 +549,29 @@ struct TeamsExportTip: Tip { } } +struct PlayerTournamentSearchTip: Tip { + var title: Text { + Text("Cherchez un tournoi autour de vous !") + } + + var message: Text? { + Text("Padel Club facilite la recherche de tournois et l'inscription !") + } + + var image: Image? { + Image(systemName: "trophy.circle") + } + + var actions: [Action] { + Action(id: ActionKey.selectAction.rawValue, title: "Éssayer") + } + + enum ActionKey: String { + case selectAction = "selectAction" + } + +} + struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index abe2126..96aff9d 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import TipKit enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { var id: Int { self.rawValue } @@ -33,6 +34,15 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { } } + func associatedTip() -> (any Tip)? { + switch self { + case .around: + return PlayerTournamentSearchTip() + default: + return nil + } + } + func selectionLabel(index: Int) -> String { localizedTitleKey } diff --git a/PadelClub/ViewModel/Selectable.swift b/PadelClub/ViewModel/Selectable.swift index 6734828..66309f9 100644 --- a/PadelClub/ViewModel/Selectable.swift +++ b/PadelClub/ViewModel/Selectable.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import TipKit protocol Selectable { func selectionLabel(index: Int) -> String @@ -15,9 +16,14 @@ protocol Selectable { func badgeValueColor() -> Color? func displayImageIfValueZero() -> Bool func systemImage() -> String? + func associatedTip() -> (any Tip)? } extension Selectable { + func associatedTip() -> (any Tip)? { + return nil + } + func systemImage() -> String? { return nil } @@ -54,3 +60,30 @@ enum Badge { } } } + +struct SelectionTipViewModifier: ViewModifier { + let selectable: Selectable + let action: () -> Void + func body(content: Content) -> some View { + if let tip = selectable.associatedTip() { + if #available(iOS 18.0, *) { + content + .popoverTip(tip, arrowEdge: .top) { _ in + action() + tip.invalidate(reason: .tipClosed) + } + } else { + content + } + } else { + content + } + } +} + +extension View { + func selectableTipViewModifier(selectable: Selectable, action: @escaping () -> Void) -> some View { + modifier(SelectionTipViewModifier(selectable: selectable, action: action)) + } +} + diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index a7f4871..59079c5 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import TipKit struct GenericDestinationPickerView: View { @EnvironmentObject var dataStore: DataStore @@ -49,6 +50,9 @@ struct GenericDestinationPickerView: .contentShape(Capsule()) } } + .selectableTipViewModifier(selectable: destination) { + selectedDestination = destination + } .padding() .background { Capsule() diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index ede4f2d..2426cf5 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -23,15 +23,44 @@ struct TournamentLookUpView: View { @State private var requestedToGetAllPages: Bool = false @State private var revealSearchParameters: Bool = true @State private var presentAlert: Bool = false + @State private var confirmSearch: Bool = false var tournaments: [FederalTournament] { federalDataViewModel.searchedFederalTournaments } + + var showLastError: Binding { + Binding { + locationManager.lastError != nil + } set: { value in + if value == false { + locationManager.lastError = nil + } + } + + } var body: some View { List { searchParametersView } + .alert(isPresented: showLastError, error: locationManager.lastError as? LocationManager.LocationError, actions: { + Button("Annuler", role: .cancel) { + + } + }) + .confirmationDialog("Attention", isPresented: $confirmSearch, titleVisibility: .visible) { + Button("Cherchez quand même") { + requestedToGetAllPages = true + runSearch() + } + + Button("Annuler", role: .cancel) { + + } + } message: { + Text("Aucune ville n'a été indiqué, il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.") + } .alert("Attention", isPresented: $presentAlert, actions: { Button { presentAlert = false @@ -70,7 +99,11 @@ struct TournamentLookUpView: View { ToolbarItem(placement: .bottomBar) { if revealSearchParameters { FooterButtonView("Lancer la recherche") { - runSearch() + if dataStore.appSettings.city.isEmpty { + confirmSearch = true + } else { + runSearch() + } } .disabled(searching) } else if searching { @@ -230,31 +263,6 @@ struct TournamentLookUpView: View { } } - @ViewBuilder - var searchContollerView: some View { - Section { - Button { - runSearch() - } label: { - HStack { - Label("Chercher un tournoi", systemImage: "magnifyingglass") - if searching { - Spacer() - ProgressView() - } - } - } - Button { - dataStore.appSettings.resetSearch() - locationManager.location = nil - locationManager.city = nil - revealSearchParameters = true - } label: { - Label("Ré-initialiser la recherche", systemImage: "xmark.circle") - } - } - } - @ViewBuilder var searchParametersView: some View { @Bindable var appSettings = dataStore.appSettings diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 1286f4d..645faf0 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -52,7 +52,7 @@ struct PlanningSettingsView: View { Section { DatePicker(selection: $tournament.startDate) { - Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized) + Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized).lineLimit(1) } LabeledContent { StepperView(count: $tournament.dayDuration, minimum: 1) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 7dbe23b..e12017a 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -141,7 +141,7 @@ struct TournamentView: View { ToolbarItem(placement: .principal) { VStack(spacing: -4.0) { Text(tournament.tournamentTitle(.title)).font(.headline) - Text(tournament.formattedDate()) + Text(tournament.formattedDate(.title)) .font(.subheadline).foregroundStyle(.secondary) } .popoverTip(tournamentSelectionTip) From 4d625cb19ac3444504a3a907edf5e74de9c0989f Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 25 Sep 2024 11:29:19 +0200 Subject: [PATCH 12/41] version --- PadelClub.xcodeproj/project.pbxproj | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 06e2066..d870d1c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3157,7 +3157,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.13; + MARKETING_VERSION = 1.0.14; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3178,7 +3178,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3200,7 +3200,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.13; + MARKETING_VERSION = 1.0.14; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3315,7 +3315,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.13; + MARKETING_VERSION = 1.0.14; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3356,7 +3356,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.13; + MARKETING_VERSION = 1.0.14; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3399,7 +3399,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.13; + MARKETING_VERSION = 1.0.14; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3440,7 +3440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.13; + MARKETING_VERSION = 1.0.14; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From a3f76e4a771f41847841dfcf5bb60fbf405419d1 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 25 Sep 2024 12:44:28 +0200 Subject: [PATCH 13/41] fix stuff --- .../Views/Cashier/Event/TournamentConfiguratorView.swift | 1 + PadelClub/Views/Player/PlayerDetailView.swift | 5 ----- PadelClub/Views/Team/EditingTeamView.swift | 4 ---- PadelClub/Views/Tournament/Screen/BroadcastView.swift | 9 +++++---- .../Screen/Components/TournamentLevelPickerView.swift | 9 +++++++++ .../Views/Tournament/Screen/TournamentRankView.swift | 3 ++- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index 68483c3..9333ee9 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -27,6 +27,7 @@ struct TournamentConfigurationView: View { } .onChange(of: tournament.federalLevelCategory) { if tournament.federalLevelCategory == .unlisted { + tournament.hideTeamsWeight = true tournament.federalCategory = .unlisted tournament.federalAgeCategory = .unlisted } else { diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index e5a203c..f80848e 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -168,17 +168,12 @@ struct PlayerDetailView: View { } } } - .scrollDismissesKeyboard(.immediately) .onChange(of: player.hasArrived) { _save() } .onChange(of: player.sex) { _save() } - .onChange(of: player.computedRank) { - player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory) - _save() - } .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index e60c949..bd32b32 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -216,8 +216,6 @@ struct EditingTeamView: View { case .failed: self.sentError = .messageFailed case .sent: - let uncalledTeams = team.getPhoneNumbers().isEmpty - if networkMonitor.connected == false { self.contactType = nil if team.getPhoneNumbers().isEmpty == false { @@ -248,8 +246,6 @@ struct EditingTeamView: View { self.contactType = nil self.sentError = .mailFailed case .sent: - let uncalledTeams = team.getMail().isEmpty - if networkMonitor.connected == false { self.contactType = nil if team.getMail().isEmpty == false { diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index 2ded795..d687e29 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -105,6 +105,11 @@ struct BroadcastView: View { Toggle(isOn: $tournament.isPrivate) { Text("Tournoi privé") } + + Toggle(isOn: $tournament.hideTeamsWeight) { + Text("Masquer les poids des équipes") + } + } footer: { let verb : String = tournament.isPrivate ? "est" : "sera" let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))" @@ -155,10 +160,6 @@ struct BroadcastView: View { Text("Publication prévue") } } - - Toggle(isOn: $tournament.hideTeamsWeight) { - Text("Masquer les poids des équipes") - } } header: { Text("Liste des équipes") } footer: { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift index 4e3bc69..bc81b41 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift @@ -20,8 +20,17 @@ struct TournamentLevelPickerView: View { } .onChange(of: tournament.federalLevelCategory) { if tournament.federalLevelCategory == .unlisted { + tournament.hideTeamsWeight = true tournament.federalCategory = .unlisted tournament.federalAgeCategory = .unlisted + } else { + tournament.hideTeamsWeight = false + if tournament.federalCategory == .unlisted { + tournament.federalCategory = .men + } + if tournament.federalAgeCategory == .unlisted { + tournament.federalAgeCategory = .senior + } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index b0ed71a..dc06df0 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -250,8 +250,9 @@ struct TournamentRankView: View { } } } + + Spacer() if tournament.isAnimation() == false && key > 0 { - Spacer() VStack(alignment: .trailing) { HStack(alignment: .lastTextBaseline, spacing: 0.0) { Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always()))) From f764e913841cbd0290f76334d7827b3a91177b04 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 25 Sep 2024 12:45:00 +0200 Subject: [PATCH 14/41] versions --- 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 d870d1c..f9b5129 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,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\""; @@ -3178,7 +3178,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; @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From 940f9ed1466b40aec226f0c60b0f05d0490fda6f Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 25 Sep 2024 19:15:54 +0200 Subject: [PATCH 15/41] wip --- .../GroupStage/LoserBracketFromGroupStageView.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift index 776e75f..41fb7ba 100644 --- a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -37,6 +37,19 @@ struct LoserBracketFromGroupStageView: View { _addNewMatch() } } + + Section { + RowButtonView("Intelligent") { + for i in tournament.qualifiedPerGroupStage.. Date: Wed, 25 Sep 2024 19:36:22 +0200 Subject: [PATCH 16/41] fix issue --- PadelClub/ViewModel/AgendaDestination.swift | 2 +- .../Navigation/Agenda/TournamentLookUpView.swift | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index 96aff9d..2f08364 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -37,7 +37,7 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { func associatedTip() -> (any Tip)? { switch self { case .around: - return PlayerTournamentSearchTip() + return nil //PlayerTournamentSearchTip() default: return nil } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 2426cf5..396fb0d 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -33,11 +33,7 @@ struct TournamentLookUpView: View { Binding { locationManager.lastError != nil } set: { value in - if value == false { - locationManager.lastError = nil - } } - } var body: some View { @@ -182,6 +178,9 @@ struct TournamentLookUpView: View { federalDataViewModel.searchAttemptCount += 1 federalDataViewModel.dayPeriod = dataStore.appSettings.dayPeriod federalDataViewModel.dayDuration = dataStore.appSettings.dayDuration + federalDataViewModel.levels = Set(levels) + federalDataViewModel.categories = Set(categories) + federalDataViewModel.ageCategories = Set(ages) Task { await getNewPage() @@ -227,6 +226,12 @@ struct TournamentLookUpView: View { let resultCommand = commands.first(where: { $0.results != nil }) if let newTournaments = resultCommand?.results?.items { newTournaments.forEach { ft in +// let isValid = ft.tournaments.anySatisfy({ build in +// let ageValid = ages.isEmpty ? true : ages.contains(build.age) +// let levelValid = levels.isEmpty ? true : levels.contains(build.level) +// let categoryValid = categories.isEmpty ? true : categories.contains(build.category) +// return ageValid && levelValid && categoryValid +// }) if tournaments.contains(where: { $0.id == ft.id }) == false { federalDataViewModel.searchedFederalTournaments.append(ft) } From 886ba02498a8e0cdc3473a10b1c845d580f45ceb Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 25 Sep 2024 19:46:41 +0200 Subject: [PATCH 17/41] wip --- PadelClub/Data/Tournament.swift | 12 ++++++++++++ .../LoserBracketFromGroupStageView.swift | 18 +++++------------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index c91457b..130c2f9 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -2073,6 +2073,18 @@ defer { self.tournamentStore.groupStages.sorted(by: \.step).last?.step ?? 0 } + func generateSmartLoserGroupStageBracket() { + for i in qualifiedPerGroupStage.. Date: Wed, 25 Sep 2024 19:47:32 +0200 Subject: [PATCH 18/41] fix stuff --- 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 f9b5129..24313e1 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3157,7 +3157,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3178,7 +3178,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3200,7 +3200,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 46032d767b66efbad251d8e5917b0c64e46efe06 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 26 Sep 2024 09:41:49 +0200 Subject: [PATCH 19/41] fix finishing tournament when only groupstages --- PadelClub/Data/GroupStage.swift | 9 +++++++ PadelClub/Data/Tournament.swift | 7 +++++ PadelClub/Views/Club/ClubSearchView.swift | 2 +- .../Navigation/Toolbox/ToolboxView.swift | 27 ++++++++++++++++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 32e07b2..f99aa95 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -145,6 +145,15 @@ final class GroupStage: ModelObject, Storable { } catch { Logger.error(error) } + + if tournament.groupStagesAreOver(), tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty { + tournament.endDate = Date() + do { + try DataStore.shared.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index bff7b30..7ef451e 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1484,6 +1484,13 @@ defer { //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } + func groupStageLoserBracketAreOver() -> Bool { + guard let groupStageLoserBracket = groupStageLoserBracket() else { + return true + } + return groupStageLoserBracket.hasEnded() + } + fileprivate func _paymentMethodMessage() -> String? { return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods } diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index fc022f1..9c5378c 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -385,7 +385,7 @@ struct ClubSearchView: View { LabeledContent { Text(club.distance(from: locationManager.location)) } label: { - Text(club.nom) + Text(club.nom).lineLimit(1) Text(club.ville).font(.caption) } } diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index f0d9198..0a19da3 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -7,6 +7,7 @@ import SwiftUI import LeStorage +import Zip struct ToolboxView: View { @EnvironmentObject var dataStore: DataStore @@ -210,14 +211,38 @@ struct ToolboxView: View { } .navigationTitle(TabDestination.toolbox.title) .toolbar { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .topBarLeading) { Link(destination: URLs.appStore.url) { Text("v\(PadelClubApp.appVersion)") } } + ToolbarItem(placement: .topBarTrailing) { + Menu { + ShareLink(item: URLs.appStore.url) { + Label("Lien AppStore", systemImage: "link") + } + if let zip = _getZip() { + ShareLink(item: zip) { + Label("Mes données", systemImage: "server.rack") + } + } + } label: { + Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly) + } + } } } } + + private func _getZip() -> URL? { + do { + let filePath = try Club.storageDirectoryPath() + return try Zip.quickZipFiles([filePath], fileName: "backup") // Zip + } catch { + Logger.error(error) + return nil + } + } } //#Preview { From 8ca2193579919e33400d175fd1bb849af04a3e06 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 26 Sep 2024 09:42:15 +0200 Subject: [PATCH 20/41] version --- 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 24313e1..d6b41f3 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,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\""; @@ -3178,7 +3178,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; From f691681e94ab46bae03d3f4dde189f5339efcb37 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 27 Sep 2024 11:36:28 +0200 Subject: [PATCH 21/41] fix a lot of stuff post PBL --- PadelClub.xcodeproj/project.pbxproj | 8 +- .../Coredata/ImportedPlayer+Extensions.swift | 4 + PadelClub/Data/Federal/PlayerHolder.swift | 1 + PadelClub/Data/GroupStage.swift | 10 +- PadelClub/Data/MatchScheduler.swift | 34 +++--- PadelClub/Data/PlayerRegistration.swift | 10 +- PadelClub/Data/TeamRegistration.swift | 1 + PadelClub/Data/Tournament.swift | 22 +++- PadelClub/Utils/PadelRule.swift | 22 ++-- .../ViewModel/FederalDataViewModel.swift | 27 +++++ .../Calling/Components/MenuWarningView.swift | 2 +- .../Views/Cashier/CashierDetailView.swift | 19 ++- .../Views/Cashier/CashierSettingsView.swift | 67 +++++++++-- PadelClub/Views/Cashier/CashierView.swift | 105 +++++++++++++--- .../Views/Components/FooterButtonView.swift | 16 ++- .../Components/GroupStageTeamView.swift | 4 +- .../Views/GroupStage/GroupStageView.swift | 17 +-- .../GroupStageTeamReplacementView.swift | 2 +- .../Match/Components/MatchDateView.swift | 2 +- .../Match/Components/PlayerBlockView.swift | 16 +-- PadelClub/Views/Match/MatchDetailView.swift | 43 ++++--- .../Navigation/Agenda/CalendarView.swift | 8 +- .../Navigation/Agenda/EventListView.swift | 4 +- .../Agenda/TournamentSubscriptionView.swift | 10 +- .../Views/Planning/PlanningSettingsView.swift | 4 +- .../Components/EditablePlayerView.swift | 18 ++- .../Views/Shared/ImportedPlayerView.swift | 113 +++++++++--------- PadelClub/Views/Team/TeamRowView.swift | 21 ++-- .../Screen/TournamentCashierView.swift | 8 +- .../Shared/TournamentCellView.swift | 17 ++- .../Tournament/TournamentBuildView.swift | 2 +- 31 files changed, 427 insertions(+), 210 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index d6b41f3..4e3d4c8 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3315,7 +3315,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3356,7 +3356,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index ab7d66f..b7679f3 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -122,6 +122,10 @@ extension ImportedPlayer: PlayerHolder { func getProgression() -> Int { return Int(progression) } + + func getComputedRank() -> Int? { + nil + } } fileprivate extension Int { diff --git a/PadelClub/Data/Federal/PlayerHolder.swift b/PadelClub/Data/Federal/PlayerHolder.swift index 07c6860..72949e9 100644 --- a/PadelClub/Data/Federal/PlayerHolder.swift +++ b/PadelClub/Data/Federal/PlayerHolder.swift @@ -27,6 +27,7 @@ protocol PlayerHolder { func isNotFromCurrentDate() -> Bool func getBirthYear() -> Int? func getProgression() -> Int + func getComputedRank() -> Int? } extension PlayerHolder { diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index f99aa95..eadf491 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -266,11 +266,11 @@ final class GroupStage: ModelObject, Storable { case 4: return [2, 3, 1, 4, 5, 0] case 5: - return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] -// return [3, 5, 8, 2, 6, 7, 1, 9, 4, 0] +// return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] + return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] case 6: - return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] - //return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] + //return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] + return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] default: return [] } @@ -283,7 +283,7 @@ final class GroupStage: ModelObject, Storable { func localizedMatchUpLabel(for matchIndex: Int) -> String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { - return "#\(index + 1) contre #\(index2 + 1)" + return "#\(index + 1) vs #\(index2 + 1)" } else { return "--" } diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 6c71fe4..d42973b 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -199,31 +199,29 @@ final class MatchScheduler : ModelObject, Storable { while slots.count < flattenedMatches.count { teamsPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = [] - let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }.map { ($0.groupIndex, 1) } - let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +) - var rotationMatches = Array(availableMatchs.filter({ match in - teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true - }).prefix(numberOfCourtsAvailablePerRotation)) - - if rotationIndex > 0 { - rotationMatches = rotationMatches.sorted(by: { - if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { - return $0.groupStageObject!.index < $1.groupStageObject!.index - } else { - return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 - } - }) - } - (0.. 0 { + rotationMatches = rotationMatches.sorted(by: { + if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { + return $0.groupStageObject!.index < $1.groupStageObject!.index + } else { + return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 + } + }) + } + if let first = rotationMatches.first(where: { match in let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) let timeIntervalToAdd = (Double(rotationIndex)) * Double(estimatedDuration) * 60 let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability) - if courtIndex >= numberOfCourtsAvailablePerRotation - courtsUnavailable.count { + if courtsUnavailable.contains(courtIndex) { return false } else { return teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true @@ -486,7 +484,7 @@ final class MatchScheduler : ModelObject, Storable { let roundObject = match.roundObject! let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability) print("courtsUnavailable \(courtsUnavailable)") - if courtPosition >= availableCourts - courtsUnavailable.count { + if courtsUnavailable.contains(courtPosition) { return false } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 28c1cff..2e966ec 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -220,11 +220,7 @@ final class PlayerRegistration: ModelObject, Storable { return "non classé" + (isMalePlayer() ? "" : "e") } } - - func getRank() -> Int { - computedRank - } - + @MainActor func updateRank(from sources: [CSVParser], lastRank: Int) async throws { if let dataFound = try await history(from: sources) { @@ -586,4 +582,8 @@ extension PlayerRegistration: PlayerHolder { func getProgression() -> Int { 0 } + + func getComputedRank() -> Int? { + computedRank + } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 1c90fef..ffbcb94 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -208,6 +208,7 @@ final class TeamRegistration: ModelObject, Storable { } func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String { + if let name { return name } return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ") } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 7ef451e..07baa79 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1522,13 +1522,27 @@ defer { return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count) } + func presenceStatus() -> Double { + let selectedPlayers = selectedPlayers() + if selectedPlayers.isEmpty { return 0 } + return Double(selectedPlayers.filter { $0.hasArrived }.count) / Double(selectedPlayers.count) + } + typealias TournamentStatus = (label:String, completion: String) func cashierStatus() async -> TournamentStatus { let selectedPlayers = selectedPlayers() - let paid = selectedPlayers.filter({ $0.hasPaid() }) + var filteredPlayers = [PlayerRegistration]() + var wording = "" + if isFree() { + wording = "présent" + filteredPlayers = selectedPlayers.filter({ $0.hasArrived }) + } else { + wording = "encaissé" + filteredPlayers = selectedPlayers.filter({ $0.hasPaid() }) + } // let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" - let label = "\(paid.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs encaissés" - let completion = (Double(paid.count) / Double(selectedPlayers.count)) + let label = "\(filteredPlayers.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs \(wording)\(filteredPlayers.count.pluralSuffix)" + let completion = (Double(filteredPlayers.count) / Double(selectedPlayers.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } @@ -2178,7 +2192,7 @@ extension Tournament: FederalTournamentHolder { } extension Tournament: TournamentBuildHolder { - func buildHolderTitle() -> String { + func buildHolderTitle(_ displayStyle: DisplayStyle) -> String { tournamentTitle(.short) } diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 62a1889..de48b13 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -30,7 +30,7 @@ protocol TournamentBuildHolder: Identifiable { var category: TournamentCategory { get } var level: TournamentLevel { get } var age: FederalTournamentAge { get } - func buildHolderTitle() -> String + func buildHolderTitle(_ displayStyle: DisplayStyle) -> String } struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { @@ -43,29 +43,29 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { // var japFirstName: String? = nil // var japLastName: String? = nil - func buildHolderTitle() -> String { - computedLabel + func buildHolderTitle(_ displayStyle: DisplayStyle) -> String { + computedLabel(displayStyle) } var identifier: String { level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() } - var computedLabel: String { - if age == .senior { return localizedLabel() } - return localizedLabel() + " " + localizedAge + func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + if age == .senior { return localizedLabel(displayStyle) } + return localizedLabel(displayStyle) + " " + localizedAge(displayStyle) } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { - level.localizedLevelLabel() + category.localizedLabel(.short) + level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) } - var localizedTitle: String { - level.localizedLevelLabel() + " " + category.localizedLabel() + func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String { + level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) } - var localizedAge: String { - age.tournamentDescriptionLabel + func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { + age.localizedLabel(displayStyle) } } diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index d4d6d24..71c0cf1 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -97,6 +97,33 @@ class FederalDataViewModel { }) } + func countForTournamentBuilds(from tournaments: [any FederalTournamentHolder]) -> Int { + tournaments.filter({ tournament in + (selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!)) + && + (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) + && + (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) + }) + .flatMap { $0.tournaments } + .filter { + (levels.isEmpty || levels.contains($0.level)) + && + (categories.isEmpty || categories.contains($0.category)) + && + (ageCategories.isEmpty || ageCategories.contains($0.age)) + } + .count + } + + func buildIsValid(_ build: any TournamentBuildHolder) -> Bool { + (levels.isEmpty || levels.contains(build.level)) + && + (categories.isEmpty || categories.contains(build.category)) + && + (ageCategories.isEmpty || ageCategories.contains(build.age)) + } + func isTournamentValidForFilters(_ tournament: Tournament) -> Bool { if tournament.isDeleted { return false } let firstPart = (levels.isEmpty || levels.contains(tournament.level)) diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index 71eacb7..ef094a0 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -124,7 +124,7 @@ struct MenuWarningView: View { @ViewBuilder func _teamActionView(_ team: TeamRegistration) -> some View { - Menu("Toute l'équipe") { + Menu(team.name ?? "Toute l'équipe") { let players = team.players() _actionView(players: players) } diff --git a/PadelClub/Views/Cashier/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift index 32992c1..074a67e 100644 --- a/PadelClub/Views/Cashier/CashierDetailView.swift +++ b/PadelClub/Views/Cashier/CashierDetailView.swift @@ -89,7 +89,8 @@ struct CashierDetailView: View { let showTournamentTitle: Bool @State private var earnings: Double? = nil @State private var paidCompletion: Double? = nil - + @State private var presence: Double? = nil + var body: some View { Section { LabeledContent { @@ -99,9 +100,15 @@ struct CashierDetailView: View { ProgressView() } } label: { - Text("Encaissement") - if let paidCompletion { - Text(paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + Text(tournament.isFree() ? "Présence" : "Encaissement") + if tournament.isFree() { + if let presence { + Text(presence.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + } + } else { + if let paidCompletion { + Text(paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + } } } CashierDetailDisclosureView(tournament: tournament) @@ -119,6 +126,10 @@ struct CashierDetailView: View { if paidCompletion == nil { paidCompletion = tournament.paidCompletion() } + + if presence == nil { + presence = tournament.presenceStatus() + } } } } diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index 908465f..ed5e63b 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -25,14 +25,12 @@ struct CashierSettingsView: View { var body: some View { List { Section { - RowButtonView("Tout le monde a réglé", role: .destructive) { + RowButtonView("Tout le monde est arrivé", role: .destructive) { for tournament in self.tournaments { let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) players.forEach { player in - if player.hasPaid() == false { - player.paymentType = .gift - } + player.hasArrived = true } do { try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) @@ -43,29 +41,72 @@ struct CashierSettingsView: View { } } footer: { - Text("Passe tous les joueurs qui n'ont pas réglé en offert") + Text("Indique tous les joueurs sont là") } - + Section { - RowButtonView("Personne n'a réglé", role: .destructive) { + RowButtonView("Personne n'est là", role: .destructive) { + for tournament in self.tournaments { - let store = tournament.tournamentStore - - let players = tournament.selectedPlayers() + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) players.forEach { player in - player.paymentType = nil + player.hasArrived = false } do { - try store.playerRegistrations.addOrUpdate(contentOfs: players) + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) } catch { Logger.error(error) } } + } } footer: { - Text("Remet à zéro le type d'encaissement de tous les joueurs") + Text("Indique qu'aucun joueur n'est arrivé") } + if tournaments.count > 1 || tournaments.first?.isFree() == false { + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + + for tournament in self.tournaments { + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + if player.hasPaid() == false { + player.paymentType = .gift + } + } + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + } + + } + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") + } + + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + for tournament in self.tournaments { + let store = tournament.tournamentStore + + let players = tournament.selectedPlayers() + players.forEach { player in + player.paymentType = nil + } + do { + try store.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + } + } + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") + } + } } } } diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index ce35f7f..b022259 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -57,6 +57,7 @@ class CashierViewModel: ObservableObject { let id: UUID = UUID() @Published var sortOption: SortOption = .callDate @Published var filterOption: FilterOption = .all + @Published var presenceFilterOption: PresenceFilterOption = .all @Published var sortOrder: SortOrder = .ascending @Published var searchText: String = "" @Published var isSearching: Bool = false @@ -69,9 +70,14 @@ class CashierViewModel: ObservableObject { func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { if searchText.isEmpty == false { - sortOption.shouldDisplayPlayer(player) && filterOption.shouldDisplayPlayer(player) && player.contains(searchText) + sortOption.shouldDisplayPlayer(player) + && filterOption.shouldDisplayPlayer(player) + && presenceFilterOption.shouldDisplayPlayer(player) + && player.contains(searchText) } else { - sortOption.shouldDisplayPlayer(player) && filterOption.shouldDisplayPlayer(player) + sortOption.shouldDisplayPlayer(player) + && filterOption.shouldDisplayPlayer(player) + && presenceFilterOption.shouldDisplayPlayer(player) } } @@ -183,6 +189,37 @@ class CashierViewModel: ObservableObject { } } + enum PresenceFilterOption: Int, Identifiable, CaseIterable { + case all + case hasArrived + case hasNotArrived + + var id: Int { self.rawValue } + + func localizedLabel() -> String { + switch self { + case .all: + return "Tous" + case .hasArrived: + return "Présent" + case .hasNotArrived: + return "Absent" + } + } + + func shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { + switch self { + case .all: + return true + case .hasArrived: + return player.hasArrived + case .hasNotArrived: + return player.hasArrived == false + + } + } + } + } struct CashierView: View { @@ -201,16 +238,42 @@ struct CashierView: View { _players = .init(wrappedValue: teams.flatMap({ $0.unsortedPlayers() })) } + private func _isFree() -> Bool { + if tournaments.count == 1 { + return tournaments.first?.isFree() == true + } else { + return false + } + } + + private func _editingOptions() -> [EditablePlayerView.PlayerEditingOption] { + if _isFree() { + return [.licenceId, .name, .presence] + } else { + return [.licenceId, .name, .payment] + } + } + var body: some View { List { if cashierViewModel.isSearching == false { Section { - Picker(selection: $cashierViewModel.filterOption) { - ForEach(CashierViewModel.FilterOption.allCases) { filterOption in + Picker(selection: $cashierViewModel.presenceFilterOption) { + ForEach(CashierViewModel.PresenceFilterOption.allCases) { filterOption in Text(filterOption.localizedLabel()).tag(filterOption) } } label: { - Text("Statut du règlement") + Text("Présence") + } + + if _isFree() == false { + Picker(selection: $cashierViewModel.filterOption) { + ForEach(CashierViewModel.FilterOption.allCases) { filterOption in + Text(filterOption.localizedLabel()).tag(filterOption) + } + } label: { + Text("Statut du règlement") + } } Picker(selection: $cashierViewModel.sortOption) { @@ -239,12 +302,12 @@ struct CashierView: View { switch cashierViewModel.sortOption { case .teamRank: - TeamRankView(teams: teams, displayTournamentTitle: tournaments.count > 1) + TeamRankView(teams: teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age: - PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1) + PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) case .callDate: let _teams = teams.filter({ $0.callDate != nil }) - TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1) + TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) } } .onAppear { @@ -279,11 +342,12 @@ struct CashierView: View { @EnvironmentObject var cashierViewModel: CashierViewModel let players: [PlayerRegistration] let displayTournamentTitle: Bool - + let editingOptions: [EditablePlayerView.PlayerEditingOption] + var body: some View { ForEach(players) { player in Section { - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: editingOptions) } header: { if displayTournamentTitle, let tournamentTitle = player.tournament()?.tournamentTitle() { Text(tournamentTitle) @@ -301,6 +365,7 @@ struct CashierView: View { @EnvironmentObject var cashierViewModel: CashierViewModel let teams: [TeamRegistration] let displayTournamentTitle: Bool + let editingOptions: [EditablePlayerView.PlayerEditingOption] var body: some View { ForEach(teams) { team in @@ -308,11 +373,17 @@ struct CashierView: View { if players.isEmpty == false { Section { ForEach(players) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: editingOptions) } } header: { - if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { - Text(tournamentTitle) + HStack { + if let name = team.name { + Text(name) + } + if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Spacer() + Text(tournamentTitle) + } } } footer: { if let callDate = team.callDate { @@ -329,6 +400,7 @@ struct CashierView: View { @EnvironmentObject var cashierViewModel: CashierViewModel let teams: [TeamRegistration] let displayTournamentTitle: Bool + let editingOptions: [EditablePlayerView.PlayerEditingOption] var body: some View { let groupedTeams = Dictionary(grouping: teams) { team in @@ -343,10 +415,15 @@ struct CashierView: View { if players.isEmpty == false { Section { ForEach(players) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: editingOptions) } } header: { + if let name = team.name { + Text(name) + } + if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Spacer() Text(tournamentTitle) } } footer: { diff --git a/PadelClub/Views/Components/FooterButtonView.swift b/PadelClub/Views/Components/FooterButtonView.swift index a710b18..7730639 100644 --- a/PadelClub/Views/Components/FooterButtonView.swift +++ b/PadelClub/Views/Components/FooterButtonView.swift @@ -11,13 +11,15 @@ fileprivate let defaultConfirmationMessage = "Êtes-vous sûr de vouloir faire c struct FooterButtonView: View { var role: ButtonRole? = nil + var systemImage: String? = nil let title: String let confirmationMessage: String let action: () -> () @State private var askConfirmation: Bool = false - init(_ title: String, role: ButtonRole? = nil, confirmationMessage: String? = nil, action: @escaping () -> Void) { + init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, confirmationMessage: String? = nil, action: @escaping () -> Void) { self.title = title + self.systemImage = systemImage self.action = action self.role = role self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage @@ -31,8 +33,16 @@ struct FooterButtonView: View { action() } } label: { - Text(title) - .underline() + if let systemImage { + HStack { + Text(title) + .underline() + Image(systemName: systemImage).font(.caption) + } + } else { + Text(title) + .underline() + } } .buttonStyle(.borderless) .confirmationDialog("Confirmation", diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 0d87d01..0d94139 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -40,9 +40,11 @@ struct GroupStageTeamView: View { var body: some View { List { Section { + if let name = team.name { + Text(name).foregroundStyle(.secondary) + } ForEach(team.players()) { player in EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) - .environmentObject(tournament.tournamentStore) } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 5b04f71..c0b238d 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -125,15 +125,16 @@ struct GroupStageView: View { HStack { VStack(alignment: .leading) { if let teamName = team.name { - Text(teamName).foregroundStyle(.secondary) - } - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1) - .overlay { - if player.hasArrived && team.isHere() == false { - Color.green.opacity(0.6) + Text(teamName).font(.title3) + } else { + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1) + .overlay { + if player.hasArrived && team.isHere() == false { + Color.green.opacity(0.6) + } } - } + } } } Spacer() diff --git a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift index b01901b..0ab01e9 100644 --- a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift +++ b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift @@ -54,7 +54,7 @@ struct GroupStageTeamReplacementView: View { Section { Picker(selection: $selectedPlayer) { HStack { - Text("Toute l'équipe") + Text(team.name ?? "Toute l'équipe") Spacer() Text(team.weight.formatted()).bold() } diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index d969252..8034631 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -93,7 +93,7 @@ struct MatchDateView: View { .foregroundStyle(Color.master) .underline() } else { - Text("en attente") + Text("démarrer") .foregroundStyle(Color.master) .underline() } diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 444aa94..5f5b278 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -57,19 +57,21 @@ struct PlayerBlockView: View { } if let name = team?.name { - Text(name).foregroundStyle(.secondary) - } - ForEach(names, id: \.self) { name in - Text(name).lineLimit(1) + Text(name).font(.title3) + } else { + ForEach(names, id: \.self) { name in + Text(name).lineLimit(1) + } } } else { ZStack(alignment: .leading) { VStack { if let name = team?.name { - Text(name).foregroundStyle(.secondary) + Text(name).font(.title3) + } else { + Text("longLabelPlayerOne").lineLimit(1) + Text("longLabelPlayerTwo").lineLimit(1) } - Text("longLabelPlayerOne").lineLimit(1) - Text("longLabelPlayerTwo").lineLimit(1) } .opacity(0) Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index bf7c291..235e648 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -107,30 +107,31 @@ struct MatchDetailView: View { } } - let players = self.match.teams().flatMap { $0.players() } - let unpaid = players.filter({ $0.hasPaid() == false }) - - if unpaid.isEmpty == false { - Section { - DisclosureGroup { - ForEach(unpaid) { player in + if self.match.currentTournament()?.isFree() == false { + let players = self.match.teams().flatMap { $0.players() } + let unpaid = players.filter({ $0.hasPaid() == false }) + + if unpaid.isEmpty == false { + Section { + DisclosureGroup { + ForEach(unpaid) { player in + LabeledContent { + PlayerPayView(player: player) + .environmentObject(tournamentStore) + } label: { + Text(player.playerLabel()) + } + } + } label: { LabeledContent { - PlayerPayView(player: player) - .environmentObject(tournamentStore) + Text(unpaid.count.formatted() + " / " + players.count.formatted()) } label: { - Text(player.playerLabel()) + Text("Encaissement manquant") } } - } label: { - LabeledContent { - Text(unpaid.count.formatted() + " / " + players.count.formatted()) - } label: { - Text("Encaissement manquant") - } } } } - menuView } .sheet(isPresented: $showDetails) { @@ -423,9 +424,9 @@ struct MatchDetailView: View { let rotationDuration = match.getDuration() Picker(selection: $startDateSetup) { if match.isReady() { + Text("Tout de suite").tag(MatchDateSetup.now) Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) - Text("Tout de suite").tag(MatchDateSetup.now) } Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration)) Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration)) @@ -464,11 +465,7 @@ struct MatchDetailView: View { Text("Au hasard parmi les libres").tag(MatchFieldSetup.random) Text("Au hasard").tag(MatchFieldSetup.fullRandom) //Text("Premier disponible").tag(MatchFieldSetup.firstAvailable) - if let club = match.currentTournament()?.club() { - ForEach(0.. 1 { - Text("\(value.formatted()) poules commenceront en parallèle") + Text("\(value.formatted()) poules en parallèle") } else { - Text("une poule sera jouer à la fois") + Text("une poule sera jouée à la fois") } } } diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index 130b127..b2955fb 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -14,6 +14,7 @@ struct EditablePlayerView: View { case payment case licenceId case name + case presence } @EnvironmentObject var dataStore: DataStore @@ -77,6 +78,13 @@ struct EditablePlayerView: View { Logger.error(error) } } + .onChange(of: player.hasArrived) { + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } } @ViewBuilder @@ -91,11 +99,6 @@ struct EditablePlayerView: View { Menu { Button { player.hasArrived.toggle() - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) - } catch { - Logger.error(error) - } } label: { Label("Présent", systemImage: player.hasArrived ? "checkmark.circle" : "circle") } @@ -172,6 +175,11 @@ struct EditablePlayerView: View { if editingOptions.contains(.payment) { Spacer() PlayerPayView(player: player) + } else if editingOptions.contains(.presence) { + Spacer() + FooterButtonView(player.hasArrived ? "Présent" : "Sur place ?", role: player.hasArrived ? nil : .cancel, systemImage: player.hasArrived ? "checkmark" : nil) { + player.hasArrived.toggle() + } } } } diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index fd99bf8..6de85dd 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -12,6 +12,9 @@ struct ImportedPlayerView: View { var index: Int? = nil var showFemaleInMaleAssimilation: Bool = false var showProgression: Bool = false + var isAnimation: Bool { + player.getComputedRank() == 0 + } var body: some View { VStack(alignment: .leading) { @@ -39,74 +42,76 @@ struct ImportedPlayerView: View { } .font(.title3) .lineLimit(1) - HStack { - HStack(alignment: .top, spacing: 0) { - Text(player.formattedRank()).italic(player.isAssimilated) - .font(.title3) - .background { - if player.isNotFromCurrentDate() { - UnderlineView() + if isAnimation == false { + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) } - if let rank = player.getRank() { - Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) - .font(.caption) } } + .lineLimit(1) - if showProgression, player.getProgression() != 0 { + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { Text("(") - Text(player.getProgression().formatted(.number.sign(strategy: .always()))) - .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) - Text(")") - }.font(.title3) - } - - if let pts = player.getPoints(), pts > 0 { - HStack(alignment: .lastTextBaseline, spacing: 0) { - Text(pts.formatted()).font(.title3) - Text(" pts").font(.caption) + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) } } - - if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { - HStack(alignment: .lastTextBaseline, spacing: 0) { - Text(tournamentPlayed.formatted()).font(.title3) - Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") } } - } - .lineLimit(1) - - if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { - HStack(alignment: .top, spacing: 2) { - Text("(") - Text(assimilatedAsMaleRank.formatted()) - VStack(alignment: .leading, spacing: 0) { - Text("équivalence") - Text("messieurs") - } - .font(.caption) - Text(")").font(.title3) + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) } - } - - HStack { - Text(player.formattedLicense()) - if let computedAge = player.computedAge { - Text(computedAge.formatted() + " ans") + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) } } - .font(.caption) - if let clubName = player.clubName { - Text(clubName) - .font(.caption) - } - if let ligueName = player.ligueName { - Text(ligueName) - .font(.caption) - } } } } diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 533d4bc..458d4e3 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -17,10 +17,6 @@ struct TeamRowView: View { TeamWeightView(team: team, teamPosition: teamPosition) } label: { VStack(alignment: .leading) { - if let name = team.name { - Text(name).foregroundStyle(.secondary) - } - if let groupStage = team.groupStageObject() { HStack { Text(groupStage.groupStageTitle()) @@ -32,13 +28,20 @@ struct TeamRowView: View { Text(round.roundTitle(.wide)) } - if team.players().isEmpty == false { - ForEach(team.players()) { player in - Text(player.playerLabel()) + if let name = team.name { + Text(name).font(.title3) + if team.players().isEmpty { + Text("Aucun joueur") } } else { - Text("Place réservée") - Text("Place réservée") + if team.players().isEmpty == false { + ForEach(team.players()) { player in + Text(player.playerLabel()) + } + } else { + Text("Place réservée") + Text("Place réservée") + } } } if displayCallDate { diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index 2353007..b965fb5 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -54,9 +54,15 @@ enum CashierDestination: Identifiable, Selectable, Equatable { case .summary: return nil case .groupStage(let groupStage): + if groupStage.tournamentObject()?.isFree() == true { + return groupStage.unsortedPlayers().filter({ $0.hasArrived == false }).count + } return groupStage.unsortedPlayers().filter({ $0.hasPaid() == false }).count case .bracket(let round): let playerRegistrations: [PlayerRegistration] = round.seeds().flatMap { $0.unsortedPlayers() } + if round.tournamentObject()?.isFree() == true { + return playerRegistrations.filter({ $0.hasArrived == false }).count + } return playerRegistrations.filter({ $0.hasPaid() == false }).count case .all(_): return nil @@ -156,7 +162,7 @@ struct TournamentCashierView: View { .environmentObject(cashierViewModel) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Encaissement") + .navigationTitle(tournament.isFree() ? "Présence" : "Encaissement") } } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 94a7353..62c22d1 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -11,6 +11,7 @@ import LeStorage struct TournamentCellView: View { @EnvironmentObject var dataStore: DataStore @Environment(NavigationViewModel.self) private var navigation + @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel let tournament: FederalTournamentHolder // let color: Color = .black @@ -23,11 +24,17 @@ struct TournamentCellView: View { var body: some View { ForEach(tournament.tournaments, id: \.id) { build in - if navigation.agendaDestination == .around, let federalTournament = tournament as? FederalTournament { - NavigationLink { - TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) - } label: { - _buildView(build, existingTournament: event?.existingBuild(build)) + if let federalTournament = tournament as? FederalTournament { + if federalDataViewModel.isFederalTournamentValidForFilters(federalTournament, build: build) { + if navigation.agendaDestination == .around { + NavigationLink { + TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) + } label: { + _buildView(build, existingTournament: event?.existingBuild(build)) + } + } else { + _buildView(build, existingTournament: event?.existingBuild(build)) + } } } else { _buildView(build, existingTournament: event?.existingBuild(build)) diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index a11e055..6da1227 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -185,7 +185,7 @@ struct TournamentBuildView: View { ProgressView() } } label: { - Text("Encaissement") + Text(tournament.isFree() ? "Présence" : "Encaissement") if let tournamentStatus { Text(tournamentStatus.label).lineLimit(1) } else { From 883a46baea94dda619c0adf757eb0c784c2faad9 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 27 Sep 2024 14:18:37 +0200 Subject: [PATCH 22/41] fix planning views and match scheduler for groupstages --- PadelClub.xcodeproj/project.pbxproj | 8 ++-- PadelClub/Data/GroupStage.swift | 6 ++- PadelClub/Data/Match.swift | 5 +- PadelClub/Data/MatchScheduler.swift | 31 +++++++------ .../Components/GroupStageTeamView.swift | 10 +++- .../Components/MatchTeamDetailView.swift | 15 +++++- PadelClub/Views/Match/MatchSummaryView.swift | 2 +- .../Views/Planning/PlanningByCourtView.swift | 1 + PadelClub/Views/Planning/PlanningView.swift | 46 ++++++++++++++++++- 9 files changed, 99 insertions(+), 25 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 4e3d4c8..f97f18d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3178,7 +3178,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 6; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index eadf491..da3e0fe 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -50,7 +50,7 @@ final class GroupStage: ModelObject, Storable { // MARK: - Computed dependencies func _matches() -> [Match] { - return self.tournamentStore.matches.filter { $0.groupStage == self.id } + return self.tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index) // Store.main.filter { $0.groupStage == self.id } } @@ -276,6 +276,10 @@ final class GroupStage: ModelObject, Storable { } } + func indexOf(_ matchIndex: Int) -> Int { + _matchOrder().firstIndex(of: matchIndex) ?? matchIndex + } + private func _matchUp(for matchIndex: Int) -> [Int] { Array((0.. [Match] { diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index d42973b..69d1011 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -199,22 +199,24 @@ final class MatchScheduler : ModelObject, Storable { while slots.count < flattenedMatches.count { teamsPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = [] + let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }.map { ($0.groupIndex, 1) } + let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +) + var rotationMatches = Array(availableMatchs.filter({ match in + teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true + }).prefix(numberOfCourtsAvailablePerRotation)) + + if rotationIndex > 0 { + rotationMatches = rotationMatches.sorted(by: { + if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { + return $0.groupStageObject!.index < $1.groupStageObject!.index + } else { + return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 + } + }) + } + (0.. 0 { - rotationMatches = rotationMatches.sorted(by: { - if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { - return $0.groupStageObject!.index < $1.groupStageObject!.index - } else { - return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 - } - }) - } - if let first = rotationMatches.first(where: { match in let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) let timeIntervalToAdd = (Double(rotationIndex)) * Double(estimatedDuration) * 60 @@ -228,6 +230,7 @@ final class MatchScheduler : ModelObject, Storable { } }) { let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index) + print(first.matchTitle()) slots.append(timeMatch) teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) rotationMatches.removeAll(where: { $0.id == first.id }) diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 0d94139..af1b072 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -37,6 +37,14 @@ struct GroupStageTeamView: View { } } + private func _editingOptions() -> [EditablePlayerView.PlayerEditingOption] { + if tournament.isFree() { + return [.licenceId, .name, .presence] + } else { + return [.licenceId, .name, .payment] + } + } + var body: some View { List { Section { @@ -44,7 +52,7 @@ struct GroupStageTeamView: View { Text(name).foregroundStyle(.secondary) } ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: _editingOptions()) } } diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift index 435f181..cf952d0 100644 --- a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -32,13 +32,26 @@ struct MatchTeamDetailView: View { private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { Section { ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: _editingOptions()) } } header: { TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team)) } } + private func _isFree() -> Bool { + let tournament = match.currentTournament() + return tournament?.isFree() == true + } + + private func _editingOptions() -> [EditablePlayerView.PlayerEditingOption] { + if _isFree() { + return [.licenceId, .name, .presence] + } else { + return [.licenceId, .name, .payment] + } + } + } //#Preview { diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 1e4bda1..5ba96eb 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -57,7 +57,7 @@ struct MatchSummaryView: View { } } Spacer() - if let courtName, matchViewStyle != .feedStyle { + if let courtName { Spacer() Text(courtName) .foregroundStyle(.gray) diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 96302fd..25cc794 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -40,6 +40,7 @@ struct PlanningByCourtView: View { var body: some View { List { _byCourtView() + .id(selectedCourt) } .overlay { if matches.allSatisfy({ $0.startDate == nil }) { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index e490a08..9dd42da 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -16,6 +16,23 @@ struct PlanningView: View { @State private var timeSlots: [Date:[Match]] @State private var days: [Date] @State private var keys: [Date] + @State private var filterOption: PlanningFilterOption = .byDefault + + enum PlanningFilterOption: Int, CaseIterable, Identifiable { + var id: Int { self.rawValue } + + case byDefault + case byCourt + + func localizedPlanningLabel() -> String { + switch self { + case .byCourt: + return "Par terrain" + case .byDefault: + return "Par défaut" + } + } + } init(matches: [Match], selectedScheduleDestination: Binding) { self.matches = matches @@ -30,6 +47,24 @@ struct PlanningView: View { List { _bySlotView() } + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) + } + } label: { + Text("Option de filtrage") + } + .labelsHidden() + .pickerStyle(.inline) + } label: { + Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") + .symbolVariant(filterOption == .byCourt ? .fill : .none) + } + } + }) .overlay { if matches.allSatisfy({ $0.startDate == nil }) { ContentUnavailableView { @@ -53,7 +88,7 @@ struct PlanningView: View { ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in if let _matches = timeSlots[key] { DisclosureGroup { - ForEach(_matches) { match in + ForEach(_matches.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting)) { match in NavigationLink { MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) } label: { @@ -98,7 +133,14 @@ struct PlanningView: View { Text(self._formattedMatchCount(matches.count)) } label: { Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) - Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", ")) + let names = matches.sorted(by: \.computedOrder) + .compactMap({ $0.roundTitle() }) + .reduce(into: [String]()) { uniqueNames, name in + if !uniqueNames.contains(name) { + uniqueNames.append(name) + } + } + Text(names.joined(separator: ", ")) } } From 3638aae3c3f28964d8a6b544433c800a27eed2e6 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 27 Sep 2024 15:39:09 +0200 Subject: [PATCH 23/41] fix issue when importing female in male tournament --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Utils/FileImportManager.swift | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f97f18d..d981b70 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 8; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3178,7 +3178,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 8; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 7; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 7; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index f61f0dd..a275e03 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -278,9 +278,9 @@ class FileImportManager { FederalTournamentAge.allCases.first(where: { $0.importingRawValue.canonicalVersion == ageCategory.canonicalVersion }) ?? .senior } - let resultOne = Array(dataOne.dropFirst(3).dropLast()) - let resultTwo = Array(dataTwo.dropFirst(3).dropLast()) - let sexUnknown: Bool = (resultOne.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) || (resultTwo.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) + let resultOne = Array(dataOne.dropFirst(3).dropLast(3)) + let resultTwo = Array(dataTwo.dropFirst(3).dropLast(3)) + let sexUnknown: Bool = (dataOne.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) || (dataTwo.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) var sexPlayerOne : Int { switch tournamentCategory { From d3ed0147be846c1ed4a6656696a22f6ee03d0001 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 27 Sep 2024 16:05:24 +0200 Subject: [PATCH 24/41] release --- 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 d981b70..a999dff 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 10; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3178,7 +3178,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 10; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3293,7 +3293,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3335,7 +3335,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From dd4501b95cc37d8d74b25f105ae1b0a17eebfedf Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 27 Sep 2024 19:48:01 +0200 Subject: [PATCH 25/41] fix issue with matchscheduler disable any locks for creatiing a player manually --- PadelClub.xcodeproj/project.pbxproj | 2 + PadelClub/Data/Match.swift | 6 +- PadelClub/Data/MatchScheduler.swift | 305 ++++++++++-------- .../Player/Components/PlayerPopoverView.swift | 2 +- PadelClub/Views/Round/RoundView.swift | 2 +- 5 files changed, 181 insertions(+), 136 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index a999dff..a2f8edc 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3139,6 +3139,7 @@ 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"; @@ -3182,6 +3183,7 @@ 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"; diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 762f886..5476029 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -408,7 +408,7 @@ defer { } func next() -> Match? { - let matches: [Match] = self.tournamentStore.matches.filter { $0.round == round && $0.index > index } + let matches: [Match] = self.tournamentStore.matches.filter { $0.round == round && $0.index > index && $0.disabled == false } return matches.sorted(by: \.index).first } @@ -435,6 +435,10 @@ defer { else { return nil } } + func roundAndMatchTitle() -> String { + [roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") + } + func topPreviousRoundMatchIndex() -> Int { return index * 2 + 1 } diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 69d1011..4264da2 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -179,30 +179,39 @@ final class MatchScheduler : ModelObject, Storable { // Get the maximum count of matches in any group let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 - // Use zip and flatMap to flatten matches in the desired order + // Flatten matches in a round-robin order by cycling through each group let flattenedMatches = (0.. 0 { @@ -216,29 +225,42 @@ final class MatchScheduler : ModelObject, Storable { } (0.. Int { if loserBracket { @@ -271,70 +301,96 @@ final class MatchScheduler : ModelObject, Storable { } func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { - print(roundObject.roundTitle(), match.matchTitle()) - + print("Evaluating match: \(match.roundAndMatchTitle()) in round: \(roundObject.roundTitle()) with index: \(match.index)") + if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate { - print("can't start \(targetedStartDate) earlier than \(roundStartDate)") + print("Cannot start at \(targetedStartDate), earlier than round start date \(roundStartDate)") if targetedStartDate == minimumTargetedEndDate { + print("Updating minimumTargetedEndDate to roundStartDate: \(roundStartDate)") minimumTargetedEndDate = roundStartDate } else { + print("Setting minimumTargetedEndDate to the earlier of \(roundStartDate) and \(minimumTargetedEndDate)") minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate) } + print("Returning false: Match cannot start earlier than the round start date.") return false } let previousMatches = roundObject.precedentMatches(ofMatch: match) - if previousMatches.isEmpty { return true } + if previousMatches.isEmpty { + print("No ancestors matches for this match, returning true. (eg beginning of tournament 1st bracket") + return true + } - let previousMatchSlots = slots.filter({ slot in - previousMatches.map { $0.id }.contains(slot.matchID) - }) + let previousMatchSlots = slots.filter { previousMatches.map { $0.id }.contains($0.matchID) } if previousMatchSlots.isEmpty { - if previousMatches.filter({ $0.disabled == false }).allSatisfy({ $0.startDate != nil }) { + if previousMatches.filter({ !$0.disabled }).allSatisfy({ $0.startDate != nil }) { + print("All previous matches have start dates, returning true.") return true } + print("Some previous matches are pending, returning false.") return false } - if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count { - if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) { + if previousMatches.filter({ !$0.disabled }).count > previousMatchSlots.count { + if previousMatches.filter({ !$0.disabled }).anySatisfy({ $0.startDate != nil }) { + print("Some previous matches started, returning true.") return true } + print("Not enough previous matches have started, returning false.") return false } - + var includeBreakTime = false - if accountLoserBracketBreakTime && roundObject.isLoserBracket() { includeBreakTime = true + print("Including break time for loser bracket.") } - - if accountUpperBracketBreakTime && roundObject.isLoserBracket() == false { + + if accountUpperBracketBreakTime && !roundObject.isLoserBracket() { includeBreakTime = true + print("Including break time for upper bracket.") } - let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex }) - - guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: includeBreakTime) }).max() else { + let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy { + $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex + } + + if previousMatchIsInPreviousRotation { + print("All previous matches are from earlier rotations, returning true.") + } else { + print("Some previous matches are from the current rotation.") + } + + guard let minimumPossibleEndDate = previousMatchSlots.map({ + $0.estimatedEndDate(includeBreakTime: includeBreakTime) + }).max() else { + print("No valid previous match end date, returning \(previousMatchIsInPreviousRotation).") return previousMatchIsInPreviousRotation } if targetedStartDate >= minimumPossibleEndDate { if rotationDifferenceIsImportant { + print("Targeted start date is after the minimum possible end date and rotation difference is important, returning \(previousMatchIsInPreviousRotation).") return previousMatchIsInPreviousRotation } else { + print("Targeted start date is after the minimum possible end date, returning true.") return true } } else { if targetedStartDate == minimumTargetedEndDate { + print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)") minimumTargetedEndDate = minimumPossibleEndDate } else { + print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) } + print("Targeted start date is before the minimum possible end date, returning false.") return false } } + func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() @@ -370,13 +426,15 @@ final class MatchScheduler : ModelObject, Storable { } func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { - var slots = [TimeMatch]() var _startDate: Date? var rotationIndex = 0 var availableMatchs = flattenedMatches.filter({ $0.startDate == nil }) let courtsUnavailability = courtsUnavailability var issueFound: Bool = false + + // Log start of the function + print("Starting roundDispatcher with \(availableMatchs.count) matches and \(numberOfCourtsAvailablePerRotation) courts available") flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in if _startDate == nil { @@ -389,24 +447,21 @@ final class MatchScheduler : ModelObject, Storable { let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime) slots.append(timeMatch) } - - if slots.isEmpty == false { + + if !slots.isEmpty { rotationIndex += 1 } var freeCourtPerRotation = [Int: [Int]]() - let availableCourt = numberOfCourtsAvailablePerRotation - var courts = initialCourts ?? (0.. 0 - while availableMatchs.count > 0 && issueFound == false { + while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { freeCourtPerRotation[rotationIndex] = [] let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate - + if shouldStartAtDispatcherDate { rotationStartDate = dispatcherStartDate shouldStartAtDispatcherDate = false @@ -414,23 +469,28 @@ final class MatchScheduler : ModelObject, Storable { courts = rotationIndex == 0 ? courts : (0.. 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], freeCourtPreviousRotation.count > 0 { - print("scenario where we are waiting for a breaktime to be over without any match to play in between or a free court was available and we need to recheck breaktime left on it") - let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }) + // Log courts availability and start date + print("Courts available at rotation \(rotationIndex): \(courts)") + print("Rotation start date: \(rotationStartDate)") + + // Check for court availability and break time conflicts + if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { + print("Handling break time conflicts or waiting for free courts") + let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) - let noBreakAlreadyTested = previousRotationSlots.anySatisfy({ $0.startDate == previousEndDateNoBreak }) + let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } + if let previousEndDate, let previousEndDateNoBreak { let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate) let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) - print("difference w break", differenceWithBreak) - print("difference w/o break", differenceWithoutBreak) + print("Difference with break: \(differenceWithBreak), without break: \(differenceWithoutBreak)") + let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) var difference = differenceWithBreak + if differenceWithBreak <= 0 { difference = differenceWithoutBreak } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { @@ -438,148 +498,127 @@ final class MatchScheduler : ModelObject, Storable { } if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate { - courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) - }) + courts.removeAll(where: { freeCourtPreviousRotation.contains($0) }) freeCourtPerRotation[rotationIndex] = courts courts = freeCourtPreviousRotation rotationStartDate = rotationStartDate.addingTimeInterval(-difference) } } - } else if let first = availableMatchs.first { - let duration = first.matchFormat.getEstimatedDuration(additionalEstimationDuration) + } else if let firstMatch = availableMatchs.first { + let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) if courtsUnavailable.count == numberOfCourtsAvailablePerRotation { - print("issue") + print("Issue: All courts unavailable in this rotation") issueFound = true } else { courts = Array(Set(courts).subtracting(Set(courtsUnavailable))) } } + // Dispatch courts and schedule matches dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) rotationIndex += 1 } - + + // Organize matches in slots var organizedSlots = [TimeMatch]() for i in 0.. courts.count { - print("roundMatchesCount \(roundMatchesCount) > \(courts.count)") - if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { - print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))") - return false - } - } - } - //if all is ok, we do a final check to see if the first - let indexInRound = match.indexInRound() - - print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)") - if roundObject.parent == nil && roundObject.index > 0, indexInRound == 0, let nextMatch = match.next() { - guard courtPosition < courts.count - 1, courts.count > 1 else { - print("next match and this match can not be played at the same time, returning false") + if shouldHandleUpperRoundSlice { + if roundObject.parent == nil && roundMatchesCount > courts.count && currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { + print("Returning false: Too many matches already played in the current rotation for round \(roundObject.roundTitle()).") return false } - if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { - - print("next match and this match can be played, returning true") - - return true - } } - - //not adding a last match of a 4-match round (final not included obviously) - print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)") + let indexInRound = match.indexInRound() - if shouldTryToFillUpCourtsAvailable == false { - if roundMatchesCount <= 4 && currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.parent == nil && ((courts.count > 1 && courtPosition >= courts.count - 1) || courts.count == 1 && availableCourts > 1) { - print("we return false") + if roundObject.parent == nil && roundObject.index > 0 && indexInRound == 0, let nextMatch = match.next() { + if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") + return true + } else { + print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).") return false } } - + print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).") return canBePlayed }) { - print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate) + print("Found match: \(firstMatch.roundAndMatchTitle()) for court \(courtIndex) at \(rotationStartDate)") + + matchPerRound[firstMatch.roundObject!.id, default: 0] += 1 + + let timeMatch = TimeMatch( + matchID: firstMatch.id, + rotationIndex: rotationIndex, + courtIndex: courtIndex, + startDate: rotationStartDate, + durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration), + minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime + ) - if first.roundObject!.parent == nil { - if let roundIndex = matchPerRound[first.roundObject!.id] { - matchPerRound[first.roundObject!.id] = roundIndex + 1 - } else { - matchPerRound[first.roundObject!.id] = 1 - } - } - let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: first.matchFormat.breakTime.breakTime) slots.append(timeMatch) - availableMatchs.removeAll(where: { $0.id == first.id }) + availableMatchs.removeAll(where: { $0.id == firstMatch.id }) } else { - freeCourtPerRotation[rotationIndex]!.append(courtIndex) + print("No suitable match found for court \(courtIndex) in rotation \(rotationIndex). Adding court to freeCourtPerRotation.") + freeCourtPerRotation[rotationIndex]?.append(courtIndex) } + } - - if freeCourtPerRotation[rotationIndex]!.count == availableCourts { - print("no match found to be put in this rotation, check if we can put anything to another date") - freeCourtPerRotation[rotationIndex] = [] - let courtsUsed = getNextEarliestAvailableDate(from: slots) - var freeCourts: [Int] = [] - if courtsUsed.isEmpty { - freeCourts = (0.. Bool { let upperRounds: [Round] = tournament.rounds() diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift index f1dbe6e..3e8ddcf 100644 --- a/PadelClub/Views/Player/Components/PlayerPopoverView.swift +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -31,7 +31,7 @@ struct PlayerPopoverView: View { @State private var source: String? - init(source: String?, sex: Int, requiredField: [PlayerCreationField] = [.firstName, .lastName], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { + init(source: String?, sex: Int, requiredField: [PlayerCreationField] = [], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { if let source { let words = source.components(separatedBy: .whitespaces) if words.isEmpty == false { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 12bda95..37aa4c5 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -259,7 +259,7 @@ struct RoundView: View { #if DEBUG Spacer() - Text(match.teamScores.count.formatted()) + Text(match.index.formatted() + " " + match.teamScores.count.formatted()) #endif } } footer: { From a0477d4fa34c8c6b0187541f28e92bcd8d897f54 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 27 Sep 2024 19:53:56 +0200 Subject: [PATCH 26/41] v1.0.15 --- 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 a2f8edc..688bcbc 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3179,7 +3179,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From 0e8cd58f742203d6f0b06ae30a73316838f2a718 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 27 Sep 2024 20:30:22 +0200 Subject: [PATCH 27/41] add loser bracket groupstage to matchscheduler --- PadelClub/Data/MatchScheduler.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 4264da2..8a0abce 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -626,16 +626,20 @@ final class MatchScheduler : ModelObject, Storable { var rounds = [Round]() + if let groupStageLoserBracketRound = tournament.groupStageLoserBracket() { + rounds.append(groupStageLoserBracketRound) + } + if shouldEndRoundBeforeStartingNext { - rounds = upperRounds.flatMap { + rounds.append(contentsOf: upperRounds.flatMap { [$0] + $0.loserRoundsAndChildren() - } + }) } else { - rounds = upperRounds.map { + rounds.append(contentsOf: upperRounds.map { $0 } + upperRounds.flatMap { $0.loserRoundsAndChildren() - } + }) } let flattenedMatches = rounds.flatMap { round in From 13be596b269cea2be223cf412a3da27309913424 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 28 Sep 2024 11:41:55 +0200 Subject: [PATCH 28/41] add new feature : double group stages and loser bracket smart auto generation --- PadelClub/Data/GroupStage.swift | 14 +++++- PadelClub/Data/MatchScheduler.swift | 15 ++++--- PadelClub/Data/Round.swift | 3 -- PadelClub/Data/Tournament.swift | 21 ++++++++- .../FixedWidthInteger+Extensions.swift | 8 ++-- .../Views/GroupStage/GroupStageView.swift | 2 +- .../GroupStage/GroupStagesSettingsView.swift | 13 +++--- .../LoserBracketFromGroupStageView.swift | 45 +++++++++++-------- PadelClub/Views/Match/MatchSummaryView.swift | 2 +- .../GroupStageScheduleEditorView.swift | 2 +- .../Views/Planning/PlanningSettingsView.swift | 2 +- PadelClub/Views/Planning/PlanningView.swift | 2 +- PadelClub/Views/Planning/SchedulerView.swift | 4 +- PadelClub/Views/Team/TeamRowView.swift | 2 +- .../Screen/TableStructureView.swift | 20 ++++++--- .../Tournament/TournamentBuildView.swift | 8 +++- 16 files changed, 110 insertions(+), 53 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 09eab0a..8ec0c09 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -71,14 +71,26 @@ final class GroupStage: ModelObject, Storable { func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String { if let name { return name } + + var stepLabel = "" + if step > 0 { + stepLabel = " (" + (step + 1).ordinalFormatted(feminine: true) + " phase)" + } + switch displayStyle { - case .wide, .title: + case .title: + return "Poule \(index + 1)" + stepLabel + case .wide: return "Poule \(index + 1)" case .short: return "#\(index + 1)" } } + var computedOrder: Int { + index + step * 100 + } + func isRunning() -> Bool { // at least a match has started _matches().anySatisfy({ $0.isRunning() }) } diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 8a0abce..2ef29f9 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -93,9 +93,9 @@ final class MatchScheduler : ModelObject, Storable { } @discardableResult - func updateGroupStageSchedule(tournament: Tournament, specificGroupStage: GroupStage? = nil) -> Date { + func updateGroupStageSchedule(tournament: Tournament, specificGroupStage: GroupStage? = nil, atStep step: Int = 0, startDate: Date? = nil) -> Date { let computedGroupStageChunkCount = groupStageChunkCount ?? tournament.getGroupStageChunkValue() - var groupStages: [GroupStage] = tournament.groupStages() + var groupStages: [GroupStage] = tournament.groupStages(atStep: step) if let specificGroupStage { groupStages = [specificGroupStage] } @@ -108,7 +108,7 @@ final class MatchScheduler : ModelObject, Storable { $0.confirmed = false }) - var lastDate : Date = tournament.startDate + var lastDate : Date = startDate ?? tournament.startDate let times = Set(groupStages.compactMap { $0.startDate }).sorted() if let first = times.first { @@ -122,8 +122,10 @@ final class MatchScheduler : ModelObject, Storable { } } - times.forEach({ time in - lastDate = time + times.forEach({ time in + if lastDate.isEarlierThan(time) { + lastDate = time + } let groups = groupStages.filter({ $0.startDate == time }) let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) @@ -743,6 +745,9 @@ final class MatchScheduler : ModelObject, Storable { if tournament.groupStageCount > 0 { lastDate = updateGroupStageSchedule(tournament: tournament) } + if tournament.groupStages(atStep: 1).isEmpty == false { + lastDate = updateGroupStageSchedule(tournament: tournament, atStep: 1, startDate: lastDate) + } return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) } } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 6f73258..caecc44 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -598,9 +598,6 @@ defer { func deleteLoserBracket() { do { let loserRounds = loserRounds() - for loserRound in loserRounds { - try loserRound.deleteDependencies() - } try self.tournamentStore.rounds.delete(contentOfs: loserRounds) } catch { Logger.error(error) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 5cec3c6..cffdbd1 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -385,6 +385,10 @@ final class Tournament : ModelObject, Storable { return groupStages.sorted(by: \.index) } + func allGroupStages() -> [GroupStage] { + return self.tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder) + } + func allRounds() -> [Round] { return Array(self.tournamentStore.rounds) } @@ -1757,7 +1761,7 @@ defer { func deleteGroupStages() { do { - try self.tournamentStore.groupStages.delete(contentOfs: groupStages()) + try self.tournamentStore.groupStages.delete(contentOfs: allGroupStages()) } catch { Logger.error(error) } @@ -2088,6 +2092,8 @@ defer { Logger.error(error) } } + + groupStages(atStep: 1).forEach { $0.buildMatches() } } func lastStep() -> Int { @@ -2095,11 +2101,24 @@ defer { } func generateSmartLoserGroupStageBracket() { + guard let groupStageLoserBracket = groupStageLoserBracket() else { return } for i in qualifiedPerGroupStage.. String { + func ordinalFormattedSuffix(feminine: Bool = false) -> String { switch self { - case 1: return "er" + case 1: return feminine ? "ère" : "er" default: return "ème" } } - func ordinalFormatted() -> String { - return self.formatted() + self.ordinalFormattedSuffix() + func ordinalFormatted(feminine: Bool = false) -> String { + return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine) } var pluralSuffix: String { diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index e1c0543..cf13eff 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -70,7 +70,7 @@ struct GroupStageView: View { _groupStageMenuView() } } - .navigationTitle(groupStage.groupStageTitle()) + .navigationTitle(groupStage.groupStageTitle(.title)) } private enum GroupStageSortingMode { diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index adf1426..ab40d64 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -10,7 +10,7 @@ import LeStorage struct GroupStagesSettingsView: View { @EnvironmentObject var dataStore: DataStore - + @Environment(\.dismiss) private var dismiss @Environment(Tournament.self) var tournament: Tournament @State private var generationDone: Bool = false let step: Int @@ -89,7 +89,6 @@ struct GroupStagesSettingsView: View { } else if let groupStageLoserBracket = tournament.groupStageLoserBracket() { RowButtonView("Supprimer les matchs de classements", role: .destructive) { do { - try groupStageLoserBracket.deleteDependencies() try tournamentStore.rounds.delete(instance: groupStageLoserBracket) } catch { Logger.error(error) @@ -98,21 +97,25 @@ struct GroupStagesSettingsView: View { } } - if tournament.lastStep() == 0, step == 0, tournament.rounds().isEmpty { + if tournament.lastStep() == 0, step == 0 { Section { RowButtonView("Ajouter une phase de poule", role: .destructive) { tournament.addNewGroupStageStep() } + } footer: { + Text("Padel Club peut vous créer une 2ème phase de poule utilisant les résultats de la première phase : les premiers de chaque poule joueront ensemble et ainsi de suite.") } } else if step > 0 { Section { RowButtonView("Supprimer cette phase de poule", role: .destructive) { - let gs = tournament.groupStages(atStep: tournament.lastStep()) + let groupStages = tournament.groupStages(atStep: tournament.lastStep()) do { - try tournament.tournamentStore.groupStages.delete(contentOfs: gs) + try tournament.tournamentStore.groupStages.delete(contentOfs: groupStages) } catch { Logger.error(error) } + + dismiss() } } diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift index 96b3511..15f41b1 100644 --- a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -33,9 +33,13 @@ struct LoserBracketFromGroupStageView: View { List { if isEditingLoserBracketGroupStage == true && displayableMatches.isEmpty == false { Section { - RowButtonView("Ajouter un match", role: .destructive) { - _addNewMatch() - } + _addButton() + } + + Section { + _smartGenerationButton() + } footer: { + Text("La génération intelligente ajoutera un match par rang entre 2 poules. Si vos poules sont terminées, Padel Club placera les équipes automatiquement.") } } @@ -75,17 +79,10 @@ struct LoserBracketFromGroupStageView: View { ContentUnavailableView { Label("Aucun match de classement", systemImage: "figure.tennis") } description: { - Text("Vous n'avez créé aucun match de classement entre les perdants de poules.") + Text("Vous n'avez créé aucun match de classement entre les perdants de poules. La génération intelligente ajoutera un match par rang entre 2 poules") } actions: { - RowButtonView("Ajouter un match") { - isEditingLoserBracketGroupStage = true - _addNewMatch() - } - - RowButtonView("Génération intelligente", role: .destructive) { - isEditingLoserBracketGroupStage = true - tournament.generateSmartLoserGroupStageBracket() - } + _addButton() + _smartGenerationButton() } } } @@ -122,15 +119,28 @@ struct LoserBracketFromGroupStageView: View { let displayableMatches = loserBracket.playedMatches().sorted(by: \.index) do { - for match in displayableMatches { - try match.deleteDependencies() - } try tournamentStore.matches.delete(contentOfs: displayableMatches) } catch { Logger.error(error) } } + + private func _smartGenerationButton() -> some View { + RowButtonView("Génération intelligente", role: .destructive, confirmationMessage: displayableMatches.isEmpty ? nil : "Les matchs de classement de poules déjà existants seront supprimés") { + isEditingLoserBracketGroupStage = true + _deleteAllMatches() + tournament.generateSmartLoserGroupStageBracket() + } + } + + private func _addButton() -> some View { + RowButtonView("Ajouter un match") { + isEditingLoserBracketGroupStage = true + _addNewMatch() + } + } + } struct GroupStageLoserBracketMatchFooterView: View { @@ -160,7 +170,6 @@ struct GroupStageLoserBracketMatchFooterView: View { Spacer() FooterButtonView("Effacer", role: .destructive) { do { - try match.deleteDependencies() try match.tournamentStore.matches.delete(instance: match) } catch { Logger.error(error) @@ -206,5 +215,5 @@ struct GroupStageLoserBracketMatchFooterView: View { } catch { Logger.error(error) } - } + } } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 5ba96eb..5b890f0 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -28,7 +28,7 @@ struct MatchSummaryView: View { self.color = Color(white: 0.9) if let groupStage = match.groupStageObject { - self.roundTitle = groupStage.groupStageTitle() + self.roundTitle = groupStage.groupStageTitle(.title) } else if let round = match.roundObject { self.roundTitle = round.roundTitle(matchViewStyle == .feedStyle ? .wide : .short) } else { diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift index 32d2347..50b7cf4 100644 --- a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -27,7 +27,7 @@ struct GroupStageScheduleEditorView: View { } var body: some View { - GroupStageDatePickingView(title: groupStage.groupStageTitle(), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { groupStage.startDate = startDate tournament.matchScheduler()?.updateGroupStageSchedule(tournament: tournament, specificGroupStage: groupStage) _save() diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 230b748..ed03bc6 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -114,7 +114,7 @@ struct PlanningSettingsView: View { } let allMatches = tournament.allMatches() - let allGroupStages = tournament.groupStages() + let allGroupStages = tournament.allGroupStages() let allRounds = tournament.allRounds() let matchesWithDate = allMatches.filter({ $0.startDate != nil }) let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 9dd42da..903ec08 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -98,7 +98,7 @@ struct PlanningView: View { } } label: { if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle()) + Text(groupStage.groupStageTitle(.title)) } else if let round = match.roundObject { Text(round.roundTitle()) } diff --git a/PadelClub/Views/Planning/SchedulerView.swift b/PadelClub/Views/Planning/SchedulerView.swift index 19d593d..333357e 100644 --- a/PadelClub/Views/Planning/SchedulerView.swift +++ b/PadelClub/Views/Planning/SchedulerView.swift @@ -43,7 +43,7 @@ struct SchedulerView: View { } } .onChange(of: tournament.groupStageMatchFormat) { - let groupStages = tournament.groupStages() + let groupStages = tournament.allGroupStages() groupStages.forEach { groupStage in groupStage.updateMatchFormat(tournament.groupStageMatchFormat) } @@ -68,7 +68,7 @@ struct SchedulerView: View { } } - ForEach(tournament.groupStages()) { + ForEach(tournament.allGroupStages()) { GroupStageScheduleEditorView(groupStage: $0, tournament: tournament) .id(UUID()) } diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 458d4e3..114d324 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -19,7 +19,7 @@ struct TeamRowView: View { VStack(alignment: .leading) { if let groupStage = team.groupStageObject() { HStack { - Text(groupStage.groupStageTitle()) + Text(groupStage.groupStageTitle(.title)) if let finalPosition = groupStage.finalPosition(ofTeam: team) { Text((finalPosition + 1).ordinalFormatted()) } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 5ce64f2..60a6120 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -174,13 +174,13 @@ struct TableStructureView: View { Section { let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0) if groupStageCount > 0 { - LabeledContent { - Text(teamsFromGroupStages.formatted()) - } label: { - Text("Équipes en poule") - } - if structurePreset == .manual { + LabeledContent { + Text(teamsFromGroupStages.formatted()) + } label: { + Text("Équipes en poule") + } + LabeledContent { Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted()) @@ -203,6 +203,14 @@ struct TableStructureView: View { } label: { Text("Équipes en tableau final") } + } else { + LabeledContent { + let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount + let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage + Text((mp1 + mp2).formatted()) + } label: { + Text("Total de matchs") + } } } diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index 7aa4836..0266797 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -30,7 +30,11 @@ struct TournamentBuildView: View { ProgressView() } } label: { - Text("Poules") + if tournament.groupStages(atStep: 1).isEmpty == false { + Text("1ère phase de poules") + } else { + Text("Poules") + } if tournament.groupStagesAreOver(), tournament.moreQualifiedToDraw() > 0 { let moreQualifiedToDraw = tournament.moreQualifiedToDraw() Text("Qualifié\(moreQualifiedToDraw.pluralSuffix) sortant\(moreQualifiedToDraw.pluralSuffix) manquant\(moreQualifiedToDraw.pluralSuffix)").foregroundStyle(.logoRed) @@ -56,7 +60,7 @@ struct TournamentBuildView: View { } if tournament.groupStages(atStep: 1).isEmpty == false { - NavigationLink("Step 1") { + NavigationLink("2ème phase de poules") { GroupStagesView(tournament: tournament, step: 1) } } From b2f38febc899b5f0d00859d7615763c579da2484 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 28 Sep 2024 17:34:13 +0200 Subject: [PATCH 29/41] fix searching players issues --- .../Coredata/ImportedPlayer+Extensions.swift | 3 +- PadelClub/Data/Tournament.swift | 36 +++- .../Extensions/Calendar+Extensions.swift | 4 +- PadelClub/Utils/PadelRule.swift | 22 +++ .../Navigation/Agenda/ActivityView.swift | 86 +++++++-- .../Views/Tournament/Screen/AddTeamView.swift | 173 +++++++++++++++--- .../Components/InscriptionInfoView.swift | 19 ++ PadelClubTests/ServerDataTests.swift | 4 +- 8 files changed, 294 insertions(+), 53 deletions(-) diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index b7679f3..ba05ab0 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -74,7 +74,8 @@ extension ImportedPlayer: PlayerHolder { firstName?.localizedCaseInsensitiveContains(searchField) == true || lastName?.localizedCaseInsensitiveContains(searchField) == true } - func hitForSearch(_ searchText: String) -> Int { + func hitForSearch(_ searchText: String?) -> Int { + guard let searchText else { return 0 } var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current) trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ") trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .symbols, replacementString: " ") diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index cffdbd1..89008b0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1009,15 +1009,39 @@ defer { return [] } return players.filter { player in - if player.rank == nil { return false } - if player.computedRank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) { - return true - } else { - return false - } + return isPlayerRankInadequate(player: player) + } + } + + func isPlayerRankInadequate(player: PlayerHolder) -> Bool { + guard let rank = player.getRank() else { return false } + let _rank = player.male ? rank : rank + PlayerRegistration.addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0) + if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) { + return true + } else { + return false + } + } + + func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { + if startDate.isInCurrentYear() == false { + return [] + } + return players.filter { player in + return isPlayerAgeInadequate(player: player) + } + } + + func isPlayerAgeInadequate(player: PlayerHolder) -> Bool { + guard let computedAge = player.computedAge else { return false } + if federalTournamentAge.isAgeValid(age: computedAge) == false { + return true + } else { + return false } } + func mandatoryRegistrationCloseDate() -> Date? { switch tournamentLevel { case .p500, .p1000, .p1500, .p2000: diff --git a/PadelClub/Extensions/Calendar+Extensions.swift b/PadelClub/Extensions/Calendar+Extensions.swift index 47971b5..bc7861a 100644 --- a/PadelClub/Extensions/Calendar+Extensions.swift +++ b/PadelClub/Extensions/Calendar+Extensions.swift @@ -30,8 +30,8 @@ extension Calendar { let currentYear = component(.year, from: currentDate) // Define the date components for 1st September and 31st December of the current year - var septemberFirstComponents = DateComponents(year: currentYear, month: 9, day: 1) - var decemberThirtyFirstComponents = DateComponents(year: currentYear, month: 12, day: 31) + let septemberFirstComponents = DateComponents(year: currentYear, month: 9, day: 1) + let decemberThirtyFirstComponents = DateComponents(year: currentYear, month: 12, day: 31) // Get the actual dates for 1st September and 31st December let septemberFirst = date(from: septemberFirstComponents)! diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index de58eb2..801276f 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -276,6 +276,28 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { var tournamentDescriptionLabel: String { return localizedLabel() } + + func isAgeValid(age: Int?) -> Bool { + guard let age else { return true } + switch self { + case .unlisted: + return true + case .a11_12: + return age < 13 + case .a13_14: + return age < 15 + case .a15_16: + return age < 17 + case .a17_18: + return age < 19 + case .senior: + return age >= 11 + case .a45: + return age >= 45 + case .a55: + return age >= 55 + } + } } enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index b7a243f..58eb1fb 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -23,9 +23,10 @@ struct ActivityView: View { @State private var presentClubSearchView: Bool = false @State private var quickAccessScreen: QuickAccessScreen? = nil @State private var displaySearchView: Bool = false + @State private var pasteString: String? = nil enum QuickAccessScreen : Identifiable, Hashable { - case inscription(pasteString: String) + case inscription var id: String { switch self { @@ -75,13 +76,29 @@ struct ActivityView: View { @ViewBuilder private func _pasteView() -> some View { - PasteButton(payloadType: String.self) { strings in - guard let first = strings.first else { return } - quickAccessScreen = .inscription(pasteString: first) + Button { + quickAccessScreen = .inscription + } label: { + Image(systemName: "person.crop.circle.badge.plus") + .resizable() + .scaledToFit() + .frame(minHeight: 32) } - .foregroundStyle(.master) - .labelStyle(.iconOnly) - .buttonBorderShape(.capsule) + .accessibilityLabel("Ajouter une équipe") + +// if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true { +// PasteButton(payloadType: String.self) { strings in +// let first = strings.first ?? "aucun texte" +// quickAccessScreen = .inscription(pasteString: first) +// } +// .foregroundStyle(.master) +// .labelStyle(.iconOnly) +// .buttonBorderShape(.capsule) +// .onAppear { +// pasteButtonIsDisplayed = true +// } +// } else if let pasteButtonIsDisplayed, pasteButtonIsDisplayed == false { +// } } var body: some View { @@ -189,6 +206,10 @@ struct ActivityView: View { .navigationDestination(for: Tournament.self) { tournament in TournamentView(tournament: tournament) } +// .onDisappear(perform: { +// pasteButtonIsDisplayed = nil +// print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed) +// }) .toolbar { ToolbarItemGroup(placement: .topBarLeading) { Button { @@ -291,28 +312,41 @@ struct ActivityView: View { } .sheet(item: $quickAccessScreen) { screen in switch screen { - case .inscription(let pasteString): + case .inscription: NavigationStack { List { - Section { - Text(pasteString) - } header: { - Text("Contenu du presse-papier") + + if let pasteString { + Section { + Text(pasteString) + .frame(maxWidth: .infinity) + .overlay { + if pasteString.isEmpty { + Text("Le presse-papier est vide") + .foregroundStyle(.secondary) + .italic() + } + } + } header: { + Text("Contenu du presse-papier") + } } - + Section { ForEach(getRunningTournaments()) { tournament in NavigationLink { AddTeamView(tournament: tournament, pasteString: pasteString, editedTeam: nil) } label: { - VStack(alignment: .leading) { + LabeledContent { + Text(tournament.unsortedTeamsWithoutWO().count.formatted()) + } label: { Text(tournament.tournamentTitle()) - Text(tournament.formattedDate()).foregroundStyle(.secondary) + Text(tournament.formattedDate()) } } } } header: { - Text("À coller dans la liste d'inscription") + Text("Ajouter à la liste d'inscription") } } .toolbar { @@ -321,6 +355,26 @@ struct ActivityView: View { self.quickAccessScreen = nil } } + + ToolbarItem(placement: .topBarTrailing) { + Button { + pasteString = UIPasteboard.general.string ?? "" + } label: { + Label("Coller", systemImage: "doc.on.clipboard").labelStyle(.iconOnly) + } + .foregroundStyle(.master) + .labelStyle(.iconOnly) + .buttonBorderShape(.capsule) + } + + ToolbarItem(placement: .bottomBar) { + PasteButton(payloadType: String.self) { strings in + pasteString = strings.first ?? "" + } + .foregroundStyle(.master) + .labelStyle(.titleAndIcon) + .buttonBorderShape(.capsule) + } } .navigationTitle("Choix du tournoi") .navigationBarTitleDisplayMode(.inline) diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index ca7ad1e..e7d65b1 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -42,7 +42,13 @@ struct AddTeamView: View { @State private var confirmHomonym: Bool = false @State private var editableTextField: String = "" @State private var textHeight: CGFloat = 100 // Default height - + @State private var hitsForSearch: [Int: Int] = [:] + @State private var searchForHit: Int = 0 + @State private var displayWarningNotEnoughCharacter: Bool = false + @State private var testMessageIndex: Int = 0 + + let filterLimit : Int = 1000 + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -74,7 +80,7 @@ struct AddTeamView: View { } var body: some View { - if pasteString != nil, fetchPlayers.isEmpty == false { + if let pasteString, pasteString.isEmpty == false, fetchPlayers.isEmpty == false { computedBody .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher dans les résultats")) } else { @@ -86,14 +92,27 @@ struct AddTeamView: View { List(selection: $createdPlayerIds) { _buildingTeamView() } - .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here - if let pasteString, count == 2, autoSelect == true { - fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in + .onReceive(fetchPlayers.publisher.count()) { receivedCount in // <-- here + if receivedCount < filterLimit, let pasteString, pasteString.isEmpty == false, count == 2, autoSelect == true { + fetchPlayers.filter { hitForSearch($0, pasteString) >= hitTarget }.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) }).forEach { player in createdPlayerIds.insert(player.license!) } autoSelect = false } } + .overlay(alignment: .bottom) { + if displayWarningNotEnoughCharacter { + Text("2 lettres mininum") + .toastFormatted() + .animation(.easeInOut(duration: 2.0), value: displayWarningNotEnoughCharacter) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + displayWarningNotEnoughCharacter = false + } + } + } + } + .alert("Présence d'homonyme", isPresented: $confirmHomonym) { Button("Créer l'équipe quand même") { _createTeam(checkDuplicates: false, checkHomonym: false) @@ -157,7 +176,7 @@ struct AddTeamView: View { if pasteString == nil { ToolbarItem(placement: .bottomBar) { PasteButton(payloadType: String.self) { strings in - guard let first = strings.first else { return } + let first = strings.first ?? "" handlePasteString(first) } .foregroundStyle(.master) @@ -165,6 +184,26 @@ struct AddTeamView: View { .buttonBorderShape(.capsule) } } + + ToolbarItem(placement: .topBarTrailing) { + Button { + let generalString = UIPasteboard.general.string ?? "" + + #if targetEnvironment(simulator) + let s = testMessages[testMessageIndex % testMessages.count] + handlePasteString(s) + testMessageIndex += 1 + #else + handlePasteString(generalString) + #endif + } label: { + Label("Coller", systemImage: "doc.on.clipboard").labelStyle(.iconOnly) + } + .foregroundStyle(.master) + .labelStyle(.iconOnly) + .buttonBorderShape(.capsule) + } + } .navigationBarBackButtonHidden(true) .toolbarBackground(.visible, for: .navigationBar) @@ -365,8 +404,12 @@ struct AddTeamView: View { } Spacer() Button("Chercher") { - self.handlePasteString(editableTextField) - self.focusedField = nil + if editableTextField.count > 1 { + self.handlePasteString(editableTextField) + self.focusedField = nil + } else { + self.displayWarningNotEnoughCharacter = true + } } .buttonStyle(.bordered) } @@ -393,7 +436,11 @@ struct AddTeamView: View { if let p = createdPlayers.first(where: { $0.id == id }) { VStack(alignment: .leading, spacing: 0) { if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == false { - Text("Déjà inscrit !!").foregroundStyle(.logoRed).bold() + Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } else if tournament.isPlayerAgeInadequate(player: p) { + Text("Âge invalide !").foregroundStyle(.logoRed).bold() + } else if tournament.isPlayerRankInadequate(player: p) { + Text("Trop bien classé !").foregroundStyle(.logoRed).bold() } PlayerView(player: p).tag(p.id) .environment(tournament) @@ -401,8 +448,12 @@ struct AddTeamView: View { } if let p = fetchPlayers.first(where: { $0.license == id }) { VStack(alignment: .leading, spacing: 0) { - if pasteString != nil, unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { + if let pasteString, pasteString.isEmpty == false, unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } else if tournament.isPlayerAgeInadequate(player: p) { + Text("Âge invalide !").foregroundStyle(.logoRed).bold() + } else if tournament.isPlayerRankInadequate(player: p) { + Text("Trop bien classé !").foregroundStyle(.logoRed).bold() } ImportedPlayerView(player: p).tag(p.license!) } @@ -454,8 +505,8 @@ struct AddTeamView: View { } - if let pasteString { - let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty }) + if let pasteString, pasteString.isEmpty == false { + let sortedPlayers = _searchFilteredPlayers() if sortedPlayers.isEmpty { ContentUnavailableView { @@ -478,20 +529,44 @@ struct AddTeamView: View { } } else { - _listOfPlayers(pasteString: pasteString) + _listOfPlayers(searchFilteredPlayers: sortedPlayers, pasteString: pasteString) } } else { _managementView() } } + @MainActor + func hitForSearch(_ ip: ImportedPlayer, _ pasteString: String?) -> Int { + guard let pasteString else { return 0 } + let _searchForHit = pasteString.hashValue + + if searchForHit != _searchForHit { + DispatchQueue.main.async { + searchForHit = _searchForHit + hitsForSearch = [:] + } + } + + let value = hitsForSearch[ip.id.hashValue] + if let value { + return value + } else { + let hit = ip.hitForSearch(pasteString) + DispatchQueue.main.async { + hitsForSearch[ip.id.hashValue] = hit + } + return hit + } + } + private var count: Int { - return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count + return fetchPlayers.filter { hitForSearch($0, pasteString) >= hitTarget }.count } private var hitTarget: Int { if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 { - if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 } + if fetchPlayers.filter({ hitForSearch($0, pasteString) == 100 }).count == 2 { return 100 } } else { return 2 } @@ -506,24 +581,22 @@ struct AddTeamView: View { } } + @MainActor private func handlePasteString(_ first: String) { - Task { - await MainActor.run { - fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - pasteString = first - editableTextField = first - textHeight = Self._calculateHeight(text: first) - autoSelect = true - } + if first.isEmpty == false { + 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 + textHeight = Self._calculateHeight(text: first) } @ViewBuilder - private func _listOfPlayers(pasteString: String) -> some View { - let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty }).sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }) + private func _listOfPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> some View { + let sortedPlayers = _sortedPlayers(searchFilteredPlayers: searchFilteredPlayers, pasteString: pasteString) Section { ForEach(sortedPlayers) { player in @@ -535,4 +608,50 @@ struct AddTeamView: View { } } + + private func _searchFilteredPlayers() -> [ImportedPlayer] { + if searchField.isEmpty { + return Array(fetchPlayers) + } else { + return fetchPlayers.filter({ $0.contains(searchField) }) + } + } + + private func _sortedPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> [ImportedPlayer] { + if searchFilteredPlayers.count < filterLimit { + return searchFilteredPlayers.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) }) + } else { + return searchFilteredPlayers + } + } } + +let testMessages = [ + "Anthony dovetta ( 3620578 K )et christophe capeau ( 4666443v)", +""" +ok merci, il s'agit de : +Olivier Seguin - licence 5033439 +JPascal Bondierlange - licence : +6508359 С +Cordialement +""", +""" +Bonsoir Lise, peux tu nous inscrire pour le 250 hommes du 15 au 17 novembre ? +Paires DESCHAMPS/PARDO. En te remerciant. Bonne soirée +Franck +""", +""" +Coucou inscription pour le tournoi du 11 / +12 octobre +Dumoutier/ Liagre Charlotte +Merci de ta confirmation" +""", +""" +Anthony Contet 6081758f +Tullou Benjamin 8990867f +""", +""" +Sms Julien La Croix +33622886688 +Salut Raz, c'est ! Ju Lacroix J'espère que tu vas bien depuis le temps! Est-ce que tu peux nous inscrire au 1000 de Bandol avec Derek Gerson stp? +""" +] diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index 2114d81..4161e47 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -18,6 +18,7 @@ struct InscriptionInfoView: View { @State private var duplicates : [PlayerRegistration] = [] @State private var problematicPlayers : [PlayerRegistration] = [] @State private var inadequatePlayers : [PlayerRegistration] = [] + @State private var ageInadequatePlayers : [PlayerRegistration] = [] @State private var playersWithoutValidLicense : [PlayerRegistration] = [] @State private var entriesFromBeachPadel : [TeamRegistration] = [] @State private var playersMissing : [TeamRegistration] = [] @@ -177,6 +178,23 @@ struct InscriptionInfoView: View { Text("Il s'agit des joueurs ou joueuses dont le rang est inférieur à la limite fédérale.") } + Section { + DisclosureGroup { + ForEach(ageInadequatePlayers) { player in + ImportedPlayerView(player: player) + } + } label: { + LabeledContent { + Text(ageInadequatePlayers.count.formatted()) + } label: { + Text("Joueurs trop jeunes ou trop âgés") + } + } + .listRowView(color: .logoRed) + } footer: { + Text("Il s'agit des joueurs ou joueuses dont l'âge sportif est inférieur ou supérieur à la limite fédérale.") + } + Section { DisclosureGroup { ForEach(playersWithoutValidLicense) { @@ -228,6 +246,7 @@ struct InscriptionInfoView: View { homonyms = tournament.homonyms(in: players) problematicPlayers = players.filter({ $0.sex == nil }) inadequatePlayers = tournament.inadequatePlayers(in: players) + ageInadequatePlayers = tournament.ageInadequatePlayers(in: players) playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players) entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() }) playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 7d19cb5..3947937 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -150,7 +150,7 @@ final class ServerDataTests: XCTestCase { return } - let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, matchFormat: MatchFormat.nineGames, startDate: Date(), name: "Yeah!") + let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, matchFormat: MatchFormat.nineGames, startDate: Date(), name: "Yeah!", step: 1) let gs: GroupStage = try await StoreCenter.main.service().post(groupStage) assert(gs.tournament == groupStage.tournament) @@ -159,6 +159,8 @@ final class ServerDataTests: XCTestCase { assert(gs.size == groupStage.size) assert(gs.matchFormat == groupStage.matchFormat) assert(gs.startDate != nil) + assert(gs.step == groupStage.step) + } From 6f929c44cf0131064d07cd78d46b6ba8dc7a93a5 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 28 Sep 2024 19:38:34 +0200 Subject: [PATCH 30/41] fix search stuff --- PadelClub.xcodeproj/project.pbxproj | 4 +- PadelClub/Data/PlayerRegistration.swift | 23 +++---- PadelClub/Data/TeamRegistration.swift | 1 + .../Player/Components/PlayerPopoverView.swift | 2 +- .../Shared/SelectablePlayerListView.swift | 2 +- .../Team/Components/TeamWeightView.swift | 17 +++-- .../Views/Tournament/Screen/AddTeamView.swift | 69 +++++++++++-------- 7 files changed, 66 insertions(+), 52 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 688bcbc..f7d304a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3295,7 +3295,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3337,7 +3337,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 2e966ec..bb46b2d 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -73,6 +73,7 @@ final class PlayerRegistration: ModelObject, Storable { self.ligueName = importedPlayer.ligueName self.assimilation = importedPlayer.assimilation self.source = .frenchFederation + self.birthdate = importedPlayer.birthYear } internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { @@ -123,19 +124,17 @@ final class PlayerRegistration: ModelObject, Storable { var computedAge: Int? { if let birthdate { let components = birthdate.components(separatedBy: "/") - if components.count == 3 { - if let age = components.last, let ageInt = Int(age) { - let year = Calendar.current.getSportAge() - - if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier - if ageInt < 23 { - return year - 2000 - ageInt - } else { - return year - 2000 + 100 - ageInt - } - } else { //si l'année est représenté sur 4 chiffres - return year - ageInt + if let age = components.last, let ageInt = Int(age) { + let year = Calendar.current.getSportAge() + + if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier + if ageInt < 23 { + return year - 2000 - ageInt + } else { + return year - 2000 + 100 - ageInt } + } else { //si l'année est représenté sur 4 chiffres + return year - ageInt } } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 67a84e4..ea0f42d 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -242,6 +242,7 @@ final class TeamRegistration: ModelObject, Storable { let arrayOfIds : [String] = unsortedPlayers().compactMap({ $0.licenceId?.strippedLicense?.canonicalVersion }) let ids : Set = Set(arrayOfIds.sorted()) let searchedIds = Set(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted()) + if ids.isEmpty || searchedIds.isEmpty { return false } return ids.hashValue == searchedIds.hashValue } diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift index 3e8ddcf..9bd893b 100644 --- a/PadelClub/Views/Player/Components/PlayerPopoverView.swift +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -31,7 +31,7 @@ struct PlayerPopoverView: View { @State private var source: String? - init(source: String?, sex: Int, requiredField: [PlayerCreationField] = [], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { + init(source: String? = nil, sex: Int, requiredField: [PlayerCreationField] = [], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { if let source { let words = source.components(separatedBy: .whitespaces) if words.isEmpty == false { diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 8ecd8cc..c555a1a 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -1033,7 +1033,7 @@ struct MySearchView: View { Text(searchViewModel.contentUnavailableMessage) } actions: { - RowButtonView("Lancer une nouvelle recherche") { + RowButtonView("Nouvelle recherche") { searchViewModel.debouncableText = "" } .padding() diff --git a/PadelClub/Views/Team/Components/TeamWeightView.swift b/PadelClub/Views/Team/Components/TeamWeightView.swift index e74226c..f19b6e6 100644 --- a/PadelClub/Views/Team/Components/TeamWeightView.swift +++ b/PadelClub/Views/Team/Components/TeamWeightView.swift @@ -8,17 +8,16 @@ import SwiftUI struct TeamWeightView: View { - var team: TeamRegistration + @EnvironmentObject var dataStore: DataStore + let team: TeamRegistration var teamPosition: TeamPosition? = nil - var teamIndex: Int? - var displayWeight: Bool = true + + var teamIndex: Int? { + team.tournamentObject()?.indexOf(team: team) + } - init(team: TeamRegistration, teamPosition: TeamPosition? = nil) { - self.team = team - self.teamPosition = teamPosition - let tournament = team.tournamentObject() - self.teamIndex = tournament?.indexOf(team: team) - self.displayWeight = tournament?.hideWeight() == false + var displayWeight: Bool { + team.tournamentObject()?.hideWeight() == false } var body: some View { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index e7d65b1..518c8a7 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -7,16 +7,15 @@ import SwiftUI import LeStorage +import CoreData struct AddTeamView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) var dismiss - @FetchRequest( - sortDescriptors: [], - animation: .default) - private var fetchPlayers: FetchedResults + private var fetchRequest: FetchRequest + private var fetchPlayers: FetchedResults { fetchRequest.wrappedValue } var tournament: Tournament var cancelShouldDismiss: Bool = false @@ -69,14 +68,19 @@ struct AddTeamView: View { _createdPlayerIds = .init(wrappedValue: createdPlayerIds) } + let request: NSFetchRequest = ImportedPlayer.fetchRequest() + request.sortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] + request.fetchLimit = 1000 if let pasteString { _pasteString = .init(wrappedValue: pasteString) - _fetchPlayers = FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption)) + request.predicate = SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption) _autoSelect = .init(wrappedValue: true) _editableTextField = .init(wrappedValue: pasteString) _textHeight = .init(wrappedValue: Self._calculateHeight(text: pasteString)) cancelShouldDismiss = true } + + fetchRequest = FetchRequest(fetchRequest: request, animation: .default) } var body: some View { @@ -138,12 +142,9 @@ struct AddTeamView: View { } message: { Text("Cette équipe existe déjà dans votre liste d'inscription.") } - .sheet(isPresented: $presentPlayerSearch, onDismiss: { - selectionSearchField = nil - }) { + .sheet(isPresented: $presentPlayerSearch) { NavigationStack { SelectablePlayerListView(allowSelection: -1, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in - selectionSearchField = nil players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) newPlayer.setComputedRank(in: tournament) @@ -151,15 +152,24 @@ struct AddTeamView: View { createdPlayerIds.insert(newPlayer.id) } } contentUnavailableAction: { searchViewModel in - selectionSearchField = searchViewModel.searchText presentPlayerSearch = false - presentPlayerCreation = true + selectionSearchField = searchViewModel.searchText } } .tint(.master) } .sheet(isPresented: $presentPlayerCreation) { - PlayerPopoverView(source: _searchSource(), sex: _addPlayerSex()) { p in + PlayerPopoverView(sex: _addPlayerSex()) { p in + p.setComputedRank(in: tournament) + createdPlayers.insert(p) + createdPlayerIds.insert(p.id) + } + .tint(.master) + } + .sheet(item: $selectionSearchField, onDismiss: { + selectionSearchField = nil + }) { selectionSearchField in + PlayerPopoverView(source: selectionSearchField, sex: _addPlayerSex()) { p in p.setComputedRank(in: tournament) createdPlayers.insert(p) createdPlayerIds.insert(p.id) @@ -235,7 +245,11 @@ struct AddTeamView: View { Section { RowButtonView("Créer un non classé / non licencié") { - presentPlayerCreation = true + if let pasteString, pasteString.isEmpty == false { + selectionSearchField = pasteString + } else { + presentPlayerCreation = true + } } } footer: { Text("Si le joueur n'a pas encore de licence ou n'a pas encore participé à une compétition, vous pouvez le créer vous-même.") @@ -257,11 +271,7 @@ struct AddTeamView: View { private func _filterOption() -> PlayerFilterOption { return tournament.tournamentCategory.playerFilterOption } - - private func _searchSource() -> String? { - selectionSearchField ?? pasteString - } - + private func _currentSelection() -> Set { var currentSelection = Set() createdPlayerIds.compactMap { id in @@ -437,9 +447,11 @@ struct AddTeamView: View { VStack(alignment: .leading, spacing: 0) { if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == false { Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() - } else if tournament.isPlayerAgeInadequate(player: p) { + } + if tournament.isPlayerAgeInadequate(player: p) { Text("Âge invalide !").foregroundStyle(.logoRed).bold() - } else if tournament.isPlayerRankInadequate(player: p) { + } + if tournament.isPlayerRankInadequate(player: p) { Text("Trop bien classé !").foregroundStyle(.logoRed).bold() } PlayerView(player: p).tag(p.id) @@ -450,9 +462,11 @@ struct AddTeamView: View { VStack(alignment: .leading, spacing: 0) { if let pasteString, pasteString.isEmpty == false, unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() - } else if tournament.isPlayerAgeInadequate(player: p) { + } + if tournament.isPlayerAgeInadequate(player: p) { Text("Âge invalide !").foregroundStyle(.logoRed).bold() - } else if tournament.isPlayerRankInadequate(player: p) { + } + if tournament.isPlayerRankInadequate(player: p) { Text("Trop bien classé !").foregroundStyle(.logoRed).bold() } ImportedPlayerView(player: p).tag(p.license!) @@ -515,7 +529,7 @@ struct AddTeamView: View { Text("Aucun joueur classé n'a été trouvé dans ce message. Attention, si un joueur n'a pas joué de tournoi dans les 12 derniers, Padel Club ne pourra pas le trouver.") } actions: { RowButtonView("Créer un joueur non classé") { - presentPlayerCreation = true + selectionSearchField = pasteString } RowButtonView("Chercher dans la base") { @@ -584,16 +598,17 @@ struct AddTeamView: View { @MainActor private func handlePasteString(_ first: String) { if first.isEmpty == false { - fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - autoSelect = true + 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 + } } pasteString = first editableTextField = first textHeight = Self._calculateHeight(text: first) } - @ViewBuilder private func _listOfPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> some View { let sortedPlayers = _sortedPlayers(searchFilteredPlayers: searchFilteredPlayers, pasteString: pasteString) From 5ec65c88d57e634fc7386c1a6a908687f9ba1cbe Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 28 Sep 2024 19:53:04 +0200 Subject: [PATCH 31/41] v1.0.16 b1 --- 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 f7d304a..831567e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3158,7 +3158,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.16; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3179,7 +3179,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3202,7 +3202,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.16; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From eca125ef731f274452285993293462eeb59a6824 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 29 Sep 2024 11:15:48 +0200 Subject: [PATCH 32/41] fix little glitches --- .../Views/Tournament/Screen/TableStructureView.swift | 8 ++++---- .../Views/Tournament/Shared/TournamentCellView.swift | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 60a6120..4d3e783 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -109,16 +109,16 @@ struct TableStructureView: View { if structurePreset == .manual { LabeledContent { - StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) + StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) } label: { - Text("Qualifiés par poule") + Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule") } if qualifiedPerGroupStage < teamsPerGroupStage - 1 { LabeledContent { - StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) + StepperView(count: $groupStageAdditionalQualified, minimum: 1, maximum: maxMoreQualified) } label: { - Text("Qualifiés supplémentaires") + Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires") Text(moreQualifiedLabel) } .onChange(of: groupStageAdditionalQualified) { diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 62c22d1..ed82548 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -11,7 +11,6 @@ import LeStorage struct TournamentCellView: View { @EnvironmentObject var dataStore: DataStore @Environment(NavigationViewModel.self) private var navigation - @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel let tournament: FederalTournamentHolder // let color: Color = .black @@ -25,7 +24,7 @@ struct TournamentCellView: View { var body: some View { ForEach(tournament.tournaments, id: \.id) { build in if let federalTournament = tournament as? FederalTournament { - if federalDataViewModel.isFederalTournamentValidForFilters(federalTournament, build: build) { + if FederalDataViewModel.shared.isFederalTournamentValidForFilters(federalTournament, build: build) { if navigation.agendaDestination == .around { NavigationLink { TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) From 6e68226ac7ce89d541378030df13f926b84d57c0 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 29 Sep 2024 13:22:31 +0200 Subject: [PATCH 33/41] fix some stuff on team selection and add a special animation mode for selecting players from club --- PadelClub.xcodeproj/project.pbxproj | 4 +- .../Data/Federal/FederalTournament.swift | 10 ++++- .../Federal/FederalTournamentHolder.swift | 4 +- PadelClub/Data/Tournament.swift | 41 ++++++++++++++++- PadelClub/Extensions/String+Extensions.swift | 1 + PadelClub/Utils/DisplayContext.swift | 38 ++++++++++++++++ PadelClub/Utils/PadelRule.swift | 8 +++- .../GroupStageTeamReplacementView.swift | 2 +- .../Navigation/Toolbox/ToolboxView.swift | 2 +- .../Views/Shared/ImportedPlayerView.swift | 1 + .../Shared/SelectablePlayerListView.swift | 12 +++-- .../Views/Tournament/Screen/AddTeamView.swift | 44 +++++++++++++++---- .../Shared/TournamentCellView.swift | 15 ++++--- 13 files changed, 156 insertions(+), 26 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 831567e..2bc0a9a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,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\""; @@ -3179,7 +3179,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; diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index 63595b5..e61f36c 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -210,9 +210,17 @@ extension FederalTournament: FederalTournamentHolder { nomClub ?? villeEngagement ?? installation?.nom ?? "" } - func subtitleLabel() -> String { + func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String { "" } + + func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { + build.level.localizedLevelLabel(displayStyle) + } + + func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool { + true + } } // MARK: - CategorieAge diff --git a/PadelClub/Data/Federal/FederalTournamentHolder.swift b/PadelClub/Data/Federal/FederalTournamentHolder.swift index b2e3890..4ee6bd6 100644 --- a/PadelClub/Data/Federal/FederalTournamentHolder.swift +++ b/PadelClub/Data/Federal/FederalTournamentHolder.swift @@ -14,9 +14,11 @@ protocol FederalTournamentHolder { var codeClub: String? { get } var tournaments: [any TournamentBuildHolder] { get } func clubLabel() -> String - func subtitleLabel() -> String + func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String var dayDuration: Int { get } var dayPeriod: DayPeriod { get } + func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String + func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool } extension FederalTournamentHolder { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 89008b0..9edcef5 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -2251,6 +2251,20 @@ extension Tournament: Hashable { } extension Tournament: FederalTournamentHolder { + + func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { + if isAnimation() { + if let name { + return name.trunc(length: DeviceHelper.charLength()) + } else if build.age == .unlisted, build.category == .unlisted { + return build.level.localizedLevelLabel(.title) + } else { + return build.level.localizedLevelLabel(displayStyle) + } + } + return build.level.localizedLevelLabel(displayStyle) + } + var codeClub: String? { club()?.code } @@ -2261,8 +2275,18 @@ extension Tournament: FederalTournamentHolder { locationLabel() } - func subtitleLabel() -> String { - subtitle() + func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String { + if isAnimation() { + if displayAgeAndCategory(forBuild: build) == false { + return [build.category.localizedLabel(), build.age.localizedLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") + } else if name != nil { + return build.level.localizedLevelLabel(.title) + } else { + return "" + } + } else { + return subtitle() + } } var tournaments: [any TournamentBuildHolder] { @@ -2280,6 +2304,19 @@ extension Tournament: FederalTournamentHolder { return .weekend } } + + func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool { + if isAnimation() { + if let name, name.count < DeviceHelper.maxCharacter() { + return true + } else if build.age == .unlisted, build.category == .unlisted { + return true + } else { + return DeviceHelper.isBigScreen() + } + } + return true + } } extension Tournament: TournamentBuildHolder { diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index ee29135..cce72bc 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -10,6 +10,7 @@ import Foundation // MARK: - Trimming and stuff extension String { func trunc(length: Int, trailing: String = "…") -> String { + if length <= 0 { return self } return (self.count > length) ? self.prefix(length) + trailing : self } diff --git a/PadelClub/Utils/DisplayContext.swift b/PadelClub/Utils/DisplayContext.swift index 1e99890..a5aaebe 100644 --- a/PadelClub/Utils/DisplayContext.swift +++ b/PadelClub/Utils/DisplayContext.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit enum DisplayContext { case addition @@ -27,3 +28,40 @@ enum MatchViewStyle { case plainStyle // vue detail case tournamentResultStyle //vue resultat tournoi } + +struct DeviceHelper { + static func isBigScreen() -> Bool { + switch UIDevice.current.userInterfaceIdiom { + case .pad: // iPads + return true + case .phone: // iPhones (you can add more cases here for large vs small phones) + if UIScreen.main.bounds.size.width > 375 { // iPhone X, 11, 12, 13 Pro Max etc. + return true // large phones + } else { + return false // smaller phones + } + default: + return false // Other devices (Apple Watch, TV, etc.) + } + + } + + static func maxCharacter() -> Int { + switch UIDevice.current.userInterfaceIdiom { + case .pad: // iPads + return 30 + case .phone: // iPhones (you can add more cases here for large vs small phones) + if UIScreen.main.bounds.size.width > 375 { // iPhone X, 11, 12, 13 Pro Max etc. + return 15 // large phones + } else { + return 9 // smaller phones + } + default: + return 9 // Other devices (Apple Watch, TV, etc.) + } + } + + static func charLength() -> Int { + isBigScreen() ? 0 : 15 + } +} diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 801276f..e809629 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -488,7 +488,13 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { } func localizedLevelLabel(_ displayStyle: DisplayStyle = .wide) -> String { - if self == .unlisted { return displayStyle == .title ? "Animation" : "Anim." } + if self == .unlisted { + if DeviceHelper.isBigScreen() { + return "Animation" + } else { + return displayStyle == .title ? "Animation" : "Anim." + } + } return String(describing: self).capitalized } diff --git a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift index 0ab01e9..cf613a4 100644 --- a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift +++ b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift @@ -123,7 +123,7 @@ struct GroupStageTeamReplacementView: View { private func _searchLinkView(_ teamRange: TeamRegistration.TeamRange) -> some View { NavigationStack { let tournament = team.tournamentObject() - SelectablePlayerListView(searchField: _searchableRange(teamRange), dataSet: .favoriteClubs, filterOption: tournament?.tournamentCategory.playerFilterOption ?? .all, sortOption: .rank, showFemaleInMaleAssimilation: true, tokens: [_searchToken(teamRange)], hidePlayers: tournament?.selectedPlayers().compactMap { $0.licenceId }) + SelectablePlayerListView(isPresented: false, searchField: _searchableRange(teamRange), dataSet: .favoriteClubs, filterOption: tournament?.tournamentCategory.playerFilterOption ?? .all, sortOption: .rank, showFemaleInMaleAssimilation: true, tokens: [_searchToken(teamRange)], hidePlayers: tournament?.selectedPlayers().compactMap { $0.licenceId }) } } diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 0a19da3..8c9dd36 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() + SelectablePlayerListView(isPresented: false) } label: { Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") } diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index 6de85dd..f81ae56 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -82,6 +82,7 @@ struct ImportedPlayerView: View { } } .lineLimit(1) + .truncationMode(.tail) if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index c555a1a..6c3b1d9 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -34,7 +34,7 @@ struct SelectablePlayerListView: View { return URL.importDateFormatter.date(from: lastDataSource) } - init(allowSelection: Int = 0, 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, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { self.allowSelection = allowSelection self.playerSelectionAction = playerSelectionAction self.contentUnavailableAction = contentUnavailableAction @@ -45,7 +45,7 @@ struct SelectablePlayerListView: View { searchViewModel.debouncableText = searchField ?? "" searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation searchViewModel.searchText = searchField ?? "" - searchViewModel.isPresented = allowSelection != 0 + searchViewModel.isPresented = isPresented searchViewModel.allowSelection = allowSelection searchViewModel.codeClub = fromPlayer?.clubCode ?? codeClub searchViewModel.clubName = nil @@ -221,7 +221,7 @@ struct SelectablePlayerListView: View { if searchViewModel.selectedPlayers.isEmpty && searchViewModel.filterSelectionEnabled { searchViewModel.filterSelectionEnabled = false - } else { + } else if searchViewModel.allowSelection >= searchViewModel.selectedPlayers.count { searchViewModel.filterSelectionEnabled = true } } @@ -430,6 +430,7 @@ struct MySearchView: View { } } .lineLimit(1) + .truncationMode(.tail) if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { @@ -540,6 +541,7 @@ struct MySearchView: View { } } .lineLimit(1) + .truncationMode(.tail) if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { @@ -654,6 +656,7 @@ struct MySearchView: View { } } .lineLimit(1) + .truncationMode(.tail) if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { @@ -763,6 +766,7 @@ struct MySearchView: View { } } .lineLimit(1) + .truncationMode(.tail) if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { @@ -874,6 +878,7 @@ struct MySearchView: View { } } .lineLimit(1) + .truncationMode(.tail) if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { @@ -972,6 +977,7 @@ struct MySearchView: View { } } .lineLimit(1) + .truncationMode(.tail) if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 518c8a7..431e7a1 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -45,9 +45,8 @@ struct AddTeamView: View { @State private var searchForHit: Int = 0 @State private var displayWarningNotEnoughCharacter: Bool = false @State private var testMessageIndex: Int = 0 - - let filterLimit : Int = 1000 - + @State private var presentLocalMultiplayerSearch: Bool = false + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -97,7 +96,7 @@ struct AddTeamView: View { _buildingTeamView() } .onReceive(fetchPlayers.publisher.count()) { receivedCount in // <-- here - if receivedCount < filterLimit, let pasteString, pasteString.isEmpty == false, count == 2, autoSelect == true { + if let pasteString, pasteString.isEmpty == false, count == 2, autoSelect == true { fetchPlayers.filter { hitForSearch($0, pasteString) >= hitTarget }.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) }).forEach { player in createdPlayerIds.insert(player.license!) } @@ -142,6 +141,27 @@ struct AddTeamView: View { } message: { Text("Cette équipe existe déjà dans votre liste d'inscription.") } + .sheet(isPresented: $presentLocalMultiplayerSearch) { + NavigationStack { + SelectablePlayerListView(allowSelection: -1, isPresented: false, searchField: searchField, dataSet: .club, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in + players.forEach { player in + let newPlayer = PlayerRegistration(importedPlayer: player) + newPlayer.setComputedRank(in: tournament) + createdPlayers = Set() + createdPlayerIds = Set() + createdPlayers.insert(newPlayer) + createdPlayerIds.insert(newPlayer.id) + _createTeam(checkDuplicates: false, checkHomonym: false) + } + + } contentUnavailableAction: { searchViewModel in + presentLocalMultiplayerSearch = false + selectionSearchField = searchViewModel.searchText + } + } + .tint(.master) + + } .sheet(isPresented: $presentPlayerSearch) { NavigationStack { SelectablePlayerListView(allowSelection: -1, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in @@ -243,6 +263,16 @@ struct AddTeamView: View { } } + if tournament.isAnimation(), createdPlayers.isEmpty == true { + Section { + RowButtonView("Ajouter plusieurs joueurs du club") { + presentLocalMultiplayerSearch = true + } + } footer: { + Text("Crée une équipe par joueur sélectionné") + } + } + Section { RowButtonView("Créer un non classé / non licencié") { if let pasteString, pasteString.isEmpty == false { @@ -633,11 +663,7 @@ struct AddTeamView: View { } private func _sortedPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> [ImportedPlayer] { - if searchFilteredPlayers.count < filterLimit { - return searchFilteredPlayers.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) }) - } else { - return searchFilteredPlayers - } + return searchFilteredPlayers.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) }) } } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index ed82548..06c6ce2 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -94,9 +94,9 @@ struct TournamentCellView: View { .font(.caption) } HStack(alignment: .bottom) { - Text(build.level.localizedLevelLabel()) + Text(tournament.tournamentTitle(displayStyle, forBuild: build)) .fontWeight(.semibold) - if displayStyle == .wide { + if displayStyle == .wide, tournament.displayAgeAndCategory(forBuild: build) { VStack(alignment: .leading, spacing: 0) { Text(build.category.localizedLabel()) Text(build.age.localizedLabel()) @@ -128,8 +128,14 @@ struct TournamentCellView: View { .font(displayStyle == .wide ? .title : .title3) if displayStyle == .wide { - HStack { - Text(tournament.durationLabel()) + HStack(alignment: .top) { + VStack(alignment: .leading) { + let sub = tournament.subtitleLabel(forBuild: build) + if sub.isEmpty == false { + Text(sub).lineLimit(1) + } + Text(tournament.durationLabel()) + } Spacer() if let tournament = tournament as? Tournament, tournament.isCanceled == false, let teamCount { let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted() @@ -137,7 +143,6 @@ struct TournamentCellView: View { Text(word + teamCount.pluralSuffix) } } - Text(tournament.subtitleLabel()).lineLimit(1) } else { Text(build.category.localizedLabel()) Text(build.age.localizedLabel()) From 8dec31dd1467bc30970a57b25b539b12b951506d Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 29 Sep 2024 16:41:35 +0200 Subject: [PATCH 34/41] add toolbarBackground in player search list --- .../Shared/SelectablePlayerListView.swift | 30 +++++++++---------- .../Views/Tournament/Screen/AddTeamView.swift | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 6c3b1d9..9b61575 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -171,7 +171,8 @@ struct SelectablePlayerListView: View { } .scrollDismissesKeyboard(.immediately) .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) - //.toolbarBackground(.visible, for: .bottomBar) + .toolbarBackground(searchViewModel.allowMultipleSelection ? .visible : .hidden, for: .bottomBar) + .toolbarBackground(.visible, for: .navigationBar) // .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) .navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet)) @@ -247,22 +248,21 @@ struct SelectablePlayerListView: View { } } - if searchViewModel.selectedPlayers.isEmpty == false { - ToolbarItem(placement: .topBarTrailing) { - ButtonValidateView { - if let playerSelectionAction { - playerSelectionAction(searchViewModel.selectedPlayers) - } - dismiss() + ToolbarItem(placement: .topBarTrailing) { + ButtonValidateView { + if let playerSelectionAction { + playerSelectionAction(searchViewModel.selectedPlayers) } + dismiss() } - ToolbarItem(placement: .status) { - let count = searchViewModel.selectedPlayers.count - VStack(spacing: 0) { - Text(count.formatted() + " joueur" + count.pluralSuffix + " séléctionné" + count.pluralSuffix).font(.footnote).foregroundStyle(.secondary) - FooterButtonView("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la liste") { - searchViewModel.filterSelectionEnabled.toggle() - } + .disabled(searchViewModel.selectedPlayers.isEmpty) + } + ToolbarItem(placement: .status) { + let count = searchViewModel.selectedPlayers.count + VStack(spacing: 0) { + Text(count.formatted() + " joueur" + count.pluralSuffix + " séléctionné" + count.pluralSuffix).font(.footnote).foregroundStyle(.secondary) + FooterButtonView("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la liste") { + searchViewModel.filterSelectionEnabled.toggle() } } } diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 431e7a1..71aebc0 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -164,7 +164,7 @@ struct AddTeamView: View { } .sheet(isPresented: $presentPlayerSearch) { NavigationStack { - SelectablePlayerListView(allowSelection: -1, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in + SelectablePlayerListView(allowSelection: 2 - _currentSelectionIds().count, isPresented: true, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) newPlayer.setComputedRank(in: tournament) From a01dfcea59500ca8175010e811d0e1879e9762d6 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 29 Sep 2024 19:43:21 +0200 Subject: [PATCH 35/41] fix bugs double groupstage mode --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Data/GroupStage.swift | 12 ++++++++---- PadelClub/Data/Tournament.swift | 9 +++++---- PadelClub/Views/GroupStage/GroupStageView.swift | 14 +++++++++++++- .../Views/Tournament/TournamentBuildView.swift | 13 ++++++++++++- 5 files changed, 40 insertions(+), 12 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 2bc0a9a..cb7b062 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3179,7 +3179,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 8ec0c09..00aff51 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -163,7 +163,11 @@ final class GroupStage: ModelObject, Storable { Logger.error(error) } - if tournament.groupStagesAreOver(), tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty { + let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0) + let nextStepGroupStages = tournament.groupStages(atStep: 1) + let groupStagesAreOverAtSecondStep = tournament.groupStagesAreOver(atStep: 1) + + if groupStagesAreOverAtFirstStep, nextStepGroupStages.isEmpty || groupStagesAreOverAtSecondStep == true, tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty { tournament.endDate = Date() do { try DataStore.shared.tournaments.addOrUpdate(instance: tournament) @@ -365,17 +369,17 @@ final class GroupStage: ModelObject, Storable { func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { if sortedByScore { return unsortedTeams().compactMap({ team in - scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePosition!) + scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) }).sorted { (lhs, rhs) in let predicates: [TeamScoreAreInIncreasingOrder] = [ { $0.wins < $1.wins }, { $0.setDifference < $1.setDifference }, { $0.gameDifference < $1.gameDifference}, { self._headToHead($0.team, $1.team) }, - { $0.team.groupStagePosition! > $1.team.groupStagePosition! } + { [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } ] - for predicate in predicates { + for predicate in predicates { if !predicate(lhs, rhs) && !predicate(rhs, lhs) { continue } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 9edcef5..07f7d0b 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1225,8 +1225,9 @@ defer { let groupStages = groupStages(atStep: lastStep) for groupStage in groupStages { - for (teamIndex, team) in groupStage.teams(true).enumerated() { - teams[groupStage.index + 1 + teamIndex] = [team.id] + let groupStageTeams = groupStage.teams(true) + for teamIndex in 0.. Bool { - let groupStages = groupStages() + func groupStagesAreOver(atStep: Int = 0) -> Bool { + let groupStages = groupStages(atStep: atStep) guard groupStages.isEmpty == false else { return true } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index cf13eff..8a2e134 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -57,12 +57,24 @@ struct GroupStageView: View { MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true) .listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true) MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) - MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), isExpanded: false) + MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false) if playedMatches.isEmpty { RowButtonView("Créer les matchs de poules") { groupStage.buildMatches() } + } else if groupStage.step > 0, playedMatches.flatMap({ $0.teamScores }).isEmpty { + Section { + RowButtonView("Préparer les matchs") { + playedMatches.forEach { match in + match.updateTeamScores() + } + } + .disabled(tournament.groupStagesAreOver(atStep: 0) == false) + } footer: { + Text("La première phase doit être terminée avant de pouvoir préparer les matchs de la deuxième phase de poule.") + } + } } .toolbar { diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index 0266797..fcb5b0e 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -60,8 +60,19 @@ struct TournamentBuildView: View { } if tournament.groupStages(atStep: 1).isEmpty == false { - NavigationLink("2ème phase de poules") { + NavigationLink { GroupStagesView(tournament: tournament, step: 1) + } label: { + LabeledContent { + if tournament.groupStagesAreOver(atStep: 1) { + Text("terminées") + } else { + Text("") + } + } label: { + Text("2ème phase de poules") + } + } } From ea671ae14d4a66a2812b82fab6b6f2a92813bb16 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 30 Sep 2024 11:34:22 +0200 Subject: [PATCH 36/41] remove a debug option --- .../Tournament/TournamentBuildView.swift | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index fcb5b0e..50dbfca 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -113,24 +113,6 @@ struct TournamentBuildView: View { Section { - #if DEBUG - NavigationLink(value: Screen.rankings) { - LabeledContent { - if tournament.publishRankings == false { - Image(systemName: "exclamationmark.circle.fill") - .foregroundStyle(.logoYellow) - } else { - Image(systemName: "checkmark") - .foregroundStyle(.green) - } - } label: { - Text("Classement final des équipes") - if tournament.publishRankings == false { - Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) - } - } - } - #else if tournament.hasEnded() { NavigationLink(value: Screen.rankings) { LabeledContent { @@ -149,7 +131,6 @@ struct TournamentBuildView: View { } } } - #endif if state == .running || state == .finished { TournamentInscriptionView(tournament: tournament) TournamentBroadcastRowView(tournament: tournament) From c9b25227d70addff4f8515a4798494e6c390714b Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 2 Oct 2024 05:48:01 +0200 Subject: [PATCH 37/41] fix senior+ import fix refresh of team row view fix default entry fee --- PadelClub/Utils/PadelRule.swift | 14 ++++++++++++-- PadelClub/Views/Team/TeamRowView.swift | 1 + .../Tournament/Screen/InscriptionManagerView.swift | 8 ++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index e809629..b313f00 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -209,9 +209,9 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case .senior: return "Senior" case .a45: - return "+45 ans" + return "45 ans" case .a55: - return "+55 ans" + return "55 ans" } } @@ -315,6 +315,16 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { self.init(rawValue: value) } + var entryFee: Double? { + switch self { + case .unlisted: + return nil + case .p25: + return 15 + default: + return 20 + } + } func searchRawValue() -> String { String(describing: self) } diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 114d324..7ee5da2 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -8,6 +8,7 @@ import SwiftUI struct TeamRowView: View { + @EnvironmentObject var dataStore: DataStore var team: TeamRegistration var teamPosition: TeamPosition? = nil var displayCallDate: Bool = false diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 16ddaf8..063f382 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -350,6 +350,14 @@ struct InscriptionManagerView: View { } } } else { + rankingDateSourcePickerView(showDateInLabel: true) + + Divider() + + _sharingTeamsMenuView() + + Divider() + Button { presentImportView = true } label: { From d5ea4f533677c75d4ec5131ed384eddb06102852 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 2 Oct 2024 06:08:50 +0200 Subject: [PATCH 38/41] fix wildcard bracket compute bug --- PadelClub/Data/Tournament.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 07f7d0b..aa16e3a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -859,7 +859,7 @@ defer { let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending) let groupStageSpots: Int = self.groupStageSpots() - var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots - wcBracket.count + var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if bracketSeeds < 0 { bracketSeeds = 0 } From 8d67d7efab6d6ac8d1d0a029013387c7ba39e4ca Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 2 Oct 2024 09:31:09 +0200 Subject: [PATCH 39/41] fix refresh round title fix refresh final rankings fix update of loser bracket mode --- PadelClub/Data/Match.swift | 2 + PadelClub/Data/Tournament.swift | 228 +++++++++++------- .../Views/Cashier/CashierSettingsView.swift | 178 +++++++++----- PadelClub/Views/Match/MatchDetailView.swift | 1 + .../Views/Round/LoserRoundSettingsView.swift | 81 ++++++- PadelClub/Views/Round/RoundView.swift | 30 ++- .../Team/Components/TeamHeaderView.swift | 2 +- PadelClub/Views/Team/TeamRowView.swift | 20 +- .../TournamentGeneralSettingsView.swift | 141 ++++++++--- .../Screen/TournamentRankView.swift | 22 +- 10 files changed, 477 insertions(+), 228 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 5476029..8b109da 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -517,6 +517,7 @@ defer { losingTeamId = teamScoreWalkout.teamRegistration groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() + currentTournament()?.updateTournamentState() updateFollowingMatchTeamScore() } @@ -542,6 +543,7 @@ defer { groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() + currentTournament()?.updateTournamentState() updateFollowingMatchTeamScore() } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index aa16e3a..8dd11f1 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1230,115 +1230,143 @@ defer { teams[groupStage.index * groupStage.size + 1 + teamIndex] = [groupStageTeams[teamIndex].id] } } + } else { - return teams - } - - let final = rounds.last?.playedMatches().last - if let winner = final?.winningTeamId { - teams[1] = [winner] - ids.insert(winner) - } - if let finalist = final?.losingTeamId { - teams[2] = [finalist] - ids.insert(finalist) - } - - let others: [Round] = rounds.flatMap { round in - let losers = round.losers() - let minimumFinalPosition = round.seedInterval()?.last ?? teamCount - if teams[minimumFinalPosition] == nil { - teams[minimumFinalPosition] = losers.map { $0.id } - } else { - teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id }) + let final = rounds.last?.playedMatches().last + if let winner = final?.winningTeamId { + teams[1] = [winner] + ids.insert(winner) + } + if let finalist = final?.losingTeamId { + teams[2] = [finalist] + ids.insert(finalist) } - print("round", round.roundTitle()) - let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } - print(rounds.count, rounds.map { $0.roundTitle() }) - return rounds - }.compactMap({ $0 }) - - others.forEach { round in - print("round", round.roundTitle()) - if let interval = round.seedInterval() { - print("interval", interval.localizedInterval()) - let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() } - print("playedMatches", playedMatches.count) - let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false }) - print("winners", winners.count) - let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false }) - print("losers", losers.count) - if winners.isEmpty { - let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) - if disabledIds.isEmpty == false { - _removeStrings(from: &teams, stringsToRemove: disabledIds) - teams[interval.last] = disabledIds - let teamNames : [String] = disabledIds.compactMap { - let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0) - return t - }.map { $0.canonicalName } - print("winners.isEmpty", "\(interval.last) : ", teamNames) - disabledIds.forEach { - ids.insert($0) + let others: [Round] = rounds.flatMap { round in + let losers = round.losers() + let minimumFinalPosition = round.seedInterval()?.last ?? teamCount + if teams[minimumFinalPosition] == nil { + teams[minimumFinalPosition] = losers.map { $0.id } + } else { + teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id }) + } + + print("round", round.roundTitle()) + let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } + print(rounds.count, rounds.map { $0.roundTitle() }) + return rounds + }.compactMap({ $0 }) + + others.forEach { round in + print("round", round.roundTitle()) + if let interval = round.seedInterval() { + print("interval", interval.localizedInterval()) + let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() } + print("playedMatches", playedMatches.count) + let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false }) + print("winners", winners.count) + let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false }) + print("losers", losers.count) + if winners.isEmpty { + let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) + if disabledIds.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: disabledIds) + teams[interval.last] = disabledIds + let teamNames : [String] = disabledIds.compactMap { + let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0) + return t + }.map { $0.canonicalName } + print("winners.isEmpty", "\(interval.last) : ", teamNames) + disabledIds.forEach { + ids.insert($0) + } + } + } else { + if winners.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: winners) + teams[interval.first + winners.count - 1] = winners + let teamNames : [String] = winners.compactMap { + let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) + return t + }.map { $0.canonicalName } + print("winners", "\(interval.last + winners.count - 1) : ", teamNames) + winners.forEach { ids.insert($0) } + } + + if losers.isEmpty == false { + _removeStrings(from: &teams, stringsToRemove: losers) + teams[interval.first + winners.count] = losers + let loserTeamNames : [String] = losers.compactMap { + let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) + return t + }.map { $0.canonicalName } + print("losers", "\(interval.first + winners.count) : ", loserTeamNames) + losers.forEach { ids.insert($0) } } } - } else { - if winners.isEmpty == false { - _removeStrings(from: &teams, stringsToRemove: winners) - teams[interval.first + winners.count - 1] = winners - let teamNames : [String] = winners.compactMap { - let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) - return t - }.map { $0.canonicalName } - print("winners", "\(interval.last + winners.count - 1) : ", teamNames) - winners.forEach { ids.insert($0) } + } + } + + if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() { + groupStageLoserBracketPlayedMatches.forEach({ match in + if match.hasEnded() { + let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count + teams.setOrAppend(match.winningTeamId, at: match.index) + teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount) } - - if losers.isEmpty == false { - _removeStrings(from: &teams, stringsToRemove: losers) - teams[interval.first + winners.count] = losers - let loserTeamNames : [String] = losers.compactMap { - let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) - return t - }.map { $0.canonicalName } - print("losers", "\(interval.first + winners.count) : ", loserTeamNames) - losers.forEach { ids.insert($0) } + }) + } + + let groupStages = groupStages() + let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) + groupStages.forEach { groupStage in + let groupStageTeams = groupStage.teams(true) + for (index, team) in groupStageTeams.enumerated() { + if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { + let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) + + let _index = baseRank + groupStageWidth + 1 + if let existingTeams = teams[_index] { + teams[_index] = existingTeams + [team.id] + } else { + teams[_index] = [team.id] + } } } } } - if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() { - groupStageLoserBracketPlayedMatches.forEach({ match in - if match.hasEnded() { - let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count - teams.setOrAppend(match.winningTeamId, at: match.index) - teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount) - } - }) - } + return teams + } + + func setRankings(finalRanks: [Int: [String]]) async -> [Int: [TeamRegistration]] { + var rankings: [Int: [TeamRegistration]] = [:] - let groupStages = groupStages() - let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) - groupStages.forEach { groupStage in - let groupStageTeams = groupStage.teams(true) - for (index, team) in groupStageTeams.enumerated() { - if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { - let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) - - let _index = baseRank + groupStageWidth + 1 - if let existingTeams = teams[_index] { - teams[_index] = existingTeams + [team.id] - } else { - teams[_index] = [team.id] - } + finalRanks.keys.sorted().forEach { rank in + if let rankedTeamIds = finalRanks[rank] { + let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } + rankings[rank] = teams + } + } + + rankings.keys.sorted().forEach { rank in + if let rankedTeams = rankings[rank] { + rankedTeams.forEach { team in + team.finalRanking = rank + team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: rank - 1, count: teamCount) } } } - return teams + do { + try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + } catch { + Logger.error(error) + } + + + return rankings } func lockRegistration() { @@ -1977,6 +2005,7 @@ defer { groupStageMatchFormat = groupStageSmartMatchFormat() loserBracketMatchFormat = loserBracketSmartMatchFormat(5) matchFormat = roundSmartMatchFormat(5) + entryFee = tournamentLevel.entryFee } func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { @@ -2150,6 +2179,19 @@ defer { } + func updateTournamentState() { + Task { + if hasEnded() { + let fr = await finalRanking() + _ = await setRankings(finalRanks: fr) + } + } + } + + func allLoserRoundMatches() -> [Match] { + rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } + } + // MARK: - func insertOnServer() throws { diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index ed5e63b..fd51e73 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -11,34 +11,43 @@ import LeStorage struct CashierSettingsView: View { @EnvironmentObject var dataStore: DataStore + @State private var entryFee: Double? = nil + @Bindable var tournament: Tournament + @FocusState private var focusedField: Tournament.CodingKeys? + let priceTags: [Double] = [15.0, 20.0, 25.0] - var tournaments: [Tournament] - - init(tournaments: [Tournament]) { - self.tournaments = tournaments - } - init(tournament: Tournament) { - self.tournaments = [tournament] + self.tournament = tournament + _entryFee = State(wrappedValue: tournament.entryFee) } var body: some View { List { + Section { + LabeledContent { + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($focusedField, equals: ._entryFee) + } label: { + Text("Inscription") + } + } footer: { + Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") + } + Section { RowButtonView("Tout le monde est arrivé", role: .destructive) { - - for tournament in self.tournaments { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = true - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + player.hasArrived = true + } + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) } - } } footer: { Text("Indique tous les joueurs sont là") @@ -46,68 +55,107 @@ struct CashierSettingsView: View { Section { RowButtonView("Personne n'est là", role: .destructive) { - - for tournament in self.tournaments { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = false - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + player.hasArrived = false + } + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) } - } } footer: { Text("Indique qu'aucun joueur n'est arrivé") } - if tournaments.count > 1 || tournaments.first?.isFree() == false { - Section { - RowButtonView("Tout le monde a réglé", role: .destructive) { - - for tournament in self.tournaments { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - if player.hasPaid() == false { - player.paymentType = .gift - } - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + if player.hasPaid() == false { + player.paymentType = .gift } - } - } footer: { - Text("Passe tous les joueurs qui n'ont pas réglé en offert") + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } } - - Section { - RowButtonView("Personne n'a réglé", role: .destructive) { - for tournament in self.tournaments { - let store = tournament.tournamentStore - - let players = tournament.selectedPlayers() - players.forEach { player in - player.paymentType = nil + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") + } + + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + let store = tournament.tournamentStore + + let players = tournament.selectedPlayers() + players.forEach { player in + player.paymentType = nil + } + do { + try store.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") + } + } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + + ToolbarItem(placement: .keyboard) { + HStack { + if tournament.isFree() { + ForEach(priceTags, id: \.self) { priceTag in + Button(priceTag.formatted(.currency(code: "EUR"))) { + entryFee = priceTag + tournament.entryFee = priceTag + focusedField = nil + } + .buttonStyle(.bordered) } - do { - try store.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) + } else { + Button("Gratuit") { + entryFee = nil + tournament.entryFee = nil + focusedField = nil } + .buttonStyle(.bordered) + } + Spacer() + Button("Valider") { + tournament.entryFee = entryFee + focusedField = nil + } + .buttonStyle(.bordered) } - } footer: { - Text("Remet à zéro le type d'encaissement de tous les joueurs") } } } + .onChange(of: tournament.entryFee) { + _save() + } + } + + private func _save() { + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 235e648..f8766d1 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -105,6 +105,7 @@ struct MatchDetailView: View { RowButtonView("Saisir les résultats", systemImage: "list.clipboard") { self._editScores() } + .disabled(match.teams().count < 2) } if self.match.currentTournament()?.isFree() == false { diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index 08952fe..3b4706a 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -13,7 +13,15 @@ struct LoserRoundSettingsView: View { @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(Tournament.self) var tournament: Tournament @State var upperBracketRound: UpperRound + @State private var confirmationRequired: Bool = false + @State private var presentConfirmation: Bool = false + @State private var loserBracketMode: LoserBracketMode + init(upperBracketRound: UpperRound) { + self.upperBracketRound = upperBracketRound + _loserBracketMode = .init(wrappedValue: upperBracketRound.round.loserBracketMode) + } + var body: some View { List { Section { @@ -23,25 +31,31 @@ struct LoserRoundSettingsView: View { } Section { - @Bindable var round: Round = upperBracketRound.round - Picker(selection: $round.loserBracketMode) { + Picker(selection: $loserBracketMode) { ForEach(LoserBracketMode.allCases) { Text($0.localizedLoserBracketMode()).tag($0) } } label: { Text("Position des perdants") } - .onChange(of: round.loserBracketMode) { - do { - try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round) - } catch { - Logger.error(error) + .onChange(of: loserBracketMode) { + if upperBracketRound.round.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false { + _refreshLoserBracketMode() + } else { + confirmationRequired = true } } } header: { Text("Matchs de classement") } footer: { - Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription()) + if confirmationRequired == false { + Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription()) + } else { + _footerViewConfirmationRequired() + .onTapGesture(perform: { + presentConfirmation = true + }) + } } Section { @@ -81,7 +95,58 @@ struct LoserRoundSettingsView: View { //todo proposer ici l'impression des matchs de classements peut-être? } + .confirmationDialog("Attention", isPresented: $presentConfirmation, actions: { + Button("Confirmer", role: .destructive) { + _refreshLoserBracketMode() + confirmationRequired = false + } + + Button("Annuler", role: .cancel) { + loserBracketMode = upperBracketRound.round.loserBracketMode + } + + }) + + } + + private func _refreshLoserBracketMode() { + let matches = upperBracketRound.round.loserRoundsAndChildren().flatMap({ $0._matches() }) + matches.forEach { match in + match.resetTeamScores(outsideOf: []) + match.resetMatch() + if loserBracketMode == .automatic { + match.updateTeamScores() + } + match.confirmed = false + } + + upperBracketRound.round.loserBracketMode = loserBracketMode + + if loserBracketMode == .automatic { + matches.forEach { match in + match.updateTeamScores() + } + } + + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: matches) + } catch { + Logger.error(error) + } + + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round) + } catch { + Logger.error(error) + } } + + private func _footerViewConfirmationRequired() -> some View { + Text("Au moins un match de classement est terminé, en modifiant ce réglage, les résultats de ces matchs de classement seront perdus.") + + + Text(" Modifier quand même ?").foregroundStyle(.red) + } + } //#Preview { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 37aa4c5..540b94a 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -275,6 +275,25 @@ struct RoundView: View { } } } + + if upperRound.round.index == 0, tournament.hasEnded() { + NavigationLink(value: Screen.rankings) { + LabeledContent { + if tournament.publishRankings == false { + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.logoYellow) + } else { + Image(systemName: "checkmark") + .foregroundStyle(.green) + } + } label: { + Text("Classement final des équipes") + if tournament.publishRankings == false { + Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) + } + } + } + } } .navigationDestination(isPresented: $showPrintScreen) { PrintSettingsView(tournament: tournament) @@ -327,13 +346,20 @@ struct RoundView: View { match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) } } + + let loserMatches = self.upperRound.loserMatches() + loserMatches.forEach { match in + match.name = match.roundTitle() + } + let allRoundMatches = tournament.allRoundMatches() + do { - try self.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + try tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) } catch { Logger.error(error) } - + if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } diff --git a/PadelClub/Views/Team/Components/TeamHeaderView.swift b/PadelClub/Views/Team/Components/TeamHeaderView.swift index 6e896a1..4109b18 100644 --- a/PadelClub/Views/Team/Components/TeamHeaderView.swift +++ b/PadelClub/Views/Team/Components/TeamHeaderView.swift @@ -45,7 +45,7 @@ struct TeamHeaderView: View { let positionLabel = team.positionLabel() let cutLabel = tournament.cutLabel(index: teamIndex, teamCount: teamCount) if team.isWildCard() { - Text("wildcard").font(.caption).italic() + Text("wildcard").foregroundStyle(.red).font(.caption).italic() Text(positionLabel ?? cutLabel) } else { if let positionLabel { diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 7ee5da2..1e11da3 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -18,15 +18,21 @@ struct TeamRowView: View { TeamWeightView(team: team, teamPosition: teamPosition) } label: { VStack(alignment: .leading) { - if let groupStage = team.groupStageObject() { - HStack { - Text(groupStage.groupStageTitle(.title)) - if let finalPosition = groupStage.finalPosition(ofTeam: team) { - Text((finalPosition + 1).ordinalFormatted()) + HStack { + if let groupStage = team.groupStageObject() { + HStack { + Text(groupStage.groupStageTitle(.title)) + if let finalPosition = groupStage.finalPosition(ofTeam: team) { + Text((finalPosition + 1).ordinalFormatted()) + } } + } else if let round = team.initialRound() { + Text(round.roundTitle(.wide)) + } + + if team.isWildCard() { + Text("wildcard").italic().foregroundStyle(.red).font(.caption) } - } else if let round = team.initialRound() { - Text(round.roundTitle(.wide)) } if let name = team.name { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 1258938..f7096d1 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -14,10 +14,15 @@ struct TournamentGeneralSettingsView: View { @Bindable var tournament: Tournament @State private var tournamentName: String = "" @State private var entryFee: Double? = nil + @State private var confirmationRequired: Bool = false + @State private var presentConfirmation: Bool = false + @State private var loserBracketMode: LoserBracketMode @FocusState private var focusedField: Tournament.CodingKeys? - + let priceTags: [Double] = [15.0, 20.0, 25.0] + init(tournament: Tournament) { self.tournament = tournament + _loserBracketMode = .init(wrappedValue: tournament.loserBracketMode) _tournamentName = State(wrappedValue: tournament.name ?? "") _entryFee = State(wrappedValue: tournament.entryFee) } @@ -25,6 +30,17 @@ struct TournamentGeneralSettingsView: View { var body: some View { @Bindable var tournament = tournament Form { + + Section { + TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) + .lineLimit(2) + .frame(maxWidth: .infinity) + .keyboardType(.alphabet) + .focused($focusedField, equals: ._name) + } header: { + Text("Nom du tournoi") + } + Section { TournamentDatePickerView() TournamentDurationManagerView() @@ -37,17 +53,8 @@ struct TournamentGeneralSettingsView: View { } label: { Text("Inscription") } - - } - - Section { - TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) - .lineLimit(2) - .frame(maxWidth: .infinity) - .keyboardType(.alphabet) - .focused($focusedField, equals: ._name) - } header: { - Text("Nom du tournoi") + } footer: { + Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") } Section { @@ -55,42 +62,51 @@ struct TournamentGeneralSettingsView: View { } Section { - Picker(selection: $tournament.loserBracketMode) { + Picker(selection: $loserBracketMode) { ForEach(LoserBracketMode.allCases) { Text($0.localizedLoserBracketMode()).tag($0) } } label: { Text("Position des perdants") } - .onChange(of: tournament.loserBracketMode) { - - _save() - - let rounds = tournament.rounds() - rounds.forEach { round in - round.loserBracketMode = tournament.loserBracketMode - } - - do { - try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) - } catch { - Logger.error(error) + .onChange(of: loserBracketMode) { + if tournament.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false { + _refreshLoserBracketMode() + } else { + confirmationRequired = true } } } header: { Text("Matchs de classement") } footer: { - if dataStore.user.loserBracketMode != tournament.loserBracketMode { - _footerView() + if confirmationRequired == false { + if dataStore.user.loserBracketMode != tournament.loserBracketMode { + _footerView() + .onTapGesture(perform: { + self.dataStore.user.loserBracketMode = tournament.loserBracketMode + self.dataStore.saveUser() + }) + } else { + Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) + } + } else { + _footerViewConfirmationRequired() .onTapGesture(perform: { - self.dataStore.user.loserBracketMode = tournament.loserBracketMode - self.dataStore.saveUser() + presentConfirmation = true }) - } else { - Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) } } } + .confirmationDialog("Attention", isPresented: $presentConfirmation, actions: { + Button("Confirmer", role: .destructive) { + _refreshLoserBracketMode() + confirmationRequired = false + } + Button("Annuler", role: .cancel) { + loserBracketMode = tournament.loserBracketMode + } + + }) .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { @@ -106,6 +122,26 @@ struct TournamentGeneralSettingsView: View { if focusedField != nil { ToolbarItem(placement: .keyboard) { HStack { + if focusedField == ._entryFee { + if tournament.isFree() { + ForEach(priceTags, id: \.self) { priceTag in + Button(priceTag.formatted(.currency(code: "EUR"))) { + entryFee = priceTag + tournament.entryFee = priceTag + focusedField = nil + } + .buttonStyle(.bordered) + } + } else { + Button("Gratuit") { + entryFee = nil + tournament.entryFee = nil + focusedField = nil + } + .buttonStyle(.bordered) + + } + } Spacer() Button("Valider") { if focusedField == ._name { @@ -166,9 +202,50 @@ struct TournamentGeneralSettingsView: View { } } + private func _refreshLoserBracketMode() { + tournament.loserBracketMode = loserBracketMode + _save() + + let rounds = tournament.rounds() + rounds.forEach { round in + let matches = round.loserRoundsAndChildren().flatMap({ $0._matches() }) + matches.forEach { match in + match.resetTeamScores(outsideOf: []) + match.resetMatch() + match.confirmed = false + } + + round.loserBracketMode = tournament.loserBracketMode + + if loserBracketMode == .automatic { + matches.forEach { match in + match.updateTeamScores() + } + } + + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: matches) + } catch { + Logger.error(error) + } + } + + do { + try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) + } catch { + Logger.error(error) + } + } + private func _footerView() -> some View { Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) + Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue) } + + private func _footerViewConfirmationRequired() -> some View { + Text("Au moins un match de classement est terminé, en modifiant ce réglage, les résultats de ces matchs de classement seront perdus.") + + + Text(" Modifier quand même ?").foregroundStyle(.red) + } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index dc06df0..ba33832 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -311,26 +311,8 @@ struct TournamentRankView: View { self.rankings.removeAll() let finalRanks = await tournament.finalRanking() - finalRanks.keys.sorted().forEach { rank in - if let rankedTeamIds = finalRanks[rank] { - let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } - self.rankings[rank] = teams - } - } - - await MainActor.run { - rankings.keys.sorted().forEach { rank in - if let rankedTeams = rankings[rank] { - rankedTeams.forEach { team in - team.finalRanking = rank - team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount) - } - } - } - _save() - - calculating = false - } + self.rankings = await tournament.setRankings(finalRanks: finalRanks) + calculating = false } private func _save() { From 74ee3a4525f996d313f0e5ea2b11fc37c5fd949a Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 2 Oct 2024 09:31:49 +0200 Subject: [PATCH 40/41] v1.0.17 b1 --- 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 cb7b062..1aed825 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3158,7 +3158,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.16; + MARKETING_VERSION = 1.0.17; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3179,7 +3179,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3202,7 +3202,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.16; + MARKETING_VERSION = 1.0.17; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From be4ee154850b4618ed52c91e55fbb87a94a3d7a6 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 2 Oct 2024 10:42:20 +0200 Subject: [PATCH 41/41] fix line limit --- .../Views/Tournament/Shared/TournamentCellView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 06c6ce2..be746d3 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -77,7 +77,9 @@ struct TournamentCellView: View { VStack(alignment: .leading, spacing: 0.0) { if let tournament = tournament as? Tournament { HStack { - Text(tournament.locationLabel(displayStyle)).lineLimit(1) + Text(tournament.locationLabel(displayStyle)) + .lineLimit(1) + .truncationMode(.tail) .font(.caption) Spacer() if tournament.isPrivate { @@ -96,6 +98,8 @@ struct TournamentCellView: View { HStack(alignment: .bottom) { Text(tournament.tournamentTitle(displayStyle, forBuild: build)) .fontWeight(.semibold) + .lineLimit(1) + .truncationMode(.tail) if displayStyle == .wide, tournament.displayAgeAndCategory(forBuild: build) { VStack(alignment: .leading, spacing: 0) { Text(build.category.localizedLabel()) @@ -132,7 +136,9 @@ struct TournamentCellView: View { VStack(alignment: .leading) { let sub = tournament.subtitleLabel(forBuild: build) if sub.isEmpty == false { - Text(sub).lineLimit(1) + Text(sub) + .lineLimit(1) + .truncationMode(.tail) } Text(tournament.durationLabel()) }