diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 0408367..9c90089 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -1859,7 +1859,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 44; + CURRENT_PROJECT_VERSION = 45; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -1882,7 +1882,6 @@ ); MARKETING_VERSION = 0.1; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50"; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1897,7 +1896,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; +<<<<<<< HEAD CURRENT_PROJECT_VERSION = 44; +======= + CURRENT_PROJECT_VERSION = 45; +>>>>>>> 599c942c5910dc651db20fcea6c660dff139e08c DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -1920,7 +1923,6 @@ ); MARKETING_VERSION = 0.1; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50"; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index 582701b..ee42ba9 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -63,7 +63,7 @@ class Club : ModelObject, Storable, Hashable { } func shareURL() -> URL? { - return URLs.main.url.appending(path: "?club=\(id)") + return URL(string: URLs.main.url.appending(path: "?club=\(id)").absoluteString.removingPercentEncoding!) } var customizedCourts: [Court] { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 2ec0663..a601504 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -191,6 +191,13 @@ class TeamRegistration: ModelObject, Storable { func contains(_ searchField: String) -> Bool { unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true } + + func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool { + let arrayOfIds : [String] = unsortedPlayers().compactMap({ $0.licenceId?.strippedLicense?.canonicalVersion }) + let ids : Set = Set(arrayOfIds.sorted()) + let searchedIds = Set(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted()) + return ids.hashValue == searchedIds.hashValue + } func includes(_ players: [PlayerRegistration]) -> Bool { players.allSatisfy { player in diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index dba3201..06e87bd 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1075,7 +1075,7 @@ class Tournament : ModelObject, Storable { } let groupStages = groupStages() - let baseRank = teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified groupStages.forEach { groupStage in let groupStageTeams = groupStage.teams(true) diff --git a/PadelClub/Views/Components/FooterButtonView.swift b/PadelClub/Views/Components/FooterButtonView.swift index 3a44a7b..a710b18 100644 --- a/PadelClub/Views/Components/FooterButtonView.swift +++ b/PadelClub/Views/Components/FooterButtonView.swift @@ -7,22 +7,43 @@ import SwiftUI +fileprivate let defaultConfirmationMessage = "Êtes-vous sûr de vouloir faire cela ?" + struct FooterButtonView: View { + var role: ButtonRole? = nil let title: String + let confirmationMessage: String let action: () -> () - - init(_ title: String, action: @escaping () -> Void) { + @State private var askConfirmation: Bool = false + + init(_ title: String, role: ButtonRole? = nil, confirmationMessage: String? = nil, action: @escaping () -> Void) { self.title = title self.action = action + self.role = role + self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage } var body: some View { - Button { - action() + Button(role: role) { + if role == .destructive { + askConfirmation = true + } else { + action() + } } label: { Text(title) .underline() } .buttonStyle(.borderless) + .confirmationDialog("Confirmation", + isPresented: $askConfirmation, + titleVisibility: .visible) { + Button("OK") { + action() + } + Button("Annuler", role: .cancel) {} + } message: { + Text(confirmationMessage) + } } } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index bcbeaf3..33adcab 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -27,6 +27,11 @@ struct ActivityView: View { .filter({ federalDataViewModel.isTournamentValidForFilters($0) }) } + func getRunningTournaments() -> [Tournament] { + dataStore.tournaments.filter({ $0.endDate == nil }) + .filter({ federalDataViewModel.isTournamentValidForFilters($0) }) + } + var endedTournaments: [Tournament] { dataStore.tournaments.filter({ $0.endDate != nil }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) }) @@ -53,6 +58,29 @@ struct ActivityView: View { } } + @ViewBuilder + private func _pasteView() -> some View { + if UIPasteboard.general.hasStrings { + // Enable string-related control... + if let string = UIPasteboard.general.string { + // use the string here + Section { + Menu("Utiliser le contenu du presse-papier") { + Section { + ForEach(getRunningTournaments()) { tournament in + Button(tournament.tournamentTitle()) { + navigation.path.append(tournament) + tournament.navigationPath = [Screen.inscription] + } + } + } header: { + Text("coller dans") + } + } + } + } + } + } var body: some View { @Bindable var navigation = navigation NavigationStack(path: $navigation.path) { @@ -62,6 +90,7 @@ struct ActivityView: View { List { switch navigation.agendaDestination! { case .activity: + _pasteView() EventListView(tournaments: runningTournaments, viewStyle: viewStyle, sortAscending: true) case .history: EventListView(tournaments: endedTournaments, viewStyle: viewStyle, sortAscending: false) diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 46d5449..4ce48f1 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -48,8 +48,12 @@ struct MainView: View { dataStore.matches.filter({ $0.confirmed && $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }) } + + private func _isConnected() -> Bool { + Store.main.hasToken() && Store.main.userId != nil + } var badgeText: Text? { - Store.main.userId == nil ? Text("!").font(.headline) : nil + _isConnected() == false ? Text("!").font(.headline) : nil } var body: some View { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 4641793..b5bb7b9 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -58,6 +58,7 @@ struct InscriptionManagerView: View { @State private var unsortedTeamsWithoutWO: [TeamRegistration] = [] @State private var unsortedPlayers: [PlayerRegistration] = [] @State private var teamPaste: URL? + @State private var confirmDuplicate: Bool = false var messageSentFailed: Binding { Binding { @@ -197,6 +198,21 @@ struct InscriptionManagerView: View { } } + .alert("Cette équipe existe déjà", isPresented: $confirmDuplicate) { + Button("Créer l'équipe quand même") { + _createTeam(checkDuplicates: false) + } + + Button("Annuler", role: .cancel) { + pasteString = nil + editedTeam = nil + createdPlayers.removeAll() + createdPlayerIds.removeAll() + } + + } message: { + Text("Cette équipe existe déjà dans votre liste d'inscription.") + } .alert("Un problème est survenu", isPresented: messageSentFailed) { Button("OK") { } @@ -362,7 +378,7 @@ struct InscriptionManagerView: View { //_prioritizeClubMembersButton() Button("Bloquer une place") { - _createTeam() + _createTeam(checkDuplicates: false) } } Divider() @@ -829,7 +845,7 @@ struct InscriptionManagerView: View { return predicate } - + private func _currentSelection() -> Set { var currentSelection = Set() createdPlayerIds.compactMap { id in @@ -847,8 +863,37 @@ struct InscriptionManagerView: View { } return currentSelection } + + private func _currentSelectionIds() -> [String?] { + var currentSelection = [String?]() + createdPlayerIds.compactMap { id in + fetchPlayers.first(where: { id == $0.license }) + }.forEach { player in + currentSelection.append(player.license) + } + + createdPlayerIds.compactMap { id in + createdPlayers.first(where: { id == $0.id }) + }.forEach { + currentSelection.append($0.licenceId) + } + return currentSelection + } - private func _createTeam() { + private func _isDuplicate() -> Bool { + let ids : [String?] = _currentSelectionIds() + if unfilteredTeams.anySatisfy({ $0.containsExactlyPlayerLicenses(ids) }) { + return true + } + return false + } + + private func _createTeam(checkDuplicates: Bool) { + if checkDuplicates && _isDuplicate() { + confirmDuplicate = true + return + } + let players = _currentSelection() let team = tournament.addTeam(players) do { @@ -870,8 +915,13 @@ struct InscriptionManagerView: View { _getTeams() } - private func _updateTeam() { + private func _updateTeam(checkDuplicates: Bool) { guard let editedTeam else { return } + if checkDuplicates && _isDuplicate() { + confirmDuplicate = true + return + } + let players = _currentSelection() editedTeam.updatePlayers(players, inTournamentCategory: tournament.tournamentCategory) do { @@ -915,10 +965,20 @@ struct InscriptionManagerView: View { Section { ForEach(createdPlayerIds.sorted(), id: \.self) { id in if let p = createdPlayers.first(where: { $0.id == id }) { - PlayerView(player: p).tag(p.id) + VStack(alignment: .leading, spacing: 0) { + if unsortedPlayers.first(where: { $0.licenceId == p.licenceId }) != nil { + Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } + PlayerView(player: p).tag(p.id) + } } if let p = fetchPlayers.first(where: { $0.license == id }) { - ImportedPlayerView(player: p).tag(p.license!) + VStack(alignment: .leading, spacing: 0) { + if unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { + Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } + ImportedPlayerView(player: p).tag(p.license!) + } } } } @@ -926,16 +986,16 @@ struct InscriptionManagerView: View { if editedTeam == nil { if createdPlayerIds.isEmpty { RowButtonView("Bloquer une place") { - _createTeam() + _createTeam(checkDuplicates: false) } } else { RowButtonView("Ajouter l'équipe") { - _createTeam() + _createTeam(checkDuplicates: true) } } } else { RowButtonView("Modifier l'équipe") { - _updateTeam() + _updateTeam(checkDuplicates: true) } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 3aa8c4a..ab99337 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -36,7 +36,7 @@ struct TournamentRankView: View { Text("Classement publié") } - RowButtonView("Publier le classement", role: .destructive) { + RowButtonView(rankingPublished ? "Re-publier le classement" : "Publier le classement", role: .destructive) { rankings.keys.sorted().forEach { rank in if let rankedTeams = rankings[rank] { rankedTeams.forEach { team in @@ -48,7 +48,7 @@ struct TournamentRankView: View { _save() } } footer: { - FooterButtonView("masquer le classement") { + FooterButtonView("masquer le classement", role: .destructive) { tournament.unsortedTeams().forEach { team in team.finalRanking = nil team.pointsEarned = nil