Laurent 2 years ago
commit a88e04f25b
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 29
      PadelClub/Data/Coredata/Persistence.swift
  3. 11
      PadelClub/Data/GroupStage.swift
  4. 53
      PadelClub/Data/Match.swift
  5. 29
      PadelClub/Data/Round.swift
  6. 13
      PadelClub/Data/TeamRegistration.swift
  7. 11
      PadelClub/Data/Tournament.swift
  8. 6
      PadelClub/Extensions/Date+Extensions.swift
  9. 9
      PadelClub/Extensions/String+Extensions.swift
  10. 10
      PadelClub/Manager/PadelRule.swift
  11. 370
      PadelClub/ViewModel/MatchScheduler.swift
  12. 1
      PadelClub/ViewModel/NavigationViewModel.swift
  13. 2
      PadelClub/ViewModel/SearchViewModel.swift
  14. 12
      PadelClub/Views/Match/MatchSetupView.swift
  15. 9
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  16. 3
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  17. 141
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  18. 11
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  19. 170
      PadelClub/Views/Planning/PlanningSettingsView.swift
  20. 19
      PadelClub/Views/Planning/PlanningView.swift
  21. 18
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  22. 49
      PadelClub/Views/Planning/SchedulerView.swift
  23. 2
      PadelClub/Views/Score/SetInputView.swift
  24. 58
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift
  25. 11
      PadelClub/Views/Tournament/Shared/DateBoxView.swift
  26. 4
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  27. 14
      PadelClub/Views/Tournament/TournamentRunningView.swift

@ -213,6 +213,7 @@
FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964542BC266CF00EEF017 /* SchedulerView.swift */; }; FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964542BC266CF00EEF017 /* SchedulerView.swift */; };
FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */; }; FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */; };
FFF9645B2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */; }; FFF9645B2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */; };
FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -470,6 +471,7 @@
FFF964542BC266CF00EEF017 /* SchedulerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerView.swift; sourceTree = "<group>"; }; FFF964542BC266CF00EEF017 /* SchedulerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerView.swift; sourceTree = "<group>"; };
FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundScheduleEditorView.swift; sourceTree = "<group>"; }; FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundScheduleEditorView.swift; sourceTree = "<group>"; };
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageScheduleEditorView.swift; sourceTree = "<group>"; }; FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageScheduleEditorView.swift; sourceTree = "<group>"; };
FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundScheduleEditorView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -1037,6 +1039,7 @@
FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */, FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */,
FFF964542BC266CF00EEF017 /* SchedulerView.swift */, FFF964542BC266CF00EEF017 /* SchedulerView.swift */,
FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */, FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */,
FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */,
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
); );
@ -1325,6 +1328,7 @@
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */, FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */, FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */, FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */, C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */,
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */,

