fix scheduler and add court pickup

paca_championship
Raz 1 year ago
parent b3144be82d
commit c025559b5e
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 93
      PadelClub/Data/MatchScheduler.swift
  3. 4
      PadelClub/Data/Tournament.swift
  4. 38
      PadelClub/Views/Planning/Components/MultiCourtPickerView.swift
  5. 54
      PadelClub/Views/Planning/PlanningSettingsView.swift

@ -81,6 +81,9 @@
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; };
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; };
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; };
FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; };
FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; };
FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; };
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; };
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; };
@ -977,6 +980,7 @@
FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; }; FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; };
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; }; FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; };
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; };
FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = "<group>"; };
FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; };
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; };
@ -1514,6 +1518,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */, FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */,
FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2388,6 +2393,7 @@
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */, FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
@ -2657,6 +2663,7 @@
FF4CBFE92C996C0600151637 /* CloudConvert.swift in Sources */, FF4CBFE92C996C0600151637 /* CloudConvert.swift in Sources */,
FF4CBFEA2C996C0600151637 /* EventTournamentsView.swift in Sources */, FF4CBFEA2C996C0600151637 /* EventTournamentsView.swift in Sources */,
FF4CBFEB2C996C0600151637 /* DisplayContext.swift in Sources */, FF4CBFEB2C996C0600151637 /* DisplayContext.swift in Sources */,
FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF4CBFEC2C996C0600151637 /* TournamentCallView.swift in Sources */, FF4CBFEC2C996C0600151637 /* TournamentCallView.swift in Sources */,
FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */, FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */,
FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */, FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */,
@ -2905,6 +2912,7 @@
FF70FB682C90584900129CC2 /* CloudConvert.swift in Sources */, FF70FB682C90584900129CC2 /* CloudConvert.swift in Sources */,
FF70FB692C90584900129CC2 /* EventTournamentsView.swift in Sources */, FF70FB692C90584900129CC2 /* EventTournamentsView.swift in Sources */,
FF70FB6A2C90584900129CC2 /* DisplayContext.swift in Sources */, FF70FB6A2C90584900129CC2 /* DisplayContext.swift in Sources */,
FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF70FB6B2C90584900129CC2 /* TournamentCallView.swift in Sources */, FF70FB6B2C90584900129CC2 /* TournamentCallView.swift in Sources */,
FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */, FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */,
FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */, FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */,

