overhault ongoing view

paca_championship
Raz 1 year ago
parent 969fa5094f
commit d6e87daa3d
  1. 16
      PadelClub.xcodeproj/project.pbxproj
  2. 16
      PadelClub/Data/DataStore.swift
  3. 95
      PadelClub/Data/Match.swift
  4. 2
      PadelClub/Data/MatchScheduler.swift
  5. 34
      PadelClub/Data/Tournament.swift
  6. 16
      PadelClub/Extensions/Date+Extensions.swift
  7. 4
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  8. 31
      PadelClub/Views/Components/MatchListView.swift
  9. 8
      PadelClub/Views/GroupStage/GroupStagesView.swift
  10. 24
      PadelClub/Views/Match/Components/MatchDateView.swift
  11. 30
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  12. 18
      PadelClub/Views/Match/MatchDetailView.swift
  13. 29
      PadelClub/Views/Match/MatchSummaryView.swift
  14. 2
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  15. 2
      PadelClub/Views/Navigation/MainView.swift
  16. 100
      PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift
  17. 121
      PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift
  18. 142
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  19. 21
      PadelClub/Views/Planning/PlanningByCourtView.swift
  20. 22
      PadelClub/Views/Planning/PlanningView.swift
  21. 223
      PadelClub/Views/Score/FollowUpMatchView.swift
  22. 8
      PadelClub/Views/Team/TeamRestingView.swift
  23. 4
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  24. 29
      PadelClub/Views/Tournament/TournamentRunningView.swift

@ -777,6 +777,12 @@
FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; };
FFA252AE2CDB734A0074E63F /* 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 */; }; 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 */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.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 = "<group>"; }; FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubButtonView.swift; sourceTree = "<group>"; };
FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatisticView.swift; sourceTree = "<group>"; }; FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatisticView.swift; sourceTree = "<group>"; };
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireStatisticView.swift; sourceTree = "<group>"; }; FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireStatisticView.swift; sourceTree = "<group>"; };
FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingContainerView.swift; sourceTree = "<group>"; };
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = "<group>"; };
FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; };
FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; }; FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; };
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@ -1741,6 +1749,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF5D30552BD95B1100F2B93D /* OngoingView.swift */, FF5D30552BD95B1100F2B93D /* OngoingView.swift */,
FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */,
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */,
); );
path = Ongoing; path = Ongoing;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2433,6 +2443,7 @@
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */,
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
@ -2482,6 +2493,7 @@
C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */,
C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */,
FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */,
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
@ -2715,6 +2727,7 @@
FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */, FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */,
FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */,
FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */,
FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */,
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */,
@ -2764,6 +2777,7 @@
FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */,
FF4CBFFD2C996C0600151637 /* User.swift in Sources */, FF4CBFFD2C996C0600151637 /* User.swift in Sources */,
FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */,
FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */,
FF4CC0002C996C0600151637 /* MockData.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */,
FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */,
@ -2976,6 +2990,7 @@
FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */, FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */,
FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */,
FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */,
FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */,
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */,
@ -3025,6 +3040,7 @@
FF70FB7C2C90584900129CC2 /* User.swift in Sources */, FF70FB7C2C90584900129CC2 /* User.swift in Sources */,
C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */,
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */,
FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */, FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */,
FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */,