@ -99,6 +99,7 @@ class PersistenceController: NSObject {
func batchInsertPlayers(_ importedPlayers: [FederalPlayer], importingDate: Date) async { func batchInsertPlayers(_ importedPlayers: [FederalPlayer], importingDate: Date) async {
guard !importedPlayers.isEmpty else { return } guard !importedPlayers.isEmpty else { return }
let context = newTaskContext() let context = newTaskContext()
context.performAndWait { context.performAndWait {
context.transactionAuthor = PersistenceController.remoteDataImportAuthorName context.transactionAuthor = PersistenceController.remoteDataImportAuthorName
@ -121,6 +122,15 @@ class PersistenceController: NSObject {
// 1 // 1
var index = 0 var index = 0
let total = imported.count let total = imported.count
let replacements: [(Character, Character)] = [("Á", "ç"), ("", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")]
let replacementsCharacters = replacements
var fixApril2024 = false
//april 04-2024 bug with accent characters / adobe / fft
if URL.importDateFormatter.string(from: importingDate) == "04-2024" {
fixApril2024 = true
}
// 2 // 2
let batchInsert = NSBatchInsertRequest( let batchInsert = NSBatchInsertRequest(
@ -133,15 +143,34 @@ class PersistenceController: NSObject {
let data = imported[index] let data = imported[index]
importedPlayer.license = data.license importedPlayer.license = data.license
importedPlayer.ligueName = data.ligue importedPlayer.ligueName = data.ligue
if fixApril2024 {
importedPlayer.ligueName?.replace(characters: replacementsCharacters)
}
importedPlayer.rank = Int64(data.rank) importedPlayer.rank = Int64(data.rank)
importedPlayer.points = data.points ?? 0 importedPlayer.points = data.points ?? 0
importedPlayer.assimilation = data.assimilation importedPlayer.assimilation = data.assimilation
importedPlayer.country = data.country importedPlayer.country = data.country
if fixApril2024 {
importedPlayer.country?.replace(characters: replacementsCharacters)
}
importedPlayer.tournamentCount = Int64(data.tournamentCount ?? 0) importedPlayer.tournamentCount = Int64(data.tournamentCount ?? 0)
importedPlayer.lastName = data.lastName importedPlayer.lastName = data.lastName
if fixApril2024 {
importedPlayer.lastName?.replace(characters: replacementsCharacters)
}
importedPlayer.firstName = data.firstName importedPlayer.firstName = data.firstName
if fixApril2024 {
importedPlayer.firstName?.replace(characters: replacementsCharacters)
}
importedPlayer.fullName = data.firstName + " " + data.lastName importedPlayer.fullName = data.firstName + " " + data.lastName
if fixApril2024 {
importedPlayer.fullName?.replace(characters: replacementsCharacters)
}
importedPlayer.clubName = data.club importedPlayer.clubName = data.club
if fixApril2024 {
importedPlayer.clubName?.replace(characters: replacementsCharacters)
}
importedPlayer.clubCode = data.clubCode.replaceCharactersFromSet(characterSet: .whitespaces) importedPlayer.clubCode = data.clubCode.replaceCharactersFromSet(characterSet: .whitespaces)
importedPlayer.male = data.isMale importedPlayer.male = data.isMale
importedPlayer.importDate = importingDate importedPlayer.importDate = importingDate

@ -98,7 +98,16 @@ class GroupStage: ModelObject, Storable {
func scoreLabel(forGroupStagePosition groupStagePosition: Int) -> String? { func scoreLabel(forGroupStagePosition groupStagePosition: Int) -> String? {
if let scoreData = _score(forGroupStagePosition: groupStagePosition) { if let scoreData = _score(forGroupStagePosition: groupStagePosition) {
return "\(scoreData.wins)/\(scoreData.loses) " + scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false))) let hideGameDifference = matchFormat.setsToWin == 1
let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
var differenceAsString = "\n" + gameDifference + " jeux"
if hideGameDifference == false {
differenceAsString = "\n" + setDifference + " sets" + differenceAsString
} else {
differenceAsString = setDifference
}
return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString
} else { } else {
return nil return nil
} }

@ -66,11 +66,45 @@ class Match: ModelObject, Storable {
} }
} }
func isSeedLocked(atTeamPosition teamPosition: TeamPosition) -> Bool {
previousMatch(teamPosition)?.disabled == true
}
func unlockSeedPosition(atTeamPosition teamPosition: TeamPosition) {
previousMatch(teamPosition)?.enableMatch()
}
@discardableResult
func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int {
let matchIndex = index
var teamPosition : TeamPosition {
if let slot {
return slot
} else {
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
return teamPosition
}
}
previousMatch(teamPosition)?.disableMatch()
return matchIndex * 2 + teamPosition.rawValue
}
func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> Bool { func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> Bool {
guard let bracketPosition = team.bracketPosition else { return false } guard let bracketPosition = team.bracketPosition else { return false }
return index * 2 + teamPosition.rawValue == bracketPosition return index * 2 + teamPosition.rawValue == bracketPosition
} }
func estimatedEndDate() -> Date? {
let minutesToAdd = Double(matchFormat.estimatedDuration)
return startDate?.addingTimeInterval(minutesToAdd * 60.0)
}
func resetMatch() { func resetMatch() {
losingTeamId = nil losingTeamId = nil
winningTeamId = nil winningTeamId = nil
@ -151,6 +185,13 @@ class Match: ModelObject, Storable {
func next() -> Match? { func next() -> Match? {
Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first
} }
func roundTitle() -> String? {
if groupStage != nil { return "Poule" }
else if let roundObject { return roundObject.roundTitle() }
else { return nil }
}
func topPreviousRoundMatchIndex() -> Int { func topPreviousRoundMatchIndex() -> Int {
index * 2 + 1 index * 2 + 1
} }
@ -274,6 +315,12 @@ class Match: ModelObject, Storable {
} }
} }
func courtIndex() -> Int? {
guard let court else { return nil }
if let courtIndex = Int(court) { return courtIndex - 1 }
return nil
}
func courtCount() -> Int { func courtCount() -> Int {
currentTournament()?.courtCount ?? 1 currentTournament()?.courtCount ?? 1
} }
@ -376,12 +423,12 @@ class Match: ModelObject, Storable {
let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut())
var setDifference : Int = 0 var setDifference : Int = 0
if endedSetsOne.count == 1 { let zip = zip(endedSetsOne, endedSetsTwo)
if matchFormat.setsToWin == 1 {
setDifference = endedSetsOne[0] - endedSetsTwo[0] setDifference = endedSetsOne[0] - endedSetsTwo[0]
} else { } else {
setDifference = endedSetsOne.filter { $0 == matchFormat.setFormat.scoreToWin }.count - endedSetsTwo.filter { $0 == matchFormat.setFormat.scoreToWin }.count setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count
} }
let zip = zip(endedSetsOne, endedSetsTwo)
let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+)
return (setDifference * reverseValue, gameDifference * reverseValue) return (setDifference * reverseValue, gameDifference * reverseValue)
} }

@ -200,6 +200,35 @@ class Round: ModelObject, Storable {
_matches().allSatisfy({ $0.disabled }) _matches().allSatisfy({ $0.disabled })
} }
func resetRound(updateMatchFormat: MatchFormat? = nil) {
let _updateMatchFormat = updateMatchFormat ?? self.matchFormat
_matches().forEach({
$0.startDate = nil
$0.matchFormat = updateMatchFormat ?? $0.matchFormat
})
self.matchFormat = _updateMatchFormat
loserRoundsAndChildren().forEach { round in
round.resetRound(updateMatchFormat: _updateMatchFormat)
}
nextRound()?.resetRound(updateMatchFormat: _updateMatchFormat)
}
func resetRound(from match: Match, updateMatchFormat: MatchFormat? = nil) {
let _updateMatchFormat = updateMatchFormat ?? self.matchFormat
self.matchFormat = _updateMatchFormat
let matches = _matches()
if let index = matches.firstIndex(where: { $0.id == match.id }) {
matches[index...].forEach { match in
match.startDate = nil
match.matchFormat = _updateMatchFormat
}
}
loserRoundsAndChildren().forEach { round in
round.resetRound(updateMatchFormat: _updateMatchFormat)
}
nextRound()?.resetRound(updateMatchFormat: _updateMatchFormat)
}
func getActiveLoserRound() -> Round? { func getActiveLoserRound() -> Round? {
let rounds = loserRounds() let rounds = loserRounds()
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false }) return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false })

@ -54,18 +54,11 @@ class TeamRegistration: ModelObject, Storable {
} }
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
let matchIndex = match.index let seedPosition = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding)
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) bracketPosition = seedPosition
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
match.previousMatch(teamPosition)?.disableMatch()
bracketPosition = matchIndex * 2 + teamPosition.rawValue
} }
var initialWeight: Int { var initialWeight: Int {
lockWeight ?? weight lockWeight ?? weight
} }

@ -101,6 +101,12 @@ class Tournament : ModelObject, Storable {
case build case build
} }
func getCourtIndex(_ court: String?) -> Int? {
guard let court else { return nil }
if let courtIndex = Int(court) { return courtIndex - 1 }
return nil
}
func courtUsed() -> [String] { func courtUsed() -> [String] {
let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id }) let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id })
return Set(runningMatches.compactMap { $0.court }).sorted() return Set(runningMatches.compactMap { $0.court }).sorted()
@ -142,7 +148,8 @@ class Tournament : ModelObject, Storable {
} }
func state() -> Tournament.State { func state() -> Tournament.State {
if groupStageCount > 0 && groupStages().isEmpty == false { if (groupStageCount > 0 && groupStages().isEmpty == false)
|| rounds().isEmpty == false {
return .build return .build
} }
return .initial return .initial
@ -363,7 +370,7 @@ class Tournament : ModelObject, Storable {
} }
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print(id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) //print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
return _sortedTeams return _sortedTeams
} }

