update scheduler and planning stuff

multistore
Razmig Sarkissian 2 years ago
parent bb17216ec6
commit 15323dbb4a
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 40
      PadelClub/Data/Match.swift
  3. 29
      PadelClub/Data/Round.swift
  4. 13
      PadelClub/Data/TeamRegistration.swift
  5. 11
      PadelClub/Data/Tournament.swift
  6. 6
      PadelClub/Extensions/Date+Extensions.swift
  7. 190
      PadelClub/ViewModel/MatchScheduler.swift
  8. 12
      PadelClub/Views/Match/MatchSetupView.swift
  9. 141
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  10. 11
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  11. 85
      PadelClub/Views/Planning/PlanningSettingsView.swift
  12. 25
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  13. 33
      PadelClub/Views/Planning/SchedulerView.swift
  14. 51
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift
  15. 14
      PadelClub/Views/Tournament/TournamentRunningView.swift

@ -212,6 +212,7 @@
FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964542BC266CF00EEF017 /* SchedulerView.swift */; };
FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.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 */
/* Begin PBXContainerItemProxy section */
@ -468,6 +469,7 @@
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@ -1034,6 +1036,7 @@
FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */,
FFF964542BC266CF00EEF017 /* SchedulerView.swift */,
FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */,
FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */,
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
);
@ -1321,6 +1324,7 @@
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */,
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */,

