diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 015c73a..e06cb96 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; @@ -2385,7 +2395,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 */, @@ -2650,7 +2662,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 */, @@ -2893,7 +2907,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 */, @@ -3139,12 +3155,12 @@ 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", ); - MARKETING_VERSION = 1.2.48; + MARKETING_VERSION = 1.2.52; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3169,6 +3185,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"; @@ -3185,12 +3202,12 @@ 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", ); - MARKETING_VERSION = 1.2.48; + MARKETING_VERSION = 1.2.52; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3288,6 +3305,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)"; @@ -3304,7 +3322,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", @@ -3333,6 +3351,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)"; @@ -3349,7 +3368,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", @@ -3393,7 +3412,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", @@ -3436,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/AppDelegate.swift b/PadelClub/AppDelegate.swift index 3fe421e..56a2312 100644 --- a/PadelClub/AppDelegate.swift +++ b/PadelClub/AppDelegate.swift @@ -67,9 +67,10 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel StoreCenter.main.forceNoSynchronization = !synchronized } + func applicationWillEnterForeground(_ application: UIApplication) { Task { - try await Guard.main.refreshPurchasedAppleProducts() + await Guard.main.refreshPurchases() } } 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/Extensions/Tournament+Extensions.swift b/PadelClub/Extensions/Tournament+Extensions.swift index 65408aa..b8c3277 100644 --- a/PadelClub/Extensions/Tournament+Extensions.swift +++ b/PadelClub/Extensions/Tournament+Extensions.swift @@ -11,36 +11,6 @@ import PadelClubData import LeStorage extension Tournament { - - func setupFederalSettings() { - teamSorting = tournamentLevel.defaultTeamSortingType - groupStageMatchFormat = groupStageSmartMatchFormat() - loserBracketMatchFormat = loserBracketSmartMatchFormat(5) - matchFormat = roundSmartMatchFormat(5) - entryFee = tournamentLevel.entryFee - registrationDateLimit = deadline(for: .inscription) - if enableOnlineRegistration, isAnimation() == false { - accountIsRequired = true - licenseIsRequired = true - } - } - - func customizeUsingPreferences() { - guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in - tournament.tournamentLevel == self.tournamentLevel - && tournament.tournamentCategory == self.tournamentCategory - && tournament.federalTournamentAge == self.federalTournamentAge - && tournament.hasEnded() == true - && tournament.isCanceled == false - && tournament.isDeleted == false - }).sorted(by: \.endDate!, order: .descending).first else { - return - } - - self.dayDuration = lastTournamentWithSameBuild.dayDuration - self.teamCount = (lastTournamentWithSameBuild.teamCount / 2) * 2 - self.enableOnlineRegistration = lastTournamentWithSameBuild.enableOnlineRegistration - } func addTeam(_ players: Set, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date(), name: name) @@ -137,9 +107,9 @@ extension Tournament { players.filter({ $0.hasHomonym() }) } - func payIfNecessary() throws { + func payIfNecessary() async throws { if self.payment != nil { return } - if let payment = Guard.main.paymentForNewTournament() { + if let payment = await Guard.main.paymentForNewTournament() { self.payment = payment DataStore.shared.tournaments.addOrUpdate(instance: self) return 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/SyncedProducts.storekit b/PadelClub/SyncedProducts.storekit index 35f21ef..acf9b32 100644 --- a/PadelClub/SyncedProducts.storekit +++ b/PadelClub/SyncedProducts.storekit @@ -1,11 +1,36 @@ { + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, "identifier" : "2055C391", "nonRenewingSubscriptions" : [ ], "products" : [ { - "displayPrice" : "14.0", + "displayPrice" : "129.0", + "familyShareable" : false, + "internalID" : "6751947241", + "localizations" : [ + { + "description" : "Achetez 10 tournois", + "displayName" : "Pack de 10 tournois", + "locale" : "fr" + } + ], + "productID" : "app.padelclub.tournament.unit.10", + "referenceName" : "Pack de 10 tournois", + "type" : "Consumable" + }, + { + "displayPrice" : "17.0", "familyShareable" : false, "internalID" : "6484163993", "localizations" : [ @@ -22,57 +47,53 @@ ], "settings" : { "_applicationInternalID" : "6484163558", + "_askToBuyEnabled" : false, + "_billingGracePeriodEnabled" : false, + "_billingIssuesEnabled" : false, "_compatibilityTimeRate" : { "3" : 6 }, "_developerTeamID" : "BQ3Y44M3Q6", + "_disableDialogs" : false, "_failTransactionsEnabled" : false, - "_lastSynchronizedDate" : 735034894.72550702, - "_locale" : "en_US", - "_storefront" : "USA", + "_lastSynchronizedDate" : 779705033.96878397, + "_locale" : "fr", + "_renewalBillingIssuesEnabled" : false, + "_storefront" : "FRA", "_storeKitErrors" : [ { - "current" : null, "enabled" : false, "name" : "Load Products" }, { - "current" : null, "enabled" : false, "name" : "Purchase" }, { - "current" : null, "enabled" : false, "name" : "Verification" }, { - "current" : null, "enabled" : false, "name" : "App Store Sync" }, { - "current" : null, "enabled" : false, "name" : "Subscription Status" }, { - "current" : null, "enabled" : false, "name" : "App Transaction" }, { - "current" : null, "enabled" : false, "name" : "Manage Subscriptions Sheet" }, { - "current" : null, "enabled" : false, "name" : "Refund Request Sheet" }, { - "current" : null, "enabled" : false, "name" : "Offer Code Redeem Sheet" } @@ -89,7 +110,15 @@ "subscriptions" : [ { "adHocOffers" : [ - + { + "displayPrice" : "45.0", + "internalID" : "1A02CDB5", + "numberOfPeriods" : 12, + "offerID" : "PRICE50", + "paymentMode" : "payAsYouGo", + "referenceName" : "ancien prix 50", + "subscriptionPeriod" : "P1M" + } ], "codeOffers" : [ @@ -110,7 +139,10 @@ "recurringSubscriptionPeriod" : "P1M", "referenceName" : "Monthly Five", "subscriptionGroupID" : "21474782", - "type" : "RecurringSubscription" + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] }, { "adHocOffers" : [ @@ -135,13 +167,16 @@ "recurringSubscriptionPeriod" : "P1M", "referenceName" : "Monthly Unlimited", "subscriptionGroupID" : "21474782", - "type" : "RecurringSubscription" + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] } ] } ], "version" : { - "major" : 3, + "major" : 4, "minor" : 0 } } 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 593c6c5..30d5f74 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()) } @@ -68,11 +70,12 @@ class FederalDataViewModel { selectedClubs.removeAll() dayPeriod = .all dayDuration = nil + weekdays.removeAll() id = UUID() } 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 +99,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 +111,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 +144,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 +166,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/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 821cdd4..dd25e95 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -262,10 +262,8 @@ struct CallView: View { NavigationStack { LoginView(reason: LoginReason.loginRequiredForFeature) { _ in self.showUserCreationView = false - self._payTournamentAndExecute { - self._summon(byMessage: self.summonParamByMessage, - reSummon: self.summonParamByMessage) - } + self._summon(byMessage: self.summonParamByMessage, + reSummon: self.summonParamByMessage) } } }) @@ -353,12 +351,10 @@ struct CallView: View { self.summonParamByMessage = byMessage self.summonParamReSummon = reSummon self._verifyUser { - self._payTournamentAndExecute { - if byMessage { - self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) - } else { - self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) - } + if byMessage { + self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) + } else { + self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) } } } @@ -371,14 +367,14 @@ struct CallView: View { } } - fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { - do { - try self.tournament.payIfNecessary() - handler() - } catch { - self.showSubscriptionView = true - } - } +// fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { +// do { +// try self.tournament.payIfNecessary() +// handler() +// } catch { +// self.showSubscriptionView = true +// } +// } fileprivate func _contactByMessage(reSummon: Bool, forcedEmptyMessage: Bool) { self.contactType = .message(date: callDate, diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index 885b1cf..baa3a8a 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 { @@ -147,9 +146,7 @@ struct MenuWarningView: View { fileprivate func _tryToContact() { self._verifyUser { - self._payTournamentAndExecute { - self.contactType = self.savedContactType - } + self.contactType = self.savedContactType } } @@ -161,14 +158,16 @@ struct MenuWarningView: View { } } - fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { - do { - try tournament.payIfNecessary() - handler() - } catch { - self.showSubscriptionView = true - } - } +// fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { +// Task { +// do { +// try await tournament.payIfNecessary() +// handler() +// } catch { +// self.showSubscriptionView = true +// } +// } +// } } diff --git a/PadelClub/Views/Calling/SendToAllView.swift b/PadelClub/Views/Calling/SendToAllView.swift index 8547947..41c82ce 100644 --- a/PadelClub/Views/Calling/SendToAllView.swift +++ b/PadelClub/Views/Calling/SendToAllView.swift @@ -272,13 +272,10 @@ struct SendToAllView: View { fileprivate func _contact() { self._verifyUser { - self._payTournamentAndExecute { - - if contactMethod == 0 { - contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, 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) - } + if contactMethod == 0 { + contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, 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) } } @@ -292,14 +289,14 @@ struct SendToAllView: View { } } - fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { - do { - try tournament.payIfNecessary() - handler() - } catch { - self.showSubscriptionView = true - } - } +// fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { +// do { +// try tournament.payIfNecessary() +// handler() +// } catch { +// self.showSubscriptionView = true +// } +// } private var _networkErrorMessage: String { ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected) diff --git a/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift b/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift index bbb6ef9..6e08537 100644 --- a/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift +++ b/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift @@ -23,10 +23,17 @@ struct ClubCourtSetupView: View { .disabled(displayContext == .lockedForEditing) .onChange(of: club.courtCount) { if displayContext != .addition { - do { - try dataStore.clubs.addOrUpdate(instance: club) - } catch { - Logger.error(error) + dataStore.clubs.addOrUpdate(instance: club) + dataStore.events.filter { event in + event.club?.id == club.id + }.forEach { event in + let tournaments = event.tournaments.filter({ tournament in + tournament.startDate > Date() + }) + tournaments.forEach { tournament in + tournament.courtCount = club.courtCount + } + dataStore.tournaments.addOrUpdate(contentOfs: tournaments) } } } 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/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 00e83b9..f286b4b 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -147,55 +147,45 @@ struct GroupStageTeamView: View { Group { switch contactType { case .message(_, let recipients, let body, _): - if Guard.main.paymentForNewTournament() != nil { - MessageComposeView(recipients: recipients, body: body) { result in - switch result { - case .cancelled: - break - case .failed: - self.sentError = .messageFailed - case .sent: - if networkMonitor.connected == false { - self.contactType = nil - if team.getPhoneNumbers().isEmpty == false { - self.sentError = .uncalledTeams([team]) - } else { - self.sentError = .messageNotSent - } + MessageComposeView(recipients: recipients, body: body) { result in + switch result { + case .cancelled: + break + case .failed: + self.sentError = .messageFailed + case .sent: + if networkMonitor.connected == false { + self.contactType = nil + if team.getPhoneNumbers().isEmpty == false { + self.sentError = .uncalledTeams([team]) + } else { + self.sentError = .messageNotSent } - @unknown default: - break } + @unknown default: + break } - } else { - SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) - .environment(\.colorScheme, .light) } case .mail(_, let recipients, let bccRecipients, let body, let subject, _): - if Guard.main.paymentForNewTournament() != nil { - MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in - switch result { - case .cancelled, .saved: - self.contactType = nil - case .failed: + MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in + switch result { + case .cancelled, .saved: + self.contactType = nil + case .failed: + self.contactType = nil + self.sentError = .mailFailed + case .sent: + if networkMonitor.connected == false { self.contactType = nil - self.sentError = .mailFailed - case .sent: - if networkMonitor.connected == false { - self.contactType = nil - if team.getMail().isEmpty == false { - self.sentError = .uncalledTeams([team]) - } else { - self.sentError = .mailNotSent - } + if team.getMail().isEmpty == false { + self.sentError = .uncalledTeams([team]) + } else { + self.sentError = .mailNotSent } - @unknown default: - break } + @unknown default: + break } - } else { - SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) - .environment(\.colorScheme, .light) } } } 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 62896f5..af7b2e3 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -324,7 +324,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 { @@ -615,9 +626,20 @@ struct MatchDetailView: View { } self._verifyUser { - self._payTournamentAndExecute { - self.scoreType = .edition + + Task { + do { + try await self._payTournamentAndExecute() + self.scoreType = .edition + } catch { + self.showSubscriptionView = true + } + } + +// self._payTournamentAndExecute { +// self.scoreType = .edition +// } } } @@ -629,15 +651,9 @@ struct MatchDetailView: View { } } - fileprivate func _payTournamentAndExecute(_ handler: () -> ()) { + fileprivate func _payTournamentAndExecute() async throws { guard let tournament = match.currentTournament() else { fatalError("missing tournament") } - - do { - try tournament.payIfNecessary() - handler() - } catch { - self.showSubscriptionView = true - } + try await tournament.payIfNecessary() } private func save() { diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index cd0c005..33d66e7 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 @@ -217,85 +224,124 @@ 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) // }) .toolbar { - ToolbarItemGroup(placement: .topBarLeading) { - Button { - switch viewStyle { - case .list: - viewStyle = .calendar - case .calendar: - viewStyle = .list + ToolbarItem(placement: .topBarLeading) { + if #available(iOS 26.0, *) { + 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 + } + } + } + } else { + Button { + switch viewStyle { + case .list: + viewStyle = .calendar + case .calendar: + viewStyle = .list + } + } label: { + Image(systemName: "calendar.circle") + .resizable() + .scaledToFit() + .frame(minHeight: 32) } - } 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, *) { + if federalDataViewModel.areFiltersEnabled() { + Button("Filtre", systemImage: "line.3.horizontal.decrease") { + presentFilterView.toggle() + } + .buttonStyle(.borderedProminent) + } else { + Button("Filtre", systemImage: "line.3.horizontal.decrease") { + presentFilterView.toggle() + } + } + } 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) + } } } - 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() } } } } + .sheet(isPresented: $presentOnboarding, content: { + OnboardingView() + .environmentObject(dataStore) + }) .sheet(isPresented: $presentFilterView) { TournamentFilterView(federalDataViewModel: federalDataViewModel) .environment(navigation) @@ -397,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 { @@ -474,6 +555,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 +571,7 @@ struct ActivityView: View { } } + @ViewBuilder private func _tenupEmptyView() -> some View { if dataStore.user.hasTenupClubs() == false { ContentUnavailableView { @@ -496,6 +583,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 +609,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 { @@ -561,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/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 7668224..50e8b80 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -158,6 +158,29 @@ struct EventListView: View { } Divider() } + + Menu { + Picker("Choix du montant", selection: Binding(get: { + // If all tournaments share the same price, show it; otherwise default to 0 + let prices = Set(pcTournaments.compactMap { $0.entryFee }) + return prices.count == 1 ? prices.first ?? 0.0 : 0.0 + }, set: { (newValue: Double) in + // Apply the chosen price to every tournament + pcTournaments.forEach { tournament in + tournament.entryFee = newValue + } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + })) { + ForEach([Double](stride(from: 0.0, through: 50.0, by: 5.0)), id: \.self) { (price: Double) in + Text(price.formatted(.currency(code: Locale.current.currency?.identifier ?? "EUR"))).tag(price as Double) + } + } + } label: { + Text("Montant de l'inscription") + } + + Divider() + Menu { Button { pcTournaments.forEach { tournament in @@ -536,3 +559,4 @@ struct EventListView: View { //#Preview { // EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true) //} + diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index d6417e7..df86047 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,23 +152,28 @@ 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) { 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() @@ -193,11 +210,12 @@ struct TournamentLookUpView: View { revealSearchParameters = true federalDataViewModel.searchedFederalTournaments = [] federalDataViewModel.searchAttemptCount = 0 + federalDataViewModel.removeFilters() } label: { Text("Ré-initialiser la recherche") } } label: { - Label("Options", systemImage: "ellipsis.circle") + LabelOptions() } } } @@ -221,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() @@ -340,8 +378,54 @@ struct TournamentLookUpView: View { var searchParametersView: some View { @Bindable var appSettings = dataStore.appSettings Section { - DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date) - DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date) + 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(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?) @@ -350,7 +434,10 @@ struct TournamentLookUpView: View { } label: { Text("Durée souhaitée (en jours)") } - + + @Bindable var federalDataViewModel = federalDataViewModel + WeekdayselectionView(weekdays: $federalDataViewModel.weekdays) + Picker(selection: $appSettings.dayPeriod) { ForEach(DayPeriod.allCases) { Text($0.localizedDayPeriodLabel().capitalized).tag($0) @@ -392,12 +479,12 @@ struct TournamentLookUpView: View { } .symbolVariant(.fill) .foregroundColor (Color.white) - .cornerRadius (20) .font(.system(size: 12)) } } 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/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/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..003a22a 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -17,9 +17,16 @@ 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? { dataStore.appSettings.lastDataSource } @@ -90,6 +97,34 @@ 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 { + 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 +133,8 @@ struct MainView: View { navigation.path.removeLast(navigation.path.count) mainViewId = UUID() } + + canPresentOnboarding = true } .environmentObject(dataStore) .task { @@ -247,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 new file mode 100644 index 0000000..d6d7869 --- /dev/null +++ b/PadelClub/Views/Navigation/OnboardingView.swift @@ -0,0 +1,239 @@ +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 + }), + ("Accès au classement mensuel", { + dismiss() + navigation.selectedTab = .toolbox + }), + ("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/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/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index cd129fd..93e1132 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -48,28 +48,26 @@ struct UmpireView: View { List { PurchaseListView() - - if Guard.main.currentPlan != .monthlyUnlimited { - 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 - } - ) - - } + + 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 { @@ -317,18 +315,6 @@ struct UmpireView: View { licenseMessage = nil } .navigationTitle("Juge-Arbitre") - .toolbar { -#if DEBUG - ToolbarItem(placement: .topBarTrailing) { - NetworkStatusView() -// if StoreCenter.main.collectionsCanSynchronize { -// Image(systemName: "checkmark.icloud") -// } else { -// Image(systemName: "icloud.slash") -// } - } -#endif - } .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { 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/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 6136e36..5b81bf2 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -209,11 +209,6 @@ struct EditingTeamView: View { Text(registrationDateModified.localizedWeekDay().capitalized) } } - #if DEBUG - .disabled(false) - #else - .disabled(team.hasPaidOnline() || team.hasRegisteredOnline()) - #endif Toggle(isOn: $wildCardBracket) { Text("Wildcard Tableau") @@ -354,55 +349,45 @@ struct EditingTeamView: View { Group { switch contactType { case .message(_, let recipients, let body, _): - if Guard.main.paymentForNewTournament() != nil { - MessageComposeView(recipients: recipients, body: body) { result in - switch result { - case .cancelled: - break - case .failed: - self.sentError = .messageFailed - case .sent: - if networkMonitor.connected == false { - self.contactType = nil - if team.getPhoneNumbers().isEmpty == false { - self.sentError = .uncalledTeams([team]) - } else { - self.sentError = .messageNotSent - } + MessageComposeView(recipients: recipients, body: body) { result in + switch result { + case .cancelled: + break + case .failed: + self.sentError = .messageFailed + case .sent: + if networkMonitor.connected == false { + self.contactType = nil + if team.getPhoneNumbers().isEmpty == false { + self.sentError = .uncalledTeams([team]) + } else { + self.sentError = .messageNotSent } - @unknown default: - break } + @unknown default: + break } - } else { - SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) - .environment(\.colorScheme, .light) } case .mail(_, let recipients, let bccRecipients, let body, let subject, _): - if Guard.main.paymentForNewTournament() != nil { - MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in - switch result { - case .cancelled, .saved: + MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in + switch result { + case .cancelled, .saved: + self.contactType = nil + case .failed: + self.contactType = nil + self.sentError = .mailFailed + case .sent: + if networkMonitor.connected == false { self.contactType = nil - case .failed: - self.contactType = nil - self.sentError = .mailFailed - case .sent: - if networkMonitor.connected == false { - self.contactType = nil - if team.getMail().isEmpty == false { - self.sentError = .uncalledTeams([team]) - } else { - self.sentError = .mailNotSent - } + if team.getMail().isEmpty == false { + self.sentError = .uncalledTeams([team]) + } else { + self.sentError = .mailNotSent } - @unknown default: - break } + @unknown default: + break } - } else { - SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) - .environment(\.colorScheme, .light) } } } 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/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)) diff --git a/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift b/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift index 3527cb5..1cb2854 100644 --- a/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift +++ b/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift @@ -63,12 +63,16 @@ struct PaymentStatusView: View { EmptyView() } }.onAppear { -// self.payment = nil - self.payment = Guard.main.paymentForNewTournament() + self._loadPayment() } } + fileprivate func _loadPayment() { + Task { + self.payment = await Guard.main.paymentForNewTournament() + } + } } struct FreeTournamentTip: Tip { diff --git a/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift b/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift index 0549710..1df730f 100644 --- a/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift +++ b/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift @@ -110,7 +110,7 @@ struct PurchaseView: View { var body: some View { HStack { - Image(systemName: self.purchaseRow.item.systemImage) + Image(systemName: self.purchaseRow.item.summarySystemImage) .foregroundColor(.accentColor).font(.title2) VStack(alignment: .leading) { Text(self.purchaseRow.name) diff --git a/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift b/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift index 8673763..fa861a1 100644 --- a/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift +++ b/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift @@ -238,14 +238,9 @@ struct SubscriptionView: View { fileprivate func _restore() { Task { - do { - self.isRestoring = true - try await Guard.main.refreshPurchasedAppleProducts() - self.isRestoring = false - } catch { - self.isRestoring = false - Logger.error(error) - } + self.isRestoring = true + await Guard.main.refreshPurchases() + self.isRestoring = 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 c87755a..6f80d8c 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -218,10 +218,12 @@ struct TournamentView: View { #if DEBUG Button { - do { - try self.tournament.payIfNecessary() - } catch { - Logger.error(error) + Task { + do { + try await self.tournament.payIfNecessary() + } catch { + Logger.error(error) + } } } label: { Label("Payer le tournoi", systemImage: "dollarsign.circle.fill") @@ -238,10 +240,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 +292,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") } @@ -309,6 +311,10 @@ 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()