@ -181,3 +181,9 @@ extension Date {
} }
} }
extension Date {
func isEarlierThan(_ date: Date) -> Bool {
self < date
}
}

@ -141,3 +141,12 @@ extension String {
extension LosslessStringConvertible { extension LosslessStringConvertible {
var string: String { .init(self) } var string: String { .init(self) }
} }
extension String {
//april 04-2024 bug with accent characters / adobe / fft
mutating func replace(characters: [(Character, Character)]) {
for (targetChar, replacementChar) in characters {
self = String(self.map { $0 == targetChar ? replacementChar : $0 })
}
}
}

@ -939,7 +939,7 @@ enum SetFormat: Int, Hashable, Codable {
var firstGameFormat: Format { var firstGameFormat: Format {
switch self { switch self {
case .megaTieBreak: case .megaTieBreak:
return .tiebreakFiveTeen return .tiebreakFifteen
case .superTieBreak: case .superTieBreak:
return .tiebreakTen return .tiebreakTen
default: default:
@ -1243,7 +1243,7 @@ enum Format: Int, Hashable, Codable {
case normal case normal
case tiebreakSeven case tiebreakSeven
case tiebreakTen case tiebreakTen
case tiebreakFiveTeen case tiebreakFifteen
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self { switch self {
@ -1253,7 +1253,7 @@ enum Format: Int, Hashable, Codable {
return "tie-break en 7" return "tie-break en 7"
case .tiebreakTen: case .tiebreakTen:
return "tie-break en 10" return "tie-break en 10"
case .tiebreakFiveTeen: case .tiebreakFifteen:
return "tie-break en 15" return "tie-break en 15"
} }
} }
@ -1261,7 +1261,7 @@ enum Format: Int, Hashable, Codable {
switch self { switch self {
case .normal: case .normal:
return false return false
case .tiebreakSeven, .tiebreakTen, .tiebreakFiveTeen: case .tiebreakSeven, .tiebreakTen, .tiebreakFifteen:
return true return true
} }
} }
@ -1274,7 +1274,7 @@ enum Format: Int, Hashable, Codable {
return 7 return 7
case .tiebreakTen: case .tiebreakTen:
return 10 return 10
case .tiebreakFiveTeen: case .tiebreakFifteen:
return 15 return 15
} }
} }

@ -6,19 +6,40 @@
// //
import Foundation import Foundation
import LeStorage
struct TimeMatch { struct GroupStageTimeMatch {
let matchID: String let matchID: String
let rotationIndex: Int let rotationIndex: Int
var courtIndex: Int var courtIndex: Int
let groupIndex: Int let groupIndex: Int
} }
struct TimeMatch {
let matchID: String
let rotationIndex: Int
var courtIndex: Int
var startDate: Date
var durationLeft: Int //in minutes
var minimumBreakTime: Int //in minutes
func estimatedEndDate(includeBreakTime: Bool) -> Date {
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
return startDate.addingTimeInterval(minutesToAdd * 60.0)
}
}
struct GroupStageMatchDispatcher {
let timedMatches: [GroupStageTimeMatch]
let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int
let groupLastRotation: [Int: Int]
}
struct MatchDispatcher { struct MatchDispatcher {
let timedMatches: [TimeMatch] let timedMatches: [TimeMatch]
let freeCourtPerRotation: [Int: [Int]] let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int let rotationCount: Int
let groupLastRotation: [Int: Int]
} }
extension Match { extension Match {
@ -31,12 +52,44 @@ extension Match {
} }
} }
enum MatchSchedulerOption: Hashable {
case accountUpperBracketBreakTime
case accountLoserBracketBreakTime
case randomizeCourts
case rotationDifferenceIsImportant
case shouldHandleUpperRoundSlice
}
class MatchScheduler { class MatchScheduler {
static let shared = MatchScheduler() static let shared = MatchScheduler()
var options: Set<MatchSchedulerOption> = Set(arrayLiteral: .accountUpperBracketBreakTime)
var timeDifferenceLimit: Double = 300.0
var loserBracketRotationDifference: Int = 0
var upperBracketRotationDifference: Int = 1
func shouldHandleUpperRoundSlice() -> Bool {
options.contains(.shouldHandleUpperRoundSlice)
}
func accountLoserBracketBreakTime() -> Bool {
options.contains(.accountLoserBracketBreakTime)
}
func accountUpperBracketBreakTime() -> Bool {
options.contains(.accountUpperBracketBreakTime)
}
func randomizeCourts() -> Bool {
options.contains(.randomizeCourts)
}
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> MatchDispatcher { func rotationDifferenceIsImportant() -> Bool {
options.contains(.rotationDifferenceIsImportant)
}
let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate } func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?) -> GroupStageMatchDispatcher {
let _groupStages = groupStages
// Get the maximum count of matches in any group // Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
@ -50,7 +103,7 @@ class MatchScheduler {
} }
} }
var slots = [TimeMatch]() var slots = [GroupStageTimeMatch]()
var availableMatchs = flattenedMatches var availableMatchs = flattenedMatches
var rotationIndex = 0 var rotationIndex = 0
var teamsPerRotation = [Int: [String]]() var teamsPerRotation = [Int: [String]]()
@ -81,7 +134,8 @@ class MatchScheduler {
if let first = rotationMatches.first(where: { match in if let first = rotationMatches.first(where: { match in
teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
}) { }) {
slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index )) let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index)
slots.append(timeMatch)
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds())
rotationMatches.removeAll(where: { $0.id == first.id }) rotationMatches.removeAll(where: { $0.id == first.id })
availableMatchs.removeAll(where: { $0.id == first.id }) availableMatchs.removeAll(where: { $0.id == first.id })
@ -96,10 +150,10 @@ class MatchScheduler {
rotationIndex += 1 rotationIndex += 1
} }
var organizedSlots = [TimeMatch]() var organizedSlots = [GroupStageTimeMatch]()
for i in 0..<rotationIndex { for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted let courts = randomizeCourts() ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
for j in 0..<matches.count { for j in 0..<matches.count {
@ -109,11 +163,19 @@ class MatchScheduler {
} }
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) return GroupStageMatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation)
} }
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int) -> Bool { func rotationDifference(loserBracket: Bool) -> Int {
print(roundObject.roundTitle(), match.matchTitle()) if loserBracket {
return loserBracketRotationDifference
} else {
return upperBracketRotationDifference
}
}
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
//print(roundObject.roundTitle(), match.matchTitle())
let previousMatches = roundObject.precedentMatches(ofMatch: match) let previousMatches = roundObject.precedentMatches(ofMatch: match)
if previousMatches.isEmpty { return true } if previousMatches.isEmpty { return true }
@ -135,73 +197,154 @@ class MatchScheduler {
return false return false
} }
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + (roundObject.loser == nil ? 1 : 0) < rotationIndex }) var includeBreakTime = false
return previousMatchIsInPreviousRotation
if accountLoserBracketBreakTime() && roundObject.isLoserBracket() {
includeBreakTime = true
}
if accountUpperBracketBreakTime() && roundObject.isLoserBracket() == false {
includeBreakTime = true
}
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex })
guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: includeBreakTime) }).max() else {
return previousMatchIsInPreviousRotation
}
if targetedStartDate >= minimumPossibleEndDate {
if rotationDifferenceIsImportant() {
return previousMatchIsInPreviousRotation
} else {
return true
}
} else {
if targetedStartDate == minimumTargetedEndDate {
minimumTargetedEndDate = minimumPossibleEndDate
} else {
minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate)
}
return false
}
} }
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0) -> MatchDispatcher { func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
}
func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] {
let byCourt = Dictionary(grouping: slots, by: { $0.courtIndex })
return (byCourt.keys.flatMap { courtIndex in
let matchesByCourt = byCourt[courtIndex]?.sorted(by: \.startDate)
let lastMatch = matchesByCourt?.last
var results = [(Int, Date)]()
if let courtFreeDate = lastMatch?.estimatedEndDate(includeBreakTime: false) {
results.append((courtIndex, courtFreeDate))
}
return results
}
)
}
func getAvailableCourts(from matches: [Match]) -> [(String, Date)] {
let validMatches = matches.filter({ $0.court != nil && $0.startDate != nil })
let byCourt = Dictionary(grouping: validMatches, by: { $0.court! })
return (byCourt.keys.flatMap { court in
let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!)
let lastMatch = matchesByCourt?.last
var results = [(String, Date)]()
if let courtFreeDate = lastMatch?.estimatedEndDate() {
results.append((court, courtFreeDate))
}
return results
}
)
}
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
var slots = [TimeMatch]() var slots = [TimeMatch]()
var availableMatchs = flattenedMatches var _startDate: Date?
var rotationIndex = 0 var rotationIndex = 0
var availableMatchs = flattenedMatches.filter({ $0.startDate == nil })
flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in
if _startDate == nil {
_startDate = match.startDate
} else if match.startDate! > _startDate! {
_startDate = match.startDate
rotationIndex += 1
}
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex() ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.estimatedDuration, minimumBreakTime: match.matchFormat.breakTime.breakTime)
slots.append(timeMatch)
}
if slots.isEmpty == false {
rotationIndex += 1
}
var freeCourtPerRotation = [Int: [Int]]() var freeCourtPerRotation = [Int: [Int]]()
var groupLastRotation = [Int: Int]()
while slots.count < flattenedMatches.count { let availableCourt = numberOfCourtsAvailablePerRotation
freeCourtPerRotation[rotationIndex] = []
var matchPerRound = [Int: Int]()
var availableCourt = numberOfCourtsAvailablePerRotation
if rotationIndex == 0 {
availableCourt = availableCourt - initialOccupiedCourt
}
(0..<availableCourt).forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
if let first = availableMatchs.first(where: { match in var courts = initialCourts ?? (0..<availableCourt).map { $0 }
let roundObject = match.roundObject!
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex)
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, numberOfCourtsAvailablePerRotation > 1, let nextMatch = match.next() {
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex) {
return true
} else {
return false
}
}
if (matchPerRound[roundObject.index] ?? 0)%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == numberOfCourtsAvailablePerRotation - 1 { var shouldStartAtDispatcherDate = rotationIndex > 0
return false
}
return canBePlayed while availableMatchs.count > 0 {
}) { freeCourtPerRotation[rotationIndex] = []
//print(first.roundObject!.roundTitle(), first.matchTitle()) let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
if first.roundObject!.loser == nil {
if let roundIndex = matchPerRound[first.roundObject!.index] { if shouldStartAtDispatcherDate {
matchPerRound[first.roundObject!.index] = roundIndex + 1 rotationStartDate = dispatcherStartDate
} else { shouldStartAtDispatcherDate = false
matchPerRound[first.roundObject!.index] = 1 } else {
} courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 }
}
courts.sort()
print("courts available at rotation \(rotationIndex)", courts)
print("rotationStartDate", rotationStartDate)
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], freeCourtPreviousRotation.count > 0 {
print("scenario where we are waiting for a breaktime to be over without any match to play in between or a free court was available and we need to recheck breaktime left on it")
let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) })
let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true)
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
let noBreakAlreadyTested = previousRotationSlots.anySatisfy({ $0.startDate == previousEndDateNoBreak })
if let previousEndDate, let previousEndDateNoBreak {
let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate)
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak)
print("difference w break", differenceWithBreak)
print("difference w/o break", differenceWithoutBreak)
var difference = differenceWithBreak
if differenceWithBreak <= 0 {
difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit {
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
} }
slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index ))
availableMatchs.removeAll(where: { $0.id == first.id }) if difference > timeDifferenceLimit {
if let index = first.roundObject?.index { courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index)
groupLastRotation[index] = rotationIndex })
freeCourtPerRotation[rotationIndex] = courts
courts = freeCourtPreviousRotation
rotationStartDate = rotationStartDate.addingTimeInterval(-difference)
} }
} else {
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
} }
} }
dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation)
rotationIndex += 1 rotationIndex += 1
} }
var organizedSlots = [TimeMatch]() var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex { for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted let courts = randomizeCourts() ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex))
for j in 0..<matches.count { for j in 0..<matches.count {
matches[j].courtIndex = courts[j] matches[j].courtIndex = courts[j]
@ -210,42 +353,127 @@ class MatchScheduler {
} }
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex)
}
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) {
var matchPerRound = [Int: Int]()
var minimumTargetedEndDate: Date = rotationStartDate
courts.forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
if let first = availableMatchs.first(where: { match in
let roundObject = match.roundObject!
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0
if shouldHandleUpperRoundSlice() {
let roundMatchesCount = roundObject.playedMatches().count
if roundObject.loser == nil && roundMatchesCount > courts.count {
if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { return false }
}
}
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, courts.count > 1, let nextMatch = match.next() {
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) {
return true
} else {
return false
}
}
if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 {
return false
}
return canBePlayed
}) {
print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate)
if first.roundObject!.loser == nil {
if let roundIndex = matchPerRound[first.roundObject!.index] {
matchPerRound[first.roundObject!.index] = roundIndex + 1
} else {
matchPerRound[first.roundObject!.index] = 1
}
}
let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime)
slots.append(timeMatch)
availableMatchs.removeAll(where: { $0.id == first.id })
} else {
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
}
}
if freeCourtPerRotation[rotationIndex]!.count == availableCourts {
freeCourtPerRotation[rotationIndex] = []
let courtsUsed = getNextEarliestAvailableDate(from: slots)
let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
availableDate <= minimumTargetedEndDate
}.sorted(by: \.1).map { $0.0 }
dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation)
}
} }
func updateSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, randomizeCourts: Bool, startDate: Date) { func updateSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) {
let upperRounds = tournament.rounds() let upperRounds = tournament.rounds()
let allMatches = tournament.allMatches()
var roundIndex = 0 var roundIndex = 0
if let roundId {
roundIndex = upperRounds.firstIndex(where: { $0.id == roundId }) ?? 0
}
let rounds = upperRounds.flatMap { let rounds = upperRounds.map {
[$0] + $0.loserRoundsAndChildren() $0
} + upperRounds.flatMap {
$0.loserRoundsAndChildren()
} }
var flattenedMatches = rounds[roundIndex...].flatMap { round in var flattenedMatches = rounds.flatMap { round in
round._matches().filter({ $0.disabled == false }).sorted(by: \.index) round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
} }
if let matchId, let matchIndex = flattenedMatches.firstIndex(where: { $0.id == matchId }) { flattenedMatches.forEach({
flattenedMatches = Array(flattenedMatches[matchIndex...]) if (roundId == nil && matchId == nil) || $0.startDate?.isEarlierThan(startDate) == false {
$0.startDate = nil
}
})
if let roundId {
if let round : Round = Store.main.findById(roundId) {
let matches = round._matches()
round.resetRound()
flattenedMatches = matches + flattenedMatches
}
} else if let matchId {
if let match : Match = Store.main.findById(matchId) {
if let round = match.roundObject {
round.resetRound(from: match)
}
flattenedMatches = [match] + flattenedMatches
}
} }
flattenedMatches.forEach({ $0.startDate = nil }) let usedCourts = getAvailableCourts(from: allMatches.filter({ $0.startDate?.isEarlierThan(startDate) == true }))
let initialCourts = usedCourts.filter { (court, availableDate) in
availableDate <= startDate
}.sorted(by: \.1).compactMap { tournament.getCourtIndex($0.0) }
let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, initialOccupiedCourt: 0) let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts)
roundDispatch.timedMatches.forEach { matchSchedule in roundDispatch.timedMatches.forEach { matchSchedule in
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 match.startDate = matchSchedule.startDate
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.setCourt(matchSchedule.courtIndex + 1) match.setCourt(matchSchedule.courtIndex + 1)
} }
} }
try? DataStore.shared.matches.addOrUpdate(contentOfs: flattenedMatches) try? DataStore.shared.matches.addOrUpdate(contentOfs: allMatches)
} }
} }