@ -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 {
guard let bracketPosition = team.bracketPosition else { return false }
return index * 2 + teamPosition.rawValue == bracketPosition
}
func estimatedEndDate() -> Date? {
let minutesToAdd = Double(matchFormat.estimatedDuration)
return startDate?.addingTimeInterval(minutesToAdd * 60.0)
}
func resetMatch() {
losingTeamId = nil
winningTeamId = nil
@ -281,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 {
currentTournament()?.courtCount ?? 1
}

@ -200,6 +200,35 @@ class Round: ModelObject, Storable {
_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? {
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 })

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

@ -86,6 +86,12 @@ class Tournament : ModelObject, Storable {
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] {
let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id })
return Set(runningMatches.compactMap { $0.court }).sorted()
@ -127,7 +133,8 @@ class Tournament : ModelObject, Storable {
}
func state() -> Tournament.State {
if groupStageCount > 0 && groupStages().isEmpty == false {
if (groupStageCount > 0 && groupStages().isEmpty == false)
|| rounds().isEmpty == false {
return .build
}
return .initial
@ -348,7 +355,7 @@ class Tournament : ModelObject, Storable {
}
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
}

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

@ -6,6 +6,7 @@
//
import Foundation
import LeStorage
struct GroupStageTimeMatch {
let matchID: String
@ -51,10 +52,42 @@ extension Match {
}
}
enum MatchSchedulerOption: Hashable {
case accountUpperBracketBreakTime
case accountLoserBracketBreakTime
case randomizeCourts
case rotationDifferenceIsImportant
case shouldHandleUpperRoundSlice
}
class MatchScheduler {
static let shared = MatchScheduler()
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> GroupStageMatchDispatcher {
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 rotationDifferenceIsImportant() -> Bool {
options.contains(.rotationDifferenceIsImportant)
}
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?) -> GroupStageMatchDispatcher {
let _groupStages = groupStages
@ -120,7 +153,7 @@ class MatchScheduler {
var organizedSlots = [GroupStageTimeMatch]()
for i in 0..<rotationIndex {
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))
for j in 0..<matches.count {
@ -133,6 +166,14 @@ class MatchScheduler {
return GroupStageMatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation)
}
func rotationDifference(loserBracket: Bool) -> Int {
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)
@ -156,13 +197,28 @@ class MatchScheduler {
return false
}
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex })
guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: roundObject.isLoserBracket() == false) }).max() else {
var includeBreakTime = false
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 {
return true
if rotationDifferenceIsImportant() {
return previousMatchIsInPreviousRotation
} else {
return true
}
} else {
if targetedStartDate == minimumTargetedEndDate {
minimumTargetedEndDate = minimumPossibleEndDate
@ -191,24 +247,66 @@ class MatchScheduler {
)
}
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, dispatcherStartDate: Date) -> MatchDispatcher {
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 availableMatchs = flattenedMatches
var _startDate: Date?
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 courts = [Int]()
let availableCourt = numberOfCourtsAvailablePerRotation
var courts = initialCourts ?? (0..<availableCourt).map { $0 }
var shouldStartAtDispatcherDate = rotationIndex > 0
while availableMatchs.count > 0 {
freeCourtPerRotation[rotationIndex] = []
var matchPerRound = [Int: Int]()
var availableCourt = numberOfCourtsAvailablePerRotation
courts = (0..<availableCourt).map { $0 }
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
if shouldStartAtDispatcherDate {
rotationStartDate = dispatcherStartDate
shouldStartAtDispatcherDate = false
} 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) })
@ -224,11 +322,11 @@ class MatchScheduler {
var difference = differenceWithBreak
if differenceWithBreak <= 0 {
difference = differenceWithoutBreak
} else if differenceWithBreak > 0 && differenceWithoutBreak > 0 {
} else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit {
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
}
if difference > 0 {
if difference > timeDifferenceLimit {
courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index)
})
freeCourtPerRotation[rotationIndex] = courts
@ -245,7 +343,7 @@ class MatchScheduler {
var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex {
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(\.courtIndex))
for j in 0..<matches.count {
@ -267,6 +365,16 @@ class MatchScheduler {
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
@ -275,7 +383,7 @@ class MatchScheduler {
}
}
if (matchPerRound[roundObject.index] ?? 0)%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 {
if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 {
return false
}
@ -309,9 +417,12 @@ class MatchScheduler {
}
}
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 allMatches = tournament.allMatches()
var roundIndex = 0
let rounds = upperRounds.map {
@ -319,22 +430,41 @@ class MatchScheduler {
} + upperRounds.flatMap {
$0.loserRoundsAndChildren()
}
if let roundId {
roundIndex = rounds.firstIndex(where: { $0.id == roundId }) ?? 0
}
var flattenedMatches = rounds[roundIndex...].flatMap { round in
var flattenedMatches = rounds.flatMap { round in
round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
}
flattenedMatches.forEach({
if (roundId == nil && matchId == nil) || $0.startDate?.isEarlierThan(startDate) == false {
$0.startDate = nil
}
})
if let matchId, let matchIndex = flattenedMatches.firstIndex(where: { $0.id == matchId }) {
flattenedMatches = Array(flattenedMatches[matchIndex...])
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 roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, dispatcherStartDate: startDate)
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, dispatcherStartDate: startDate, initialCourts: courts)
roundDispatch.timedMatches.forEach { matchSchedule in
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
@ -343,7 +473,7 @@ class MatchScheduler {
}
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: flattenedMatches)
try? DataStore.shared.matches.addOrUpdate(contentOfs: allMatches)
}
}

@ -91,6 +91,18 @@ struct MatchSetupView: View {
Text("Tirage").tag(nil as SeedInterval?)
}
.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)

