From df8609d1a0f89c44ea71a9034c4128c42c7a849e Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 21 May 2025 14:07:07 +0200 Subject: [PATCH 01/64] Adds sharing for matches --- PadelClub/AppDelegate.swift | 6 ++--- PadelClub/Views/Match/MatchDetailView.swift | 5 +++++ PadelClub/Views/Round/DrawLogsView.swift | 6 +---- PadelClub/Views/User/ShareModelView.swift | 25 ++++++++++++--------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/PadelClub/AppDelegate.swift b/PadelClub/AppDelegate.swift index 1b38c5b..b58e458 100644 --- a/PadelClub/AppDelegate.swift +++ b/PadelClub/AppDelegate.swift @@ -28,16 +28,16 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel fileprivate func _configureLeStorage() { StoreCenter.main.blackListUserName("apple-test") - + StoreCenter.main.classProject = "PadelClubData" // let secureScheme = true let domain: String = URLs.activationHost.rawValue #if DEBUG if let secure = PListReader.readBool(plist: "local", key: "secure_server"), let domain = PListReader.readString(plist: "local", key: "server_domain") { - StoreCenter.main.configureURLs(secureScheme: secure, domain: domain, webSockets: false) + StoreCenter.main.configureURLs(secureScheme: secure, domain: domain, webSockets: true, useSynchronization: true) } else { - StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: false) + StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: true, useSynchronization: true) } #else StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: false) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 7f0fa2a..2c8ee62 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -309,6 +309,11 @@ struct MatchDetailView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { + + NavigationLink("Partager") { + ShareModelView(instance: self.match) + } + Toggle(isOn: .init(get: { return match.confirmed }, set: { value in diff --git a/PadelClub/Views/Round/DrawLogsView.swift b/PadelClub/Views/Round/DrawLogsView.swift index f89c3a7..1c24996 100644 --- a/PadelClub/Views/Round/DrawLogsView.swift +++ b/PadelClub/Views/Round/DrawLogsView.swift @@ -52,11 +52,7 @@ struct DrawLogsView: View { Divider() Button("Tout effacer", role: .destructive) { - do { - try tournament.tournamentStore?.drawLogs.deleteAll() - } catch { - Logger.error(error) - } + tournament.tournamentStore?.drawLogs.reset() } } label: { LabelOptions() diff --git a/PadelClub/Views/User/ShareModelView.swift b/PadelClub/Views/User/ShareModelView.swift index d1a1960..b504e6a 100644 --- a/PadelClub/Views/User/ShareModelView.swift +++ b/PadelClub/Views/User/ShareModelView.swift @@ -13,9 +13,9 @@ import PadelClubData class UserSearchViewModel: ObservableObject { @Published var searchText = "" - @Published var userNames: [ShortUser] = [] +// @Published var userNames: [ShortUser] = [] - @Published var users: [String] = [] +// @Published var users: [String] = [] @Published var availableUsers: [ShortUser] = [] @Published var selectedUsers: [String] = [] @@ -23,19 +23,24 @@ class UserSearchViewModel: ObservableObject { Task { do { let service = try StoreCenter.main.service() - let userNames = try await service.getUserNames() - DispatchQueue.main.async { - self.userNames = userNames - self.availableUsers = self.users.compactMap { userId in - self.userNames.first(where: { $0.id == userId }) - } - } + let shortUsers = try await service.getUserAgents() + await self.setAvailableUsers(shortUsers) } catch { Logger.error(error) } } } + @MainActor + func setAvailableUsers(_ users: [ShortUser]) { + self.availableUsers = users + +// self.userNames = users +// self.availableUsers = self.users.compactMap { userId in +// self.userNames.first(where: { $0.id == userId }) +// } + } + func userTapped(_ user: String) { if let index = self.selectedUsers.firstIndex(of: user) { self.selectedUsers.remove(at: index) @@ -76,7 +81,7 @@ struct ShareModelView : View { }.onAppear { self.viewModel.selectedUsers = StoreCenter.main.authorizedUsers(for: self.instance.stringId) - self.viewModel.users = DataStore.shared.user.agents +// self.viewModel.users = DataStore.shared.user.agents } } From 6cefe91b37ed972ef7601c5093431f7d76f90565 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 26 May 2025 12:31:29 +0200 Subject: [PATCH 02/64] add sharing buttons --- PadelClub/Views/Match/MatchDetailView.swift | 4 +++- PadelClub/Views/Tournament/TournamentView.swift | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 2c8ee62..36a3755 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -310,8 +310,10 @@ struct MatchDetailView: View { ToolbarItem(placement: .topBarTrailing) { Menu { - NavigationLink("Partager") { + NavigationLink { ShareModelView(instance: self.match) + } label: { + Label("Partager", systemImage: "square.and.arrow.up") } Toggle(isOn: .init(get: { diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 81a00f9..26f986c 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -286,9 +286,9 @@ struct TournamentView: View { Label("Imprimer", systemImage: "printer") } -// NavigationLink(value: Screen.share) { -// Label("Partager", systemImage: "square.and.arrow.up") -// } + NavigationLink(value: Screen.share) { + Label("Partager", systemImage: "square.and.arrow.up") + } Divider() From c7575d1d675328f31e0ea4a91d91b84037e7114e Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 26 May 2025 12:31:39 +0200 Subject: [PATCH 03/64] refactoring --- .../Views/Calling/GroupStageCallingView.swift | 2 +- .../Components/GroupStageSettingsView.swift | 14 +++++++------- .../GroupStage/Components/GroupStageTeamView.swift | 2 +- PadelClub/Views/GroupStage/GroupStageView.swift | 2 +- .../Views/GroupStage/GroupStagesSettingsView.swift | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index bfade2a..7dc9f34 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -114,7 +114,7 @@ struct GroupStageCallingView: View { } } .overlay { - if groupStage.startDate == nil && groupStage._matches().filter({ $0.startDate != nil }).isEmpty { + if groupStage.startDate == nil && groupStage.matches().filter({ $0.startDate != nil }).isEmpty { ContentUnavailableView { Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") } description: { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index a7968eb..9a84d8c 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -27,7 +27,7 @@ struct GroupStageSettingsView: View { _groupStage = Bindable(groupStage) _groupStageName = .init(wrappedValue: groupStage.name ?? "") _size = .init(wrappedValue: groupStage.size) - _courtIndex = .init(wrappedValue: groupStage._matches().first?.courtIndex ?? 0) + _courtIndex = .init(wrappedValue: groupStage.matches().first?.courtIndex ?? 0) } var tournamentStore: TournamentStore? { @@ -66,12 +66,12 @@ struct GroupStageSettingsView: View { Section { CourtPicker(title: "Terrain dédié", selection: $courtIndex, maxCourt: tournament.courtCount) RowButtonView("Confirmer", role: .destructive) { - groupStage._matches().forEach { match in + groupStage.matches().forEach { match in match.setCourt(courtIndex) } do { - try tournamentStore?.matches.addOrUpdate(contentOfs: groupStage._matches()) + try tournamentStore?.matches.addOrUpdate(contentOfs: groupStage.matches()) } catch { Logger.error(error) } @@ -91,7 +91,7 @@ struct GroupStageSettingsView: View { teams.forEach { team in team.groupStagePosition = nil team.groupStage = nil - groupStage._matches().forEach({ $0.updateTeamScores() }) + groupStage.matches().forEach({ $0.updateTeamScores() }) } do { try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) @@ -111,13 +111,13 @@ struct GroupStageSettingsView: View { Section { RowButtonView("Retirer tous les horaires", role: .destructive) { - groupStage._matches().forEach { match in + groupStage.matches().forEach { match in match.startDate = nil match.endDate = nil } do { - try tournamentStore?.matches.addOrUpdate(contentOfs: groupStage._matches()) + try tournamentStore?.matches.addOrUpdate(contentOfs: groupStage.matches()) } catch { Logger.error(error) } @@ -130,7 +130,7 @@ struct GroupStageSettingsView: View { teams.forEach { team in team.groupStagePosition = nil team.groupStage = nil - groupStage._matches().forEach({ $0.updateTeamScores() }) + groupStage.matches().forEach({ $0.updateTeamScores() }) } do { try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 2ed3de8..cb93030 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -103,7 +103,7 @@ struct GroupStageTeamView: View { team.groupStagePosition = nil team.groupStage = nil _save() - groupStage._matches().forEach({ $0.updateTeamScores() }) + groupStage.matches().forEach({ $0.updateTeamScores() }) } } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index f0bff80..a85d47d 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -245,7 +245,7 @@ struct GroupStageView: View { print(team.pasteData()) team.groupStage = groupStage.id team.groupStagePosition = index - groupStage._matches().forEach({ $0.updateTeamScores() }) + groupStage.matches().forEach({ $0.updateTeamScores() }) do { try tournamentStore?.teamRegistrations.addOrUpdate(instance: team) } catch { diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index 67dff1c..a9cd798 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -144,8 +144,8 @@ struct GroupStagesSettingsView: View { Section { RowButtonView("Retirer tous les horaires", role: .destructive) { - let matches = tournament.groupStages().flatMap({ $0._matches() }) - tournament.groupStages().flatMap({ $0._matches() }).forEach { match in + let matches = tournament.groupStages().flatMap({ $0.matches() }) + tournament.groupStages().flatMap({ $0.matches() }).forEach { match in match.startDate = nil match.endDate = nil } @@ -239,7 +239,7 @@ struct GroupStagesSettingsView: View { teams.forEach { team in team.groupStagePosition = nil team.groupStage = nil - groupStage._matches().forEach({ $0.updateTeamScores() }) + groupStage.matches().forEach({ $0.updateTeamScores() }) } do { try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) From a28a72075e13fe424c4fa0596486f63467a8347e Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 2 Jun 2025 16:27:04 +0200 Subject: [PATCH 04/64] Adds restriction for shared tournaments --- PadelClub/Views/Navigation/Agenda/ActivityView.swift | 6 +++--- .../Views/Navigation/Agenda/EventListView.swift | 10 ++++++---- PadelClub/Views/Tournament/TournamentView.swift | 12 +++++++----- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 81c9bbf..0c6925a 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -38,17 +38,17 @@ struct ActivityView: View { } var runningTournaments: [FederalTournamentHolder] { - return dataStore.tournaments.filter({ $0.endDate == nil }) + return dataStore.tournaments.filter({ $0.endDate == nil && $0.sharing != .granted }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) }) } func getRunningTournaments() -> [Tournament] { - return dataStore.tournaments.filter({ $0.endDate == nil }) + return dataStore.tournaments.filter({ $0.endDate == nil && $0.sharing != .granted }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) }) } var endedTournaments: [Tournament] { - return dataStore.tournaments.filter({ $0.endDate != nil }) + return dataStore.tournaments.filter({ $0.endDate != nil && $0.sharing != .granted }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) }) } // diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 837d6ae..44d873e 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -420,10 +420,12 @@ struct EventListView: View { } #if DEBUG .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button(role: .destructive) { - dataStore.deleteTournament(tournament) - } label: { - LabelDelete() + if tournament.sharing == nil { + Button(role: .destructive) { + dataStore.deleteTournament(tournament) + } label: { + LabelDelete() + } } // Button() { // self.showUserSearch = true diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 26f986c..9fad158 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -290,11 +290,13 @@ struct TournamentView: View { Label("Partager", systemImage: "square.and.arrow.up") } - Divider() - - NavigationLink(value: Screen.stateSettings) { - Text("Gestion du tournoi") - Text("Annuler, supprimer ou terminer le tournoi") + if self.tournament.sharing == nil { + Divider() + + NavigationLink(value: Screen.stateSettings) { + Text("Gestion du tournoi") + Text("Annuler, supprimer ou terminer le tournoi") + } } } label: { LabelOptions() From c4bd58a1af7911b372ce2cd901ec54c15b6f65c3 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 3 Jun 2025 10:41:54 +0200 Subject: [PATCH 05/64] Improve tournament selection --- PadelClub/ViewModel/AgendaDestination.swift | 4 ++-- .../Views/Navigation/Organizer/TournamentOrganizerView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index d1c56ee..e025a0b 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -60,9 +60,9 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { func badgeValue() -> Int? { switch self { case .activity: - DataStore.shared.tournaments.filter { $0.endDate == nil && $0.isDeleted == false && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count + DataStore.shared.tournaments.filter { $0.endDate == nil && $0.isDeleted == false && FederalDataViewModel.shared.isTournamentValidForFilters($0) && $0.sharing != .granted }.count case .history: - DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count + DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) && $0.sharing != .granted }.count case .tenup: FederalDataViewModel.shared.filteredFederalTournaments.map { $0.tournaments.count }.reduce(0,+) case .around: diff --git a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift index 8358e10..fb883cf 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift @@ -31,7 +31,7 @@ struct TournamentOrganizerView: View { .toolbarBackground(.visible, for: .navigationBar) } } - let tournaments = dataStore.tournaments.filter({ $0.hasEnded() == false && $0.isDeleted == false && $0.isCanceled == false }).sorted(by: \.startDate).reversed() + let tournaments = dataStore.tournaments.filter({ $0.hasEnded() == false && $0.isDeleted == false && $0.isCanceled == false && $0.sharing != .granted }).sorted(by: \.startDate).reversed() if tournaments.isEmpty == false { Divider() HStack { From 8d33bc02048f2d5a1bba0bf7b165cdefef8d3ff8 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 3 Jun 2025 10:49:36 +0200 Subject: [PATCH 06/64] cleanup view --- PadelClub/Views/User/ShareModelView.swift | 82 ++++++++++------------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/PadelClub/Views/User/ShareModelView.swift b/PadelClub/Views/User/ShareModelView.swift index b504e6a..70b33d9 100644 --- a/PadelClub/Views/User/ShareModelView.swift +++ b/PadelClub/Views/User/ShareModelView.swift @@ -10,51 +10,8 @@ import LeStorage import SwiftUI import PadelClubData -class UserSearchViewModel: ObservableObject { - - @Published var searchText = "" -// @Published var userNames: [ShortUser] = [] - -// @Published var users: [String] = [] - @Published var availableUsers: [ShortUser] = [] - @Published var selectedUsers: [String] = [] - - init() { - Task { - do { - let service = try StoreCenter.main.service() - let shortUsers = try await service.getUserAgents() - await self.setAvailableUsers(shortUsers) - } catch { - Logger.error(error) - } - } - } - - @MainActor - func setAvailableUsers(_ users: [ShortUser]) { - self.availableUsers = users - -// self.userNames = users -// self.availableUsers = self.users.compactMap { userId in -// self.userNames.first(where: { $0.id == userId }) -// } - } - - func userTapped(_ user: String) { - if let index = self.selectedUsers.firstIndex(of: user) { - self.selectedUsers.remove(at: index) - } else { - self.selectedUsers.append(user) - } - } - - func contains(_ user: String) -> Bool { - return self.selectedUsers.firstIndex(of: user) != nil - } -} - struct ShareModelView : View { + @StateObject private var viewModel = UserSearchViewModel() let instance: T @@ -81,7 +38,6 @@ struct ShareModelView : View { }.onAppear { self.viewModel.selectedUsers = StoreCenter.main.authorizedUsers(for: self.instance.stringId) -// self.viewModel.users = DataStore.shared.user.agents } } @@ -111,6 +67,42 @@ struct UserRow: View { } } +class UserSearchViewModel: ObservableObject { + + @Published var searchText = "" + @Published var availableUsers: [ShortUser] = [] + @Published var selectedUsers: [String] = [] + + init() { + Task { + do { + let service = try StoreCenter.main.service() + let shortUsers = try await service.getUserAgents() + await self.setAvailableUsers(shortUsers) + } catch { + Logger.error(error) + } + } + } + + @MainActor + func setAvailableUsers(_ users: [ShortUser]) { + self.availableUsers = users + } + + func userTapped(_ user: String) { + if let index = self.selectedUsers.firstIndex(of: user) { + self.selectedUsers.remove(at: index) + } else { + self.selectedUsers.append(user) + } + } + + func contains(_ user: String) -> Bool { + return self.selectedUsers.firstIndex(of: user) != nil + } +} + // Preview provider struct ShareModelView_Previews: PreviewProvider { static var previews: some View { From a41080685c5c501e086c5c554b32053e61cac99d Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 10 Jun 2025 14:53:51 +0200 Subject: [PATCH 07/64] adds sync and fix build --- PadelClub.xcodeproj/project.pbxproj | 28 ++++++++-------------------- PadelClub/AppDelegate.swift | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 2745959..a3d49d0 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD2F22C412681000DBD9A /* AppDelegate.swift */; }; + C410F54E2DF340FE009713ED /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4D05D462DC10AE5009B053C /* WebKit.framework */; }; C411C9C32BEBA453003017AD /* ServerDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C22BEBA453003017AD /* ServerDataTests.swift */; }; C411C9C92BF219CB003017AD /* UserDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C82BF219CB003017AD /* UserDataTests.swift */; }; C411C9D02BF38F41003017AD /* TokenExemptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */; }; @@ -615,7 +616,6 @@ FF70FBAF2C90584900129CC2 /* UpdateSourceRankDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */; }; FF70FBB02C90584900129CC2 /* GlobalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */; }; FF70FBB22C90584900129CC2 /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; }; - FF70FBB42C90584900129CC2 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = FF70FABE2C90584900129CC2 /* Algorithms */; }; FF70FBB52C90584900129CC2 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF70FAC02C90584900129CC2 /* Zip */; }; FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; }; FF70FBB82C90584900129CC2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; }; @@ -1205,7 +1205,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FF70FBB42C90584900129CC2 /* Algorithms in Frameworks */, + C410F54E2DF340FE009713ED /* WebKit.framework in Frameworks */, FF39B6172DC88267004E10CE /* PadelClubData.framework in Frameworks */, FF70FBB52C90584900129CC2 /* Zip in Frameworks */, FF70FBB62C90584900129CC2 /* LeStorage.framework in Frameworks */, @@ -2050,7 +2050,6 @@ ); name = "PadelClub TestFlight"; packageProductDependencies = ( - FF70FABE2C90584900129CC2 /* Algorithms */, FF70FAC02C90584900129CC2 /* Zip */, ); productName = PadelClub; @@ -3352,7 +3351,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\""; @@ -3375,13 +3374,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.33; + MARKETING_VERSION = 1.2.34; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -3395,7 +3395,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; @@ -3417,7 +3417,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.33; + MARKETING_VERSION = 1.2.34; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3425,6 +3425,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = TESTFLIGHT; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -3506,14 +3507,6 @@ minimumVersion = 2.1.2; }; }; - FF70FABF2C90584900129CC2 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-algorithms.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.2.0; - }; - }; FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/marmelroy/Zip"; @@ -3543,11 +3536,6 @@ package = FF4CBF422C996C0600151637 /* XCRemoteSwiftPackageReference "Zip" */; productName = Zip; }; - FF70FABE2C90584900129CC2 /* Algorithms */ = { - isa = XCSwiftPackageProductDependency; - package = FF70FABF2C90584900129CC2 /* XCRemoteSwiftPackageReference "swift-algorithms" */; - productName = Algorithms; - }; FF70FAC02C90584900129CC2 /* Zip */ = { isa = XCSwiftPackageProductDependency; package = FF70FAC12C90584900129CC2 /* XCRemoteSwiftPackageReference "Zip" */; diff --git a/PadelClub/AppDelegate.swift b/PadelClub/AppDelegate.swift index b58e458..05b217c 100644 --- a/PadelClub/AppDelegate.swift +++ b/PadelClub/AppDelegate.swift @@ -26,11 +26,23 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel return true } + fileprivate func _domain() -> String { +#if DEBUG + return "xlr.alwaysdata.net" +#elseif TESTFLIGHT + return "asgi.padelclub.app" +#elseif PRODTEST + return "padelclub.app" +#else + return "padelclub.app" +#endif + } + fileprivate func _configureLeStorage() { StoreCenter.main.blackListUserName("apple-test") StoreCenter.main.classProject = "PadelClubData" // let secureScheme = true - let domain: String = URLs.activationHost.rawValue + let domain: String = self._domain() #if DEBUG if let secure = PListReader.readBool(plist: "local", key: "secure_server"), @@ -40,7 +52,7 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: true, useSynchronization: true) } #else - StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: false) + StoreCenter.main.configureURLs(secureScheme: true, domain: domain, webSockets: true, useSynchronization: true) #endif StoreCenter.main.logsFailedAPICalls() From 4ee338df6d51dca230348172b2f35238dce71aec Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 26 Jun 2025 11:25:09 +0200 Subject: [PATCH 08/64] merge main --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- .../xcschemes/PadelClub ProdTest.xcscheme | 2 +- .../xcshareddata/xcschemes/PadelClub Raw.xcscheme | 2 +- .../xcschemes/PadelClub TestFlight.xcscheme | 4 ++-- .../xcshareddata/xcschemes/PadelClub.xcscheme | 2 +- .../Views/Calling/GroupStageCallingView.swift | 2 +- .../Components/GroupStageSettingsView.swift | 14 +++++++------- .../GroupStage/Components/GroupStageTeamView.swift | 2 +- PadelClub/Views/GroupStage/GroupStageView.swift | 2 +- .../Views/GroupStage/GroupStagesSettingsView.swift | 6 +++--- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 04dc537..650c1ec 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3359,7 +3359,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3403,7 +3403,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 6; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme b/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme index 08847de..739a24c 100644 --- a/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme +++ b/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 3 Jul 2025 09:43:40 +0200 Subject: [PATCH 09/64] version 1.2.40 for testflight --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- .../xcshareddata/xcschemes/PadelClub ProdTest.xcscheme | 2 +- .../xcshareddata/xcschemes/PadelClub Raw.xcscheme | 2 +- .../xcshareddata/xcschemes/PadelClub TestFlight.xcscheme | 4 ++-- .../xcshareddata/xcschemes/PadelClub.xcscheme | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c3c2b61..3a1585f 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3359,7 +3359,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3382,7 +3382,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.37; + MARKETING_VERSION = 1.2.40; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3403,7 +3403,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3425,7 +3425,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.37; + MARKETING_VERSION = 1.2.40; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme b/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme index 739a24c..08847de 100644 --- a/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme +++ b/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme @@ -1,6 +1,6 @@ Date: Fri, 5 Sep 2025 14:23:44 +0200 Subject: [PATCH 10/64] fix playerblock view --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Views/Match/Components/PlayerBlockView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b9369ed..f153323 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3398,7 +3398,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.40; + MARKETING_VERSION = 1.2.49; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3441,7 +3441,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.40; + MARKETING_VERSION = 1.2.49; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index c1a6a5c..56d88c9 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -77,7 +77,7 @@ struct PlayerBlockView: View { VStack(alignment: .leading) { ZStack(alignment: .leading) { VStack { - if let teamName = team?.name { + if let teamName = team?.name, teamName.isEmpty == false { Text(teamName).foregroundStyle(.secondary).font(.footnote) } Text("longLabelPlayerOne").lineLimit(1) @@ -91,7 +91,7 @@ struct PlayerBlockView: View { Text("Repêchée").italic().font(.caption) } - if let teamName = team.name { + if let teamName = team.name, teamName.isEmpty == false { Text(teamName).foregroundStyle(.secondary).font(.footnote) } else if team.players().isEmpty { if team.isWildCard() { From 8b7202b0ebf9096b01b39b1f368fc0bbc0aa54b4 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 6 Sep 2025 13:05:35 +0200 Subject: [PATCH 11/64] fix issue with import session fix naming --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- .../Views/Tournament/ConsolationTournamentImportView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f153323..015c73a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3375,7 +3375,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\""; @@ -3419,7 +3419,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/Views/Tournament/ConsolationTournamentImportView.swift b/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift index d26cddc..1dd587e 100644 --- a/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift +++ b/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift @@ -218,8 +218,8 @@ struct ConsolationTournamentImportView: View { case .losers: if selectedGroupStagePosition.isEmpty { return $0.qualified == false - } else if let position = $0.groupStagePosition { - return $0.qualified == false && selectedGroupStagePosition.contains(position) + } else if let position = $0.groupStageObject()?.finalPosition(ofTeam: $0) { + return $0.qualified == false && selectedGroupStagePosition.contains(position + 1) } else { return $0.qualified == false } From a9ce9659f1cd6da1dce2a091fafc929285ecffcb Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 11 Sep 2025 15:54:55 +0200 Subject: [PATCH 12/64] add information about sync --- .../Navigation/Toolbox/APICallsListView.swift | 25 ++++++---- .../Toolbox/DebugSettingsView.swift | 46 ++++++++++++++++++- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/PadelClub/Views/Navigation/Toolbox/APICallsListView.swift b/PadelClub/Views/Navigation/Toolbox/APICallsListView.swift index 0382d5f..a934fe3 100644 --- a/PadelClub/Views/Navigation/Toolbox/APICallsListView.swift +++ b/PadelClub/Views/Navigation/Toolbox/APICallsListView.swift @@ -12,20 +12,27 @@ import PadelClubData struct APICallsListView: View { @State var descriptors: [CollectionDescriptor] = [] + @State var total = 0 var body: some View { List { - ForEach(self.descriptors) { descriptor in - - NavigationLink { - if let syncedType = descriptor.type as? any SyncedStorable.Type { - APICallsView(name: descriptor.name, type: syncedType) + Section { + LabeledContent("Total count", value: "\(total)") + } + + Section { + ForEach(self.descriptors) { descriptor in + + NavigationLink { + if let syncedType = descriptor.type as? any SyncedStorable.Type { + APICallsView(name: descriptor.name, type: syncedType) + } + } label: { + LabeledContent(descriptor.name, value: descriptor.count.string) } - } label: { - LabeledContent(descriptor.name, value: descriptor.count.string) + } - } }.onAppear { self.load() @@ -49,6 +56,8 @@ struct APICallsListView: View { func loadCount(_ type: T.Type, _ descriptor: CollectionDescriptor) async { let calls = await StoreCenter.main.apiCalls(type: type) descriptor.count = calls.count + + self.total = total + descriptor.count // Logger.log("\(descriptor.name), count = \(calls.count)") } diff --git a/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift index 95ec2a3..874b833 100644 --- a/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift +++ b/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift @@ -10,6 +10,10 @@ import LeStorage import PadelClubData struct DebugSettingsView: View { + @State private var errorMessage: String? + @State private var showingError = false + @State private var isSynchronizing = false + var body: some View { List { @@ -17,7 +21,19 @@ struct DebugSettingsView: View { LabeledContent("Has Websocket Manager", value: self._hasWebSocketManager) LabeledContent("Websocket ping", value: self._wsPingStatus) LabeledContent("Websocket failure", value: self._wsFailure) - LabeledContent("Last sync date", value: self._lastSyncDate) + LabeledContent("Last synced object date", value: self._lastSyncDate) + if isSynchronizing { + HStack { + ProgressView() + .scaleEffect(0.8) + Text("Synchronizing...") + .foregroundColor(.secondary) + } + } else { + Button("Synchronize") { + self._synchronize() + } + } } Section("Settings") { @@ -40,6 +56,11 @@ struct DebugSettingsView: View { } } + .alert("Synchronization Error", isPresented: $showingError) { + Button("OK") { } + } message: { + Text(errorMessage ?? "An unknown error occurred") + } } fileprivate var _userId: String { @@ -79,6 +100,29 @@ struct DebugSettingsView: View { fileprivate var _lastSyncDate: String { return "\(StoreCenter.main.lastSyncDate)" } + + fileprivate func _synchronize() { + Logger.log("launch sync...") + Task { + await MainActor.run { + isSynchronizing = true + } + + do { + try await StoreCenter.main.synchronizeLastUpdates() + } catch { + Logger.error(error) + await MainActor.run { + errorMessage = error.localizedDescription + showingError = true + } + } + + await MainActor.run { + isSynchronizing = false + } + } + } } struct DebugPurchaseView: View { From 2e2b83f8043867e33d5648843bdabf958b82a61d Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 17:35:00 +0200 Subject: [PATCH 13/64] change sync server to padelclub.app --- PadelClub/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/AppDelegate.swift b/PadelClub/AppDelegate.swift index 05b217c..3fe421e 100644 --- a/PadelClub/AppDelegate.swift +++ b/PadelClub/AppDelegate.swift @@ -30,7 +30,7 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel #if DEBUG return "xlr.alwaysdata.net" #elseif TESTFLIGHT - return "asgi.padelclub.app" + return "padelclub.app" #elseif PRODTEST return "padelclub.app" #else From 1601f72ad2333bb798e9a5218722948cad2c5b39 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 24 Sep 2025 20:29:00 +0200 Subject: [PATCH 14/64] fix issue with ios 26 add onboarding view fix bugs --- PadelClub.xcodeproj/project.pbxproj | 17 +- .../ViewModel/FederalDataViewModel.swift | 12 +- PadelClub/ViewModel/SearchViewModel.swift | 10 + .../Calling/Components/MenuWarningView.swift | 1 - .../Views/Components/ButtonValidateView.swift | 14 +- PadelClub/Views/Components/Labels.swift | 12 +- .../Components/GroupStageSettingsView.swift | 20 +- .../Views/GroupStage/GroupStageView.swift | 2 +- .../GroupStage/GroupStagesSettingsView.swift | 8 + .../Views/GroupStage/GroupStagesView.swift | 2 +- PadelClub/Views/Match/MatchDetailView.swift | 11 + .../Navigation/Agenda/ActivityView.swift | 144 ++++++++--- .../Agenda/TournamentLookUpView.swift | 64 ++++- .../Agenda/WeekdaySelectionView.swift | 36 +++ PadelClub/Views/Navigation/MainView.swift | 17 ++ .../Views/Navigation/OnboardingView.swift | 235 ++++++++++++++++++ PadelClub/Views/Score/EditScoreView.swift | 23 +- PadelClub/Views/Score/FollowUpMatchView.swift | 6 +- .../Shared/SelectablePlayerListView.swift | 97 ++++---- .../Views/Shared/TournamentFilterView.swift | 2 + PadelClub/Views/Team/TeamRestingView.swift | 2 +- .../Screen/InscriptionManagerView.swift | 10 +- .../Tournament/Screen/PrintSettingsView.swift | 2 +- .../Screen/TournamentRankView.swift | 2 +- .../Tournament/TournamentRunningView.swift | 2 +- .../Views/Tournament/TournamentView.swift | 15 +- 26 files changed, 634 insertions(+), 132 deletions(-) create mode 100644 PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift create mode 100644 PadelClub/Views/Navigation/OnboardingView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 28b1759..57ea32a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -711,6 +711,12 @@ FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; + FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; + FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; + FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; + FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; }; + FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; }; + FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; @@ -1112,6 +1118,8 @@ FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = ""; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + FFB0FF662E81B671009EDEAC /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdaySelectionView.swift; sourceTree = ""; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = ""; }; FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.swift; sourceTree = ""; }; FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = ""; }; @@ -1569,6 +1577,7 @@ isa = PBXGroup; children = ( FF59FFB62B90EFBF0061EFF9 /* MainView.swift */, + FFB0FF662E81B671009EDEAC /* OnboardingView.swift */, FFD783FB2B91B919000F62A6 /* Agenda */, FF3F74FA2B91A04B004CFE0E /* Organizer */, FF3F74FB2B91A060004CFE0E /* Toolbox */, @@ -1897,6 +1906,7 @@ FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */, FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */, FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */, + FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */, FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */, ); path = Agenda; @@ -2386,7 +2396,9 @@ FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, + FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, + FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */, C497723A2DC28A92005CD239 /* ComposeViews.swift in Sources */, FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, @@ -2651,7 +2663,9 @@ FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */, FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, + FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */, C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, + FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */, C49772392DC28A92005CD239 /* ComposeViews.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */, @@ -2894,7 +2908,9 @@ FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */, FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, + FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */, C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, + FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */, C497723B2DC28A92005CD239 /* ComposeViews.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */, @@ -3123,7 +3139,6 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; - ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 593c6c5..4297c96 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -23,6 +23,7 @@ class FederalDataViewModel { var searchAttemptCount: Int = 0 var dayDuration: Int? var dayPeriod: DayPeriod = .all + var weekdays: Set = Set() var lastError: NetworkManagerError? func filterStatus() -> String { @@ -36,6 +37,7 @@ class FederalDataViewModel { } labels.append(contentsOf: clubNames.formatList()) + labels.append(contentsOf: weekdays.map { Date.weekdays[$0 - 1] }.formatList()) if dayPeriod != .all { labels.append(dayPeriod.localizedDayPeriodLabel()) } @@ -72,7 +74,7 @@ class FederalDataViewModel { } func areFiltersEnabled() -> Bool { - (levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false + (weekdays.isEmpty && levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false } var filteredFederalTournaments: [FederalTournamentHolder] { @@ -96,6 +98,8 @@ class FederalDataViewModel { (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) && (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) + && + (weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay)) }) } @@ -106,6 +110,8 @@ class FederalDataViewModel { (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) && (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) + && + (weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay)) }) .flatMap { $0.tournaments } .filter { @@ -137,6 +143,8 @@ class FederalDataViewModel { (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) && (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) + && + (weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay)) if let codeClub = tournament.club()?.code { return firstPart && (selectedClubs.isEmpty || selectedClubs.contains(codeClub)) @@ -157,6 +165,8 @@ class FederalDataViewModel { (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) && (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) + && + (weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay)) } func gatherTournaments(clubs: [Club], startDate: Date, endDate: Date? = nil) async throws { diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index e6ffc3b..cab9f3d 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -74,6 +74,16 @@ class SearchViewModel: ObservableObject, Identifiable { } return message.joined(separator: "\n") } + + func sortTitle() -> String { + var base = [sortOption.localizedLabel()] + base.append((ascending ? "croissant" : "décroissant")) + + if selectedAgeCategory != .unlisted { + base.append(selectedAgeCategory.localizedFederalAgeLabel()) + } + return base.joined(separator: " ") + } func codeClubs() -> [String] { let clubs: [Club] = DataStore.shared.user.clubsObjects() diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index fb3d4bf..86301a1 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -44,7 +44,6 @@ struct MenuWarningView: View { } } label: { Text("Prévenir") - .underline() } .sheet(isPresented: self.$showSubscriptionView, content: { NavigationStack { diff --git a/PadelClub/Views/Components/ButtonValidateView.swift b/PadelClub/Views/Components/ButtonValidateView.swift index 0543e4d..fb50b34 100644 --- a/PadelClub/Views/Components/ButtonValidateView.swift +++ b/PadelClub/Views/Components/ButtonValidateView.swift @@ -13,10 +13,16 @@ struct ButtonValidateView: View { let action: () -> () var body: some View { - Button(title, role: role) { - action() + if #available(iOS 26.0, *) { + + Button(title, systemImage: "checkmark", role: role) { + action() + } + .buttonStyle(.borderedProminent) + } else { + Button(title, role: role) { + action() + } } - .clipShape(Capsule()) - .buttonStyle(.bordered) } } diff --git a/PadelClub/Views/Components/Labels.swift b/PadelClub/Views/Components/Labels.swift index a87e34a..45f8947 100644 --- a/PadelClub/Views/Components/Labels.swift +++ b/PadelClub/Views/Components/Labels.swift @@ -9,7 +9,11 @@ import SwiftUI struct LabelOptions: View { var body: some View { - Label("Options", systemImage: "ellipsis.circle") + if #available(iOS 26.0, *) { + Label("Options", systemImage: "ellipsis") + } else { + Label("Options", systemImage: "ellipsis.circle") + } } } @@ -39,6 +43,10 @@ struct ShareLabel: View { struct LabelFilter: View { var body: some View { - Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") + if #available(iOS 26.0, *) { + Label("Filtrer", systemImage: "line.3.horizontal.decrease") + } else { + Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") + } } } diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 865e42c..ade9616 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -126,17 +126,7 @@ struct GroupStageSettingsView: View { Section { RowButtonView("Retirer tout le monde", role: .destructive) { - let teams = groupStage.teams() - teams.forEach { team in - team.groupStagePosition = nil - team.groupStage = nil - groupStage._matches().forEach({ $0.updateTeamScores() }) - } - do { - try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) - } catch { - Logger.error(error) - } + groupStage.removeAllTeams() } } footer: { Text("Toutes les équipes seront retirées et les scores des matchs seront perdus.") @@ -188,6 +178,14 @@ struct GroupStageSettingsView: View { } footer: { Text("Mets à jour les équipes de la poule si jamais une erreur est persistante.") } + + if tournament.lastStep() == 0 { + RowButtonView("Effacer la poule", role: .destructive) { + tournament.deleteGroupStage(groupStage) + dismiss() + dataStore.tournaments.addOrUpdate(instance: self.tournament) + } + } } .onChange(of: size) { if size != groupStage.size { diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index bc8acb9..7007e66 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -70,7 +70,7 @@ struct GroupStageView: View { } Section { - MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) + MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches, runningMatches: runningMatches), hideWhenEmpty: true) } Section { diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index f87f6d3..d085b4d 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -99,6 +99,14 @@ struct GroupStagesSettingsView: View { } if tournament.lastStep() == 0, step == 0 { + + Section { + RowButtonView("Ajouter une poule", role: .destructive) { + self.tournament.addGroupStage() + dataStore.tournaments.addOrUpdate(instance: self.tournament) + } + } + Section { RowButtonView("Ajouter une phase de poule", role: .destructive) { tournament.addNewGroupStageStep() diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index ddf5d81..a14d4bb 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -234,7 +234,7 @@ struct GroupStagesView: View { Section { - MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), isExpanded: false) + MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches, runningMatches: runningMatches), isExpanded: false) } Section { diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 5e59ccb..2b130a8 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -317,7 +317,18 @@ struct MatchDetailView: View { })) { Text(match.confirmed ? "Confirmé" : "Non confirmé") } + + if match.hasWalkoutTeam() == true { + Divider() + Button(role: .destructive) { + match.removeWalkOut() + save() + } label: { + Text("Annuler le forfait") + } + } + Divider() if match.courtIndex != nil { diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 6a29320..25573f3 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -25,7 +25,8 @@ struct ActivityView: View { @State private var quickAccessScreen: QuickAccessScreen? = nil @State private var displaySearchView: Bool = false @State private var pasteString: String? = nil - + @State private var presentOnboarding: Bool = false + enum QuickAccessScreen : Identifiable, Hashable { case inscription @@ -77,15 +78,21 @@ struct ActivityView: View { @ViewBuilder private func _pasteView() -> some View { - Button { - quickAccessScreen = .inscription - } label: { - Image(systemName: "person.crop.circle.badge.plus") - .resizable() - .scaledToFit() - .frame(minHeight: 32) + if #available(iOS 26.0, *) { + Button("Ajouter une équipe", systemImage: "person.badge.plus") { + quickAccessScreen = .inscription + } + } else { + Button { + quickAccessScreen = .inscription + } label: { + Image(systemName: "person.crop.circle.badge.plus") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + .accessibilityLabel("Ajouter une équipe") } - .accessibilityLabel("Ajouter une équipe") // if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true { // PasteButton(payloadType: String.self) { strings in @@ -222,44 +229,82 @@ struct ActivityView: View { // print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed) // }) .toolbar { - ToolbarItemGroup(placement: .topBarLeading) { - Button { - switch viewStyle { - case .list: - viewStyle = .calendar - case .calendar: - viewStyle = .list + ToolbarItem(placement: .topBarLeading) { + if #available(iOS 26.0, *) { + Button("Vue calendrier", systemImage: "calendar") { + switch viewStyle { + case .list: + viewStyle = .calendar + case .calendar: + viewStyle = .list + } } - } label: { - Image(systemName: "calendar.circle") - .resizable() - .scaledToFit() - .frame(minHeight: 32) + .symbolVariant(viewStyle == .calendar ? .fill : .none) + } else { + Button { + switch viewStyle { + case .list: + viewStyle = .calendar + case .calendar: + viewStyle = .list + } + } label: { + Image(systemName: "calendar.circle") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + .symbolVariant(viewStyle == .calendar ? .fill : .none) } - .symbolVariant(viewStyle == .calendar ? .fill : .none) + } + + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .topBarLeading) + } + + ToolbarItem(placement: .topBarLeading) { - Button { - presentFilterView.toggle() - } label: { - Image(systemName: "line.3.horizontal.decrease.circle") - .resizable() - .scaledToFit() - .frame(minHeight: 32) + if #available(iOS 26.0, *) { + + Button("Filtre", systemImage: "line.3.horizontal.decrease") { + presentFilterView.toggle() + } + .symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) + } else { + Button { + presentFilterView.toggle() + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + .symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) } - .symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) - + } + + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .topBarLeading) + } + + ToolbarItem(placement: .topBarLeading) { _pasteView() } ToolbarItem(placement: .topBarTrailing) { - Button { - newTournament = Tournament.newEmptyInstance() - - } label: { - Image(systemName: "plus.circle.fill") - .resizable() - .scaledToFit() - .frame(minHeight: 32) + if #available(iOS 26.0, *) { + Button("Ajouter", systemImage: "plus") { + newTournament = Tournament.newEmptyInstance() + } + } else { + Button { + newTournament = Tournament.newEmptyInstance() + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } } } @@ -296,6 +341,10 @@ struct ActivityView: View { } } } + .sheet(isPresented: $presentOnboarding, content: { + OnboardingView() + .environmentObject(dataStore) + }) .sheet(isPresented: $presentFilterView) { TournamentFilterView(federalDataViewModel: federalDataViewModel) .environment(navigation) @@ -474,6 +523,11 @@ struct ActivityView: View { navigation.agendaDestination = .tenup } SupportButtonView(contentIsUnavailable: true) + + FooterButtonView("Vous n'êtes pas un juge-arbitre ou un organisateur de tournoi ? En savoir plus") { + presentOnboarding = true + } + .tint(.logoBackground) } } @@ -485,6 +539,7 @@ struct ActivityView: View { } } + @ViewBuilder private func _tenupEmptyView() -> some View { if dataStore.user.hasTenupClubs() == false { ContentUnavailableView { @@ -496,6 +551,10 @@ struct ActivityView: View { presentClubSearchView = true } .padding() + FooterButtonView("Cette app est dédié aux juge-arbitres et organisateurs de tournoi. Vous êtes un joueur à la recherche d'un tournoi homologué ? Utilisez notre outil de recherche") { + navigation.agendaDestination = .around + } + .tint(.logoBackground) } } else { ContentUnavailableView { @@ -518,13 +577,16 @@ struct ActivityView: View { ContentUnavailableView { Label("Recherche de tournoi", systemImage: "magnifyingglass") } description: { - Text("Chercher les tournois autour de vous pour mieux décider les tournois à proposer dans votre club. Padel Club vous facilite même l'inscription !") + Text("Chercher les tournois homologués autour de vous. Padel Club vous facilite même l'inscription !") } actions: { - RowButtonView("Lancer la recherche") { + RowButtonView("Chercher un tournoi") { displaySearchView = true } .padding() } + .onAppear { + displaySearchView = true + } } else { if federalDataViewModel.lastError == nil { ContentUnavailableView { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index d6417e7..102f900 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -30,7 +30,19 @@ struct TournamentLookUpView: View { @State private var confirmSearch: Bool = false @State private var locationRequested = false @State private var apiError: StoreError? + @State private var quickOption: QuickDateOption? = nil + enum QuickDateOption: String, Identifiable, Hashable { + case thisMonth + case thisWeek + case nextWeek + case nextMonth + case twoWeeks + case nextThreeMonth + + var id: String { self.rawValue } + } + var tournaments: [FederalTournament] { federalDataViewModel.searchedFederalTournaments } @@ -140,15 +152,21 @@ struct TournamentLookUpView: View { } .toolbarTitleDisplayMode(.large) .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", systemImage: "xmark", role: .cancel) { + dismiss() + } + } ToolbarItem(placement: .bottomBar) { if revealSearchParameters { - FooterButtonView("Lancer la recherche") { + Button("Lancer la recherche") { if dataStore.appSettings.city.isEmpty { confirmSearch = true } else { runSearch() } } + .buttonStyle(.borderedProminent) .disabled(searching) } else if searching { HStack(spacing: 20) { @@ -197,7 +215,7 @@ struct TournamentLookUpView: View { Text("Ré-initialiser la recherche") } } label: { - Label("Options", systemImage: "ellipsis.circle") + LabelOptions() } } } @@ -253,6 +271,7 @@ struct TournamentLookUpView: View { federalDataViewModel.searchedFederalTournaments = [] searching = true requestedToGetAllPages = false + federalDataViewModel.weekdays = dataStore.appSettings.weekdays federalDataViewModel.searchAttemptCount += 1 federalDataViewModel.dayPeriod = dataStore.appSettings.dayPeriod federalDataViewModel.dayDuration = dataStore.appSettings.dayDuration @@ -340,6 +359,42 @@ struct TournamentLookUpView: View { var searchParametersView: some View { @Bindable var appSettings = dataStore.appSettings Section { + Picker(selection: $quickOption) { + Text("Libre").tag(nil as QuickDateOption?) + Text("Cette semaine").tag(QuickDateOption.thisWeek as QuickDateOption?) + Text("2 prochaines semaines").tag(QuickDateOption.twoWeeks as QuickDateOption?) + Text("La semaine prochaine").tag(QuickDateOption.nextWeek as QuickDateOption?) + Text("Ce mois-ci").tag(QuickDateOption.thisMonth as QuickDateOption?) + Text("2 prochains mois").tag(QuickDateOption.nextMonth as QuickDateOption?) + Text("3 prochains mois").tag(QuickDateOption.nextThreeMonth as QuickDateOption?) + } label: { + Text("Choix de dates") + } + .pickerStyle(.menu) + .onChange(of: quickOption) { oldValue, newValue in + switch newValue { + case nil: + break + case .twoWeeks: + appSettings.startDate = Date().startOfDay + appSettings.endDate = Date().endOfWeek.addingTimeInterval(14 * 24 * 60 * 60) + case .nextWeek: + appSettings.startDate = Date().endOfWeek.nextDay.startOfDay + appSettings.endDate = Date().endOfWeek.addingTimeInterval(7 * 24 * 60 * 60) + case .thisMonth: + appSettings.startDate = Date().startOfDay + appSettings.endDate = Date().endOfMonth.endOfDay() + case .thisWeek: + appSettings.startDate = Date().startOfDay + appSettings.endDate = Date().endOfWeek + case .nextMonth: + appSettings.startDate = Date().startOfDay + appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth + case .nextThreeMonth: + appSettings.startDate = Date().startOfDay + appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth.nextDay.endOfMonth + } + } DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date) DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date) Picker(selection: $appSettings.dayDuration) { @@ -350,7 +405,9 @@ struct TournamentLookUpView: View { } label: { Text("Durée souhaitée (en jours)") } - + + WeekdayselectionView(weekdays: $appSettings.weekdays) + Picker(selection: $appSettings.dayPeriod) { ForEach(DayPeriod.allCases) { Text($0.localizedDayPeriodLabel().capitalized).tag($0) @@ -398,6 +455,7 @@ struct TournamentLookUpView: View { } Picker(selection: $appSettings.distance) { + Text(distanceLimit(distance:15).formatted()).tag(15.0) Text(distanceLimit(distance:30).formatted()).tag(30.0) Text(distanceLimit(distance:50).formatted()).tag(50.0) Text(distanceLimit(distance:60).formatted()).tag(60.0) diff --git a/PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift b/PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift new file mode 100644 index 0000000..6b4fddc --- /dev/null +++ b/PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift @@ -0,0 +1,36 @@ +// +// WeekdayselectionView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 24/09/2025. +// + +import SwiftUI +import PadelClubData +import LeStorage + +struct WeekdayselectionView: View { + @Binding var weekdays: Set + + var body: some View { + NavigationLink { + List((1...7), selection: $weekdays) { type in + Text(Date.weekdays[type - 1]).tag(type as Int) + } + .navigationTitle("Jour de la semaine") + .environment(\.editMode, Binding.constant(EditMode.active)) + } label: { + HStack { + Text("Jour de la semaine") + Spacer() + if weekdays.isEmpty || weekdays.count == 7 { + Text("N'importe") + .foregroundStyle(.secondary) + } else { + Text(weekdays.sorted().map({ Date.weekdays[$0 - 1] }).joined(separator: ", ")) + .foregroundStyle(.secondary) + } + } + } + } +} diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 8d282fe..209a81f 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -19,7 +19,11 @@ struct MainView: View { @Environment(ImportObserver.self) private var importObserver: ImportObserver @State private var mainViewId: UUID = UUID() + @State private var presentOnboarding: Bool = false + @State private var canPresentOnboarding: Bool = false + @AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false + var lastDataSource: String? { dataStore.appSettings.lastDataSource } @@ -90,6 +94,17 @@ struct MainView: View { // PadelClubView() // .tabItem(for: .padelClub) } + .onAppear { + if canPresentOnboarding || StoreCenter.main.userId != nil { + if didSeeOnboarding == false { + presentOnboarding = true + } + } + } + .sheet(isPresented: $presentOnboarding, content: { + OnboardingView() + .environmentObject(dataStore) + }) .id(mainViewId) .onChange(of: dataStore.user.id) { print("dataStore.user.id = ", dataStore.user.id) @@ -98,6 +113,8 @@ struct MainView: View { navigation.path.removeLast(navigation.path.count) mainViewId = UUID() } + + canPresentOnboarding = true } .environmentObject(dataStore) .task { diff --git a/PadelClub/Views/Navigation/OnboardingView.swift b/PadelClub/Views/Navigation/OnboardingView.swift new file mode 100644 index 0000000..b167b9c --- /dev/null +++ b/PadelClub/Views/Navigation/OnboardingView.swift @@ -0,0 +1,235 @@ +import SwiftUI + +struct OnboardingView: View { + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @State private var selection = 0 + @Environment(\.openURL) var openURL + @Environment(\.dismiss) private var dismiss + @AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false + + var steps: [OnboardingStep] { + [ + // Écran 1 – Bienvenue + .single( + title: "Bienvenue sur Padel Club", + description: "L’outil idéal des juges-arbitres et organisateurs pour gérer leurs tournois de A à Z.", + image: .padelClubLogoFondclairTransparent, + imageSystem: nil, + buttonTitle: "Suivant", + action: { selection += 1 } + ), + + // Écran 2 – Juges arbitres + .single( + title: "Pour les Juges-Arbitres", + description: "Planification, convocations, tirages, résultats… Tout ce qu’il faut pour organiser un tournoi de padel.", + image: nil, + imageSystem: "calendar.badge.clock", + buttonTitle: "Suivant", + action: { selection += 1 } + ), + + // Écran 3 – Joueurs (Multi boutons) + .multi( + title: "Vous êtes joueur ?", + description: "Cette app a été pensée faite pour les organisateurs.\nPour suivre vos tournois et convocations, rendez-vous sur https://padelclub.app", + image: nil, + imageSystem: "person.fill.questionmark", + tools: [ + ("Aller sur le site joueur", { + if let url = URL(string: "https://padelclub.app") { + openURL(url) + } + }) + ], + finalButtonTitle: "Continuer", + finalAction: { + selection += 1 + } + ), + + // Écran 4 – Outils utiles aux joueurs + .multi( + title: "Quelques outils utiles", + description: "Même si pensée pour les organisateurs, vous trouverez aussi quelques fonctions pratiques en tant que joueur.", + image: nil, + imageSystem: "wrench.and.screwdriver", + tools: [ + ("Chercher un tournoi Ten'Up", { + dismiss() + navigation.agendaDestination = .around + }), + ("Calculateur de points", { + dismiss() + navigation.selectedTab = .toolbox + }), + ("Consulter les règles du jeu", { + dismiss() + navigation.selectedTab = .toolbox + }), + ("Créer vos animations amicales", { + dismiss() + navigation.agendaDestination = .activity + }) + ], + finalButtonTitle: "J'ai compris", + finalAction: { + UserDefaults.standard.set(true, forKey: "didSeeOnboarding") + dismiss() + } + ) + ] + } + + var body: some View { + NavigationStack { + TabView(selection: $selection) { + ForEach(Array(steps.enumerated()), id: \.offset) { index, step in + switch step { + case let .single(title, description, image, imageSystem, buttonTitle, action): + OnboardingPage( + title: title, + description: description, + image: image, + imageSystem: imageSystem, + buttonTitle: buttonTitle, + action: action + ) + .tag(index) + + case let .multi(title, description, image, imageSystem, tools, finalButtonTitle, finalAction): + OnboardingMultiButtonPage( + title: title, + description: description, + image: image, + imageSystem: imageSystem, + tools: tools, + finalButtonTitle: finalButtonTitle, + finalAction: finalAction + ) + .tag(index) + } + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) + .indexViewStyle(.page(backgroundDisplayMode: .always)) // <- ensures background + .tint(.black) // <- sets the indicator color + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + didSeeOnboarding = true + dismiss() + } label: { + Text("Plus tard") + } + } + } + } + .tint(.master) + } +} + +// MARK: - Enum de configuration +enum OnboardingStep { + case single(title: String, description: String, image: ImageResource?, imageSystem: String?, buttonTitle: String, action: () -> Void) + case multi(title: String, description: String, image: ImageResource?, imageSystem: String?, tools: [(String, () -> Void)], finalButtonTitle: String?, finalAction: () -> Void) +} + +// MARK: - Vue de base commune +struct OnboardingBasePage: View { + var title: String + var description: String + var image: ImageResource? + var imageSystem: String? + @ViewBuilder var content: () -> Content + + var body: some View { + VStack(spacing: 20) { + Spacer() + + if let imageSystem { + Image(systemName: imageSystem) + .resizable() + .scaledToFit() + .frame(width: 100, height: 100) + } else if let image { + Image(image) + .resizable() + .scaledToFit() + .frame(width: 100, height: 100) + } + + Text(title) + .font(.title) + .fontWeight(.bold) + .multilineTextAlignment(.center) + + Text(description) + .font(.body) + .multilineTextAlignment(.center) + .padding(.horizontal, 30) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + + Spacer() + + content() + + Spacer(minLength: 40) + } + } +} + +// MARK: - Page avec un bouton +struct OnboardingPage: View { + var title: String + var description: String + var image: ImageResource? + var imageSystem: String? + var buttonTitle: String + var action: () -> Void + + var body: some View { + OnboardingBasePage(title: title, description: description, image: image, imageSystem: imageSystem) { + RowButtonView(buttonTitle) { + action() + } + .padding() + } + } +} + +// MARK: - Page avec plusieurs boutons +struct OnboardingMultiButtonPage: View { + var title: String + var description: String + var image: ImageResource? + var imageSystem: String? + var tools: [(String, () -> Void)] + var finalButtonTitle: String? + var finalAction: () -> Void + + var body: some View { + OnboardingBasePage(title: title, description: description, image: image, imageSystem: imageSystem) { + VStack(spacing: 12) { + ForEach(Array(tools.enumerated()), id: \.offset) { _, tool in + FooterButtonView(tool.0) { + tool.1() + } + .tint(.master) + } + } + + if let finalButtonTitle = finalButtonTitle { + RowButtonView(finalButtonTitle) { + finalAction() + } + .padding() + } + } + } +} + +#Preview { + OnboardingView() +} diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 9a355aa..49d6be6 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -139,13 +139,15 @@ struct EditScoreView: View { Text(matchDescriptor.teamLabelTwo) } - Divider() - - Button { - self.matchDescriptor.match?.removeWalkOut() - save() - } label: { - Text("Annuler un forfait") + if self.matchDescriptor.match?.hasWalkoutTeam() == true { + Divider() + + Button { + self.matchDescriptor.match?.removeWalkOut() + save() + } label: { + Text("Annuler un forfait") + } } } label: { Text("Forfait d'une équipe ?") @@ -174,6 +176,13 @@ struct EditScoreView: View { } if matchDescriptor.hasEnded { + if self.matchDescriptor.match?.hasWalkoutTeam() == true { + RowButtonView("Annuler le forfait", role: .destructive) { + self.matchDescriptor.match?.removeWalkOut() + save() + } + } + Section { HStack { Spacer() diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 2b65381..93bb494 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -88,7 +88,7 @@ struct FollowUpMatchView: View { let allMatches = currentTournament?.allMatches() ?? [] self.matchesLeft = Tournament.matchesLeft(allMatches) let runningMatches = Tournament.runningMatches(allMatches) - let readyMatches = Tournament.readyMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches) self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.isFree = currentTournament?.isFree() ?? true } @@ -100,7 +100,7 @@ struct FollowUpMatchView: View { self.autoDismiss = autoDismiss self.matchesLeft = Tournament.matchesLeft(allMatches) let runningMatches = Tournament.runningMatches(allMatches) - let readyMatches = Tournament.readyMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches) self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.isFree = false } @@ -156,7 +156,7 @@ struct FollowUpMatchView: View { case .index: return matches case .restingTime: - return matches.sorted(by: \.restingTimeForSorting) + return readyMatches.sorted(by: \.restingTimeForSorting) case .court: return matchesLeft.filter({ $0.courtIndex == selectedCourt }) case .winner: diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 61228b8..97a9b0f 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -96,16 +96,27 @@ struct SelectablePlayerListView: View { var body: some View { VStack(spacing: 0) { if importObserver.isImportingFile() == false { - if searchViewModel.filterSelectionEnabled == false { - VStack { - HStack { - Picker(selection: $searchViewModel.filterOption) { - ForEach(PlayerFilterOption.allCases, id: \.self) { scope in - Text(scope.icon().capitalized) - } - } label: { + VStack { + HStack { + Picker(selection: $searchViewModel.filterOption) { + ForEach(PlayerFilterOption.allCases, id: \.self) { scope in + Text(scope.icon().capitalized) + } + } label: { + } + .pickerStyle(.segmented) + + Picker(selection: $searchViewModel.dataSet) { + ForEach(DataSet.allCases) { dataSet in + Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet) } - .pickerStyle(.segmented) + } label: { + + } + } + + if searchViewModel.isPresented == false { + HStack { Menu { if let lastDataSource = dataStore.appSettings.localizedLastDataSource() { Section { @@ -132,7 +143,7 @@ struct SelectablePlayerListView: View { } Divider() - Section { + Menu { Picker(selection: $searchViewModel.selectedAgeCategory) { ForEach(FederalTournamentAge.allCases) { ageCategory in Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory) @@ -141,11 +152,11 @@ struct SelectablePlayerListView: View { Text("Catégorie d'âge") } - } header: { + } label: { Text("Catégorie d'âge") } Divider() - + Section { Toggle(isOn: .init(get: { return searchViewModel.hideAssimilation == false @@ -165,23 +176,36 @@ struct SelectablePlayerListView: View { Text("Assimilés") } } label: { - VStack(alignment: .trailing) { - Label(searchViewModel.sortOption.localizedLabel(), systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down") - if searchViewModel.selectedAgeCategory != .unlisted { - Text(searchViewModel.selectedAgeCategory.localizedFederalAgeLabel()).font(.caption) - } + Text("tri par " + searchViewModel.sortTitle().lowercased()) + .underline() + .font(.caption) + // Label("Filtre", systemImage: "line.3.horizontal.decrease") + // .labelsHidden() + } + + if searchViewModel.selectedPlayers.count > 0 { + Divider() + + Button { + searchViewModel.filterSelectionEnabled.toggle() + } label: { + Text("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la sélection") + .underline() + .font(.caption) } } } + .fixedSize() } - .padding(.bottom) - .padding(.horizontal) - .background(Material.thick) - Divider() } + .padding(.bottom) + .padding(.horizontal) + .background(Material.thick) + Divider() + MySearchView(searchViewModel: searchViewModel, contentUnavailableAction: contentUnavailableAction) .environment(\.editMode, searchViewModel.allowMultipleSelection ? .constant(.active) : .constant(.inactive)) - .searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, isPresented: $searchViewModel.isPresented, placement: .navigationBarDrawer(displayMode: .always), prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in + .searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, isPresented: $searchViewModel.isPresented, placement: .toolbar, prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in Text(token.shortLocalizedLabel) }) .keyboardType(.alphabet) @@ -212,11 +236,10 @@ struct SelectablePlayerListView: View { } .scrollDismissesKeyboard(.immediately) .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) - .toolbarBackground(searchViewModel.allowMultipleSelection ? .visible : .hidden, for: .bottomBar) + .toolbarBackground(.hidden, for: .bottomBar) .toolbarBackground(.visible, for: .navigationBar) // .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) - .navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet)) .navigationBarTitleDisplayMode(.inline) } else { List { @@ -284,7 +307,7 @@ struct SelectablePlayerListView: View { searchViewModel.selectedPlayers.removeAll() dismiss() } label: { - Text("Annuler") + Label("Annuler", systemImage: "xmark") } } @@ -297,28 +320,16 @@ struct SelectablePlayerListView: View { } .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() - } - } - } + } + + if #available(iOS 26.0, *) { + DefaultToolbarItem(kind: .search, placement: .bottomBar) } } + .navigationTitle("Recherche") + .navigationBarTitleDisplayMode(.large) // .modifierWithCondition(searchViewModel.user != nil) { thisView in // thisView - .toolbarTitleMenu { - Picker(selection: $searchViewModel.dataSet) { - ForEach(DataSet.allCases) { dataSet in - Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet) - } - } label: { - - } - } // } // .bottomBarAlternative(hide: searchViewModel.selectedPlayers.isEmpty) { // ZStack { diff --git a/PadelClub/Views/Shared/TournamentFilterView.swift b/PadelClub/Views/Shared/TournamentFilterView.swift index 3367e0f..cfdb29e 100644 --- a/PadelClub/Views/Shared/TournamentFilterView.swift +++ b/PadelClub/Views/Shared/TournamentFilterView.swift @@ -47,6 +47,8 @@ struct TournamentFilterView: View { } label: { Text("En semaine ou week-end") } + + WeekdayselectionView(weekdays: $federalDataViewModel.weekdays) } Section { diff --git a/PadelClub/Views/Team/TeamRestingView.swift b/PadelClub/Views/Team/TeamRestingView.swift index 765052b..9471e2a 100644 --- a/PadelClub/Views/Team/TeamRestingView.swift +++ b/PadelClub/Views/Team/TeamRestingView.swift @@ -90,7 +90,7 @@ struct TeamRestingView: View { let allMatches = tournament.allMatches() let matchesLeft = Tournament.matchesLeft(allMatches) let runningMatches = Tournament.runningMatches(allMatches) - let readyMatches = Tournament.readyMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches) self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.matchesLeft = matchesLeft self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting) diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 89b091d..04a5981 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -336,7 +336,7 @@ struct InscriptionManagerView: View { .tint(.master) } .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { + ToolbarItem(placement: .navigationBarTrailing) { Menu { Toggle(isOn: $compactMode) { Text("Vue compact") @@ -364,6 +364,14 @@ struct InscriptionManagerView: View { LabelFilter() .symbolVariant(filterMode == .all ? .none : .fill) } + } + + + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .navigationBarTrailing) + } + + ToolbarItem(placement: .navigationBarTrailing) { Menu { if tournament.inscriptionClosed() == false { Menu { diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift index 2cc6c92..b5fddb2 100644 --- a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -201,7 +201,7 @@ struct PrintSettingsView: View { Text("Partager le code source HTML") } } label: { - Label("Options", systemImage: "ellipsis.circle") + LabelOptions() } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index cfd8944..bb72722 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -42,7 +42,7 @@ struct TournamentRankView: View { Section { let all = tournament.allMatches() let runningMatches = Tournament.runningMatches(all) - let matchesLeft = Tournament.readyMatches(all) + let matchesLeft = Tournament.readyMatches(all, runningMatches: runningMatches) MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false) diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index fd6e575..b991db7 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -22,7 +22,7 @@ struct TournamentRunningView: View { let runningMatches = Tournament.runningMatches(allMatches) let matchesLeft = Tournament.matchesLeft(allMatches) - let readyMatches = Tournament.readyMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches) let availableToStart = Tournament.availableToStart(allMatches, in: runningMatches, checkCanPlay: true) Section { diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 457f9d2..49ab26f 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -238,10 +238,10 @@ struct TournamentView: View { } NavigationLink(value: Screen.event) { - Text("Réglages de l'événement") + Label("Événement", systemImage: "wrench.and.screwdriver") } NavigationLink(value: Screen.settings) { - LabelSettings() + Label("Tournoi", systemImage: "wrench.and.screwdriver") } NavigationLink(value: Screen.call) { @@ -290,10 +290,10 @@ struct TournamentView: View { } } - NavigationLink(value: Screen.broadcast) { - Label("Publication", systemImage: "airplayvideo") - } - +// NavigationLink(value: Screen.broadcast) { +// Label("Publication", systemImage: "airplayvideo") +// } +// NavigationLink(value: Screen.print) { Label("Imprimer", systemImage: "printer") } @@ -305,8 +305,7 @@ struct TournamentView: View { Divider() NavigationLink(value: Screen.stateSettings) { - Text("Gestion du tournoi") - Text("Annuler, supprimer ou terminer le tournoi") + Label("Tournoi", systemImage: "trash") } } label: { LabelOptions() From 3af1de6ff9759bb15c35bde6b59c38dc44b82dbe Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 25 Sep 2025 18:17:44 +0200 Subject: [PATCH 15/64] fix issue with ios 26 --- PadelClub.xcodeproj/project.pbxproj | 16 ++- .../Data/Federal/FederalTournament.swift | 104 +++++++++++++--- PadelClub/PadelClubApp.swift | 3 +- .../Utils/Network/FederalDataService.swift | 74 +---------- .../Utils/Network/NetworkFederalService.swift | 2 +- .../ViewModel/FederalDataViewModel.swift | 1 + .../Navigation/Agenda/ActivityView.swift | 117 ++++++++++++------ .../Navigation/Agenda/CalendarView.swift | 11 +- .../Agenda/TournamentLookUpView.swift | 75 +++++++---- .../Agenda/TournamentSubscriptionView.swift | 94 +++++++++----- PadelClub/Views/Navigation/MainView.swift | 99 ++++++++++++++- .../Views/Navigation/OnboardingView.swift | 4 + .../Navigation/Toolbox/ToolboxView.swift | 1 + .../Shared/TournamentCellView.swift | 14 ++- 14 files changed, 420 insertions(+), 195 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 57ea32a..e40ee47 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; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3155,7 +3156,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3185,6 +3186,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3201,7 +3203,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3304,6 +3306,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; @@ -3320,7 +3323,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3349,6 +3352,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + ENABLE_DEBUG_DYLIB = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; @@ -3365,7 +3369,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3409,7 +3413,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3451,7 +3455,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index cb214d4..9a3c99b 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -10,7 +10,7 @@ import PadelClubData // MARK: - FederalTournament -struct FederalTournament: Identifiable, Codable { +struct FederalTournament: Identifiable, Codable, Hashable { func getEvent() -> Event { let club = DataStore.shared.user.clubsObjects().first(where: { $0.code == codeClub }) @@ -313,7 +313,7 @@ extension FederalTournament: FederalTournamentHolder { } // MARK: - CategorieAge -struct CategorieAge: Codable { +struct CategorieAge: Codable, Hashable { var ageJoueurMin, ageMin, ageJoueurMax, ageRechercheMax: Int? var categoriesAgeTypePratique: [CategoriesAgeTypePratique]? var ageMax: Int? @@ -335,18 +335,18 @@ struct CategorieAge: Codable { } // MARK: - CategoriesAgeTypePratique -struct CategoriesAgeTypePratique: Codable { +struct CategoriesAgeTypePratique: Codable, Hashable { var id: ID? } // MARK: - ID -struct ID: Codable { +struct ID: Codable, Hashable { var typePratique: String? var idCategorieAge: Int? } // MARK: - CategorieTournoi -struct CategorieTournoi: Codable { +struct CategorieTournoi: Codable, Hashable { var code, codeTaxe: String? var compteurGda: CompteurGda? var libelle, niveauHierarchique: String? @@ -354,14 +354,14 @@ struct CategorieTournoi: Codable { } // MARK: - CompteurGda -struct CompteurGda: Codable { +struct CompteurGda: Codable, Hashable { var classementMax: Classement? var libelle: String? var classementMin: Classement? } // MARK: - Classement -struct Classement: Codable { +struct Classement: Codable, Hashable { var nature, libelle: String? var serie: Serie? var sexe: String? @@ -371,7 +371,7 @@ struct Classement: Codable { } // MARK: - Serie -struct Serie: Codable { +struct Serie: Codable, Hashable { var code, libelle: String? var valide: Bool? var sexe: String? @@ -382,7 +382,7 @@ struct Serie: Codable { } // MARK: - Epreuve -struct Epreuve: Codable { +struct Epreuve: Codable, Hashable { var inscriptionEnLigneEnCours: Bool? var categorieAge: CategorieAge? var typeEpreuve: TypeEpreuve? @@ -419,7 +419,7 @@ struct Epreuve: Codable { } // MARK: - TypeEpreuve -struct TypeEpreuve: Codable { +struct TypeEpreuve: Codable, Hashable { let code: String? let delai: Int? let libelle: String? @@ -437,12 +437,12 @@ struct TypeEpreuve: Codable { } // MARK: - BorneAnneesNaissance -struct BorneAnneesNaissance: Codable { +struct BorneAnneesNaissance: Codable, Hashable { var min, max: Int? } // MARK: - Installation -struct Installation: Codable { +struct Installation: Codable, Hashable { var ville: String? var lng: Double? var surfaces: [JSONAny]? @@ -457,7 +457,7 @@ struct Installation: Codable { } // MARK: - JugeArbitre -struct JugeArbitre: Codable { +struct JugeArbitre: Codable, Hashable { var idCRM, id: Int? var nom, prenom: String? @@ -468,7 +468,7 @@ struct JugeArbitre: Codable { } // MARK: - ModeleDeBalle -struct ModeleDeBalle: Codable { +struct ModeleDeBalle: Codable, Hashable { var libelle: String? var marqueDeBalle: MarqueDeBalle? var id: Int? @@ -476,7 +476,7 @@ struct ModeleDeBalle: Codable { } // MARK: - MarqueDeBalle -struct MarqueDeBalle: Codable { +struct MarqueDeBalle: Codable, Hashable { var id: Int? var valide: Bool? var marque: String? @@ -529,9 +529,13 @@ class JSONCodingKey: CodingKey { } } -class JSONAny: Codable { +class JSONAny: Codable, Hashable, Equatable { - let value: Any + var value: Any + + init() { + self.value = () + } static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") @@ -722,4 +726,70 @@ class JSONAny: Codable { try JSONAny.encode(to: &container, value: self.value) } } + + public static func == (lhs: JSONAny, rhs: JSONAny) -> Bool { + switch (lhs.value, rhs.value) { + case (let l as Bool, let r as Bool): return l == r + case (let l as Int64, let r as Int64): return l == r + case (let l as Double, let r as Double): return l == r + case (let l as String, let r as String): return l == r + case (let l as JSONNull, let r as JSONNull): return true + case (let l as [Any], let r as [Any]): + guard l.count == r.count else { return false } + return zip(l, r).allSatisfy { (a, b) in + // Recursively wrap in JSONAny for comparison + JSONAny(value: a) == JSONAny(value: b) + } + case (let l as [String: Any], let r as [String: Any]): + guard l.count == r.count else { return false } + for (key, lVal) in l { + guard let rVal = r[key], JSONAny(value: lVal) == JSONAny(value: rVal) else { return false } + } + return true + default: + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch value { + case let v as Bool: + hasher.combine(0) + hasher.combine(v) + case let v as Int64: + hasher.combine(1) + hasher.combine(v) + case let v as Double: + hasher.combine(2) + hasher.combine(v) + case let v as String: + hasher.combine(3) + hasher.combine(v) + case is JSONNull: + hasher.combine(4) + case let v as [Any]: + hasher.combine(5) + for elem in v { + JSONAny(value: elem).hash(into: &hasher) + } + case let v as [String: Any]: + hasher.combine(6) + // Order of hashing dictionary keys shouldn't matter + for key in v.keys.sorted() { + hasher.combine(key) + if let val = v[key] { + JSONAny(value: val).hash(into: &hasher) + } + } + default: + hasher.combine(-1) + } + } + + // Helper init for internal use + convenience init(value: Any) { + self.init() + self.value = value + } } + diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 5ec6606..182c68d 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -248,7 +248,8 @@ struct DownloadNewVersionView: View { }.padding().background(.logoYellow) .clipShape(.buttonBorder) - }.frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity) .foregroundStyle(.logoBackground) .fontWeight(.medium) .multilineTextAlignment(.center) diff --git a/PadelClub/Utils/Network/FederalDataService.swift b/PadelClub/Utils/Network/FederalDataService.swift index 9cf9bf0..97442eb 100644 --- a/PadelClub/Utils/Network/FederalDataService.swift +++ b/PadelClub/Utils/Network/FederalDataService.swift @@ -240,7 +240,8 @@ class FederalDataService { let queryString = urlComponents.query ?? "" // The servicePath now points to your backend's endpoint for all tournaments: 'fft/all-tournaments/' - let urlRequest = try service._baseRequest(servicePath: "fft/all-tournaments?\(queryString)", method: .get, requiresToken: true) + var urlRequest = try service._baseRequest(servicePath: "fft/all-tournaments?\(queryString)", method: .get, requiresToken: true) + urlRequest.timeoutInterval = 180 let (data, response) = try await URLSession.shared.data(for: urlRequest) @@ -275,7 +276,8 @@ class FederalDataService { // The servicePath now points to your backend's endpoint for umpire data: 'fft/umpire/{tournament_id}/' let servicePath = "fft/umpire/\(idTournament)/" - let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false) + var urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false) + urlRequest.timeoutInterval = 120.0 let (data, response) = try await URLSession.shared.data(for: urlRequest) @@ -297,72 +299,4 @@ class FederalDataService { throw NetworkManagerError.apiError("Failed to decode UmpireContactInfo: \(error.localizedDescription)") } } - - - /// Fetches umpire contact data for multiple tournament IDs. - /// This function calls your backend endpoint that handles multiple tournament IDs via query parameters. - /// - Parameter tournamentIds: An array of tournament ID strings. - /// - Returns: A dictionary mapping tournament IDs to tuples `(name: String?, email: String?, phone: String?)` containing the umpire's contact info. - /// - Throws: An error if the network request fails or decoding the response is unsuccessful. - func getUmpiresData(tournamentIds: [String]) async throws -> [String: (name: String?, email: String?, phone: String?)] { - let service = try StoreCenter.main.service() - - // Validate input - guard !tournamentIds.isEmpty else { - throw NetworkManagerError.apiError("Tournament IDs array cannot be empty") - } - - // Create the base service path - let basePath = "fft/umpires/" - - // Build query parameters - join tournament IDs with commas - let tournamentIdsParam = tournamentIds.joined(separator: ",") - let queryItems = [URLQueryItem(name: "tournament_ids", value: tournamentIdsParam)] - - // Create the URL with query parameters - var urlComponents = URLComponents() - urlComponents.queryItems = queryItems - - let servicePath = basePath + (urlComponents.url?.query.map { "?\($0)" } ?? "") - - let urlRequest = try service._baseRequest(servicePath: servicePath, method: .get, requiresToken: false) - - let (data, response) = try await URLSession.shared.data(for: urlRequest) - - guard let httpResponse = response as? HTTPURLResponse else { - throw URLError(.badServerResponse) - } - - guard !data.isEmpty else { - throw NetworkManagerError.noDataReceived - } - - // Check for HTTP errors - guard httpResponse.statusCode == 200 else { - if let errorData = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let message = errorData["message"] as? String { - throw NetworkManagerError.apiError("Server error: \(message)") - } - throw NetworkManagerError.apiError("HTTP error: \(httpResponse.statusCode)") - } - - do { - let umpireResponse = try JSONDecoder().decode(UmpireDataResponse.self, from: data) - - // Convert the results to the expected return format - var resultDict: [String: (name: String?, email: String?, phone: String?)] = [:] - - for (tournamentId, umpireInfo) in umpireResponse.results { - resultDict[tournamentId] = (name: umpireInfo.name, email: umpireInfo.email, phone: umpireInfo.phone) - } - - print("Umpire data fetched for \(resultDict.count) tournaments") - return resultDict - - } catch { - print("Decoding error for UmpireDataResponse: \(error)") - throw NetworkManagerError.apiError("Failed to decode UmpireDataResponse: \(error.localizedDescription)") - } - } - } diff --git a/PadelClub/Utils/Network/NetworkFederalService.swift b/PadelClub/Utils/Network/NetworkFederalService.swift index 2d09be2..7f76fc5 100644 --- a/PadelClub/Utils/Network/NetworkFederalService.swift +++ b/PadelClub/Utils/Network/NetworkFederalService.swift @@ -93,7 +93,7 @@ class NetworkFederalService { //"geocoding%5Bcountry%5D=fr&geocoding%5Bville%5D=13%20Avenue%20Emile%20Bodin%2013260%20Cassis&geocoding%5Brayon%5D=15&geocoding%5BuserPosition%5D%5Blat%5D=43.22278594081477&geocoding%5BuserPosition%5D%5Blng%5D=5.556953900769194&geocoding%5BuserPosition%5D%5BshowDistance%5D=true&nombreResultat=0&diplomeEtatOption=false&galaxieOption=false&fauteuilOption=false&tennisSanteOption=false" let postData = parameters.data(using: .utf8) - var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/clubs/ajax")!,timeoutInterval: Double.infinity) + var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/clubs/ajax")!) request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language") request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding") diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 4297c96..30d5f74 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -70,6 +70,7 @@ class FederalDataViewModel { selectedClubs.removeAll() dayPeriod = .all dayDuration = nil + weekdays.removeAll() id = UUID() } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 25573f3..8b444ae 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -224,6 +224,12 @@ struct ActivityView: View { .navigationDestination(for: Tournament.self) { tournament in TournamentView(tournament: tournament) } + .navigationDestination(for: SubScreen.self) { build in + switch build { + case .subscription(let federalTournament, let build): + TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) + } + } // .onDisappear(perform: { // pasteButtonIsDisplayed = nil // print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed) @@ -231,15 +237,26 @@ struct ActivityView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { if #available(iOS 26.0, *) { - Button("Vue calendrier", systemImage: "calendar") { - switch viewStyle { - case .list: - viewStyle = .calendar - case .calendar: - viewStyle = .list + if viewStyle == .calendar { + Button("Vue calendrier", systemImage: "calendar") { + switch viewStyle { + case .list: + viewStyle = .calendar + case .calendar: + viewStyle = .list + } + } + .buttonStyle(.borderedProminent) + } else { + Button("Vue calendrier", systemImage: "calendar") { + switch viewStyle { + case .list: + viewStyle = .calendar + case .calendar: + viewStyle = .list + } } } - .symbolVariant(viewStyle == .calendar ? .fill : .none) } else { Button { switch viewStyle { @@ -265,11 +282,16 @@ struct ActivityView: View { ToolbarItem(placement: .topBarLeading) { if #available(iOS 26.0, *) { - - Button("Filtre", systemImage: "line.3.horizontal.decrease") { - presentFilterView.toggle() + if federalDataViewModel.areFiltersEnabled() { + Button("Filtre", systemImage: "line.3.horizontal.decrease") { + presentFilterView.toggle() + } + .buttonStyle(.borderedProminent) + } else { + Button("Filtre", systemImage: "line.3.horizontal.decrease") { + presentFilterView.toggle() + } } - .symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) } else { Button { presentFilterView.toggle() @@ -308,35 +330,10 @@ struct ActivityView: View { } } - if tournaments.isEmpty == false, federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around { - ToolbarItemGroup(placement: .bottomBar) { - VStack(spacing: 0) { - let searchStatus = _searchStatus() - if searchStatus.isEmpty == false { - Text(_searchStatus()) - .font(.footnote) - .foregroundStyle(.secondary) - } - - HStack { - if navigation.agendaDestination == .around { - FooterButtonView("modifier votre recherche") { - displaySearchView = true - } - - if federalDataViewModel.areFiltersEnabled() { - Text("ou") - } - } - - if federalDataViewModel.areFiltersEnabled() { - FooterButtonView(_filterButtonTitle()) { - presentFilterView = true - } - - } - } - .padding(.bottom, 8) + if #unavailable(iOS 26.0) { + if _shouldDisplaySearchStatus() { + ToolbarItemGroup(placement: .bottomBar) { + _searchBoxView() } } } @@ -446,6 +443,41 @@ struct ActivityView: View { } } + private func _shouldDisplaySearchStatus() -> Bool { + tournaments.isEmpty == false && (federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around) + } + + private func _searchBoxView() -> some View { + VStack(spacing: 0) { + let searchStatus = _searchStatus() + if searchStatus.isEmpty == false { + Text(_searchStatus()) + .font(.footnote) + .foregroundStyle(.secondary) + } + + HStack { + if navigation.agendaDestination == .around { + FooterButtonView("modifier votre recherche") { + displaySearchView = true + } + + if federalDataViewModel.areFiltersEnabled() { + Text("ou") + } + } + + if federalDataViewModel.areFiltersEnabled() { + FooterButtonView(_filterButtonTitle()) { + presentFilterView = true + } + + } + } + .padding(.bottom, 8) + } + } + private func _searchStatus() -> String { var searchStatus : [String] = [] if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { @@ -623,3 +655,8 @@ struct ActivityView: View { //#Preview { // ActivityView() //} + +enum SubScreen: Hashable { + case subscription(FederalTournament, TournamentBuild) +} + diff --git a/PadelClub/Views/Navigation/Agenda/CalendarView.swift b/PadelClub/Views/Navigation/Agenda/CalendarView.swift index a3a7d81..cf96a94 100644 --- a/PadelClub/Views/Navigation/Agenda/CalendarView.swift +++ b/PadelClub/Views/Navigation/Agenda/CalendarView.swift @@ -95,10 +95,15 @@ struct CalendarView: View { if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) { if navigation.agendaDestination == .around { - NavigationLink(build.buildHolderTitle(.wide)) { - TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user) + + if #available(iOS 26.0, *) { + NavigationLink(build.buildHolderTitle(.wide), value: SubScreen.subscription(tournament, build as! TournamentBuild)) + } else { + NavigationLink(build.buildHolderTitle(.wide)) { + TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user) + } } - } else { + } else { Button(build.buildHolderTitle(.wide)) { _createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build) } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 102f900..df86047 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -173,8 +173,7 @@ struct TournamentLookUpView: View { Spacer() ProgressView() if total > 0 { - let percent = Double(tournaments.count) / Double(total) - Text(percent.formatted(.percent.precision(.significantDigits(1...3))) + " en récupération de Tenup") + Text("\(total) tournois en cours de récupération") .font(.caption) } Spacer() @@ -211,6 +210,7 @@ struct TournamentLookUpView: View { revealSearchParameters = true federalDataViewModel.searchedFederalTournaments = [] federalDataViewModel.searchAttemptCount = 0 + federalDataViewModel.removeFilters() } label: { Text("Ré-initialiser la recherche") } @@ -239,26 +239,46 @@ struct TournamentLookUpView: View { private func _gatherNumbers() { Task { print("Doing.....") + let tournamentsToFetch = tournaments.enumerated().filter { (idx, tournament) in + tournament.japPhoneNumber == nil || tournament.japPhoneNumber?.isEmpty == true + } + let idIndexPairs: [(Int, String)] = tournamentsToFetch.map { ($0.offset, $0.element.id) } + let tournamentIDs: [String] = idIndexPairs.map { $0.1 } + guard !tournamentIDs.isEmpty else { + print("All numbers already gathered.") + return + } - await withTaskGroup(of: (Int, String?).self) { group in - for i in 0.. count / 50 && page < total / 30 { if total < 200 || requestedToGetAllPages { page += 1 await getNewPage() @@ -395,8 +414,18 @@ struct TournamentLookUpView: View { appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth.nextDay.endOfMonth } } - DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date) - DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date) + DatePicker(selection: $appSettings.startDate, displayedComponents: .date) { + Text("Début") + .onTapGesture(count: 2) { + appSettings.startDate = appSettings.startDate.startOfCurrentMonth + } + } + DatePicker(selection: $appSettings.endDate, displayedComponents: .date) { + Text("Fin") + .onTapGesture(count: 2) { + appSettings.endDate = appSettings.endDate.nextDay.endOfMonth + } + } Picker(selection: $appSettings.dayDuration) { Text("Aucune").tag(nil as Int?) Text(1.formatted()).tag(1 as Int?) @@ -406,7 +435,8 @@ struct TournamentLookUpView: View { Text("Durée souhaitée (en jours)") } - WeekdayselectionView(weekdays: $appSettings.weekdays) + @Bindable var federalDataViewModel = federalDataViewModel + WeekdayselectionView(weekdays: $federalDataViewModel.weekdays) Picker(selection: $appSettings.dayPeriod) { ForEach(DayPeriod.allCases) { @@ -449,7 +479,6 @@ struct TournamentLookUpView: View { } .symbolVariant(.fill) .foregroundColor (Color.white) - .cornerRadius (20) .font(.system(size: 12)) } } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index d57b02a..1c27a96 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -22,6 +22,7 @@ struct TournamentSubscriptionView: View { @State private var didSendMessage: Bool = false @State private var didSaveInCalendar: Bool = false @State private var phoneNumber: String? = nil + @State private var errorWhenGatheringPhone: Bool = false init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: CustomUser) { self.federalTournament = federalTournament @@ -111,9 +112,13 @@ struct TournamentSubscriptionView: View { Text(federalTournament.phoneLabel()) } - if let phoneNumber { - LabeledContent("Téléphone JAP") { + LabeledContent("Téléphone JAP") { + if let phoneNumber { Text(phoneNumber) + } else if errorWhenGatheringPhone == false { + ProgressView() + } else { + Image(systemName: "exclamationmark.triangle") } } } header: { @@ -163,8 +168,15 @@ struct TournamentSubscriptionView: View { CopyPasteButtonView(pasteValue: messageBody) } } + .ifAvailableiOS26 { view in + view.toolbar(.hidden, for: .tabBar) + } .task { - self.phoneNumber = try? await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id).phone + do { + self.phoneNumber = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id).phone + } catch { + self.errorWhenGatheringPhone = true + } } .toolbarBackground(.visible, for: .bottomBar) .toolbarBackground(.visible, for: .navigationBar) @@ -176,51 +188,61 @@ struct TournamentSubscriptionView: View { } } .toolbar(content: { - ToolbarItem(placement: .status) { + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .bottomBar) + } + ToolbarItem(placement: .bottomBar) { Menu { - if let courrielEngagement = federalTournament.courrielEngagement { - Section { - RowButtonView("S'inscrire par email", systemImage: "envelope") { + Menu { + if let courrielEngagement = federalTournament.courrielEngagement { + Button("Email", systemImage: "envelope") { contactType = .mail(date: nil, recipients: [courrielEngagement], bccRecipients: nil, body: messageBody, subject: messageSubject, tournamentBuild: build as? TournamentBuild) } } - } - - if let telephone = phoneNumber { - if telephone.isMobileNumber() { - Section { - RowButtonView("S'inscrire par message", systemImage: "message") { + + if let telephone = phoneNumber { + if telephone.isMobileNumber() { + Button("Message", systemImage: "message") { contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild) } } - } - let number = telephone.replacingOccurrences(of: " ", with: "") - if let url = URL(string: "tel:\(number)") { - Link(destination: url) { - Label("Appeler le JAP", systemImage: "phone") + let number = telephone.replacingOccurrences(of: " ", with: "") + if let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler le JAP", systemImage: "phone") + } } } + } label: { + Label("Inscription", systemImage: "pencil.and.list.clipboard") } - if let installation = federalTournament.installation, let telephone = installation.telephone { - Section { - RowButtonView("Contacter le club", systemImage: "house.and.flag") { + Menu { + if let installation = federalTournament.installation, let telephone = installation.telephone { + Button("Email", systemImage: "envelope") { contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild) } - } - let number = telephone.replacingOccurrences(of: " ", with: "") - if let url = URL(string: "tel:\(number)") { - Link(destination: url) { - Label("Appeler le club", systemImage: "phone") + let number = telephone.replacingOccurrences(of: " ", with: "") + if let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler", systemImage: "phone") + } } } + } label: { + Label("Contacter le club", systemImage: "house.and.flag") } - + } label: { - Text("Contact et inscription") + Text("S'inscrire") + .foregroundStyle(.white) + .frame(maxWidth: .infinity) } .menuStyle(.button) .buttonStyle(.borderedProminent) - .offset(y:-2) + } + + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .bottomBar) } ToolbarItem(placement: .topBarTrailing) { @@ -361,3 +383,17 @@ struct TournamentSubscriptionView: View { } } + +extension View { + /// Runs a transform only on iOS 26+, otherwise returns self + @ViewBuilder + func ifAvailableiOS26( + @ViewBuilder transform: (Self) -> Content + ) -> some View { + if #available(iOS 26.0, *) { + transform(self) + } else { + self + } + } +} diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 209a81f..003a22a 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -17,11 +17,14 @@ struct MainView: View { @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(ImportObserver.self) private var importObserver: ImportObserver + @State private var federalDataViewModel: FederalDataViewModel = FederalDataViewModel.shared @State private var mainViewId: UUID = UUID() @State private var presentOnboarding: Bool = false @State private var canPresentOnboarding: Bool = false - + @State private var presentFilterView: Bool = false + @State private var displaySearchView: Bool = false + @AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false var lastDataSource: String? { @@ -94,6 +97,23 @@ struct MainView: View { // PadelClubView() // .tabItem(for: .padelClub) } + .applyTabViewBottomAccessory(content: { + if (navigation.selectedTab == .activity || navigation.selectedTab == nil) && _shouldDisplaySearchStatus() { + _searchBoxView() + } + }) + .sheet(isPresented: $presentFilterView) { + TournamentFilterView(federalDataViewModel: federalDataViewModel) + .environment(navigation) + .tint(.master) + } + .sheet(isPresented: $displaySearchView) { + NavigationStack { + TournamentLookUpView() + .environment(federalDataViewModel) + .environment(navigation) + } + } .onAppear { if canPresentOnboarding || StoreCenter.main.userId != nil { if didSeeOnboarding == false { @@ -264,8 +284,85 @@ struct MainView: View { } } } + + private func _searchStatus() -> String { + var searchStatus : [String] = [] + if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { + let filteredSearchedFederalTournaments = federalDataViewModel.filteredSearchedFederalTournaments + + let status : String = filteredSearchedFederalTournaments.count.formatted() + " tournoi" + filteredSearchedFederalTournaments.count.pluralSuffix + searchStatus.append(status) + } + + if federalDataViewModel.areFiltersEnabled() { + searchStatus.append(federalDataViewModel.filterStatus()) + } + + return searchStatus.joined(separator: " ") + } + + + private func _shouldDisplaySearchStatus() -> Bool { + guard navigation.path.count == 0 else { return false } + return federalDataViewModel.areFiltersEnabled() || (navigation.agendaDestination == .around && federalDataViewModel.searchedFederalTournaments.isEmpty == false) + } + + private func _searchBoxView() -> some View { + VStack(spacing: 0) { + let searchStatus = _searchStatus() + if searchStatus.isEmpty == false { + Text(_searchStatus()) + .font(.footnote) + .foregroundStyle(.secondary) + } + + HStack { + if navigation.agendaDestination == .around { + FooterButtonView("modifier votre recherche") { + displaySearchView = true + } + + if federalDataViewModel.areFiltersEnabled() { + Text("ou") + } + } + + if federalDataViewModel.areFiltersEnabled() { + FooterButtonView(_filterButtonTitle()) { + presentFilterView = true + } + + } + } + } + } + + private func _filterButtonTitle() -> String { + var prefix = "modifier " + if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false { + prefix = "" + } + return prefix + "vos filtres" + } + + } //#Preview { // MainView() //} + +fileprivate extension View { + @ViewBuilder + func applyTabViewBottomAccessory( + @ViewBuilder content: () -> Content + ) -> some View { + if #available(iOS 26.0, *) { + self.tabViewBottomAccessory { + content() + } + } else { + self + } + } +} diff --git a/PadelClub/Views/Navigation/OnboardingView.swift b/PadelClub/Views/Navigation/OnboardingView.swift index b167b9c..d6d7869 100644 --- a/PadelClub/Views/Navigation/OnboardingView.swift +++ b/PadelClub/Views/Navigation/OnboardingView.swift @@ -59,6 +59,10 @@ struct OnboardingView: View { dismiss() navigation.agendaDestination = .around }), + ("Accès au classement mensuel", { + dismiss() + navigation.selectedTab = .toolbox + }), ("Calculateur de points", { dismiss() navigation.selectedTab = .toolbox diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index c867526..df94d1c 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -65,6 +65,7 @@ struct ToolboxView: View { Section { NavigationLink { SelectablePlayerListView(isPresented: false, lastDataSource: true) + .toolbar(.hidden, for: .tabBar) } label: { Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index fe260cc..ae86dda 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -28,10 +28,16 @@ struct TournamentCellView: View { if let federalTournament = tournament as? FederalTournament { if FederalDataViewModel.shared.isFederalTournamentValidForFilters(federalTournament, build: build) { if navigation.agendaDestination == .around { - NavigationLink { - TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) - } label: { - _buildView(build, existingTournament: event?.existingBuild(build)) + if #available(iOS 26.0, *) { + NavigationLink(value: SubScreen.subscription(federalTournament, build as! TournamentBuild)) { + _buildView(build, existingTournament: event?.existingBuild(build)) + } + } else { + NavigationLink { + TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) + } label: { + _buildView(build, existingTournament: event?.existingBuild(build)) + } } } else { _buildView(build, existingTournament: event?.existingBuild(build)) From 3305c9aaf432a4e489232beb2317cbcb2c4f17a3 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 25 Sep 2025 18:18:18 +0200 Subject: [PATCH 16/64] v1.2.52 --- 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 e40ee47..6c74e32 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3161,7 +3161,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.51; + MARKETING_VERSION = 1.2.52; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3208,7 +3208,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.51; + MARKETING_VERSION = 1.2.52; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From dd91369c6b15e16fcef1bbd4a7bfce0912a08040 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 26 Sep 2025 12:49:15 +0200 Subject: [PATCH 17/64] animation fix in debug --- PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index df86047..9d4b9a5 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -237,6 +237,7 @@ struct TournamentLookUpView: View { } private func _gatherNumbers() { + searching = true Task { print("Doing.....") let tournamentsToFetch = tournaments.enumerated().filter { (idx, tournament) in @@ -279,6 +280,7 @@ struct TournamentLookUpView: View { print("Completed batch \(batchIndex + 1) of \(batches.count)") } + searching = false print(".....Done") } } From 7259777ba58ebbdbe64633414bd2f41752425a2b Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 26 Sep 2025 13:15:57 +0200 Subject: [PATCH 18/64] fix tournament menu --- .../Views/Tournament/TournamentView.swift | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 9e36c51..b4621d7 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -234,20 +234,36 @@ struct TournamentView: View { Button { navigation.openTournamentInOrganizer(tournament) } label: { - Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + Label("Gestionnaire", systemImage: "pin") } Divider() } - NavigationLink(value: Screen.event) { - Label("Événement", systemImage: "wrench.and.screwdriver") - } - NavigationLink(value: Screen.settings) { - Label("Tournoi", systemImage: "wrench.and.screwdriver") + Menu { + NavigationLink(value: Screen.event) { + Text("Événement") + } + NavigationLink(value: Screen.settings) { + Text("Tournoi") + } + + NavigationLink(value: Screen.structure) { + LabelStructure() + } + + Divider() + + NavigationLink(value: Screen.stateSettings) { + Label("Gestion", systemImage: "trash") + } + + } label: { + Label("Réglages", systemImage: "wrench.and.screwdriver") + } NavigationLink(value: Screen.call) { - Text("Convocations") + Label("Convocations", systemImage: "calendar.badge.clock") } if tournament.tournamentLevel.haveDeadlines() { @@ -258,20 +274,16 @@ struct TournamentView: View { .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Rappel des délais") } label: { - Text("Rappel des délais") + Label("Rappel des délais", systemImage: "calendarcalendar.badge.exclamationmark") } } - - NavigationLink(value: Screen.structure) { - LabelStructure() - } - + NavigationLink(value: Screen.cashier) { - Text(tournament.isFree() ? "Présence" : "Encaissement") + Label(tournament.isFree() ? "Présence" : "Encaissement", systemImage: tournament.isFree() ? "person.crop.circle.badge.checkmark" : "eurosign.circle") } NavigationLink(value: Screen.statistics) { - Text("Statistiques") + Label("Statistiques", systemImage: "123.rectangle") } @@ -304,11 +316,6 @@ struct TournamentView: View { // Label("Partager", systemImage: "square.and.arrow.up") // } - Divider() - - NavigationLink(value: Screen.stateSettings) { - Label("Tournoi", systemImage: "trash") - } } label: { LabelOptions() .popoverTip(tournamentRunningTip) From 64d0d7c30743e8b781569593af98b8747beeada5 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 26 Sep 2025 13:45:08 +0200 Subject: [PATCH 19/64] fix issue with merge --- PadelClub/Views/Tournament/TournamentView.swift | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index ca4eb25..f7c9cc1 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -256,6 +256,7 @@ struct TournamentView: View { NavigationLink(value: Screen.stateSettings) { Label("Gestion", systemImage: "trash") } + .disabled(self.tournament.sharing) } label: { Label("Réglages", systemImage: "wrench.and.screwdriver") @@ -315,19 +316,7 @@ struct TournamentView: View { NavigationLink(value: Screen.share) { Label("Partager", systemImage: "square.and.arrow.up") } - - if self.tournament.sharing == nil { - Divider() - - NavigationLink(value: Screen.stateSettings) { - Text("Gestion du tournoi") - Text("Annuler, supprimer ou terminer le tournoi") - } - Divider() - - NavigationLink(value: Screen.stateSettings) { - Label("Tournoi", systemImage: "trash") - } + } label: { LabelOptions() .popoverTip(tournamentRunningTip) From 74b61c6046d8c8ee5fdf2b53832c273c27578a8e Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 26 Sep 2025 13:45:51 +0200 Subject: [PATCH 20/64] remove refresh team action and task --- .../Navigation/Agenda/EventListView.swift | 60 +++++++-------- .../Screen/InscriptionManagerView.swift | 74 +++++++++---------- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 50e8b80..a3166f3 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -204,15 +204,15 @@ struct EventListView: View { } Divider() Menu { - Button { - Task { - await pcTournaments.concurrentForEach { tournament in - await tournament.refreshTeamList(forced: true) - } - } - } label: { - Text("M-à-j des inscriptions") - } +// Button { +// Task { +// await pcTournaments.concurrentForEach { tournament in +// await tournament.refreshTeamList(forced: true) +// } +// } +// } label: { +// Text("M-à-j des inscriptions") +// } Button { pcTournaments.forEach { tournament in @@ -413,27 +413,27 @@ struct EventListView: View { private func _tournamentView(_ tournament: Tournament) -> some View { NavigationLink(value: tournament) { TournamentCellView(tournament: tournament) - .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name.CollectionDidLoad), perform: { notification in - - if let store = notification.object as? SyncedCollection { - if store.storeId == tournament.id { - if tournament.store?.fileCollectionsAllLoaded() == true { - tournament.lastTeamRefresh = nil - } - } - } - if let store = notification.object as? SyncedCollection { - if store.storeId == tournament.id { - if tournament.store?.fileCollectionsAllLoaded() == true { - tournament.lastTeamRefresh = nil - } - } - } - }) - .id(tournament.lastTeamRefresh) - .task(priority: .background) { - await tournament.refreshTeamList(forced: false) - } +// .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name.CollectionDidLoad), perform: { notification in +// +// if let store = notification.object as? SyncedCollection { +// if store.storeId == tournament.id { +// if tournament.store?.fileCollectionsAllLoaded() == true { +// tournament.lastTeamRefresh = nil +// } +// } +// } +// if let store = notification.object as? SyncedCollection { +// if store.storeId == tournament.id { +// if tournament.store?.fileCollectionsAllLoaded() == true { +// tournament.lastTeamRefresh = nil +// } +// } +// } +// }) +// .id(tournament.lastTeamRefresh) +// .task(priority: .background) { +// await tournament.refreshTeamList(forced: false) +// } } .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) .onChange(of: tournament.isTemplate) { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 04a5981..b4fea9f 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -247,9 +247,9 @@ struct InscriptionManagerView: View { } if tournament.enableOnlineRegistration { - RowButtonView("Rafraîchir la liste", cornerRadius: 20) { - await _refreshList(forced: true) - } +// RowButtonView("Rafraîchir la liste", cornerRadius: 20) { +// await _refreshList(forced: true) +// } } else if tournament.onlineRegistrationCanBeEnabled() { RowButtonView("Inscription en ligne") { navigation.path.append(Screen.settings) @@ -261,12 +261,12 @@ struct InscriptionManagerView: View { } } } - .task(priority: .background) { - await _refreshList(forced: false) - } - .refreshable { - await _refreshList(forced: true) - } +// .task(priority: .background) { +// await _refreshList(forced: false) +// } +// .refreshable { +// await _refreshList(forced: true) +// } .onAppear { if tournament.enableOnlineRegistration == false || refreshStatus == true { _setHash(currentSelectedSortedTeams: selectedSortedTeams) @@ -581,31 +581,31 @@ struct InscriptionManagerView: View { // try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) // } // - private func _refreshList(forced: Bool) async { - if refreshStatus == true, forced == false { return } - if tournament.enableOnlineRegistration == false { return } - if tournament.hasEnded() { return } - if tournament.refreshInProgress { return } - - refreshResult = nil - refreshStatus = nil - do { - await self.tournament.refreshTeamList(forced: forced) - - _setHash() - if let lastTeamRefresh = self.tournament.lastTeamRefresh?.formatted(date: .abbreviated, time: .shortened) { - self.refreshResult = "Dernière m-à-j : \(lastTeamRefresh)" - } else { - self.refreshResult = "La synchronization a réussi" - } - self.refreshStatus = true - - } catch { - Logger.error(error) - self.refreshResult = "La synchronization a échoué" - self.refreshStatus = false - } - } +// private func _refreshList(forced: Bool) async { +// if refreshStatus == true, forced == false { return } +// if tournament.enableOnlineRegistration == false { return } +// if tournament.hasEnded() { return } +// if tournament.refreshInProgress { return } +// +// refreshResult = nil +// refreshStatus = nil +// do { +// await self.tournament.refreshTeamList(forced: forced) +// +// _setHash() +// if let lastTeamRefresh = self.tournament.lastTeamRefresh?.formatted(date: .abbreviated, time: .shortened) { +// self.refreshResult = "Dernière m-à-j : \(lastTeamRefresh)" +// } else { +// self.refreshResult = "La synchronization a réussi" +// } +// self.refreshStatus = true +// +// } catch { +// Logger.error(error) +// self.refreshResult = "La synchronization a échoué" +// self.refreshStatus = false +// } +// } private func _teamRegisteredView(selectedSortedTeams: [TeamRegistration]) -> some View { List { @@ -896,9 +896,9 @@ struct InscriptionManagerView: View { } } - RowButtonView("Rafraîchir les inscriptions en ligne") { - await _refreshList(forced: true) - } +// RowButtonView("Rafraîchir les inscriptions en ligne") { +// await _refreshList(forced: true) +// } } } header: { HStack { From 86c1fa26cc8f097fc5fec651e79ea78f7259bc0f Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 26 Sep 2025 13:47:56 +0200 Subject: [PATCH 21/64] add subtitle to menu gestion du tournoi --- PadelClub/Views/Tournament/TournamentView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index b4621d7..8e452f9 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -253,8 +253,11 @@ struct TournamentView: View { Divider() - NavigationLink(value: Screen.stateSettings) { + Button { + navigation.path.append(Screen.stateSettings) + } label: { Label("Gestion", systemImage: "trash") + Text("Annuler, supprimer ou terminer le tournoi") } } label: { From 67cfa830a84eb1fea96dde49c97dc58fd4e23d1c Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 26 Sep 2025 14:43:55 +0200 Subject: [PATCH 22/64] fix build --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Views/Tournament/TournamentView.swift | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index e06cb96..194c0e8 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3394,7 +3394,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\""; @@ -3417,7 +3417,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.49; + MARKETING_VERSION = 1.2.52; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3438,7 +3438,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; @@ -3460,7 +3460,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.49; + MARKETING_VERSION = 1.2.52; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 6f80d8c..ce16768 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -311,11 +311,13 @@ struct TournamentView: View { Text("Gestion du tournoi") Text("Annuler, supprimer ou terminer le tournoi") } + } Divider() NavigationLink(value: Screen.stateSettings) { Label("Tournoi", systemImage: "trash") } + } label: { LabelOptions() .popoverTip(tournamentRunningTip) From aaa1c166604b48fadc06946c09485036f2a31947 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 26 Sep 2025 14:46:45 +0200 Subject: [PATCH 23/64] fix merge issue --- PadelClub/Views/Tournament/TournamentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 939c876..d674daf 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -256,7 +256,7 @@ struct TournamentView: View { NavigationLink(value: Screen.stateSettings) { Label("Gestion", systemImage: "trash") } - .disabled(self.tournament.sharing) + .disabled((self.tournament.sharing != nil)) } label: { Label("Réglages", systemImage: "wrench.and.screwdriver") From 8602881562590a13fa1403cc3f0391106f3da69b Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 29 Sep 2025 11:04:09 +0200 Subject: [PATCH 24/64] fix ios 26 add tournament name in subtitle in a lot of subviews add the ability to transfer tournaments into an event fix broadcast view page to better display some sharing options --- PadelClub.xcodeproj/project.pbxproj | 16 ++++ PadelClub/Extensions/View+Extensions.swift | 22 +++++ PadelClub/Utils/Tips.swift | 15 +++ .../Cashier/Event/EventTournamentsView.swift | 31 ++++++- .../Cashier/Event/TournamentPickerView.swift | 91 +++++++++++++++++++ .../Views/Components/BarButtonView.swift | 25 ++--- .../Views/GroupStage/GroupStagesView.swift | 5 + PadelClub/Views/Match/MatchDetailView.swift | 8 +- .../Agenda/TournamentSubscriptionView.swift | 14 --- .../CourtAvailabilitySettingsView.swift | 10 +- PadelClub/Views/Round/LoserRoundsView.swift | 7 ++ PadelClub/Views/Round/RoundsView.swift | 6 ++ PadelClub/Views/Score/EditScoreView.swift | 7 ++ PadelClub/Views/Score/FollowUpMatchView.swift | 7 ++ .../Tournament/Screen/BroadcastView.swift | 55 +++++++---- .../Screen/InscriptionManagerView.swift | 5 + .../Tournament/Screen/PrintSettingsView.swift | 5 + .../Screen/RegistrationSetupView.swift | 5 + .../Screen/TableStructureView.swift | 6 ++ .../Screen/TournamentCallView.swift | 5 + .../Screen/TournamentCashierView.swift | 5 + .../Screen/TournamentRankView.swift | 5 + .../Screen/TournamentScheduleView.swift | 6 ++ .../Screen/TournamentSettingsView.swift | 6 ++ .../Views/Tournament/TournamentView.swift | 20 +++- 25 files changed, 330 insertions(+), 57 deletions(-) create mode 100644 PadelClub/Extensions/View+Extensions.swift create mode 100644 PadelClub/Views/Cashier/Event/TournamentPickerView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 6c74e32..16a1d4f 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -711,6 +711,12 @@ FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; + FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; }; + FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; }; + FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */; }; + FFA97C9E2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; }; + FFA97C9F2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; }; + FFA97CA02E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; }; FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; @@ -1118,6 +1124,8 @@ FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = ""; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentPickerView.swift; sourceTree = ""; }; + FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; FFB0FF662E81B671009EDEAC /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdaySelectionView.swift; sourceTree = ""; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = ""; }; @@ -1341,6 +1349,7 @@ C49771DE2DC25F04005CD239 /* Badge+Extensions.swift */, C49771DF2DC25F04005CD239 /* SpinDrawable+Extensions.swift */, C49772022DC260D3005CD239 /* Round+Extensions.swift */, + FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -1866,6 +1875,7 @@ FFE103072C353B7600684FC9 /* EventClubSettingsView.swift */, FF8F263C2BAD627A00650388 /* TournamentConfiguratorView.swift */, FF8E52332DF01D6100099B75 /* EventStatusView.swift */, + FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */, ); name = Event; path = Cashier/Event; @@ -2346,6 +2356,7 @@ FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, + FFA97CA02E8A7C080089EA22 /* View+Extensions.swift in Sources */, FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */, FFF03C942BD91D0C00B516FC /* ButtonValidateView.swift in Sources */, FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */, @@ -2434,6 +2445,7 @@ FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */, FFC91AF92BD6A09100B29808 /* FortuneWheelView.swift in Sources */, FFE8B5C82DAA390900BDE966 /* StripeValidationService.swift in Sources */, + FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */, C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */, FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */, FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, @@ -2613,6 +2625,7 @@ FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */, FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */, FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */, + FFA97C9E2E8A7C080089EA22 /* View+Extensions.swift in Sources */, FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */, FF4CBFC32C996C0600151637 /* GroupStageCallingView.swift in Sources */, FF4CBFC52C996C0600151637 /* CashierSettingsView.swift in Sources */, @@ -2701,6 +2714,7 @@ FF4CC0102C996C0600151637 /* LoadingViewModifier.swift in Sources */, FF4CC0112C996C0600151637 /* PlayerView.swift in Sources */, FF4CC0122C996C0600151637 /* MatchDetailView.swift in Sources */, + FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */, FF4CC0142C996C0600151637 /* PlayerBlockView.swift in Sources */, FF4CC0172C996C0600151637 /* PointView.swift in Sources */, FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */, @@ -2858,6 +2872,7 @@ FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */, FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */, FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */, + FFA97C9F2E8A7C080089EA22 /* View+Extensions.swift in Sources */, FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */, FF70FB422C90584900129CC2 /* GroupStageCallingView.swift in Sources */, FF70FB442C90584900129CC2 /* CashierSettingsView.swift in Sources */, @@ -2946,6 +2961,7 @@ FF70FB8F2C90584900129CC2 /* LoadingViewModifier.swift in Sources */, FF70FB902C90584900129CC2 /* PlayerView.swift in Sources */, FF70FB912C90584900129CC2 /* MatchDetailView.swift in Sources */, + FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */, FF70FB932C90584900129CC2 /* PlayerBlockView.swift in Sources */, FF70FB962C90584900129CC2 /* PointView.swift in Sources */, FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */, diff --git a/PadelClub/Extensions/View+Extensions.swift b/PadelClub/Extensions/View+Extensions.swift new file mode 100644 index 0000000..d24a949 --- /dev/null +++ b/PadelClub/Extensions/View+Extensions.swift @@ -0,0 +1,22 @@ +// +// View+Extensions.swift +// PadelClub +// +// Created by Razmig Sarkissian on 29/09/2025. +// + +import SwiftUI + +extension View { + /// Runs a transform only on iOS 26+, otherwise returns self + @ViewBuilder + func ifAvailableiOS26( + @ViewBuilder transform: (Self) -> Content + ) -> some View { + if #available(iOS 26.0, *) { + transform(self) + } else { + self + } + } +} diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index 1b0cb6c..befc086 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -660,6 +660,21 @@ struct UpdatePlannedDatesTip: Tip { } } +struct MergeTournamentTip: Tip { + var title: Text { + Text("Transfert de tournois") + } + + var message: Text? { + Text("Vous pouvez transferer des tournois d'un autre événement dans celui-ci.") + } + + var image: Image? { + Image(systemName: "square.and.arrow.down") + } +} + + struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? diff --git a/PadelClub/Views/Cashier/Event/EventTournamentsView.swift b/PadelClub/Views/Cashier/Event/EventTournamentsView.swift index 2d554fc..e80f3bb 100644 --- a/PadelClub/Views/Cashier/Event/EventTournamentsView.swift +++ b/PadelClub/Views/Cashier/Event/EventTournamentsView.swift @@ -15,7 +15,8 @@ struct EventTournamentsView: View { let event: Event @State private var newTournament: Tournament? @State private var mainTournament: Tournament? - + @State private var showTournamentPicker: Bool = false + var presentTournamentCreationView: Binding { Binding( get: { newTournament != nil }, set: { isPresented in @@ -122,13 +123,35 @@ struct EventTournamentsView: View { } .toolbar { ToolbarItem(placement: .topBarTrailing) { - BarButtonView("Ajouter un tournoi", icon: "plus.circle.fill") { - let tournament = Tournament.newEmptyInstance() - newTournament = tournament + BarButtonView("Importer un tournoi", icon: "square.and.arrow.down") { + showTournamentPicker = true + } + } + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .topBarTrailing) + } + + ToolbarItem(placement: .topBarTrailing) { + if #available(iOS 26.0, *) { + BarButtonView("Ajouter une indisponibilité", icon: "plus") { + let tournament = Tournament.newEmptyInstance() + newTournament = tournament + } + } else { + BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { + let tournament = Tournament.newEmptyInstance() + newTournament = tournament + } } } } .headerProminence(.increased) + .sheet(isPresented: $showTournamentPicker, content: { + NavigationStack { + TournamentPickerView(event: event) + .environmentObject(dataStore) + } + }) .sheet(isPresented: presentTournamentCreationView) { if let newTournament { NavigationStack { diff --git a/PadelClub/Views/Cashier/Event/TournamentPickerView.swift b/PadelClub/Views/Cashier/Event/TournamentPickerView.swift new file mode 100644 index 0000000..1b265d0 --- /dev/null +++ b/PadelClub/Views/Cashier/Event/TournamentPickerView.swift @@ -0,0 +1,91 @@ +// +// TournamentPickerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 29/09/2025. +// + +import SwiftUI +import LeStorage +import PadelClubData +import TipKit + +struct TournamentPickerView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) private var dismiss + let mergeTournamentTip = MergeTournamentTip() + let event: Event + @State private var selectedTournamentIds: Set = Set() + @State private var shouldEraseEmptyEvents: Bool = false + + var tournaments: [Tournament] { + dataStore.tournaments.filter({ $0.isDeleted == false && $0.event?.id != event.id }).sorted(by: \.startDate, order: .descending) + } + + var body: some View { + List(selection: $selectedTournamentIds) { + Section { + TipView(mergeTournamentTip) + .tipStyle(tint: .green) + } + + Section { + Toggle(isOn: $shouldEraseEmptyEvents) { + Text("Effacer les événements vides") + Text("Les événements qui n'ont plus de tournois seront effacés automatiquement.") + } + } + + ForEach(tournaments) { tournament in + TournamentCellView(tournament: tournament).tag(tournament.id) + } + } + .environment(\.editMode, Binding.constant(EditMode.active)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler") { + dismiss() + } + } + ToolbarItem(placement: .topBarTrailing) { + ButtonValidateView { + _transferTournaments() + dismiss() + } + .disabled(selectedTournamentIds.isEmpty) + } + } + .navigationTitle("Transfert de tournois") + } + + private func _transferTournaments() { + let tournaments = tournaments + var eventIdsToCheck = Set() + var tournamentsToSave = [Tournament]() + selectedTournamentIds.forEach { id in + if let tournament = tournaments.first(where: { $0.id == id }) { + if let eventId = tournament.event{ + eventIdsToCheck.insert(eventId) + } + tournament.event = event.id + tournamentsToSave.append(tournament) + } + } + + dataStore.tournaments.addOrUpdate(contentOfs: tournamentsToSave) + + if shouldEraseEmptyEvents { + + var eventsToDelete = [Event]() + eventIdsToCheck.forEach { eventId in + if let eventToCheck = dataStore.events.first(where: { $0.id == eventId }) { + if eventToCheck.tournaments.isEmpty && shouldEraseEmptyEvents { + eventsToDelete.append(eventToCheck) + } + } + } + + dataStore.events.delete(contentOfs: eventsToDelete) + } + } +} diff --git a/PadelClub/Views/Components/BarButtonView.swift b/PadelClub/Views/Components/BarButtonView.swift index b92c01e..6caf801 100644 --- a/PadelClub/Views/Components/BarButtonView.swift +++ b/PadelClub/Views/Components/BarButtonView.swift @@ -22,23 +22,14 @@ struct BarButtonView: View { Button(action: { action() }) { - Image(systemName: icon) - .resizable() - .scaledToFit() - .frame(minHeight: 28) - - /* - Label { - Text(accessibilityLabel) - } icon: { - Image(systemName: icon) - .resizable() - .scaledToFit() - .frame(minHeight: 36) - } - .labelStyle(.iconOnly) - //todo: resizing not working when label used - */ + if #available(iOS 26.0, *) { + Label(accessibilityLabel, systemImage: icon) + } else { + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } } } } diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index a14d4bb..cff819c 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -256,5 +256,10 @@ struct GroupStagesView: View { .environment(tournament) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 882cc00..e6cca86 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -420,7 +420,13 @@ struct MatchDetailView: View { .navigationTitle(match.matchTitle()) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + if let tournament = match.currentTournament() { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } + } } var quickLookHeader: some View { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index 1c27a96..b60e091 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -383,17 +383,3 @@ struct TournamentSubscriptionView: View { } } - -extension View { - /// Runs a transform only on iOS 26+, otherwise returns self - @ViewBuilder - func ifAvailableiOS26( - @ViewBuilder transform: (Self) -> Content - ) -> some View { - if #available(iOS 26.0, *) { - transform(self) - } else { - self - } - } -} diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 3f68a1f..28d8bd6 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -111,8 +111,14 @@ struct CourtAvailabilitySettingsView: View { } .toolbar { ToolbarItem(placement: .topBarTrailing) { - BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { - showingPopover = true + if #available(iOS 26.0, *) { + BarButtonView("Ajouter une indisponibilité", icon: "plus") { + showingPopover = true + } + } else { + BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { + showingPopover = true + } } } } diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 53179cd..55808a5 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -297,5 +297,12 @@ struct LoserRoundsView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle(upperBracketRound.correspondingLoserRoundTitle) + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + if let tournament = upperBracketRound.round.tournamentObject() { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } + } } } diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index 63a471e..81affa9 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -48,11 +48,17 @@ struct RoundsView: View { case .some(let selectedRound): RoundView(upperRound: selectedRound).id(selectedRound.id) .navigationTitle(selectedRound.round.roundTitle()) + } } .environment(\.isEditingTournamentSeed, $isEditingTournamentSeed) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } } } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 49d6be6..abf53d8 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -244,6 +244,13 @@ struct EditScoreView: View { matchDescriptor.setDescriptors.removeAll() matchDescriptor.addNewSet() } + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + if let tournament = matchDescriptor.match?.currentTournament() { + $0.navigationBarTitle(tournament.tournamentTitle()) + } + } + } } func save() { diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 93bb494..6134a5d 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -292,6 +292,13 @@ struct FollowUpMatchView: View { } } } + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + if let tournament = match?.currentTournament() { + $0.navigationBarTitle(tournament.tournamentTitle()) + } + } + } .onChange(of: readyMatches) { dismissWhenPresentFollowUpMatchIsDismissed = true if autoDismiss { diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index 33ef854..0af1148 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -84,6 +84,24 @@ struct BroadcastView: View { .tipStyle(tint: nil) } + if let shareURL = tournament.shareURL(.info) { + Section { + Link(destination: shareURL) { + Text(shareURL.absoluteString) + } + } header: { + Text("Page d'information") + } footer: { + HStack { + CopyPasteButtonView(pasteValue: shareURL.absoluteString) + Spacer() + ShareLink(item: shareURL) { + Label("Partager", systemImage: "square.and.arrow.up") + } + } + } + } + if let url = tournament.shareURL(.clubBroadcast) { Section { Link(destination: url) { @@ -113,6 +131,21 @@ struct BroadcastView: View { } } + Section { + let links : [PageLink] = [.info, .teams, .summons, .groupStages, .matches, .rankings, .broadcast, .clubBroadcast] + Picker(selection: $pageLink) { + ForEach(links) { pageLink in + Text(pageLink.localizedLabel()).tag(pageLink) + } + } label: { + Text("Page à partager") + } + .pickerStyle(.menu) + actionForURL(title: "Partager la page '" + pageLink.localizedLabel() + "'", url: tournament.shareURL(pageLink)) + } header: { + Text("Lien du tournoi à partager") + } + if tournament.isPrivate == false { Section { @@ -300,24 +333,9 @@ struct BroadcastView: View { } } .toolbar(content: { - if StoreCenter.main.userId != nil, tournament.isPrivate == false, tournament.club() != nil { + if StoreCenter.main.userId != nil, tournament.club() != nil { ToolbarItem(placement: .topBarTrailing) { Menu { - Section { - let links : [PageLink] = [.info, .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") - } - .pickerStyle(.menu) - actionForURL(title: "Partager la page '" + pageLink.localizedLabel() + "'", url: tournament.shareURL(pageLink)) - } header: { - Text("Lien du tournoi à partager") - } - #if DEBUG Section { actionForURL(title: "La Boutique", url: URLs.main.url.appending(path: "shop")) @@ -344,6 +362,11 @@ struct BroadcastView: View { }) .headerProminence(.increased) .navigationTitle("Publication") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .sheet(item: $urlToShow) { urlToShow in diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 04a5981..a93f24b 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -485,6 +485,11 @@ struct InscriptionManagerView: View { } .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Inscriptions") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } .navigationBarTitleDisplayMode(.inline) .onChange(of: tournament.hideTeamsWeight) { _save() diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift index b5fddb2..b7fc725 100644 --- a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -169,6 +169,11 @@ struct PrintSettingsView: View { } } .navigationTitle("Imprimer") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } .toolbarBackground(.visible, for: .navigationBar) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $presentShareView) { diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index 4b51a39..53ef012 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -358,6 +358,11 @@ struct RegistrationSetupView: View { .toolbarRole(.editor) .headerProminence(.increased) .navigationTitle("Inscription en ligne") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .navigationBarBackButtonHidden(hasChanges) diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 652f4d9..1867d8f 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -412,6 +412,12 @@ struct TableStructureView: View { } .navigationTitle("Structure") .navigationBarTitleDisplayMode(.inline) + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } + } diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index d6eb3c1..c8a96c0 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -137,6 +137,11 @@ struct TournamentCallView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Convocations") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index 66b1fc5..3882b56 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -166,6 +166,11 @@ struct TournamentCashierView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle(tournament.isFree() ? "Présence" : "Encaissement") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } } else { Text("no store") } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index bb72722..afe9356 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -158,6 +158,11 @@ struct TournamentRankView: View { } } .navigationTitle("Classement") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .toolbar { diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 30da32a..81cdf5c 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -101,6 +101,12 @@ struct TournamentScheduleView: View { .toolbarRole(.editor) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Horaires et formats") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } + } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index a9b29aa..9dcd82f 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -81,6 +81,12 @@ struct TournamentSettingsView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Réglages du tournoi") + .ifAvailableiOS26 { + if #available(iOS 26.0, *) { + $0.navigationSubtitle(tournament.tournamentTitle()) + } + } + } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 8e452f9..8539028 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -240,15 +240,29 @@ struct TournamentView: View { } Menu { - NavigationLink(value: Screen.event) { + Button { + navigation.path.append(Screen.event) + } label: { Text("Événement") + if let tournamentCount = tournament.eventObject()?.confirmedTournaments().count { + Text(tournamentCount.formatted() + " tournoi" + tournamentCount.pluralSuffix) + .foregroundStyle(.secondary) + } } - NavigationLink(value: Screen.settings) { + Button { + navigation.path.append(Screen.settings) + } label: { Text("Tournoi") + Text("Inscription en ligne, pistes, formats") + .foregroundStyle(.secondary) } - NavigationLink(value: Screen.structure) { + Button { + navigation.path.append(Screen.structure) + } label: { LabelStructure() + Text("Poules, tableau, qualifiés sortants") + .foregroundStyle(.secondary) } Divider() From 052204a8d6753e60ddd8d9ff69bf9bcffe51edb2 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 29 Sep 2025 11:04:23 +0200 Subject: [PATCH 25/64] v1.2.53 --- 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 16a1d4f..984b308 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3177,7 +3177,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.52; + MARKETING_VERSION = 1.2.53; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3224,7 +3224,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.52; + MARKETING_VERSION = 1.2.53; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 445a18076226964aeb5647b1533a0f6ad7572c1b Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 29 Sep 2025 11:37:09 +0200 Subject: [PATCH 26/64] fix left aligne planning view --- PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 81cdf5c..a9ea739 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -98,7 +98,6 @@ struct TournamentScheduleView: View { } } .navigationBarTitleDisplayMode(.inline) - .toolbarRole(.editor) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Horaires et formats") .ifAvailableiOS26 { From a0d6580a983a8b51f0ab32e07b06dfe7004010f1 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 30 Sep 2025 09:43:20 +0200 Subject: [PATCH 27/64] fix sync issues --- PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift | 6 ++---- PadelClubTests/SynchronizationTests.swift | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift index 874b833..5cd1f1b 100644 --- a/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift +++ b/PadelClub/Views/Navigation/Toolbox/DebugSettingsView.swift @@ -108,10 +108,8 @@ struct DebugSettingsView: View { isSynchronizing = true } - do { - try await StoreCenter.main.synchronizeLastUpdates() - } catch { - Logger.error(error) + let error = await StoreCenter.main.synchronizeLastUpdates() + if let error { await MainActor.run { errorMessage = error.localizedDescription showingError = true diff --git a/PadelClubTests/SynchronizationTests.swift b/PadelClubTests/SynchronizationTests.swift index 8d35757..aaee13d 100644 --- a/PadelClubTests/SynchronizationTests.swift +++ b/PadelClubTests/SynchronizationTests.swift @@ -21,7 +21,7 @@ struct SynchronizationTests { @Test func synchronizationTest() async throws { _ = try await self.login() - try await StoreCenter.main.synchronizeLastUpdates() + await StoreCenter.main.synchronizeLastUpdates() } From 06dd0fc3cca5121306246f10976b47bef3d07e13 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 30 Sep 2025 17:04:55 +0200 Subject: [PATCH 28/64] fix stuff --- .../RankingGroupStageSetupView.swift | 18 ++++++ .../Match/Components/MatchDateView.swift | 10 ++++ .../Screen/RegistrationSetupView.swift | 1 - .../Screen/TableStructureView.swift | 60 ++++++++++++------- .../Views/Tournament/TournamentView.swift | 24 ++++---- 5 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 PadelClub/Views/GroupStage/RankingGroupStageSetupView.swift diff --git a/PadelClub/Views/GroupStage/RankingGroupStageSetupView.swift b/PadelClub/Views/GroupStage/RankingGroupStageSetupView.swift new file mode 100644 index 0000000..bcf88d2 --- /dev/null +++ b/PadelClub/Views/GroupStage/RankingGroupStageSetupView.swift @@ -0,0 +1,18 @@ +// +// RankingGroupStageSetupView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 30/09/2025. +// + +import SwiftUI + +struct RankingGroupStageSetupView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + RankingGroupStageSetupView() +} diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index df0e586..202faf3 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -42,6 +42,16 @@ struct MatchDateView: View { let estimatedDuration = match.getDuration() if isReady { Section { + Button("Démarrer il y a 5 minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.updateStartDate(Calendar.current.date(byAdding: .minute, value: -5, to: currentDate), keepPlannedStartDate: true) + match.endDate = nil + match.confirmed = true + _save() + } + Button("Démarrer maintenant") { if let updatedField { match.setCourt(updatedField) diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index 53ef012..4eec9a2 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -355,7 +355,6 @@ struct RegistrationSetupView: View { }, message: { Text(ValidationError.onlinePaymentNotEnabled.localizedDescription) }) - .toolbarRole(.editor) .headerProminence(.increased) .navigationTitle("Inscription en ligne") .ifAvailableiOS26 { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 1867d8f..2033927 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -23,6 +23,7 @@ struct TableStructureView: View { @State private var structurePreset: PadelTournamentStructurePreset = .manual @State private var buildWildcards: Bool = true @FocusState private var stepperFieldIsFocused: Bool + @State private var confirmReset: Bool = false func displayWarning() -> Bool { let unsortedTeamsCount = tournament.unsortedTeamsCount() @@ -297,27 +298,7 @@ struct TableStructureView: View { Section { RowButtonView("Remise-à-zéro", role: .destructive) { - tournament.removeWildCards() - tournament.deleteGroupStages() - tournament.deleteStructure() - - if structurePreset != .manual { - structurePreset = PadelTournamentStructurePreset.manual - } else { - _updatePreset() - } - - tournament.teamCount = teamCount - tournament.groupStageCount = groupStageCount - tournament.teamsPerGroupStage = teamsPerGroupStage - tournament.qualifiedPerGroupStage = qualifiedPerGroupStage - tournament.groupStageAdditionalQualified = groupStageAdditionalQualified - - do { - try dataStore.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) - } + _reset() } } } @@ -376,6 +357,24 @@ struct TableStructureView: View { } } .toolbar { + if tournament.state() != .initial { + ToolbarItem(placement: .topBarTrailing) { + Button("Remise-à-zéro", systemImage: "trash", role: .destructive) { + confirmReset.toggle() + } + .confirmationDialog("Remise-à-zéro", isPresented: $confirmReset, titleVisibility: .visible, actions: { + Button("Tout effacer") { + _reset() + } + + Button("Annuler") { + + } + }, message: { + Text("Vous êtes sur le point d'effacer le tableau et les poules déjà créés et perdre les matchs et scores correspondant.") + }) + } + } ToolbarItem(placement: .confirmationAction) { if tournament.state() == .initial { ButtonValidateView { @@ -420,6 +419,25 @@ struct TableStructureView: View { } + private func _reset() { + tournament.removeWildCards() + tournament.deleteGroupStages() + tournament.deleteStructure() + + if structurePreset != .manual { + structurePreset = PadelTournamentStructurePreset.manual + } else { + _updatePreset() + } + + tournament.teamCount = teamCount + tournament.groupStageCount = groupStageCount + tournament.teamsPerGroupStage = teamsPerGroupStage + tournament.qualifiedPerGroupStage = qualifiedPerGroupStage + tournament.groupStageAdditionalQualified = groupStageAdditionalQualified + + dataStore.tournaments.addOrUpdate(instance: tournament) + } private func _saveWithoutRebuild() { tournament.teamCount = teamCount diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 8539028..148a7ce 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -172,7 +172,8 @@ struct TournamentView: View { TeamRestingView() case .stateSettings: TournamentStatusView(tournament: tournament) - + case .rankingGroupStageSetup: + RankingGroupStageSetupView() } } .environment(tournament) @@ -265,17 +266,9 @@ struct TournamentView: View { .foregroundStyle(.secondary) } - Divider() - - Button { - navigation.path.append(Screen.stateSettings) - } label: { - Label("Gestion", systemImage: "trash") - Text("Annuler, supprimer ou terminer le tournoi") - } - } label: { Label("Réglages", systemImage: "wrench.and.screwdriver") + Text("Événement, tournoi, structure") } @@ -333,6 +326,17 @@ struct TournamentView: View { // Label("Partager", systemImage: "square.and.arrow.up") // } + + Divider() + + Button { + navigation.path.append(Screen.stateSettings) + } label: { + Label("Clôture du tournoi", systemImage: "stop.fill") + Text("Annuler, supprimer ou terminer le tournoi") + } + + } label: { LabelOptions() .popoverTip(tournamentRunningTip) From 58f61f395fdd487ba160723c024077dea2505dee Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 30 Sep 2025 17:37:07 +0200 Subject: [PATCH 29/64] fix sharing stuff --- .../Navigation/Agenda/ActivityView.swift | 2 +- .../Navigation/Toolbox/ToolboxView.swift | 2 +- .../Views/Shared/SupportButtonView.swift | 18 ++++++-- .../Views/Tournament/TournamentView.swift | 43 ++++++++----------- PadelClub/Views/User/LoginView.swift | 2 +- PadelClub/Views/User/ShareModelView.swift | 8 +++- 6 files changed, 42 insertions(+), 33 deletions(-) diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 33d66e7..8caba04 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -554,7 +554,7 @@ struct ActivityView: View { RowButtonView("Importer via Tenup") { navigation.agendaDestination = .tenup } - SupportButtonView(contentIsUnavailable: true) + SupportButtonView(supportButtonType: .contentIsUnavailable) FooterButtonView("Vous n'êtes pas un juge-arbitre ou un organisateur de tournoi ? En savoir plus") { presentOnboarding = true diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index df94d1c..cc61638 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -47,7 +47,7 @@ struct ToolboxView: View { ShareLink(item: URLs.main.url) } - SupportButtonView(contentIsUnavailable: false) + SupportButtonView(supportButtonType: .bugReport) Link(destination: URLs.appReview.url) { Text("Partagez vos impressions !") diff --git a/PadelClub/Views/Shared/SupportButtonView.swift b/PadelClub/Views/Shared/SupportButtonView.swift index 9304c8a..492eb86 100644 --- a/PadelClub/Views/Shared/SupportButtonView.swift +++ b/PadelClub/Views/Shared/SupportButtonView.swift @@ -15,8 +15,15 @@ extension URL: Identifiable { return self.absoluteString } } + +enum SupportButtonType { + case contentIsUnavailable + case supervisorRequest + case bugReport +} + struct SupportButtonView: View { - let contentIsUnavailable: Bool + let supportButtonType: SupportButtonType @State private var sentError: ContactManagerError? = nil @State private var zipFilePath: URL? @@ -34,11 +41,16 @@ struct SupportButtonView: View { var body: some View { Group { - if contentIsUnavailable { + switch supportButtonType { + case .supervisorRequest: + Button("Demande d'ajout de superviseur") { + _zip() + } + case .contentIsUnavailable: FooterButtonView("Besoin d'aide ? Un problème ? Contactez-nous !") { _zip() } - } else { + case .bugReport: Button("Signaler un problème") { _zip() } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 1752144..8bc7794 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -172,8 +172,6 @@ struct TournamentView: View { TeamRestingView() case .stateSettings: TournamentStatusView(tournament: tournament) - case .rankingGroupStageSetup: - RankingGroupStageSetupView() } } .environment(tournament) @@ -265,17 +263,7 @@ struct TournamentView: View { Text("Poules, tableau, qualifiés sortants") .foregroundStyle(.secondary) } - - Divider() - Button { - navigation.path.append(Screen.stateSettings) - } label: { - Label("Gestion", systemImage: "trash") - Text("Annuler, supprimer ou terminer le tournoi") - } - .disabled((self.tournament.sharing != nil)) - } label: { Label("Réglages", systemImage: "wrench.and.screwdriver") Text("Événement, tournoi, structure") @@ -332,25 +320,28 @@ struct TournamentView: View { Label("Imprimer", systemImage: "printer") } - NavigationLink(value: Screen.share) { - Label("Partager", systemImage: "square.and.arrow.up") + if self.tournament.sharing == nil { + NavigationLink(value: Screen.share) { + Label("Partager", systemImage: "square.and.arrow.up") + } } - Divider() - NavigationLink(value: Screen.stateSettings) { - Label("Tournoi", systemImage: "trash") - } - - Button { - navigation.path.append(Screen.stateSettings) - } label: { - Label("Clôture du tournoi", systemImage: "stop.fill") - Text("Annuler, supprimer ou terminer le tournoi") + if self.tournament.sharing == nil { + Button { + navigation.path.append(Screen.stateSettings) + } label: { + Label("Clôture du tournoi", systemImage: "stop.fill") + Text("Annuler, supprimer ou terminer le tournoi") + } + } else { + Button { + //todo RAZ se retirer du partage + } label: { + Label("Quitter la supervision", systemImage: "person.crop.circle.badge.minus") + } } - - } label: { LabelOptions() .popoverTip(tournamentRunningTip) diff --git a/PadelClub/Views/User/LoginView.swift b/PadelClub/Views/User/LoginView.swift index fd352f6..3a15eef 100644 --- a/PadelClub/Views/User/LoginView.swift +++ b/PadelClub/Views/User/LoginView.swift @@ -89,7 +89,7 @@ struct LoginView: View { } description: { Text("Vous pourrez ensuite vous connecter ici. N'oubliez pas de vérifiez vos spams !") } actions: { - SupportButtonView(contentIsUnavailable: true) + SupportButtonView(supportButtonType: .contentIsUnavailable) } } } diff --git a/PadelClub/Views/User/ShareModelView.swift b/PadelClub/Views/User/ShareModelView.swift index 70b33d9..92f3397 100644 --- a/PadelClub/Views/User/ShareModelView.swift +++ b/PadelClub/Views/User/ShareModelView.swift @@ -33,7 +33,13 @@ struct ShareModelView : View { .listStyle(PlainListStyle()) .navigationTitle("Partage") } else { - ContentUnavailableView("Si vous souhaitez partager votre tournoi avec d'autres utilisateurs, veuillez contacter notre support", image: "person.crop.circle.badge.xmark") + ContentUnavailableView { + Label("Aucun superviseurs", systemImage: "person.crop.circle.badge.xmark") + } description: { + Text("Si vous souhaitez partager votre tournoi avec d'autres utilisateurs, veuillez contacter notre support") + } actions: { + SupportButtonView(supportButtonType: .supervisorRequest) + } } }.onAppear { From 4acb1e8ea4f68fac6801e09f462cd15635b3ff3b Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 1 Oct 2025 08:47:27 +0200 Subject: [PATCH 30/64] fix ios 26 stuff --- .../CallMessageCustomizationView.swift | 2 +- .../Views/Cashier/CashierSettingsView.swift | 64 +++++----- .../Cashier/Event/EventCreationView.swift | 12 +- .../Cashier/Event/EventSettingsView.swift | 32 +++-- PadelClub/Views/Club/ClubDetailView.swift | 11 +- PadelClub/Views/Components/StepperView.swift | 13 +- .../Views/Navigation/Umpire/UmpireView.swift | 40 +++--- .../Player/Components/PlayerPopoverView.swift | 28 ++-- PadelClub/Views/Player/PlayerDetailView.swift | 1 + .../Views/Tournament/Screen/AddTeamView.swift | 3 +- .../TournamentGeneralSettingsView.swift | 120 +++++++++--------- .../Screen/RegistrationSetupView.swift | 24 ++-- .../Views/Tournament/TournamentView.swift | 2 - 13 files changed, 168 insertions(+), 184 deletions(-) diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 8d2ebe9..0ef3dfb 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -126,7 +126,7 @@ struct CallMessageCustomizationView: View { } label: { Text("Valider") } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index 2baab1c..151c80a 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -127,49 +127,47 @@ struct CashierSettingsView: View { } } - ToolbarItem(placement: .keyboard) { - HStack { - if focusedField == ._entryFee { - if tournament.isFree() { - ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()))) { - entryFee = priceTag - tournament.entryFee = priceTag - focusedField = nil - } - .buttonStyle(.bordered) - } - } else { - Button("Gratuit") { - entryFee = nil - tournament.entryFee = nil + ToolbarItemGroup(placement: .keyboard) { + if focusedField == ._entryFee { + if tournament.isFree() { + ForEach(priceTags, id: \.self) { priceTag in + Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()))) { + entryFee = priceTag + tournament.entryFee = priceTag focusedField = nil } - .buttonStyle(.bordered) - - } - } else if focusedField == ._clubMemberFeeDeduction { - ForEach(deductionTags, id: \.self) { deductionTag in - Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { - clubMemberFeeDeduction = deductionTag - tournament.clubMemberFeeDeduction = deductionTag - focusedField = nil - } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) } + } else { Button("Gratuit") { - clubMemberFeeDeduction = entryFee - tournament.clubMemberFeeDeduction = clubMemberFeeDeduction + entryFee = nil + tournament.entryFee = nil focusedField = nil } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) + } - Spacer() - Button("Valider") { + } else if focusedField == ._clubMemberFeeDeduction { + ForEach(deductionTags, id: \.self) { deductionTag in + Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { + clubMemberFeeDeduction = deductionTag + tournament.clubMemberFeeDeduction = deductionTag + focusedField = nil + } + .buttonStyle(.borderedProminent) + } + Button("Gratuit") { + clubMemberFeeDeduction = entryFee + tournament.clubMemberFeeDeduction = clubMemberFeeDeduction focusedField = nil } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) + } + Spacer() + Button("Valider") { + focusedField = nil } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Cashier/Event/EventCreationView.swift b/PadelClub/Views/Cashier/Event/EventCreationView.swift index ef14271..4fa8d9b 100644 --- a/PadelClub/Views/Cashier/Event/EventCreationView.swift +++ b/PadelClub/Views/Cashier/Event/EventCreationView.swift @@ -73,14 +73,12 @@ struct EventCreationView: View { .focused($textFieldIsFocus) .toolbar { if textFieldIsFocus { - ToolbarItem(placement: .keyboard) { - HStack { - Spacer() - Button("Valider") { - textFieldIsFocus = false - } - .buttonStyle(.bordered) + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Valider") { + textFieldIsFocus = false } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index 76a8db2..71e2417 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -187,26 +187,24 @@ struct EventSettingsView: View { } if focusedField != nil { - ToolbarItem(placement: .keyboard) { - HStack { - if focusedField == ._name, eventName.isEmpty == false { - Button("Effacer") { - event.name = nil - eventName = "" - } - .buttonStyle(.borderless) - } else if focusedField == ._information, tournamentInformation.isEmpty == false { - Button("Effacer") { - tournamentInformation = "" - } - .buttonStyle(.borderless) + ToolbarItemGroup(placement: .keyboard) { + if focusedField == ._name, eventName.isEmpty == false { + Button("Effacer") { + event.name = nil + eventName = "" } - Spacer() - Button("Valider") { - focusedField = nil + .buttonStyle(.borderedProminent) + } else if focusedField == ._information, tournamentInformation.isEmpty == false { + Button("Effacer") { + tournamentInformation = "" } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) } + Spacer() + Button("Valider") { + focusedField = nil + } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index 1c0f9eb..bde5d69 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -223,13 +223,12 @@ struct ClubDetailView: View { .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { - ToolbarItem(placement: .keyboard) { - HStack { - Button("Fermer", role: .cancel) { - focusedField = nil - } - Spacer() + ToolbarItemGroup(placement: .keyboard) { + Button("Fermer", role: .cancel) { + focusedField = nil } + .buttonStyle(.borderedProminent) + Spacer() } } }) diff --git a/PadelClub/Views/Components/StepperView.swift b/PadelClub/Views/Components/StepperView.swift index 5319f92..7989dfa 100644 --- a/PadelClub/Views/Components/StepperView.swift +++ b/PadelClub/Views/Components/StepperView.swift @@ -67,15 +67,14 @@ struct StepperView: View { } .multilineTextAlignment(.trailing) .toolbar { - ToolbarItem(placement: .keyboard) { + ToolbarItemGroup(placement: .keyboard) { if amountIsFocused { - HStack { - Spacer() - Button("Confirmer") { - amountIsFocused = false - _validate() - } + Spacer() + Button("Confirmer") { + amountIsFocused = false + _validate() } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index 93e1132..f1526f0 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -327,30 +327,28 @@ struct UmpireView: View { }) .toolbar { if focusedField != nil { - ToolbarItem(placement: .keyboard) { - HStack { - if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { - Button("Effacer") { - _deleteUmpireMail() - } - .buttonStyle(.borderless) - } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { - Button("Effacer") { - _deleteUmpirePhone() - } - .buttonStyle(.borderless) - } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { - Button("Effacer") { - _deleteUmpireContact() - } - .buttonStyle(.borderless) + ToolbarItemGroup(placement: .keyboard) { + if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { + Button("Effacer") { + _deleteUmpireMail() } - Spacer() - Button("Valider") { - focusedField = nil + .buttonStyle(.borderedProminent) + } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { + Button("Effacer") { + _deleteUmpirePhone() } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) + } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { + Button("Effacer") { + _deleteUmpireContact() + } + .buttonStyle(.borderedProminent) + } + Spacer() + Button("Valider") { + focusedField = nil } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift index 76758fe..afdcad3 100644 --- a/PadelClub/Views/Player/Components/PlayerPopoverView.swift +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -202,27 +202,25 @@ struct PlayerPopoverView: View { } if licenseIsFocused || amountIsFocused { - ToolbarItem(placement: .keyboard) { - HStack { - Spacer() - Button("Confirmer") { - if licenseIsFocused { - license = license.trimmedMultiline - if requiredField.contains(.license) { - if license.isLicenseNumber { - amountIsFocused = true - } else { - displayWrongLicenceError = true - } - } else { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Confirmer") { + if licenseIsFocused { + license = license.trimmedMultiline + if requiredField.contains(.license) { + if license.isLicenseNumber { amountIsFocused = true + } else { + displayWrongLicenceError = true } } else { - amountIsFocused = false + amountIsFocused = true } + } else { + amountIsFocused = false } - .buttonStyle(.bordered) } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 3b1eaa8..74fb923 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -401,6 +401,7 @@ struct PlayerDetailView: View { } focusedField = nil } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 66f6a1a..3686ac1 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -461,6 +461,7 @@ struct AddTeamView: View { self.editableTextField = pasteString self.focusedField = nil } + .buttonStyle(.borderedProminent) Spacer() Button("Chercher") { if editableTextField.count > 1 { @@ -470,7 +471,7 @@ struct AddTeamView: View { self.displayWarningNotEnoughCharacter = true } } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) } } } header: { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index abdb908..6112cfe 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -217,78 +217,76 @@ struct TournamentGeneralSettingsView: View { .toolbarBackground(.visible, for: .navigationBar) .toolbar { if focusedField != nil { - ToolbarItem(placement: .keyboard) { - HStack { - if focusedField == ._entryFee { - if tournament.isFree() { - ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { - entryFee = priceTag - tournament.entryFee = priceTag - focusedField = nil - } - .buttonStyle(.bordered) - } - } else { - Button("Gratuit") { - entryFee = nil - tournament.entryFee = nil - focusedField = nil - } - .buttonStyle(.bordered) - - } - } else if focusedField == ._clubMemberFeeDeduction { - ForEach(deductionTags, id: \.self) { deductionTag in - Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { - clubMemberFeeDeduction = deductionTag - tournament.clubMemberFeeDeduction = deductionTag + ToolbarItemGroup(placement: .keyboard) { + if focusedField == ._entryFee { + if tournament.isFree() { + ForEach(priceTags, id: \.self) { priceTag in + Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { + entryFee = priceTag + tournament.entryFee = priceTag focusedField = nil } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) } + } else { Button("Gratuit") { - clubMemberFeeDeduction = entryFee - tournament.clubMemberFeeDeduction = clubMemberFeeDeduction + entryFee = nil + tournament.entryFee = nil focusedField = nil } - .buttonStyle(.bordered) - } else { - if focusedField == ._name, tournamentName.isEmpty == false { - Button("Effacer") { - tournament.name = nil - tournamentName = "" - } - .buttonStyle(.borderless) - } else if focusedField == ._information, tournamentInformation.isEmpty == false { - Button("Effacer") { - tournament.information = nil - tournamentInformation = "" - } - .buttonStyle(.borderless) - } else if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { - Button("Effacer") { - _deleteUmpireMail() - } - .buttonStyle(.borderless) - } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { - Button("Effacer") { - _deleteUmpirePhone() - } - .buttonStyle(.borderless) - } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { - Button("Effacer") { - _deleteUmpireContact() - } - .buttonStyle(.borderless) + .buttonStyle(.borderedProminent) + + } + } else if focusedField == ._clubMemberFeeDeduction { + ForEach(deductionTags, id: \.self) { deductionTag in + Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { + clubMemberFeeDeduction = deductionTag + tournament.clubMemberFeeDeduction = deductionTag + focusedField = nil } + .buttonStyle(.borderedProminent) } - Spacer() - Button("Valider") { + Button("Gratuit") { + clubMemberFeeDeduction = entryFee + tournament.clubMemberFeeDeduction = clubMemberFeeDeduction focusedField = nil } - .buttonStyle(.bordered) + .buttonStyle(.borderedProminent) + } else { + if focusedField == ._name, tournamentName.isEmpty == false { + Button("Effacer") { + tournament.name = nil + tournamentName = "" + } + .buttonStyle(.borderless) + } else if focusedField == ._information, tournamentInformation.isEmpty == false { + Button("Effacer") { + tournament.information = nil + tournamentInformation = "" + } + .buttonStyle(.borderedProminent) + } else if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { + Button("Effacer") { + _deleteUmpireMail() + } + .buttonStyle(.borderedProminent) + } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { + Button("Effacer") { + _deleteUmpirePhone() + } + .buttonStyle(.borderedProminent) + } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { + Button("Effacer") { + _deleteUmpireContact() + } + .buttonStyle(.borderedProminent) + } + } + Spacer() + Button("Valider") { + focusedField = nil } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index 4eec9a2..b94984c 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -331,20 +331,18 @@ struct RegistrationSetupView: View { }) .toolbar { if focusedField == ._stripeAccountId, stripeAccountId.isEmpty == false { - ToolbarItem(placement: .keyboard) { - HStack { - Button("Effacer") { - stripeAccountId = "" - stripeAccountIdIsInvalid = nil - tournament.stripeAccountId = nil - } - .buttonStyle(.borderless) - Spacer() - Button("Valider") { - focusedField = nil - } - .buttonStyle(.bordered) + ToolbarItemGroup(placement: .keyboard) { + Button("Effacer") { + stripeAccountId = "" + stripeAccountIdIsInvalid = nil + tournament.stripeAccountId = nil + } + .buttonStyle(.borderedProminent) + Spacer() + Button("Valider") { + focusedField = nil } + .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 148a7ce..0a31c6d 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -172,8 +172,6 @@ struct TournamentView: View { TeamRestingView() case .stateSettings: TournamentStatusView(tournament: tournament) - case .rankingGroupStageSetup: - RankingGroupStageSetupView() } } .environment(tournament) From e7b0571e9f242ec01bed65921bb58195d0a06580 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 1 Oct 2025 09:03:31 +0200 Subject: [PATCH 31/64] Bumps version to 1.2.53 --- 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 c3d0ac4..9ba3653 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3433,7 +3433,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.52; + MARKETING_VERSION = 1.2.53; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3476,7 +3476,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.52; + MARKETING_VERSION = 1.2.53; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 4a07d430b76b3985d77af8582ce7840f3626b930 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 1 Oct 2025 16:58:56 +0200 Subject: [PATCH 32/64] adds code to remove tournament from being shared --- PadelClub/Views/Tournament/TournamentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 8bc7794..c65ba53 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -337,7 +337,7 @@ struct TournamentView: View { } } else { Button { - //todo RAZ se retirer du partage + DataStore.shared.deleteTournament(self.tournament, noSync: true) } label: { Label("Quitter la supervision", systemImage: "person.crop.circle.badge.minus") } From 08cf60629be1d75f84cfd9eb7de28e93ca70bab3 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 1 Oct 2025 17:10:54 +0200 Subject: [PATCH 33/64] Implement the capacity from a user to remove himself from the sharing --- .../Views/Tournament/TournamentView.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index c65ba53..bed4487 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -12,6 +12,7 @@ import PadelClubData struct TournamentView: View { @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) private var dismiss @Environment(NavigationViewModel.self) var navigation: NavigationViewModel @State var tournament: Tournament @@ -337,7 +338,12 @@ struct TournamentView: View { } } else { Button { - DataStore.shared.deleteTournament(self.tournament, noSync: true) + self._removeUserFromSharing() + + + // StoreCenter.main.setAuthorizedUsers(for: <#T##T#>, users: T##[String]) +// +// DataStore.shared.deleteTournament(self.tournament, noSync: true) } label: { Label("Quitter la supervision", systemImage: "person.crop.circle.badge.minus") } @@ -354,6 +360,19 @@ struct TournamentView: View { Logger.log("Tournament Id = \(self.tournament.id), Payment = \(String(describing: self.tournament.payment))") } } + + fileprivate func _removeUserFromSharing() { + guard let userId = StoreCenter.main.userId else { return } + var users = StoreCenter.main.authorizedUsers(for: self.tournament.id) + users.removeAll(where: { $0 == userId }) + do { + try StoreCenter.main.setAuthorizedUsers(for: self.tournament, users: users) + DataStore.shared.deleteTournament(self.tournament, noSync: true) + dismiss() + } catch { + Logger.error(error) + } + } private func _save() { dataStore.tournaments.addOrUpdate(instance: tournament) From d1435688eb5e34ba746b79755160ca5c3f4de004 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 2 Oct 2025 07:53:55 +0200 Subject: [PATCH 34/64] fix payment stuff --- PadelClub.xcodeproj/project.pbxproj | 24 +++++ PadelClub/Utils/Network/PaymentService.swift | 41 ++++++++ PadelClub/Views/Player/PlayerDetailView.swift | 4 +- PadelClub/Views/Team/EditingTeamView.swift | 4 + .../Views/Team/PaymentRequestButton.swift | 51 ++++++++++ PadelClub/Views/Team/PaymentService.swift | 24 +++++ .../Screen/InscriptionManagerView.swift | 99 ++++++++++++++----- 7 files changed, 221 insertions(+), 26 deletions(-) create mode 100644 PadelClub/Utils/Network/PaymentService.swift create mode 100644 PadelClub/Views/Team/PaymentRequestButton.swift create mode 100644 PadelClub/Views/Team/PaymentService.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 984b308..3383ce2 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -154,6 +154,12 @@ FF2B51612C7E302C00FFF126 /* local.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = FF2B51602C7E302C00FFF126 /* local.sqlite */; }; FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; }; FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; }; + FF30ACED2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; }; + FF30ACEE2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; }; + FF30ACEF2E8D700B008B6006 /* PaymentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACEC2E8D700B008B6006 /* PaymentService.swift */; }; + FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; }; + FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; }; + FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; FF39B6152DC8825E004E10CE /* PadelClubData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49770202DC25A23005CD239 /* PadelClubData.framework */; }; @@ -717,6 +723,9 @@ FFA97C9E2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; }; FFA97C9F2E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; }; FFA97CA02E8A7C080089EA22 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */; }; + FFA97CA22E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; }; + FFA97CA32E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; }; + FFA97CA42E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */; }; FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; }; @@ -1019,6 +1028,8 @@ FF2B51622C7F073100FFF126 /* Model_1_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_1_1.xcdatamodel; sourceTree = ""; }; FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLinksView.swift; sourceTree = ""; }; FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = ""; }; + FF30ACEC2E8D700B008B6006 /* PaymentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentService.swift; sourceTree = ""; }; + FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRequestButton.swift; sourceTree = ""; }; FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; FF39B60F2DC87FEB004E10CE /* PadelClubData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PadelClubData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1126,6 +1137,7 @@ FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; FFA97C8C2E8A59D00089EA22 /* TournamentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentPickerView.swift; sourceTree = ""; }; FFA97C9D2E8A7C080089EA22 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; + FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingGroupStageSetupView.swift; sourceTree = ""; }; FFB0FF662E81B671009EDEAC /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdaySelectionView.swift; sourceTree = ""; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = ""; }; @@ -1762,6 +1774,7 @@ FF4AB6B42B9248200002987F /* NetworkManager.swift */, FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */, FFE8B5BA2DA9896800BDE966 /* RefundService.swift */, + FF30ACEC2E8D700B008B6006 /* PaymentService.swift */, FFE8B5C62DAA390000BDE966 /* StripeValidationService.swift */, FFE8B5CA2DAA429E00BDE966 /* XlsToCsvService.swift */, FFE8B6392DACEAEC00BDE966 /* ConfigurationService.swift */, @@ -1814,6 +1827,7 @@ FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF5DA18E2BB9268800A33061 /* GroupStagesSettingsView.swift */, FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */, + FFA97CA12E8BAC880089EA22 /* RankingGroupStageSetupView.swift */, FF9AC3932BE3625D00C2E883 /* Components */, FF9AC3922BE3625200C2E883 /* Shared */, ); @@ -1842,6 +1856,7 @@ FF1162862BD004AD000C4809 /* EditingTeamView.swift */, FF17CA562CC02FEA003C7323 /* CoachListView.swift */, FF7DCD382CC330260041110C /* TeamRestingView.swift */, + FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */, FF025AD62BD0C0FB00A86CF8 /* Components */, ); path = Team; @@ -2281,6 +2296,7 @@ C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */, FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, + FF30ACEF2E8D700B008B6006 /* PaymentService.swift in Sources */, FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */, FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */, @@ -2315,6 +2331,7 @@ FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, FFC91B032BD85E2400B29808 /* CourtView.swift in Sources */, FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */, + FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */, FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, C40CD2F32C412681000DBD9A /* AppDelegate.swift in Sources */, FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */, @@ -2451,6 +2468,7 @@ FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, + FFA97CA32E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */, FFCFC0122BBC3E1A00B82851 /* PointView.swift in Sources */, FF1CBC232BB53E590036DAAB /* ClubHolder.swift in Sources */, FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */, @@ -2550,6 +2568,7 @@ FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */, FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */, + FF30ACEE2E8D700B008B6006 /* PaymentService.swift in Sources */, FF4CBF722C996C0600151637 /* SwiftParser.swift in Sources */, FF4CBF732C996C0600151637 /* ChangePasswordView.swift in Sources */, FF4CBF742C996C0600151637 /* TournamentSubscriptionView.swift in Sources */, @@ -2584,6 +2603,7 @@ FF4CBF882C996C0600151637 /* TeamRowView.swift in Sources */, FF4CBF8A2C996C0600151637 /* AppDelegate.swift in Sources */, FF4CBF8C2C996C0600151637 /* EditScoreView.swift in Sources */, + FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */, FF4CBF8D2C996C0600151637 /* TournamentOrganizerView.swift in Sources */, FF4CBF8F2C996C0600151637 /* TournamentRunningView.swift in Sources */, FF4CBF902C996C0600151637 /* TournamentDatePickerView.swift in Sources */, @@ -2720,6 +2740,7 @@ FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */, FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */, C49771E72DC25F04005CD239 /* Color+Extensions.swift in Sources */, + FFA97CA22E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */, C49771E82DC25F04005CD239 /* Badge+Extensions.swift in Sources */, C49771E92DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */, FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */, @@ -2797,6 +2818,7 @@ FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */, FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */, + FF30ACED2E8D700B008B6006 /* PaymentService.swift in Sources */, FF70FAF12C90584900129CC2 /* SwiftParser.swift in Sources */, FF70FAF22C90584900129CC2 /* ChangePasswordView.swift in Sources */, FF70FAF32C90584900129CC2 /* TournamentSubscriptionView.swift in Sources */, @@ -2831,6 +2853,7 @@ FF70FB072C90584900129CC2 /* TeamRowView.swift in Sources */, FF70FB092C90584900129CC2 /* AppDelegate.swift in Sources */, FF70FB0B2C90584900129CC2 /* EditScoreView.swift in Sources */, + FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */, FF70FB0C2C90584900129CC2 /* TournamentOrganizerView.swift in Sources */, FF70FB0E2C90584900129CC2 /* TournamentRunningView.swift in Sources */, FF70FB0F2C90584900129CC2 /* TournamentDatePickerView.swift in Sources */, @@ -2967,6 +2990,7 @@ FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */, FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */, C49771E42DC25F04005CD239 /* Color+Extensions.swift in Sources */, + FFA97CA42E8BAC880089EA22 /* RankingGroupStageSetupView.swift in Sources */, C49771E52DC25F04005CD239 /* Badge+Extensions.swift in Sources */, C49771E62DC25F04005CD239 /* SpinDrawable+Extensions.swift in Sources */, FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */, diff --git a/PadelClub/Utils/Network/PaymentService.swift b/PadelClub/Utils/Network/PaymentService.swift new file mode 100644 index 0000000..1eef4db --- /dev/null +++ b/PadelClub/Utils/Network/PaymentService.swift @@ -0,0 +1,41 @@ +// +// PaymentService.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/10/2025. +// + +import Foundation +import LeStorage +import PadelClubData + +class PaymentService { + static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse { + let service = try StoreCenter.main.service() + let urlRequest = try service._baseRequest( + servicePath: "resend-payment-email/\(teamRegistrationId)/", + method: .post, + requiresToken: true + ) + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 else { + throw PaymentError.requestFailed + } + + return try JSON.decoder.decode(SimpleResponse.self, from: data) + } +} + +enum PaymentError: Error { + case requestFailed + case unauthorized + case unknown +} + +struct SimpleResponse: Codable { + let success: Bool + let message: String +} diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 74fb923..457217b 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -286,7 +286,7 @@ struct PlayerDetailView: View { } LabeledContent { - TextField("Téléphone contact", text: $contactPhoneNumber) + TextField("Téléphone", text: $contactPhoneNumber) .focused($focusedField, equals: ._contactPhoneNumber) .keyboardType(.namePhonePad) .textContentType(nil) @@ -320,7 +320,7 @@ struct PlayerDetailView: View { } LabeledContent { - TextField("Email contact", text: $contactEmail) + TextField("Email", text: $contactEmail) .focused($focusedField, equals: ._contactEmail) .keyboardType(.emailAddress) .textContentType(nil) diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 5b81bf2..6d10c35 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -140,6 +140,10 @@ struct EditingTeamView: View { } label: { Text("Payé en ligne") } + + if team.hasPaidOnline() == false { + PaymentRequestButton(teamRegistration: team) + } } if let refundMessage, refundMessage.isEmpty == false { diff --git a/PadelClub/Views/Team/PaymentRequestButton.swift b/PadelClub/Views/Team/PaymentRequestButton.swift new file mode 100644 index 0000000..7ce6843 --- /dev/null +++ b/PadelClub/Views/Team/PaymentRequestButton.swift @@ -0,0 +1,51 @@ +// +// PaymentRequestButton.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/10/2025. +// + + +import SwiftUI +import PadelClubData + +struct PaymentRequestButton: View { + let teamRegistration: TeamRegistration + @State private var isLoading = false + @State private var showAlert = false + @State private var alertMessage = "" + + var body: some View { + Button("Renvoyer email de paiement") { + resendEmail() + } + .disabled(isLoading) + .alert("Résultat", isPresented: $showAlert) { + Button("OK") { } + } message: { + Text(alertMessage) + } + } + + private func resendEmail() { + isLoading = true + Task { + do { + let response = try await PaymentService.resendPaymentEmail( + teamRegistrationId: teamRegistration.id + ) + await MainActor.run { + isLoading = false + alertMessage = response.message + showAlert = true + } + } catch { + await MainActor.run { + isLoading = false + alertMessage = "Erreur lors de l'envoi" + showAlert = true + } + } + } + } +} diff --git a/PadelClub/Views/Team/PaymentService.swift b/PadelClub/Views/Team/PaymentService.swift new file mode 100644 index 0000000..59cdfe5 --- /dev/null +++ b/PadelClub/Views/Team/PaymentService.swift @@ -0,0 +1,24 @@ +class PaymentService { + static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse { + let service = try StoreCenter.main.service() + let urlRequest = try service._baseRequest( + servicePath: "resend-payment-email/\(teamRegistrationId)/", + method: .post, + requiresToken: true + ) + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 else { + throw PaymentError.requestFailed + } + + return try JSON.decoder.decode(SimpleResponse.self, from: data) + } +} + +struct SimpleResponse: Codable { + let success: Bool + let message: String +} diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index a93f24b..9bbab53 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -54,7 +54,10 @@ struct InscriptionManagerView: View { @State private var refreshResult: String? = nil @State private var refreshStatus: Bool? @State private var showLegendView: Bool = false - + @State private var isLoading = false + @State private var showAlert = false + @State private var alertMessage = "" + var tournamentStore: TournamentStore? { return self.tournament.tournamentStore } @@ -335,6 +338,11 @@ struct InscriptionManagerView: View { } .tint(.master) } + .alert("Requête de paiement", isPresented: $showAlert) { + Button("OK") { } + } message: { + Text(alertMessage) + } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Menu { @@ -373,6 +381,40 @@ struct InscriptionManagerView: View { ToolbarItem(placement: .navigationBarTrailing) { Menu { + + #if PRODTEST + if tournament.enableOnlinePayment { + Button { + isLoading = true + Task { + do { + try await selectedSortedTeams.filter { team in + team.hasPaidOnline() == false && team.hasPaid() == false + }.concurrentForEach { team in + _ = try await PaymentService.resendPaymentEmail(teamRegistrationId: team.id) + } + + await MainActor.run { + isLoading = false + alertMessage = "Relance effectuée avec succès" + showAlert = true + } + } catch { + Logger.error(error) + await MainActor.run { + isLoading = false + alertMessage = "Erreur lors de la requête" + showAlert = true + } + } + } + } label: { + Text("Requête de paiement") + } + .disabled(isLoading) + } + #endif + if tournament.inscriptionClosed() == false { Menu { _sortingTypePickerView() @@ -396,7 +438,7 @@ struct InscriptionManagerView: View { if tournament.isAnimation() == false { Divider() - Section { + Menu { Button("+1 en tableau") { tournament.addWildCard(1, .bracket) _setHash() @@ -408,7 +450,7 @@ struct InscriptionManagerView: View { _setHash() } } - } header: { + } label: { Text("Ajout de wildcards") } @@ -421,21 +463,29 @@ struct InscriptionManagerView: View { } - Divider() - - Button { - presentImportView = true + Menu { + Button { + presentImportView = true + } label: { + Label("Importer un fichier", systemImage: "square.and.arrow.down") + } + Link(destination: URLs.beachPadel.url) { + Label("beach-padel.app.fft.fr", systemImage: "safari") + } } label: { - Label("Importer beach-padel", systemImage: "square.and.arrow.down") - } - Link(destination: URLs.beachPadel.url) { - Label("beach-padel.app.fft.fr", systemImage: "safari") + Text("Beach Padel") } if tournament.inscriptionClosed() == false { Divider() - _importTeamsMenuView(title: "Importer des paires") - _sharingTeamsMenuView() + + Menu { + _importTeamsMenuView(title: "Importer des paires d'un autre tournoi") + _sharingTeamsMenuView() + } label: { + Text("Importer / Exporter") + } + } else { _sharingTeamsMenuView() @@ -459,19 +509,20 @@ struct InscriptionManagerView: View { } //rankingDateSourcePickerView(showDateInLabel: true) - - Divider() - - _sharingTeamsMenuView() - Divider() - - _importTeamsMenuView(title: "Importer des paires") - Button { - presentImportView = true + Menu { + _sharingTeamsMenuView() + + _importTeamsMenuView(title: "Importer des paires d'un autre tournoi") + + Button { + presentImportView = true + } label: { + Label("Importer un fichier", systemImage: "square.and.arrow.down") + } } label: { - Label("Importer un fichier", systemImage: "square.and.arrow.down") + Text("Importer / Exporter") } } } label: { @@ -741,7 +792,7 @@ struct InscriptionManagerView: View { .pickerStyle(.menu) if currentRankSourceDate == SourceFileManager.shared.mostRecentDateAvailable { - Button("Rafraîchir") { + Button("Rafraîchir le rang des joueurs") { confirmUpdateRank = true } } From c7d5f4930e9ecee2564ca052bf5108dbbba3e060 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 2 Oct 2025 18:25:37 +0200 Subject: [PATCH 35/64] fix match format setup --- PadelClub/Views/Planning/SchedulerView.swift | 2 +- PadelClub/Views/Tournament/TournamentView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub/Views/Planning/SchedulerView.swift b/PadelClub/Views/Planning/SchedulerView.swift index ed33436..9038421 100644 --- a/PadelClub/Views/Planning/SchedulerView.swift +++ b/PadelClub/Views/Planning/SchedulerView.swift @@ -172,7 +172,7 @@ struct SchedulerView: View { Text("Match de classement \(round.roundTitle(.short))") } footer: { if tournament.isAnimation() == false, round.index == 1, let semi = round.loserRounds().first { - let federalFormat = tournament.loserBracketSmartMatchFormat(1) + let federalFormat = tournament.loserBracketSmartMatchFormat() if semi.matchFormat.weight > federalFormat.weight { Button { round.updateMatchFormatAndAllMatches(federalFormat) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 0a31c6d..8b998d0 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -282,7 +282,7 @@ struct TournamentView: View { .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Rappel des délais") } label: { - Label("Rappel des délais", systemImage: "calendarcalendar.badge.exclamationmark") + Label("Rappel des délais", systemImage: "calendar.badge.exclamationmark") } } From c2ccbf5dd7cbda6edbe0fc20224ad34727d33780 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 3 Oct 2025 11:23:45 +0200 Subject: [PATCH 36/64] improve view sharing --- .../Views/Navigation/Umpire/UmpireView.swift | 51 +---------------- .../Views/Shared/SupportButtonView.swift | 8 ++- PadelClub/Views/User/AccountView.swift | 56 +++++++++++++++++++ PadelClub/Views/User/ShareModelView.swift | 10 +++- 4 files changed, 72 insertions(+), 53 deletions(-) diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index f1526f0..4756177 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -75,56 +75,7 @@ struct UmpireView: View { AccountView(user: dataStore.user) { } } label: { AccountRowView(userName: dataStore.user.username) - } - - let onlineRegPaymentMode = dataStore.user.registrationPaymentMode - Section { - LabeledContent { - switch onlineRegPaymentMode { - case .corporate: - Text("Activé") - .bold() - .foregroundStyle(.green) - case .disabled: - Text("Désactivé") - .bold() - case .noFee: - Text("Activé") - .bold() - .foregroundStyle(.green) - case .stripe: - Text("Activé") - .bold() - .foregroundStyle(.green) - } - } label: { - Text("Option 'Paiement en ligne'") - if onlineRegPaymentMode == .corporate { - Text("Mode Padel Club") - .foregroundStyle(.secondary) - } else if onlineRegPaymentMode == .noFee { - Text("Commission Stripe") - .foregroundStyle(.secondary) - } else if onlineRegPaymentMode == .stripe { - Text("Commission Stripe et Padel Club") - .foregroundStyle(.secondary) - } - } - } footer: { - if onlineRegPaymentMode == .disabled { - FooterButtonView("Contactez nous pour activer cette option.") { - let emailTo: String = "support@padelclub.app" - let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)" - if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - .font(.callout) - .multilineTextAlignment(.leading) - } else { - Text("Permet de proposer le paiement de vos tournois en ligne.") - } - } + } } else { NavigationLink(value: UmpireScreen.login) { AccountRowView(userName: dataStore.user.username) diff --git a/PadelClub/Views/Shared/SupportButtonView.swift b/PadelClub/Views/Shared/SupportButtonView.swift index 492eb86..8411af0 100644 --- a/PadelClub/Views/Shared/SupportButtonView.swift +++ b/PadelClub/Views/Shared/SupportButtonView.swift @@ -20,6 +20,7 @@ enum SupportButtonType { case contentIsUnavailable case supervisorRequest case bugReport + case sharingRequest } struct SupportButtonView: View { @@ -42,10 +43,15 @@ struct SupportButtonView: View { var body: some View { Group { switch supportButtonType { + case .sharingRequest: + Button("Nous contacter") { + _zip() + } case .supervisorRequest: - Button("Demande d'ajout de superviseur") { + Button("Demande d'ajout de superviseur", systemImage: "plus") { _zip() } + .labelStyle(.titleOnly) case .contentIsUnavailable: FooterButtonView("Besoin d'aide ? Un problème ? Contactez-nous !") { _zip() diff --git a/PadelClub/Views/User/AccountView.swift b/PadelClub/Views/User/AccountView.swift index 922a9b8..e47d216 100644 --- a/PadelClub/Views/User/AccountView.swift +++ b/PadelClub/Views/User/AccountView.swift @@ -12,6 +12,7 @@ import PadelClubData struct AccountView: View { @Environment(\.dismiss) private var dismiss + @EnvironmentObject var dataStore: DataStore var user: CustomUser var handler: () -> () @@ -23,6 +24,61 @@ struct AccountView: View { PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item)) } #endif + + let onlineRegPaymentMode = dataStore.user.registrationPaymentMode + Section { + LabeledContent { + switch onlineRegPaymentMode { + case .corporate: + Text("Activé") + .bold() + .foregroundStyle(.green) + case .disabled: + Text("Désactivé") + .bold() + case .noFee: + Text("Activé") + .bold() + .foregroundStyle(.green) + case .stripe: + Text("Activé") + .bold() + .foregroundStyle(.green) + } + } label: { + Text("Option 'Paiement en ligne'") + if onlineRegPaymentMode == .corporate { + Text("Mode Padel Club") + .foregroundStyle(.secondary) + } else if onlineRegPaymentMode == .noFee { + Text("Commission Stripe") + .foregroundStyle(.secondary) + } else if onlineRegPaymentMode == .stripe { + Text("Commission Stripe et Padel Club") + .foregroundStyle(.secondary) + } + } + } footer: { + if onlineRegPaymentMode == .disabled { + FooterButtonView("Contactez nous pour activer cette option.") { + let emailTo: String = "support@padelclub.app" + let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)" + if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + .font(.callout) + .multilineTextAlignment(.leading) + } else { + Text("Permet de proposer le paiement de vos tournois en ligne.") + } + } + + Section { + Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?") + SupportButtonView(supportButtonType: .sharingRequest) + } + Section { NavigationLink("Changer de mot de passe") { ChangePasswordView() diff --git a/PadelClub/Views/User/ShareModelView.swift b/PadelClub/Views/User/ShareModelView.swift index 92f3397..1d5fbf7 100644 --- a/PadelClub/Views/User/ShareModelView.swift +++ b/PadelClub/Views/User/ShareModelView.swift @@ -30,8 +30,14 @@ struct ShareModelView : View { } } } - .listStyle(PlainListStyle()) - .navigationTitle("Partage") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + SupportButtonView(supportButtonType: .supervisorRequest) + } + } + .navigationTitle("Partager à vos superviseurs") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } else { ContentUnavailableView { Label("Aucun superviseurs", systemImage: "person.crop.circle.badge.xmark") From 09d5da914ca970f4f457ac065b6f110858157afb Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 3 Oct 2025 13:59:00 +0200 Subject: [PATCH 37/64] fix title partager --- .../Calling/Components/MenuWarningView.swift | 2 + PadelClub/Views/Player/PlayerDetailView.swift | 4 +- .../Views/Shared/SupportButtonView.swift | 43 ++++++++++++++++--- PadelClub/Views/User/ShareModelView.swift | 40 ++++++++--------- 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index baa3a8a..d0ec6a9 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -45,6 +45,8 @@ struct MenuWarningView: View { } label: { Text("Prévenir") } + .menuStyle(.button) + .buttonStyle(.borderedProminent) .sheet(isPresented: self.$showSubscriptionView, content: { NavigationStack { SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 457217b..93a1688 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -315,7 +315,7 @@ struct PlayerDetailView: View { CopyPasteButtonView(pasteValue: player.contactPhoneNumber) PasteButtonView(text: $contactPhoneNumber) } label: { - Text("Téléphone contact") + Text("Téléphone") } } @@ -342,7 +342,7 @@ struct PlayerDetailView: View { CopyPasteButtonView(pasteValue: player.contactEmail) PasteButtonView(text: $contactEmail) } label: { - Text("Email contact") + Text("Email") } } } header: { diff --git a/PadelClub/Views/Shared/SupportButtonView.swift b/PadelClub/Views/Shared/SupportButtonView.swift index 8411af0..eb20ba5 100644 --- a/PadelClub/Views/Shared/SupportButtonView.swift +++ b/PadelClub/Views/Shared/SupportButtonView.swift @@ -21,10 +21,37 @@ enum SupportButtonType { case supervisorRequest case bugReport case sharingRequest + + var localizedPrefix: String { + switch self { + case .contentIsUnavailable: + return "Décrivez votre problème" + case .supervisorRequest: + return localizedTopic + case .bugReport: + return "Décrivez votre problème" + case .sharingRequest: + return localizedTopic + } + } + + var localizedTopic: String { + switch self { + case .contentIsUnavailable: + return "Support Padel Club" + case .supervisorRequest: + return "Demande d'ajout de superviseur" + case .bugReport: + return "Support Padel Club" + case .sharingRequest: + return "Demande de partage" + } + } } struct SupportButtonView: View { let supportButtonType: SupportButtonType + var showIcon: Bool = false @State private var sentError: ContactManagerError? = nil @State private var zipFilePath: URL? @@ -48,10 +75,16 @@ struct SupportButtonView: View { _zip() } case .supervisorRequest: - Button("Demande d'ajout de superviseur", systemImage: "plus") { - _zip() + if showIcon { + Button("Demande d'ajout de superviseur", systemImage: "person.badge.plus") { + _zip() + } + .labelStyle(.iconOnly) + } else { + Button("Demande d'ajout de superviseur") { + _zip() + } } - .labelStyle(.titleOnly) case .contentIsUnavailable: FooterButtonView("Besoin d'aide ? Un problème ? Contactez-nous !") { _zip() @@ -92,14 +125,14 @@ struct SupportButtonView: View { private func _getSubject() -> String { let device = UIDevice.current let iOSVersion = device.systemVersion - return "[\(PadelClubApp.appVersion), \(iOSVersion), \(_getDeviceIdentifier())] Support Padel Club" + return "[\(PadelClubApp.appVersion), \(iOSVersion), \(_getDeviceIdentifier())] \(supportButtonType.localizedTopic)" } private func _getBody() -> String { let separator = "---------------------------------------------" let token = try? StoreCenter.main.token() - return ["Décrivez votre problème", "\n\n\n", separator, "token", token ?? "", separator, "userId", StoreCenter.main.userId, separator, "dataStore userId", DataStore.shared.user.id].compacted().joined(separator: "\n") + return [supportButtonType.localizedPrefix, "\n\n\n", separator, "token", token ?? "", separator, "userId", StoreCenter.main.userId, separator, "dataStore userId", DataStore.shared.user.id].compacted().joined(separator: "\n") } private func _getDeviceIdentifier() -> String { diff --git a/PadelClub/Views/User/ShareModelView.swift b/PadelClub/Views/User/ShareModelView.swift index 1d5fbf7..ba19a8b 100644 --- a/PadelClub/Views/User/ShareModelView.swift +++ b/PadelClub/Views/User/ShareModelView.swift @@ -17,27 +17,17 @@ struct ShareModelView : View { let instance: T var body: some View { - NavigationView { + List { if !self.viewModel.availableUsers.isEmpty { - List { - ForEach(self.viewModel.availableUsers, id: \.id) { user in - let isSelected = viewModel.contains(user.id) - UserRow(user: user, isSelected: isSelected) - .contentShape(Rectangle()) - .onTapGesture { - self.viewModel.userTapped(user.id) - self._modifyAuthorizedUsersList() - } - } + ForEach(self.viewModel.availableUsers, id: \.id) { user in + let isSelected = viewModel.contains(user.id) + UserRow(user: user, isSelected: isSelected) + .contentShape(Rectangle()) + .onTapGesture { + self.viewModel.userTapped(user.id) + self._modifyAuthorizedUsersList() + } } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - SupportButtonView(supportButtonType: .supervisorRequest) - } - } - .navigationTitle("Partager à vos superviseurs") - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.visible, for: .navigationBar) } else { ContentUnavailableView { Label("Aucun superviseurs", systemImage: "person.crop.circle.badge.xmark") @@ -47,8 +37,16 @@ struct ShareModelView : View { SupportButtonView(supportButtonType: .supervisorRequest) } } - - }.onAppear { + } + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + SupportButtonView(supportButtonType: .supervisorRequest, showIcon: true) + } + }) + .navigationTitle("Gérer le partage") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .onAppear { self.viewModel.selectedUsers = StoreCenter.main.authorizedUsers(for: self.instance.stringId) } } From cc50cc45acdcc0d6950aa9589628ac36dac4e688 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 3 Oct 2025 13:59:45 +0200 Subject: [PATCH 38/64] draft --- PadelClub/Views/User/ShareModelView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PadelClub/Views/User/ShareModelView.swift b/PadelClub/Views/User/ShareModelView.swift index 1d5fbf7..a71bd46 100644 --- a/PadelClub/Views/User/ShareModelView.swift +++ b/PadelClub/Views/User/ShareModelView.swift @@ -16,6 +16,8 @@ struct ShareModelView : View { let instance: T + @State var payment: TournamentPayment? = nil + var body: some View { NavigationView { if !self.viewModel.availableUsers.isEmpty { @@ -51,6 +53,9 @@ struct ShareModelView : View { }.onAppear { self.viewModel.selectedUsers = StoreCenter.main.authorizedUsers(for: self.instance.stringId) } + .task { + self.payment = await Guard.main.paymentForNewTournament() + } } fileprivate func _modifyAuthorizedUsersList() { From 2d40e5b8168d40e2e09a38c36d1e83fc71c164a8 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 3 Oct 2025 14:48:37 +0200 Subject: [PATCH 39/64] fix shared tournament umpire stuff --- .../Navigation/Agenda/EventListView.swift | 6 ++- .../TournamentGeneralSettingsView.swift | 42 +++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index a3166f3..78ac09a 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -532,11 +532,15 @@ struct EventListView: View { if federalTournament.umpireLabel().isEmpty == false { newTournament.umpireCustomContact = federalTournament.umpireLabel() + } else { + newTournament.umpireCustomContact = DataStore.shared.user.fullName() } if federalTournament.mailLabel().isEmpty == false { newTournament.umpireCustomMail = federalTournament.mailLabel() + } else { + newTournament.umpireCustomMail = DataStore.shared.user.email } - + newTournament.umpireCustomPhone = DataStore.shared.user.phone do { let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id) if let email = umpireData.email { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 6112cfe..d52c348 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -479,10 +479,34 @@ struct TournamentGeneralSettingsView: View { dataStore.tournaments.addOrUpdate(instance: tournament) } + private func _email() -> String { + if tournament.sharing == nil { + return dataStore.user.email + } else { + return "Mail" + } + } + + private func _phone() -> String { + if tournament.sharing == nil { + return dataStore.user.phone ?? "Téléphone" + } else { + return "Téléphone" + } + } + + private func _contact() -> String { + if tournament.sharing == nil { + return dataStore.user.fullName() + } else { + return "Contact" + } + } + private func _customUmpireView() -> some View { Section { VStack(alignment: .leading) { - TextField(dataStore.user.email, text: $umpireCustomMail) + TextField(_email(), text: $umpireCustomMail) .frame(maxWidth: .infinity) .keyboardType(.emailAddress) .autocapitalization(.none) @@ -496,7 +520,7 @@ struct TournamentGeneralSettingsView: View { } VStack(alignment: .leading) { - TextField(dataStore.user.phone ?? "Téléphone", text: $umpireCustomPhone) + TextField(_phone(), text: $umpireCustomPhone) .frame(maxWidth: .infinity) .keyboardType(.phonePad) .focused($focusedField, equals: ._umpireCustomPhone) @@ -510,26 +534,30 @@ struct TournamentGeneralSettingsView: View { VStack(alignment: .leading) { - TextField(dataStore.user.fullName(), text: $umpireCustomContact) + TextField(_contact(), text: $umpireCustomContact) .frame(maxWidth: .infinity) .keyboardType(.default) .focused($focusedField, equals: ._umpireCustomContact) .onSubmit { _confirmUmpireContact() } - if dataStore.user.getSummonsMessageSignature() != nil, umpireCustomContact != dataStore.user.fullName() { + if tournament.sharing == nil, dataStore.user.getSummonsMessageSignature() != nil, umpireCustomContact != dataStore.user.fullName() { Text("Attention vous avez une signature personnalisée contenant un contact différent.").foregroundStyle(.logoRed) FooterButtonView("retirer la personnalisation ?") { dataStore.user.summonsMessageSignature = nil self.dataStore.saveUser() } - } } - + } + } } header: { Text("Juge-arbitre") } footer: { - Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") + if tournament.sharing == nil { + Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") + } else { + Text("Ce tournoi vous avez été partagé par un autre utilisateur. Par défaut ses informations seront utilisés pour ces champs si jamais ils restent vides.") + } } } } From 15e480cf2868bc69353699987d80a9323695b6aa Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 3 Oct 2025 17:35:36 +0200 Subject: [PATCH 40/64] fix main menu --- .../Views/Tournament/TournamentView.swift | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index fde1a96..fe59ff8 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -238,37 +238,17 @@ struct TournamentView: View { } Divider() } - - Menu { - Button { - navigation.path.append(Screen.event) - } label: { - Text("Événement") - if let tournamentCount = tournament.eventObject()?.confirmedTournaments().count { - Text(tournamentCount.formatted() + " tournoi" + tournamentCount.pluralSuffix) - .foregroundStyle(.secondary) - } - } - Button { - navigation.path.append(Screen.settings) - } label: { - Text("Tournoi") - Text("Inscription en ligne, pistes, formats") - .foregroundStyle(.secondary) - } - - Button { - navigation.path.append(Screen.structure) - } label: { - LabelStructure() - Text("Poules, tableau, qualifiés sortants") - .foregroundStyle(.secondary) - } - - } label: { - Label("Réglages", systemImage: "wrench.and.screwdriver") - Text("Événement, tournoi, structure") + NavigationLink(value: Screen.event) { + Label("Événement", systemImage: "info") + } + + NavigationLink(value: Screen.settings) { + Label("Tournoi", systemImage: "gearshape") + } + + NavigationLink(value: Screen.structure) { + Label("Structure", systemImage: "flowchart") } NavigationLink(value: Screen.call) { @@ -281,9 +261,9 @@ struct TournamentView: View { TournamentDeadlinesView(tournament: tournament) } .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Rappel des délais") + .navigationTitle("Rappel des échéances") } label: { - Label("Rappel des délais", systemImage: "calendar.badge.exclamationmark") + Label("Échéances", systemImage: "checklist") } } @@ -291,10 +271,10 @@ struct TournamentView: View { Label(tournament.isFree() ? "Présence" : "Encaissement", systemImage: tournament.isFree() ? "person.crop.circle.badge.checkmark" : "eurosign.circle") } - NavigationLink(value: Screen.statistics) { - Label("Statistiques", systemImage: "123.rectangle") - } - +// NavigationLink(value: Screen.statistics) { +// Label("Statistiques", systemImage: "123.rectangle") +// } +// NavigationLink(value: Screen.rankings) { LabeledContent { @@ -306,10 +286,7 @@ struct TournamentView: View { .foregroundStyle(.green) } } label: { - Text("Classement final") - if tournament.publishRankings == false { - Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) - } + Label("Classement", systemImage: "trophy") } } @@ -330,11 +307,8 @@ struct TournamentView: View { Divider() if self.tournament.sharing == nil { - Button { - navigation.path.append(Screen.stateSettings) - } label: { - Label("Clôture du tournoi", systemImage: "stop.fill") - Text("Annuler, supprimer ou terminer le tournoi") + NavigationLink(value: Screen.stateSettings) { + Label("Clôturer", systemImage: "stop.fill") } } else { Button { From b239ff9a074c1263e1c393416bdcbf6c2c1e6177 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 3 Oct 2025 18:23:14 +0200 Subject: [PATCH 41/64] v1.2.54 --- 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 a8685f6..e006acf 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3200,7 +3200,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.53; + MARKETING_VERSION = 1.2.54; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3247,7 +3247,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.53; + MARKETING_VERSION = 1.2.54; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From dec6f21db98f26f3330637895feee028eab51feb Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 6 Oct 2025 10:19:08 +0200 Subject: [PATCH 42/64] Adds payment when adding supervisors --- .../Subscription/PaymentStatusView.swift | 9 ++++++--- PadelClub/Views/Tournament/TournamentView.swift | 8 +++++++- PadelClub/Views/User/ShareModelView.swift | 17 ++++++++++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift b/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift index 1cb2854..4ab8682 100644 --- a/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift +++ b/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift @@ -48,6 +48,8 @@ struct PaymentStatusView: View { @State var payment: TournamentPayment? = .free + @State var noOfferMessage: String? = nil + var body: some View { Group { @@ -58,7 +60,8 @@ struct PaymentStatusView: View { let text = "Tournoi offert (\(remaining) restant\(end))" ImageInfoView(systemImage: "gift.fill", text: text, tip: FreeTournamentTip()) case nil: - ImageInfoView(systemImage: "exclamationmark.bubble.fill", text: "Veuillez souscrire à une offre pour convoquer ou entrer un résultat", textColor: .white, backgroundColor: .logoRed, tip: NoPaymentTip()) + var text = noOfferMessage ?? "Veuillez souscrire à une offre pour entrer un résultat" + ImageInfoView(systemImage: "exclamationmark.bubble.fill", text: text, textColor: .white, backgroundColor: .logoRed, tip: NoPaymentTip()) default: EmptyView() } @@ -77,7 +80,7 @@ struct PaymentStatusView: View { struct FreeTournamentTip: Tip { var title: Text { - return Text("Nous vous offrons vos 3 premiers tournois ! Convoquez les équipes, créez les poules, le tableau comme vous le souhaitez. \nEnregistrez les résultats de chaque équipes et diffusez les scores en temps réel sur les écrans de votre club !\n\n Votre tournoi est décompté lorsque vous convoquez ou que vous rentrez un résultat.") + return Text("Nous vous offrons vos 3 premiers tournois ! Convoquez les équipes, créez les poules, le tableau comme vous le souhaitez. \nEnregistrez les résultats de chaque équipes et diffusez les scores en temps réel sur les écrans de votre club !\n\n Votre tournoi est décompté lorsque vous rentrez un résultat.") } var image: Image? { @@ -88,7 +91,7 @@ struct FreeTournamentTip: Tip { struct NoPaymentTip: Tip { var title: Text { - return Text("Vous ne disposez plus d'une offre vous permettant de convoquer les joueurs et de rentrer les résultats des matchs. Nous vous invitons à consulter les offres dans l'onglet JA.").foregroundStyle(.white) + return Text("Vous ne disposez plus d'une offre vous permettant de rentrer les résultats des matchs. Nous vous invitons à consulter les offres dans l'onglet JA.").foregroundStyle(.white) } var image: Image? { diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index fe59ff8..38f8199 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -168,7 +168,13 @@ struct TournamentView: View { case .print: PrintSettingsView(tournament: tournament) case .share: - ShareModelView(instance: tournament) + ShareModelView(instance: tournament, handler: { users in + if users.count > 0 { + Task { + try? await self.tournament.payIfNecessary() + } + } + }) case .restingTime: TeamRestingView() case .stateSettings: diff --git a/PadelClub/Views/User/ShareModelView.swift b/PadelClub/Views/User/ShareModelView.swift index 5cbe647..b0f2c08 100644 --- a/PadelClub/Views/User/ShareModelView.swift +++ b/PadelClub/Views/User/ShareModelView.swift @@ -10,16 +10,22 @@ import LeStorage import SwiftUI import PadelClubData +typealias UsersClosure = (([String]) -> ()) + struct ShareModelView : View { @StateObject private var viewModel = UserSearchViewModel() let instance: T - - @State var payment: TournamentPayment? = nil + var handler: UsersClosure? = nil var body: some View { List { + if T.self is Tournament.Type { + Section { + PaymentStatusView(noOfferMessage: "Veuillez souscrire à une offre afin de payer le tournoi") + } + } if !self.viewModel.availableUsers.isEmpty { ForEach(self.viewModel.availableUsers, id: \.id) { user in let isSelected = viewModel.contains(user.id) @@ -50,9 +56,10 @@ struct ShareModelView : View { .toolbarBackground(.visible, for: .navigationBar) .onAppear { self.viewModel.selectedUsers = StoreCenter.main.authorizedUsers(for: self.instance.stringId) - } - .task { - self.payment = await Guard.main.paymentForNewTournament() + }.onDisappear { + if let handler { + handler(self.viewModel.selectedUsers) + } } } From 2183f2863f82319fb2cbf0733c7ad661535ad5d6 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 6 Oct 2025 10:20:07 +0200 Subject: [PATCH 43/64] Remove sharing button --- PadelClub/Views/Tournament/TournamentView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 38f8199..c15760d 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -304,11 +304,11 @@ struct TournamentView: View { Label("Imprimer", systemImage: "printer") } - if self.tournament.sharing == nil { - NavigationLink(value: Screen.share) { - Label("Partager", systemImage: "square.and.arrow.up") - } - } +// if self.tournament.sharing == nil { +// NavigationLink(value: Screen.share) { +// Label("Partager", systemImage: "square.and.arrow.up") +// } +// } Divider() From 8fdeff82f111929c3072446bbaff0254f631cef8 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 7 Oct 2025 09:06:01 +0200 Subject: [PATCH 44/64] overhaul screens disposition --- PadelClub.xcodeproj/project.pbxproj | 32 ++ PadelClub/PadelClubApp.swift | 10 +- PadelClub/ViewModel/NavigationViewModel.swift | 2 +- PadelClub/ViewModel/TabDestination.swift | 5 + PadelClub/Views/Club/ClubsView.swift | 77 ++-- .../Views/GroupStage/GroupStageView.swift | 3 + .../Navigation/Agenda/ActivityView.swift | 2 +- PadelClub/Views/Navigation/MainView.swift | 13 +- .../Navigation/MyAccount/MyAccountView.swift | 226 ++++++++++ .../Navigation/Toolbox/ToolboxView.swift | 116 ++--- .../Navigation/Umpire/UmpireOptionsView.swift | 68 +++ .../Umpire/UmpireSettingsView.swift | 85 ++++ .../Views/Navigation/Umpire/UmpireView.swift | 412 +++++------------- PadelClub/Views/Round/LoserRoundView.swift | 3 + PadelClub/Views/Round/RoundView.swift | 1 + .../Views/Shared/SupportButtonView.swift | 11 +- .../Views/Tournament/TournamentView.swift | 23 +- PadelClub/Views/User/AccountView.swift | 55 --- PadelClub/Views/User/LoginView.swift | 2 +- 19 files changed, 654 insertions(+), 492 deletions(-) create mode 100644 PadelClub/Views/Navigation/MyAccount/MyAccountView.swift create mode 100644 PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift create mode 100644 PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index e006acf..67a13b2 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -161,6 +161,15 @@ FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; }; FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; }; FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; }; + FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; }; + FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; }; + FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; }; + FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; }; + FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; }; + FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; }; + FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; }; + FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; }; + FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; FF39B6152DC8825E004E10CE /* PadelClubData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49770202DC25A23005CD239 /* PadelClubData.framework */; }; @@ -1030,6 +1039,9 @@ FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = ""; }; FF30ACEC2E8D700B008B6006 /* PaymentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentService.swift; sourceTree = ""; }; FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRequestButton.swift; sourceTree = ""; }; + FF30AD2F2E92A994008B6006 /* MyAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountView.swift; sourceTree = ""; }; + FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireOptionsView.swift; sourceTree = ""; }; + FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireSettingsView.swift; sourceTree = ""; }; FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; FF39B60F2DC87FEB004E10CE /* PadelClubData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PadelClubData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1594,12 +1606,21 @@ path = SeedData; sourceTree = ""; }; + FF30AD2E2E92A936008B6006 /* MyAccount */ = { + isa = PBXGroup; + children = ( + FF30AD2F2E92A994008B6006 /* MyAccountView.swift */, + ); + path = MyAccount; + sourceTree = ""; + }; FF39719B2B8DE04B004C4E75 /* Navigation */ = { isa = PBXGroup; children = ( FF59FFB62B90EFBF0061EFF9 /* MainView.swift */, FFB0FF662E81B671009EDEAC /* OnboardingView.swift */, FFD783FB2B91B919000F62A6 /* Agenda */, + FF30AD2E2E92A936008B6006 /* MyAccount */, FF3F74FA2B91A04B004CFE0E /* Organizer */, FF3F74FB2B91A060004CFE0E /* Toolbox */, FF3F74FC2B91A06B004CFE0E /* Umpire */, @@ -1683,6 +1704,8 @@ isa = PBXGroup; children = ( FF3F74F52B919E45004CFE0E /* UmpireView.swift */, + FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */, + FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */, FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */, FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */, @@ -2270,11 +2293,13 @@ FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */, FFE103122C366E5900684FC9 /* ImagePickerView.swift in Sources */, FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */, + FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */, FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */, FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, + FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */, @@ -2440,6 +2465,7 @@ FF8E52342DF01D6100099B75 /* EventStatusView.swift in Sources */, FFE8B63C2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */, + FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */, FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, @@ -2542,11 +2568,13 @@ FF4CBF532C996C0600151637 /* ClubsView.swift in Sources */, FF4CBF542C996C0600151637 /* ImagePickerView.swift in Sources */, FF4CBF552C996C0600151637 /* MatchTypeSelectionView.swift in Sources */, + FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */, FF4CBF562C996C0600151637 /* MatchSetupView.swift in Sources */, FF4CBF572C996C0600151637 /* NetworkManager.swift in Sources */, FF4CBF582C996C0600151637 /* EventLinksView.swift in Sources */, FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */, FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */, + FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */, FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */, FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */, @@ -2712,6 +2740,7 @@ FF8E52362DF01D6100099B75 /* EventStatusView.swift in Sources */, FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */, FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, + FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, FFE8B5B52DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, @@ -2792,11 +2821,13 @@ FF70FAD22C90584900129CC2 /* ClubsView.swift in Sources */, FF70FAD32C90584900129CC2 /* ImagePickerView.swift in Sources */, FF70FAD42C90584900129CC2 /* MatchTypeSelectionView.swift in Sources */, + FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */, FF70FAD52C90584900129CC2 /* MatchSetupView.swift in Sources */, FF70FAD62C90584900129CC2 /* NetworkManager.swift in Sources */, FF70FAD72C90584900129CC2 /* EventLinksView.swift in Sources */, FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */, FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */, + FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */, FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */, FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */, @@ -2962,6 +2993,7 @@ FF8E52352DF01D6100099B75 /* EventStatusView.swift in Sources */, FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */, FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */, + FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, FFE8B5B42DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 182c68d..e295549 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -193,11 +193,11 @@ struct PadelClubApp: App { navigationViewModel.selectedTab = .umpire } - if navigationViewModel.umpirePath.isEmpty { - navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login) - } else if navigationViewModel.umpirePath.last! != .login { - navigationViewModel.umpirePath.removeAll() - navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login) + if navigationViewModel.accountPath.isEmpty { + navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login) + } else if navigationViewModel.accountPath.last! != .login { + navigationViewModel.accountPath.removeAll() + navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login) } } }.resume() diff --git a/PadelClub/ViewModel/NavigationViewModel.swift b/PadelClub/ViewModel/NavigationViewModel.swift index e53e2d5..2898190 100644 --- a/PadelClub/ViewModel/NavigationViewModel.swift +++ b/PadelClub/ViewModel/NavigationViewModel.swift @@ -12,7 +12,7 @@ import PadelClubData class NavigationViewModel { var path = NavigationPath() var toolboxPath = NavigationPath() - var umpirePath: [UmpireView.UmpireScreen] = [] + var accountPath: [MyAccountView.AccountScreen] = [] var ongoingPath = NavigationPath() var selectedTab: TabDestination? var agendaDestination: AgendaDestination? = .activity diff --git a/PadelClub/ViewModel/TabDestination.swift b/PadelClub/ViewModel/TabDestination.swift index 429b2fd..edd8e84 100644 --- a/PadelClub/ViewModel/TabDestination.swift +++ b/PadelClub/ViewModel/TabDestination.swift @@ -17,6 +17,7 @@ enum TabDestination: CaseIterable, Identifiable { case tournamentOrganizer case umpire case ongoing + case myAccount var title: String { switch self { @@ -30,6 +31,8 @@ enum TabDestination: CaseIterable, Identifiable { return "Gestionnaire" case .umpire: return "Juge-Arbitre" + case .myAccount: + return "Compte" } } @@ -45,6 +48,8 @@ enum TabDestination: CaseIterable, Identifiable { return "squares.below.rectangle" case .umpire: return "person.bust" + case .myAccount: + return "person.crop.circle" } } } diff --git a/PadelClub/Views/Club/ClubsView.swift b/PadelClub/Views/Club/ClubsView.swift index 85839f8..53cd3fc 100644 --- a/PadelClub/Views/Club/ClubsView.swift +++ b/PadelClub/Views/Club/ClubsView.swift @@ -28,19 +28,19 @@ struct ClubsView: View { var body: some View { List { - #if DEBUG - Section { - RowButtonView("Delete unexisted clubs", action: { - let ids = dataStore.user.clubs - ids.forEach { clubId in - if dataStore.clubs.findById(clubId) == nil { - dataStore.user.clubs.removeAll(where: { $0 == clubId }) - } - } - dataStore.saveUser() - }) - } - #endif +// #if DEBUG +// Section { +// RowButtonView("Delete unexisted clubs", action: { +// let ids = dataStore.user.clubs +// ids.forEach { clubId in +// if dataStore.clubs.findById(clubId) == nil { +// dataStore.user.clubs.removeAll(where: { $0 == clubId }) +// } +// } +// dataStore.saveUser() +// }) +// } +// #endif let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: false) let onlyCreatedClubs : [Club] = dataStore.user.createdClubsObjectsNotFavorite() @@ -106,7 +106,6 @@ struct ClubsView: View { } } .navigationTitle(selection == nil ? "Clubs favoris" : "Choisir un club") - .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .sheet(isPresented: presentClubCreationView) { if let newClub { @@ -129,23 +128,41 @@ struct ClubsView: View { .tint(.master) } .toolbar { - ToolbarItemGroup(placement: .topBarTrailing) { - Button { - presentClubSearchView = true - } label: { - Image(systemName: "magnifyingglass.circle.fill") - .resizable() - .scaledToFit() - .frame(minHeight: 28) + ToolbarItem(placement: .topBarTrailing) { + if #available(iOS 26.0, *) { + Button("Chercher", systemImage: "magnifyingglass") { + presentClubSearchView = true + } + } else { + Button { + presentClubSearchView = true + } label: { + Image(systemName: "magnifyingglass.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 28) + } } - - Button { - newClub = Club.newEmptyInstance() - } label: { - Image(systemName: "plus.circle.fill") - .resizable() - .scaledToFit() - .frame(minHeight: 28) + } + if #available(iOS 26.0, *) { + ToolbarSpacer(placement: .topBarTrailing) + } + + + ToolbarItem(placement: .topBarTrailing) { + if #available(iOS 26.0, *) { + Button("Ajouter", systemImage: "plus") { + newClub = Club.newEmptyInstance() + } + } else { + Button { + newClub = Club.newEmptyInstance() + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 28) + } } } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 7007e66..ac06bfa 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -96,6 +96,9 @@ struct GroupStageView: View { } } + .onAppear(perform: { + groupStage.clearScoreCache() + }) .toolbar { ToolbarItem(placement: .topBarTrailing) { _groupStageMenuView() diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 8caba04..f2c4bce 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -546,7 +546,7 @@ struct ActivityView: View { .frame(width: 100) } } description: { - Text("Aucun événement en cours ou à venir dans votre agenda.") + Text("Aucun événement dans votre agenda.") } actions: { RowButtonView("Créer un nouvel événement") { newTournament = Tournament.newEmptyInstance() diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 003a22a..3fd2bc7 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -80,9 +80,12 @@ struct MainView: View { } .toolbarBackground(.visible, for: .tabBar) - TournamentOrganizerView() - .tabItem(for: .tournamentOrganizer) + UmpireOptionsView() + .tabItem(for: .umpire) .toolbarBackground(.visible, for: .tabBar) +// TournamentOrganizerView() +// .tabItem(for: .tournamentOrganizer) +// .toolbarBackground(.visible, for: .tabBar) OngoingContainerView() .tabItem(for: .ongoing) .badge(self.dataStore.runningMatches().count) @@ -90,10 +93,10 @@ struct MainView: View { ToolboxView() .tabItem(for: .toolbox) .toolbarBackground(.visible, for: .tabBar) - UmpireView() - .tabItem(for: .umpire) - .badge(badgeText) + MyAccountView() + .tabItem(for: .myAccount) .toolbarBackground(.visible, for: .tabBar) + .badge(badgeText) // PadelClubView() // .tabItem(for: .padelClub) } diff --git a/PadelClub/Views/Navigation/MyAccount/MyAccountView.swift b/PadelClub/Views/Navigation/MyAccount/MyAccountView.swift new file mode 100644 index 0000000..0568ea5 --- /dev/null +++ b/PadelClub/Views/Navigation/MyAccount/MyAccountView.swift @@ -0,0 +1,226 @@ +// +// MyAccountView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/03/2024. +// + +import SwiftUI +import CoreLocation +import LeStorage +import StoreKit +import PadelClubData + +struct MyAccountView: View { + + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @EnvironmentObject var dataStore: DataStore + + @State private var showSubscriptions: Bool = false + @State private var showProductIds: Bool = false + + @FocusState private var focusedField: CustomUser.CodingKeys? + +// @State var isConnected: Bool = false + + enum AccountScreen { + case login + } + + var body: some View { + @Bindable var navigation = navigation + NavigationStack(path: $navigation.accountPath) { + List { + Section { + SupportButtonView(supportButtonType: .bugReport, showIcon: true) + } + + PurchaseListView() + + Section { + Button { + self.showSubscriptions = true + } label: { + Label("Les offres", systemImage: "bookmark.fill") + }.simultaneousGesture( + LongPressGesture() + .onEnded { _ in + self.showProductIds = true + } + ) + + .highPriorityGesture( + TapGesture() + .onEnded { _ in + self.showSubscriptions = true + } + ) + + } + + if StoreCenter.main.isAuthenticated { + NavigationLink { + AccountView(user: dataStore.user) { } + } label: { + AccountRowView(userName: dataStore.user.username) + } + } else { + NavigationLink(value: AccountScreen.login) { + AccountRowView(userName: dataStore.user.username) + } + } + + if StoreCenter.main.isAuthenticated { + let onlineRegPaymentMode = dataStore.user.registrationPaymentMode + Section { + LabeledContent { + switch onlineRegPaymentMode { + case .corporate: + Text("Activé") + .bold() + .foregroundStyle(.green) + case .disabled: + Text("Désactivé") + .bold() + case .noFee: + Text("Activé") + .bold() + .foregroundStyle(.green) + case .stripe: + Text("Activé") + .bold() + .foregroundStyle(.green) + } + } label: { + Text("Option 'Paiement en ligne'") + if onlineRegPaymentMode == .corporate { + Text("Mode Padel Club") + .foregroundStyle(.secondary) + } else if onlineRegPaymentMode == .noFee { + Text("Commission Stripe") + .foregroundStyle(.secondary) + } else if onlineRegPaymentMode == .stripe { + Text("Commission Stripe et Padel Club") + .foregroundStyle(.secondary) + } + } + } footer: { + if onlineRegPaymentMode == .disabled { + FooterButtonView("Contactez nous pour activer cette option.") { + let emailTo: String = "support@padelclub.app" + let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)" + if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + .font(.callout) + .multilineTextAlignment(.leading) + } else { + Text("Permet de proposer le paiement de vos tournois en ligne.") + } + } + + Section { + SupportButtonView(supportButtonType: .sharingRequest) + } header: { + Text("Partage et délégation de compte") + } footer: { + Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?") + } + } + + Section { + Link(destination: URLs.appReview.url) { + Text("Partagez vos impressions !") + } + + Link(destination: URLs.instagram.url) { + Text("Compte Instagram PadelClub.app") + } + + Link(destination: URLs.appDescription.url) { + Text("Page de présentation de Padel Club") + } + + } + + Section { + Link(destination: URLs.privacy.url) { + Text("Politique de confidentialité") + } + Link(destination: URLs.eula.url) { + Text("Contrat d'utilisation") + } + } + } + .sheet(isPresented: self.$showSubscriptions, content: { + NavigationStack { + SubscriptionView(isPresented: self.$showSubscriptions) + .environment(\.colorScheme, .light) + } + }) + .sheet(isPresented: self.$showProductIds, content: { + ProductIdsView() + }) + .navigationDestination(for: AccountScreen.self) { screen in + switch screen { + case .login: + LoginView {_ in } + } + } + .navigationTitle("Mon compte") + } + } +} + +struct AccountRowView: View { + @EnvironmentObject var dataStore: DataStore + + var userName: String + var body: some View { + + let isAuthenticated = StoreCenter.main.isAuthenticated + LabeledContent { + if isAuthenticated { + Text(self.userName) + } else if StoreCenter.main.userName != nil { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.logoRed) + } + } label: { + Label("Mon compte", systemImage: "person.fill") + if isAuthenticated && dataStore.user.email.isEmpty == false { + Text(dataStore.user.email) + } + } + } +} + +struct ProductIdsView: View { + + @State var transactions: [StoreKit.Transaction] = [] + + var body: some View { + VStack { + List { + LabeledContent("count", value: String(self.transactions.count)) + ForEach(self.transactions) { transaction in + if #available(iOS 17.2, *) { + if let offer = transaction.offer { + LabeledContent(transaction.productID, value: "\(offer.type)") + } else { + LabeledContent(transaction.productID, value: "no offer") + } + } else { + Text("need ios 17.2") + } + } + }.onAppear { + Task { + self.transactions = Array(Guard.main.purchasedTransactions) + } + } + } + } + +} diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index cc61638..38060f4 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -39,44 +39,6 @@ struct ToolboxView: View { @Bindable var navigation = navigation NavigationStack(path: $navigation.toolboxPath) { List { - Section { - Link(destination: URLs.main.url) { - Text("Accéder à padelclub.app") - } - .contextMenu { - ShareLink(item: URLs.main.url) - } - - SupportButtonView(supportButtonType: .bugReport) - - Link(destination: URLs.appReview.url) { - Text("Partagez vos impressions !") - } - - Link(destination: URLs.instagram.url) { - Text("Compte Instagram PadelClub.app") - } - } - - if self.showDebugViews { - DebugView() - } - - Section { - NavigationLink { - SelectablePlayerListView(isPresented: false, lastDataSource: true) - .toolbar(.hidden, for: .tabBar) - } label: { - Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") - } - - NavigationLink { - RankCalculatorView() - } label: { - Label("Calculateur de points", systemImage: "scalemass") - } - } - Section { NavigationLink { PadelClubView() @@ -86,8 +48,7 @@ struct ToolboxView: View { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) } label: { - Text(_lastDataSourceDate.monthYearFormatted) - Text("Classement mensuel utilisé") + Label(_lastDataSourceDate.monthYearFormatted, systemImage: "calendar.badge.checkmark") } } else { LabeledContent { @@ -95,16 +56,46 @@ struct ToolboxView: View { .tint(.logoRed) } label: { if let _mostRecentDateAvailable { - Text(_mostRecentDateAvailable.monthYearFormatted) + Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "calendar.badge") } else { - Text("Aucun") + Label("Aucun", systemImage: "calendar.badge.exclamationmark") } Text("Classement mensuel disponible") } } } + } header: { + Text("Classement mensuel utilisé") + } + + Section { + NavigationLink { + SelectablePlayerListView(isPresented: false, lastDataSource: true) + .toolbar(.hidden, for: .tabBar) + } label: { + Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") + } + + NavigationLink { + RankCalculatorView() + } label: { + Label("Calculateur de points", systemImage: "scalemass") + } } + Section { + Link(destination: URLs.main.url) { + Label("Padel Club sur le Web", systemImage: "link") + } + .contextMenu { + ShareLink(item: URLs.main.url) + } + + ShareLink(item: URLs.appStore.url) { + Label("Padel Club sur l'App Store", systemImage: "link") + } + } + Section { Link("Guide de la compétition", destination: URLs.padelCompetitionGeneralGuide.url) Link("CDC des tournois", destination: URLs.padelCompetitionSpecificGuide.url) @@ -119,21 +110,10 @@ struct ToolboxView: View { } } - Section { - Link(destination: URLs.appDescription.url) { - Text("Page de présentation de Padel Club") - } - } - - Section { - Link(destination: URLs.privacy.url) { - Text("Politique de confidentialité") - } - Link(destination: URLs.eula.url) { - Text("Contrat d'utilisation") - } + if self.showDebugViews { + DebugView() } - + Section { RowButtonView("Effacer les logs", role: .destructive) { StoreCenter.main.resetLoggingCollections() @@ -163,16 +143,9 @@ struct ToolboxView: View { } } } -// .navigationBarTitleDisplayMode(.large) - // .navigationTitle(TabDestination.toolbox.title) + .navigationBarTitleDisplayMode(.large) + .navigationTitle(TabDestination.toolbox.title) .toolbar { - ToolbarItem(placement: .principal) { - Text(TabDestination.toolbox.title) - .font(.headline) - .onTapGesture { - _handleTitleTap() - } - } ToolbarItem(placement: .topBarLeading) { Link(destination: URLs.appStore.url) { Text("v\(PadelClubApp.appVersion)") @@ -180,15 +153,16 @@ struct ToolboxView: View { } ToolbarItem(placement: .topBarTrailing) { Menu { - ShareLink(item: URLs.appStore.url) { - Label("Lien AppStore", systemImage: "link") - } - ShareLink(item: ZipLog(), preview: .init("Mon archive")) { - Label("Mes données", systemImage: "server.rack") + Text("Archiver mes données") } + + Divider() + + Toggle("Outils avancées", isOn: $showDebugViews) + } label: { - Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly) + LabelOptions() } } } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift b/PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift new file mode 100644 index 0000000..61fa208 --- /dev/null +++ b/PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift @@ -0,0 +1,68 @@ +// +// UmpireOptionsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 06/10/2025. +// + +import SwiftUI +import PadelClubData + +struct UmpireOptionsView: View { + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @State private var umpireOption: UmpireOption? = .umpire + + var body: some View { + @Bindable var navigation = navigation + NavigationStack { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $umpireOption, destinations: UmpireOption.allCases, nilDestinationIsValid: true) + switch umpireOption { + case .none: + UmpireSettingsView() + .navigationTitle("Préférences") + case .umpire: + UmpireView() + .navigationTitle("Juge-Arbitre") + case .clubs: + ClubsView() + } + } + .navigationBarTitleDisplayMode(.large) + .navigationTitle("Juge-Arbitre") + .toolbarBackground(.visible, for: .navigationBar) + } + } +} + +enum UmpireOption: Int, CaseIterable, Identifiable, Selectable, Equatable { + func badgeValue() -> Int? { + nil + } + + func badgeImage() -> PadelClubData.Badge? { + nil + } + + func badgeValueColor() -> Color? { + nil + } + + var id: Int { self.rawValue } + + case umpire + case clubs + + var localizedTitleKey: String { + switch self { + case .umpire: + return "Juge-Arbitre" + case .clubs: + return "Clubs Favoris" + } + } + + func selectionLabel(index: Int) -> String { + localizedTitleKey + } +} diff --git a/PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift b/PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift new file mode 100644 index 0000000..3706385 --- /dev/null +++ b/PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift @@ -0,0 +1,85 @@ +// +// UmpireSettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 06/10/2025. +// + +import SwiftUI +import CoreLocation +import LeStorage +import StoreKit +import PadelClubData + +struct UmpireSettingsView: View { + + @EnvironmentObject var dataStore: DataStore + + var body: some View { + List { + if dataStore.user.canEnableOnlinePayment() { + Section { + if let tournamentTemplate = Tournament.getTemplateTournament() { + NavigationLink { + RegistrationSetupView(tournament: tournamentTemplate) + } label: { + Text("Référence") + Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary) + } + } else { + Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité") + } + } header: { + Text("Inscription et paiement en ligne") + } footer: { + Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne") + } + } + + Section { + @Bindable var user = dataStore.user + Toggle(isOn: $user.disableRankingFederalRuling) { + Text("Désactiver la règle fédéral") + } + .onChange(of: user.disableRankingFederalRuling) { + dataStore.saveUser() + } + } header: { + Text("Règle fédérale classement finale") + } footer: { + Text("Dernier de poule ≠ dernier du tournoi") + } + + Section { + @Bindable var user = dataStore.user + Picker(selection: $user.loserBracketMode) { + ForEach(LoserBracketMode.allCases) { + Text($0.localizedLoserBracketMode()).tag($0) + } + } label: { + Text("Position des perdants") + } + .onChange(of: user.loserBracketMode) { + dataStore.saveUser() + } + } header: { + Text("Matchs de classement") + } + + Section { + NavigationLink { + GlobalSettingsView() + } label: { + Label("Formats de jeu par défaut", systemImage: "megaphone") + } + NavigationLink { + DurationSettingsView() + } label: { + Label("Définir les durées moyennes", systemImage: "deskclock") + } + } footer: { + Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.") + } + } + } +} diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index 4756177..5d8ae4f 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -17,8 +17,6 @@ struct UmpireView: View { @EnvironmentObject var dataStore: DataStore @State private var presentSearchView: Bool = false - @State private var showSubscriptions: Bool = false - @State private var showProductIds: Bool = false @State private var umpireCustomMail: String @State private var umpireCustomPhone: String @State private var umpireCustomContact: String @@ -38,50 +36,9 @@ struct UmpireView: View { _umpireCustomContact = State(wrappedValue: DataStore.shared.user.umpireCustomContact ?? "") } - enum UmpireScreen { - case login - } - var body: some View { - @Bindable var navigation = navigation - NavigationStack(path: $navigation.umpirePath) { - List { - - PurchaseListView() - - Section { - Button { - self.showSubscriptions = true - } label: { - Label("Les offres", systemImage: "bookmark.fill") - }.simultaneousGesture( - LongPressGesture() - .onEnded { _ in - self.showProductIds = true - } - ) - - .highPriorityGesture( - TapGesture() - .onEnded { _ in - self.showSubscriptions = true - } - ) - - } - - if StoreCenter.main.isAuthenticated { - NavigationLink { - AccountView(user: dataStore.user) { } - } label: { - AccountRowView(userName: dataStore.user.username) - } - } else { - NavigationLink(value: UmpireScreen.login) { - AccountRowView(userName: dataStore.user.username) - } - } - + List { + if StoreCenter.main.isAuthenticated { let currentPlayerData = dataStore.user.currentPlayerData() Section { if let reason = licenseMessage { @@ -90,11 +47,11 @@ struct UmpireView: View { if let currentPlayerData { //todo palmares ImportedPlayerView(player: currentPlayerData, showProgression: true) -// NavigationLink { -// -// } label: { -// ImportedPlayerView(player: currentPlayerData) -// } + // NavigationLink { + // + // } label: { + // ImportedPlayerView(player: currentPlayerData) + // } } else { RowButtonView("Ma fiche joueur", systemImage: "person.bust") { presentSearchView = true @@ -106,6 +63,8 @@ struct UmpireView: View { .autocorrectionDisabled() .frame(maxWidth: .infinity) } + } header: { + Text("Mes infos licencié") } footer: { if dataStore.user.licenceId == nil { Text("Si vous avez participé à un tournoi dans les 12 derniers mois, Padel Club peut vous retrouver.") @@ -124,7 +83,7 @@ struct UmpireView: View { self.licenseMessage = nil self.dataStore.saveUser() } - + } label: { Text("options") .foregroundStyle(Color.master) @@ -132,231 +91,119 @@ struct UmpireView: View { } } } + _customUmpireView() Section { - NavigationLink { - ClubsView() - } label: { - LabeledContent { - Text(dataStore.user.clubs.count.formatted()) - } label: { - Label("Clubs favoris", systemImage: "house.and.flag") - } + @Bindable var user = dataStore.user + if dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone { + Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed) } - } footer: { - Text("Il s'agit des clubs qui sont utilisés pour récupérer les tournois tenup.") - } - -// Section { -// NavigationLink { -// UmpireStatisticView() -// } label: { -// Text("Statistiques de participations") -// } -// } -// - if StoreCenter.main.isAuthenticated { - _customUmpireView() - Section { - @Bindable var user = dataStore.user - if dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone { - Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed) - } - - Toggle(isOn: $user.hideUmpireMail) { - Text("Masquer l'email") - } - Toggle(isOn: $user.hideUmpirePhone) { - Text("Masquer le téléphone") - } - - } footer: { - Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.") + Toggle(isOn: $user.hideUmpireMail) { + Text("Masquer l'email") } - } - - if dataStore.user.canEnableOnlinePayment() { - Section { - if let tournamentTemplate = Tournament.getTemplateTournament() { - NavigationLink { - RegistrationSetupView(tournament: tournamentTemplate) - } label: { - Text("Référence") - Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary) - } - } else { - Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité") - } - } header: { - Text("Inscription et paiement en ligne") - } footer: { - Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne") + Toggle(isOn: $user.hideUmpirePhone) { + Text("Masquer le téléphone") } - } - - Section { - @Bindable var user = dataStore.user - Toggle(isOn: $user.disableRankingFederalRuling) { - Text("Désactiver la règle fédéral") - } - .onChange(of: user.disableRankingFederalRuling) { - dataStore.saveUser() - } - } header: { - Text("Règle fédérale classement finale") + } footer: { - Text("Dernier de poule ≠ dernier du tournoi") + Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.") } - - Section { - @Bindable var user = dataStore.user - Picker(selection: $user.loserBracketMode) { - ForEach(LoserBracketMode.allCases) { - Text($0.localizedLoserBracketMode()).tag($0) - } - } label: { - Text("Position des perdants") - } - .onChange(of: user.loserBracketMode) { - dataStore.saveUser() + } + } + .overlay(content: { + if StoreCenter.main.isAuthenticated == false { + ContentUnavailableView { + Label("Aucun compte", systemImage: "person.crop.circle.badge.exclamationmark") + } description: { + Text("Créer un compte Padel Club pour personnaliser vos informations de Juge-Arbitre") + } actions: { + RowButtonView("Créer un compte") { + _openCreateAccountView() } - } header: { - Text("Matchs de classement") } - - Section { - NavigationLink { - GlobalSettingsView() - } label: { - Label("Formats de jeu par défaut", systemImage: "megaphone") - } - NavigationLink { - DurationSettingsView() - } label: { - Label("Définir les durées moyennes", systemImage: "deskclock") + } + }) + .onChange(of: StoreCenter.main.userId) { + license = dataStore.user.licenceId ?? "" + licenseMessage = nil + } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil } - } footer: { - Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.") } - -// Section { -// Text("Tenup ID") -// } -// -// Section { -// Text("Tournois") -// } -// -// Section { -// NavigationLink { -// -// } label: { -// Text("Favori") -// } -// NavigationLink { -// -// } label: { -// Text("Black list") -// } -// } - } - .onChange(of: StoreCenter.main.userId) { - license = dataStore.user.licenceId ?? "" - licenseMessage = nil } - .navigationTitle("Juge-Arbitre") - .navigationBarBackButtonHidden(focusedField != nil) - .toolbar(content: { - if focusedField != nil { - ToolbarItem(placement: .topBarLeading) { - Button("Annuler", role: .cancel) { - focusedField = nil + }) + .toolbar { + if focusedField != nil { + ToolbarItemGroup(placement: .keyboard) { + if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { + Button("Effacer") { + _deleteUmpireMail() } - } - } - }) - .toolbar { - if focusedField != nil { - ToolbarItemGroup(placement: .keyboard) { - if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { - Button("Effacer") { - _deleteUmpireMail() - } - .buttonStyle(.borderedProminent) - } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { - Button("Effacer") { - _deleteUmpirePhone() - } - .buttonStyle(.borderedProminent) - } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { - Button("Effacer") { - _deleteUmpireContact() - } - .buttonStyle(.borderedProminent) + .buttonStyle(.borderedProminent) + } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { + Button("Effacer") { + _deleteUmpirePhone() } - Spacer() - Button("Valider") { - focusedField = nil + .buttonStyle(.borderedProminent) + } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { + Button("Effacer") { + _deleteUmpireContact() } .buttonStyle(.borderedProminent) } + Spacer() + Button("Valider") { + focusedField = nil + } + .buttonStyle(.borderedProminent) } } - .onChange(of: [dataStore.user.umpireCustomMail, dataStore.user.umpireCustomPhone, dataStore.user.umpireCustomContact]) { - self.dataStore.saveUser() - } - .onChange(of: [dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone]) { - self.dataStore.saveUser() - } - .onChange(of: focusedField) { old, new in - if old == ._umpireCustomMail { - _confirmUmpireMail() - } else if old == ._umpireCustomPhone { - _confirmUmpirePhone() - } else if old == ._umpireCustomContact { - _confirmUmpireContact() - } else if old == ._licenceId { - _confirmlicense() - } + } + .onChange(of: [dataStore.user.umpireCustomMail, dataStore.user.umpireCustomPhone, dataStore.user.umpireCustomContact]) { + self.dataStore.saveUser() + } + .onChange(of: [dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone]) { + self.dataStore.saveUser() + } + .onChange(of: focusedField) { old, new in + if old == ._umpireCustomMail { + _confirmUmpireMail() + } else if old == ._umpireCustomPhone { + _confirmUmpirePhone() + } else if old == ._umpireCustomContact { + _confirmUmpireContact() + } else if old == ._licenceId { + _confirmlicense() } - .sheet(isPresented: self.$showSubscriptions, content: { - NavigationStack { - SubscriptionView(isPresented: self.$showSubscriptions) - .environment(\.colorScheme, .light) - } - }) - .sheet(isPresented: self.$showProductIds, content: { - ProductIdsView() - }) - .sheet(isPresented: $presentSearchView) { - let user = dataStore.user - NavigationStack { - SelectablePlayerListView(allowSelection: 1, searchField: user.firstName + " " + user.lastName, playerSelectionAction: { players in - if let player = players.first { - if user.clubsObjects().contains(where: { $0.code == player.clubCode }) == false { - let userClub = Club.findOrCreate(name: player.clubName!, code: player.clubCode) - if userClub.hasBeenCreated(by: StoreCenter.main.userId) { - dataStore.clubs.addOrUpdate(instance: userClub) - } - user.setUserClub(userClub) + } + .sheet(isPresented: $presentSearchView) { + let user = dataStore.user + NavigationStack { + SelectablePlayerListView(allowSelection: 1, searchField: user.firstName + " " + user.lastName, playerSelectionAction: { players in + if let player = players.first { + if user.clubsObjects().contains(where: { $0.code == player.clubCode }) == false { + let userClub = Club.findOrCreate(name: player.clubName!, code: player.clubCode) + if userClub.hasBeenCreated(by: StoreCenter.main.userId) { + dataStore.clubs.addOrUpdate(instance: userClub) } - self._updateUserLicense(license: player.license?.computedLicense) + user.setUserClub(userClub) } - }) - } - .task { - do { - try await dataStore.clubs.loadDataFromServerIfAllowed() - } catch { - Logger.error(error) + self._updateUserLicense(license: player.license?.computedLicense) } - } + }) } - .navigationDestination(for: UmpireScreen.self) { screen in - switch screen { - case .login: - LoginView {_ in } + .task { + do { + try await dataStore.clubs.loadDataFromServerIfAllowed() + } catch { + Logger.error(error) } } } @@ -374,6 +221,10 @@ struct UmpireView: View { } + private func _openCreateAccountView() { + navigation.selectedTab = .myAccount + } + private func _updateUserLicense(license: String?) { guard let license else { return } @@ -481,67 +332,10 @@ struct UmpireView: View { } } } header: { - Text("Juge-arbitre") + Text("Mes infos juge-arbitre") } footer: { Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") } } } - -struct AccountRowView: View { - @EnvironmentObject var dataStore: DataStore - - var userName: String - var body: some View { - - let isAuthenticated = StoreCenter.main.isAuthenticated - LabeledContent { - if isAuthenticated { - Text(self.userName) - } else if StoreCenter.main.userName != nil { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(.logoRed) - } - } label: { - Label("Mon compte", systemImage: "person.fill") - if isAuthenticated && dataStore.user.email.isEmpty == false { - Text(dataStore.user.email) - } - } - } -} - -struct ProductIdsView: View { - - @State var transactions: [StoreKit.Transaction] = [] - - var body: some View { - VStack { - List { - LabeledContent("count", value: String(self.transactions.count)) - ForEach(self.transactions) { transaction in - if #available(iOS 17.2, *) { - if let offer = transaction.offer { - LabeledContent(transaction.productID, value: "\(offer.type)") - } else { - LabeledContent(transaction.productID, value: "no offer") - } - } else { - Text("need ios 17.2") - } - } - }.onAppear { - Task { - self.transactions = Array(Guard.main.purchasedTransactions) - } - } - } - } - - -} - -//#Preview { -// UmpireView() -//} diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index 235c7c1..56b8077 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -103,6 +103,9 @@ struct LoserRoundView: View { } .onAppear(perform: { updateDisplayedMatches() + self.loserBracket.rounds.forEach({ round in + round.invalidateCache() + }) }) .onChange(of: isEditingTournamentSeed.wrappedValue) { updateDisplayedMatches() diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index b97768b..904880b 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -29,6 +29,7 @@ struct RoundView: View { func _refreshRound() { self.upperRound.playedMatches = self.upperRound.round.playedMatches() + self.upperRound.round.invalidateCache() } init(upperRound: UpperRound) { diff --git a/PadelClub/Views/Shared/SupportButtonView.swift b/PadelClub/Views/Shared/SupportButtonView.swift index eb20ba5..94af878 100644 --- a/PadelClub/Views/Shared/SupportButtonView.swift +++ b/PadelClub/Views/Shared/SupportButtonView.swift @@ -90,8 +90,15 @@ struct SupportButtonView: View { _zip() } case .bugReport: - Button("Signaler un problème") { - _zip() + if showIcon { + Button("Signaler un problème", systemImage: "square.and.pencil") { + _zip() + } + .labelStyle(.titleAndIcon) + } else { + Button("Signaler un problème") { + _zip() + } } } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index c15760d..350712d 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -236,14 +236,14 @@ struct TournamentView: View { } #endif - if presentationContext == .agenda { - Button { - navigation.openTournamentInOrganizer(tournament) - } label: { - Label("Gestionnaire", systemImage: "pin") - } - Divider() - } +// if presentationContext == .agenda { +// Button { +// navigation.openTournamentInOrganizer(tournament) +// } label: { +// Label("Gestionnaire", systemImage: "pin") +// } +// Divider() +// } NavigationLink(value: Screen.event) { Label("Événement", systemImage: "info") @@ -277,10 +277,9 @@ struct TournamentView: View { Label(tournament.isFree() ? "Présence" : "Encaissement", systemImage: tournament.isFree() ? "person.crop.circle.badge.checkmark" : "eurosign.circle") } -// NavigationLink(value: Screen.statistics) { -// Label("Statistiques", systemImage: "123.rectangle") -// } -// + NavigationLink(value: Screen.statistics) { + Label("Statistiques", systemImage: "123.rectangle") + } NavigationLink(value: Screen.rankings) { LabeledContent { diff --git a/PadelClub/Views/User/AccountView.swift b/PadelClub/Views/User/AccountView.swift index e47d216..1cfa234 100644 --- a/PadelClub/Views/User/AccountView.swift +++ b/PadelClub/Views/User/AccountView.swift @@ -24,61 +24,6 @@ struct AccountView: View { PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item)) } #endif - - let onlineRegPaymentMode = dataStore.user.registrationPaymentMode - Section { - LabeledContent { - switch onlineRegPaymentMode { - case .corporate: - Text("Activé") - .bold() - .foregroundStyle(.green) - case .disabled: - Text("Désactivé") - .bold() - case .noFee: - Text("Activé") - .bold() - .foregroundStyle(.green) - case .stripe: - Text("Activé") - .bold() - .foregroundStyle(.green) - } - } label: { - Text("Option 'Paiement en ligne'") - if onlineRegPaymentMode == .corporate { - Text("Mode Padel Club") - .foregroundStyle(.secondary) - } else if onlineRegPaymentMode == .noFee { - Text("Commission Stripe") - .foregroundStyle(.secondary) - } else if onlineRegPaymentMode == .stripe { - Text("Commission Stripe et Padel Club") - .foregroundStyle(.secondary) - } - } - } footer: { - if onlineRegPaymentMode == .disabled { - FooterButtonView("Contactez nous pour activer cette option.") { - let emailTo: String = "support@padelclub.app" - let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)" - if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - .font(.callout) - .multilineTextAlignment(.leading) - } else { - Text("Permet de proposer le paiement de vos tournois en ligne.") - } - } - - Section { - Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?") - SupportButtonView(supportButtonType: .sharingRequest) - } - Section { NavigationLink("Changer de mot de passe") { ChangePasswordView() diff --git a/PadelClub/Views/User/LoginView.swift b/PadelClub/Views/User/LoginView.swift index 3a15eef..7959230 100644 --- a/PadelClub/Views/User/LoginView.swift +++ b/PadelClub/Views/User/LoginView.swift @@ -207,7 +207,7 @@ struct LoginView: View { dataStore.appSettingsStorage.write() } self.handler(user) - navigation.umpirePath.removeAll() + navigation.accountPath.removeAll() } catch { self.isLoading = false self.errorText = ErrorUtils.message(error: error) From e466543628de621b55f01baaebac866ec15f3bab Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 7 Oct 2025 09:09:18 +0200 Subject: [PATCH 45/64] add sharing back --- PadelClub/Views/Tournament/TournamentView.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 350712d..a94689c 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -277,9 +277,9 @@ struct TournamentView: View { Label(tournament.isFree() ? "Présence" : "Encaissement", systemImage: tournament.isFree() ? "person.crop.circle.badge.checkmark" : "eurosign.circle") } - NavigationLink(value: Screen.statistics) { - Label("Statistiques", systemImage: "123.rectangle") - } +// NavigationLink(value: Screen.statistics) { +// Label("Statistiques", systemImage: "123.rectangle") +// } NavigationLink(value: Screen.rankings) { LabeledContent { @@ -303,11 +303,11 @@ struct TournamentView: View { Label("Imprimer", systemImage: "printer") } -// if self.tournament.sharing == nil { -// NavigationLink(value: Screen.share) { -// Label("Partager", systemImage: "square.and.arrow.up") -// } -// } + if self.tournament.sharing == nil { + NavigationLink(value: Screen.share) { + Label("Partager", systemImage: "square.and.arrow.up") + } + } Divider() From fbd2a083b1e676240704299cd371f729a8b49b17 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 7 Oct 2025 10:12:13 +0200 Subject: [PATCH 46/64] fix wording --- PadelClub/Views/Navigation/Toolbox/ToolboxView.swift | 2 +- PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 38060f4..ac4f7db 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -85,7 +85,7 @@ struct ToolboxView: View { Section { Link(destination: URLs.main.url) { - Label("Padel Club sur le Web", systemImage: "link") + Label("Padel Club sur le Web", systemImage: "square.and.arrow.up") } .contextMenu { ShareLink(item: URLs.main.url) diff --git a/PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift b/PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift index 3706385..f7b6274 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift @@ -39,13 +39,13 @@ struct UmpireSettingsView: View { Section { @Bindable var user = dataStore.user Toggle(isOn: $user.disableRankingFederalRuling) { - Text("Désactiver la règle fédéral") + Text("Désactiver la règle fédérale") } .onChange(of: user.disableRankingFederalRuling) { dataStore.saveUser() } } header: { - Text("Règle fédérale classement finale") + Text("Règle fédérale classement final") } footer: { Text("Dernier de poule ≠ dernier du tournoi") } From 44f9ab1b1cdc4c156d4085549cb20ecd03443b1d Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 7 Oct 2025 11:11:35 +0200 Subject: [PATCH 47/64] fix agenda --- PadelClub/ViewModel/AgendaDestination.swift | 2 +- PadelClub/Views/Navigation/MainView.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index e025a0b..1862235 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -25,7 +25,7 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { var localizedTitleKey: String { switch self { case .activity: - return "En cours" + return "À venir" case .history: return "Terminé" case .tenup: diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 3fd2bc7..cc90f20 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -80,16 +80,16 @@ struct MainView: View { } .toolbarBackground(.visible, for: .tabBar) + OngoingContainerView() + .tabItem(for: .ongoing) + .badge(self.dataStore.runningMatches().count) + .toolbarBackground(.visible, for: .tabBar) UmpireOptionsView() .tabItem(for: .umpire) .toolbarBackground(.visible, for: .tabBar) // TournamentOrganizerView() // .tabItem(for: .tournamentOrganizer) // .toolbarBackground(.visible, for: .tabBar) - OngoingContainerView() - .tabItem(for: .ongoing) - .badge(self.dataStore.runningMatches().count) - .toolbarBackground(.visible, for: .tabBar) ToolboxView() .tabItem(for: .toolbox) .toolbarBackground(.visible, for: .tabBar) From cc533081ac0049e86e9e08602b5e55bb5b517361 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 7 Oct 2025 11:11:52 +0200 Subject: [PATCH 48/64] build 2 --- 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 67a13b2..4298dac 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3205,7 +3205,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\""; @@ -3253,7 +3253,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 0f148528585b515b3d83373c5f781f2f91d1d2fc Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 7 Oct 2025 11:35:14 +0200 Subject: [PATCH 49/64] fix icons --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Views/Navigation/Toolbox/ToolboxView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 4298dac..73e77a8 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3205,7 +3205,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\""; @@ -3253,7 +3253,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/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index ac4f7db..fde36a3 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -85,14 +85,14 @@ struct ToolboxView: View { Section { Link(destination: URLs.main.url) { - Label("Padel Club sur le Web", systemImage: "square.and.arrow.up") + Label("Padel Club sur le Web", systemImage: "link") } .contextMenu { ShareLink(item: URLs.main.url) } ShareLink(item: URLs.appStore.url) { - Label("Padel Club sur l'App Store", systemImage: "link") + Label("Padel Club sur l'App Store", systemImage: "square.and.arrow.up") } } From ef28a98f201dc8b8abe360aac61b1474e0437fe9 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 8 Oct 2025 11:52:10 +0200 Subject: [PATCH 50/64] add format selection to horaire and format view --- PadelClub/Views/Components/FortuneWheelView.swift | 1 + PadelClub/Views/Planning/PlanningSettingsView.swift | 12 ++++++++++++ .../TournamentMatchFormatsSettingsView.swift | 1 + 3 files changed, 14 insertions(+) diff --git a/PadelClub/Views/Components/FortuneWheelView.swift b/PadelClub/Views/Components/FortuneWheelView.swift index f880f35..e29fbd5 100644 --- a/PadelClub/Views/Components/FortuneWheelView.swift +++ b/PadelClub/Views/Components/FortuneWheelView.swift @@ -220,6 +220,7 @@ struct FortuneWheelContainerView: View { .frame(width: 20, height: 20) .rotationEffect(.degrees(180)) } + .frame(maxWidth: 600, maxHeight: 600) .onAppear { if autoMode { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index db24db4..8e1ecd7 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -56,6 +56,18 @@ struct PlanningSettingsView: View { DatePicker(selection: $tournament.startDate) { Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized).lineLimit(1) } + + NavigationLink { + TournamentMatchFormatsSettingsView() + .environment(tournament) + } label: { + VStack(alignment: .leading) { + Text(tournament.formatSummary()) + Text("Formats par défaut").foregroundStyle(.secondary).font(.caption) + } + } + + LabeledContent { StepperView(count: $tournament.dayDuration, minimum: 1) } label: { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift index bc3906d..d4280a0 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift @@ -73,6 +73,7 @@ struct TournamentMatchFormatsSettingsView: View { .deferredRendering(for: .seconds(2)) } } + .navigationTitle("Formats") } private func _confirmOrSave() { From 4badce1a063a00392bc94c98096a56d0ad8731d8 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 9 Oct 2025 15:57:53 +0200 Subject: [PATCH 51/64] couple of fixes add the ability to set a planning from a previous tournament --- PadelClub.xcodeproj/project.pbxproj | 16 ++ PadelClub/Views/Planning/PlanningView.swift | 3 +- PadelClub/Views/Team/EditingTeamView.swift | 20 +- .../ConsolationTournamentImportView.swift | 2 +- .../Views/Tournament/Screen/AddTeamView.swift | 1 + .../Screen/Components/HeadManagerView.swift | 43 ++++ .../Components/TournamentSelectorView.swift | 51 +++++ .../Screen/TableStructureView.swift | 190 ++++++++++++++++-- .../Shared/TournamentCellView.swift | 7 + .../Views/Tournament/TournamentView.swift | 2 +- 10 files changed, 307 insertions(+), 28 deletions(-) create mode 100644 PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift create mode 100644 PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 73e77a8..629c7aa 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -764,6 +764,12 @@ FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; + FFC2DB202E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; }; + FFC2DB212E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; }; + FFC2DB222E97D00300869317 /* TournamentSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */; }; + FFC2DB242E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; }; + FFC2DB252E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; }; + FFC2DB262E97DD0A00869317 /* HeadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */; }; FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */; }; FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; }; FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; }; @@ -1167,6 +1173,8 @@ FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = ""; }; + FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSelectorView.swift; sourceTree = ""; }; + FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadManagerView.swift; sourceTree = ""; }; FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundView.swift; sourceTree = ""; }; FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = ""; }; FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = ""; }; @@ -1823,6 +1831,8 @@ FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */, FFE8B5BE2DAA325400BDE966 /* RefundResultsView.swift */, FFCF76062C3BE9BC006C8C3D /* CloseDatePicker.swift */, + FFC2DB1F2E97D00300869317 /* TournamentSelectorView.swift */, + FFC2DB232E97DD0A00869317 /* HeadManagerView.swift */, ); path = Components; sourceTree = ""; @@ -2371,6 +2381,7 @@ FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, + FFC2DB252E97DD0A00869317 /* HeadManagerView.swift in Sources */, FF3A74332D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */, FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, @@ -2433,6 +2444,7 @@ FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, + FFC2DB212E97D00300869317 /* TournamentSelectorView.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, @@ -2646,6 +2658,7 @@ FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */, FF3A74322D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF4CBFA12C996C0600151637 /* LoserRoundSettingsView.swift in Sources */, + FFC2DB262E97DD0A00869317 /* HeadManagerView.swift in Sources */, FF4CBFA22C996C0600151637 /* Persistence.swift in Sources */, FF4CBFA32C996C0600151637 /* CloseDatePicker.swift in Sources */, FF4CBFA42C996C0600151637 /* BarButtonView.swift in Sources */, @@ -2708,6 +2721,7 @@ FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */, FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */, FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */, + FFC2DB202E97D00300869317 /* TournamentSelectorView.swift in Sources */, FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */, FFE8B63B2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */, @@ -2899,6 +2913,7 @@ FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */, FF3A74342D37DCF2007E3032 /* InscriptionLegendView.swift in Sources */, FF70FB202C90584900129CC2 /* LoserRoundSettingsView.swift in Sources */, + FFC2DB242E97DD0A00869317 /* HeadManagerView.swift in Sources */, FF70FB212C90584900129CC2 /* Persistence.swift in Sources */, FF70FB222C90584900129CC2 /* CloseDatePicker.swift in Sources */, FF70FB232C90584900129CC2 /* BarButtonView.swift in Sources */, @@ -2961,6 +2976,7 @@ FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */, FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */, FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */, + FFC2DB222E97D00300869317 /* TournamentSelectorView.swift in Sources */, FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */, FFE8B63A2DACEAED00BDE966 /* ConfigurationService.swift in Sources */, FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */, diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index f6f7a34..c4fe24e 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -315,8 +315,9 @@ struct PlanningView: View { matchesToUpdate = matches.filter({ selectedIds.contains($0.stringId) }) } label: { Text("Modifier") + .frame(maxWidth: .infinity) } - .buttonStyle(.borderless) + .buttonStyle(.borderedProminent) .disabled(selectedIds.isEmpty) } } diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 6d10c35..ab9b3f9 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -88,6 +88,19 @@ struct EditingTeamView: View { team.uniqueRandomIndex = 0 } + var hasRegisteredOnline: Binding { + Binding { + team.hasRegisteredOnline() + } set: { hasRegisteredOnline in + let players = team.players() + players.forEach { player in + player.registeredOnline = hasRegisteredOnline + } + + tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) + } + } + var body: some View { List { Section { @@ -126,11 +139,10 @@ struct EditingTeamView: View { } .headerProminence(.increased) - if team.hasRegisteredOnline() || team.hasPaidOnline() { + if team.hasRegisteredOnline() || team.hasPaidOnline() || tournament.enableOnlineRegistration { Section { - LabeledContent { - Text(team.hasRegisteredOnline() ? "Oui" : "Non") - } label: { + + Toggle(isOn: hasRegisteredOnline) { Text("Inscrits en ligne") } diff --git a/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift b/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift index 1dd587e..bb0d8c9 100644 --- a/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift +++ b/PadelClub/Views/Tournament/ConsolationTournamentImportView.swift @@ -101,7 +101,7 @@ struct ConsolationTournamentImportView: View { Picker(selection: $selectedTournament) { Text("Aucun tournoi").tag(nil as Tournament?) ForEach(tournaments) { tournament in - TournamentCellView(tournament: tournament).tag(tournament) + TournamentCellView(tournament: tournament, displayContext: .selection).tag(tournament) } } label: { if selectedTournament == nil { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 3686ac1..1fa1dc5 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -231,6 +231,7 @@ struct AddTeamView: View { .disabled(_limitPlayerCount()) .foregroundStyle(.master) .labelStyle(.titleAndIcon) + .frame(maxWidth: .infinity) .buttonBorderShape(.capsule) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift new file mode 100644 index 0000000..926340a --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -0,0 +1,43 @@ +// +// HeadManagerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 09/10/2025. +// + +import SwiftUI +import LeStorage +import PadelClubData + +struct HeadManagerView: View { + + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject private var dataStore: DataStore + @Environment(\.dismiss) var dismiss + @Binding var initialSeedRound: Int + @Binding var initialSeedCount: Int + + var body: some View { + List { + Section { + Picker(selection: $initialSeedRound) { + ForEach((0...10)) { + Text(RoundRule.roundName(fromRoundIndex: $0)).tag($0) + } + } label: { + Text("Premier tour") + } + .onChange(of: initialSeedRound) { + initialSeedCount = RoundRule.numberOfMatches(forRoundIndex: initialSeedRound) + } + + LabeledContent { + StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)) + } label: { + Text("Têtes de série") + } + } + } + .navigationTitle("Têtes de série") + } +} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift new file mode 100644 index 0000000..bc69b46 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentSelectorView.swift @@ -0,0 +1,51 @@ +// +// TournamentSelectorView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 09/10/2025. +// + +import SwiftUI +import LeStorage +import PadelClubData + +struct TournamentSelectorView: View { + @Binding var selectedTournament: Tournament? + @Environment(Tournament.self) private var tournament: Tournament + @EnvironmentObject private var dataStore: DataStore + @Environment(\.dismiss) var dismiss + + @State private var allTournaments: Bool = false + + var tournaments: [Tournament] { + dataStore.tournaments.filter({ $0.isDeleted == false && $0.id != tournament.id && (allTournaments || ($0.endDate != nil && $0.level == tournament.level && $0.category == tournament.category && $0.age == tournament.age)) }).sorted(by: \.startDate, order: .descending) + } + + var body: some View { + List { + Picker(selection: $selectedTournament) { + Text("Aucun tournoi").tag(nil as Tournament?) + ForEach(tournaments) { tournament in + TournamentCellView(tournament: tournament, displayContext: .selection).tag(tournament) + } + } label: { + Text("Sélection d'un tournoi") + } + .labelsHidden() + .pickerStyle(.inline) + } + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Toggle("Tous les tournois", isOn: $allTournaments) + } label: { + Label("Filtre", systemImage: "line.3.horizontal.decrease") + } + } + }) + .navigationTitle("Sélection d'un tournoi") + .onChange(of: selectedTournament) { oldValue, newValue in + dismiss() + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 2033927..f7ff58a 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -10,7 +10,7 @@ import LeStorage import PadelClubData struct TableStructureView: View { - @Environment(Tournament.self) private var tournament: Tournament + var tournament: Tournament @EnvironmentObject private var dataStore: DataStore @Environment(\.dismiss) var dismiss @State private var presentRefreshStructureWarning: Bool = false @@ -24,7 +24,10 @@ struct TableStructureView: View { @State private var buildWildcards: Bool = true @FocusState private var stepperFieldIsFocused: Bool @State private var confirmReset: Bool = false - + @State private var selectedTournament: Tournament? + @State private var initialSeedCount: Int = 0 + @State private var initialSeedRound: Int = 0 + func displayWarning() -> Bool { let unsortedTeamsCount = tournament.unsortedTeamsCount() return tournament.shouldWarnOnlineRegistrationUpdates() && teamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount) @@ -60,9 +63,18 @@ struct TableStructureView: View { var tsPure: Int { max(teamCount - groupStageCount * teamsPerGroupStage, 0) } - - - @ViewBuilder + + init(tournament: Tournament) { + self.tournament = tournament + _teamCount = .init(wrappedValue: tournament.teamCount) + _groupStageCount = .init(wrappedValue: tournament.groupStageCount) + _teamsPerGroupStage = .init(wrappedValue: tournament.teamsPerGroupStage) + _qualifiedPerGroupStage = .init(wrappedValue: tournament.qualifiedPerGroupStage) + _groupStageAdditionalQualified = .init(wrappedValue: tournament.groupStageAdditionalQualified) + _initialSeedCount = .init(wrappedValue: tournament.initialSeedCount) + _initialSeedRound = .init(wrappedValue: tournament.initialSeedRound) + } + var body: some View { List { if displayWarning() { @@ -79,9 +91,33 @@ struct TableStructureView: View { } label: { Text("Préréglage") } + .disabled(selectedTournament != nil) + + NavigationLink { + TournamentSelectorView(selectedTournament: $selectedTournament) + .environment(tournament) + } label: { + if let selectedTournament { + TournamentCellView(tournament: selectedTournament, displayContext: .selection) + } else { + Text("À partir d'un tournoi existant") + } + } + + NavigationLink { + HeadManagerView(initialSeedRound: $initialSeedRound, initialSeedCount: $initialSeedCount) + .environment(tournament) + } label: { + Text("Configuration des têtes de série") + } + .disabled(selectedTournament != nil) + } footer: { Text(structurePreset.localizedDescriptionStructurePresetTitle()) } + .onChange(of: selectedTournament) { + _updatePreset() + } .onChange(of: structurePreset) { _updatePreset() } @@ -226,7 +262,7 @@ struct TableStructureView: View { LabeledContent { Text(tsPure.formatted()) } label: { - Text("Nombre de têtes de série") + Text("Équipes à placer en tableau") if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) { Text("Attention !").foregroundStyle(.red) @@ -235,7 +271,12 @@ struct TableStructureView: View { LabeledContent { Text(tf.formatted()) } label: { - Text("Équipes en tableau final") + Text("Effectif") + } + LabeledContent { + Text(RoundRule.teamsInFirstRound(forTeams: tf).formatted()) + } label: { + Text("Dimension") } } else { LabeledContent { @@ -266,7 +307,7 @@ struct TableStructureView: View { } } - if tournament.rounds().isEmpty { + if tournament.rounds().isEmpty && tournament.state() == .build { Section { RowButtonView("Ajouter un tableau", role: .destructive) { tournament.buildBracket(minimalBracketTeamCount: 4) @@ -312,13 +353,6 @@ struct TableStructureView: View { } } .toolbarBackground(.visible, for: .navigationBar) - .onAppear { - teamCount = tournament.teamCount - groupStageCount = tournament.groupStageCount - teamsPerGroupStage = tournament.teamsPerGroupStage - qualifiedPerGroupStage = tournament.qualifiedPerGroupStage - groupStageAdditionalQualified = tournament.groupStageAdditionalQualified - } .onChange(of: teamCount) { if teamCount != tournament.teamCount { updatedElements.insert(.teamCount) @@ -490,12 +524,79 @@ struct TableStructureView: View { tournament.groupStageAdditionalQualified = groupStageAdditionalQualified if rebuildEverything { + if let selectedTournament { + tournament.matchFormat = selectedTournament.matchFormat + tournament.groupStageMatchFormat = selectedTournament.groupStageMatchFormat + tournament.loserBracketMatchFormat = selectedTournament.loserBracketMatchFormat + tournament.initialSeedRound = selectedTournament.initialSeedRound + tournament.initialSeedCount = selectedTournament.initialSeedCount + } else { + tournament.initialSeedRound = initialSeedRound + tournament.initialSeedCount = initialSeedCount + } tournament.removeWildCards() if structurePreset.hasWildcards(), buildWildcards { tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket) tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage) } tournament.deleteAndBuildEverything(preset: structurePreset) + + if let selectedTournament { + let oldTournamentStart = selectedTournament.startDate + let newTournamentStart = tournament.startDate + let calendar = Calendar.current + let oldComponents = calendar.dateComponents([.hour, .minute, .second], from: oldTournamentStart) + var newComponents = calendar.dateComponents([.year, .month, .day], from: newTournamentStart) + + newComponents.hour = oldComponents.hour + newComponents.minute = oldComponents.minute + newComponents.second = oldComponents.second ?? 0 + + if let updatedStartDate = calendar.date(from: newComponents) { + tournament.startDate = updatedStartDate + } + let allRounds = selectedTournament.rounds() + let allRoundsNew = tournament.rounds() + allRoundsNew.forEach { round in + if let pRound = allRounds.first(where: { r in + round.index == r.index + }) { + round.setData(from: pRound, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart) + } + } + + let allGroupStages = selectedTournament.allGroupStages() + let allGroupStagesNew = tournament.allGroupStages() + allGroupStagesNew.forEach { groupStage in + if let pGroupStage = allGroupStages.first(where: { gs in + groupStage.index == gs.index + }) { + groupStage.setData(from: pGroupStage, tournamentStartDate: tournament.startDate, previousTournamentStartDate: oldTournamentStart) + } + } + + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) + } else { + if initialSeedRound > 0 { + if let round = tournament.rounds().first(where: { $0.index == initialSeedRound }) { + let seedSorted = frenchUmpireOrder(for: RoundRule.numberOfMatches(forRoundIndex: round.index)) + print(seedSorted) + seedSorted.prefix(initialSeedCount).forEach { index in + if let match = round._matches()[safe:index] { + if match.indexInRound() < RoundRule.numberOfMatches(forRoundIndex: round.index) / 2 { + match.previousMatch(.one)?.disableMatch() + } else { + match.previousMatch(.two)?.disableMatch() + } + } + } + + if initialSeedCount > 0 { + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) + } + } + } + } } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() tournament.buildGroupStages() @@ -515,12 +616,21 @@ struct TableStructureView: View { } private func _updatePreset() { - teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount() - groupStageCount = structurePreset.groupStageCount() - teamsPerGroupStage = structurePreset.teamsPerGroupStage() - qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() - groupStageAdditionalQualified = 0 - buildWildcards = tournament.level.wildcardArePossible() + if let selectedTournament { + teamCount = selectedTournament.teamCount + groupStageCount = selectedTournament.groupStageCount + teamsPerGroupStage = selectedTournament.teamsPerGroupStage + qualifiedPerGroupStage = selectedTournament.qualifiedPerGroupStage + groupStageAdditionalQualified = selectedTournament.groupStageAdditionalQualified + buildWildcards = tournament.level.wildcardArePossible() + } else { + teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount() + groupStageCount = structurePreset.groupStageCount() + teamsPerGroupStage = structurePreset.teamsPerGroupStage() + qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() + groupStageAdditionalQualified = 0 + buildWildcards = tournament.level.wildcardArePossible() + } } private func _verifyValueIntegrity() { @@ -620,3 +730,41 @@ extension TableStructureView { // .environmentObject(DataStore.shared) // } //} + +func frenchUmpireOrder(for matches: [Int]) -> [Int] { + guard matches.count > 1 else { return matches } + + // Base case + if matches.count == 2 { + return [matches[1], matches[0]] // bottom, top + } + + let n = matches.count + let mid = n / 2 + + let topHalf = Array(matches[0.. [Int] { + return frenchUmpireOrder(for: Array(0.. Date: Thu, 9 Oct 2025 15:58:06 +0200 Subject: [PATCH 52/64] v1.2.55 --- 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 629c7aa..9838443 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3221,7 +3221,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\""; @@ -3248,7 +3248,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.54; + MARKETING_VERSION = 1.2.55; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3269,7 +3269,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; @@ -3295,7 +3295,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.54; + MARKETING_VERSION = 1.2.55; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 05f132316ca538e75a37c9fcf6e970d78067f72f Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 9 Oct 2025 18:52:51 +0200 Subject: [PATCH 53/64] add global format picker wording --- .../Screen/Components/HeadManagerView.swift | 4 ++-- .../TournamentFormatSelectionView.swift | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift index 926340a..690340c 100644 --- a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -25,7 +25,7 @@ struct HeadManagerView: View { Text(RoundRule.roundName(fromRoundIndex: $0)).tag($0) } } label: { - Text("Premier tour") + Text("Tour contenant la meilleure tête de série") } .onChange(of: initialSeedRound) { initialSeedCount = RoundRule.numberOfMatches(forRoundIndex: initialSeedRound) @@ -34,7 +34,7 @@ struct HeadManagerView: View { LabeledContent { StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)) } label: { - Text("Têtes de série") + Text("Nombre de tête de série") } } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift index 3cc37ea..188e61e 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift @@ -9,12 +9,25 @@ import SwiftUI import PadelClubData struct TournamentFormatSelectionView: View { + @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) private var tournament: Tournament - + @State private var globalFormat: MatchFormat = DataStore.shared.user.bracketMatchFormatPreference ?? .nineGamesDecisivePoint + @ViewBuilder var body: some View { @Bindable var tournament = tournament + Section { + MatchTypeSelectionView(selectedFormat: $globalFormat, format: "Tout") + .onChange(of: globalFormat) { oldValue, newValue in + tournament.matchFormat = newValue + tournament.loserBracketMatchFormat = newValue + tournament.groupStageMatchFormat = newValue + } + } footer: { + Text("Modifier le format de tous les types de matchs") + } + Section { MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule", additionalEstimationDuration: tournament.additionalEstimationDuration) MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau", additionalEstimationDuration: tournament.additionalEstimationDuration) From ac18a148635b9914d1606466acf0775aa1f63260 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sun, 12 Oct 2025 10:32:49 +0200 Subject: [PATCH 54/64] fix toolbox debug view --- .../Navigation/Toolbox/ToolboxView.swift | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index fde36a3..16f9890 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -219,66 +219,6 @@ struct DebugView: View { Logger.log("Api calls reset") } } - Section { - RowButtonView("Fix Names") { - - for tournament in dataStore.tournaments { - - if let store = tournament.tournamentStore { - let playerRegistrations = store.playerRegistrations - playerRegistrations.forEach { player in - player.firstName = player.firstName.trimmed.capitalized - player.lastName = player.lastName.trimmed.uppercased() - } - - do { - try store.playerRegistrations.addOrUpdate(contentOfs: playerRegistrations) - } catch { - Logger.error(error) - } - } - } - - } - } - - Section { - RowButtonView("Delete teams") { - - for tournament in DataStore.shared.tournaments { - - if let store: TournamentStore = tournament.tournamentStore { - - let teamRegistrations = store.teamRegistrations.filter({ $0.tournamentObject() == nil }) - do { - try store.teamRegistrations.delete(contentOfs: teamRegistrations) - } catch { - Logger.error(error) - } - } - } - } - } - - Section { - // TODO - RowButtonView("Delete players") { - - for tournament in DataStore.shared.tournaments { - if let store: TournamentStore = tournament.tournamentStore { - - let playersRegistrations = store.playerRegistrations.filter({ $0.team() == nil }) - do { - try store.playerRegistrations.delete(contentOfs: playersRegistrations) - } catch { - Logger.error(error) - } - } - } - - } - - } } } From 13011e2b1c0c51ddaec46bbe74a95aa9f12b4bf7 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 13 Oct 2025 14:55:14 +0200 Subject: [PATCH 55/64] add heads config system --- PadelClub/Views/Round/RoundView.swift | 12 +- .../Screen/Components/HeadManagerView.swift | 186 +++++++++++++- .../Screen/TableStructureView.swift | 233 ++++++++++++------ 3 files changed, 339 insertions(+), 92 deletions(-) diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 904880b..119b016 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -80,9 +80,11 @@ struct RoundView: View { } } - if let disabledMatchesCount, disabledMatchesCount > 0 { - let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) - TipView(bracketTip).tipStyle(tint: .green, asSection: true) + if let disabledMatchesCount { + if disabledMatchesCount > 0 { + let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) + TipView(bracketTip).tipStyle(tint: .green, asSection: true) + } let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) @@ -94,7 +96,9 @@ struct RoundView: View { Text("Match\(leftToPlay.pluralSuffix) à jouer en \(upperRound.title)") } } footer: { - Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement") + if disabledMatchesCount > 0 { + Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement") + } } } } diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift index 690340c..d53e10b 100644 --- a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -11,33 +11,195 @@ import PadelClubData struct HeadManagerView: View { - @Environment(Tournament.self) private var tournament: Tournament @EnvironmentObject private var dataStore: DataStore @Environment(\.dismiss) var dismiss - @Binding var initialSeedRound: Int - @Binding var initialSeedCount: Int - + let teamsInBracket: Int + let heads: Int + let initialSeedRepartition: [Int]? + let result: ([Int]) -> Void + + @State private var seedRepartition: [Int] + @State private var selectedSeedRound: Int? = nil + + init(teamsInBracket: Int, heads: Int, initialSeedRepartition: [Int], result: @escaping ([Int]) -> Void) { + self.teamsInBracket = teamsInBracket + self.heads = heads + self.initialSeedRepartition = initialSeedRepartition + self.result = result + + if initialSeedRepartition.isEmpty == false { + _seedRepartition = .init(wrappedValue: initialSeedRepartition) + _selectedSeedRound = .init(wrappedValue: initialSeedRepartition.firstIndex(where: { $0 > 0 })) + } else { + let seedRepartition = Self.place(heads: heads, teamsInBracket: teamsInBracket, initialSeedRound: nil) + _seedRepartition = .init(wrappedValue: seedRepartition) + _selectedSeedRound = .init(wrappedValue: seedRepartition.firstIndex(where: { $0 > 0 })) + } + } + + static func leftToPlace(heads: Int, teamsPerRound: [Int]) -> Int { + let re = heads - teamsPerRound.reduce(0, +) + return re + } + + static func place(heads: Int, teamsInBracket: Int, initialSeedRound: Int?) -> [Int] { + var teamsPerRound: [Int] = [] + let dimension = RoundRule.teamsInFirstRound(forTeams: teamsInBracket) + /* + si 32 = 32, ok si N < 32 alors on mets le max en 16 - ce qui reste à mettre en 32 + */ + + var startingRound = RoundRule.numberOfRounds(forTeams: dimension) - 1 + if let initialSeedRound, initialSeedRound > 0 { + teamsPerRound = Array(repeating: 0, count: initialSeedRound) + startingRound = initialSeedRound + } else { + if dimension != teamsInBracket { + startingRound -= 1 + } + + teamsPerRound = Array(repeating: 0, count: startingRound) + } + while leftToPlace(heads: heads, teamsPerRound: teamsPerRound) > 0 { + // maxAssignable: On retire toutes les équipes placées dans les tours précédents, pondérées par leur propagation (puissance du tour) + let headsLeft = heads - teamsPerRound.reduce(0, +) + // Calculate how many teams from previous rounds propagate to this round + let currentRound = teamsPerRound.count + var previousTeams = 0 + for (i, teams) in teamsPerRound.enumerated() { + previousTeams += teams * (1 << (currentRound - i)) + } + let totalAvailable = RoundRule.numberOfMatches(forRoundIndex: currentRound) * 2 + let maxAssignable = max(0, totalAvailable - previousTeams) + var valueToAppend = min(max(0, headsLeft), maxAssignable) + + if headsLeft - maxAssignable > 0 { + let theory = valueToAppend - (headsLeft - maxAssignable) + if theory > 0 && maxAssignable - theory == 0 { + valueToAppend = theory + } else { + let lastValue = teamsPerRound.last ?? 0 + var newValueToAppend = theory + if theory > maxAssignable || theory < 0 { + newValueToAppend = valueToAppend / 2 + } + valueToAppend = lastValue > 0 ? lastValue : newValueToAppend + } + } + teamsPerRound.append(valueToAppend) + + } + return teamsPerRound + } + + var leftToPlace: Int { + Self.leftToPlace(heads: heads, teamsPerRound: seedRepartition) + } + var body: some View { List { Section { - Picker(selection: $initialSeedRound) { - ForEach((0...10)) { - Text(RoundRule.roundName(fromRoundIndex: $0)).tag($0) + Picker(selection: $selectedSeedRound) { + Text("Choisir").tag(nil as Int?) + ForEach(seedRepartition.indices, id: \.self) { idx in + Text(RoundRule.roundName(fromRoundIndex: idx)).tag(idx) } } label: { Text("Tour contenant la meilleure tête de série") } - .onChange(of: initialSeedRound) { - initialSeedCount = RoundRule.numberOfMatches(forRoundIndex: initialSeedRound) + .onChange(of: selectedSeedRound) { + seedRepartition = Self.place(heads: heads, teamsInBracket: teamsInBracket, initialSeedRound: selectedSeedRound) } - + } footer: { + FooterButtonView("remise à zéro") { + selectedSeedRound = nil + } + } + + Section { + LabeledContent { + Text(teamsInBracket.formatted()) + } label: { + Text("Effectif du tableau") + } + + LabeledContent { - StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)) + Text(leftToPlace.formatted()) } label: { - Text("Nombre de tête de série") + Text("Restant à placer") + } + } + + Section { +// +// LabeledContent { +// StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)) +// } label: { +// Text("Nombre de tête de série") +// } +// + ForEach(seedRepartition.sorted().indices, id: \.self) { index in + SeedStepperRowView(count: $seedRepartition[index], roundIndex: index, max: leftToPlace + seedRepartition[index]) + } + } + + if leftToPlace > 0 { + RowButtonView("Ajouter une manche") { + while leftToPlace > 0 { + let headsLeft = heads - seedRepartition.reduce(0, +) + let lastValue = seedRepartition.last ?? 0 + let maxAssignable = RoundRule.numberOfMatches(forRoundIndex: seedRepartition.count - 1) * 2 - lastValue + var valueToAppend = min(max(0, headsLeft), maxAssignable * 2) + + if headsLeft - maxAssignable > 0 { + valueToAppend = valueToAppend - (headsLeft - maxAssignable) + } + print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)") + seedRepartition.append(valueToAppend) + } } } } .navigationTitle("Têtes de série") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + ButtonValidateView(title: "Valider") { + self.result(seedRepartition) + dismiss() + } + } + + ToolbarItem(placement: .topBarLeading) { + Button("Annuler") { + dismiss() + } + } + } +// .onChange(of: seedRepartition) { old, new in +// if modifiyingSeedRound == false { +// let minCount = min(old.count, new.count) +// if let idx = (0.. new[$0] }) { +// seedRepartition = Array(new.prefix(idx+1)) +// } +// } +// } + } +} + +private struct SeedStepperRowView: View { + @Binding var count: Int + var roundIndex: Int + var max: Int + + var body: some View { + LabeledContent { + HStack { + StepperView(count: $count, minimum: 0, maximum: min(RoundRule.numberOfMatches(forRoundIndex: roundIndex) * 2, max)) + } + } label: { + Text("Équipes en \(RoundRule.roundName(fromRoundIndex: roundIndex))") + } } } + diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index f7ff58a..21f0542 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -27,7 +27,9 @@ struct TableStructureView: View { @State private var selectedTournament: Tournament? @State private var initialSeedCount: Int = 0 @State private var initialSeedRound: Int = 0 - + @State private var showSeedRepartition: Bool = false + @State private var seedRepartition: [Int] = [] + func displayWarning() -> Bool { let unsortedTeamsCount = tournament.unsortedTeamsCount() return tournament.shouldWarnOnlineRegistrationUpdates() && teamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount) @@ -64,6 +66,10 @@ struct TableStructureView: View { max(teamCount - groupStageCount * teamsPerGroupStage, 0) } + var tf: Int { + max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0) + } + init(tournament: Tournament) { self.tournament = tournament _teamCount = .init(wrappedValue: tournament.teamCount) @@ -92,7 +98,14 @@ struct TableStructureView: View { Text("Préréglage") } .disabled(selectedTournament != nil) + } footer: { + Text(structurePreset.localizedDescriptionStructurePresetTitle()) + } + .onChange(of: structurePreset) { + _updatePreset() + } + Section { NavigationLink { TournamentSelectorView(selectedTournament: $selectedTournament) .environment(tournament) @@ -103,24 +116,10 @@ struct TableStructureView: View { Text("À partir d'un tournoi existant") } } - - NavigationLink { - HeadManagerView(initialSeedRound: $initialSeedRound, initialSeedCount: $initialSeedCount) - .environment(tournament) - } label: { - Text("Configuration des têtes de série") - } - .disabled(selectedTournament != nil) - - } footer: { - Text(structurePreset.localizedDescriptionStructurePresetTitle()) } .onChange(of: selectedTournament) { _updatePreset() } - .onChange(of: structurePreset) { - _updatePreset() - } } Section { @@ -240,7 +239,6 @@ struct TableStructureView: View { } Section { - let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0) if groupStageCount > 0 { if structurePreset != .doubleGroupStage { LabeledContent { @@ -268,15 +266,13 @@ struct TableStructureView: View { Text("Attention !").foregroundStyle(.red) } } - LabeledContent { - Text(tf.formatted()) - } label: { - Text("Effectif") - } - LabeledContent { - Text(RoundRule.teamsInFirstRound(forTeams: tf).formatted()) - } label: { - Text("Dimension") + + if groupStageCount > 0 { + LabeledContent { + Text(tf.formatted()) + } label: { + Text("Effectif") + } } } else { LabeledContent { @@ -307,6 +303,27 @@ struct TableStructureView: View { } } + if tournament.state() != .build { + Section { + LabeledContent { + Image(systemName: seedRepartition.isEmpty ? "xmark" : "checkmark") + } label: { + FooterButtonView("Configuration du tableau") { + showSeedRepartition = true + } + .disabled(selectedTournament != nil) + } + } footer: { + if seedRepartition.isEmpty { + Text("Aucune répartition n'a été indiqué, vous devrez réserver ou placer les têtes de séries dans le tableau manuellement.") + } else { + FooterButtonView("Supprimer la configuration", role: .destructive) { + seedRepartition = [] + } + } + } + } + if tournament.rounds().isEmpty && tournament.state() == .build { Section { RowButtonView("Ajouter un tableau", role: .destructive) { @@ -327,13 +344,13 @@ struct TableStructureView: View { Section { RowButtonView("Reconstruire les poules", role:.destructive) { - _save(rebuildEverything: false) + await _save(rebuildEverything: false) } } Section { RowButtonView("Tout refaire", role: .destructive) { - _save(rebuildEverything: true) + await _save(rebuildEverything: true) } } @@ -360,6 +377,13 @@ struct TableStructureView: View { updatedElements.remove(.teamCount) } } + .sheet(isPresented: $showSeedRepartition, content: { + NavigationStack { + HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in + self.seedRepartition = seedRepartition + } + } + }) .onChange(of: groupStageCount) { if groupStageCount != tournament.groupStageCount { updatedElements.insert(.groupStageCount) @@ -412,13 +436,17 @@ struct TableStructureView: View { ToolbarItem(placement: .confirmationAction) { if tournament.state() == .initial { ButtonValidateView { - _save(rebuildEverything: true) + Task { + await _save(rebuildEverything: true) + } } } else { let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) ButtonValidateView(role: .destructive) { if requirements.isEmpty { - _save(rebuildEverything: false) + Task { + await _save(rebuildEverything: false) + } } else { presentRefreshStructureWarning = true } @@ -429,11 +457,15 @@ struct TableStructureView: View { } Button("Reconstruire les poules") { - _save(rebuildEverything: false) + Task { + await _save(rebuildEverything: false) + } } Button("Tout refaire", role: .destructive) { - _save(rebuildEverything: true) + Task { + await _save(rebuildEverything: true) + } } }, message: { ForEach(Array(requirements)) { requirement in @@ -511,7 +543,7 @@ struct TableStructureView: View { } } - private func _save(rebuildEverything: Bool = false) { + private func _save(rebuildEverything: Bool = false) async { _verifyValueIntegrity() do { @@ -531,8 +563,8 @@ struct TableStructureView: View { tournament.initialSeedRound = selectedTournament.initialSeedRound tournament.initialSeedCount = selectedTournament.initialSeedCount } else { - tournament.initialSeedRound = initialSeedRound - tournament.initialSeedCount = initialSeedCount + tournament.initialSeedRound = seedRepartition.firstIndex(where: { $0 > 0 }) ?? 0 + tournament.initialSeedCount = seedRepartition.first(where: { $0 > 0 }) ?? 0 } tournament.removeWildCards() if structurePreset.hasWildcards(), buildWildcards { @@ -541,6 +573,12 @@ struct TableStructureView: View { } tournament.deleteAndBuildEverything(preset: structurePreset) + if seedRepartition.count > 0 { + while tournament.rounds().count < seedRepartition.count { + await tournament.addNewRound(tournament.rounds().count) + } + } + if let selectedTournament { let oldTournamentStart = selectedTournament.startDate let newTournamentStart = tournament.startDate @@ -577,25 +615,65 @@ struct TableStructureView: View { tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) } else { - if initialSeedRound > 0 { - if let round = tournament.rounds().first(where: { $0.index == initialSeedRound }) { - let seedSorted = frenchUmpireOrder(for: RoundRule.numberOfMatches(forRoundIndex: round.index)) - print(seedSorted) - seedSorted.prefix(initialSeedCount).forEach { index in - if let match = round._matches()[safe:index] { - if match.indexInRound() < RoundRule.numberOfMatches(forRoundIndex: round.index) / 2 { - match.previousMatch(.one)?.disableMatch() + for (index, seedCount) in seedRepartition.enumerated() { + if let round = tournament.rounds().first(where: { $0.index == index }) { + let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index) + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index) + let playedMatches = round.playedMatches().map { $0.index - baseIndex } + let allMatches = round._matches() + let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in + playedMatches.contains(index) + }).prefix(seedCount) + for (index, value) in seedSorted.enumerated() { + let isOpponentTurn = index >= playedMatches.count + if let match = allMatches[safe:value] { + if match.index - baseIndex < numberOfMatches / 2 { + if isOpponentTurn { + match.previousMatch(.two)?.disableMatch() + } else { + match.previousMatch(.one)?.disableMatch() + } } else { - match.previousMatch(.two)?.disableMatch() + if isOpponentTurn { + match.previousMatch(.one)?.disableMatch() + } else { + match.previousMatch(.two)?.disableMatch() + } } } } - if initialSeedCount > 0 { - tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) + if seedCount > 0 { + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches()) + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches()) + round.deleteLoserBracket() + round.buildLoserBracket() + round.loserRounds().forEach { loserRound in + loserRound.disableUnplayedLoserBracketMatches() + } } } + } +// if initialSeedRound > 0 { +// if let round = tournament.rounds().first(where: { $0.index == initialSeedRound }) { +// let seedSorted = frenchUmpireOrder(for: RoundRule.numberOfMatches(forRoundIndex: round.index)) +// print(seedSorted) +// seedSorted.prefix(initialSeedCount).forEach { index in +// if let match = round._matches()[safe:index] { +// if match.indexInRound() < RoundRule.numberOfMatches(forRoundIndex: round.index) / 2 { +// match.previousMatch(.one)?.disableMatch() +// } else { +// match.previousMatch(.two)?.disableMatch() +// } +// } +// } +// +// if initialSeedCount > 0 { +// tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) +// } +// } +// } } } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() @@ -730,41 +808,44 @@ extension TableStructureView { // .environmentObject(DataStore.shared) // } //} - func frenchUmpireOrder(for matches: [Int]) -> [Int] { - guard matches.count > 1 else { return matches } - - // Base case - if matches.count == 2 { - return [matches[1], matches[0]] // bottom, top + if matches.count <= 1 { return matches } + if matches.count == 2 { return [matches[1], matches[0]] } + + var result: [Int] = [] + + // Step 1: Take last, then first + result.append(matches.last!) + result.append(matches.first!) + + // Step 2: Get remainder (everything except first and last) + let remainder = Array(matches[1.. [Int] { - return frenchUmpireOrder(for: Array(0.. Date: Tue, 14 Oct 2025 09:08:05 +0200 Subject: [PATCH 56/64] fix stuff headmanager --- PadelClub/Views/Round/RoundSettingsView.swift | 9 +- .../Screen/Components/HeadManagerView.swift | 51 +++- .../Screen/TableStructureView.swift | 236 ++++++++++-------- 3 files changed, 166 insertions(+), 130 deletions(-) diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index d742100..940a091 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -160,14 +160,7 @@ struct RoundSettingsView: View { } private func _removeRound(_ lastRound: Round) async { - await MainActor.run { - let teams = lastRound.seeds() - teams.forEach { team in - team.resetBracketPosition() - } - tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) - tournamentStore?.rounds.delete(instance: lastRound) - } + await tournament.removeRound(lastRound) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift index d53e10b..3fa4ed0 100644 --- a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -79,7 +79,7 @@ struct HeadManagerView: View { valueToAppend = theory } else { let lastValue = teamsPerRound.last ?? 0 - var newValueToAppend = theory + var newValueToAppend = theory == 0 ? maxAssignable / 2 : theory if theory > maxAssignable || theory < 0 { newValueToAppend = valueToAppend / 2 } @@ -102,33 +102,62 @@ struct HeadManagerView: View { Picker(selection: $selectedSeedRound) { Text("Choisir").tag(nil as Int?) ForEach(seedRepartition.indices, id: \.self) { idx in - Text(RoundRule.roundName(fromRoundIndex: idx)).tag(idx) + Text(RoundRule.roundName(fromRoundIndex: idx, displayStyle: .short)).tag(idx) } } label: { - Text("Tour contenant la meilleure tête de série") + Text("Tour de la tête de série n°1") } .onChange(of: selectedSeedRound) { seedRepartition = Self.place(heads: heads, teamsInBracket: teamsInBracket, initialSeedRound: selectedSeedRound) } - } footer: { - FooterButtonView("remise à zéro") { - selectedSeedRound = nil - } } Section { LabeledContent { - Text(teamsInBracket.formatted()) + Text(heads.formatted()) } label: { - Text("Effectif du tableau") + Text("Équipes à placer en tableau") } + if (teamsInBracket - heads) > 0 { + LabeledContent { + Text((teamsInBracket - heads).formatted()) + } label: { + Text("Qualifiés entrants") + } + } LabeledContent { Text(leftToPlace.formatted()) } label: { Text("Restant à placer") } + + LabeledContent { + let matchCount = seedRepartition.enumerated().map { (index, value) in + var result = 0 + var count = value + if count == 0, let selectedSeedRound, index < selectedSeedRound { + let t = RoundRule.numberOfMatches(forRoundIndex: index) + result = RoundRule.cumulatedNumberOfMatches(forTeams: t * 2) + } else { + if index == seedRepartition.count - 1 { + count += (teamsInBracket - heads) + } else if index == seedRepartition.count - 2 { + count += ((seedRepartition[index + 1] + (teamsInBracket - heads)) / 2) + } else { + count += (seedRepartition[index + 1]) + } + result = RoundRule.cumulatedNumberOfMatches(forTeams: count) + } + + return result + } + .reduce(0, +) + Text(matchCount.formatted()) + } label: { + Text("Matchs estimés") + } } Section { @@ -155,13 +184,13 @@ struct HeadManagerView: View { if headsLeft - maxAssignable > 0 { valueToAppend = valueToAppend - (headsLeft - maxAssignable) } - print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)") +// print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)") seedRepartition.append(valueToAppend) } } } } - .navigationTitle("Têtes de série") + .navigationTitle("Répartition") .toolbar { ToolbarItem(placement: .topBarTrailing) { ButtonValidateView(title: "Valider") { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 21f0542..f6d10ee 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -267,13 +267,13 @@ struct TableStructureView: View { } } - if groupStageCount > 0 { - LabeledContent { - Text(tf.formatted()) - } label: { - Text("Effectif") - } - } +// if groupStageCount > 0 { +// LabeledContent { +// Text(tf.formatted()) +// } label: { +// Text("Effectif tableau") +// } +// } } else { LabeledContent { let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount @@ -283,16 +283,30 @@ struct TableStructureView: View { Text("Total de matchs") } } - } footer: { - if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0 { - if tsPure > teamCount / 2 { - Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif.").foregroundStyle(.red) - } else if tsPure < teamCount / 8 { - Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif.").foregroundStyle(.red) - } else if tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified { - Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red) + + LabeledContent { + FooterButtonView("configurer") { + showSeedRepartition = true + } + } label: { + if tournament.state() == .build { + Text("Répartition des équipes") + } else if selectedTournament != nil { + Text("La configuration du tournoi séléctionné sera utilisée.") + } else { + Text(_seeds()) } } + .onAppear { + if seedRepartition.isEmpty && tournament.state() == .initial && selectedTournament == nil { + seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil) + } + } + + } footer: { + if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0, tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified { + Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red) + } } if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() { @@ -303,27 +317,6 @@ struct TableStructureView: View { } } - if tournament.state() != .build { - Section { - LabeledContent { - Image(systemName: seedRepartition.isEmpty ? "xmark" : "checkmark") - } label: { - FooterButtonView("Configuration du tableau") { - showSeedRepartition = true - } - .disabled(selectedTournament != nil) - } - } footer: { - if seedRepartition.isEmpty { - Text("Aucune répartition n'a été indiqué, vous devrez réserver ou placer les têtes de séries dans le tableau manuellement.") - } else { - FooterButtonView("Supprimer la configuration", role: .destructive) { - seedRepartition = [] - } - } - } - } - if tournament.rounds().isEmpty && tournament.state() == .build { Section { RowButtonView("Ajouter un tableau", role: .destructive) { @@ -336,6 +329,12 @@ struct TableStructureView: View { if tournament.state() != .initial { + if seedRepartition.isEmpty == false { + RowButtonView("Modifier la répartition des équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") { + await _handleSeedRepartition() + } + } + Section { RowButtonView("Sauver sans reconstuire l'existant") { _saveWithoutRebuild() @@ -370,13 +369,6 @@ struct TableStructureView: View { } } .toolbarBackground(.visible, for: .navigationBar) - .onChange(of: teamCount) { - if teamCount != tournament.teamCount { - updatedElements.insert(.teamCount) - } else { - updatedElements.remove(.teamCount) - } - } .sheet(isPresented: $showSeedRepartition, content: { NavigationStack { HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in @@ -384,6 +376,14 @@ struct TableStructureView: View { } } }) + .onChange(of: teamCount) { + if teamCount != tournament.teamCount { + updatedElements.insert(.teamCount) + } else { + updatedElements.remove(.teamCount) + } + _verifyValueIntegrity() + } .onChange(of: groupStageCount) { if groupStageCount != tournament.groupStageCount { updatedElements.insert(.groupStageCount) @@ -394,25 +394,31 @@ struct TableStructureView: View { if structurePreset.isFederalPreset(), groupStageCount == 0 { teamCount = structurePreset.tableDimension() } + _verifyValueIntegrity() } .onChange(of: teamsPerGroupStage) { if teamsPerGroupStage != tournament.teamsPerGroupStage { updatedElements.insert(.teamsPerGroupStage) } else { updatedElements.remove(.teamsPerGroupStage) - } } + } + _verifyValueIntegrity() + } .onChange(of: qualifiedPerGroupStage) { if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage { updatedElements.insert(.qualifiedPerGroupStage) } else { updatedElements.remove(.qualifiedPerGroupStage) - } } + } + _verifyValueIntegrity() + } .onChange(of: groupStageAdditionalQualified) { if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified { updatedElements.insert(.groupStageAdditionalQualified) } else { updatedElements.remove(.groupStageAdditionalQualified) } + _verifyValueIntegrity() } .toolbar { if tournament.state() != .initial { @@ -484,6 +490,19 @@ struct TableStructureView: View { } } + + private func _seeds() -> String { + if seedRepartition.isEmpty || seedRepartition.reduce(0, +) == 0 { + return "Aucune configuration" + } + return seedRepartition.enumerated().compactMap { (index, count) in + if count > 0 { + return RoundRule.roundName(fromRoundIndex: index) + " : \(count)" + } else { + return nil + } + }.joined(separator: ", ") + } private func _reset() { tournament.removeWildCards() @@ -573,12 +592,6 @@ struct TableStructureView: View { } tournament.deleteAndBuildEverything(preset: structurePreset) - if seedRepartition.count > 0 { - while tournament.rounds().count < seedRepartition.count { - await tournament.addNewRound(tournament.rounds().count) - } - } - if let selectedTournament { let oldTournamentStart = selectedTournament.startDate let newTournamentStart = tournament.startDate @@ -614,66 +627,10 @@ struct TableStructureView: View { } tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) - } else { - for (index, seedCount) in seedRepartition.enumerated() { - if let round = tournament.rounds().first(where: { $0.index == index }) { - let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index) - let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index) - let playedMatches = round.playedMatches().map { $0.index - baseIndex } - let allMatches = round._matches() - let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in - playedMatches.contains(index) - }).prefix(seedCount) - for (index, value) in seedSorted.enumerated() { - let isOpponentTurn = index >= playedMatches.count - if let match = allMatches[safe:value] { - if match.index - baseIndex < numberOfMatches / 2 { - if isOpponentTurn { - match.previousMatch(.two)?.disableMatch() - } else { - match.previousMatch(.one)?.disableMatch() - } - } else { - if isOpponentTurn { - match.previousMatch(.one)?.disableMatch() - } else { - match.previousMatch(.two)?.disableMatch() - } - } - } - } - - if seedCount > 0 { - tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches()) - tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches()) - round.deleteLoserBracket() - round.buildLoserBracket() - round.loserRounds().forEach { loserRound in - loserRound.disableUnplayedLoserBracketMatches() - } - } - } - - } -// if initialSeedRound > 0 { -// if let round = tournament.rounds().first(where: { $0.index == initialSeedRound }) { -// let seedSorted = frenchUmpireOrder(for: RoundRule.numberOfMatches(forRoundIndex: round.index)) -// print(seedSorted) -// seedSorted.prefix(initialSeedCount).forEach { index in -// if let match = round._matches()[safe:index] { -// if match.indexInRound() < RoundRule.numberOfMatches(forRoundIndex: round.index) / 2 { -// match.previousMatch(.one)?.disableMatch() -// } else { -// match.previousMatch(.two)?.disableMatch() -// } -// } -// } -// -// if initialSeedCount > 0 { -// tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled()) -// } -// } -// } + } + + if seedRepartition.count > 0 { + await _handleSeedRepartition() } } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() @@ -693,8 +650,63 @@ struct TableStructureView: View { } } + private func _handleSeedRepartition() async { + while tournament.rounds().count < seedRepartition.count { + await tournament.addNewRound(tournament.rounds().count) + } + + if seedRepartition.reduce(0, +) > 0 { + let rounds = tournament.rounds() + let roundsToDelete = rounds.suffix(rounds.count - seedRepartition.count) + for round in roundsToDelete { + await tournament.removeRound(round) + } + } + + for (index, seedCount) in seedRepartition.enumerated() { + if let round = tournament.rounds().first(where: { $0.index == index }) { + let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index) + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index) + let playedMatches = round.playedMatches().map { $0.index - baseIndex } + let allMatches = round._matches() + let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in + playedMatches.contains(index) + }).prefix(seedCount) + for (index, value) in seedSorted.enumerated() { + let isOpponentTurn = index >= playedMatches.count + if let match = allMatches[safe:value] { + if match.index - baseIndex < numberOfMatches / 2 { + if isOpponentTurn { + match.previousMatch(.two)?.disableMatch() + } else { + match.previousMatch(.one)?.disableMatch() + } + } else { + if isOpponentTurn { + match.previousMatch(.one)?.disableMatch() + } else { + match.previousMatch(.two)?.disableMatch() + } + } + } + } + + if seedCount > 0 { + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches()) + tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches()) + round.deleteLoserBracket() + round.buildLoserBracket() + round.loserRounds().forEach { loserRound in + loserRound.disableUnplayedLoserBracketMatches() + } + } + } + } + } + private func _updatePreset() { if let selectedTournament { + seedRepartition = [] teamCount = selectedTournament.teamCount groupStageCount = selectedTournament.groupStageCount teamsPerGroupStage = selectedTournament.teamsPerGroupStage @@ -709,6 +721,7 @@ struct TableStructureView: View { groupStageAdditionalQualified = 0 buildWildcards = tournament.level.wildcardArePossible() } + _verifyValueIntegrity() } private func _verifyValueIntegrity() { @@ -754,6 +767,7 @@ struct TableStructureView: View { } } + seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil) } } From b41e8064d7771eb1a197da835becd8c5c3852c7c Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 14 Oct 2025 11:35:18 +0200 Subject: [PATCH 57/64] some fixes --- .../Views/Calling/BracketCallingView.swift | 2 +- .../CallMessageCustomizationView.swift | 1 - PadelClub/Views/Planning/PlanningView.swift | 52 +++++++++++++------ .../Screen/TableStructureView.swift | 21 +++++++- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift index 50e1583..e203388 100644 --- a/PadelClub/Views/Calling/BracketCallingView.swift +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -106,7 +106,7 @@ struct BracketCallingView: View { ForEach(filteredRounds()) { round in let seeds = seeds(forRoundIndex: round.index) - let startDate = round.startDate ?? round.playedMatches().first?.startDate + let startDate = ([round.startDate] + round.playedMatches().map { $0.startDate }).compacted().min() let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) == false }) if seeds.isEmpty == false { Section { diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 0ef3dfb..1c9c7ef 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -236,7 +236,6 @@ struct CallMessageCustomizationView: View { let hasBeenCreated: Bool = eventClub.hasBeenCreated(by: StoreCenter.main.userId) Section { TextField("Nom du club", text: $customClubName, axis: .vertical) - .lineLimit(2) .autocorrectionDisabled() .focused($focusedField, equals: .clubName) .onSubmit { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index c4fe24e..b2afebe 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -728,9 +728,6 @@ struct PlanningView: View { } } - private func _eventCourtCount() -> Int { timeSlots.first?.value.first?.currentTournament()?.eventObject()?.eventCourtCount() ?? 2 - } - private func _save() { let groupByTournaments = allMatches.grouped { match in match.currentTournament() @@ -749,16 +746,27 @@ struct PlanningView: View { Button("Tirer au sort") { _removeCourts() - - let eventCourtCount = _eventCourtCount() - + for slot in timeSlots { - var courtsAvailable = Array(0...eventCourtCount) let matches = slot.value - matches.forEach { match in - if let rand = courtsAvailable.randomElement() { + var courtsByTournament: [String: Set] = [:] + for match in matches { + if let tournament = match.currentTournament(), + let available = tournament.matchScheduler()?.courtsAvailable { + courtsByTournament[tournament.id, default: []].formUnion(available) + } + } + + for match in matches { + guard let tournament = match.currentTournament() else { continue } + // Get current set of available courts for this tournament id + guard var courts = courtsByTournament[tournament.id], !courts.isEmpty else { continue } + // Pick a random court + if let rand = courts.randomElement() { match.courtIndex = rand - courtsAvailable.remove(elements: [rand]) + // Remove from local copy and assign back into the dictionary + courts.remove(rand) + courtsByTournament[tournament.id] = courts } } } @@ -768,16 +776,27 @@ struct PlanningView: View { Button("Fixer par ordre croissant") { _removeCourts() - - let eventCourtCount = _eventCourtCount() - + for slot in timeSlots { - var courtsAvailable = Array(0..] = [:] + for match in matches { + if let tournament = match.currentTournament(), + let available = tournament.matchScheduler()?.courtsAvailable { + courtsByTournament[tournament.id, default: []].formUnion(available.sorted()) + } + } + for i in 0.. Date: Tue, 14 Oct 2025 16:12:47 +0200 Subject: [PATCH 58/64] fix stuff --- .../Tournament/Screen/Components/HeadManagerView.swift | 6 ++++-- .../Components/TournamentMatchFormatsSettingsView.swift | 8 +++----- .../Views/Tournament/Screen/TableStructureView.swift | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift index 3fa4ed0..5e5ccf4 100644 --- a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -62,7 +62,9 @@ struct HeadManagerView: View { } while leftToPlace(heads: heads, teamsPerRound: teamsPerRound) > 0 { // maxAssignable: On retire toutes les équipes placées dans les tours précédents, pondérées par leur propagation (puissance du tour) - let headsLeft = heads - teamsPerRound.reduce(0, +) + let alreadyPut = teamsPerRound.reduce(0, +) + let headsLeft = alreadyPut == 0 ? teamsInBracket : heads - alreadyPut + // Calculate how many teams from previous rounds propagate to this round let currentRound = teamsPerRound.count var previousTeams = 0 @@ -150,7 +152,7 @@ struct HeadManagerView: View { } result = RoundRule.cumulatedNumberOfMatches(forTeams: count) } - + print(index, value, result, count) return result } .reduce(0, +) diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift index d4280a0..3dcc1a4 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift @@ -25,12 +25,10 @@ struct TournamentMatchFormatsSettingsView: View { var body: some View { @Bindable var tournament = tournament List { - if confirmUpdate { - RowButtonView("Modifier les matchs existants", role: .destructive) { - _updateAllFormat() - } + RowButtonView("Modifier les matchs existants", role: .destructive) { + _updateAllFormat() } - + TournamentFormatSelectionView() Section { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index bc20363..82f3436 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -691,6 +691,10 @@ struct TableStructureView: View { let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in playedMatches.contains(index) }).prefix(seedCount) + + if playedMatches.count == numberOfMatches && seedCount == numberOfMatches * 2 { + continue + } for (index, value) in seedSorted.enumerated() { let isOpponentTurn = index >= playedMatches.count if let match = allMatches[safe:value] { From a3880b04bd977b82f40fbe3ce897b627f795da86 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 14 Oct 2025 23:56:35 +0200 Subject: [PATCH 59/64] fix head manager match count --- .../Views/Tournament/Screen/Components/HeadManagerView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift index 5e5ccf4..f40cb82 100644 --- a/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift @@ -63,7 +63,7 @@ struct HeadManagerView: View { while leftToPlace(heads: heads, teamsPerRound: teamsPerRound) > 0 { // maxAssignable: On retire toutes les équipes placées dans les tours précédents, pondérées par leur propagation (puissance du tour) let alreadyPut = teamsPerRound.reduce(0, +) - let headsLeft = alreadyPut == 0 ? teamsInBracket : heads - alreadyPut + let headsLeft = heads - alreadyPut // Calculate how many teams from previous rounds propagate to this round let currentRound = teamsPerRound.count @@ -152,7 +152,7 @@ struct HeadManagerView: View { } result = RoundRule.cumulatedNumberOfMatches(forTeams: count) } - print(index, value, result, count) +// print(index, value, result, count) return result } .reduce(0, +) From 43f5ac97a4888df07597b747deaa65f8e133809c Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 15 Oct 2025 07:44:17 +0200 Subject: [PATCH 60/64] add helper footer fix player search view --- .../Navigation/Toolbox/ToolboxView.swift | 19 ++++++++++++++++--- .../Screen/TableStructureView.swift | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 16f9890..04f955d 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -21,6 +21,7 @@ struct ToolboxView: View { @State private var tapCount = 0 @State private var lastTapTime: Date? = nil private let tapTimeThreshold: TimeInterval = 1.0 + @State private var displaySearchPlayer: Bool = false var lastDataSource: String? { dataStore.appSettings.lastDataSource @@ -69,9 +70,8 @@ struct ToolboxView: View { } Section { - NavigationLink { - SelectablePlayerListView(isPresented: false, lastDataSource: true) - .toolbar(.hidden, for: .tabBar) + Button { + displaySearchPlayer = true } label: { Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") } @@ -121,6 +121,19 @@ struct ToolboxView: View { } } } + .sheet(isPresented: $displaySearchPlayer, content: { + NavigationStack { + SelectablePlayerListView(isPresented: false, lastDataSource: true) + .toolbar(.hidden, for: .tabBar) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Fermer") { + displaySearchPlayer = false + } + } + } + } + }) .onAppear { #if DEBUG self.showDebugViews = true diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 82f3436..070fe94 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -330,8 +330,12 @@ struct TableStructureView: View { if tournament.state() != .initial { if seedRepartition.isEmpty == false { - RowButtonView("Modifier la répartition des équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") { - await _handleSeedRepartition() + Section { + RowButtonView("Répartir les équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") { + await _handleSeedRepartition() + } + } footer: { + Text("Cette action va effacer le répartition actuelle des équipes dans le tableau et la refaire, les manches seront ré-initialisées") } } @@ -339,24 +343,33 @@ struct TableStructureView: View { RowButtonView("Sauver sans reconstuire l'existant") { _saveWithoutRebuild() } + } footer: { + Text("Cette action sauve les paramètres du tournoi sans modifier vos poules / tableaux actuels.") + } Section { RowButtonView("Reconstruire les poules", role:.destructive) { await _save(rebuildEverything: false) } + } footer: { + Text("Cette action efface les poules existantes et les reconstruits, leurs données seront perdues.") } Section { RowButtonView("Tout refaire", role: .destructive) { await _save(rebuildEverything: true) } + } footer: { + Text("Cette action efface le tableau et les poules existantes et reconstruit tout de zéro, leurs données seront perdues.") } Section { RowButtonView("Remise-à-zéro", role: .destructive) { _reset() } + } footer: { + Text("Retourne à la structure initiale, comme si vous veniez de créer le tournoi. Les données existantes seront perdues.") } Section { From 8379eccfb6f617e61c0c5a587dafda437a6832fc Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 15 Oct 2025 07:51:37 +0200 Subject: [PATCH 61/64] fix registion issues not displayed --- .../Tournament/Screen/InscriptionManagerView.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index a0c5470..5bd37aa 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -271,9 +271,7 @@ struct InscriptionManagerView: View { // await _refreshList(forced: true) // } .onAppear { - if tournament.enableOnlineRegistration == false || refreshStatus == true { - _setHash(currentSelectedSortedTeams: selectedSortedTeams) - } + _setHash(currentSelectedSortedTeams: selectedSortedTeams) } .onDisappear { _handleHashDiff(selectedSortedTeams: selectedSortedTeams) @@ -942,14 +940,9 @@ struct InscriptionManagerView: View { if tournament.enableOnlineRegistration { LabeledContent { Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted()) - .font(.largeTitle) + .fontWeight(.bold) } label: { Text("Inscriptions en ligne") - if let refreshResult { - Text(refreshResult).foregroundStyle(.secondary) - } else { - Text(" ") - } } // RowButtonView("Rafraîchir les inscriptions en ligne") { From dbd970f87f00fb219da366f89321adbc9761714a Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 15 Oct 2025 09:59:25 +0200 Subject: [PATCH 62/64] add custom club name option in tournament for calling teams --- .../Views/Calling/CallMessageCustomizationView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 1c9c7ef..8b54d0e 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -31,7 +31,7 @@ struct CallMessageCustomizationView: View { self.tournament = tournament _customCallMessageBody = State(wrappedValue: DataStore.shared.user.summonsMessageBody ?? (DataStore.shared.user.summonsUseFullCustomMessage ? "" : ContactType.defaultCustomMessage)) _customCallMessageSignature = State(wrappedValue: DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament)) - _customClubName = State(wrappedValue: tournament.clubName ?? "Lieu du tournoi") + _customClubName = State(wrappedValue: tournament.customClubName ?? tournament.clubName ?? "Lieu du tournoi") _summonsAvailablePaymentMethods = State(wrappedValue: DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods) } @@ -235,13 +235,13 @@ struct CallMessageCustomizationView: View { if let eventClub = tournament.eventObject()?.clubObject() { let hasBeenCreated: Bool = eventClub.hasBeenCreated(by: StoreCenter.main.userId) Section { - TextField("Nom du club", text: $customClubName, axis: .vertical) + TextField("Nom du club", text: $customClubName) .autocorrectionDisabled() .focused($focusedField, equals: .clubName) .onSubmit { - eventClub.name = customClubName + tournament.customClubName = customClubName.prefixTrimmed(100) do { - try dataStore.clubs.addOrUpdate(instance: eventClub) + try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } From 18228396bf21d25d65c116c3828e8d8d78f6b914 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 15 Oct 2025 10:10:59 +0200 Subject: [PATCH 63/64] build 2 --- 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 9838443..91699b7 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3221,7 +3221,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\""; @@ -3269,7 +3269,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 bd03321cc0bbbc8933736d55d1a06c92bab05bea Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 16 Oct 2025 09:19:39 +0200 Subject: [PATCH 64/64] improve export data capability for teams / players --- .../PlayersWithoutContactView.swift | 4 +-- PadelClub/Views/Calling/SendToAllView.swift | 4 +-- PadelClub/Views/Cashier/CashierView.swift | 2 +- .../Views/GroupStage/GroupStageView.swift | 1 - PadelClub/Views/Match/MatchSetupView.swift | 1 - PadelClub/Views/Player/PlayerDetailView.swift | 2 +- .../Views/Shared/LearnMoreSheetView.swift | 2 +- PadelClub/Views/Team/EditingTeamView.swift | 2 +- .../Screen/InscriptionManagerView.swift | 29 ++++++++++++++----- 9 files changed, 30 insertions(+), 17 deletions(-) diff --git a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift index 07b2747..d670b41 100644 --- a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift +++ b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift @@ -14,7 +14,7 @@ struct PlayersWithoutContactView: View { var body: some View { Section { - let withoutEmails = players.filter({ $0.email?.isEmpty == true || $0.email == nil }) + let withoutEmails = players.filter({ $0.hasMail() == false }) DisclosureGroup { ForEach(withoutEmails) { player in NavigationLink { @@ -32,7 +32,7 @@ struct PlayersWithoutContactView: View { } } - let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false }) + let withoutPhones = players.filter({ $0.hasMobilePhone() == false }) DisclosureGroup { ForEach(withoutPhones) { player in NavigationLink { diff --git a/PadelClub/Views/Calling/SendToAllView.swift b/PadelClub/Views/Calling/SendToAllView.swift index 41c82ce..3b464e8 100644 --- a/PadelClub/Views/Calling/SendToAllView.swift +++ b/PadelClub/Views/Calling/SendToAllView.swift @@ -273,9 +273,9 @@ struct SendToAllView: View { self._verifyUser { if contactMethod == 0 { - contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil) + contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.phoneNumber, $0.contactPhoneNumber] }.compactMap({ $0 }), body: finalMessage(), tournamentBuild: nil) } else { - contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil) + contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.email, $0.contactEmail] }.compactMap({ $0 }), body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil) } } diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index 5433655..f175f52 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -18,7 +18,7 @@ struct ShareableObject { func sharedData() async -> Data? { let _players = players.filter({ cashierViewModel._shouldDisplayPlayer($0) }) .map { - [$0.pasteData()] + [$0.pasteData(type: .payment)] .compacted() .joined(separator: "\n") } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index ac06bfa..d1956d3 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -245,7 +245,6 @@ struct GroupStageView: View { Text("#\(index + 1)") .font(.caption) TeamPickerView(groupStagePosition: index, pickTypeContext: .groupStage, teamPicked: { team in - print(team.pasteData()) team.groupStage = groupStage.id team.groupStagePosition = index groupStage._matches().forEach({ $0.updateTeamScores() }) diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 67cd78c..364d912 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -65,7 +65,6 @@ struct MatchSetupView: View { HStack { let luckyLosers = walkOutSpot ? match.luckyLosers() : [] TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, pickTypeContext: matchTypeContext == .bracket ? .bracket : .loserBracket, luckyLosers: luckyLosers, teamPicked: { team in - print(team.pasteData()) if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket { match.setLuckyLoser(team: team, teamPosition: teamPosition) do { diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 93a1688..2671928 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -372,7 +372,7 @@ struct PlayerDetailView: View { .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: player.pasteData()) { + ShareLink(item: player.pasteData(type: .sharing)) { Label("Partager", systemImage: "square.and.arrow.up") } } diff --git a/PadelClub/Views/Shared/LearnMoreSheetView.swift b/PadelClub/Views/Shared/LearnMoreSheetView.swift index 47ab4ef..fbe4b00 100644 --- a/PadelClub/Views/Shared/LearnMoreSheetView.swift +++ b/PadelClub/Views/Shared/LearnMoreSheetView.swift @@ -28,7 +28,7 @@ struct LearnMoreSheetView: View { """) } actions: { - ShareLink(item: tournament.pasteDataForImporting().createFile(tournament.tournamentTitle(.short))) { + ShareLink(item: tournament.pasteDataForImporting(type: .sharing).createFile(tournament.tournamentTitle(.short))) { Text("Exporter les inscriptions") } diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index ab9b3f9..811607f 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -116,7 +116,7 @@ struct EditingTeamView: View { } } footer: { HStack { - CopyPasteButtonView(pasteValue: team.playersPasteData()) + CopyPasteButtonView(pasteValue: team.playersPasteData(type: .sharing)) Spacer() if team.isWildCard(), team.unsortedPlayers().isEmpty { TeamPickerView(pickTypeContext: .wildcard) { teamregistration in diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 5bd37aa..96bc4e8 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -547,11 +547,25 @@ struct InscriptionManagerView: View { private func _sharingTeamsMenuView() -> some View { Menu { - ShareLink(item: teamPaste(), preview: .init("Inscriptions")) { - Text("En texte") + Menu { + ShareLink(item: teamPaste(.rawText, type: .sharing), preview: .init(ExportType.sharing.localizedString().capitalized)) { + Text("En texte") + } + ShareLink(item: teamPaste(.csv, type: .sharing), preview: .init(ExportType.sharing.localizedString().capitalized)) { + Text("En csv") + } + } label: { + Text("Pour diffusion") } - ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) { - Text("En csv") + Menu { + ShareLink(item: teamPaste(.rawText, type: .payment), preview: .init(ExportType.payment.localizedString().capitalized)) { + Text("En texte") + } + ShareLink(item: teamPaste(.csv, type: .payment), preview: .init(ExportType.payment.localizedString().capitalized)) { + Text("En csv") + } + } label: { + Text("Pour encaissement") } } label: { Label("Exporter les paires", systemImage: "square.and.arrow.up") @@ -575,8 +589,8 @@ struct InscriptionManagerView: View { tournament.unsortedTeamsWithoutWO() } - func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile { - TournamentShareFile(tournament: tournament, exportFormat: exportFormat) + func teamPaste(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> TournamentShareFile { + TournamentShareFile(tournament: tournament, exportFormat: exportFormat, type: type) } var unsortedPlayers: [PlayerRegistration] { @@ -1251,10 +1265,11 @@ struct TournamentGroupStageShareContent: Transferable { struct TournamentShareFile: Transferable { let tournament: Tournament let exportFormat: ExportFormat + let type: ExportType func shareFile() -> URL { print("Generating URL...") - return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat) + return tournament.pasteDataForImporting(exportFormat, type: type).createFile(self.tournament.tournamentTitle()+"-"+type.localizedString(), exportFormat) } static var transferRepresentation: some TransferRepresentation {