@ -9,6 +9,7 @@ import SwiftUI
@Observable @Observable
class NavigationViewModel { class NavigationViewModel {
var path = NavigationPath()
var agendaDestination: AgendaDestination? = .activity var agendaDestination: AgendaDestination? = .activity
var tournament: Tournament? var tournament: Tournament?
} }

@ -118,7 +118,7 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
func wordsPredicates() -> NSPredicate? { func wordsPredicates() -> NSPredicate? {
let words = words().filter({ $0.isEmpty }) let words = words().filter({ $0.isEmpty == false })
switch words.count { switch words.count {
case 2: case 2:
let predicates = [ let predicates = [

@ -91,6 +91,18 @@ struct MatchSetupView: View {
Text("Tirage").tag(nil as SeedInterval?) Text("Tirage").tag(nil as SeedInterval?)
} }
.disabled(availableSeedGroups.isEmpty && walkOutSpot == false) .disabled(availableSeedGroups.isEmpty && walkOutSpot == false)
if match.isSeedLocked(atTeamPosition: teamPosition) {
Button("Libérer") {
match.unlockSeedPosition(atTeamPosition: teamPosition)
try? dataStore.matches.addOrUpdate(instance: match)
}
} else {
Button("Réserver") {
_ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition)
try? dataStore.matches.addOrUpdate(instance: match)
}
}
} }
} }
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)