@ -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) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Modifier") {
RowButtonView("Valider la modification") {
_updateSchedule()
}
} header: {
Text(match.matchTitle())
if let round = match.roundObject {
Text(round.roundTitle() + " " + match.matchTitle())
} else {
Text(match.matchTitle())
}
}
.headerProminence(.increased)
}
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
var tournament: Tournament
@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 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) {
self.tournament = tournament
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 {
@ -39,8 +54,11 @@ struct PlanningSettingsView: View {
Section {
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 {
} label: {
@ -54,6 +72,38 @@ struct PlanningSettingsView: View {
Text("Distribuer les terrains au hasard")
}
Toggle(isOn: $shouldHandleUpperRoundSlice) {
Text("Équilibrer les matchs d'une manche sur plusieurs tours")
}
Toggle(isOn: $upperBracketBreakTime) {
Text("Tableau : tenir compte des pauses")
}
Toggle(isOn: $loserBracketBreakTime) {
Text("Classement : tenir compte des pauses")
}
Toggle(isOn: $rotationDifferenceIsImportant) {
Text("Forcer un créneau supplémentaire entre 2 phases")
}
LabeledContent {
StepperView(count: $upperBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
Text("Tableau")
}
.disabled(rotationDifferenceIsImportant == false)
LabeledContent {
StepperView(count: $loserBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
Text("Classement")
}
.disabled(rotationDifferenceIsImportant == false)
//timeDifferenceLimit
RowButtonView("Horaire intelligent", role: .destructive) {
_setupSchedule()
}
@ -96,7 +146,32 @@ struct PlanningSettingsView: View {
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 })
@ -112,7 +187,7 @@ struct PlanningSettingsView: View {
groups.forEach({ $0.startDate = lastDate })
try? dataStore.groupStages.addOrUpdate(contentOfs: groups)
let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate, randomizeCourts: randomCourtDistribution)
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 }) {
@ -128,7 +203,7 @@ struct PlanningSettingsView: View {
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, randomizeCourts: randomCourtDistribution, startDate: lastDate)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
scheduleSetup = true

@ -24,13 +24,10 @@ struct RoundScheduleEditorView: View {
List {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
}
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Modifier") {
RowButtonView("Valider la modification") {
_updateSchedule()
}
}
@ -39,20 +36,18 @@ struct RoundScheduleEditorView: View {
MatchScheduleEditorView(match: match)
}
}
.onChange(of: round.matchFormat) {
let matches = round._matches()
matches.forEach { match in
match.matchFormat = round.matchFormat
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
_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() {

@ -20,19 +20,21 @@ extension Round: Schedulable {
struct SchedulerView: View {
var tournament: Tournament
var destination: ScheduleDestination
var body: some View {
List {
ForEach(tournament.groupStages()) {
_schedulerView($0)
}
ForEach(tournament.rounds()) { round in
_schedulerView(round)
ForEach(round.loserRoundsAndChildren()) { loserRound in
if loserRound.isDisabled() == false {
_schedulerView(loserRound)
}
switch destination {
case .scheduleGroupStage:
ForEach(tournament.groupStages()) {
_schedulerView($0)
}
case .scheduleBracket:
ForEach(tournament.rounds()) { round in
_schedulerView(round)
}
default:
EmptyView()
}
}
.headerProminence(.increased)
@ -63,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: {
Text(schedulable.titleLabel())
}
.headerProminence(.increased)
}
}
#Preview {
SchedulerView(tournament: Tournament.mock())
SchedulerView(tournament: Tournament.mock(), destination: .scheduleBracket)
}

@ -20,33 +20,21 @@ extension Schedulable {
}
}
enum ScheduleDestination: Identifiable, Selectable {
var id: String {
switch self {
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 schedule
case groupStage(GroupStage)
case round(Round)
case scheduleGroupStage
case scheduleBracket
func selectionLabel() -> String {
switch self {
case .schedule:
return "Horaires"
case .scheduleGroupStage:
return "Poules"
case .scheduleBracket:
return "Tableau"
case .planning:
return "Progr."
case .groupStage(let groupStage):
return groupStage.selectionLabel()
case .round(let round):
return round.selectionLabel()
return "Programmation"
}
}
@ -59,12 +47,17 @@ struct TournamentScheduleView: View {
var tournament: Tournament
@State private var selectedScheduleDestination: ScheduleDestination? = nil
let allDestinations: [ScheduleDestination]
let verticalDestinations: [ScheduleDestination]
init(tournament: Tournament) {
self.tournament = tournament
self.verticalDestinations = tournament.groupStages().map({ .groupStage($0) }) + tournament.rounds().map({ .round($0) })
self.allDestinations = [.schedule, .planning]
var destinations = [ScheduleDestination.planning]
if tournament.groupStages().isEmpty == false {
destinations.append(.scheduleGroupStage)
}
if tournament.rounds().isEmpty == false {
destinations.append(.scheduleBracket)
}
self.allDestinations = destinations
}
var body: some View {
@ -76,14 +69,12 @@ struct TournamentScheduleView: View {
.navigationTitle("Réglages")
case .some(let selectedSchedule):
switch selectedSchedule {
case .groupStage(let groupStage):
Text("ok")
case .round(let round):
Text("ok")
case .scheduleGroupStage:
SchedulerView(tournament: tournament, destination: selectedSchedule)
case .scheduleBracket:
SchedulerView(tournament: tournament, destination: selectedSchedule)
case .planning:
PlanningView(matches: tournament.allMatches())
case .schedule:
SchedulerView(tournament: tournament)
}
}
}

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

Loading…
Cancel
Save