@ -304,7 +304,21 @@ class DataStore: ObservableObject {
var runningMatches: [Match] = [] var runningMatches: [Match] = []
for tournament in lastTournaments { for tournament in lastTournaments {
let matches = tournament.tournamentStore.matches.filter { match in 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) runningMatches.append(contentsOf: matches)
} }
return runningMatches return runningMatches

@ -151,7 +151,7 @@ defer {
case .wide, .title: case .wide, .title:
return "Match \(indexInRound(in: matches) + 1)" return "Match \(indexInRound(in: matches) + 1)"
case .short: case .short:
return "#\(indexInRound(in: matches) + 1)" return "\(indexInRound(in: matches) + 1)"
} }
} }
@ -212,7 +212,7 @@ defer {
} }
func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { func cleanScheduleAndSave(_ targetStartDate: Date? = nil) {
startDate = targetStartDate startDate = targetStartDate ?? startDate
confirmed = false confirmed = false
endDate = nil endDate = nil
followingMatch()?.cleanScheduleAndSave(nil) followingMatch()?.cleanScheduleAndSave(nil)
@ -452,14 +452,14 @@ defer {
} }
} }
func roundTitle() -> String? { func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? {
if groupStage != nil { return groupStageObject?.groupStageTitle() } if groupStage != nil { return groupStageObject?.groupStageTitle() }
else if let roundObject { return roundObject.roundTitle() } else if let roundObject { return roundObject.roundTitle() }
else { return nil } else { return nil }
} }
func roundAndMatchTitle() -> String { func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
[roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ")
} }
func topPreviousRoundMatchIndex() -> Int { 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 { var computedOrder: Int {
if let groupStageObject { if let groupStageObject {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) 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 { func courtCount() -> Int {
return currentTournament()?.courtCount ?? 1 return currentTournament()?.courtCount ?? 1
} }
@ -948,6 +965,74 @@ defer {
previousMatches().allSatisfy({ $0.isSeeded() == false }) 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 { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _round = "round" case _round = "round"

@ -575,7 +575,7 @@ final class MatchScheduler : ModelObject, Storable {
print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches") 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 { func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date {

@ -514,7 +514,7 @@ final class Tournament : ModelObject, Storable {
} }
func courtUsed() -> [Int] { func courtUsed() -> [Int] {
#if DEBUG //DEBUGING TIME #if _DEBUGING_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -522,7 +522,7 @@ defer {
} }
#endif #endif
let runningMatches: [Match] = self.tournamentStore.matches.filter { $0.isRunning() } let runningMatches: [Match] = DataStore.shared.runningMatches()
return Set(runningMatches.compactMap { $0.courtIndex }).sorted() return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
} }
@ -1169,7 +1169,9 @@ defer {
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) // 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<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)]
static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1177,10 +1179,10 @@ defer {
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #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 #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1188,10 +1190,10 @@ defer {
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #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 #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1199,10 +1201,10 @@ defer {
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #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 #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1210,11 +1212,11 @@ defer {
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #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 #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -2398,6 +2400,16 @@ defer {
} }
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..<endDate
return dateInterval.range.overlaps(range)
})
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {

@ -36,6 +36,14 @@ enum TimeOfDay {
extension Date { extension Date {
func withoutSeconds() -> 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 { func localizedDate() -> String {
self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute() self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute()
} }
@ -232,6 +240,14 @@ extension Date {
self.formatted(.dateTime.weekday(.wide)) 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 = { static var hourMinuteFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute] // Customize units formatter.allowedUnits = [.hour, .minute] // Customize units

@ -73,7 +73,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
) )
.offset(x: 3, y: 3) .offset(x: 3, y: 3)
} else if let count, count > 0 { } 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) .foregroundColor(destination.badgeValueColor() ?? .logoRed)
.imageScale(.medium) .imageScale(.medium)
.background ( .background (
@ -93,7 +93,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
) )
.offset(x: 3, y: 3) .offset(x: 3, y: 3)
} else if let count = destination.badgeValue(), count > 0 { } 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) .foregroundColor(destination.badgeValueColor() ?? .logoRed)
.imageScale(.medium) .imageScale(.medium)
.background ( .background (

@ -10,7 +10,6 @@ import SwiftUI
struct MatchListView: View { struct MatchListView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament
let section: String let section: String
let matches: [Match]? let matches: [Match]?
@ -30,24 +29,22 @@ struct MatchListView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if _shouldHide() == false { if _shouldHide() == false {
Section { DisclosureGroup(isExpanded: $isExpanded) {
DisclosureGroup(isExpanded: $isExpanded) { if let matches {
if let matches { ForEach(matches) { match in
ForEach(matches) { match in MatchRowView(match: match, matchViewStyle: matchViewStyle)
MatchRowView(match: match, matchViewStyle: matchViewStyle) .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
}
} }
} label: { }
LabeledContent { } label: {
if matches == nil { LabeledContent {
ProgressView() if matches == nil {
} else { ProgressView()
Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) } else {
} Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix)
} label: {
Text(section.firstCapitalized)
} }
} label: {
Text(section.firstCapitalized)
} }
} }
} }

@ -111,7 +111,7 @@ struct GroupStagesView: View {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedDestination { switch selectedDestination {
case .all: case .all:
let finishedMatches = tournament.finishedMatches(allMatches, limit: nil) let finishedMatches = Tournament.finishedMatches(allMatches, limit: nil)
List { List {
if tournament.groupStageAdditionalQualified > 0 { 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: "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: "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: "à lancer", matches: Tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false)
} }
.navigationTitle("Toutes les poules") .navigationTitle("Toutes les poules")

@ -26,7 +26,17 @@ struct MatchDateView: View {
self.isReady = match.isReady() self.isReady = match.isReady()
self.hasWalkoutTeam = match.hasWalkoutTeam() self.hasWalkoutTeam = match.hasWalkoutTeam()
self.hasEnded = match.hasEnded() 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 { var body: some View {
@ -41,7 +51,7 @@ struct MatchDateView: View {
if let updatedField { if let updatedField {
match.setCourt(updatedField) match.setCourt(updatedField)
} }
match.startDate = Date() match.startDate = currentDate
match.endDate = nil match.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
@ -50,7 +60,7 @@ struct MatchDateView: View {
if let updatedField { if let updatedField {
match.setCourt(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.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
@ -59,7 +69,7 @@ struct MatchDateView: View {
if let updatedField { if let updatedField {
match.setCourt(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.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
@ -68,13 +78,15 @@ struct MatchDateView: View {
if let updatedField { if let updatedField {
match.setCourt(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.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
} }
} header: { } header: {
Text("Le match apparaîtra dans les en cours") if let updatedField {
Text(match.courtName(for: updatedField))
}
} }
} else { } else {
Button("Décaler de \(estimatedDuration) minutes") { Button("Décaler de \(estimatedDuration) minutes") {

@ -46,8 +46,26 @@ struct PlayerBlockView: View {
teamScore?.score?.components(separatedBy: ",") ?? [] teamScore?.score?.components(separatedBy: ",") ?? []
} }
private func _defaultLabel() -> String { private func _defaultLabel() -> [String] {
teamPosition.localizedLabel() 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 { var body: some View {
@ -74,7 +92,13 @@ struct PlayerBlockView: View {
Text("longLabelPlayerTwo").lineLimit(1) Text("longLabelPlayerTwo").lineLimit(1)
} }
.opacity(0) .opacity(0)
Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) VStack(alignment: .leading) {
ForEach(_defaultLabel(), id: \.self) { name in
Text(name)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
} }
} }

@ -162,8 +162,11 @@ struct MatchDetailView: View {
dismiss() dismiss()
} }
}) { }) {
FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) NavigationStack {
.tint(.master)
FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed)
}
.tint(.master)
} }
.sheet(isPresented: $presentRanking, content: { .sheet(isPresented: $presentRanking, content: {
if let currentTournament = match.currentTournament() { if let currentTournament = match.currentTournament() {
@ -492,19 +495,20 @@ struct MatchDetailView: View {
Text("Horaire") Text("Horaire")
} }
.onChange(of: startDateSetup) { .onChange(of: startDateSetup) {
let date = Date().withoutSeconds()
switch startDateSetup { switch startDateSetup {
case .customDate: case .customDate:
break break
case .now: case .now:
startDate = Date() startDate = date
case .nextRotation: case .nextRotation:
let baseDate = match.startDate ?? Date() let baseDate = match.startDate ?? date
startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60) startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60)
case .previousRotation: case .previousRotation:
let baseDate = match.startDate ?? Date() let baseDate = match.startDate ?? date
startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60) startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60)
case .inMinutes(let minutes): 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") { 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() save()

@ -59,12 +59,26 @@ struct MatchSummaryView: View {
} }
} }
Spacer() Spacer()
if let courtName { VStack(alignment: .trailing, spacing: 0) {
Spacer() if let courtName, match.canBePlayedInSpecifiedCourt() {
Text(courtName) if match.isRunning() == false {
.foregroundStyle(.gray) Text("prévu")
.font(.caption) }
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) .lineLimit(1)
} }
@ -91,6 +105,11 @@ struct MatchSummaryView: View {
if matchViewStyle != .plainStyle { if matchViewStyle != .plainStyle {
HStack { HStack {
if match.expectedToBeRunning() {
Text(match.expectedFormattedStartDate())
.font(.footnote)
.foregroundStyle(.secondary)
}
Spacer() Spacer()
MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField) MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField)
} }

@ -124,7 +124,7 @@ struct CalendarView: View {
) )
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {
if let count = counts[day.dayInt] { 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) .foregroundColor(.secondary)
.imageScale(.medium) .imageScale(.medium)
.background ( .background (

@ -79,7 +79,7 @@ struct MainView: View {
TournamentOrganizerView() TournamentOrganizerView()
.tabItem(for: .tournamentOrganizer) .tabItem(for: .tournamentOrganizer)
.toolbarBackground(.visible, for: .tabBar) .toolbarBackground(.visible, for: .tabBar)
OngoingView() OngoingContainerView()
.tabItem(for: .ongoing) .tabItem(for: .ongoing)
.badge(self.dataStore.runningMatches().count) .badge(self.dataStore.runningMatches().count)
.toolbarBackground(.visible, for: .tabBar) .toolbarBackground(.visible, for: .tabBar)

@ -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<Match>] = [.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)
})
}
}

@ -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
}
}

@ -8,82 +8,110 @@
import SwiftUI import SwiftUI
import LeStorage import LeStorage
extension Int: @retroactive Identifiable {
public var id: Int {
return self
}
}
struct OngoingView: View { struct OngoingView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel
@State private var sortByField: Bool = false var filterMode: OngoingDestination {
ongoingViewModel.destination!
let fieldSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.courtIndexForSorting), .keyPath(\Match.startDate!)]
let defaultSorting : [MySortDescriptor<Match>] = [.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 body: some View { var body: some View {
@Bindable var navigation = navigation let filteredMatches = filterMode.sortedMatches
NavigationStack(path: $navigation.ongoingPath) { List {
List { ForEach(filteredMatches) { match in
ForEach(matches) { match in let tournament = match.currentTournament()
Section {
if let tournament = match.currentTournament() { MatchRowView(match: match, matchViewStyle: .standardStyle)
} header: {
Section { if let tournament {
MatchRowView(match: match, matchViewStyle: .standardStyle) HStack {
} header: { Text(tournament.tournamentTitle(.short))
HStack { Spacer()
Text(tournament.tournamentTitle(.short)) if let club = tournament.club() {
Spacer() Text("@" + club.clubTitle(.short))
if let club = tournament.club() {
Text("@" + club.clubTitle(.short))
}
} }
} footer: { }
HStack { }
Text(tournament.eventLabel()) } footer: {
HStack {
if let tournament {
Text(tournament.eventLabel())
}
#if DEBUG #if DEBUG
Spacer() Spacer()
FooterButtonView("copier l'id") { FooterButtonView("copier l'id") {
let pasteboard = UIPasteboard.general let pasteboard = UIPasteboard.general
pasteboard.string = match.id pasteboard.string = match.id
}
#endif
}
} }
#endif
} }
} }
} }
.headerProminence(.increased) }
.overlay { .headerProminence(.increased)
if matches.isEmpty { .overlay {
ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) if filteredMatches.isEmpty {
} filterMode.contentUnavailable()
} }
.navigationTitle("En cours") }
.toolbarBackground(.visible, for: .bottomBar) }
.toolbar(matches.isEmpty ? .hidden : .visible, for: .navigationBar) }
.toolbar {
ToolbarItem(placement: .status) { struct OngoingCourtView: View {
Picker(selection: $sortByField) {
Text("tri par date").tag(false) @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
Text("tri par terrain").tag(true) @EnvironmentObject var dataStore: DataStore
} label: { @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()
//}

@ -19,7 +19,7 @@ struct PlanningByCourtView: View {
@State private var uuid: UUID = UUID() @State private var uuid: UUID = UUID()
var timeSlots: [Date:[Match]] { var timeSlots: [Date:[Match]] {
Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } Dictionary(grouping: matches.filter({ $0.startDate != nil })) { $0.startDate! }
} }
var days: [Date] { var days: [Date] {
@ -147,23 +147,4 @@ struct PlanningByCourtView: View {
} }
} }
} }
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)"
}
} }

@ -79,7 +79,11 @@ struct PlanningView: View {
Picker(selection: $selectedDay) { Picker(selection: $selectedDay) {
Text("Tous les jours").tag(nil as Date?) Text("Tous les jours").tag(nil as Date?)
ForEach(days, id: \.self) { day in 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: { } label: {
Text("Jour") Text("Jour")
@ -177,7 +181,11 @@ struct PlanningView: View {
} }
} header: { } header: {
HStack { 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() Spacer()
let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots)
if showFinishedMatches { if showFinishedMatches {
@ -186,6 +194,10 @@ struct PlanningView: View {
Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") 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) .headerProminence(.increased)
} }
@ -201,7 +213,11 @@ struct PlanningView: View {
LabeledContent { LabeledContent {
Text(self._formattedMatchCount(matches.count)) Text(self._formattedMatchCount(matches.count))
} label: { } 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 { if matches.count <= tournament.courtCount {
let names = matches.sorted(by: \.computedOrder) let names = matches.sorted(by: \.computedOrder)
.compactMap({ $0.roundTitle() }) .compactMap({ $0.roundTitle() })

@ -10,17 +10,43 @@ import SwiftUI
struct FollowUpMatchView: View { struct FollowUpMatchView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
let match: Match let match: Match?
let readyMatches: [Match] let readyMatches: [Match]
let matchesLeft: [Match] let matchesLeft: [Match]
let isFree: Bool 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 selectedCourt: Int?
@State private var checkCanPlay: Bool = false @State private var checkCanPlay: Bool = false
@State private var seeAll: Bool = false
@Binding var dismissWhenPresentFollowUpMatchIsDismissed: Bool @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 } var id: Int { self.rawValue }
case winner case winner
case loser case loser
@ -28,14 +54,23 @@ struct FollowUpMatchView: View {
case restingTime case restingTime
case court case court
func canHaveSeeAllOption() -> Bool {
switch self {
case .index, .restingTime:
return true
case .winner, .loser, .court:
return false
}
}
func localizedSortingModeLabel() -> String { func localizedSortingModeLabel() -> String {
switch self { switch self {
case .index: case .index:
return "Ordre" return "Ordre prévu"
case .court: case .court:
return "Terrain" return "Terrain"
case .restingTime: case .restingTime:
return "Repos" return "Temps de repos"
case .winner: case .winner:
return "Gagnant" return "Gagnant"
case .loser: case .loser:
@ -50,37 +85,50 @@ struct FollowUpMatchView: View {
_selectedCourt = .init(wrappedValue: match.courtIndex) _selectedCourt = .init(wrappedValue: match.courtIndex)
let currentTournament = match.currentTournament() let currentTournament = match.currentTournament()
let allMatches = currentTournament?.allMatches() ?? [] let allMatches = currentTournament?.allMatches() ?? []
self.matchesLeft = currentTournament?.matchesLeft(allMatches) ?? [] self.matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = currentTournament?.runningMatches(allMatches) ?? [] let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = currentTournament?.readyMatches(allMatches) ?? [] let readyMatches = Tournament.readyMatches(allMatches)
self.readyMatches = currentTournament?.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) ?? [] self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.isFree = currentTournament?.isFree() ?? true 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? { var winningTeam: TeamRegistration? {
match.winner() match?.winner()
} }
var losingTeam: TeamRegistration? { var losingTeam: TeamRegistration? {
match.loser() match?.loser()
} }
var sortingModeCases: [SortingMode] { var sortingModeCases: [SortingMode] {
var sortingModes = [SortingMode]() var sortingModes = [SortingMode]()
if let winningTeam { if winningTeam != nil {
sortingModes.append(.winner) sortingModes.append(.winner)
} }
if let losingTeam { if losingTeam != nil {
sortingModes.append(.loser) sortingModes.append(.loser)
} }
sortingModes.append(.index) sortingModes.append(.index)
sortingModes.append(.restingTime) sortingModes.append(.restingTime)
sortingModes.append(.court) // sortingModes.append(.court)
return sortingModes return sortingModes
} }
func contentUnavailableDescriptionLabel() -> String { func contentUnavailableDescriptionLabel() -> String {
switch sortingMode { switch sortingMode! {
case .winner: case .winner:
if let winningTeam { if let winningTeam {
return "Aucun match à suivre pour \(winningTeam.teamLabel())" return "Aucun match à suivre pour \(winningTeam.teamLabel())"
@ -103,13 +151,13 @@ struct FollowUpMatchView: View {
} }
var sortedMatches: [Match] { var sortedMatches: [Match] {
switch sortingMode { switch sortingMode! {
case .index: case .index:
return readyMatches return matches
case .restingTime: case .restingTime:
return readyMatches.sorted(by: \.restingTimeForSorting) return matches.sorted(by: \.restingTimeForSorting)
case .court: case .court:
return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) return matchesLeft.filter({ $0.courtIndex == selectedCourt })
case .winner: case .winner:
if let winningTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(winningTeam.id) }) { if let winningTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(winningTeam.id) }) {
return [followUpMatch] return [followUpMatch]
@ -127,77 +175,98 @@ struct FollowUpMatchView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
List { VStack(spacing: 0) {
Section { GenericDestinationPickerView(selectedDestination: $sortingMode, destinations: sortingModeCases, nilDestinationIsValid: false)
Picker(selection: $selectedCourt) {
Text("Aucun").tag(nil as Int?) List {
if let tournament = match.currentTournament() { //
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in // Toggle(isOn: $checkCanPlay) {
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex as Int?) // if isFree {
} // Text("Vérifier le paiement ou la présence")
} else { // } else {
ForEach(0..<20, id: \.self) { courtIndex in // Text("Vérifier la présence")
Text(Court.courtIndexedTitle(atIndex: courtIndex)).tag(courtIndex as Int?) // }
} // }
} // } footer: {
} label: { // if isFree {
Text("Sur le terrain") // Text("Masque les matchs où un ou plusieurs joueurs qui ne sont pas encore arrivé")
} // } else {
// // Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé")
// Toggle(isOn: $checkCanPlay) { // }
// if isFree {
// Text("Vérifier le paiement ou la présence")
// } else {
// Text("Vérifier la présence")
// }
// }
// } footer: {
// if isFree {
// Text("Masque les matchs où un ou plusieurs joueurs qui ne sont pas encore arrivé")
// } else {
// Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé")
// }
}
Section {
if sortedMatches.isEmpty == false { if sortedMatches.isEmpty == false {
ForEach(sortedMatches) { match in ForEach(sortedMatches) { match in
MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) Section {
MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt)
}
} }
} else { } else {
ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
} }
} header: { }
Picker(selection: $sortingMode) { .navigationTitle("À suivre sur")
ForEach(sortingModeCases) { sortingMode in .toolbarBackground(.visible, for: .navigationBar)
Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .bottomBar)
.toolbarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Retour", role: .cancel) {
if readyMatches.isEmpty && matchesLeft.isEmpty {
dismissWhenPresentFollowUpMatchIsDismissed = true
}
dismiss()
} }
} label: {
Text("Méthode de tri")
} }
.labelsHidden()
.pickerStyle(.segmented) ToolbarItem(placement: .status) {
} Button {
.headerProminence(.increased) seeAll.toggle()
.textCase(nil) } label: {
} Text(seeAll ? "Masquer les matchs incomplets" : "Voir tous les matchs")
.toolbarBackground(.visible, for: .navigationBar) .underline()
.navigationTitle("Match à suivre") }
.navigationBarTitleDisplayMode(.inline) .disabled(sortingMode?.canHaveSeeAllOption() == false)
.toolbar { }
ToolbarItem(placement: .topBarLeading) {
Button("Retour", role: .cancel) { // ToolbarItem(placement: .principal) {
if readyMatches.isEmpty && matchesLeft.isEmpty { // Picker(selection: $sortingMode) {
dismissWhenPresentFollowUpMatchIsDismissed = true // ForEach(sortingModeCases) { sortingMode in
// Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode)
// }
// } label: {
// Text("Méthode de tri")
// }
// .labelsHidden()
// .pickerStyle(.segmented)
// }
ToolbarItem(placement: .topBarTrailing) {
Picker(selection: $selectedCourt) {
Text("choix du terrain").tag(nil as Int?)
if let tournament = match?.currentTournament() {
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex as Int?)
}
} else {
ForEach(0..<12, id: \.self) { courtIndex in
Text(Court.courtIndexedTitle(atIndex: courtIndex)).tag(courtIndex as Int?)
}
}
} label: {
Text("Sur le terrain")
} }
dismiss() .labelsHidden()
.underline()
} }
} }
} }
} .onChange(of: readyMatches) {
.onChange(of: readyMatches) { if autoDismiss {
dismissWhenPresentFollowUpMatchIsDismissed = true dismissWhenPresentFollowUpMatchIsDismissed = true
dismiss() dismiss()
}
}
} }
} }
} }

@ -86,10 +86,10 @@ struct TeamRestingView: View {
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.onAppear { .onAppear {
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
let matchesLeft = tournament.matchesLeft(allMatches) let matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = tournament.readyMatches(allMatches) let readyMatches = Tournament.readyMatches(allMatches)
self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.matchesLeft = matchesLeft self.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting) self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)
} }

@ -35,8 +35,8 @@ struct TournamentRankView: View {
if editMode?.wrappedValue.isEditing == false { if editMode?.wrappedValue.isEditing == false {
Section { Section {
let all = tournament.allMatches() let all = tournament.allMatches()
let runningMatches = tournament.runningMatches(all) let runningMatches = Tournament.runningMatches(all)
let matchesLeft = tournament.readyMatches(all) let matchesLeft = Tournament.readyMatches(all)
MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false)
MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false)

@ -19,15 +19,26 @@ struct TournamentRunningView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
let runningMatches = tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = tournament.readyMatches(allMatches) let matchesLeft = Tournament.matchesLeft(allMatches)
let readyMatches = Tournament.readyMatches(allMatches)
MatchListView(section: "à venir", matches: readyMatches, hideWhenEmpty: true, isExpanded: false)
Section {
MatchListView(section: "en cours", matches: runningMatches, hideWhenEmpty: tournament.hasEnded()) MatchListView(section: "prêt à démarrer", matches: readyMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: true)
// MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false) }
let finishedMatches = tournament.finishedMatches(allMatches, limit: tournament.courtCount)
MatchListView(section: "Dernier\(finishedMatches.count.pluralSuffix) match\(finishedMatches.count.pluralSuffix) terminé\(finishedMatches.count.pluralSuffix)", matches: finishedMatches, isExpanded: tournament.hasEnded()) Section {
MatchListView(section: "à venir", matches: matchesLeft, hideWhenEmpty: true, isExpanded: false)
}
Section {
MatchListView(section: "en cours", matches: runningMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: false)
}
Section {
let finishedMatches = Tournament.finishedMatches(allMatches, limit: tournament.courtCount)
MatchListView(section: "Dernier\(finishedMatches.count.pluralSuffix) match\(finishedMatches.count.pluralSuffix) terminé\(finishedMatches.count.pluralSuffix)", matches: finishedMatches, isExpanded: tournament.hasEnded())
}
} }
} }

Loading…
Cancel
Save