@ -52,8 +52,8 @@ struct ActivityView: View {
} }
var body: some View { var body: some View {
NavigationStack { @Bindable var navigation = navigation
@Bindable var navigation = navigation NavigationStack(path: $navigation.path) {
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false)
List { List {
@ -187,16 +187,13 @@ struct ActivityView: View {
TournamentView() TournamentView()
.environment(tournament) .environment(tournament)
} }
.navigationDestination(item: $navigation.tournament) { tournament in
TournamentView()
.environment(tournament)
}
} }
} }
} }
private func _gatherFederalTournaments() { private func _gatherFederalTournaments() {
isGatheringFederalTournaments = true isGatheringFederalTournaments = true
NetworkFederalService.shared.formId = ""
Task { Task {
do { do {
try await dataStore.clubs.filter { $0.code != nil }.concurrentForEach { club in try await dataStore.clubs.filter { $0.code != nil }.concurrentForEach { club in

@ -29,7 +29,8 @@ struct EventListView: View {
HStack { HStack {
Text(section.monthYearFormatted) Text(section.monthYearFormatted)
Spacer() Spacer()
Text(_tournaments.map { $0.tournaments.count }.reduce(0,+).formatted()) let count = _tournaments.map { $0.tournaments.count }.reduce(0,+)
Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} }
.headerProminence(.increased) .headerProminence(.increased)

@ -0,0 +1,141 @@
//
// LoserRoundScheduleEditorView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 14/04/2024.
//
import SwiftUI
struct LoserRoundStepScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var round: Round
var upperRound: Round
var matches: [Match]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
init(round: Round, upperRound: Round) {
self.upperRound = upperRound
self.round = round
let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.playedMatches() })
self.matches = _matches
self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date())
self._matchFormat = State(wrappedValue: round.matchFormat)
}
var body: some View {
@Bindable var round = round
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Valider la modification") {
_updateSchedule()
}
} header: {
Text(round.selectionLabel())
} footer: {
NavigationLink {
List {
ForEach(matches) { match in
if match.disabled == false {
MatchScheduleEditorView(match: match)
}
}
}
.headerProminence(.increased)
.navigationTitle(round.selectionLabel())
.environment(tournament)
} label: {
Text("voir tous les matchs")
}
}
.headerProminence(.increased)
}
private func _updateSchedule() {
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.resetRound(updateMatchFormat: round.matchFormat)
})
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
_save()
}
private func _save() {
try? dataStore.rounds.addOrUpdate(contentOfs: upperRound.loserRounds(forRoundIndex: round.index))
}
}
struct LoserRoundScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var upperRound: Round
var loserRounds: [Round]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
init(upperRound: Round) {
self.upperRound = upperRound
let _loserRounds = upperRound.loserRounds()
self.loserRounds = _loserRounds
self._startDate = State(wrappedValue: _loserRounds.first?.startDate ?? _loserRounds.first?.playedMatches().first?.startDate ?? Date())
self._matchFormat = State(wrappedValue: _loserRounds.first?.matchFormat ?? upperRound.matchFormat)
}
var body: some View {
List {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Valider la modification") {
_updateSchedule()
}
} header: {
Text("Classement " + upperRound.roundTitle())
}
ForEach(upperRound.loserRounds()) { loserRound in
if loserRound.isDisabled() == false {
LoserRoundStepScheduleEditorView(round: loserRound, upperRound: upperRound)
}
}
}
.headerProminence(.increased)
.navigationTitle("Réglages")
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)
}
private func _updateSchedule() {
let matches = upperRound.loserRounds().flatMap({ round in
round.playedMatches()
})
upperRound.loserRounds().forEach({ round in
round.resetRound(updateMatchFormat: matchFormat)
})
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate)
_save()
}
private func _save() {
try? dataStore.rounds.addOrUpdate(contentOfs: upperRound.loserRounds())
}
}

