diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5200355..fce005c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -130,6 +130,9 @@ C49C73142D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; }; C49C73152D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; }; C49C73162D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; }; + C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; }; + C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; }; + C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; }; C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; }; C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; }; C49EF0262BD80AE80077B5AA /* SubscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */; }; @@ -906,6 +909,9 @@ FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.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 */; }; + FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; @@ -1089,6 +1095,7 @@ C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusView.swift; sourceTree = ""; }; C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = ""; }; C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPaymentType.swift; sourceTree = ""; }; + C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionComparator.swift; sourceTree = ""; }; C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = ""; }; C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = ""; }; @@ -1346,6 +1353,7 @@ FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = ""; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; 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 = ""; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = ""; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = ""; }; FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = ""; }; @@ -2225,6 +2233,7 @@ FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */, C49EF01A2BD6A1E80077B5AA /* URLs.swift */, + C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */, FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */, ); path = Utils; @@ -2263,6 +2272,7 @@ FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */, + FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */, FF1162882BD0523B000C4809 /* Components */, ); path = Planning; @@ -2651,6 +2661,7 @@ FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */, FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */, + C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, @@ -2748,6 +2759,7 @@ FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, + FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */, @@ -2962,6 +2974,7 @@ FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */, FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */, FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */, + C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */, FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */, FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */, FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */, @@ -3061,6 +3074,7 @@ FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, + FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */, @@ -3252,6 +3266,7 @@ FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */, FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */, FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */, + C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */, FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */, FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */, FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */, @@ -3351,6 +3366,7 @@ FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, + FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */, FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */, @@ -3613,7 +3629,6 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; - GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3635,14 +3650,13 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.8; + MARKETING_VERSION = 1.1.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -3660,7 +3674,6 @@ DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; - GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3682,14 +3695,13 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.8; + MARKETING_VERSION = 1.1.13; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/PadelClub/Data/Federal/FederalTournamentHolder.swift b/PadelClub/Data/Federal/FederalTournamentHolder.swift index 4ee6bd6..594e98b 100644 --- a/PadelClub/Data/Federal/FederalTournamentHolder.swift +++ b/PadelClub/Data/Federal/FederalTournamentHolder.swift @@ -22,7 +22,6 @@ protocol FederalTournamentHolder { } extension FederalTournamentHolder { - func durationLabel() -> String { switch dayDuration { case 1: diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 93007fd..65a459d 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -11,43 +11,50 @@ import SwiftUI @Observable final class TeamRegistration: BaseTeamRegistration, SideStorable { - -// static func resourceName() -> String { "team-registrations" } -// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } -// static func filterByStoreIdentifier() -> Bool { return true } -// static var relationshipNames: [String] = [] -// -// var id: String = Store.randomId() -// var lastUpdate: Date -// var tournament: String -// var groupStage: String? -// var registrationDate: Date? -// var callDate: Date? -// var bracketPosition: Int? -// var groupStagePosition: Int? -// var comment: String? -// var source: String? -// var sourceValue: String? -// var logo: String? -// var name: String? -// -// var walkOut: Bool = false -// var wildCardBracket: Bool = false -// var wildCardGroupStage: Bool = false -// var weight: Int = 0 -// var lockedWeight: Int? -// var confirmationDate: Date? -// var qualified: Bool = false -// var finalRanking: Int? -// var pointsEarned: Int? -// -// var storeId: String? = nil - - init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) { - + + // static func resourceName() -> String { "team-registrations" } + // static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + // static func filterByStoreIdentifier() -> Bool { return true } + // static var relationshipNames: [String] = [] + // + // var id: String = Store.randomId() + // var lastUpdate: Date + // var tournament: String + // var groupStage: String? + // var registrationDate: Date? + // var callDate: Date? + // var bracketPosition: Int? + // var groupStagePosition: Int? + // var comment: String? + // var source: String? + // var sourceValue: String? + // var logo: String? + // var name: String? + // + // var walkOut: Bool = false + // var wildCardBracket: Bool = false + // var wildCardGroupStage: Bool = false + // var weight: Int = 0 + // var lockedWeight: Int? + // var confirmationDate: Date? + // var qualified: Bool = false + // var finalRanking: Int? + // var pointsEarned: Int? + // + // var storeId: String? = nil + + init( + tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, + callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, + comment: String? = nil, source: String? = nil, sourceValue: String? = nil, + logo: String? = nil, name: String? = nil, walkOut: Bool = false, + wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, + lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false + ) { + super.init() - -// self.storeId = tournament + + // self.storeId = tournament self.tournament = tournament self.groupStage = groupStage self.registrationDate = registrationDate ?? Date() @@ -67,83 +74,85 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { self.confirmationDate = confirmationDate self.qualified = qualified } - + func hasRegisteredOnline() -> Bool { players().anySatisfy({ $0.registeredOnline }) } - + func unrankedOrUnknown() -> Bool { players().anySatisfy({ $0.source == nil }) } - func isOutOfTournament() -> Bool { walkOut } - + required init(from decoder: any Decoder) throws { try super.init(from: decoder) } - + var tournamentStore: TournamentStore? { return TournamentLibrary.shared.store(tournamentId: self.tournament) } // MARK: - Computed dependencies - + func unsortedPlayers() -> [PlayerRegistration] { guard let tournamentStore = self.tournamentStore else { return [] } - return tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false } + return tournamentStore.playerRegistrations.filter { + $0.teamRegistration == self.id && $0.coach == false + } } - + // MARK: - - + func deleteTeamScores() { guard let tournamentStore = self.tournamentStore else { return } let ts = tournamentStore.teamScores.filter({ $0.teamRegistration == id }) tournamentStore.teamScores.delete(contentOfs: ts) } - + override func deleteDependencies() { let unsortedPlayers = unsortedPlayers() for player in unsortedPlayers { player.deleteDependencies() } self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers) - + let teamScores = teamScores() for teamScore in teamScores { teamScore.deleteDependencies() } self.tournamentStore?.teamScores.deleteDependencies(teamScores) } - + func hasArrived(isHere: Bool = false) { let unsortedPlayers = unsortedPlayers() unsortedPlayers.forEach({ $0.hasArrived = !isHere }) self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) } - + func isHere() -> Bool { let unsortedPlayers = unsortedPlayers() if unsortedPlayers.isEmpty { return false } return unsortedPlayers.allSatisfy({ $0.hasArrived }) } - func isSeedable() -> Bool { bracketPosition == nil && groupStage == nil } - + func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { - var teamPosition : TeamPosition { + var teamPosition: TeamPosition { if let slot { return slot } else { let matchIndex = match.index let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) - let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) + let isUpper = + RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) + < (numberOfMatches / 2) var teamPosition = slot ?? (isUpper ? .one : .two) if opposingSeeding { teamPosition = slot ?? (isUpper ? .two : .one) @@ -160,7 +169,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } if let tournament = tournamentObject() { if let index = index(in: tournament.selectedSortedTeams()) { - let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition, drawType: .seed) + let drawLog = DrawLog( + tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, + drawTeamPosition: teamPosition, drawType: .seed) do { try tournamentStore?.drawLogs.addOrUpdate(instance: drawLog) } catch { @@ -170,7 +181,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { tournament.updateTeamScores(in: bracketPosition) } } - + func expectedSummonDate() -> Date? { if let groupStageStartDate = groupStageObject()?.startDate { return groupStageStartDate @@ -179,11 +190,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } return nil } - + var initialWeight: Int { return lockedWeight ?? weight } - + func called() -> Bool { return callDate != nil } @@ -195,36 +206,36 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { func getPhoneNumbers() -> [String] { return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false }) } - + func getMail() -> [String] { let mails = players().compactMap({ $0.email }) return mails } - + func isImported() -> Bool { let unsortedPlayers = unsortedPlayers() if unsortedPlayers.isEmpty { return false } return unsortedPlayers.allSatisfy({ $0.isImported() }) } - + func isWildCard() -> Bool { return wildCardBracket || wildCardGroupStage } - + func isPlaying() -> Bool { return currentMatch() != nil } - + func currentMatch() -> Match? { return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() }) } - + func teamScores() -> [TeamScore] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.teamScores.filter({ $0.teamRegistration == id }) } - + func wins() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter({ $0.winningTeamId == id }) @@ -234,7 +245,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter({ $0.losingTeamId == id }) } - + func matches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id }) @@ -243,62 +254,74 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { var tournamentCategory: TournamentCategory { tournamentObject()?.tournamentCategory ?? .men } - + @objc var canonicalName: String { players().map { $0.canonicalName }.joined(separator: " ") } - + func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool { guard let codeClubOrClubName else { return true } return unsortedPlayers().anySatisfy({ - $0.clubName?.contains(codeClubOrClubName) == true || $0.clubName?.contains(codeClubOrClubName) == true + $0.clubName?.contains(codeClubOrClubName) == true + || $0.clubName?.contains(codeClubOrClubName) == true }) } - + func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) { self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory) } - func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&") -> String { + func teamLabel( + _ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&" + ) -> String { if let name { return name } - return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " \(separator) ") + return players().map { $0.playerLabel(displayStyle) }.joined( + separator: twoLines ? "\n" : " \(separator) ") } - + func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String { - [displayTeamName ? name : nil, displayRank ? seedIndex() : nil, displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel()].compactMap({ $0 }).joined(separator: " ") + [ + displayTeamName ? name : nil, displayRank ? seedIndex() : nil, + displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel(), + ].compactMap({ $0 }).joined(separator: " ") } - + func seedIndex() -> String? { guard let tournament = tournamentObject() else { return nil } guard let index = index(in: tournament.selectedSortedTeams()) else { return nil } return "(\(index + 1))" } - + func index(in teams: [TeamRegistration]) -> Int? { return teams.firstIndex(where: { $0.id == id }) } - func formattedSeed(in teams: [TeamRegistration]) -> String { - if let index = index(in: teams) { + func formattedSeed(in teams: [TeamRegistration]? = nil) -> String { + let selectedSortedTeams = teams ?? tournamentObject()?.selectedSortedTeams() ?? [] + if let index = index(in: selectedSortedTeams) { return "#\(index + 1)" } else { return "###" } } - + func contains(_ searchField: String) -> Bool { - return unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true + return unsortedPlayers().anySatisfy({ $0.contains(searchField) }) + || self.name?.localizedCaseInsensitiveContains(searchField) == true } func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool { - let arrayOfIds : [String] = unsortedPlayers().compactMap({ $0.licenceId?.strippedLicense?.canonicalVersion }) - let ids : Set = Set(arrayOfIds.sorted()) - let searchedIds = Set(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted()) + let arrayOfIds: [String] = unsortedPlayers().compactMap({ + $0.licenceId?.strippedLicense?.canonicalVersion + }) + let ids: Set = Set(arrayOfIds.sorted()) + let searchedIds = Set( + playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted()) if ids.isEmpty || searchedIds.isEmpty { return false } return ids.hashValue == searchedIds.hashValue } - + func includes(players: [PlayerRegistration]) -> Bool { let unsortedPlayers = unsortedPlayers() guard players.count == unsortedPlayers.count else { return false } @@ -308,32 +331,33 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } } } - + func includes(player: PlayerRegistration) -> Bool { return unsortedPlayers().anySatisfy { _player in _player.isSameAs(player) } } - + func canPlay() -> Bool { let unsortedPlayers = unsortedPlayers() if unsortedPlayers.isEmpty { return false } - return matches().isEmpty == false || unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived }) + return matches().isEmpty == false + || unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived }) } - + func availableForSeedPick() -> Bool { return groupStage == nil && bracketPosition == nil } - + func inGroupStage() -> Bool { return groupStagePosition != nil } - + func inRound() -> Bool { return bracketPosition != nil } - + func positionLabel() -> String? { if groupStagePosition != nil { return "Poule" } if let initialRound = initialRound() { @@ -342,62 +366,75 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { return nil } } - + func initialRoundColor() -> Color? { if walkOut { return Color.logoRed } if groupStagePosition != nil { return Color.blue } - if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] { + if let initialRound = initialRound(), + let colorHex = RoundRule.colors[safe: initialRound.index] + { return Color(uiColor: .init(fromHex: colorHex)) } else { return nil } } - + func resetGroupeStagePosition() { guard let tournamentStore = self.tournamentStore else { return } if let groupStage { - let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map { $0.id } - let teamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) }) + let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map { + $0.id + } + let teamScores = tournamentStore.teamScores.filter({ + $0.teamRegistration == id && matches.contains($0.match) + }) tournamentStore.teamScores.delete(contentOfs: teamScores) } //groupStageObject()?._matches().forEach({ $0.updateTeamScores() }) groupStage = nil groupStagePosition = nil } - + func resetBracketPosition() { guard let tournamentStore = self.tournamentStore else { return } let matches = tournamentStore.matches.filter({ $0.groupStage == nil }).map { $0.id } - let teamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) }) + let teamScores = tournamentStore.teamScores.filter({ + $0.teamRegistration == id && matches.contains($0.match) + }) tournamentStore.teamScores.delete(contentOfs: teamScores) self.bracketPosition = nil } - + func resetPositions() { resetGroupeStagePosition() - resetBracketPosition() + resetBracketPosition() } - + func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String { switch exportFormat { case .rawText: - return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name].compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator()) + return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name] + .compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator()) case .csv: - return [index.formatted(), playersPasteData(exportFormat), isWildCard() ? "WC" : weight.formatted()].joined(separator: exportFormat.separator()) + return [ + index.formatted(), playersPasteData(exportFormat), + isWildCard() ? "WC" : weight.formatted(), + ].joined(separator: exportFormat.separator()) } } - + var computedRegistrationDate: Date { return registrationDate ?? .distantFuture } - + func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? { guard let registrationDate else { return nil } - - let formattedDate = registrationDate.formatted(.dateTime.weekday().day().month().hour().minute()) + + let formattedDate = registrationDate.formatted( + .dateTime.weekday().day().month().hour().minute()) let onlineSuffix = hasRegisteredOnline() ? " en ligne" : "" - + switch exportFormat { case .rawText: return "Inscrit\(onlineSuffix) le \(formattedDate)" @@ -405,13 +442,14 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { return formattedDate } } - + func formattedSummonDate(_ exportFormat: ExportFormat = .rawText) -> String? { - + switch exportFormat { case .rawText: if let callDate { - return "Convoqué le " + callDate.formatted(.dateTime.weekday().day().month().hour().minute()) + return "Convoqué le " + + callDate.formatted(.dateTime.weekday().day().month().hour().minute()) } else { return nil } @@ -423,22 +461,31 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } } } - + func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String { switch exportFormat { case .rawText: - return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.newLineSeparator()) + return players().map { $0.pasteData(exportFormat) }.joined( + separator: exportFormat.newLineSeparator()) case .csv: - return players().map { [$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted() ].joined(separator: exportFormat.separator()) }.joined(separator: exportFormat.separator()) + return players().map { + [$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()] + .joined(separator: exportFormat.separator()) + }.joined(separator: exportFormat.separator()) } } - - func updatePlayers(_ players: Set, inTournamentCategory tournamentCategory: TournamentCategory) { + + func updatePlayers( + _ players: Set, + inTournamentCategory tournamentCategory: TournamentCategory + ) { let previousPlayers = Set(unsortedPlayers()) - + players.forEach { player in previousPlayers.forEach { oldPlayer in - if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense, player.licenceId?.strippedLicense != nil { + if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense, + player.licenceId?.strippedLicense != nil + { player.registeredOnline = oldPlayer.registeredOnline player.coach = oldPlayer.coach player.tournamentPlayed = oldPlayer.tournamentPlayed @@ -449,8 +496,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } } } - - + let playersToRemove = previousPlayers.subtracting(players) self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove) setWeight(from: Array(players), inTournamentCategory: tournamentCategory) @@ -458,16 +504,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { players.forEach { player in player.teamRegistration = id } - -// do { -// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) -// } catch { -// Logger.error(error) -// } - } - + + // do { + // try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + // } catch { + // Logger.error(error) + // } + } + typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?) - + func replacementRange() -> TeamRange? { guard let tournamentObject = tournamentObject() else { return nil } guard let index = tournamentObject.indexOf(team: self) else { return nil } @@ -476,7 +522,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { let right = selectedSortedTeams[safe: index + 1] return (left: left, right: right) } - + func replacementRangeExtended() -> TeamRange? { guard let tournamentObject = tournamentObject() else { return nil } guard let groupStagePosition else { return nil } @@ -485,19 +531,23 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { if groupStagePosition == 0 { left = tournamentObject.seeds().last } else { - let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight) + let previousHat = selectedSortedTeams.filter({ + $0.groupStagePosition == groupStagePosition - 1 + }).sorted(by: \.weight) left = previousHat.last } var right: TeamRegistration? = nil if groupStagePosition == tournamentObject.teamsPerGroupStage - 1 { right = nil } else { - let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition + 1 }).sorted(by: \.weight) + let previousHat = selectedSortedTeams.filter({ + $0.groupStagePosition == groupStagePosition + 1 + }).sorted(by: \.weight) right = previousHat.first } return (left: left, right: right) } - + typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool func players() -> [PlayerRegistration] { @@ -506,36 +556,43 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { let predicates: [AreInIncreasingOrder] = [ { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, { $0.rank ?? Int.max < $1.rank ?? Int.max }, - { $0.lastName < $1.lastName}, - { $0.firstName < $1.firstName } + { $0.lastName < $1.lastName }, + { $0.firstName < $1.firstName }, ] - + for predicate in predicates { if !predicate(lhs, rhs) && !predicate(rhs, lhs) { continue } - + return predicate(lhs, rhs) } - + return false } } - + func coaches() -> [PlayerRegistration] { guard let store = self.tournamentStore else { return [] } return store.playerRegistrations.filter { $0.coach } } - - func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) { + + func setWeight( + from players: [PlayerRegistration], + inTournamentCategory tournamentCategory: TournamentCategory + ) { let significantPlayerCount = significantPlayerCount() - weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+) + weight = + (players.prefix(significantPlayerCount).map { $0.computedRank } + + missingPlayerType(inTournamentCategory: tournamentCategory).map { + unrankValue(for: $0 == 1 ? true : false) + }).prefix(significantPlayerCount).reduce(0, +) } - + func significantPlayerCount() -> Int { return tournamentObject()?.significantPlayerCount() ?? 2 } - + func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] { let players = unsortedPlayers() if players.count >= 2 { return [] } @@ -548,16 +605,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } return missing } - + func unrankValue(for malePlayer: Bool) -> Int { return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000 } - + func groupStageObject() -> GroupStage? { guard let groupStage else { return nil } return self.tournamentStore?.groupStages.findById(groupStage) } - + func initialRound() -> Round? { guard let bracketPosition else { return nil } let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2) @@ -567,22 +624,23 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { func initialMatch() -> Match? { guard let bracketPosition else { return nil } guard let initialRoundObject = initialRound() else { return nil } - return self.tournamentStore?.matches.first(where: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }) + return self.tournamentStore?.matches.first(where: { + $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 + }) } func toggleSummonConfirmation() { - if confirmationDate == nil { confirmationDate = Date() } - else { confirmationDate = nil } + if confirmationDate == nil { confirmationDate = Date() } else { confirmationDate = nil } } - + func didConfirmSummon() -> Bool { confirmationDate != nil } - + func tournamentObject() -> Tournament? { return Store.main.findById(tournament) } - + func groupStagePositionAtStep(_ step: Int) -> Int? { guard let groupStagePosition else { return nil } if step == 0 { @@ -592,33 +650,36 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } return nil } - + func wildcardLabel() -> String? { if isWildCard() { - let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")].joined(separator: " ") + let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")] + .joined(separator: " ") return wildcardLabel } else { return nil } } - + var _cachedRestingTime: (Bool, Date?)? - + func restingTime() -> Date? { if let _cachedRestingTime { return _cachedRestingTime.1 } - let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate + let restingTime = matches().filter({ $0.hasEnded() }).sorted( + by: \.computedEndDateForSorting + ).last?.endDate _cachedRestingTime = (true, restingTime) return restingTime } - + func resetRestingTime() { _cachedRestingTime = nil } - + var restingTimeForSorting: Date { restingTime()! } - + func teamNameLabel() -> String { if let name, name.isEmpty == false { return name @@ -626,25 +687,29 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { return "Toute l'équipe" } } - + func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { if let bracketPosition, let drawMatchIndex { return drawMatchIndex != bracketPosition - } else if let bracketPosition { + } else if bracketPosition != nil { return true - } else if let drawMatchIndex { + } else if drawMatchIndex != nil { return true } return false } - + + func shouldDisplayRankAndWeight() -> Bool { + unsortedPlayers().count > 0 + } + func insertOnServer() { self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self) for playerRegistration in self.unsortedPlayers() { playerRegistration.insertOnServer() } } - + } enum TeamDataSource: Int, Codable { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a05d5c0..b1becf1 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -14,12 +14,13 @@ final class Tournament: BaseTournament { @ObservationIgnored var navigationPath: [Screen] = [] - + // internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0) { // super.init() // } - internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) { + + internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) { super.init() self.event = event self.name = name @@ -29,11 +30,7 @@ final class Tournament: BaseTournament { #if DEBUG self.isPrivate = false #else - if Guard.main.currentPlan == .monthlyUnlimited { - self.isPrivate = true - } else { - self.isPrivate = Guard.main.purchasedTransactions.isEmpty - } + self.isPrivate = isPrivate #endif self.groupStageFormat = groupStageFormat self.roundFormat = roundFormat @@ -89,15 +86,15 @@ final class Tournament: BaseTournament { self.maximumPlayerPerTeam = maximumPlayerPerTeam self.information = information } - + required init(from decoder: Decoder) throws { try super.init(from: decoder) } - + var tournamentStore: TournamentStore? { return TournamentLibrary.shared.store(tournamentId: self.id) } - + override func deleteDependencies() { guard let store = self.tournamentStore else { return } @@ -112,13 +109,13 @@ final class Tournament: BaseTournament { team.deleteDependencies() } store.teamRegistrations.deleteDependencies(teams) - + let groups = Array(store.groupStages) for group in groups { group.deleteDependencies() } store.groupStages.deleteDependencies(groups) - + let rounds = Array(store.rounds) for round in rounds { round.deleteDependencies() @@ -126,34 +123,38 @@ final class Tournament: BaseTournament { store.rounds.deleteDependencies(rounds) store.matchSchedulers.deleteDependencies(self._matchSchedulers()) - + } // MARK: - Computed Dependencies - + func unsortedTeams() -> [TeamRegistration] { guard let tournamentStore = self.tournamentStore else { return [] } return Array(tournamentStore.teamRegistrations) } - + + func unsortedTeamsCount() -> Int { + return self.tournamentStore?.teamRegistrations.count ?? 0 + } + func groupStages(atStep step: Int = 0) -> [GroupStage] { guard let tournamentStore = self.tournamentStore else { return [] } let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } return groupStages.sorted(by: \.index) } - + func allGroupStages() -> [GroupStage] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder) } - + func allRounds() -> [Round] { guard let tournamentStore = self.tournamentStore else { return [] } return Array(tournamentStore.rounds) } - + // MARK: - - + enum State { case initial case build @@ -161,7 +162,7 @@ final class Tournament: BaseTournament { case canceled case finished } - + func eventLabel() -> String { if let event = eventObject(), let name = event.name { return name @@ -169,15 +170,15 @@ final class Tournament: BaseTournament { return "" } } - + func publishedTournamentDate() -> Date { return min(creationDate.tomorrowAtNine, startDate) } - + func publishedTeamsDate() -> Date { return self.startDate } - + func canBePublished() -> Bool { switch state() { case .build, .finished, .running: @@ -186,11 +187,11 @@ final class Tournament: BaseTournament { return false } } - + func isTournamentPublished() -> Bool { return (Date() >= publishedTournamentDate()) || publishTournament } - + func areTeamsPublished() -> Bool { return Date() >= startDate || publishTeams } @@ -198,7 +199,7 @@ final class Tournament: BaseTournament { func areSummonsPublished() -> Bool { return Date() >= startDate || publishSummons } - + fileprivate func _publishedDateFromMatches(_ matches: [Match]) -> Date? { let startDates: [Date] = matches.compactMap { $0.startDate } let sortedDates: [Date] = startDates.sorted() @@ -213,12 +214,12 @@ final class Tournament: BaseTournament { return startDate } } - + func publishedGroupStagesDate() -> Date? { let matches: [Match] = self.groupStages().flatMap { $0.playedMatches() } return self._publishedDateFromMatches(matches) } - + func areGroupStagesPublished() -> Bool { if publishGroupStages { return true } if let publishedGroupStagesDate = publishedGroupStagesDate() { @@ -227,12 +228,12 @@ final class Tournament: BaseTournament { return false } } - + func publishedBracketsDate() -> Date? { let matches: [Match] = self.rounds().flatMap { $0.playedMatches() } return self._publishedDateFromMatches(matches) } - + func areBracketsPublished() -> Bool { if publishBrackets { return true } if let publishedBracketsDate = publishedBracketsDate() { @@ -241,7 +242,7 @@ final class Tournament: BaseTournament { return false } } - + func shareURL(_ pageLink: PageLink = .matches) -> URL? { if pageLink == .clubBroadcast { let club = club() @@ -266,18 +267,19 @@ defer { #endif return Set(runningMatches.compactMap { $0.courtIndex }).sorted() } - + func hasStarted() -> Bool { return startDate <= Date() } - + func eventObject() -> Event? { guard let event else { return nil } return Store.main.findById(event) } - + func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { - let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams() + let _selectedSortedTeams = selectedSortedTeams() + let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams) switch exportFormat { case .rawText: return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2)) @@ -290,11 +292,11 @@ defer { return teamPaste.joined(separator: exportFormat.newLineSeparator()) } } - + func club() -> Club? { return eventObject()?.clubObject() } - + func locationLabel(_ displayStyle: DisplayStyle = .wide) -> String { if let club = club() { switch displayStyle { @@ -307,29 +309,29 @@ defer { return "" } } - + func hasEnded() -> Bool { return endDate != nil } - + func state() -> State { if self.isCanceled == true { return .canceled } - + if self.hasEnded() { return .finished } - + let isBuild = (groupStageCount > 0 && groupStages().isEmpty == false) || rounds().isEmpty == false - + if isBuild && startDate <= Date() { return .running } - + if isBuild { return .build } return .initial } - + func seededTeams() -> [TeamRegistration] { return selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil }) } @@ -347,7 +349,7 @@ defer { let seeds = max(selectedSortedTeams.count - groupStageSpots() , 0) return Array(selectedSortedTeams.prefix(seeds)) } - + func availableSeeds() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -360,7 +362,7 @@ defer { return seeds().filter { $0.isSeedable() } } - + func lastSeedRound() -> Int { if let last = seeds().filter({ $0.bracketPosition != nil }).last { return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2) @@ -368,12 +370,12 @@ defer { return 0 } } - + func getRound(atRoundIndex roundIndex: Int) -> Round? { return self.tournamentStore?.rounds.first(where: { $0.index == roundIndex }) // return Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first } - + func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? [] } @@ -381,7 +383,7 @@ defer { func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? [] } - + func availableSeedGroups() -> [SeedInterval] { let seeds = seeds() var availableSeedGroup = Set() @@ -392,7 +394,7 @@ defer { } return availableSeedGroup.sorted(by: <) } - + func seedGroup(for alreadySetupSeeds: Int) -> SeedInterval? { switch alreadySetupSeeds { case 0...1: @@ -411,7 +413,7 @@ defer { return nil } } - + func availableSeedGroup() -> SeedInterval? { let seeds = seeds() if let firstIndex = seeds.firstIndex(where: { $0.isSeedable() }) { @@ -420,18 +422,18 @@ defer { } return nil } - + func randomSeed(fromSeedGroup seedGroup: SeedInterval) -> TeamRegistration? { let availableSeeds = seeds(inSeedGroup: seedGroup) return availableSeeds.randomElement() } - + func seeds(inSeedGroup seedGroup: SeedInterval) -> [TeamRegistration] { let availableSeedInSeedGroup = (seedGroup.last - seedGroup.first) + 1 let availableSeeds = seeds().dropFirst(seedGroup.first - 1).prefix(availableSeedInSeedGroup).filter({ $0.isSeedable() }) return availableSeeds } - + func seedGroupAvailable(atRoundIndex roundIndex: Int) -> SeedInterval? { if let availableSeedGroup = availableSeedGroup() { return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: availableSeedGroup) @@ -441,7 +443,7 @@ defer { } func seedGroupAvailable(atRoundIndex roundIndex: Int, availableSeedGroup: SeedInterval) -> SeedInterval? { - + if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() { if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup } @@ -466,11 +468,11 @@ defer { } } } - + return nil } - - func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) { + + func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) { if seedGroup == SeedInterval(first: 1, last: 2) { let seeds = seeds() if let matches = getRound(atRoundIndex: roundIndex)?.playedMatches() { @@ -482,11 +484,11 @@ defer { } } } else { - + let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeeds = seeds(inSeedGroup: seedGroup) - + if seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 { var spots = [Match]() spots.append(availableSeedSpot[1]) @@ -502,7 +504,7 @@ defer { seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) } } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { - + let spots = availableSeedOpponentSpot.shuffled() for (index, seed) in availableSeeds.enumerated() { seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) @@ -517,8 +519,8 @@ defer { } } } - - + + func inscriptionClosed() -> Bool { closedRegistrationDate != nil } @@ -531,14 +533,14 @@ defer { func matchesWithSpace() -> [Match] { getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? [] } - + func getActiveRound(withSeeds: Bool = false) -> Round? { let rounds: [Round] = self.rounds() let unfinishedRounds: [Round] = rounds.filter { $0.hasStarted() && $0.hasEnded() == false } let sortedRounds: [Round] = unfinishedRounds.sorted(by: \.index).reversed() - + let round = sortedRounds.first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first - + if withSeeds { if round?.seeds().isEmpty == false { return round @@ -556,11 +558,11 @@ defer { DateInterval(event: event.id, courtIndex: match.courtIndex!, startDate: match.startDate!, endDate: match.estimatedEndDate(additionalEstimationDuration)!) } } - + func allRoundMatches() -> [Match] { return allRounds().flatMap { $0._matches() } } - + func allMatches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter { $0.disabled == false } @@ -576,18 +578,18 @@ defer { let rounds: [Round] = tournamentStore.rounds.filter { $0.isUpperBracket() } return rounds.sorted(by: \.index).reversed() } - - func sortedTeams() -> [TeamRegistration] { - let teams = selectedSortedTeams() + + func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { + let teams = selectedSortedTeams return teams + waitingListTeams(in: teams, includingWalkOuts: true) } - - func waitingListSortedTeams() -> [TeamRegistration] { - let teams = selectedSortedTeams() + + func waitingListSortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { + let teams = selectedSortedTeams return waitingListTeams(in: teams, includingWalkOuts: false) } - + func selectedSortedTeams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -598,7 +600,7 @@ defer { #endif var _sortedTeams : [TeamRegistration] = [] var _teams = unsortedTeams().filter({ $0.isOutOfTournament() == false }) - + if let closedRegistrationDate { _teams = _teams.filter({ team in if let registrationDate = team.registrationDate { @@ -622,26 +624,26 @@ defer { var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if bracketSeeds < 0 { bracketSeeds = 0 } - + if prioritizeClubMembers { - + var bracketTeams: [TeamRegistration] = [] bracketTeams.append(contentsOf: _completeTeams.filter { $0.hasMemberOfClub(clubName) }) - + let others: [TeamRegistration] = _completeTeams.filter { $0.hasMemberOfClub(clubName) == false } let sortedOthers: [TeamRegistration] = others.sorted(using: defaultSorting, order: .ascending) bracketTeams.append(contentsOf: sortedOthers) - + bracketTeams = bracketTeams .prefix(bracketSeeds) .sorted(using: _currentSelectionSorting, order: .ascending) bracketTeams.append(contentsOf: wcBracket) - + // let bracketTeams: [TeamRegistration] = (_completeTeams.filter { $0.hasMemberOfClub(clubName) } + _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket - + let groupStageTeamsNoFiltering = Set(_completeTeams).subtracting(bracketTeams) let groupStageTeams = (groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) } + groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage - + _sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending) } else { let bracketTeams = _completeTeams.prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket @@ -650,7 +652,7 @@ defer { } return _sortedTeams } - + func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] { let waitingList = Set(unsortedTeams()).subtracting(teams) let waitings = waitingList.filter { $0.isOutOfTournament() == false }.sorted(using: _defaultSorting(), order: .ascending) @@ -661,15 +663,15 @@ defer { return waitings } } - + func bracketCut(teamCount: Int) -> Int { return max(0, teamCount - groupStageCut()) } - + func groupStageCut() -> Int { return groupStageSpots() } - + func cutLabel(index: Int, teamCount: Int?) -> String { let _teamCount = teamCount ?? selectedSortedTeams().count let bracketCut = bracketCut(teamCount: _teamCount) @@ -705,7 +707,7 @@ defer { return tournamentStore.teamRegistrations.filter { $0.walkOut == true } // return Store.main.filter { $0.tournament == self.id && $0.walkOut == true } } - + func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] { var duplicates = [PlayerRegistration]() Set(players.compactMap({ $0.licenceId })).forEach { licenceId in @@ -716,11 +718,11 @@ defer { } return duplicates } - + func homonyms(in players: [PlayerRegistration]) -> [PlayerRegistration] { players.filter({ $0.hasHomonym() }) } - + func unsortedPlayers() -> [PlayerRegistration] { guard let tournamentStore = self.tournamentStore else { return [] } return Array(tournamentStore.playerRegistrations) @@ -762,12 +764,12 @@ defer { var clubName: String? { return self.eventObject()?.clubObject()?.name } - + //todo func significantPlayerCount() -> Int { return minimumPlayerPerTeam } - + func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { if startDate.isInCurrentYear() == false { return [] @@ -776,7 +778,7 @@ defer { return isPlayerRankInadequate(player: player) } } - + func isPlayerRankInadequate(player: PlayerHolder) -> Bool { guard let rank = player.getRank() else { return false } let _rank = player.male ? rank : rank + PlayerRegistration.addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0) @@ -786,7 +788,7 @@ defer { return false } } - + func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { if startDate.isInCurrentYear() == false { return [] @@ -795,7 +797,7 @@ defer { return isPlayerAgeInadequate(player: player) } } - + func isPlayerAgeInadequate(player: PlayerHolder) -> Bool { guard let computedAge = player.computedAge else { return false } if federalTournamentAge.isAgeValid(age: computedAge) == false { @@ -804,7 +806,7 @@ defer { return false } } - + func licenseYearValidity() -> Int { if startDate.get(.month) > 8 { return startDate.get(.year) + 1 @@ -812,7 +814,7 @@ defer { return startDate.get(.year) } } - + func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] { let licenseYearValidity = self.licenseYearValidity() return players.filter({ player in @@ -823,20 +825,20 @@ defer { // Player is not imported: validate license and handle `isImported` flag for non-imported players let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false - + // If global `isImported` is true, check license number as well let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity) - + return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag } }) } - + func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { guard let seedIndex else { return nil } return selectedSortedTeams()[safe: seedIndex]?.callDate } - + func importTeams(_ teams: [FileImportManager.TeamHolder]) { var teamsToImport = [TeamRegistration]() let players = players().filter { $0.licenceId != nil } @@ -861,17 +863,17 @@ defer { teamsToImport.append(newTeam) } } - + if let tournamentStore = self.tournamentStore { tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport) tournamentStore.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players }) } - + if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty { setGroupStage(randomize: groupStageSortMode == .random) } } - + func maximumCourtsPerGroupSage() -> Int { if teamsPerGroupStage > 1 { return min(teamsPerGroupStage / 2, courtCount) @@ -879,10 +881,9 @@ defer { return max(1, courtCount) } } - - func registrationIssues() -> Int { + + func registrationIssues(selectedTeams: [TeamRegistration]) -> Int { let players : [PlayerRegistration] = unsortedPlayers() - let selectedTeams : [TeamRegistration] = selectedSortedTeams() let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let duplicates : [PlayerRegistration] = duplicates(in: players) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) @@ -898,20 +899,20 @@ defer { return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count } - + func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool { guard let summonDate = team.callDate else { return true } let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate guard let expectedSummonDate else { return true } return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame } - + func groupStagesMatches(atStep step: Int = 0) -> [Match] { return groupStages(atStep: step).flatMap({ $0._matches() }) // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - - static let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)] + + static let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.computedOrder)] static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME @@ -923,7 +924,7 @@ defer { #endif return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending) } - + static func runningMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -934,7 +935,7 @@ defer { #endif return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) } - + static func readyMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -945,7 +946,7 @@ defer { #endif return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) } - + static func matchesLeft(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() @@ -972,12 +973,12 @@ defer { return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()) } } - + func teamsRanked() -> [TeamRegistration] { let selected = selectedSortedTeams().filter({ $0.finalRanking != nil }) return selected.sorted(by: \.finalRanking!, order: .ascending) } - + private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) { for key in dictionary.keys { if var stringArray = dictionary[key] { @@ -996,7 +997,7 @@ defer { let lastStep = lastStep() if rounds.isEmpty, lastStep > 0 { let groupStages = groupStages(atStep: lastStep) - + for groupStage in groupStages { let groupStageTeams = groupStage.teams(true) for teamIndex in 0.. qualifiedPerGroupStage ? groupStageAdditionalQualified : 0) if let existingTeams = teams[_index] { teams[_index] = existingTeams + [team.id] @@ -1111,15 +1112,15 @@ defer { } } } - + return teams } - + func setRankings(finalRanks: [Int: [String]]) async -> [Int: [TeamRegistration]] { guard let tournamentStore = self.tournamentStore else { return [:] } - + var rankings: [Int: [TeamRegistration]] = [:] - + finalRanks.keys.sorted().forEach { rank in if let rankedTeamIds = finalRanks[rank] { let teams: [TeamRegistration] = rankedTeamIds.compactMap { tournamentStore.teamRegistrations.findById($0) } @@ -1135,7 +1136,7 @@ defer { } } } - + do { try self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) } catch { @@ -1150,10 +1151,10 @@ defer { Logger.error(error) } } - + return rankings } - + func lockRegistration() { closedRegistrationDate = Date() let count = selectedSortedTeams().count @@ -1166,7 +1167,7 @@ defer { } self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) } - + func unlockRegistration() { closedRegistrationDate = nil let teams = unsortedTeams() @@ -1186,9 +1187,9 @@ defer { } self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) } - + func updateRank(to newDate: Date?) async throws { - + #if DEBUG_TIME let start = Date() defer { @@ -1202,18 +1203,18 @@ defer { // Fetch current month data only once let monthData = currentMonthData() - + if monthData == nil { async let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) async let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) - + let formatted = URL.importDateFormatter.string(from: newDate) let newMonthData = MonthData(monthKey: formatted) - + newMonthData.maleUnrankedValue = await lastRankMan newMonthData.femaleUnrankedValue = await lastRankWoman - + do { try DataStore.shared.monthData.addOrUpdate(instance: newMonthData) } catch { @@ -1227,7 +1228,7 @@ defer { // Fetch only the required files let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } guard !dataURLs.isEmpty else { return } // Early return if no files found - + let sources = dataURLs.map { CSVParser(url: $0) } let players = unsortedPlayers() try await players.concurrentForEach { player in @@ -1240,11 +1241,11 @@ defer { func missingUnrankedValue() -> Bool { return maleUnrankedValue == nil || femaleUnrankedValue == nil } - + func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? { return unsortedTeams().first(where: { $0.includes(players: players) }) } - + func tournamentTitle(_ displayStyle: DisplayStyle = .wide, hideSenior: Bool = false) -> String { if tournamentLevel == .unlisted, displayStyle == .title { if let name { @@ -1266,7 +1267,7 @@ defer { return title } } - + func localizedTournamentType() -> String { switch tournamentLevel { case .unlisted: @@ -1275,19 +1276,19 @@ defer { return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedLabel(.short) } } - + func hideWeight() -> Bool { return hideTeamsWeight || tournamentLevel.hideWeight() } - + func isAnimation() -> Bool { federalLevelCategory == .unlisted } - + func subtitle(_ displayStyle: DisplayStyle = .wide) -> String { return name ?? "" } - + func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .title: @@ -1298,11 +1299,11 @@ defer { startDate.formatted(date: .numeric, time: .omitted) } } - + func qualifiedFromGroupStage() -> Int { return groupStageCount * qualifiedPerGroupStage } - + func availableQualifiedTeams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME @@ -1315,11 +1316,11 @@ defer { return unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil }) } - + func qualifiedTeams() -> [TeamRegistration] { return unsortedTeams().filter({ $0.qualified }) } - + func moreQualifiedToDraw() -> Int { return max((qualifiedFromGroupStage() + groupStageAdditionalQualified) - qualifiedTeams().count, 0) } @@ -1343,18 +1344,18 @@ defer { return groupStages.allSatisfy({ $0.hasEnded() }) //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } - + func groupStageLoserBracketAreOver() -> Bool { guard let groupStageLoserBracket = groupStageLoserBracket() else { return true } return groupStageLoserBracket.hasEnded() } - + fileprivate func _paymentMethodMessage() -> String? { return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods } - + var entryFeeMessage: String { if let entryFee { let message: String = "Inscription : \(entryFee.formatted(.currency(code: Locale.defaultCurrency()))) par joueur." @@ -1363,7 +1364,7 @@ defer { return "Inscription : gratuite." } } - + func umpireMail() -> [String]? { return [DataStore.shared.user.email] } @@ -1375,7 +1376,7 @@ defer { return 0.0 } } - + func paidCompletion() -> Double { let selectedPlayers = selectedPlayers() if selectedPlayers.isEmpty { return 0 } @@ -1406,7 +1407,7 @@ defer { let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } - + func scheduleStatus() async -> TournamentStatus { let allMatches = allMatches() let ready = allMatches.filter({ $0.startDate != nil }) @@ -1416,7 +1417,7 @@ defer { let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } - + func callStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } @@ -1427,7 +1428,7 @@ defer { let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } - + func confirmedSummonStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { $0.confirmationDate != nil } @@ -1436,7 +1437,7 @@ defer { let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } - + func bracketStatus() async -> (status: String, description: String?, cut: TeamRegistration.TeamRange?) { let availableSeeds = availableSeeds() var description: String? = nil @@ -1449,32 +1450,32 @@ defer { description = "placer \(availableQualifiedTeams.count) qualifié" + availableQualifiedTeams.count.pluralSuffix } } - + var cut: TeamRegistration.TeamRange? = nil if description == nil && isAnimation() == false { cut = TeamRegistration.TeamRange(availableSeeds.first, availableSeeds.last) } - + if let round = getActiveRound() { return ([round.roundTitle(.short), round.roundStatus()].joined(separator: " ").lowercased(), description, cut) } else { return ("", description, nil) } } - + func groupStageStatus() async -> (status: String, cut: TeamRegistration.TeamRange?) { let groupStageTeams = groupStageTeams() let groupStageTeamsCount = groupStageTeams.count if groupStageTeamsCount == 0 || groupStageTeamsCount != groupStageSpots() { return ("à compléter", nil) } - + let cut : TeamRegistration.TeamRange? = isAnimation() ? nil : TeamRegistration.TeamRange(groupStageTeams.first, groupStageTeams.last) - + let runningGroupStages = groupStages().filter({ $0.isRunning() }) if groupStagesAreOver() { return ("terminées", cut) } if runningGroupStages.isEmpty { - + let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }) if ongoingGroupStages.isEmpty == false { return ("Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut) @@ -1488,7 +1489,7 @@ defer { func settingsDescriptionLocalizedLabel() -> String { [courtCount.formatted() + " terrain\(courtCount.pluralSuffix)", entryFeeMessage].joined(separator: ", ") } - + func structureDescriptionLocalizedLabel() -> String { let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ") @@ -1498,7 +1499,7 @@ defer { resetBracketPosition() deleteStructure() deleteGroupStages() - + switch preset { case .doubleGroupStage: buildGroupStages() @@ -1510,7 +1511,7 @@ defer { buildBracket() } } - + func addWildCardIfNeeded(_ count: Int, _ type: MatchType) { let currentCount = selectedSortedTeams().filter({ if type == .bracket { @@ -1519,13 +1520,13 @@ defer { return $0.wildCardGroupStage } }).count - + if currentCount < count { let _diff = count - currentCount addWildCard(_diff, type) } } - + func addWildCard(_ count: Int, _ type: MatchType) { let wcs = (0.. Int { let bracketTeamCount = teamCount - (teamsPerGroupStage - qualifiedPerGroupStage) * groupStageCount + (groupStageAdditionalQualified * (groupStageCount > 0 ? 1 : 0)) return bracketTeamCount @@ -1584,15 +1587,15 @@ defer { func buildBracket() { guard rounds().isEmpty else { return } let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount()) - + let rounds = (0.. Bool { return entryFee == nil || entryFee == 0 } - + func indexOf(team: TeamRegistration) -> Int? { return selectedSortedTeams().firstIndex(where: { $0.id == team.id }) } - + func labelIndexOf(team: TeamRegistration) -> String? { if let teamIndex = indexOf(team: team) { return "Tête de série #" + (teamIndex + 1).formatted() @@ -1719,7 +1722,7 @@ defer { return nil } } - + func addTeam(_ players: Set, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) @@ -1742,7 +1745,7 @@ defer { roundFormat = newValue } } - + var groupStageMatchFormat: MatchFormat { get { groupStageFormat ?? .defaultFormatForMatchType(.groupStage) @@ -1751,7 +1754,7 @@ defer { groupStageFormat = newValue } } - + var loserBracketMatchFormat: MatchFormat { get { loserRoundFormat ?? .defaultFormatForMatchType(.loserBracket) @@ -1760,7 +1763,7 @@ defer { loserRoundFormat = newValue } } - + var groupStageOrderingMode: GroupStageOrderingMode { get { groupStageSortMode @@ -1769,7 +1772,7 @@ defer { groupStageSortMode = newValue } } - + var tournamentCategory: TournamentCategory { get { federalCategory @@ -1783,7 +1786,7 @@ defer { } } } - + var tournamentLevel: TournamentLevel { get { federalLevelCategory @@ -1796,7 +1799,7 @@ defer { matchFormat = roundSmartMatchFormat(5) } } - + var federalTournamentAge: FederalTournamentAge { get { federalAgeCategory @@ -1805,7 +1808,7 @@ defer { federalAgeCategory = newValue } } - + func loserBracketSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForLoserBracketRound(roundIndex) if tournamentLevel == .p25 { return .superTie } @@ -1815,7 +1818,7 @@ defer { return loserBracketMatchFormat } } - + func groupStageSmartMatchFormat() -> MatchFormat { let format = tournamentLevel.federalFormatForGroupStage() if tournamentLevel == .p25 { return .superTie } @@ -1836,12 +1839,31 @@ defer { //enableOnlineRegistration = true registrationDateLimit = deadline(for: .inscription) } + + //self.customizeUsingPreferences() + } + + 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 onlineRegistrationCanBeEnabled() -> Bool { isAnimation() == false } - + func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForBracketRound(roundIndex) if tournamentLevel == .p25 { return .superTie } @@ -1851,7 +1873,7 @@ defer { return matchFormat } } - + private func _defaultSorting() -> [MySortDescriptor] { switch teamSorting { case .rank: @@ -1860,7 +1882,7 @@ defer { [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] } } - + func isSameBuild(_ build: any TournamentBuildHolder) -> Bool { tournamentLevel == build.level && tournamentCategory == build.category @@ -1878,21 +1900,21 @@ defer { func matchScheduler() -> MatchScheduler? { return self._matchSchedulers().first } - + func courtsAvailable() -> [Int] { (0.. MonthData? { guard let rankSourceDate else { return nil } let dateString = URL.importDateFormatter.string(from: rankSourceDate) return DataStore.shared.monthData.first(where: { $0.monthKey == dateString }) } - + var maleUnrankedValue: Int? { return currentMonthData()?.maleUnrankedValue } - + var femaleUnrankedValue: Int? { return currentMonthData()?.femaleUnrankedValue } @@ -1900,16 +1922,16 @@ defer { func courtNameIfAvailable(atIndex courtIndex: Int) -> String? { return club()?.customizedCourts.first(where: { $0.index == courtIndex })?.name } - + func courtName(atIndex courtIndex: Int) -> String { return courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex) } - + func tournamentWinner() -> TeamRegistration? { let finals: Round? = self.tournamentStore?.rounds.first(where: { $0.index == 0 && $0.isUpperBracket() }) return finals?.playedMatches().first?.winner() } - + func getGroupStageChunkValue() -> Int { if groupStageCount > 0 && teamsPerGroupStage >= 2 { let result = courtCount / (teamsPerGroupStage / 2) @@ -1920,7 +1942,7 @@ defer { return 1 } } - + func replacementRangeExtended(groupStagePosition: Int) -> TeamRegistration.TeamRange? { let selectedSortedTeams = selectedSortedTeams() var left: TeamRegistration? = nil @@ -1940,7 +1962,7 @@ defer { return (left: left, right: right) } - + typealias TeamPlacementIssue = (shouldBeInIt: [String], shouldNotBeInIt: [String]) func groupStageTeamPlacementIssue() -> TeamPlacementIssue { let selected = selectedSortedTeams() @@ -1953,7 +1975,7 @@ defer { let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds) return (Array(shouldBeInIt), Array(shouldNotBeInIt)) } - + func bracketTeamPlacementIssue() -> TeamPlacementIssue { let selected = selectedSortedTeams() let allTeams = unsortedTeams() @@ -1967,29 +1989,29 @@ defer { let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds) return (Array(shouldBeInIt), Array(shouldNotBeInIt)) } - + func groupStageLoserBracket() -> Round? { self.tournamentStore?.rounds.first(where: { $0.groupStageLoserBracket }) } - + func groupStageLoserBracketsInitialPlace() -> Int { return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1 } - + func addNewGroupStageStep() { let lastStep = lastStep() + 1 for i in 0.. Int { self.tournamentStore?.groupStages.sorted(by: \.step).last?.step ?? 0 } - + func generateSmartLoserGroupStageBracket() { guard let groupStageLoserBracket = groupStageLoserBracket() else { return } for i in qualifiedPerGroupStage.. [Match] { rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } } @@ -2029,24 +2051,24 @@ defer { func seedsCount() -> Int { selectedSortedTeams().count - groupStageSpots() } - + func lastDrawnDate() -> Date? { drawLogs().last?.drawDate } - + func drawLogs() -> [DrawLog] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.drawLogs.sorted(by: \.drawDate) } - + func seedSpotsLeft() -> Bool { let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false }) if alreadySeededRounds.isEmpty { return true } let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() } - + return spotsLeft.isEmpty == false } - + func isRoundValidForSeeding(roundIndex: Int) -> Bool { if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) { return roundIndex >= lastRoundWithSeeds.index @@ -2055,7 +2077,7 @@ defer { } } - + func updateSeedsBracketPosition() async { await removeAllSeeds() let drawLogs = drawLogs().reversed() @@ -2065,14 +2087,14 @@ defer { drawLog.updateTeamBracketPosition(seed) } } - + do { try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: seeds) } catch { Logger.error(error) } } - + func removeAllSeeds() async { let teams = unsortedTeams() teams.forEach({ team in @@ -2097,7 +2119,7 @@ defer { } catch { Logger.error(error) } - + do { try tournamentStore?.matches.addOrUpdate(contentOfs: allMatches) } catch { @@ -2111,7 +2133,7 @@ defer { } updateTournamentState() } - + func addNewRound(_ roundIndex: Int) async { let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat) let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) @@ -2138,7 +2160,7 @@ defer { match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex)) currentIndex += 1 } - + return match } do { @@ -2156,14 +2178,14 @@ defer { $0._toggleLoserMatchDisableState(true) } } - + func exportedDrawLogs() -> String { var logs : [String] = ["Journal des tirages\n\n"] logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n")) return logs.joined() } - - + + func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool { guard let source = eventObject()?.courtsUnavailability else { return false } let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex }) @@ -2201,7 +2223,7 @@ defer { } let currentTeamCount = unsortedTeamsWithoutWO().count - + if currentTeamCount >= teamCount { if let waitingListLimit = waitingListLimit { let waitingListCount = currentTeamCount - teamCount @@ -2214,9 +2236,9 @@ defer { return .open } - + // MARK: - Status - + func shouldTournamentBeOver() -> Bool { #if _DEBUGING_TIME //DEBUGING TIME let start = Date() @@ -2228,23 +2250,23 @@ defer { if isDeleted == false && hasEnded() == false && hasStarted() { let allMatches = allMatches() let remainingMatches = allMatches.filter({ $0.hasEnded() == false && $0.startDate != nil }) - + let calendar = Calendar.current let anyTomorrow = remainingMatches.anySatisfy({ calendar.isDateInTomorrow($0.startDate!) }) - - + + if anyTomorrow == false, let endDate = allMatches.filter({ $0.hasEnded() }).sorted(by: \.endDate!, order: .ascending).last?.endDate, endDate.timeIntervalSinceNow <= -2 * 3600 { return true } } - + return false } // MARK: - - + func insertOnServer() throws { - + // DataStore.shared.tournaments.writeChangeAndInsertOnServer(instance: self) // // for teamRegistration in self.tournamentStore?.teamRegistrations { @@ -2258,13 +2280,13 @@ defer { // } } - + // MARK: - Payments & Crypto - + enum PaymentError: Error { case cantPayTournament } - + func payIfNecessary() throws { if self.payment != nil { return } if let payment = Guard.main.paymentForNewTournament() { @@ -2274,7 +2296,7 @@ defer { } throw PaymentError.cantPayTournament } - + } extension Bool { @@ -2333,7 +2355,6 @@ extension Bool { //} extension Tournament: FederalTournamentHolder { - func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { if isAnimation() { if let name { @@ -2350,13 +2371,13 @@ extension Tournament: FederalTournamentHolder { var codeClub: String? { club()?.code } - + var holderId: String { id } func clubLabel() -> String { locationLabel() } - + func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String { if isAnimation() { if displayAgeAndCategory(forBuild: build) == false { @@ -2376,7 +2397,7 @@ extension Tournament: FederalTournamentHolder { self ] } - + var dayPeriod: DayPeriod { let day = startDate.get(.weekday) switch day { @@ -2386,7 +2407,7 @@ extension Tournament: FederalTournamentHolder { return .weekend } } - + func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool { if isAnimation() { if let name, name.count < DeviceHelper.maxCharacter() { @@ -2405,15 +2426,15 @@ extension Tournament: TournamentBuildHolder { func buildHolderTitle(_ displayStyle: DisplayStyle) -> String { tournamentTitle(.short) } - + var category: TournamentCategory { tournamentCategory } - + var level: TournamentLevel { tournamentLevel } - + var age: FederalTournamentAge { federalTournamentAge } @@ -2428,28 +2449,34 @@ extension Tournament { } let rankSourceDate = _mostRecentDateAvailable - let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false } + let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false }.sorted(by: \.endDate!, order: .descending) + + var shouldBePrivate = tournaments.first?.isPrivate ?? true + + if Guard.main.currentPlan == .monthlyUnlimited { + shouldBePrivate = false + } else if Guard.main.purchasedTransactions.isEmpty == false { + shouldBePrivate = false + } + let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) //creator: DataStore.shared.user?.id - return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode) + return Tournament(isPrivate: shouldBePrivate, groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode) } static func fake() -> Tournament { return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil) } - + } extension Tournament { func deadline(for type: TournamentDeadlineType) -> Date? { guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } - - var daysOffset = type.daysOffset - if tournamentLevel == .p500 { - daysOffset += 7 - } + + var daysOffset = type.daysOffset(level: tournamentLevel) if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) { let startOfDay = Calendar.current.startOfDay(for: date) return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) @@ -2461,7 +2488,7 @@ extension Tournament { /// Warning: if the enum has more than 10 cases, the payment algo is broken enum TournamentPayment: Int, CaseIterable { case free, unit, subscriptionUnit, unlimited - + var isSubscription: Bool { switch self { case .subscriptionUnit, .unlimited: @@ -2470,5 +2497,5 @@ enum TournamentPayment: Int, CaseIterable { return false } } - + } diff --git a/PadelClub/HTML Templates/bracket-template.html b/PadelClub/HTML Templates/bracket-template.html index c344123..8c3ec91 100644 --- a/PadelClub/HTML Templates/bracket-template.html +++ b/PadelClub/HTML Templates/bracket-template.html @@ -1,4 +1,7 @@
    -
  •  {{roundLabel}}
  • +
  • +  {{roundLabel}} +
    {{formatLabel}}
    +
  • {{match-template}}
diff --git a/PadelClub/HTML Templates/groupstage-template.html b/PadelClub/HTML Templates/groupstage-template.html index 6e203b9..32de623 100644 --- a/PadelClub/HTML Templates/groupstage-template.html +++ b/PadelClub/HTML Templates/groupstage-template.html @@ -82,6 +82,7 @@ body{

{{bracketTitle}}

{{bracketStartDate}}

+

{{formatLabel}}

diff --git a/PadelClub/HTML Templates/player-template.html b/PadelClub/HTML Templates/player-template.html index 38579d9..56c92dc 100644 --- a/PadelClub/HTML Templates/player-template.html +++ b/PadelClub/HTML Templates/player-template.html @@ -1,3 +1,4 @@ +
{{teamIndex}}
{{playerOne}}{{weightOne}}
{{playerTwo}}{{weightTwo}}
diff --git a/PadelClub/HTML Templates/tournament-template.html b/PadelClub/HTML Templates/tournament-template.html index 1d6f1b9..e98ebf0 100644 --- a/PadelClub/HTML Templates/tournament-template.html +++ b/PadelClub/HTML Templates/tournament-template.html @@ -9,6 +9,7 @@ flex-direction:row; padding: 1%; } + .round{ display:flex; flex-direction:column; @@ -27,7 +28,7 @@ .round .spacer{ flex-grow:1; font-size:24px; text-align: center; - color: #bbb; + color: #000000; font-style:italic; } .round .spacer:first-child, @@ -65,7 +66,7 @@ li.game-spacer{ border-right:2px solid #4f7a38; - min-height:156px; + min-height:{{minHeight}}px; text-align: right; display : flex; justify-content: center; @@ -95,7 +96,7 @@ -

{{tournamentTitle}}

+

{{tournamentTitle}} - {{tournamentStartDate}}

{{brackets}}
diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 20c2389..35e88fd 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -19,6 +19,8 @@ struct PadelClubApp: App { @State private var importObserverViewModel = ImportObserver() @Environment(\.horizontalSizeClass) var horizontalSizeClass + @State var blockApp = false + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var presentError: Binding { @@ -62,54 +64,86 @@ struct PadelClubApp: App { var body: some Scene { WindowGroup { - MainView() - .environment(\.horizontalSizeClass, .compact) - .alert(isPresented: presentError, error: registrationError) { - Button("Contactez-nous") { - _openMail() - } - Button("Annuler", role: .cancel) { - registrationError = nil + + if self.blockApp { + DownloadNewVersionView() + } else { + MainView() + .environment(\.horizontalSizeClass, .compact) + .alert(isPresented: presentError, error: registrationError) { + Button("Contactez-nous") { + _openMail() + } + Button("Annuler", role: .cancel) { + registrationError = nil + } } - } - .onOpenURL { url in + .onOpenURL { url in #if targetEnvironment(simulator) #else - _handleIncomingURL(url) + _handleIncomingURL(url) #endif - } - .environmentObject(networkMonitor) - .environmentObject(dataStore) - .environment(importObserverViewModel) - .environment(navigationViewModel) - .accentColor(.master) - .onAppear { + } + .environmentObject(networkMonitor) + .environmentObject(dataStore) + .environment(importObserverViewModel) + .environment(navigationViewModel) + .accentColor(.master) + .onAppear { + self._checkVersion() #if DEBUG -print("Running in Debug mode") + print("Running in Debug mode") #elseif TESTFLIGHT -print("Running in TestFlight mode") + print("Running in TestFlight mode") #elseif PRODTEST -print("Running in ProdTest mode") + print("Running in ProdTest mode") #else -print("Running in Release mode") + print("Running in Release mode") #endif - networkMonitor.checkConnection() - self._onAppear() - print(PersistenceController.getModelVersion()) - } - .task { - - try? Tips.resetDatastore() - - try? Tips.configure([ - .displayFrequency(.immediate), - .datastoreLocation(.applicationDefault) - ]) + networkMonitor.checkConnection() + self._onAppear() + print(PersistenceController.getModelVersion()) + } + .task { + + try? Tips.resetDatastore() + + try? Tips.configure([ + .displayFrequency(.immediate), + .datastoreLocation(.applicationDefault) + ]) + } + .environment(\.managedObjectContext, persistenceController.localContainer.viewContext) + } + } + } + + fileprivate func _checkVersion() { + Task.detached(priority: .high) { + if let requiredVersion = await self._retrieveRequiredVersion() { + let cleanedRequired = requiredVersion.replacingOccurrences(of: "\n", with: "") + Logger.log(">>> REQUIRED VERSION = \(requiredVersion)") + if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + await MainActor.run { + self.blockApp = VersionComparator.compare(cleanedRequired, currentVersion) == 1 + } } - .environment(\.managedObjectContext, persistenceController.localContainer.viewContext) + } } } + fileprivate func _retrieveRequiredVersion() async -> String? { + let requiredVersionURL = URLs.main.extend(path: "static/misc/required-version.txt") + + do { + let (data, _) = try await URLSession.shared.data(from: requiredVersionURL) + return String(data: data, encoding: .utf8) + } catch { + Logger.log("Error fetching required version: \(error)") + return nil + } + } + private func _handleIncomingURL(_ url: URL) { // Parse the URL let pathComponents = url.pathComponents @@ -173,3 +207,31 @@ print("Running in Release mode") } } } + + +struct DownloadNewVersionView: View { + + var body: some View { + + VStack { +// AngledStripesBackground() + Spacer() + Text("Veuillez télécharger la nouvelle version de Padel Club pour continuer à vous servir de l'app !") + .padding(32.0) + .background(.logoYellow) + .clipShape(.buttonBorder) + .foregroundStyle(.logoBackground) + .fontWeight(.medium) + .multilineTextAlignment(.center) + .padding(.horizontal, 64.0) + Image("logo").padding(.vertical, 50.0) + Spacer() + }.background(.logoBackground) + .onTapGesture { + UIApplication.shared.open(URLs.appStore.url) + } + + } + +} + diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift index 4139f67..09d5ab1 100644 --- a/PadelClub/Utils/HtmlGenerator.swift +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -24,6 +24,9 @@ class HtmlGenerator: ObservableObject { @Published var displayHeads: Bool = false @Published var groupStageIsReady: Bool = false @Published var displayRank: Bool = false + @Published var displayTeamIndex: Bool = false + @Published var displayScore: Bool = false + private var pdfDocument: PDFDocument = PDFDocument() private var rects: [CGRect] = [] private var completionHandler: ((Result) -> ())? @@ -167,12 +170,12 @@ class HtmlGenerator: ObservableObject { func generateHtml() -> String { //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() - HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false) + HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore) } func generateLoserBracketHtml(upperRound: Round) -> String { //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() - HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withScore: false) + HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore) } var pdfURL: URL? { diff --git a/PadelClub/Utils/HtmlService.swift b/PadelClub/Utils/HtmlService.swift index 33145b5..3ebd892 100644 --- a/PadelClub/Utils/HtmlService.swift +++ b/PadelClub/Utils/HtmlService.swift @@ -50,7 +50,7 @@ enum HtmlService { } } - func html(headName: Bool, withRank: Bool, withScore: Bool) -> String { + func html(headName: Bool, withRank: Bool, withTeamIndex: Bool, withScore: Bool) -> String { guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else { fatalError() } @@ -69,12 +69,12 @@ enum HtmlService { } template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle(.short)) template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle()) - + template = template.replacingOccurrences(of: "{{formatLabel}}", with: bracket.matchFormat.formatTitle()) var col = "" var row = "" bracket.teams().forEach { entrant in - col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withScore: withScore)) - row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withScore: withScore)) + col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) + row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } template = template.replacingOccurrences(of: "{{teamsCol}}", with: col) template = template.replacingOccurrences(of: "{{teamsRow}}", with: row) @@ -82,6 +82,12 @@ enum HtmlService { return template case .groupstageEntrant(let entrant): var template = html + if withTeamIndex == false { + template = template.replacingOccurrences(of: #"
{{teamIndex}}
"#, with: "") + } else { + template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.seedIndex() ?? "") + } + if let playerOne = entrant.players()[safe: 0] { template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) if withRank { @@ -108,7 +114,7 @@ enum HtmlService { return template case .groupstageRow(let entrant, let teamsPerBracket): var template = html - template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) var scores = "" (0..{{teamIndex}}"#, with: "") + } else { + template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.formattedSeed()) + } + + if let playerOne = entrant.players()[safe: 0] { template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) if withRank { @@ -164,18 +177,22 @@ enum HtmlService { } return template case .hiddenPlayer: - return html + html + var template = html + html + if withTeamIndex { + template += html + } + return template case .match(let match): var template = html if let entrantOne = match.team(.one) { - template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } else { - template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } if let entrantTwo = match.team(.two) { - template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } else { - template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } if match.disabled { template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden") @@ -196,19 +213,20 @@ enum HtmlService { var template = "" var bracket = "" for (_, match) in round._matches().enumerated() { - template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore)) + template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle()) + bracket = bracket.replacingOccurrences(of: "{{formatLabel}}", with: round.matchFormat.formatTitle()) return bracket case .loserBracket(let upperRound): var template = html template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle()) var brackets = "" for round in upperRound.loserRounds() { - brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore)) + brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } - var winnerName = "" + let winnerName = "" let winner = """
  •  
  • @@ -224,15 +242,17 @@ enum HtmlService { return template case .template(let tournament): var template = html - template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short)) + template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156") + template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.title)) + template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: tournament.formattedDate()) var brackets = "" for round in tournament.rounds() { - brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore)) + brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) } var winnerName = "" if let tournamentWinner = tournament.tournamentWinner() { - winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withScore: withScore) + winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore) } let winner = """
      diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index ff9b1d8..dd7cbd2 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1996,16 +1996,30 @@ enum TournamentDeadlineType: String, CaseIterable { case wildcardLicensePurchase = "Prise de licence des WC" case definitiveBroadcastList = "Publication définitive" - var daysOffset: Int { - switch self { - case .inscription: - return -13 - case .broadcastList: - return -12 - case .wildcardRequest: - return -9 - case .wildcardLicensePurchase, .definitiveBroadcastList: - return -8 + func daysOffset(level: TournamentLevel) -> Int { + if level == .p500 { + switch self { + case .inscription: + return -6 + case .broadcastList: + return -6 + case .wildcardRequest: + return -4 + case .wildcardLicensePurchase, .definitiveBroadcastList: + return -4 + } + } else { + switch self { + case .inscription: + return -13 + case .broadcastList: + return -12 + case .wildcardRequest: + return -9 + case .wildcardLicensePurchase, .definitiveBroadcastList: + return -8 + } + } } diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index dc0afd5..03652a8 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -46,7 +46,12 @@ enum URLs: String, Identifiable { var url: URL { return URL(string: self.rawValue)! - } + } + + func extend(path: String) -> URL { + return URL(string: self.rawValue + path)! + } + } enum PageLink: String, Identifiable, CaseIterable { diff --git a/PadelClub/Utils/VersionComparator.swift b/PadelClub/Utils/VersionComparator.swift new file mode 100644 index 0000000..9c9a240 --- /dev/null +++ b/PadelClub/Utils/VersionComparator.swift @@ -0,0 +1,33 @@ +// +// VersionComparator.swift +// PadelClub +// +// Created by Laurent Morvillier on 13/02/2025. +// + +class VersionComparator { + + static func compare(_ version1: String, _ version2: String) -> Int { + // Split versions into components + let v1Components = version1.split(separator: ".").map { Int($0) ?? 0 } + let v2Components = version2.split(separator: ".").map { Int($0) ?? 0 } + + // Get the maximum length to compare + let maxLength = max(v1Components.count, v2Components.count) + + // Compare each component + for i in 0.. v2Num { + return 1 // version1 is larger + } + } + + return 0 // versions are equal + } + +} diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index cc16ead..8857997 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -89,8 +89,31 @@ class SearchViewModel: ObservableObject, Identifiable { return nil } + func shouldIncludeSearchTextPredicate() -> Bool { + if allowMultipleSelection { + return true + } + + if allowSingleSelection { + return true + } + + if tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted { + return true + } + + return dataSet == .national && searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) + } + func showIndex() -> Bool { - if (dataSet == .national || dataSet == .ligue) { return isFiltering() } + if dataSet == .national { + if searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) { + return false + } else { + return isFiltering() + } + } + if (dataSet == .ligue) { return isFiltering() } if filterOption == .all { return isFiltering() } return true } @@ -149,66 +172,85 @@ class SearchViewModel: ObservableObject, Identifiable { } } + func searchTextPredicate() -> NSPredicate? { + var predicates : [NSPredicate] = [] + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) + let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed + + if canonicalVersionWithoutPunctuation.isEmpty == false { + let wordsPredicates = wordsPredicates() + if let wordsPredicates { + predicates.append(wordsPredicates) + } else { + predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) + } + predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation)) + let components = canonicalVersionWithoutPunctuation.split(separator: " ") + let pattern = components.joined(separator: ".*") + let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) + predicates.append(predicate) + } + if predicates.isEmpty { + return nil + } + return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) + } + func orPredicate() -> NSPredicate? { var predicates : [NSPredicate] = [] let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed - switch tokens.first { - case .none: - if canonicalVersionWithoutPunctuation.isEmpty == false { - let wordsPredicates = wordsPredicates() - if let wordsPredicates { - predicates.append(wordsPredicates) - } else { - predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) + + if tokens.isEmpty { + if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false { + if let searchTextPredicate = searchTextPredicate() { + predicates.append(searchTextPredicate) } - predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation)) - let components = canonicalVersionWithoutPunctuation.split(separator: " ") - let pattern = components.joined(separator: ".*") - let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) - predicates.append(predicate) - } - case .ligue: - if canonicalVersionWithoutPunctuation.isEmpty { - predicates.append(NSPredicate(format: "ligueName == nil")) - } else { - predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation)) - } - case .club: - if canonicalVersionWithoutPunctuation.isEmpty { - predicates.append(NSPredicate(format: "clubName == nil")) - } else { - predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation)) - } - case .rankMoreThan: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { - predicates.append(NSPredicate(format: "rank == 0")) - } else { - predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation)) - } - case .rankLessThan: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { - predicates.append(NSPredicate(format: "rank == 0")) - } else { - predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation)) - } - case .rankBetween: - let values = canonicalVersionWithPunctuation.components(separatedBy: ",") - if canonicalVersionWithPunctuation.isEmpty || values.count != 2 { - predicates.append(NSPredicate(format: "rank == 0")) - } else { - predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) } - case .age: - if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { - predicates.append(NSPredicate(format: "birthYear == 0")) - } else if let birthYear = Int(canonicalVersionWithoutPunctuation) { - predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) + } + for token in tokens { + switch token { + case .ligue: + if canonicalVersionWithoutPunctuation.isEmpty { + predicates.append(NSPredicate(format: "ligueName == nil")) + } else { + predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation)) + } + case .club: + if canonicalVersionWithoutPunctuation.isEmpty { + predicates.append(NSPredicate(format: "clubName == nil")) + } else { + predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation)) + } + case .rankMoreThan: + if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation)) + } + case .rankLessThan: + if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation)) + } + case .rankBetween: + let values = canonicalVersionWithPunctuation.components(separatedBy: ",") + if canonicalVersionWithPunctuation.isEmpty || values.count != 2 { + predicates.append(NSPredicate(format: "rank == 0")) + } else { + predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) + } + case .age: + if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { + predicates.append(NSPredicate(format: "birthYear == 0")) + } else if let birthYear = Int(canonicalVersionWithoutPunctuation) { + predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) + } + } - } - if predicates.isEmpty { return nil } @@ -314,6 +356,17 @@ class SearchViewModel: ObservableObject, Identifiable { static func pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { + var andPredicates = [NSPredicate]() + var orPredicates = [NSPredicate]() + + let matches = pasteField.licencesFound() + let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } + orPredicates = licensesPredicates + + if matches.count == 2 { + return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates) + } + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) // Remove all characters that are not in the allowedCharacterSet @@ -327,14 +380,8 @@ class SearchViewModel: ObservableObject, Identifiable { let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines) let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 } let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 }) - var andPredicates = [NSPredicate]() - var orPredicates = [NSPredicate]() - //self.wordsCount = nameComponents.count - - if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) { - orPredicates.append(slashPredicate) - } + //self.wordsCount = nameComponents.count if filterOption == .female { andPredicates.append(NSPredicate(format: "male == NO")) } @@ -343,11 +390,20 @@ class SearchViewModel: ObservableObject, Identifiable { andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } - if nameComponents.count > 1 { - orPredicates.append(contentsOf: nameComponents.pairs().map { - return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }) - } else { - orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) + + if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) { + orPredicates.append(slashPredicate) + } + + print("nameComponents", nameComponents.count) + + if nameComponents.count < 50 { + if nameComponents.count > 1 { + orPredicates.append(contentsOf: nameComponents.pairs().map { + return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) }) + } else { + orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) + } } let components = text.split(separator: " ") @@ -355,11 +411,7 @@ class SearchViewModel: ObservableObject, Identifiable { print(text, pattern) let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) orPredicates.append(canonicalFullNamePredicate) - - let matches = pasteField.licencesFound() - let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } - orPredicates = orPredicates + licensesPredicates - + var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) if orPredicates.isEmpty == false { diff --git a/PadelClub/Views/Calling/CallSettingsView.swift b/PadelClub/Views/Calling/CallSettingsView.swift index 334cd41..49db7c5 100644 --- a/PadelClub/Views/Calling/CallSettingsView.swift +++ b/PadelClub/Views/Calling/CallSettingsView.swift @@ -22,6 +22,8 @@ struct CallSettingsView: View { var body: some View { List { + + Section { NavigationLink { CallMessageCustomizationView(tournament: tournament) diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index cb12cd4..020d603 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -6,15 +6,32 @@ // import SwiftUI +import LeStorage struct GroupStageCallingView: View { @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore @State private var displayByTeam: Bool = false var body: some View { let groupStages = tournament.groupStages() List { - + if tournament.isPrivate { + Section { + RowButtonView("Rendre visible sur Padel Club") { + tournament.isPrivate = false + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.") + .foregroundStyle(.logoRed) + } + } + let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }) if uncalled.isEmpty == false { NavigationLink { diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index c1b899e..d13f4b3 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -6,13 +6,31 @@ // import SwiftUI +import LeStorage struct SeedsCallingView: View { + @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament @State private var displayByMatch: Bool = true var body: some View { List { + if tournament.isPrivate { + Section { + RowButtonView("Rendre visible sur Padel Club") { + tournament.isPrivate = false + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.") + .foregroundStyle(.logoRed) + } + } + let tournamentRounds = tournament.rounds() let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil }) diff --git a/PadelClub/Views/Calling/SendToAllView.swift b/PadelClub/Views/Calling/SendToAllView.swift index 3c06250..a0515e0 100644 --- a/PadelClub/Views/Calling/SendToAllView.swift +++ b/PadelClub/Views/Calling/SendToAllView.swift @@ -223,13 +223,14 @@ struct SendToAllView: View { } func _teams() -> [TeamRegistration] { + let selectedSortedTeams = tournament.selectedSortedTeams() if onlyWaitingList { - return tournament.waitingListSortedTeams() + return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) } if _roundTeams().isEmpty && _groupStagesTeams().isEmpty { - return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : []) + return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : []) } - return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : []) + return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : []) } func _roundTeams() -> [TeamRegistration] { diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 677fb2b..017c5fa 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -10,6 +10,7 @@ import LeStorage struct TeamsCallingView: View { @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore let teams : [TeamRegistration] @State private var hideConfirmed: Bool = false @@ -31,6 +32,23 @@ struct TeamsCallingView: View { var body: some View { List { + if tournament.isPrivate { + Section { + RowButtonView("Rendre visible sur Padel Club") { + tournament.isPrivate = false + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + } footer: { + Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.") + .foregroundStyle(.logoRed) + } + } + + PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false } diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 43f2e64..9a33dda 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -38,6 +38,10 @@ struct EventListView: View { let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments) Text("\(count.formatted()) tournoi" + count.pluralSuffix) } + } footer: { + if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] { + _menuOptions(pcTournaments) + } } .headerProminence(.increased) } @@ -56,6 +60,10 @@ struct EventListView: View { let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments) Text("\(count.formatted()) tournoi" + count.pluralSuffix) } + } footer: { + if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] { + _menuOptions(pcTournaments) + } } .id(sectionIndex) .headerProminence(.increased) @@ -84,6 +92,88 @@ struct EventListView: View { } } + private func _menuOptions(_ pcTournaments: [Tournament]) -> some View { + Menu { + _options(pcTournaments) + } label: { + Text("Options rapides pour ce mois") + .underline() + } + } + + @ViewBuilder + private func _options(_ pcTournaments: [Tournament]) -> some View { + Section { + if pcTournaments.anySatisfy({ $0.isPrivate == true }) { + Button { + pcTournaments.forEach { tournament in + tournament.isPrivate = false + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Afficher ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club") + } + } + + if pcTournaments.anySatisfy({ $0.isPrivate == false }) { + Button { + pcTournaments.forEach { tournament in + tournament.isPrivate = true + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Masquer ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club") + } + } + } header: { + Text("Visibilité sur Padel Club") + } + Divider() + if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { + Section { + if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) { + Button { + pcTournaments.forEach { tournament in + tournament.enableOnlineRegistration = true + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Activer l'inscription en ligne") + } + } + + if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { + Button { + pcTournaments.forEach { tournament in + tournament.enableOnlineRegistration = false + } + do { + try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } catch { + Logger.error(error) + } + } label: { + Text("Désactiver l'inscription en ligne") + } + } + } header: { + Text("Inscription en ligne") + } + } + } + private func _nextMonths() -> [Date] { let currentDate = Date().startOfMonth let uniqueDates = tournaments.map { $0.startDate.startOfMonth }.uniqued().sorted() @@ -124,13 +214,20 @@ struct EventListView: View { ShareModelView(instance: tournament) } } + .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) .contextMenu { if tournament.hasEnded() == false { Button { navigation.openTournamentInOrganizer(tournament) } label: { - Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + Label("Afficher dans le gestionnaire", systemImage: "line.diagonal.arrow") } + + Divider() + + _options([tournament]) + + } } #if DEBUG diff --git a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift index 7d90518..9565658 100644 --- a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift +++ b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift @@ -22,7 +22,7 @@ struct MatchFormatStorageView: View { var body: some View { Section { LabeledContent { - StepperView(title: "minutes", count: $estimatedDuration, step: 5) + StepperView(title: "minute", count: $estimatedDuration, step: 5) } label: { MatchFormatRowView(matchFormat: matchFormat, hideDuration: true) } diff --git a/PadelClub/Views/Planning/MatchFormatGuideView.swift b/PadelClub/Views/Planning/MatchFormatGuideView.swift new file mode 100644 index 0000000..4596a77 --- /dev/null +++ b/PadelClub/Views/Planning/MatchFormatGuideView.swift @@ -0,0 +1,72 @@ +// +// MatchFormatGuideView.swift +// PadelClub +// +// Created by razmig on 20/02/2025. +// + +import SwiftUI + +struct MatchFormatGuideView: View { + let matchCounts = Array(2...7) + let formats: [MatchFormat] = [ + .twoSets, .twoSetsDecisivePoint, + .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, + .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, + .nineGames, .nineGamesDecisivePoint, + .superTie + ] + + func getFormatDescription(for matchCount: Int) -> String { + var description = "" + + // Group formats by their behavior + let formatGroups = Dictionary(grouping: formats) { format in + format.maximumMatchPerDay(for: matchCount) + } + + // Sort by maximum matches allowed (descending) + let sortedMaxMatches = formatGroups.keys.sorted(by: >) + + for maxMatches in sortedMaxMatches { + if let formatsForMax = formatGroups[maxMatches] { + let formatStrings = formatsForMax.map { $0.format }.joined(separator: "/") + if maxMatches > 0 && maxMatches <= matchCount { + description += "Maximum \(maxMatches) matchs en format \(formatStrings)\n" + } else if maxMatches == 0 { + description += "Aucun match au format \(formatStrings)\n" + } + } + } + + if matchCount >= 7 { + description += "Format \(MatchFormat.superTie.format) principalement" + } + + return description.isEmpty ? "Aucun match possible" : description + } + + var body: some View { + List { + Section { + ForEach(matchCounts, id: \.self) { count in + VStack { + Text("\(count) matchs par jour") + .font(.headline) + Text(getFormatDescription(for: count)) + } + } + + // Special case for 7+ matches + VStack { + Text("7+ matchs par jour") + .font(.headline) + Text("Tournois P 25 uniquement (soirée/demi-journée/journée)") + } + } + } + .navigationTitle("Guide des Formats de Match") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 22312e4..8b1ab94 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -14,7 +14,7 @@ struct PlanningSettingsView: View { @Bindable var tournament: Tournament @Bindable var matchScheduler: MatchScheduler - + @State private var groupStageChunkCount: Int @State private var isScheduling: Bool = false @State private var schedulingDone: Bool = false @@ -23,11 +23,12 @@ struct PlanningSettingsView: View { @State private var parallelType: Bool = false @State private var deletingDateMatchesDone: Bool = false @State private var deletingDone: Bool = false + @State private var presentFormatHelperView: Bool = false var tournamentStore: TournamentStore? { return self.tournament.tournamentStore } - + init(tournament: Tournament) { self.tournament = tournament if let matchScheduler = tournament.matchScheduler() { @@ -43,7 +44,7 @@ struct PlanningSettingsView: View { self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) } } - + var body: some View { List { if tournament.payment == nil { @@ -62,7 +63,7 @@ struct PlanningSettingsView: View { } TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) - + if let event = tournament.eventObject() { NavigationLink { CourtAvailabilitySettingsView(event: event) @@ -75,7 +76,7 @@ struct PlanningSettingsView: View { } } } - + NavigationLink { MultiCourtPickerView(matchScheduler: matchScheduler) .environment(tournament) @@ -118,7 +119,7 @@ struct PlanningSettingsView: View { } } } - + if issueFound { Text("Padel Club n'a pas réussi à définir un horaire pour tous les matchs de ce tournoi, à cause de la programmation d'autres tournois ou de l'indisponibilité des terrains.") .foregroundStyle(.logoRed) @@ -131,7 +132,7 @@ struct PlanningSettingsView: View { } label: { Text("Voir plus d'options intelligentes") } - + if let event, event.tournaments.count > 1 { Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { Text("Ne pas tenir compte des autres tournois") @@ -142,9 +143,31 @@ struct PlanningSettingsView: View { Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.") } } - + _smartView() } + .navigationTitle("Réglages") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + presentFormatHelperView = true + } label: { + Text("Aide-mémoire") + } + } + } + .sheet(isPresented: $presentFormatHelperView) { + NavigationStack { + MatchFormatGuideView() + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Retour", role: .cancel) { + presentFormatHelperView = false + } + } + } + } + } .headerProminence(.increased) .onAppear { do { @@ -165,13 +188,13 @@ struct PlanningSettingsView: View { .deferredRendering(for: .seconds(2)) } } - + if deletingDone { Label("Tous les horaires ont été supprimés", systemImage: "clock.badge.xmark") .toastFormatted() .deferredRendering(for: .seconds(2)) } - + if deletingDateMatchesDone { Label("Horaires des matchs supprimés", systemImage: "clock.badge.xmark") .toastFormatted() @@ -189,7 +212,7 @@ struct PlanningSettingsView: View { _save() } } - + private func _localizedFooterMessage(groupStagesWithDateIsEmpty: Bool, roundsWithDateIsEmpty: Bool) -> String { let base = "Supprime les horaires des matchs restants non démarrés." let extend = " Garde les horaires définis pour les " @@ -203,20 +226,20 @@ struct PlanningSettingsView: View { return base + extend + "poules et les manches du tableau." } } - + @ViewBuilder private func _smartView() -> some View { let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) let allGroupStages = tournament.allGroupStages() let allRounds = tournament.allRounds() let matchesWithDate = allMatches.filter({ $0.startDate != nil }) - + let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) - + let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) - + _formatPerDayView(matchCountPerDay: countedSet) - + let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) let roundsWithDate = allRounds.filter({ $0.startDate != nil }) if matchesWithDate.isEmpty == false { @@ -238,7 +261,7 @@ struct PlanningSettingsView: View { Text(_localizedFooterMessage(groupStagesWithDateIsEmpty: groupStagesWithDate.isEmpty, roundsWithDateIsEmpty: roundsWithDate.isEmpty)) } } - + if groupStagesWithDate.isEmpty == false { Section { RowButtonView("Supprimer les horaires des poules", role: .destructive) { @@ -246,7 +269,7 @@ struct PlanningSettingsView: View { deletingDone = false allGroupStages.forEach({ $0.startDate = nil }) try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages) - + deletingDone = true } catch { Logger.error(error) @@ -254,7 +277,7 @@ struct PlanningSettingsView: View { } } } - + if roundsWithDate.isEmpty == false { Section { RowButtonView("Supprimer les horaires du tableau", role: .destructive) { @@ -271,7 +294,7 @@ struct PlanningSettingsView: View { Text("Supprime les horaires définis pour les manches du tableau.") } } - + if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false { Section { RowButtonView("Supprimer tous les horaires", role: .destructive) { @@ -282,10 +305,10 @@ struct PlanningSettingsView: View { $0.confirmed = false }) try self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches) - + allGroupStages.forEach({ $0.startDate = nil }) try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages) - + allRounds.forEach({ $0.startDate = nil }) try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds) deletingDone = true @@ -297,7 +320,7 @@ struct PlanningSettingsView: View { Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.") } } - + #if DEBUG Section { RowButtonView("Debug delete all dates", role: .destructive) { @@ -309,10 +332,10 @@ struct PlanningSettingsView: View { $0.confirmed = false }) try self.tournamentStore?.matches.addOrUpdate(contentOfs: tournament.allMatches()) - + allGroupStages.forEach({ $0.startDate = nil }) try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages) - + allRounds.forEach({ $0.startDate = nil }) try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds) deletingDone = true @@ -325,7 +348,7 @@ struct PlanningSettingsView: View { } #endif - + Section { if groupStagesWithDate.isEmpty == false { Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") @@ -348,7 +371,7 @@ struct PlanningSettingsView: View { Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() } } - + @ViewBuilder private func _optionsView() -> some View { List { @@ -375,7 +398,7 @@ struct PlanningSettingsView: View { matchScheduler.groupStageChunkCount = nil } } - + if parallelType { TournamentFieldsManagerView(localizedStringKey: "Poule en parallèle", count: $groupStageChunkCount, max: tournament.groupStageCount) .onChange(of: groupStageChunkCount) { @@ -385,7 +408,7 @@ struct PlanningSettingsView: View { } footer: { Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.") } - + Section { Toggle(isOn: $matchScheduler.simultaneousStart) { Text("Démarrage simultané") @@ -394,7 +417,7 @@ struct PlanningSettingsView: View { Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.") } } - + Section { Toggle(isOn: $matchScheduler.randomizeCourts) { Text("Distribuer les terrains au hasard") @@ -408,7 +431,7 @@ struct PlanningSettingsView: View { } footer: { Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") } - + Section { Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { Text("Équilibrer les matchs d'une manche") @@ -416,13 +439,13 @@ struct PlanningSettingsView: View { } footer: { Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") } - + Section { Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) { Text("Finir une manche, classement inclus avant de continuer") } } - + Section { Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) { Text("Tenir compte des temps de pause réglementaires") @@ -430,7 +453,7 @@ struct PlanningSettingsView: View { } header: { Text("Tableau") } - + Section { Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) { Text("Tenir compte des temps de pause réglementaires") @@ -438,19 +461,19 @@ struct PlanningSettingsView: View { } header: { Text("Classement") } - + Section { Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) { Text("Forcer une rotation d'attente supplémentaire entre 2 phases") } - + LabeledContent { StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2) } label: { Text("Tableau") } .disabled(matchScheduler.rotationDifferenceIsImportant == false) - + LabeledContent { StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2) } label: { @@ -460,7 +483,7 @@ struct PlanningSettingsView: View { } footer: { Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.") } - + Section { LabeledContent { StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5) @@ -478,11 +501,11 @@ struct PlanningSettingsView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } - + private func _setupSchedule() async -> Bool { return matchScheduler.updateSchedule(tournament: tournament) } - + private func _save() { do { try self.tournamentStore?.matchSchedulers.addOrUpdate(instance: matchScheduler) @@ -491,21 +514,21 @@ struct PlanningSettingsView: View { Logger.error(error) } } - + private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] { var matchesByDay = [Date: [Match]]() let calendar = Calendar.current - + for match in matches { // Extract day/month/year and create a date with only these components let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting) let strippedDate = calendar.date(from: components)! - + // Group matches by the strippedDate (only day/month/year) if matchesByDay[strippedDate] == nil { matchesByDay[strippedDate] = [] } - + let shouldIncludeMatch: Bool switch match.matchType { case .groupStage: @@ -515,24 +538,24 @@ struct PlanningSettingsView: View { case .loserBracket: shouldIncludeMatch = true } - + if shouldIncludeMatch { matchesByDay[strippedDate]!.append(match) } } - + return matchesByDay } - + private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] { let days = matchesByDay.keys var matchCountPerDay = [Date: NSCountedSet]() - + for day in days { if let matches = matchesByDay[day] { var groupStageCount = 0 let countedSet = NSCountedSet() - + for match in matches { switch match.matchType { case .groupStage: @@ -547,15 +570,15 @@ struct PlanningSettingsView: View { break } } - + if groupStageCount > 0 { for _ in 0.. totalForThisFormat @@ -605,7 +628,7 @@ struct PlanningSettingsView: View { } } } - + // Helper function to format date to string (you can customize the format) private func _formattedDate(_ date: Date) -> String { let formatter = DateFormatter() diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index bd96ee7..c2c76bd 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -262,7 +262,7 @@ struct RoundView: View { .foregroundStyle(.green) } } label: { - Text("Classement final des équipes") + Text("Classement final") if tournament.publishRankings == false { Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) } diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 36feb3a..4210ef9 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -337,6 +337,21 @@ struct MySearchView: View { _searchViewModel = ObservedObject(wrappedValue: searchViewModel) _players = FetchRequest(sortDescriptors: searchViewModel.sortDescriptors(), predicate: searchViewModel.predicate()) } + + func searchedPlayers() -> [ImportedPlayer] { + if searchViewModel.searchText.isEmpty { + return Array(players) + } + + if let searchPredicate = searchViewModel.searchTextPredicate() { + let filteredPlayers = players.filter { player in + searchPredicate.evaluate(with: player) + } + return filteredPlayers + } + + return Array(players) + } var body: some View { playersView @@ -371,8 +386,6 @@ struct MySearchView: View { @ViewBuilder var playersView: some View { - let showProgression = true - let showFemaleInMaleAssimilation = searchViewModel.showFemaleInMaleAssimilation if searchViewModel.allowMultipleSelection { List(selection: $searchViewModel.selectedPlayers) { if searchViewModel.filterSelectionEnabled { @@ -423,7 +436,7 @@ struct MySearchView: View { } } .id(UUID()) - } else { + } else if searchViewModel.shouldIncludeSearchTextPredicate() { Section { ForEach(players.indices, id: \.self) { index in let player = players[index] @@ -435,26 +448,45 @@ struct MySearchView: View { } } .id(UUID()) + } else { + let filteredPlayers = searchedPlayers() + + Section { + ForEach(filteredPlayers.indices, id: \.self) { index in + let player = filteredPlayers[index] + let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil + let computedIndex = realIndex != nil ? realIndex! + 1 : nil + ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + } + } header: { + if filteredPlayers.isEmpty == false { + headerView() + } + } + .id(UUID()) } } else { + let filteredPlayers = searchedPlayers() Section { - ForEach(players.indices, id: \.self) { index in - let player = players[index] + ForEach(filteredPlayers.indices, id: \.self) { index in + let player = filteredPlayers[index] + let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil + let computedIndex = realIndex != nil ? realIndex! + 1 : nil if searchViewModel.allowSingleSelection { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) } else { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) + ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) } } } header: { - if players.isEmpty == false { + if filteredPlayers.isEmpty == false { headerView() } } diff --git a/PadelClub/Views/Team/Components/TeamWeightView.swift b/PadelClub/Views/Team/Components/TeamWeightView.swift index f19b6e6..0868ed3 100644 --- a/PadelClub/Views/Team/Components/TeamWeightView.swift +++ b/PadelClub/Views/Team/Components/TeamWeightView.swift @@ -12,16 +12,15 @@ struct TeamWeightView: View { let team: TeamRegistration var teamPosition: TeamPosition? = nil - var teamIndex: Int? { - team.tournamentObject()?.indexOf(team: team) - } + var teamIndex: Int? var displayWeight: Bool { - team.tournamentObject()?.hideWeight() == false + team.shouldDisplayRankAndWeight() && team.tournamentObject()?.hideWeight() == false } var body: some View { VStack(alignment: .trailing, spacing: 0) { + let displayWeight = self.displayWeight if (teamPosition == .one || teamPosition == nil) && displayWeight { Text(team.weight.formatted()) .monospacedDigit() diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index a499b15..0c79849 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -47,7 +47,8 @@ struct EditingTeamView: View { } private func _resetTeam() { - self.currentWaitingList = tournament.waitingListSortedTeams().filter({ $0.hasRegisteredOnline() }).first + let selectedSortedTeams = tournament.selectedSortedTeams() + self.currentWaitingList = tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams).filter({ $0.hasRegisteredOnline() }).first team.resetPositions() team.wildCardGroupStage = false team.walkOut = false diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 966f903..4a1ba04 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -13,10 +13,11 @@ struct TeamRowView: View { var teamPosition: TeamPosition? = nil var displayCallDate: Bool = false var displayRestingTime: Bool = false + var teamIndex: Int? var body: some View { LabeledContent { - TeamWeightView(team: team, teamPosition: teamPosition) + TeamWeightView(team: team, teamPosition: teamPosition, teamIndex: teamIndex) } label: { VStack(alignment: .leading) { TeamHeadlineView(team: team) diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 673014c..d8c2bae 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -312,7 +312,7 @@ struct FileImportView: View { } } else if didImport { let _filteredTeams = filteredTeams - let previousTeams = tournament.sortedTeams() + let previousTeams = tournament.sortedTeams(selectedSortedTeams: tournament.selectedSortedTeams()) if previousTeams.isEmpty == false { Section { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 93a3f24..6bb0074 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -595,14 +595,11 @@ struct AddTeamView: View { return 1 } - @MainActor private func handlePasteString(_ first: String) { if first.isEmpty == false { - DispatchQueue.main.async { - fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) - fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] - autoSelect = true - } + fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) + fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] + autoSelect = true } pasteString = first editableTextField = first diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index b3abcb9..635d458 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -93,8 +93,9 @@ struct TournamentGeneralSettingsView: View { } .frame(maxHeight: 200) .overlay { - if tournamentInformation.isEmpty { + if tournamentInformation.isEmpty, focusedField != ._information { Text("Texte visible dans l'onglet informations sur Padel Club.").italic() + .foregroundStyle(.secondary) } } } header: { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift index c89afd4..06b57d8 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift @@ -34,7 +34,7 @@ struct TournamentMatchFormatsSettingsView: View { Section { LabeledContent { - StepperView(title: "minutes", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10) + StepperView(title: "minute", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10) } label: { Text("Modifier les durées moyennes") } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index c09d34d..44632c2 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -180,7 +180,7 @@ struct InscriptionManagerView: View { return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) } - private func _setHash() { + private func _setHash(currentSelectedSortedTeams: [TeamRegistration]? = nil) { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -188,18 +188,17 @@ struct InscriptionManagerView: View { print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - let selectedSortedTeams = tournament.selectedSortedTeams() + let selectedSortedTeams = currentSelectedSortedTeams == nil ? tournament.selectedSortedTeams() : currentSelectedSortedTeams! if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) } self.registrationIssues = nil DispatchQueue.main.async { - self.registrationIssues = tournament.registrationIssues() + self.registrationIssues = tournament.registrationIssues(selectedTeams: selectedSortedTeams) } } - private func _handleHashDiff() { - let selectedSortedTeams = tournament.selectedSortedTeams() + private func _handleHashDiff(selectedSortedTeams: [TeamRegistration]) { let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) { self.teamsHash = newHash @@ -225,9 +224,10 @@ struct InscriptionManagerView: View { } var body: some View { - Group { - if tournament.unsortedTeams().isEmpty == false { - _teamRegisteredView() + let selectedSortedTeams = tournament.selectedSortedTeams() + return Group { + if tournament.unsortedTeamsCount() > 0 { + _teamRegisteredView(selectedSortedTeams: selectedSortedTeams) } else { List { @@ -263,10 +263,10 @@ struct InscriptionManagerView: View { await _refreshList() } .onAppear { - _setHash() + _setHash(currentSelectedSortedTeams: selectedSortedTeams) } .onDisappear { - _handleHashDiff() + _handleHashDiff(selectedSortedTeams: selectedSortedTeams) } .sheet(isPresented: $isLearningMore) { LearnMoreSheetView(tournament: tournament) @@ -490,47 +490,43 @@ struct InscriptionManagerView: View { tournament.unsortedPlayers() } - var sortedTeams: [TeamRegistration] { + func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] { if filterMode == .waiting { - return tournament.waitingListSortedTeams() + return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) } else { - return tournament.sortedTeams() + return tournament.sortedTeams(selectedSortedTeams: selectedSortedTeams) } } - var filteredTeams: [TeamRegistration] { - - var teams = sortedTeams - switch filterMode { - case .wildcardBracket: - teams = teams.filter({ $0.wildCardBracket }) - case .wildcardGroupStage: - teams = teams.filter({ $0.wildCardGroupStage }) - case .walkOut: - teams = teams.filter({ $0.walkOut }) - case .bracket: - teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false }) - case .groupStage: - teams = teams.filter({ $0.inGroupStage() }) - case .notImported: - teams = teams.filter({ $0.isImported() == false }) - case .registeredLocally: - teams = teams.filter({ $0.hasRegisteredOnline() == false }) - case .registeredOnline: - teams = teams.filter({ $0.hasRegisteredOnline() == true }) - default: - break - } - - if sortingMode == .registrationDate { - teams = teams.sorted(by: \.computedRegistrationDate) + func filteredTeams(sortedTeams: [TeamRegistration]) -> [TeamRegistration] { + let filtered = sortedTeams.lazy.filter { team in + switch filterMode { + case .wildcardBracket: + return team.wildCardBracket + case .wildcardGroupStage: + return team.wildCardGroupStage + case .walkOut: + return team.walkOut + case .bracket: + return team.inRound() && !team.inGroupStage() + case .groupStage: + return team.inGroupStage() + case .notImported: + return !team.isImported() + case .registeredLocally: + return !team.hasRegisteredOnline() + case .registeredOnline: + return team.hasRegisteredOnline() + default: + return true + } } - if byDecreasingOrdering { - return teams.reversed() - } else { - return teams - } + let sorted = sortingMode == .registrationDate + ? filtered.sorted(by: { $0.computedRegistrationDate < $1.computedRegistrationDate }) + : Array(filtered) + + return byDecreasingOrdering ? sorted.reversed() : sorted } // private func _fixModel() { @@ -572,12 +568,10 @@ struct InscriptionManagerView: View { } } - private func _teamRegisteredView() -> some View { + private func _teamRegisteredView(selectedSortedTeams: [TeamRegistration]) -> some View { List { - let selectedSortedTeams = tournament.selectedSortedTeams() - if presentSearch == false { - _informationView() + _informationView(for: selectedSortedTeams) if tournament.isAnimation() == false { _rankHandlerView() @@ -585,7 +579,8 @@ struct InscriptionManagerView: View { } } - let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) + let sortedTeams = sortedTeams(selectedSortedTeams: selectedSortedTeams) + let teams = searchField.isEmpty ? filteredTeams(sortedTeams: sortedTeams) : filteredTeams(sortedTeams: sortedTeams).filter({ $0.contains(searchField.canonicalVersion) }) if teams.isEmpty && searchField.isEmpty == false { ContentUnavailableView { @@ -622,7 +617,7 @@ struct InscriptionManagerView: View { EditingTeamView(team: team) .environment(tournament) } label: { - TeamRowView(team: team) + TeamRowView(team: team, teamIndex: teamIndex) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if tournament.enableOnlineRegistration == false { @@ -735,18 +730,18 @@ struct InscriptionManagerView: View { } } - private func _teamCountForFilterMode(filterMode: FilterMode) -> String { + private func _teamCountForFilterMode(filterMode: FilterMode, in teams: [TeamRegistration]) -> String { switch filterMode { case .wildcardBracket: - return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted() + return teams.filter({ $0.wildCardBracket }).count.formatted() case .wildcardGroupStage: - return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted() + return teams.filter({ $0.wildCardGroupStage }).count.formatted() case .all: return unsortedTeamsWithoutWO.count.formatted() case .bracket: - return tournament.selectedSortedTeams().filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted() + return teams.filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted() case .groupStage: - return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted() + return teams.filter({ $0.inGroupStage() }).count.formatted() case .walkOut: let wo = walkoutTeams.count.formatted() return wo @@ -754,20 +749,20 @@ struct InscriptionManagerView: View { let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) return waiting.formatted() case .notImported: - let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count) + let notImported: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.isImported() == false }).count) return notImported.formatted() case .registeredLocally: - let registeredLocally: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() == false }).count) + let registeredLocally: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() == false }).count) return registeredLocally.formatted() case .registeredOnline: - let registeredOnline: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() }).count) + let registeredOnline: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() }).count) return registeredOnline.formatted() } } @ViewBuilder - private func _informationView() -> some View { + private func _informationView(for teams: [TeamRegistration]) -> some View { Section { HStack { // VStack(alignment: .leading, spacing: 0) { @@ -781,7 +776,7 @@ struct InscriptionManagerView: View { // } // ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in - _filterModeView(filterMode: filterMode) + _filterModeView(filterMode: filterMode, in: teams) } Button { @@ -809,7 +804,7 @@ struct InscriptionManagerView: View { .listRowSeparator(.hidden) HStack { ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in - _filterModeView(filterMode: filterMode) + _filterModeView(filterMode: filterMode, in: teams) } } .padding(.bottom, -4) @@ -883,7 +878,7 @@ struct InscriptionManagerView: View { } } - private func _filterModeView(filterMode: FilterMode) -> some View { + private func _filterModeView(filterMode: FilterMode, in teams: [TeamRegistration]) -> some View { Button { if self.filterMode == filterMode { @@ -894,7 +889,7 @@ struct InscriptionManagerView: View { } label: { VStack(alignment: .center, spacing: -2) { Text(filterMode.localizedLabel(.short)).font(.caption).padding(.horizontal, -8) - Text(_teamCountForFilterMode(filterMode: filterMode)).font(.largeTitle) + Text(_teamCountForFilterMode(filterMode: filterMode, in: teams)).font(.largeTitle) } .frame(maxWidth: .infinity) .contentShape(Rectangle()) diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift index a8cc7d3..f9206bf 100644 --- a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -28,10 +28,20 @@ struct PrintSettingsView: View { // Toggle(isOn: $generator.displayHeads, label: { // Text("Afficher les têtes de séries") // }) + + Toggle(isOn: $generator.displayTeamIndex, label: { + Text("Afficher le poids et le rang de l'équipe") + }) + Toggle(isOn: $generator.displayRank, label: { Text("Afficher le classement du joueur") }) - + + Toggle(isOn: $generator.displayScore, label: { + Text("Afficher le score") + Text("Affiche le score des matchs terminés") + }) + Toggle(isOn: $generator.includeBracket, label: { Text("Tableau") }) @@ -152,32 +162,34 @@ struct PrintSettingsView: View { .navigationTitle("Imprimer") .toolbarBackground(.visible, for: .navigationBar) .navigationBarTitleDisplayMode(.inline) -// .toolbar { -// ToolbarItem(placement: .topBarTrailing) { -// Menu { -// Section { -// ShareLink(item: generator.generateHtml()) { -// Text("Tableau") -// } -// -// if let groupStage = tournament.groupStages().first { -// ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) { -// Text("Poule") -// } -// } -// } header: { -// Text("Partager le code source HTML") -// } -// } label: { -// Label("Options", systemImage: "ellipsis.circle") -// } -// } -// } .sheet(isPresented: $presentShareView) { if let pdfURL = generator.pdfURL { ShareSheet(urls: [pdfURL]) } } + #if DEBUG + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Section { + ShareLink(item: generator.generateHtml()) { + Text("Tableau") + } + + if let groupStage = tournament.groupStages().first { + ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)) { + Text("Poule") + } + } + } header: { + Text("Partager le code source HTML") + } + } label: { + Label("Options", systemImage: "ellipsis.circle") + } + } + } + #endif } @ViewBuilder @@ -199,7 +211,7 @@ struct PrintSettingsView: View { Group { if prepareGroupStage { ForEach(tournament.groupStages()) { groupStage in - WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in + WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore), loadStatusChanged: { loaded, error, webView in if let error { print("preparePDF", error) } else if loaded == false { @@ -301,7 +313,7 @@ struct WebViewPreview: View { ProgressView() .onAppear { if let groupStage { - html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false) + html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore) } else if let round { html = generator.generateLoserBracketHtml(upperRound: round) } else { diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index ea4d552..33f08f7 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -141,17 +141,18 @@ struct RegistrationSetupView: View { } Section { - Toggle(isOn: $targetTeamCountEnabled) { - Text("Activer une limite") - } - - if targetTeamCountEnabled { - StepperView(count: $targetTeamCount, minimum: 4) - } - } header: { +// Toggle(isOn: $targetTeamCountEnabled) { +// Text("Activer une limite") +// } +// +// if targetTeamCountEnabled { +// StepperView(count: $targetTeamCount, minimum: 4) +// } + StepperView(count: $targetTeamCount, minimum: 4) + } header: { Text("Paires admises") } footer: { - Text("Si une limite de paire existe, les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.") + Text("Les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.") } Section { @@ -160,7 +161,7 @@ struct RegistrationSetupView: View { } if waitingListLimitEnabled { - StepperView(count: $waitingListLimit, minimum: 1) + StepperView(count: $waitingListLimit, minimum: 0) } } header: { Text("Liste d'attente") diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index ffc71f7..7759ef5 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -71,7 +71,7 @@ struct TournamentRankView: View { } footer: { if let url = tournament.shareURL(.rankings) { Link(destination: url) { - Text("Voir la page des classements sur Padel Club") + Text("Voir les classements sur Padel Club") } } } diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index b76002c..d44e5aa 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -124,7 +124,7 @@ struct TournamentBuildView: View { .foregroundStyle(.green) } } label: { - Text("Classement final des équipes") + Text("Classement final") if tournament.publishRankings == false { Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) } diff --git a/PadelClub/Views/Tournament/TournamentInscriptionView.swift b/PadelClub/Views/Tournament/TournamentInscriptionView.swift index a40dee1..c06461a 100644 --- a/PadelClub/Views/Tournament/TournamentInscriptionView.swift +++ b/PadelClub/Views/Tournament/TournamentInscriptionView.swift @@ -21,6 +21,10 @@ struct TournamentInscriptionView: View { Text("Gestion des inscriptions") if let closedRegistrationDate = tournament.closedRegistrationDate { Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) + } else if tournament.enableOnlineRegistration { + Text("Inscription en ligne activée") + } else if tournament.onlineRegistrationCanBeEnabled() { + Text("Inscription en ligne désactivée") } } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 6b474f9..e9df55d 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -249,7 +249,7 @@ struct TournamentView: View { .foregroundStyle(.green) } } label: { - Text("Classement final des équipes") + Text("Classement final") if tournament.publishRankings == false { Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed) }