@ -31,6 +31,7 @@ final class MatchScheduler : ModelObject, Storable {
var groupStageChunkCount: Int? var groupStageChunkCount: Int?
var overrideCourtsUnavailability: Bool = false var overrideCourtsUnavailability: Bool = false
var shouldTryToFillUpCourtsAvailable: Bool = false var shouldTryToFillUpCourtsAvailable: Bool = false
var courtsAvailable: Set<Int> = Set<Int>()
init(tournament: String, init(tournament: String,
timeDifferenceLimit: Int = 5, timeDifferenceLimit: Int = 5,
@ -42,7 +43,7 @@ final class MatchScheduler : ModelObject, Storable {
rotationDifferenceIsImportant: Bool = false, rotationDifferenceIsImportant: Bool = false,
shouldHandleUpperRoundSlice: Bool = true, shouldHandleUpperRoundSlice: Bool = true,
shouldEndRoundBeforeStartingNext: Bool = true, shouldEndRoundBeforeStartingNext: Bool = true,
groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) { groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false, courtsAvailable: Set<Int> = Set<Int>()) {
self.tournament = tournament self.tournament = tournament
self.timeDifferenceLimit = timeDifferenceLimit self.timeDifferenceLimit = timeDifferenceLimit
self.loserBracketRotationDifference = loserBracketRotationDifference self.loserBracketRotationDifference = loserBracketRotationDifference
@ -56,6 +57,7 @@ final class MatchScheduler : ModelObject, Storable {
self.groupStageChunkCount = groupStageChunkCount self.groupStageChunkCount = groupStageChunkCount
self.overrideCourtsUnavailability = overrideCourtsUnavailability self.overrideCourtsUnavailability = overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
self.courtsAvailable = courtsAvailable
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -73,6 +75,7 @@ final class MatchScheduler : ModelObject, Storable {
case _groupStageChunkCount = "groupStageChunkCount" case _groupStageChunkCount = "groupStageChunkCount"
case _overrideCourtsUnavailability = "overrideCourtsUnavailability" case _overrideCourtsUnavailability = "overrideCourtsUnavailability"
case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable"
case _courtsAvailable = "courtsAvailable"
} }
var courtsUnavailability: [DateInterval]? { var courtsUnavailability: [DateInterval]? {
@ -99,7 +102,6 @@ final class MatchScheduler : ModelObject, Storable {
if let specificGroupStage { if let specificGroupStage {
groupStages = [specificGroupStage] groupStages = [specificGroupStage]
} }
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matches = groupStages.flatMap { $0._matches() } let matches = groupStages.flatMap { $0._matches() }
matches.forEach({ matches.forEach({
@ -127,7 +129,7 @@ final class MatchScheduler : ModelObject, Storable {
lastDate = time lastDate = time
} }
let groups = groupStages.filter({ $0.startDate == time }) let groups = groupStages.filter({ $0.startDate == time })
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
@ -151,7 +153,7 @@ final class MatchScheduler : ModelObject, Storable {
Logger.error(error) Logger.error(error)
} }
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
@ -174,7 +176,7 @@ final class MatchScheduler : ModelObject, Storable {
return lastDate return lastDate
} }
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
let _groupStages = groupStages let _groupStages = groupStages
@ -214,7 +216,7 @@ final class MatchScheduler : ModelObject, Storable {
print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)") print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)")
} }
return teamsAvailable return teamsAvailable
}).prefix(numberOfCourtsAvailablePerRotation)) }).prefix(courtsAvailable.count))
if rotationIndex > 0 { if rotationIndex > 0 {
rotationMatches = rotationMatches.sorted(by: { rotationMatches = rotationMatches.sorted(by: {
@ -226,7 +228,7 @@ final class MatchScheduler : ModelObject, Storable {
}) })
} }
(0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in courtsAvailable.forEach { courtIndex in
print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)") print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)")
if let first = rotationMatches.first(where: { match in if let first = rotationMatches.first(where: { match in
let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
@ -427,7 +429,7 @@ final class MatchScheduler : ModelObject, Storable {
) )
} }
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
var slots = [TimeMatch]() var slots = [TimeMatch]()
var _startDate: Date? var _startDate: Date?
var rotationIndex = 0 var rotationIndex = 0
@ -436,7 +438,7 @@ final class MatchScheduler : ModelObject, Storable {
var issueFound: Bool = false var issueFound: Bool = false
// Log start of the function // Log start of the function
print("Starting roundDispatcher with \(availableMatchs.count) matches and \(numberOfCourtsAvailablePerRotation) courts available") print("Starting roundDispatcher with \(availableMatchs.count) matches and \(courtsAvailable) courts available")
flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in
if _startDate == nil { if _startDate == nil {
@ -455,8 +457,7 @@ final class MatchScheduler : ModelObject, Storable {
} }
var freeCourtPerRotation = [Int: [Int]]() var freeCourtPerRotation = [Int: [Int]]()
let availableCourt = numberOfCourtsAvailablePerRotation var courts = initialCourts ?? Array(courtsAvailable)
var courts = initialCourts ?? (0..<availableCourt).map { $0 }
var shouldStartAtDispatcherDate = rotationIndex > 0 var shouldStartAtDispatcherDate = rotationIndex > 0
while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 {
@ -468,7 +469,7 @@ final class MatchScheduler : ModelObject, Storable {
rotationStartDate = dispatcherStartDate rotationStartDate = dispatcherStartDate
shouldStartAtDispatcherDate = false shouldStartAtDispatcherDate = false
} else { } else {
courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 } courts = rotationIndex == 0 ? courts : Array(courtsAvailable)
} }
courts.sort() courts.sort()
@ -520,16 +521,22 @@ final class MatchScheduler : ModelObject, Storable {
let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration) let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration)
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
if courtsUnavailable.count == numberOfCourtsAvailablePerRotation { if Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))).isEmpty {
print("Issue: All courts unavailable in this rotation") print("Issue: All courts unavailable in this rotation")
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: rotationStartDate, duration: duration, courts: courts, courtsUnavailability: courtsUnavailability)
rotationStartDate = computedStartDateAndCourts.earliestFreeDate
courts = computedStartDateAndCourts.availableCourts
} else {
issueFound = true issueFound = true
}
} else { } else {
courts = Array(Set(courts).subtracting(Set(courtsUnavailable))) courts = Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable)))
} }
} }
// Dispatch courts and schedule matches // Dispatch courts and schedule matches
dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
rotationIndex += 1 rotationIndex += 1
} }
@ -551,7 +558,7 @@ final class MatchScheduler : ModelObject, Storable {
return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound)
} }
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) { func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) {
var matchPerRound = [String: Int]() var matchPerRound = [String: Int]()
var minimumTargetedEndDate = rotationStartDate var minimumTargetedEndDate = rotationStartDate
@ -567,7 +574,7 @@ final class MatchScheduler : ModelObject, Storable {
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
if courtsUnavailable.contains(courtPosition) { if courtsUnavailable.contains(courtIndex) {
print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).") print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).")
return false return false
} }
@ -591,7 +598,9 @@ final class MatchScheduler : ModelObject, Storable {
let indexInRound = match.indexInRound() let indexInRound = match.indexInRound()
if roundObject.parent == nil && roundObject.index > 0 && indexInRound == 0, let nextMatch = match.next() {
if shouldTryToFillUpCourtsAvailable == false {
if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() {
if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) {
print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).")
return true return true
@ -600,6 +609,7 @@ final class MatchScheduler : ModelObject, Storable {
return false return false
} }
} }
}
print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).") print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).")
return canBePlayed return canBePlayed
@ -626,7 +636,7 @@ final class MatchScheduler : ModelObject, Storable {
} }
if freeCourtPerRotation[rotationIndex]?.count == availableCourts { if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count {
print("All courts in rotation \(rotationIndex) are free") print("All courts in rotation \(rotationIndex) are free")
} }
} }
@ -713,7 +723,7 @@ final class MatchScheduler : ModelObject, Storable {
print("initial available courts at beginning: \(courts ?? [])") print("initial available courts at beginning: \(courts ?? [])")
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts) let roundDispatch = self.roundDispatcher(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 }) {
@ -750,7 +760,50 @@ final class MatchScheduler : ModelObject, Storable {
}) })
} }
func getFirstFreeCourt(startDate: Date, duration: Int, courts: [Int], courtsUnavailability: [DateInterval]) -> (earliestFreeDate: Date, availableCourts: [Int]) {
var earliestEndDate: Date?
var availableCourtsAtEarliest: [Int] = []
// Iterate through each court and find the earliest time it becomes free
for courtIndex in courts {
let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex }
var isAvailable = true
for interval in unavailabilityForCourt {
if interval.startDate <= startDate && interval.endDate > startDate {
isAvailable = false
if let currentEarliest = earliestEndDate {
earliestEndDate = min(currentEarliest, interval.endDate)
} else {
earliestEndDate = interval.endDate
}
}
}
// If the court is available at the start date, add it to the list of available courts
if isAvailable {
availableCourtsAtEarliest.append(courtIndex)
}
}
// If there are no unavailable courts, return the original start date and all courts
if let earliestEndDate = earliestEndDate {
// Find which courts will be available at the earliest free date
let courtsAvailableAtEarliest = courts.filter { courtIndex in
let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex }
return unavailabilityForCourt.allSatisfy { $0.endDate <= earliestEndDate }
}
return (earliestFreeDate: earliestEndDate, availableCourts: courtsAvailableAtEarliest)
} else {
// If no courts were unavailable, all courts are available at the start date
return (earliestFreeDate: startDate.addingTimeInterval(Double(duration) * 60), availableCourts: courts)
}
}
func updateSchedule(tournament: Tournament) -> Bool { func updateSchedule(tournament: Tournament) -> Bool {
if tournament.courtCount < courtsAvailable.count {
courtsAvailable = Set(tournament.courtsAvailable())
}
var lastDate = tournament.startDate var lastDate = tournament.startDate
if tournament.groupStageCount > 0 { if tournament.groupStageCount > 0 {
lastDate = updateGroupStageSchedule(tournament: tournament) lastDate = updateGroupStageSchedule(tournament: tournament)

@ -2045,6 +2045,10 @@ defer {
return self._matchSchedulers().first return self._matchSchedulers().first
} }
func courtsAvailable() -> [Int] {
(0..<courtCount).map { $0 }
}
func currentMonthData() -> MonthData? { func currentMonthData() -> MonthData? {
guard let rankSourceDate else { return nil } guard let rankSourceDate else { return nil }
let dateString = URL.importDateFormatter.string(from: rankSourceDate) let dateString = URL.importDateFormatter.string(from: rankSourceDate)

@ -0,0 +1,38 @@
//
// MultiCourtPickerView.swift
// PadelClub
//
// Created by razmig on 11/10/2024.
//
import SwiftUI
struct MultiCourtPickerView: View {
@Bindable var matchScheduler: MatchScheduler
@Environment(Tournament.self) var tournament: Tournament
var body: some View {
List {
ForEach(tournament.courtsAvailable(), id: \.self) { courtIndex in
LabeledContent {
Button {
if matchScheduler.courtsAvailable.contains(courtIndex) {
matchScheduler.courtsAvailable.remove(courtIndex)
} else {
matchScheduler.courtsAvailable.insert(courtIndex)
}
} label: {
if matchScheduler.courtsAvailable.contains(courtIndex) {
Image(systemName: "checkmark.circle.fill")
}
}
} label: {
Text(tournament.courtName(atIndex: courtIndex))
}
}
}
.navigationTitle("Terrains disponibles")
.toolbarBackground(.visible, for: .navigationBar)
.environment(\.editMode, Binding.constant(EditMode.active))
}
}

@ -39,7 +39,7 @@ struct PlanningSettingsView: View {
_groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) _groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue())
} }
} else { } else {
self.matchScheduler = MatchScheduler(tournament: tournament.id) self.matchScheduler = MatchScheduler(tournament: tournament.id, courtsAvailable: Set(tournament.courtsAvailable()))
self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue())
} }
} }
@ -68,7 +68,26 @@ struct PlanningSettingsView: View {
CourtAvailabilitySettingsView(event: event) CourtAvailabilitySettingsView(event: event)
.environment(tournament) .environment(tournament)
} label: { } label: {
Text("Indisponibilités des terrains") LabeledContent {
Text(event.courtsUnavailability.count.formatted())
} label: {
Text("Créneaux d'indisponibilités")
}
}
}
NavigationLink {
MultiCourtPickerView(matchScheduler: matchScheduler)
.environment(tournament)
} label: {
LabeledContent {
Text(matchScheduler.courtsAvailable.count.formatted() + "/" + tournament.courtCount.formatted())
} label: {
Text("Sélection des terrains")
if matchScheduler.courtsAvailable.count > tournament.courtCount {
Text("Attention !")
.tint(.red)
}
} }
} }
} footer: { } footer: {
@ -105,12 +124,23 @@ struct PlanningSettingsView: View {
.foregroundStyle(.logoRed) .foregroundStyle(.logoRed)
} }
let event = tournament.eventObject()
Section { Section {
NavigationLink { NavigationLink {
_optionsView() _optionsView()
} label: { } label: {
Text("Voir plus d'options intelligentes") Text("Voir plus d'options intelligentes")
} }
if let event, event.tournaments.count > 1 {
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) {
Text("Ne pas tenir compte des autres tournois")
}
}
} footer: {
if let event, event.tournaments.count > 1 {
Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.")
}
} }
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
@ -271,28 +301,20 @@ struct PlanningSettingsView: View {
} }
} }
// Section {
// Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) {
// Text("Remplir au maximum les terrains d'une rotation")
// }
// } footer: {
// Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.")
// }
//
Section { Section {
Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) {
Text("Équilibrer les matchs d'une manche") Text("Remplir au maximum les terrains d'une rotation")
} }
} footer: { } footer: {
Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.")
} }
Section { Section {
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) {
Text("Ne pas tenir compte des autres tournois") Text("Équilibrer les matchs d'une manche")
} }
} footer: { } footer: {
Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi est toujours considéré comme libre.") Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.")
} }
Section { Section {

Loading…
Cancel
Save