diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 2e203a6..9fc3e62 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -324,4 +324,17 @@ class DataStore: ObservableObject { return runningMatches } + func endMatches() -> [Match] { + let dateNow : Date = Date() + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + + var runningMatches: [Match] = [] + for tournament in lastTournaments { + let matches = tournament.tournamentStore.matches.filter { match in + match.hasEnded() } + runningMatches.append(contentsOf: matches) + } + return runningMatches.sorted(by: \.endDate!, order: .descending) + } + } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 2fa5ba0..7bd6547 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -496,6 +496,11 @@ defer { } } + func loserMatches() -> [Match] { + guard let roundObject else { return [] } + return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 }) + } + func loserMatch(_ teamPosition: TeamPosition) -> Match? { if teamPosition == .one { return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil) @@ -718,7 +723,7 @@ defer { func availableCourts() -> [Int] { let courtUsed = currentTournament()?.courtUsed() ?? [] - return Array(Set(allCourts().map { $0 }).subtracting(Set(courtUsed))) + return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted() } func removeCourt() { @@ -970,9 +975,27 @@ defer { return confirmed == false && startDate.timeIntervalSinceNow < 0 } - func expectedFormattedStartDate() -> String { + func expectedFormattedStartDate(updatedField: Int?) -> String { guard let startDate else { return "" } - return "était prévu à " + startDate.formattedAsHourMinute() + guard hasEnded() == false, isRunning() == false else { return "" } + let depthReadiness = depthReadiness() + if depthReadiness == 0 { + let availableCourts = availableCourts() + if canBePlayedInSpecifiedCourt() { + return "possible tout de suite" + } else if let updatedField, availableCourts.contains(updatedField) { + return "possible tout de suite \(courtName(for: updatedField))" + } else if let first = availableCourts.first { + return "possible tout de suite \(courtName(for: first))" + } else if let estimatedStartDate = estimatedStartDate() { + return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0) + } + return "était prévu à " + startDate.formattedAsHourMinute() + } else if depthReadiness == 1 { + return "possible prochaine rotation" + } else { + return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())" + } } func runningDuration() -> String { @@ -1031,7 +1054,23 @@ defer { return false }) } - + + func depthReadiness() -> Int { + // Base case: If this match is ready, the depth is 0 + if isReady() { + return 0 + } + + // Recursive case: If not ready, check the maximum depth of readiness among previous matches + // If previousMatches() is empty, return a default depth of -1 + let previousDepth = ancestors().map { $0.depthReadiness() }.max() ?? -1 + return previousDepth + 1 + } + + func ancestors() -> [Match] { + previousMatches() + loserMatches() + } + enum CodingKeys: String, CodingKey { case _id = "id" diff --git a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift index c40c74a..37815d3 100644 --- a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift +++ b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift @@ -34,4 +34,10 @@ public extension FixedWidthInteger { func formattedAsRawString() -> String { String(self) } + + func durationInHourMinutes() -> String { + let duration = Duration.seconds(self*60) + let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) + return formatStyle.format(duration) + } } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 84ec569..9bdb2d8 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -63,23 +63,9 @@ struct MatchSummaryView: View { } Spacer() VStack(alignment: .trailing, spacing: 0) { - if match.hasEnded() == false, match.isRunning() == false { - if let courtName, match.canBePlayedInSpecifiedCourt() { - Text("prévu") - Text(courtName) - } else if let first = match.availableCourts().first { - Text("possible") - Text(match.courtName(for: first)) - } else if let estimatedStartDate { - Text(match.courtName(for: estimatedStartDate.0) + " possible") - Text("dans ~ " + estimatedStartDate.1.timeElapsedString()) - } else if let courtName { - Text(courtName) - } else { - Text("") - } - } else if let courtName { + if let courtName { Text(courtName) + .strikethrough(match.courtIndex! != updatedField && match.isReady() && match.canBePlayedInSpecifiedCourt() == false) } } .foregroundStyle(.secondary) @@ -111,18 +97,32 @@ struct MatchSummaryView: View { if matchViewStyle != .plainStyle { HStack { if match.expectedToBeRunning() { - Text(match.expectedFormattedStartDate()) + Text(match.expectedFormattedStartDate(updatedField: updatedField)) .font(.footnote) .foregroundStyle(.secondary) } Spacer() - MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField ?? estimatedStartDate?.0) + MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: possibleCourtIndex) } } } .padding(.vertical, padding) .monospacedDigit() } + + var possibleCourtIndex: Int? { + let availableCourts = match.availableCourts() + if match.canBePlayedInSpecifiedCourt() { + return nil + } else if let updatedField, availableCourts.contains(updatedField) { + return updatedField + } else if let first = availableCourts.first { + return first + } else if let estimatedStartDate { + return estimatedStartDate.0 + } + return updatedField + } } //#Preview { diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift index aa16926..15e4bb9 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift @@ -46,7 +46,7 @@ struct OngoingContainerView: View { GenericDestinationPickerView(selectedDestination: $ongoingViewModel.destination, destinations: OngoingDestination.allCases, nilDestinationIsValid: false) switch ongoingViewModel.destination! { - case .running, .followUp: + case .running, .followUp, .over: OngoingView() case .court, .free: OngoingCourtView() diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift index f97e021..ebf38f4 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift @@ -17,12 +17,16 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable case followUp case court case free + case over var runningAndNextMatches: [Match] { - if self == .followUp { - OngoingViewModel.shared.filteredRunningAndNextMatches - } else { - OngoingViewModel.shared.runningAndNextMatches + switch self { + case .running, .court, .free: + return OngoingViewModel.shared.runningAndNextMatches + case .followUp: + return OngoingViewModel.shared.filteredRunningAndNextMatches + case .over: + return DataStore.shared.endMatches() } } @@ -56,6 +60,8 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable ContentUnavailableView("Aucun match en cours", systemImage: "sportscourt", description: Text("Tous vos terrains correspondant aux matchs en cours seront visibles ici, quelque soit le tournoi.")) case .free: ContentUnavailableView("Aucun terrain libre", systemImage: "sportscourt", description: Text("Les terrains libres seront visibles ici, quelque soit le tournoi.")) + case .over: + ContentUnavailableView("Aucun match terminé", systemImage: "clock.badge.xmark", description: Text("Les matchs terminés seront visibles ici, quelque soit le tournoi.")) } } @@ -69,6 +75,8 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable return "Terrains" case .free: return "Libres" + case .over: + return "Finis" } } @@ -80,6 +88,8 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable return true case .followUp: return match.isRunning() == false + case .over: + return match.hasEnded() } } @@ -96,9 +106,7 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable func badgeValue() -> Int? { switch self { - case .running: - sortedMatches.count - case .followUp: + case .running, .followUp, .over: sortedMatches.count case .court: sortedCourtIndex.filter({ index in diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 30bb84d..9fd6867 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -14,12 +14,12 @@ struct FollowUpMatchView: View { let readyMatches: [Match] let matchesLeft: [Match] let isFree: Bool - var autoDismiss: Bool = true + var autoDismiss: Bool = false @State private var sortingMode: SortingMode? = .index @State private var selectedCourt: Int? @State private var checkCanPlay: Bool = false - @State private var seeAll: Bool = false + @State private var seeAll: Bool = true @Binding var dismissWhenPresentFollowUpMatchIsDismissed: Bool var matches: [Match] {