diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 729362f..d952dea 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -424,6 +424,15 @@ FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; + FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; + FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; + FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; @@ -1066,6 +1075,9 @@ FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = ""; }; FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = ""; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = ""; }; + FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = ""; }; + FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = ""; }; + FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = ""; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = ""; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = ""; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; @@ -1356,6 +1368,7 @@ FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, + FF6761522CC77D1900CC9BF2 /* DrawLog.swift */, FF6EC9012B94799200EA7F5A /* Coredata */, FF6EC9022B9479B900EA7F5A /* Federal */, ); @@ -1871,6 +1884,8 @@ FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, + FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */, + FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */, ); path = Round; sourceTree = ""; @@ -2331,6 +2346,7 @@ C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, + FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, @@ -2391,6 +2407,7 @@ FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, + FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, @@ -2442,6 +2459,7 @@ FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, + FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, @@ -2605,6 +2623,7 @@ FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */, FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */, FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */, + FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */, FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */, FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */, @@ -2665,6 +2684,7 @@ FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, + FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */, FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */, @@ -2716,6 +2736,7 @@ FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, + FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, @@ -2858,6 +2879,7 @@ FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */, FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */, FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */, + FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */, FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */, FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */, @@ -2918,6 +2940,7 @@ FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, + FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */, FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */, @@ -2969,6 +2992,7 @@ FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, + FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, @@ -3174,7 +3198,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3198,7 +3222,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.23; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3219,7 +3243,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3242,7 +3266,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.23; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3358,7 +3382,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3401,7 +3425,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3423,7 +3447,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3445,7 +3469,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3465,7 +3489,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3486,7 +3510,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/DrawLog.swift b/PadelClub/Data/DrawLog.swift new file mode 100644 index 0000000..29cf948 --- /dev/null +++ b/PadelClub/Data/DrawLog.swift @@ -0,0 +1,103 @@ +// +// DrawLog.swift +// PadelClub +// +// Created by razmig on 22/10/2024. +// + +import Foundation +import SwiftUI +import LeStorage + +@Observable +final class DrawLog: ModelObject, Storable { + static func resourceName() -> String { return "draw-logs" } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + static func filterByStoreIdentifier() -> Bool { return false } + static var relationshipNames: [String] = [] + + var id: String = Store.randomId() + var tournament: String + var drawDate: Date = Date() + var drawSeed: Int + var drawMatchIndex: Int + var drawTeamPosition: TeamPosition + + internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int, drawMatchIndex: Int, drawTeamPosition: TeamPosition) { + self.id = id + self.tournament = tournament + self.drawDate = drawDate + self.drawSeed = drawSeed + self.drawMatchIndex = drawMatchIndex + self.drawTeamPosition = drawTeamPosition + } + + func tournamentObject() -> Tournament? { + Store.main.findById(self.tournament) + } + + func computedBracketPosition() -> Int { + drawMatchIndex * 2 + drawTeamPosition.rawValue + } + + func updateTeamBracketPosition(_ team: TeamRegistration) { + guard let match = drawMatch() else { return } + let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition) + team.bracketPosition = seedPosition + tournamentObject()?.updateTeamScores(in: seedPosition) + } + + func exportedDrawLog() -> String { + [drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ") + } + + func localizedDrawSeedLabel() -> String { + return "Tête de série #\(drawSeed + 1)" + } + + func localizedDrawLogLabel() -> String { + return [localizedDrawSeedLabel(), positionLabel()].joined(separator: " -> ") + } + + func localizedDrawBranch() -> String { + drawTeamPosition.localizedBranchLabel() + } + + func drawMatch() -> Match? { + let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex) + return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex }) + } + + func positionLabel() -> String { + return drawMatch()?.roundAndMatchTitle() ?? "" + } + + func roundLabel() -> String { + return drawMatch()?.roundTitle() ?? "" + } + + func matchLabel() -> String { + return drawMatch()?.matchTitle() ?? "" + } + + var tournamentStore: TournamentStore { + return TournamentStore.instance(tournamentId: self.tournament) + } + + override func deleteDependencies() throws { + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _tournament = "tournament" + case _drawDate = "drawDate" + case _drawSeed = "drawSeed" + case _drawMatchIndex = "drawMatchIndex" + case _drawTeamPosition = "drawTeamPosition" + } + + func insertOnServer() throws { + self.tournamentStore.drawLogs.writeChangeAndInsertOnServer(instance: self) + } + +} diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index b7fa039..4573827 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -164,22 +164,8 @@ defer { } @discardableResult - func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int { + func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int { let matchIndex = index - var teamPosition : TeamPosition { - if let slot { - return slot - } else { - let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) - let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) - let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) - var teamPosition = slot ?? (isUpper ? .one : .two) - if opposingSeeding { - teamPosition = slot ?? (isUpper ? .two : .one) - } - return teamPosition - } - } previousMatch(teamPosition)?.disableMatch() return matchIndex * 2 + teamPosition.rawValue } @@ -247,6 +233,7 @@ defer { groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() currentTournament()?.updateTournamentState() + teams().forEach({ $0.resetRestingTime() }) } func resetScores() { @@ -548,7 +535,7 @@ defer { if endDate == nil { endDate = Date() } - + teams().forEach({ $0.resetRestingTime() }) winningTeamId = teamScoreWinning.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration groupStageObject?.updateGroupStageState() @@ -571,7 +558,9 @@ defer { teamOne?.hasArrived() teamTwo?.hasArrived() - + teamOne?.resetRestingTime() + teamTwo?.resetRestingTime() + winningTeamId = teamOne?.id losingTeamId = teamTwo?.id @@ -937,6 +926,10 @@ defer { (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow } + func isValidSpot() -> Bool { + previousMatches().allSatisfy({ $0.isSeeded() == false }) + } + enum CodingKeys: String, CodingKey { case _id = "id" case _round = "round" diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 38e8f87..3eb7450 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -493,9 +493,17 @@ final class MatchScheduler : ModelObject, Storable { if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { print("Handling break time conflicts or waiting for free courts") let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } - let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) - let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) + var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) + var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) + if let courtsUnavailability, previousEndDate != nil { + previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate + } + + if let courtsUnavailability, previousEndDateNoBreak != nil { + previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate + } + let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } if let previousEndDate, let previousEndDateNoBreak { @@ -651,9 +659,14 @@ final class MatchScheduler : ModelObject, Storable { } if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { - print("All courts in rotation \(rotationIndex) are free") + print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)") } + if let courtsUnavailability { + let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability) + return computedStartDateAndCourts.earliestFreeDate + } + return minimumTargetedEndDate } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 1b46a68..1d8d414 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -116,13 +116,39 @@ final class TeamRegistration: ModelObject, Storable { } func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { - let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding) + 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) + var teamPosition = slot ?? (isUpper ? .one : .two) + if opposingSeeding { + teamPosition = slot ?? (isUpper ? .two : .one) + } + return teamPosition + } + } + + let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) tournamentObject()?.resetTeamScores(in: bracketPosition) self.bracketPosition = seedPosition if groupStagePosition != nil && qualified == false { qualified = true } - tournamentObject()?.updateTeamScores(in: bracketPosition) + if let tournament = tournamentObject() { + if let index = index(in: tournament.selectedSortedTeams()) { + let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition) + do { + try tournamentStore.drawLogs.addOrUpdate(instance: drawLog) + } catch { + Logger.error(error) + } + } + tournament.updateTeamScores(in: bracketPosition) + } } func expectedSummonDate() -> Date? { @@ -532,8 +558,21 @@ final class TeamRegistration: ModelObject, Storable { } } + var _cachedRestingTime: (Bool, Date?)? + func restingTime() -> Date? { - matches().sorted(by: \.computedEndDateForSorting).last?.endDate + if let _cachedRestingTime { return _cachedRestingTime.1 } + 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 { @@ -544,6 +583,17 @@ final class TeamRegistration: ModelObject, Storable { } } + func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { + if let bracketPosition, let drawMatchIndex { + return drawMatchIndex != bracketPosition + } else if let bracketPosition { + return true + } else if let drawMatchIndex { + return true + } + return false + } + enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index de704a1..512ba1a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1,5 +1,5 @@ // -// Tournament.swift +// swift // PadelClub // // Created by Laurent Morvillier on 02/02/2024. @@ -120,7 +120,11 @@ final class Tournament : ModelObject, Storable { self.startDate = startDate self.endDate = endDate self.creationDate = creationDate +#if DEBUG + self.isPrivate = false +#else self.isPrivate = Guard.main.purchasedTransactions.isEmpty +#endif self.groupStageFormat = groupStageFormat self.roundFormat = roundFormat self.loserRoundFormat = loserRoundFormat @@ -142,16 +146,24 @@ final class Tournament : ModelObject, Storable { self.entryFee = entryFee self.additionalEstimationDuration = additionalEstimationDuration self.isDeleted = isDeleted +#if DEBUG + self.publishTeams = true + self.publishSummons = true + self.publishBrackets = true + self.publishGroupStages = true + self.publishRankings = true +#else self.publishTeams = publishTeams self.publishSummons = publishSummons self.publishBrackets = publishBrackets self.publishGroupStages = publishGroupStages + self.publishRankings = publishRankings +#endif self.shouldVerifyBracket = shouldVerifyBracket self.shouldVerifyGroupStage = shouldVerifyGroupStage self.hideTeamsWeight = hideTeamsWeight self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned - self.publishRankings = publishRankings self.loserBracketMode = loserBracketMode self.initialSeedRound = initialSeedRound self.initialSeedCount = initialSeedCount @@ -335,6 +347,12 @@ final class Tournament : ModelObject, Storable { override func deleteDependencies() throws { let store = self.tournamentStore + let drawLogs = self.tournamentStore.drawLogs + for drawLog in drawLogs { + try drawLog.deleteDependencies() + } + store.drawLogs.deleteDependencies(drawLogs) + let teams = self.tournamentStore.teamRegistrations for team in teams { try team.deleteDependencies() @@ -552,7 +570,7 @@ defer { return endDate != nil } - func state() -> Tournament.State { + func state() -> State { if self.isCanceled == true { return .canceled } @@ -1948,7 +1966,7 @@ defer { func labelIndexOf(team: TeamRegistration) -> String? { if let teamIndex = indexOf(team: team) { - return "#" + (teamIndex + 1).formatted() + return "Tête de série #" + (teamIndex + 1).formatted() } else { return nil } @@ -2257,6 +2275,128 @@ defer { rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } } + func seedsCount() -> Int { + selectedSortedTeams().count - groupStageSpots() + } + + func lastDrawnDate() -> Date? { + drawLogs().last?.drawDate + } + + func drawLogs() -> [DrawLog] { + self.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 + } else { + return true + } + } + + + func updateSeedsBracketPosition() async { + await removeAllSeeds() + let drawLogs = drawLogs().reversed() + let seeds = seeds() + for (index, seed) in seeds.enumerated() { + if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) { + drawLog.updateTeamBracketPosition(seed) + } + } + + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds) + } catch { + Logger.error(error) + } + } + + func removeAllSeeds() async { + unsortedTeams().forEach({ team in + team.bracketPosition = nil + }) + let ts = allRoundMatches().flatMap { match in + match.teamScores + } + + do { + try tournamentStore.teamScores.delete(contentOfs: ts) + } catch { + Logger.error(error) + } + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + } catch { + Logger.error(error) + } + allRounds().forEach({ round in + round.enableRound() + }) + + } + + func addNewRound(_ roundIndex: Int) async { + let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat) + let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) + let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) + let nextRound = round.nextRound() + var currentIndex = 0 + let matches = (0.. String { + var logs : [String] = ["Journal des tirages\n\n"] + logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n")) + return logs.joined() + } + + // MARK: - func insertOnServer() throws { diff --git a/PadelClub/Data/TournamentStore.swift b/PadelClub/Data/TournamentStore.swift index a01e6a0..78e593b 100644 --- a/PadelClub/Data/TournamentStore.swift +++ b/PadelClub/Data/TournamentStore.swift @@ -26,6 +26,7 @@ class TournamentStore: Store, ObservableObject { fileprivate(set) var teamScores: StoredCollection = StoredCollection.placeholder() fileprivate(set) var matchSchedulers: StoredCollection = StoredCollection.placeholder() + fileprivate(set) var drawLogs: StoredCollection = StoredCollection.placeholder() convenience init(tournament: Tournament) { self.init(identifier: tournament.id, parameter: "tournament") @@ -51,6 +52,7 @@ class TournamentStore: Store, ObservableObject { self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed) self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed) self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed) + self.drawLogs = self.registerCollection(synchronized: synchronized, indexed: indexed) self.loadCollectionsFromServerIfNoFile() diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 72abedc..d3b1d45 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -996,6 +996,15 @@ enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable { return shortName } } + + func localizedBranchLabel() -> String { + switch self { + case .one: + return "Branche du haut" + case .two: + return "Branche du bas" + } + } } enum SetFormat: Int, Hashable, Codable { diff --git a/PadelClub/Views/Components/FortuneWheelView.swift b/PadelClub/Views/Components/FortuneWheelView.swift index ac659d1..795773a 100644 --- a/PadelClub/Views/Components/FortuneWheelView.swift +++ b/PadelClub/Views/Components/FortuneWheelView.swift @@ -8,20 +8,20 @@ import SwiftUI protocol SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] } extension String: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { [self] } } extension Match: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { let teams = teams() - if teams.count == 1 { - return teams.first!.segmentLabel(displayStyle) + if teams.count == 1, hideNames == false { + return teams.first!.segmentLabel(displayStyle, hideNames: hideNames) } else { return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 } } @@ -29,12 +29,16 @@ extension Match: SpinDrawable { } extension TeamRegistration: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { var strings: [String] = [] let indexLabel = tournamentObject()?.labelIndexOf(team: self) if let indexLabel { strings.append(indexLabel) + if hideNames { + return strings + } } + strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) }) return strings } @@ -51,8 +55,8 @@ struct DrawOption: Identifiable, SpinDrawable { let initialIndex: Int let option: SpinDrawable - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { - option.segmentLabel(displayStyle) + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { + option.segmentLabel(displayStyle, hideNames: hideNames) } } @@ -62,6 +66,7 @@ struct SpinDrawView: View { let drawees: [any SpinDrawable] @State var segments: [any SpinDrawable] var autoMode: Bool = false + var hideNames: Bool = false let completion: ([DrawResult]) async -> Void // Completion closure @State private var drawCount: Int = 0 @@ -89,12 +94,12 @@ struct SpinDrawView: View { } } else if drawCount < drawees.count { Section { - _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) } Section { ZStack { - FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in + FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode, hideNames: hideNames) { index in self.selectedIndex = index self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex)) self.drawOptions.remove(at: index) @@ -209,8 +214,8 @@ struct SpinDrawView: View { private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View { VStack(alignment: horizontalAlignment, spacing: 0.0) { - ForEach(segment, id: \.self) { string in - Text(string).font(.title3) + ForEach(segment.indices, id: \.self) { lineIndex in + Text(segment[lineIndex]).font(.title3) .frame(maxWidth: .infinity) .lineLimit(1) } @@ -221,13 +226,13 @@ struct SpinDrawView: View { private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View { VStack(spacing: 0.0) { let draw = drawees[drawee] - _segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: draw.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) if result as? TeamRegistration != nil { Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed) } else { Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed) } - _segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: result.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) } } } @@ -236,10 +241,11 @@ struct FortuneWheelContainerView: View { @State private var rotation: Double = 0 let segments: [any SpinDrawable] let autoMode: Bool + let hideNames: Bool let completion: (Int) -> Void // Completion closure var body: some View { - FortuneWheelView(segments: segments) + FortuneWheelView(segments: segments, hideNames: hideNames) .rotationEffect(.degrees(rotation)) .aspectRatio(contentMode: .fill) .padding(.top, 5) @@ -303,6 +309,7 @@ struct FortuneWheelContainerView: View { struct FortuneWheelView: View { let segments: [any SpinDrawable] + let hideNames: Bool let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown] func getColor(forIndex index: Int) -> Color { @@ -330,12 +337,12 @@ struct FortuneWheelView: View { path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) path.closeSubpath() } - .fill(getColor(forIndex:index)) + .fill(getColor(forIndex: index)) VStack(alignment: .trailing, spacing: 0.0) { - let strings = segments[index].segmentLabel(.short) - ForEach(strings, id: \.self) { string in - Text(string).bold() + let strings = labels(forIndex: index) + ForEach(strings.indices, id: \.self) { lineIndex in + Text(strings[lineIndex]).bold() .font(.subheadline) } } @@ -349,6 +356,19 @@ struct FortuneWheelView: View { } } + private func labels(forIndex index: Int) -> [String] { + if segments.count < 5 { + return segments[index].segmentLabel(.short, hideNames: hideNames) + } else { + let values = segments[index].segmentLabel(.short, hideNames: hideNames) + if values.count < 3 { + return values + } else { + return Array(segments[index].segmentLabel(.short, hideNames: hideNames).prefix(1)) + } + } + } + // Calculate the position for the text in the middle of the arc segment private func arcPosition(index: Int, radius: Double) -> CGPoint { let segmentAngle = 360.0 / Double(segments.count) diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 11c7ee8..5901d37 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -78,16 +78,8 @@ struct PlayerBlockView: View { } } - if displayRestingTime, let restingTime = team?.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) { - if restingTime > -300 { - Text("vient de finir") - .font(.footnote) - .foregroundStyle(.secondary) - } else { - Text("en repos depuis " + value) - .font(.footnote) - .foregroundStyle(.secondary) - } + if displayRestingTime, let team { + TeamRowView.TeamRestingView(team: team) } } .bold(hasWon) diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 9ade115..8f05d8a 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -15,9 +15,6 @@ struct CourtAvailabilitySettingsView: View { let event: Event @State private var showingPopover: Bool = false - @State private var courtIndex: Int = 0 - @State private var startDate: Date = Date() - @State private var endDate: Date = Date() @State private var editingSlot: DateInterval? var courtsUnavailability: [Int: [DateInterval]] { @@ -45,10 +42,6 @@ struct CourtAvailabilitySettingsView: View { } Button("éditer") { editingSlot = dateInterval - courtIndex = dateInterval.courtIndex - startDate = dateInterval.startDate - endDate = dateInterval.endDate - showingPopover = true } Button("effacer", role: .destructive) { do { @@ -110,8 +103,6 @@ struct CourtAvailabilitySettingsView: View { Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") } actions: { RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { - startDate = tournament.startDate - endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } } @@ -120,8 +111,6 @@ struct CourtAvailabilitySettingsView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { - startDate = tournament.startDate - endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } } @@ -130,34 +119,99 @@ struct CourtAvailabilitySettingsView: View { .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Créneau indisponible") .sheet(isPresented: $showingPopover) { - NavigationStack { - Form { - Section { - CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount) - } - - Section { - DatePicker("Début", selection: $startDate) - .onChange(of: startDate) { - if endDate < startDate { - endDate = startDate.addingTimeInterval(90*60) - } - } - DatePicker("Fin", selection: $endDate) - .onChange(of: endDate) { - if startDate > endDate { - startDate = endDate.addingTimeInterval(-90*60) - } + CourtAvailabilityEditorView(event: event) + } + .sheet(item: $editingSlot) { editingSlot in + CourtAvailabilityEditorView(editingSlot: editingSlot, event: event) + } + } +} + +struct CourtPicker: View { + @Environment(Tournament.self) var tournament: Tournament + + let title: String + @Binding var selection: Int + let maxCourt: Int + + var body: some View { + Picker(title, selection: $selection) { + ForEach(0.. endDate { + startDate = endDate.addingTimeInterval(-90*60) + } + + } + } footer: { + FooterButtonView("jour entier") { + startDate = startDate.startOfDay + endDate = startDate.tomorrowAtNine.startOfDay } } - .toolbar { + + + Section { + DateAdjusterView(date: $startDate) + } header: { + Text("Modifier rapidement l'horaire de début") + } + Section { + DateAdjusterView(date: $endDate) + } header: { + Text("Modifier rapidement l'horaire de fin") + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { ButtonValidateView { if editingSlot == nil { let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) @@ -176,40 +230,50 @@ struct CourtAvailabilitySettingsView: View { Logger.error(error) } } - showingPopover = false + + dismiss() + } + } + + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + dismiss() } } - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Nouveau créneau") - .tint(.master) - } - .onAppear { - UIDatePicker.appearance().minuteInterval = 5 - } - .onDisappear { - UIDatePicker.appearance().minuteInterval = 1 } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle(_navigationTitle()) + .tint(.master) } } + + private func _navigationTitle() -> String { + editingSlot == nil ? "Nouveau créneau" : "Édition du créneau" + } } -struct CourtPicker: View { - @Environment(Tournament.self) var tournament: Tournament - - let title: String - @Binding var selection: Int - let maxCourt: Int - +struct DateAdjusterView: View { + @Binding var date: Date + var body: some View { - Picker(title, selection: $selection) { - ForEach(0.. some View { + Button(action: { + date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date + }) { + Text(label) + .frame(maxWidth: .infinity) // Make buttons take equal space + } + .buttonStyle(.borderedProminent) + .tint(.master) } } - -//#Preview { -// CourtAvailabilitySettingsView(event: Event.mock()) -//} diff --git a/PadelClub/Views/Round/DrawLogsView.swift b/PadelClub/Views/Round/DrawLogsView.swift new file mode 100644 index 0000000..6459955 --- /dev/null +++ b/PadelClub/Views/Round/DrawLogsView.swift @@ -0,0 +1,69 @@ +// +// DrawLogsView.swift +// PadelClub +// +// Created by razmig on 22/10/2024. +// + +import SwiftUI +import LeStorage + +struct DrawLogsView: View { + @Environment(Tournament.self) var tournament + + var drawLogs: [DrawLog] { + tournament.drawLogs().reversed() + } + + var body: some View { + List { + ForEach(drawLogs) { drawLog in + HStack { + VStack(alignment: .leading) { + Text(drawLog.localizedDrawSeedLabel()) + Text(drawLog.drawDate.localizedDate()) + .font(.footnote) + .foregroundStyle(.secondary) + } + Spacer() + VStack(alignment: .trailing) { + Text(drawLog.positionLabel()).lineLimit(1).truncationMode(.middle) + Text(drawLog.localizedDrawBranch()) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + } + } + .overlay(content: { + if drawLogs.isEmpty { + ContentUnavailableView("Aucun tirage", systemImage: "dice", description: Text("Aucun tirage au sort n'a été effectué.")) + } + }) + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + ShareLink(item: tournament.exportedDrawLogs()) { + Label("Partager les tirages", systemImage: "square.and.arrow.up") + .labelStyle(.titleAndIcon) + } + + Divider() + + Button("Tout effacer", role: .destructive) { + do { + try tournament.tournamentStore.drawLogs.deleteAll() + } catch { + Logger.error(error) + } + } + } label: { + LabelOptions() + } + } + }) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Journal des tirages") + } +} diff --git a/PadelClub/Views/Round/PreviewBracketPositionView.swift b/PadelClub/Views/Round/PreviewBracketPositionView.swift new file mode 100644 index 0000000..f906247 --- /dev/null +++ b/PadelClub/Views/Round/PreviewBracketPositionView.swift @@ -0,0 +1,107 @@ +// +// PreviewBracketPositionView.swift +// PadelClub +// +// Created by razmig on 23/10/2024. +// + +import SwiftUI + +struct PreviewBracketPositionView: View { + let seeds: [TeamRegistration] + let drawLogs: [DrawLog] + + @State private var filterOption: PreviewBracketPositionFilterOption = .difference + + enum PreviewBracketPositionFilterOption: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + case all + case difference + case summon + + func isDisplayable(_ team: TeamRegistration, drawLog: DrawLog?) -> Bool { + switch self { + case .all: + true + case .difference: + team.isDifferentPosition(drawLog?.computedBracketPosition()) == true + case .summon: + team.callDate != drawLog?.drawMatch()?.startDate + } + } + } + + var body: some View { + List { + Section { + ForEach(seeds.indices, id: \.self) { seedIndex in + let seed = seeds[seedIndex] + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + if filterOption.isDisplayable(seed, drawLog: drawLog) { + HStack { + VStack(alignment: .leading) { + Text("Tête de série #\(seedIndex + 1)").font(.caption) + TeamRowView.TeamView(team: seed) + TeamRowView.TeamCallDateView(team: seed) + } + Spacer() + if let drawLog { + VStack(alignment: .trailing) { + Text(drawLog.roundLabel()).lineLimit(1).truncationMode(.middle).font(.caption) + Text(drawLog.matchLabel()).lineLimit(1).truncationMode(.middle) + Text(drawLog.localizedDrawBranch()) + if let expectedDate = drawLog.drawMatch()?.startDate { + Text(expectedDate.localizedDate()) + .font(.caption) + } else { + Text("Aucun horaire") + .font(.caption) + } + } + } + } + .listRowView(isActive: true, color: seed.isDifferentPosition(drawLog?.computedBracketPosition()) ? .logoRed : .green, hideColorVariation: true) + } + } + } header: { + Picker(selection: $filterOption) { + Text("Tous").tag(PreviewBracketPositionFilterOption.all) + Text("Changements").tag(PreviewBracketPositionFilterOption.difference) + Text("Convoc.").tag(PreviewBracketPositionFilterOption.summon) + } label: { + Text("Filter") + } + .labelsHidden() + .pickerStyle(.segmented) + .textCase(nil) + } + } + .overlay(content: { + if seeds.isEmpty { + ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Aucun tête de série dans le tournoi.")) + } else if filterOption == .difference, noSeedHasDifferentPlace() { + ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau.")) + } else if filterOption == .summon, noSeedHasDifferentSummon() { + ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau.")) + } + }) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Aperçu du tableau tiré") + + } + + func noSeedHasDifferentPlace() -> Bool { + seeds.enumerated().allSatisfy({ seedIndex, seed in + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + return seed.isDifferentPosition(drawLog?.computedBracketPosition()) == false + }) + } + + func noSeedHasDifferentSummon() -> Bool { + seeds.enumerated().allSatisfy({ seedIndex, seed in + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + return seed.callDate == drawLog?.drawMatch()?.startDate + }) + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 3dcd33e..0024d88 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -73,7 +73,33 @@ struct RoundSettingsView: View { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.") } } - + + let previewAvailable = tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount() && tournament.lastDrawnDate() != nil && tournament.seedSpotsLeft() + Section { + NavigationLink { + DrawLogsView() + .environment(tournament) + } label: { + Text("Gestionnaire des tirages au sort") + } + + if previewAvailable { + + NavigationLink { + PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs()) + } label: { + Text("Aperçu du repositionnement") + } + RowButtonView("Replacer toutes les têtes de série", role: .destructive) { + await tournament.updateSeedsBracketPosition() + } + } + } footer: { + if previewAvailable { + Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") + } + } + // Section { // RowButtonView("Enabled", role: .destructive) { // let allMatches = tournament._allMatchesIncludingDisabled() @@ -127,72 +153,12 @@ struct RoundSettingsView: View { } private func _removeAllSeeds() async { - tournament.unsortedTeams().forEach({ team in - team.bracketPosition = nil - }) - let ts = tournament.allRoundMatches().flatMap { match in - match.teamScores - } - - do { - try tournamentStore.teamScores.delete(contentOfs: ts) - } catch { - Logger.error(error) - } - do { - try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) - } catch { - Logger.error(error) - } - tournament.allRounds().forEach({ round in - round.enableRound() - }) + await tournament.removeAllSeeds() self.isEditingTournamentSeed.wrappedValue = true } private func _addNewRound(_ roundIndex: Int) async { - let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) - let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) - let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) - let nextRound = round.nextRound() - var currentIndex = 0 - let matches = (0.. 0 { let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) TipView(bracketTip).tipStyle(tint: .green, asSection: true) - + let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) - + if upperRound.round.hasStarted() == false, leftToPlay >= 0 { Section { LabeledContent { @@ -96,7 +97,7 @@ struct RoundView: View { showPrintScreen = true } .tipStyle(tint: .master, asSection: true) - + if upperRound.round.index > 0 { let correspondingLoserRoundTitle = upperRound.round.correspondingLoserRoundTitle() Section { @@ -121,9 +122,10 @@ struct RoundView: View { } } } else { + let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index) let availableSeeds = tournament.availableSeeds() let availableQualifiedTeams = tournament.availableQualifiedTeams() - + if availableSeeds.isEmpty == false, let availableSeedGroup { Section { RowButtonView("Placer \(availableSeedGroup.localizedInterval())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { @@ -140,6 +142,12 @@ struct RoundView: View { if (availableSeedGroup.isFixed() == false) { Section { + Toggle(isOn: $hideNames) { + Text("Masquer les noms") + if hideNames { + Text("Réalise un tirage des positions.") + } + } RowButtonView("Tirage au sort \(availableSeedGroup.localizedInterval()) visuel") { self.selectedSeedGroup = availableSeedGroup } @@ -148,94 +156,53 @@ struct RoundView: View { } } } - + if availableQualifiedTeams.isEmpty == false { let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft - if availableSeedSpot.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableQualifiedTeams) { team in - NavigationLink { - - SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in - Task { - results.forEach { drawResult in - if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot { - team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false) - } else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match { - team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true) - } + Section { + DisclosureGroup { + ForEach(availableQualifiedTeams) { team in + NavigationLink { + + SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in + Task { + results.forEach { drawResult in + if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot { + team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false) + } else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match { + team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true) } - - _save(seeds: [team]) } + + _save(seeds: [team]) } - } label: { - TeamRowView(team: team, displayCallDate: false) } + } label: { + TeamRowView(team: team, displayCallDate: false) } - } label: { - Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) + .disabled(availableSeedSpot.isEmpty || isRoundValidForSeeding == false) } - } header: { - Text("Tirage au sort visuel d'un qualifié").font(.subheadline) + } label: { + Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) + } + } header: { + Text("Tirage au sort visuel d'un qualifié").font(.subheadline) + } footer: { + if availableSeedSpot.isEmpty || isRoundValidForSeeding == false { + Text("Aucune place disponible !") + .foregroundStyle(.red) } } } if availableSeeds.isEmpty == false { - if seedSpaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) - } - - _save(seeds: [team]) - } - } - } label: { - TeamRowView(team: team, displayCallDate: false) - } - } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) - } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) - } - } else if spaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: spaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) - } - _save(seeds: [team]) - } - } - } label: { - TeamRowView(team: team, displayCallDate: false) - } - } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) - } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) - } - } + let spots = (seedSpaceLeft.isEmpty == false) ? seedSpaceLeft : spaceLeft + let opposingSeeding = (seedSpaceLeft.isEmpty == false) ? false : true + _drawSection(availableSeeds: availableSeeds, spots: spots, opposingSeeding: opposingSeeding, isRoundValidForSeeding: isRoundValidForSeeding) } } - + if isEditingTournamentSeed.wrappedValue == true { let slideToDelete = SlideToDeleteSeedTip() TipView(slideToDelete).tipStyle(tint: .logoRed, asSection: true) @@ -259,11 +226,11 @@ struct RoundView: View { } } - #if DEBUG +#if DEBUG Spacer() Text(match.index.formatted() + " " + match.teamScores.count.formatted()) - #endif +#endif } } footer: { if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true { @@ -306,7 +273,7 @@ struct RoundView: View { let seeds = _seeds(availableSeedGroup: availableSeedGroup) let availableSeedSpot = _availableSeedSpot(availableSeedGroup: availableSeedGroup) NavigationStack { - SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true) { draws in + SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true, hideNames: hideNames) { draws in Task { draws.forEach { drawResult in seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) @@ -351,7 +318,7 @@ struct RoundView: View { return spots } } - + private func _save(seeds: [TeamRegistration]) { do { @@ -363,10 +330,10 @@ struct RoundView: View { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } - + _refreshNames() } - + private func _save() { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false @@ -399,18 +366,45 @@ struct RoundView: View { } } } + + private func _drawSection(availableSeeds: [TeamRegistration], spots: [Match], opposingSeeding: Bool, isRoundValidForSeeding: Bool) -> some View { + Section { + DisclosureGroup { + ForEach(availableSeeds) { team in + NavigationLink { + SpinDrawView(drawees: [team], segments: spots) { results in + Task { + results.forEach { drawResult in + team.setSeedPosition(inSpot: spots[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) + } + + _save(seeds: [team]) + } + } + } label: { + TeamRowView(team: team, displayCallDate: false) + } + .disabled(spots.isEmpty || isRoundValidForSeeding == false) + } + } label: { + Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) + } + } header: { + Text("Tirage au sort visuel d'une tête de série").font(.subheadline) + } footer: { + if spots.isEmpty || isRoundValidForSeeding == false { + Text("Aucune place disponible ! Ajouter une manche via les réglages du tableau.") + .foregroundStyle(.red) + } + } + } } -//#Preview { -// RoundView(round: Round.mock()) -// .environment(Tournament.mock()) -//} - struct MatchSpot: SpinDrawable { let match: Match let teamPosition: TeamPosition - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { [match.roundTitle(), matchTitle(displayStyle: displayStyle)].compactMap { $0 } } diff --git a/PadelClub/Views/Team/TeamRestingView.swift b/PadelClub/Views/Team/TeamRestingView.swift index c76297a..548e218 100644 --- a/PadelClub/Views/Team/TeamRestingView.swift +++ b/PadelClub/Views/Team/TeamRestingView.swift @@ -9,111 +9,81 @@ import SwiftUI struct TeamRestingView: View { @Environment(Tournament.self) var tournament: Tournament - @State private var sortingMode: SortingMode = .restingTime + @State private var displayMode: DisplayMode = .teams @State private var selectedCourt: Int? @State private var readyMatches: [Match] = [] @State private var matchesLeft: [Match] = [] + @State private var teams: [TeamRegistration] = [] - enum SortingMode: Int, Identifiable, CaseIterable { + enum DisplayMode: Int, Identifiable, CaseIterable { var id: Int { self.rawValue } - case index + case teams case restingTime - case court func localizedSortingModeLabel() -> String { switch self { - case .index: - return "Ordre" - case .court: - return "Terrain" + case .teams: + return "Équipes" case .restingTime: - return "Repos" + return "Matchs" } } } - var sortingModeCases: [SortingMode] { - var sortingModes = [SortingMode]() - sortingModes.append(.index) - sortingModes.append(.restingTime) - sortingModes.append(.court) - return sortingModes - } - func contentUnavailableDescriptionLabel() -> String { - switch sortingMode { - case .index: - return "Ce tournoi n'a aucun match prêt à démarrer" + switch displayMode { case .restingTime: return "Ce tournoi n'a aucun match prêt à démarrer" - case .court: - return "Ce tournoi n'a aucun match prêt à démarrer" + case .teams: + return "Ce tournoi n'a aucune équipe ayant déjà terminé un match." } } var sortedMatches: [Match] { - switch sortingMode { - case .index: - return readyMatches - case .restingTime: - return readyMatches.sorted(by: \.restingTimeForSorting) - case .court: - return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) - } + return readyMatches.sorted(by: \.restingTimeForSorting) } - + + var sortedTeams: [TeamRegistration] { + return teams + } + var body: some View { List { Section { - Picker(selection: $selectedCourt) { - Text("Aucun").tag(nil as Int?) - ForEach(0.. -300 { + Text("vient de finir") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("en repos depuis " + value) + .font(.footnote) + .foregroundStyle(.secondary) } + } + } + } + + struct TeamView: View { + let team: TeamRegistration - if let name = team.name, name.isEmpty == false { - Text(name).foregroundStyle(.secondary).font(.footnote) - if team.players().isEmpty { - Text("Aucun joueur") - } else { - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) - } - } + var body: some View { + if let name = team.name, name.isEmpty == false { + Text(name).foregroundStyle(.secondary).font(.footnote) + if team.players().isEmpty { + Text("Aucun joueur") + } else { + CompactTeamView(team: team) + } + } else { + if team.players().isEmpty == false { + CompactTeamView(team: team) } else { - if team.players().isEmpty == false { - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + Text("Place réservée") + Text("Place réservée") + } + } + } + } + + struct TeamHeadlineView: View { + let team: TeamRegistration + + var body: some View { + HStack { + if let groupStage = team.groupStageObject() { + HStack { + Text(groupStage.groupStageTitle(.title)) + if let finalPosition = groupStage.finalPosition(ofTeam: team) { + Text((finalPosition + 1).ordinalFormatted()) } - } else { - Text("Place réservée") - Text("Place réservée") } + } else if let round = team.initialRound() { + Text(round.roundTitle(.wide)) } - } - if displayCallDate { - if let callDate = team.callDate { - Text("Déjà convoquée \(callDate.localizedDate())") - .foregroundStyle(.logoRed) - .italic() - .font(.caption) - } else { - Text("Pas encore convoquée") - .foregroundStyle(.logoRed) - .italic() - .font(.caption) + + if let wildcardLabel = team.wildcardLabel() { + Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption) } } } } -} -//#Preview { -// TeamRowView(team: TeamRegistration.mock()) -//} + struct TeamCallDateView: View { + let team: TeamRegistration + + var body: some View { + if let callDate = team.callDate { + Text("Déjà convoquée \(callDate.localizedDate())") + .foregroundStyle(.logoRed) + .italic() + .font(.caption) + } else { + Text("Pas encore convoquée") + .foregroundStyle(.logoRed) + .italic() + .font(.caption) + } + } + } + + struct CompactTeamView: View { + let team: TeamRegistration + + var body: some View { + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + } + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index d687e29..4e12408 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -102,18 +102,14 @@ struct BroadcastView: View { } Section { - Toggle(isOn: $tournament.isPrivate) { - Text("Tournoi privé") - } + Toggle("Visible sur Padel Club", isOn: Binding( + get: { !tournament.isPrivate }, + set: { tournament.isPrivate = !$0 } + )) Toggle(isOn: $tournament.hideTeamsWeight) { Text("Masquer les poids des équipes") } - - } footer: { - let verb : String = tournament.isPrivate ? "est" : "sera" - let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))" - Text(.init(footerString)) } if tournament.isPrivate == false { diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 1eca68f..40f0feb 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -370,4 +370,22 @@ final class ServerDataTests: XCTestCase { } + func testDrawLog() async throws { + + let tournament: [Tournament] = try await StoreCenter.main.service().get() + guard let tournamentId = tournament.first?.id else { + assertionFailure("missing tournament in database") + return + } + + let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two) + let d: DrawLog = try await StoreCenter.main.service().post(drawLog) + + assert(d.tournament == drawLog.tournament) + assert(d.drawDate.formatted() == drawLog.drawDate.formatted()) + assert(d.drawSeed == drawLog.drawSeed) + assert(d.drawTeamPosition == drawLog.drawTeamPosition) + assert(d.drawMatchIndex == drawLog.drawMatchIndex) + } + }