@ -22,16 +22,21 @@ struct MatchScheduleEditorView: View {
DatePicker(selection: $startDate) { DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday())) Text(startDate.formatted(.dateTime.weekday()))
} }
RowButtonView("Modifier") { RowButtonView("Valider la modification") {
_updateSchedule() _updateSchedule()
} }
} header: { } header: {
Text(match.matchTitle()) if let round = match.roundObject {
Text(round.roundTitle() + " " + match.matchTitle())
} else {
Text(match.matchTitle())
}
} }
.headerProminence(.increased)
} }
private func _updateSchedule() { private func _updateSchedule() {
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, randomizeCourts: true, startDate: startDate) MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate)
} }
} }

@ -11,12 +11,27 @@ struct PlanningSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var tournament: Tournament var tournament: Tournament
@State private var scheduleSetup: Bool = false @State private var scheduleSetup: Bool = false
@State private var randomCourtDistribution: Bool = false @State private var randomCourtDistribution: Bool
@State private var groupStageCourtCount: Int @State private var groupStageCourtCount: Int
@State private var upperBracketBreakTime: Bool
@State private var loserBracketBreakTime: Bool
@State private var rotationDifferenceIsImportant: Bool
@State private var loserBracketRotationDifference: Int
@State private var upperBracketRotationDifference: Int
@State private var timeDifferenceLimit: Double
@State private var shouldHandleUpperRoundSlice: Bool
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1) self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1)
self._loserBracketRotationDifference = State(wrappedValue: MatchScheduler.shared.loserBracketRotationDifference)
self._upperBracketRotationDifference = State(wrappedValue: MatchScheduler.shared.upperBracketRotationDifference)
self._timeDifferenceLimit = State(wrappedValue: MatchScheduler.shared.timeDifferenceLimit)
self._rotationDifferenceIsImportant = State(wrappedValue: MatchScheduler.shared.rotationDifferenceIsImportant())
self._randomCourtDistribution = State(wrappedValue: MatchScheduler.shared.randomizeCourts())
self._upperBracketBreakTime = State(wrappedValue: MatchScheduler.shared.accountUpperBracketBreakTime())
self._loserBracketBreakTime = State(wrappedValue: MatchScheduler.shared.accountLoserBracketBreakTime())
self._shouldHandleUpperRoundSlice = State(wrappedValue: MatchScheduler.shared.shouldHandleUpperRoundSlice())
} }
var body: some View { var body: some View {
@ -39,7 +54,10 @@ struct PlanningSettingsView: View {
Section { Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount)
if tournament.groupStages().isEmpty == false {
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount)
}
NavigationLink { NavigationLink {
@ -54,66 +72,40 @@ struct PlanningSettingsView: View {
Text("Distribuer les terrains au hasard") Text("Distribuer les terrains au hasard")
} }
RowButtonView("Horaire intelligent", role: .destructive) { Toggle(isOn: $shouldHandleUpperRoundSlice) {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 Text("Équilibrer les matchs d'une manche sur plusieurs tours")
let groupStages = tournament.groupStages() }
let numberOfCourtsAvailablePerRotation: Int = min(tournament.courtCount, groupStageCourtCount * groupStages.count)
let matchScheduler = MatchScheduler.shared
let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil })
var times = Set(groupStages.compactMap { $0.startDate })
if times.isEmpty {
groupStages.forEach({ $0.startDate = tournament.startDate })
times.insert(tournament.startDate)
try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
}
var lastDate : Date? = nil
times.forEach { time in
let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groupStages, startingDate: time, randomizeCourts: randomCourtDistribution)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd)
lastDate = match.startDate?.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex + 1)
}
}
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
let upperRounds = tournament.rounds()
let rounds = upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren()
}
let flattenedMatches = rounds.flatMap { round in Toggle(isOn: $upperBracketBreakTime) {
round._matches().filter({ $0.disabled == false }).sorted(by: \.index) Text("Tableau : tenir compte des pauses")
} }
flattenedMatches.forEach({ $0.startDate = nil }) Toggle(isOn: $loserBracketBreakTime) {
Text("Classement : tenir compte des pauses")
}
let roundDispatch = matchScheduler.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomCourtDistribution) Toggle(isOn: $rotationDifferenceIsImportant) {
Text("Forcer un créneau supplémentaire entre 2 phases")
}
roundDispatch.timedMatches.forEach { matchSchedule in LabeledContent {
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { StepperView(count: $upperBracketRotationDifference, minimum: 0, maximum: 2)
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 } label: {
if let lastDate { Text("Tableau")
match.startDate = lastDate.addingTimeInterval(timeIntervalToAdd) }
} .disabled(rotationDifferenceIsImportant == false)
match.setCourt(matchSchedule.courtIndex + 1)
}
}
try? dataStore.matches.addOrUpdate(contentOfs: flattenedMatches) LabeledContent {
StepperView(count: $loserBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
Text("Classement")
}
.disabled(rotationDifferenceIsImportant == false)
//timeDifferenceLimit
scheduleSetup = true RowButtonView("Horaire intelligent", role: .destructive) {
_setupSchedule()
} }
if scheduleSetup { if scheduleSetup {
@ -149,6 +141,74 @@ struct PlanningSettingsView: View {
} }
} }
private func _setupSchedule() {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matchScheduler = MatchScheduler.shared
matchScheduler.options.removeAll()
if randomCourtDistribution {
matchScheduler.options.insert(.randomizeCourts)
}
if shouldHandleUpperRoundSlice {
matchScheduler.options.insert(.shouldHandleUpperRoundSlice)
}
if upperBracketBreakTime {
matchScheduler.options.insert(.accountUpperBracketBreakTime)
}
if loserBracketBreakTime {
matchScheduler.options.insert(.accountLoserBracketBreakTime)
}
if rotationDifferenceIsImportant {
matchScheduler.options.insert(.rotationDifferenceIsImportant)
}
matchScheduler.loserBracketRotationDifference = loserBracketRotationDifference
matchScheduler.upperBracketRotationDifference = upperBracketRotationDifference
matchScheduler.timeDifferenceLimit = timeDifferenceLimit
let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil })
// var times = Set(groupStages.compactMap { $0.startDate }.filter { $0 >= tournament.startDate } )
// if times.isEmpty {
// groupStages.forEach({ $0.startDate = tournament.startDate })
// times.insert(tournament.startDate)
// try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
// }
var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate })
try? dataStore.groupStages.addOrUpdate(contentOfs: groups)
let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex + 1)
}
}
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
scheduleSetup = true
}
private func _save() { private func _save() {
try? dataStore.tournaments.addOrUpdate(instance: tournament) try? dataStore.tournaments.addOrUpdate(instance: tournament)
} }

