diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 4a2c01b..cae5d4a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -777,6 +777,12 @@ FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; + FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; + FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; + FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; @@ -1164,6 +1170,8 @@ FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubButtonView.swift; sourceTree = ""; }; FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatisticView.swift; sourceTree = ""; }; FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireStatisticView.swift; sourceTree = ""; }; + FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingContainerView.swift; sourceTree = ""; }; + FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = ""; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; 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 = ""; }; @@ -1741,6 +1749,8 @@ isa = PBXGroup; children = ( FF5D30552BD95B1100F2B93D /* OngoingView.swift */, + FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */, + FFA252B42CDD2C630074E63F /* OngoingDestination.swift */, ); path = Ongoing; sourceTree = ""; @@ -2433,6 +2443,7 @@ FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, + FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, @@ -2482,6 +2493,7 @@ C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, + FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, @@ -2715,6 +2727,7 @@ FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */, FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, + FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, @@ -2764,6 +2777,7 @@ FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFD2C996C0600151637 /* User.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, + FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, @@ -2976,6 +2990,7 @@ FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */, FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, + FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, @@ -3025,6 +3040,7 @@ FF70FB7C2C90584900129CC2 /* User.swift in Sources */, C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, + FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 4ebdaad..2e203a6 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -304,10 +304,24 @@ class DataStore: ObservableObject { var runningMatches: [Match] = [] for tournament in lastTournaments { let matches = tournament.tournamentStore.matches.filter { match in - match.confirmed && match.startDate != nil && match.endDate == nil } + match.isRunning() } runningMatches.append(contentsOf: matches) } return runningMatches } + + func runningAndNextMatches() -> [Match] { + let dateNow : Date = Date() + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + + var runningMatches: [Match] = [] + for tournament in lastTournaments { + let matches = tournament.tournamentStore.matches.filter { match in + match.startDate != nil && match.endDate == nil } + runningMatches.append(contentsOf: matches) + } + return runningMatches + } + } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 3e522eb..2fa5ba0 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -151,7 +151,7 @@ defer { case .wide, .title: return "Match \(indexInRound(in: matches) + 1)" case .short: - return "#\(indexInRound(in: matches) + 1)" + return "n˚\(indexInRound(in: matches) + 1)" } } @@ -212,7 +212,7 @@ defer { } func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { - startDate = targetStartDate + startDate = targetStartDate ?? startDate confirmed = false endDate = nil followingMatch()?.cleanScheduleAndSave(nil) @@ -452,14 +452,14 @@ defer { } } - func roundTitle() -> String? { + func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? { if groupStage != nil { return groupStageObject?.groupStageTitle() } else if let roundObject { return roundObject.roundTitle() } else { return nil } } - func roundAndMatchTitle() -> String { - [roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") + func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String { + [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ") } func topPreviousRoundMatchIndex() -> Int { @@ -496,6 +496,15 @@ defer { } } + func loserMatch(_ teamPosition: TeamPosition) -> Match? { + if teamPosition == .one { + return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil) + } else { + return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil) + } + + } + var computedOrder: Int { if let groupStageObject { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) @@ -681,6 +690,14 @@ defer { } } + func courtName(for selectedIndex: Int) -> String { + if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) { + return courtName + } else { + return Court.courtIndexedTitle(atIndex: selectedIndex) + } + } + func courtCount() -> Int { return currentTournament()?.courtCount ?? 1 } @@ -948,6 +965,74 @@ defer { previousMatches().allSatisfy({ $0.isSeeded() == false }) } + func expectedToBeRunning() -> Bool { + guard let startDate else { return false } + return confirmed == false && startDate.timeIntervalSinceNow < 0 + } + + func expectedFormattedStartDate() -> String { + guard let startDate else { return "" } + return "était prévu à " + startDate.formattedAsHourMinute() + } + + func runningDuration() -> String { + guard let startDate else { return "" } + return " depuis " + startDate.timeElapsedString() + } + + func canBePlayedInSpecifiedCourt() -> Bool { + guard let courtIndex else { return false } + if expectedToBeRunning() { + return courtIsAvailable(courtIndex) + } else { + return true + } + } + + typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date) + + func nextCourtsAvailable() -> [CourtIndexAndDate] { + guard let tournament = currentTournament() else { return [] } + let availableCourts = availableCourts() + let runningMatches = Tournament.runningMatches(tournament.allMatches()) + let startDate = Date().withoutSeconds() + if runningMatches.isEmpty { + return availableCourts.map { + ($0, startDate) + } + } + + let optionalDates : [CourtIndexAndDate?] = runningMatches.map({ match in + guard let endDate = match.estimatedEndDate(tournament.additionalEstimationDuration) else { return nil } + guard let courtIndex = match.courtIndex else { return nil } + if endDate <= startDate { + return (courtIndex, startDate.addingTimeInterval(600)) + } else { + return (courtIndex, endDate) + } + }) + + let dates : [CourtIndexAndDate] = optionalDates.compacted().sorted { a, b in + a.1 < b.1 + } + return dates + } + + func estimatedStartDate() -> CourtIndexAndDate? { + guard isReady() else { return nil } + guard let tournament = currentTournament() else { return nil } + let availableCourts = nextCourtsAvailable() + return availableCourts.first(where: { (courtIndex, startDate) in + let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60) + if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false { + return true + } + + return false + }) + } + + enum CodingKeys: String, CodingKey { case _id = "id" case _round = "round" diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index f277d11..265e03f 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -575,7 +575,7 @@ final class MatchScheduler : ModelObject, Storable { print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches") - return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) + return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) } func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index f79366b..0899530 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -514,7 +514,7 @@ final class Tournament : ModelObject, Storable { } func courtUsed() -> [Int] { -#if DEBUG //DEBUGING TIME +#if _DEBUGING_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) @@ -522,7 +522,7 @@ defer { } #endif - let runningMatches: [Match] = self.tournamentStore.matches.filter { $0.isRunning() } + let runningMatches: [Match] = DataStore.shared.runningMatches() return Set(runningMatches.compactMap { $0.courtIndex }).sorted() } @@ -1169,7 +1169,9 @@ defer { // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { + static let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)] + + static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1177,10 +1179,10 @@ defer { print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending) } - func runningMatches(_ allMatches: [Match]) -> [Match] { + static func runningMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1188,10 +1190,10 @@ defer { print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) } - func readyMatches(_ allMatches: [Match]) -> [Match] { + static func readyMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1199,10 +1201,10 @@ defer { print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) } - func matchesLeft(_ allMatches: [Match]) -> [Match] { + static func matchesLeft(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1210,11 +1212,11 @@ defer { print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) } - func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { + static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -2397,6 +2399,16 @@ defer { 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 }) + return courtLockedSchedule.anySatisfy({ dateInterval in + let range = startDate.. Date { + let calendar = Calendar.current + return calendar.date(bySettingHour: calendar.component(.hour, from: self), + minute: calendar.component(.minute, from: self), + second: 0, + of: self)! + } + func localizedDate() -> String { self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute() } @@ -232,6 +240,14 @@ extension Date { self.formatted(.dateTime.weekday(.wide)) } + func timeElapsedString() -> String { + let timeInterval = abs(Date().timeIntervalSince(self)) + let duration = Duration.seconds(timeInterval) + + let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) + return formatStyle.format(duration) + } + static var hourMinuteFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.hour, .minute] // Customize units diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 59079c5..86e23c8 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -73,7 +73,7 @@ struct GenericDestinationPickerView: ) .offset(x: 3, y: 3) } else if let count, count > 0 { - Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill") .foregroundColor(destination.badgeValueColor() ?? .logoRed) .imageScale(.medium) .background ( @@ -93,7 +93,7 @@ struct GenericDestinationPickerView: ) .offset(x: 3, y: 3) } else if let count = destination.badgeValue(), count > 0 { - Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill") .foregroundColor(destination.badgeValueColor() ?? .logoRed) .imageScale(.medium) .background ( diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index 412ee38..c084661 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -10,7 +10,6 @@ import SwiftUI struct MatchListView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament let section: String let matches: [Match]? @@ -30,24 +29,22 @@ struct MatchListView: View { @ViewBuilder var body: some View { if _shouldHide() == false { - Section { - DisclosureGroup(isExpanded: $isExpanded) { - if let matches { - ForEach(matches) { match in - MatchRowView(match: match, matchViewStyle: matchViewStyle) - .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) - } + DisclosureGroup(isExpanded: $isExpanded) { + if let matches { + ForEach(matches) { match in + MatchRowView(match: match, matchViewStyle: matchViewStyle) + .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) } - } label: { - LabeledContent { - if matches == nil { - ProgressView() - } else { - Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) - } - } label: { - Text(section.firstCapitalized) + } + } label: { + LabeledContent { + if matches == nil { + ProgressView() + } else { + Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) } + } label: { + Text(section.firstCapitalized) } } } diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 6f3355d..ebc9e2a 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -111,7 +111,7 @@ struct GroupStagesView: View { GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) switch selectedDestination { case .all: - let finishedMatches = tournament.finishedMatches(allMatches, limit: nil) + let finishedMatches = Tournament.finishedMatches(allMatches, limit: nil) List { if tournament.groupStageAdditionalQualified > 0 { @@ -148,10 +148,10 @@ struct GroupStagesView: View { } } - let runningMatches = tournament.runningMatches(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "prêt à démarrer", matches: tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) } .navigationTitle("Toutes les poules") diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index d71d326..9d33fe2 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -26,7 +26,17 @@ struct MatchDateView: View { self.isReady = match.isReady() self.hasWalkoutTeam = match.hasWalkoutTeam() self.hasEnded = match.hasEnded() - self.updatedField = updatedField + if updatedField == nil, match.canBePlayedInSpecifiedCourt() { + self.updatedField = match.courtIndex + } else if let updatedField { + self.updatedField = updatedField + } else { + self.updatedField = match.availableCourts().first + } + } + + var currentDate: Date { + Date().withoutSeconds() } var body: some View { @@ -41,7 +51,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Date() + match.startDate = currentDate match.endDate = nil match.confirmed = true _save() @@ -50,7 +60,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) + match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate) match.endDate = nil match.confirmed = true _save() @@ -59,7 +69,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) + match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate) match.endDate = nil match.confirmed = true _save() @@ -68,13 +78,15 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: Date()) + match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate) match.endDate = nil match.confirmed = true _save() } } header: { - Text("Le match apparaîtra dans les en cours") + if let updatedField { + Text(match.courtName(for: updatedField)) + } } } else { Button("Décaler de \(estimatedDuration) minutes") { diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 5901d37..eca6643 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -46,8 +46,26 @@ struct PlayerBlockView: View { teamScore?.score?.components(separatedBy: ",") ?? [] } - private func _defaultLabel() -> String { - teamPosition.localizedLabel() + private func _defaultLabel() -> [String] { + var defaultLabels = [String]() + if let previous = match.previousMatch(teamPosition) { + defaultLabels.append("Gagnant \(previous.roundAndMatchTitle(.short))") + if previous.isReady() == true { + if let courtName = previous.courtName(), previous.isRunning() { + defaultLabels.append(courtName + "\(previous.runningDuration())") + } + } + } else if let loser = match.loserMatch(teamPosition) { + defaultLabels.append("Perdant \(loser.roundAndMatchTitle(.short))") + if loser.isReady() == true { + if let courtName = loser.courtName(), loser.isRunning() { + defaultLabels.append(courtName + "\(loser.runningDuration())") + } + } + } else { + defaultLabels.append(teamPosition.localizedLabel()) + } + return defaultLabels } var body: some View { @@ -74,7 +92,13 @@ struct PlayerBlockView: View { Text("longLabelPlayerTwo").lineLimit(1) } .opacity(0) - Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) + VStack(alignment: .leading) { + ForEach(_defaultLabel(), id: \.self) { name in + Text(name) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 9af3d85..4adaa02 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -162,8 +162,11 @@ struct MatchDetailView: View { dismiss() } }) { - FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) - .tint(.master) + NavigationStack { + + FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) + } + .tint(.master) } .sheet(isPresented: $presentRanking, content: { if let currentTournament = match.currentTournament() { @@ -492,19 +495,20 @@ struct MatchDetailView: View { Text("Horaire") } .onChange(of: startDateSetup) { + let date = Date().withoutSeconds() switch startDateSetup { case .customDate: break case .now: - startDate = Date() + startDate = date case .nextRotation: - let baseDate = match.startDate ?? Date() + let baseDate = match.startDate ?? date startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60) case .previousRotation: - let baseDate = match.startDate ?? Date() + let baseDate = match.startDate ?? date startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60) case .inMinutes(let minutes): - startDate = Date().addingTimeInterval(Double(minutes) * 60) + startDate = date.addingTimeInterval(Double(minutes) * 60) } } } @@ -546,7 +550,7 @@ struct MatchDetailView: View { } RowButtonView("Valider") { - match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) + match.validateMatch(fromStartDate: startDateSetup == .now ? Date().withoutSeconds() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) save() diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index fc03111..199fa43 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -59,12 +59,26 @@ struct MatchSummaryView: View { } } Spacer() - if let courtName { - Spacer() - Text(courtName) - .foregroundStyle(.gray) - .font(.caption) + VStack(alignment: .trailing, spacing: 0) { + if let courtName, match.canBePlayedInSpecifiedCourt() { + if match.isRunning() == false { + Text("prévu") + } + Text(courtName) + } else if let first = match.availableCourts().first { + Text("possible") + Text(match.courtName(for: first)) + } else { + if let estimatedStartDate = match.estimatedStartDate() { + Text(match.courtName(for: estimatedStartDate.0) + " possible") + Text("dans ~ " + estimatedStartDate.1.timeElapsedString()) + } else { + Text("aucun terrain disponible") + } + } } + .foregroundStyle(.secondary) + .font(.footnote) } .lineLimit(1) } @@ -91,6 +105,11 @@ struct MatchSummaryView: View { if matchViewStyle != .plainStyle { HStack { + if match.expectedToBeRunning() { + Text(match.expectedFormattedStartDate()) + .font(.footnote) + .foregroundStyle(.secondary) + } Spacer() MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField) } diff --git a/PadelClub/Views/Navigation/Agenda/CalendarView.swift b/PadelClub/Views/Navigation/Agenda/CalendarView.swift index b700129..1dab0aa 100644 --- a/PadelClub/Views/Navigation/Agenda/CalendarView.swift +++ b/PadelClub/Views/Navigation/Agenda/CalendarView.swift @@ -124,7 +124,7 @@ struct CalendarView: View { ) .overlay(alignment: .bottomTrailing) { if let count = counts[day.dayInt] { - Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(count).circle.fill" : "ellipsis.circle.fill") .foregroundColor(.secondary) .imageScale(.medium) .background ( diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 6fc905d..ee01859 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -79,7 +79,7 @@ struct MainView: View { TournamentOrganizerView() .tabItem(for: .tournamentOrganizer) .toolbarBackground(.visible, for: .tabBar) - OngoingView() + OngoingContainerView() .tabItem(for: .ongoing) .badge(self.dataStore.runningMatches().count) .toolbarBackground(.visible, for: .tabBar) diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift new file mode 100644 index 0000000..1946c24 --- /dev/null +++ b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift @@ -0,0 +1,100 @@ +// +// OngoingContainerView.swift +// PadelClub +// +// Created by razmig on 07/11/2024. +// + +import SwiftUI +import LeStorage + +@Observable +class OngoingViewModel { + static let shared = OngoingViewModel() + + var destination: OngoingDestination? = .running + var hideUnconfirmedMatches: Bool = false + var hideNotReadyMatches: Bool = false + + func areFiltersEnabled() -> Bool { + hideUnconfirmedMatches || hideNotReadyMatches + } + + let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.index), .keyPath(\Match.courtIndexForSorting)] + + var runningAndNextMatches: [Match] { + DataStore.shared.runningAndNextMatches().sorted(using: defaultSorting, order: .ascending) + } + + var filteredRunningAndNextMatches: [Match] { + if destination == .followUp { + return runningAndNextMatches.filter({ + (hideUnconfirmedMatches == false || hideUnconfirmedMatches == true && $0.confirmed) + && (hideNotReadyMatches == false || hideNotReadyMatches == true && $0.isReady() ) + }) + } else { + return runningAndNextMatches + } + } +} + +struct OngoingContainerView: View { + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @State private var showMatchPicker: Bool = false + + var body: some View { + @Bindable var navigation = navigation + @Bindable var ongoingViewModel = OngoingViewModel.shared + NavigationStack(path: $navigation.ongoingPath) { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $ongoingViewModel.destination, destinations: OngoingDestination.allCases, nilDestinationIsValid: false) + + switch ongoingViewModel.destination! { + case .running, .followUp: + OngoingView() + case .court, .free: + OngoingCourtView() + } + } + .toolbarBackground(.visible, for: .bottomBar) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Programmation") + .toolbar { + if ongoingViewModel.destination == .followUp { + ToolbarItem(placement: .topBarLeading) { + Menu { + Toggle(isOn: $ongoingViewModel.hideUnconfirmedMatches) { + Text("masquer non confirmés") + } + Toggle(isOn: $ongoingViewModel.hideNotReadyMatches) { + Text("masquer incomplets") + } + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + .symbolVariant(ongoingViewModel.areFiltersEnabled() ? .fill : .none) + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + showMatchPicker = true + } label: { + Image(systemName: "rectangle.stack.badge.plus") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + } + } + } + .environment(ongoingViewModel) + .sheet(isPresented: $showMatchPicker, content: { + FollowUpMatchView(selectedCourt: nil, allMatches: ongoingViewModel.runningAndNextMatches, autoDismiss: false) + .tint(.master) + }) + } +} diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift new file mode 100644 index 0000000..f97e021 --- /dev/null +++ b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift @@ -0,0 +1,121 @@ +// +// OngoingDestination.swift +// PadelClub +// +// Created by razmig on 07/11/2024. +// +import SwiftUI + +enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { + var id: Int { self.rawValue } + + static func == (lhs: OngoingDestination, rhs: OngoingDestination) -> Bool { + return lhs.id == rhs.id + } + + case running + case followUp + case court + case free + + var runningAndNextMatches: [Match] { + if self == .followUp { + OngoingViewModel.shared.filteredRunningAndNextMatches + } else { + OngoingViewModel.shared.runningAndNextMatches + } + } + + var sortedMatches: [Match] { + return runningAndNextMatches.filter({ self.shouldDisplay($0) }) + } + + var filteredMatches: [Match] { + sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) }) + } + + var sortedCourtIndex: [Int?] { + let courtUsed = sortedMatches.grouped(by: { $0.courtIndex }).keys + let sortedNumbers = courtUsed.sorted { (a, b) -> Bool in + switch (a, b) { + case (nil, _): return false + case (_, nil): return true + case let (a?, b?): return a < b + } + } + return sortedNumbers + } + + func contentUnavailable() -> some View { + switch self { + case .running: + ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) + case .followUp: + ContentUnavailableView("Aucun match à suivre", systemImage: "figure.tennis", description: Text("Tous vos matchs planifiés et confirmés, seront visibles ici, quelque soit le tournoi.")) + case .court: + 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.")) + } + } + + func localizedFilterModeLabel() -> String { + switch self { + case .running: + return "En cours" + case .followUp: + return "À suivre" + case .court: + return "Terrains" + case .free: + return "Libres" + } + } + + func shouldDisplay(_ match: Match) -> Bool { + switch self { + case .running: + return match.isRunning() + case .court, .free: + return true + case .followUp: + return match.isRunning() == false + } + } + + func selectionLabel(index: Int) -> String { + localizedFilterModeLabel() + } + + func systemImage() -> String? { + switch self { + default: + return nil + } + } + + func badgeValue() -> Int? { + switch self { + case .running: + sortedMatches.count + case .followUp: + sortedMatches.count + case .court: + sortedCourtIndex.filter({ index in + filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false + }).count + case .free: + sortedCourtIndex.filter({ index in + filteredMatches.filter({ $0.courtIndex == index }).isEmpty + }).count + } + } + + func badgeValueColor() -> Color? { + nil + } + + func badgeImage() -> Badge? { + nil + } +} diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 226b052..d36fd40 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -8,82 +8,110 @@ import SwiftUI import LeStorage +extension Int: @retroactive Identifiable { + public var id: Int { + return self + } +} + + + struct OngoingView: View { @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @EnvironmentObject var dataStore: DataStore + @Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel - @State private var sortByField: Bool = false - - let fieldSorting : [MySortDescriptor] = [.keyPath(\Match.courtIndexForSorting), .keyPath(\Match.startDate!)] - let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.courtIndexForSorting)] - - var matches: [Match] { - let sorting = self.sortByField ? fieldSorting : defaultSorting - return self.dataStore.runningMatches().sorted(using: sorting, order: .ascending) + var filterMode: OngoingDestination { + ongoingViewModel.destination! } - + var body: some View { - @Bindable var navigation = navigation - NavigationStack(path: $navigation.ongoingPath) { - List { - ForEach(matches) { match in - - if let tournament = match.currentTournament() { - - Section { - MatchRowView(match: match, matchViewStyle: .standardStyle) - } header: { - HStack { - Text(tournament.tournamentTitle(.short)) - Spacer() - if let club = tournament.club() { - Text("@" + club.clubTitle(.short)) - } + let filteredMatches = filterMode.sortedMatches + List { + ForEach(filteredMatches) { match in + let tournament = match.currentTournament() + Section { + MatchRowView(match: match, matchViewStyle: .standardStyle) + } header: { + if let tournament { + HStack { + Text(tournament.tournamentTitle(.short)) + Spacer() + if let club = tournament.club() { + Text("@" + club.clubTitle(.short)) } - } footer: { - HStack { - Text(tournament.eventLabel()) + } + } + } footer: { + HStack { + if let tournament { + Text(tournament.eventLabel()) + } #if DEBUG - Spacer() - FooterButtonView("copier l'id") { - let pasteboard = UIPasteboard.general - pasteboard.string = match.id - } -#endif - } + Spacer() + FooterButtonView("copier l'id") { + let pasteboard = UIPasteboard.general + pasteboard.string = match.id } - +#endif } - } } - .headerProminence(.increased) - .overlay { - if matches.isEmpty { - ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) - } + } + .headerProminence(.increased) + .overlay { + if filteredMatches.isEmpty { + filterMode.contentUnavailable() } - .navigationTitle("En cours") - .toolbarBackground(.visible, for: .bottomBar) - .toolbar(matches.isEmpty ? .hidden : .visible, for: .navigationBar) - .toolbar { - ToolbarItem(placement: .status) { - Picker(selection: $sortByField) { - Text("tri par date").tag(false) - Text("tri par terrain").tag(true) - } label: { - + } + } +} + +struct OngoingCourtView: View { + + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @EnvironmentObject var dataStore: DataStore + @Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel + + var filterMode: OngoingDestination { + ongoingViewModel.destination! + } + + @State private var selectedCourtForFollowUp: Int? + + var body: some View { + let sortedMatches = filterMode.sortedMatches + let filteredMatches = sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) }) + + List { + ForEach(filterMode.sortedCourtIndex, id: \.self) { index in + let courtFilteredMatches = filteredMatches.filter({ $0.courtIndex == index }) + let title : String = (index == nil ? "Aucun terrain défini" : "Terrain #\(index! + 1)") + if (filterMode == .free && courtFilteredMatches.isEmpty) || (filterMode == .court && courtFilteredMatches.isEmpty == false) { + Section { + MatchListView(section: "En cours", matches: courtFilteredMatches, hideWhenEmpty: true, isExpanded: false) + if courtFilteredMatches.isEmpty { + Button("Choisir un match") { + selectedCourtForFollowUp = index + } + } + MatchListView(section: "À venir", matches: sortedMatches.filter({ $0.courtIndex == index && $0.hasStarted() == false }), isExpanded: false) + } header: { + Text(title) } - .pickerStyle(.segmented) - .fixedSize() - .offset(y: -3) } } } + .sheet(item: $selectedCourtForFollowUp, content: { selectedCourtForFollowUp in + FollowUpMatchView(selectedCourt: selectedCourtForFollowUp, allMatches: filterMode.runningAndNextMatches) + .tint(.master) + }) + .headerProminence(.increased) + .overlay { + if (filteredMatches.isEmpty && filterMode != .free) || (filterMode == .free && filterMode.sortedCourtIndex.allSatisfy({ index in filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false })) { + filterMode.contentUnavailable() + } + } } } - -//#Preview { -// OngoingView() -//} diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 25592c6..15a1a9e 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -19,7 +19,7 @@ struct PlanningByCourtView: View { @State private var uuid: UUID = UUID() var timeSlots: [Date:[Match]] { - Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } + Dictionary(grouping: matches.filter({ $0.startDate != nil })) { $0.startDate! } } var days: [Date] { @@ -146,24 +146,5 @@ struct PlanningByCourtView: View { } actions: { } } - } - - private func _matchesCount(inDayInt dayInt: Int) -> Int { - timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count - } - - private func _timeSlotView(key: Date, matches: [Match]) -> some View { - LabeledContent { - Text(self._formattedMatchCount(matches.count)) - } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) - Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", ")) - } - } - - fileprivate func _formattedMatchCount(_ count: Int) -> String { - return "\(count.formatted()) match\(count.pluralSuffix)" - } - - + } } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 03ae980..1e738b6 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -79,7 +79,11 @@ struct PlanningView: View { Picker(selection: $selectedDay) { Text("Tous les jours").tag(nil as Date?) ForEach(days, id: \.self) { day in - Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire").tag(day as Date?) + } else { + Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + } } } label: { Text("Jour") @@ -177,7 +181,11 @@ struct PlanningView: View { } } header: { HStack { - Text(day.formatted(.dateTime.day().weekday().month())) + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire") + } else { + Text(day.formatted(.dateTime.day().weekday().month())) + } Spacer() let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) if showFinishedMatches { @@ -186,6 +194,10 @@ struct PlanningView: View { Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") } } + } footer: { + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") + } } .headerProminence(.increased) } @@ -201,7 +213,11 @@ struct PlanningView: View { LabeledContent { Text(self._formattedMatchCount(matches.count)) } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Aucun horaire") + } else { + Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + } if matches.count <= tournament.courtCount { let names = matches.sorted(by: \.computedOrder) .compactMap({ $0.roundTitle() }) diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 3ae74cc..2356a69 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -10,17 +10,43 @@ import SwiftUI struct FollowUpMatchView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) private var dismiss - let match: Match + let match: Match? let readyMatches: [Match] let matchesLeft: [Match] let isFree: Bool + var autoDismiss: Bool = true - @State private var sortingMode: SortingMode = .index + @State private var sortingMode: SortingMode? = .index @State private var selectedCourt: Int? @State private var checkCanPlay: Bool = false + @State private var seeAll: Bool = false @Binding var dismissWhenPresentFollowUpMatchIsDismissed: Bool - enum SortingMode: Int, Identifiable, CaseIterable { + var matches: [Match] { + seeAll ? matchesLeft : readyMatches + } + + enum SortingMode: Int, Identifiable, CaseIterable, Selectable, Equatable { + func selectionLabel(index: Int) -> String { + localizedSortingModeLabel() + } + + func badgeValue() -> Int? { + nil + } + + func badgeImage() -> Badge? { + nil + } + + func badgeValueColor() -> Color? { + nil + } + + static func == (lhs: SortingMode, rhs: SortingMode) -> Bool { + return lhs.id == rhs.id + } + var id: Int { self.rawValue } case winner case loser @@ -28,14 +54,23 @@ struct FollowUpMatchView: View { case restingTime case court + func canHaveSeeAllOption() -> Bool { + switch self { + case .index, .restingTime: + return true + case .winner, .loser, .court: + return false + } + } + func localizedSortingModeLabel() -> String { switch self { case .index: - return "Ordre" + return "Ordre prévu" case .court: return "Terrain" case .restingTime: - return "Repos" + return "Temps de repos" case .winner: return "Gagnant" case .loser: @@ -50,37 +85,50 @@ struct FollowUpMatchView: View { _selectedCourt = .init(wrappedValue: match.courtIndex) let currentTournament = match.currentTournament() let allMatches = currentTournament?.allMatches() ?? [] - self.matchesLeft = currentTournament?.matchesLeft(allMatches) ?? [] - let runningMatches = currentTournament?.runningMatches(allMatches) ?? [] - let readyMatches = currentTournament?.readyMatches(allMatches) ?? [] - self.readyMatches = currentTournament?.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) ?? [] + self.matchesLeft = Tournament.matchesLeft(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.isFree = currentTournament?.isFree() ?? true } + + init(selectedCourt: Int?, allMatches: [Match], autoDismiss: Bool = true) { + _dismissWhenPresentFollowUpMatchIsDismissed = .constant(false) + _selectedCourt = .init(wrappedValue: selectedCourt) + self.match = nil + self.autoDismiss = autoDismiss + self.matchesLeft = Tournament.matchesLeft(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) + self.isFree = false + } + var winningTeam: TeamRegistration? { - match.winner() + match?.winner() } var losingTeam: TeamRegistration? { - match.loser() + match?.loser() } var sortingModeCases: [SortingMode] { var sortingModes = [SortingMode]() - if let winningTeam { + if winningTeam != nil { sortingModes.append(.winner) } - if let losingTeam { + if losingTeam != nil { sortingModes.append(.loser) } sortingModes.append(.index) sortingModes.append(.restingTime) - sortingModes.append(.court) +// sortingModes.append(.court) return sortingModes } func contentUnavailableDescriptionLabel() -> String { - switch sortingMode { + switch sortingMode! { case .winner: if let winningTeam { return "Aucun match à suivre pour \(winningTeam.teamLabel())" @@ -103,13 +151,13 @@ struct FollowUpMatchView: View { } var sortedMatches: [Match] { - switch sortingMode { + switch sortingMode! { case .index: - return readyMatches + return matches case .restingTime: - return readyMatches.sorted(by: \.restingTimeForSorting) + return matches.sorted(by: \.restingTimeForSorting) case .court: - return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) + return matchesLeft.filter({ $0.courtIndex == selectedCourt }) case .winner: if let winningTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(winningTeam.id) }) { return [followUpMatch] @@ -127,77 +175,98 @@ struct FollowUpMatchView: View { var body: some View { NavigationStack { - List { - Section { - Picker(selection: $selectedCourt) { - Text("Aucun").tag(nil as Int?) - if let tournament = match.currentTournament() { - ForEach(0..