@ -74,7 +74,7 @@ struct PlanningView: View {
ForEach(_matches) { match in ForEach(_matches) { match in
LabeledContent { LabeledContent {
Text(match.matchFormat.format)
} label: { } label: {
if let groupStage = match.groupStageObject { if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle()) Text(groupStage.groupStageTitle())
@ -92,12 +92,18 @@ struct PlanningView: View {
NavigationLink { NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: { } label: {
if let groupStage = match.groupStageObject { LabeledContent {
Text(groupStage.groupStageTitle()) if let court = match.court {
} else if let round = match.roundObject { Text(court)
Text(round.roundTitle()) }
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
} }
Text(match.matchTitle())
} }
} }
} label: { } label: {
@ -140,6 +146,7 @@ struct PlanningView: View {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)
} label: { } label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle) Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle)
Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", "))
} }
} }
} }

@ -24,13 +24,10 @@ struct RoundScheduleEditorView: View {
List { List {
Section { Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
}
Section {
DatePicker(selection: $startDate) { DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday())) Text(startDate.formatted(.dateTime.weekday()))
} }
RowButtonView("Modifier") { RowButtonView("Valider la modification") {
_updateSchedule() _updateSchedule()
} }
} }
@ -39,13 +36,18 @@ struct RoundScheduleEditorView: View {
MatchScheduleEditorView(match: match) MatchScheduleEditorView(match: match)
} }
} }
.onChange(of: round.matchFormat) {
_save()
}
} }
private func _updateSchedule() { private func _updateSchedule() {
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, randomizeCourts: true, startDate: startDate) let matches = round._matches()
matches.forEach { match in
match.matchFormat = round.matchFormat
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
_save()
} }
private func _save() { private func _save() {

@ -7,24 +7,34 @@
import SwiftUI import SwiftUI
extension GroupStage: Schedulable {} extension GroupStage: Schedulable {
extension Round: Schedulable {} func titleLabel() -> String {
self.groupStageTitle()
}
}
extension Round: Schedulable {
func titleLabel() -> String {
self.roundTitle()
}
}
struct SchedulerView: View { struct SchedulerView: View {
var tournament: Tournament var tournament: Tournament
var destination: ScheduleDestination
var body: some View { var body: some View {
List { List {
ForEach(tournament.groupStages()) { switch destination {
_schedulerView($0) case .scheduleGroupStage:
} ForEach(tournament.groupStages()) {
ForEach(tournament.rounds()) { round in _schedulerView($0)
_schedulerView(round)
ForEach(round.loserRoundsAndChildren()) { loserRound in
if round.isDisabled() == false {
_schedulerView(loserRound)
}
} }
case .scheduleBracket:
ForEach(tournament.rounds()) { round in
_schedulerView(round)
}
default:
EmptyView()
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
@ -42,12 +52,12 @@ struct SchedulerView: View {
GroupStageScheduleEditorView(groupStage: groupStage) GroupStageScheduleEditorView(groupStage: groupStage)
} }
} }
.navigationTitle(schedulable.selectionLabel()) .navigationTitle(schedulable.titleLabel())
} label: { } label: {
LabeledContent { LabeledContent {
Text(schedulable.matchFormat.format).font(.largeTitle) Text(schedulable.matchFormat.format).font(.largeTitle)
} label: { } label: {
if let startDate = schedulable.startDate { if let startDate = schedulable.getStartDate() {
Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle) Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle)
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year()))
} else { } else {
@ -55,12 +65,21 @@ struct SchedulerView: View {
} }
} }
} }
if let round = schedulable as? Round {
NavigationLink {
LoserRoundScheduleEditorView(upperRound: round)
.environment(tournament)
} label: {
Text("Match de classement \(round.roundTitle(.short))")
}
}
} header: { } header: {
Text(schedulable.selectionLabel()) Text(schedulable.titleLabel())
} }
.headerProminence(.increased)
} }
} }
#Preview { #Preview {
SchedulerView(tournament: Tournament.mock()) SchedulerView(tournament: Tournament.mock(), destination: .scheduleBracket)
} }

@ -95,7 +95,7 @@ struct SetInputView: View {
return [6,5] return [6,5]
} }
if valueTeamOne == 5 && setFormat == .four { if valueTeamOne == 5 && setFormat == .four {
return [3,2] return [4,3]
} }
} }
return setFormat.possibleValues return setFormat.possibleValues

@ -7,39 +7,34 @@
import SwiftUI import SwiftUI
protocol Schedulable: Selectable, Identifiable { protocol Schedulable: Identifiable {
var startDate: Date? { get set } var startDate: Date? { get set }
var matchFormat: MatchFormat { get set } var matchFormat: MatchFormat { get set }
func playedMatches() -> [Match] func playedMatches() -> [Match]
func titleLabel() -> String
} }
enum ScheduleDestination: Identifiable, Selectable { extension Schedulable {
var id: String { func getStartDate() -> Date? {
switch self { startDate ?? playedMatches().first?.startDate
case .groupStage(let groupStage):
return groupStage.id
case .round(let round):
return round.id
default:
return String(describing: self)
}
} }
}
enum ScheduleDestination: String, Identifiable, Selectable {
var id: String { self.rawValue }
case planning case planning
case schedule case scheduleGroupStage
case groupStage(GroupStage) case scheduleBracket
case round(Round)
func selectionLabel() -> String { func selectionLabel() -> String {
switch self { switch self {
case .schedule: case .scheduleGroupStage:
return "Horaires" return "Poules"
case .scheduleBracket:
return "Tableau"
case .planning: case .planning:
return "Progr." return "Programmation"
case .groupStage(let groupStage):
return groupStage.selectionLabel()
case .round(let round):
return round.selectionLabel()
} }
} }
@ -52,12 +47,17 @@ struct TournamentScheduleView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedScheduleDestination: ScheduleDestination? = nil @State private var selectedScheduleDestination: ScheduleDestination? = nil
let allDestinations: [ScheduleDestination] let allDestinations: [ScheduleDestination]
let verticalDestinations: [ScheduleDestination]
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
self.verticalDestinations = tournament.groupStages().map({ .groupStage($0) }) + tournament.rounds().map({ .round($0) }) var destinations = [ScheduleDestination.planning]
self.allDestinations = [.schedule, .planning] if tournament.groupStages().isEmpty == false {
destinations.append(.scheduleGroupStage)
}
if tournament.rounds().isEmpty == false {
destinations.append(.scheduleBracket)
}
self.allDestinations = destinations
} }
var body: some View { var body: some View {
@ -69,14 +69,12 @@ struct TournamentScheduleView: View {
.navigationTitle("Réglages") .navigationTitle("Réglages")
case .some(let selectedSchedule): case .some(let selectedSchedule):
switch selectedSchedule { switch selectedSchedule {
case .groupStage(let groupStage): case .scheduleGroupStage:
Text("ok") SchedulerView(tournament: tournament, destination: selectedSchedule)
case .round(let round): case .scheduleBracket:
Text("ok") SchedulerView(tournament: tournament, destination: selectedSchedule)
case .planning: case .planning:
PlanningView(matches: tournament.allMatches()) PlanningView(matches: tournament.allMatches())
case .schedule:
SchedulerView(tournament: tournament)
} }
} }
} }

@ -20,11 +20,12 @@ struct DateBoxView: View {
.font(displayStyle == .wide ? .title : .title3) .font(displayStyle == .wide ? .title : .title3)
.monospacedDigit() .monospacedDigit()
} }
Text(date.formatted(.dateTime.month(.abbreviated))) if displayStyle == .wide {
.font(.caption2) Text(date.formatted(.dateTime.month(.abbreviated)))
Text(date.formatted(.dateTime.year())) .font(.caption2)
.font(.caption2) Text(date.formatted(.dateTime.year()))
.font(.caption2)
}
} }
} }
} }

@ -28,7 +28,7 @@ struct TournamentCellView: View {
private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View { private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View {
HStack { HStack {
DateBoxView(date: tournament.startDate, displayStyle: displayStyle) DateBoxView(date: tournament.startDate, displayStyle: displayStyle == .wide ? .short : .wide)
Rectangle() Rectangle()
.fill(color) .fill(color)
.frame(width: 2) .frame(width: 2)
@ -56,7 +56,7 @@ struct TournamentCellView: View {
Button { Button {
if let existingTournament { if let existingTournament {
navigation.agendaDestination = .activity navigation.agendaDestination = .activity
navigation.tournament = existingTournament navigation.path.append(existingTournament)
} else { } else {
let event = federalTournament.getEvent() let event = federalTournament.getEvent()
let newTournament = Tournament.newEmptyInstance() let newTournament = Tournament.newEmptyInstance()

@ -22,12 +22,14 @@ struct TournamentRunningView: View {
} }
} }
Section { if tournament.groupStages().isEmpty == false {
NavigationLink(value: Screen.groupStage) { Section {
LabeledContent { NavigationLink(value: Screen.groupStage) {
Text(tournament.groupStageStatus()) LabeledContent {
} label: { Text(tournament.groupStageStatus())
Text("Poules") } label: {
Text("Poules")
}
} }
} }
} }

Loading…
Cancel
Save