From 58afa51005fc07c7a642d37c640df8bf7ff09ba0 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 11 Apr 2024 22:33:55 +0200 Subject: [PATCH 1/7] wip scheduler re-write --- PadelClub/Data/Match.swift | 7 + PadelClub/ViewModel/MatchScheduler.swift | 120 +++++++++++++++--- .../Views/Planning/PlanningSettingsView.swift | 25 +--- PadelClub/Views/Planning/PlanningView.swift | 3 +- .../Planning/RoundScheduleEditorView.swift | 7 + PadelClub/Views/Planning/SchedulerView.swift | 20 ++- .../Screen/TournamentScheduleView.swift | 9 +- 7 files changed, 140 insertions(+), 51 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ee4cd30..69baa1e 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -151,6 +151,13 @@ class Match: ModelObject, Storable { func next() -> Match? { 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 { index * 2 + 1 } diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 5778eca..7919d25 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -7,11 +7,34 @@ import Foundation +struct GroupStageTimeMatch { + let matchID: String + let rotationIndex: Int + var courtIndex: Int + let groupIndex: Int +} + struct TimeMatch { let matchID: String let rotationIndex: Int var courtIndex: Int let groupIndex: Int + var startDate: Date + var durationLeft: Int //in minutes + var minimumBreakTime: Int //in minutes + var courtLocked: Bool = false + + 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 { @@ -34,7 +57,7 @@ extension Match { class MatchScheduler { static let shared = MatchScheduler() - func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> MatchDispatcher { + func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> GroupStageMatchDispatcher { let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate } @@ -50,7 +73,7 @@ class MatchScheduler { } } - var slots = [TimeMatch]() + var slots = [GroupStageTimeMatch]() var availableMatchs = flattenedMatches var rotationIndex = 0 var teamsPerRotation = [Int: [String]]() @@ -81,7 +104,8 @@ class MatchScheduler { if let first = rotationMatches.first(where: { match in 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()) rotationMatches.removeAll(where: { $0.id == first.id }) availableMatchs.removeAll(where: { $0.id == first.id }) @@ -96,7 +120,7 @@ class MatchScheduler { rotationIndex += 1 } - var organizedSlots = [TimeMatch]() + var organizedSlots = [GroupStageTimeMatch]() for i in 0.. Bool { + func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date) -> Bool { print(roundObject.roundTitle(), match.matchTitle()) let previousMatches = roundObject.precedentMatches(ofMatch: match) if previousMatches.isEmpty { return true } @@ -135,34 +159,91 @@ class MatchScheduler { return false } - let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + (roundObject.loser == nil ? 1 : 0) < rotationIndex }) - return previousMatchIsInPreviousRotation +// if roundObject.isLoserBracket() { +// let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex }) +// return previousMatchIsInPreviousRotation +// } + + let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex }) + guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: true) }).max() else { + return previousMatchIsInPreviousRotation + } + + return targetedStartDate >= minimumPossibleEndDate + } + + func getAvailableCourt(inSlots slots: [TimeMatch], nextStartDate: Date) -> [TimeMatch] { + guard let minimumDuration = slots.compactMap({ $0.durationLeft }).min() else { return [] } + var newSlots = [TimeMatch]() + slots.forEach { timeMatch in + let durationLeft = timeMatch.durationLeft + if durationLeft - minimumDuration > 0 { + let timeMatch = TimeMatch(matchID: timeMatch.matchID, rotationIndex: timeMatch.rotationIndex + 1, courtIndex: timeMatch.courtIndex, groupIndex: timeMatch.groupIndex, startDate: nextStartDate, durationLeft: durationLeft, minimumBreakTime: timeMatch.minimumBreakTime, courtLocked: true) + newSlots.append(timeMatch) + } + } + return newSlots } - 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 roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0, dispatcherStartDate: Date) -> MatchDispatcher { var slots = [TimeMatch]() var availableMatchs = flattenedMatches var rotationIndex = 0 var freeCourtPerRotation = [Int: [Int]]() var groupLastRotation = [Int: Int]() - - while slots.count < flattenedMatches.count { + var courts = [Int]() + var timeToAdd = 0.0 + while availableMatchs.count > 0 { freeCourtPerRotation[rotationIndex] = [] var matchPerRound = [Int: Int]() var availableCourt = numberOfCourtsAvailablePerRotation if rotationIndex == 0 { availableCourt = availableCourt - initialOccupiedCourt } - (0.. 0 ? freeCourtPerRotation[rotationIndex - 1]!.count : 0 + + if previousRotationSlots.isEmpty && rotationIndex > 0 { + let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 }) + rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate + } else if freeCourtPreviousRotation > 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 }) + if let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true) { + rotationStartDate = previousEndDate + courts = freeCourtPerRotation[rotationIndex - 1]! + } + } + + 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) + let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate) 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) { + if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate) { return true } else { return false @@ -184,11 +265,13 @@ class MatchScheduler { matchPerRound[first.roundObject!.index] = 1 } } - slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index )) + let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime) + slots.append(timeMatch) availableMatchs.removeAll(where: { $0.id == first.id }) if let index = first.roundObject?.index { groupLastRotation[index] = rotationIndex } + timeToAdd = 0.0 } else { freeCourtPerRotation[rotationIndex]!.append(courtIndex) } @@ -199,7 +282,7 @@ class MatchScheduler { var organizedSlots = [TimeMatch]() for i in 0.. String { + self.groupStageTitle() + } +} +extension Round: Schedulable { + func titleLabel() -> String { + self.roundTitle() + } +} struct SchedulerView: View { var tournament: Tournament @@ -21,7 +29,7 @@ struct SchedulerView: View { ForEach(tournament.rounds()) { round in _schedulerView(round) ForEach(round.loserRoundsAndChildren()) { loserRound in - if round.isDisabled() == false { + if loserRound.isDisabled() == false { _schedulerView(loserRound) } } @@ -42,12 +50,12 @@ struct SchedulerView: View { GroupStageScheduleEditorView(groupStage: groupStage) } } - .navigationTitle(schedulable.selectionLabel()) + .navigationTitle(schedulable.titleLabel()) } label: { LabeledContent { Text(schedulable.matchFormat.format).font(.largeTitle) } label: { - if let startDate = schedulable.startDate { + if let startDate = schedulable.getStartDate() { Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle) Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) } else { @@ -56,7 +64,7 @@ struct SchedulerView: View { } } } header: { - Text(schedulable.selectionLabel()) + Text(schedulable.titleLabel()) } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 4fccc36..83795fc 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -7,12 +7,19 @@ import SwiftUI -protocol Schedulable: Selectable, Identifiable { +protocol Schedulable: Identifiable { var startDate: Date? { get set } var matchFormat: MatchFormat { get set } func playedMatches() -> [Match] + func titleLabel() -> String } +extension Schedulable { + func getStartDate() -> Date? { + startDate ?? playedMatches().first?.startDate + } + +} enum ScheduleDestination: Identifiable, Selectable { var id: String { switch self { From 3732d636f750508f77f0f7154398bf0a00899b34 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 13 Apr 2024 09:02:53 +0200 Subject: [PATCH 2/7] wip --- PadelClub/ViewModel/MatchScheduler.swift | 204 ++++++++++++++--------- 1 file changed, 123 insertions(+), 81 deletions(-) diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 7919d25..61a763c 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -23,7 +23,7 @@ struct TimeMatch { var durationLeft: Int //in minutes var minimumBreakTime: Int //in minutes var courtLocked: Bool = false - + var freeCourt: Bool = false func estimatedEndDate(includeBreakTime: Bool) -> Date { let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) return startDate.addingTimeInterval(minutesToAdd * 60.0) @@ -136,8 +136,8 @@ class MatchScheduler { return GroupStageMatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) } - func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date) -> Bool { - print(roundObject.roundTitle(), match.matchTitle()) + 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) if previousMatches.isEmpty { return true } @@ -159,18 +159,21 @@ class MatchScheduler { return false } -// if roundObject.isLoserBracket() { -// let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex }) -// return previousMatchIsInPreviousRotation -// } - let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex }) - - guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: true) }).max() else { + guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: roundObject.isLoserBracket() == false) }).max() else { return previousMatchIsInPreviousRotation } - return targetedStartDate >= minimumPossibleEndDate + if targetedStartDate >= minimumPossibleEndDate { + return true + } else { + if targetedStartDate == minimumTargetedEndDate { + minimumTargetedEndDate = minimumPossibleEndDate + } else { + minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) + } + return false + } } func getAvailableCourt(inSlots slots: [TimeMatch], nextStartDate: Date) -> [TimeMatch] { @@ -212,102 +215,141 @@ class MatchScheduler { let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate - let duplicatedSlots = getAvailableCourt(inSlots: previousRotationSlots, nextStartDate: rotationStartDate) - print("duplicatedSlots", duplicatedSlots) - slots.append(contentsOf: duplicatedSlots) - courts.removeAll(where: { index in - duplicatedSlots.anySatisfy { $0.courtIndex == index } - }) +// let duplicatedSlots = getAvailableCourt(inSlots: previousRotationSlots, nextStartDate: rotationStartDate) +// print("duplicatedSlots", duplicatedSlots) +// slots.append(contentsOf: duplicatedSlots) +// courts.removeAll(where: { index in +// duplicatedSlots.anySatisfy { $0.courtIndex == index } +// }) courts.sort() print("courts available at rotation \(rotationIndex)", courts) print("rotationStartDate", rotationStartDate) - let freeCourtPreviousRotation = rotationIndex > 0 ? freeCourtPerRotation[rotationIndex - 1]!.count : 0 - - if previousRotationSlots.isEmpty && rotationIndex > 0 { - let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 }) - rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate - } else if freeCourtPreviousRotation > 0 { + 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 }) - if let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true) { - rotationStartDate = previousEndDate - courts = freeCourtPerRotation[rotationIndex - 1]! - } - } - - courts.forEach { courtIndex in - //print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) + 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) - if let first = availableMatchs.first(where: { match in - let roundObject = match.roundObject! - let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate) - 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, targetedStartDate: rotationStartDate) { - return true - } else { - return false - } - } - - if (matchPerRound[roundObject.index] ?? 0)%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == numberOfCourtsAvailablePerRotation - 1 { - return 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 > 0 && differenceWithoutBreak > 0 { + difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) } - - return canBePlayed - }) { - //print(first.roundObject!.roundTitle(), first.matchTitle()) - if first.roundObject!.loser == nil { - if let roundIndex = matchPerRound[first.roundObject!.index] { - matchPerRound[first.roundObject!.index] = roundIndex + 1 - } else { - matchPerRound[first.roundObject!.index] = 1 - } + if difference > 0 { + courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) + }) + freeCourtPerRotation[rotationIndex] = courts + courts = freeCourtPreviousRotation + rotationStartDate = rotationStartDate.addingTimeInterval(-difference) } - let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime) - slots.append(timeMatch) - availableMatchs.removeAll(where: { $0.id == first.id }) - if let index = first.roundObject?.index { - groupLastRotation[index] = rotationIndex - } - timeToAdd = 0.0 - } else { - freeCourtPerRotation[rotationIndex]!.append(courtIndex) } } - + +// if previousRotationSlots.isEmpty && rotationIndex > 0 { +// let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 }) +// rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate +// } else if freeCourtPreviousRotation > 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 }) +// if let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true) { +// rotationStartDate = previousEndDate +// courts = freeCourtPerRotation[rotationIndex - 1]! +// } +// } + dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation) rotationIndex += 1 } - var organizedSlots = [TimeMatch]() - for i in 0.. 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 (matchPerRound[roundObject.index] ?? 0)%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, groupIndex: first.roundObject!.index, 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) } } - - return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) + if freeCourtPerRotation[rotationIndex]!.count == availableCourts { + freeCourtPerRotation[rotationIndex] = [] + let freeCourts = (0.. Date: Sat, 13 Apr 2024 09:25:44 +0200 Subject: [PATCH 3/7] scheduler v1 ok --- PadelClub/ViewModel/MatchScheduler.swift | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 61a763c..33eaa61 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -193,6 +193,22 @@ class MatchScheduler { 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 roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0, dispatcherStartDate: Date) -> MatchDispatcher { var slots = [TimeMatch]() @@ -327,9 +343,13 @@ class MatchScheduler { if freeCourtPerRotation[rotationIndex]!.count == availableCourts { freeCourtPerRotation[rotationIndex] = [] - let freeCourts = (0.. Date: Sat, 13 Apr 2024 20:08:00 +0200 Subject: [PATCH 4/7] fixes --- PadelClub/Manager/PadelRule.swift | 10 +-- PadelClub/ViewModel/MatchScheduler.swift | 86 +++++-------------- .../Navigation/Agenda/EventListView.swift | 3 +- .../Views/Planning/PlanningSettingsView.swift | 80 +++++++++-------- PadelClub/Views/Planning/PlanningView.swift | 16 ++-- .../Views/Tournament/Shared/DateBoxView.swift | 11 +-- .../Shared/TournamentCellView.swift | 2 +- 7 files changed, 89 insertions(+), 119 deletions(-) diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index cd48f06..cd55595 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -939,7 +939,7 @@ enum SetFormat: Int, Hashable, Codable { var firstGameFormat: Format { switch self { case .megaTieBreak: - return .tiebreakFiveTeen + return .tiebreakFifteen case .superTieBreak: return .tiebreakTen default: @@ -1243,7 +1243,7 @@ enum Format: Int, Hashable, Codable { case normal case tiebreakSeven case tiebreakTen - case tiebreakFiveTeen + case tiebreakFifteen func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { @@ -1253,7 +1253,7 @@ enum Format: Int, Hashable, Codable { return "tie-break en 7" case .tiebreakTen: return "tie-break en 10" - case .tiebreakFiveTeen: + case .tiebreakFifteen: return "tie-break en 15" } } @@ -1261,7 +1261,7 @@ enum Format: Int, Hashable, Codable { switch self { case .normal: return false - case .tiebreakSeven, .tiebreakTen, .tiebreakFiveTeen: + case .tiebreakSeven, .tiebreakTen, .tiebreakFifteen: return true } } @@ -1274,7 +1274,7 @@ enum Format: Int, Hashable, Codable { return 7 case .tiebreakTen: return 10 - case .tiebreakFiveTeen: + case .tiebreakFifteen: return 15 } } diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 33eaa61..b1471e4 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -18,12 +18,10 @@ struct TimeMatch { let matchID: String let rotationIndex: Int var courtIndex: Int - let groupIndex: Int var startDate: Date var durationLeft: Int //in minutes var minimumBreakTime: Int //in minutes - var courtLocked: Bool = false - var freeCourt: Bool = false + func estimatedEndDate(includeBreakTime: Bool) -> Date { let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) return startDate.addingTimeInterval(minutesToAdd * 60.0) @@ -41,7 +39,6 @@ struct MatchDispatcher { let timedMatches: [TimeMatch] let freeCourtPerRotation: [Int: [Int]] let rotationCount: Int - let groupLastRotation: [Int: Int] } extension Match { @@ -59,7 +56,7 @@ class MatchScheduler { func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> GroupStageMatchDispatcher { - let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate } + let _groupStages = groupStages // Get the maximum count of matches in any group let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 @@ -176,19 +173,6 @@ class MatchScheduler { } } - func getAvailableCourt(inSlots slots: [TimeMatch], nextStartDate: Date) -> [TimeMatch] { - guard let minimumDuration = slots.compactMap({ $0.durationLeft }).min() else { return [] } - var newSlots = [TimeMatch]() - slots.forEach { timeMatch in - let durationLeft = timeMatch.durationLeft - if durationLeft - minimumDuration > 0 { - let timeMatch = TimeMatch(matchID: timeMatch.matchID, rotationIndex: timeMatch.rotationIndex + 1, courtIndex: timeMatch.courtIndex, groupIndex: timeMatch.groupIndex, startDate: nextStartDate, durationLeft: durationLeft, minimumBreakTime: timeMatch.minimumBreakTime, courtLocked: true) - newSlots.append(timeMatch) - } - } - return newSlots - } - func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() } @@ -207,37 +191,21 @@ class MatchScheduler { ) } - - - func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0, dispatcherStartDate: Date) -> MatchDispatcher { + func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, dispatcherStartDate: Date) -> MatchDispatcher { var slots = [TimeMatch]() var availableMatchs = flattenedMatches var rotationIndex = 0 var freeCourtPerRotation = [Int: [Int]]() - var groupLastRotation = [Int: Int]() var courts = [Int]() - var timeToAdd = 0.0 + while availableMatchs.count > 0 { freeCourtPerRotation[rotationIndex] = [] var matchPerRound = [Int: Int]() var availableCourt = numberOfCourtsAvailablePerRotation - if rotationIndex == 0 { - availableCourt = availableCourt - initialOccupiedCourt - } - courts = (0.. 0 { -// let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 }) -// rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate -// } else if freeCourtPreviousRotation > 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 }) -// if let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true) { -// rotationStartDate = previousEndDate -// courts = freeCourtPerRotation[rotationIndex - 1]! -// } -// } dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation) rotationIndex += 1 } -// var organizedSlots = [TimeMatch]() -// for i in 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, 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 { + 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, randomizeCourts: randomCourtDistribution, startDate: lastDate) + + scheduleSetup = true + + } + private func _save() { try? dataStore.tournaments.addOrUpdate(instance: tournament) } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index db3c14d..6214d5b 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -92,12 +92,18 @@ struct PlanningView: View { NavigationLink { MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle()) - } else if let round = match.roundObject { - Text(round.roundTitle()) + LabeledContent { + if let court = match.court { + Text(court) + } + } 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: { diff --git a/PadelClub/Views/Tournament/Shared/DateBoxView.swift b/PadelClub/Views/Tournament/Shared/DateBoxView.swift index f9377bf..1f03840 100644 --- a/PadelClub/Views/Tournament/Shared/DateBoxView.swift +++ b/PadelClub/Views/Tournament/Shared/DateBoxView.swift @@ -20,11 +20,12 @@ struct DateBoxView: View { .font(displayStyle == .wide ? .title : .title3) .monospacedDigit() } - Text(date.formatted(.dateTime.month(.abbreviated))) - .font(.caption2) - Text(date.formatted(.dateTime.year())) - .font(.caption2) - + if displayStyle == .wide { + Text(date.formatted(.dateTime.month(.abbreviated))) + .font(.caption2) + Text(date.formatted(.dateTime.year())) + .font(.caption2) + } } } } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index b796d6b..72a4f9d 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -28,7 +28,7 @@ struct TournamentCellView: View { private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View { HStack { - DateBoxView(date: tournament.startDate, displayStyle: displayStyle) + DateBoxView(date: tournament.startDate, displayStyle: .short) Rectangle() .fill(color) .frame(width: 2) From bb17216ec6cdf4ec0f2ae68191c2530291f782aa Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 13 Apr 2024 20:33:38 +0200 Subject: [PATCH 5/7] fix small bugs --- PadelClub/ViewModel/NavigationViewModel.swift | 1 + PadelClub/Views/Navigation/Agenda/ActivityView.swift | 9 +++------ .../Views/Tournament/Shared/TournamentCellView.swift | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/PadelClub/ViewModel/NavigationViewModel.swift b/PadelClub/ViewModel/NavigationViewModel.swift index b39d960..70669d8 100644 --- a/PadelClub/ViewModel/NavigationViewModel.swift +++ b/PadelClub/ViewModel/NavigationViewModel.swift @@ -9,6 +9,7 @@ import SwiftUI @Observable class NavigationViewModel { + var path = NavigationPath() var agendaDestination: AgendaDestination? = .activity var tournament: Tournament? } diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 9655ce0..3aec8bf 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -52,8 +52,8 @@ struct ActivityView: View { } var body: some View { - NavigationStack { - @Bindable var navigation = navigation + @Bindable var navigation = navigation + NavigationStack(path: $navigation.path) { VStack(spacing: 0) { GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) List { @@ -187,16 +187,13 @@ struct ActivityView: View { TournamentView() .environment(tournament) } - .navigationDestination(item: $navigation.tournament) { tournament in - TournamentView() - .environment(tournament) - } } } } private func _gatherFederalTournaments() { isGatheringFederalTournaments = true + NetworkFederalService.shared.formId = "" Task { do { try await dataStore.clubs.filter { $0.code != nil }.concurrentForEach { club in diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 72a4f9d..a1b6e89 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -28,7 +28,7 @@ struct TournamentCellView: View { private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View { HStack { - DateBoxView(date: tournament.startDate, displayStyle: .short) + DateBoxView(date: tournament.startDate, displayStyle: displayStyle == .wide ? .short : .wide) Rectangle() .fill(color) .frame(width: 2) @@ -56,7 +56,7 @@ struct TournamentCellView: View { Button { if let existingTournament { navigation.agendaDestination = .activity - navigation.tournament = existingTournament + navigation.path.append(existingTournament) } else { let event = federalTournament.getEvent() let newTournament = Tournament.newEmptyInstance() From 15323dbb4a423ffa8640802d2b972c678266b24d Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 15 Apr 2024 09:19:33 +0200 Subject: [PATCH 6/7] update scheduler and planning stuff --- PadelClub.xcodeproj/project.pbxproj | 4 + PadelClub/Data/Match.swift | 40 ++++ PadelClub/Data/Round.swift | 29 +++ PadelClub/Data/TeamRegistration.swift | 13 +- PadelClub/Data/Tournament.swift | 11 +- PadelClub/Extensions/Date+Extensions.swift | 6 + PadelClub/ViewModel/MatchScheduler.swift | 190 +++++++++++++++--- PadelClub/Views/Match/MatchSetupView.swift | 12 ++ .../LoserRoundScheduleEditorView.swift | 141 +++++++++++++ .../Planning/MatchScheduleEditorView.swift | 11 +- .../Views/Planning/PlanningSettingsView.swift | 85 +++++++- .../Planning/RoundScheduleEditorView.swift | 25 +-- PadelClub/Views/Planning/SchedulerView.swift | 33 ++- .../Screen/TournamentScheduleView.swift | 51 ++--- .../Tournament/TournamentRunningView.swift | 14 +- 15 files changed, 553 insertions(+), 112 deletions(-) create mode 100644 PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 0c8a95c..39363a7 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -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 = ""; }; FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundScheduleEditorView.swift; sourceTree = ""; }; FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageScheduleEditorView.swift; sourceTree = ""; }; + FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundScheduleEditorView.swift; sourceTree = ""; }; /* 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 */, diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 69baa1e..c3a9669 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -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 } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 9050978..cd0edcb 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -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 }) diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index d945a78..4171037 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -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 } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 63ecbed..26fe5d4 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -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 } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 9fae765..67e902f 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -181,3 +181,9 @@ extension Date { } } +extension Date { + func isEarlierThan(_ date: Date) -> Bool { + self < date + } +} + diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index b1471e4..3337951 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -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 = 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.. 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.. 0 while availableMatchs.count > 0 { freeCourtPerRotation[rotationIndex] = [] - var matchPerRound = [Int: Int]() - var availableCourt = numberOfCourtsAvailablePerRotation - courts = (0.. 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.. 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) } } diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 9b30547..6a26fd6 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -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) diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift new file mode 100644 index 0000000..50fc9d6 --- /dev/null +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -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()) + } + +} diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index 10fa53f..b1af2c8 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -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) } } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 5686f94..69e9559 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -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 diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index 17d85b8..d16ba98 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -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() { diff --git a/PadelClub/Views/Planning/SchedulerView.swift b/PadelClub/Views/Planning/SchedulerView.swift index fa25239..ab4a982 100644 --- a/PadelClub/Views/Planning/SchedulerView.swift +++ b/PadelClub/Views/Planning/SchedulerView.swift @@ -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) } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 83795fc..93b3881 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -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) } } } diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index 8e4312f..9f8209b 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -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") + } } } } From e01198fdc9e7f4f2ba5558b833bee8b4f4e49bba Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 16 Apr 2024 10:26:07 +0200 Subject: [PATCH 7/7] fix april 2024 issue fix format C issue fix goalaverage issue fix search issue --- PadelClub/Data/Coredata/Persistence.swift | 31 +++++++++++++++++++- PadelClub/Data/GroupStage.swift | 11 ++++++- PadelClub/Data/Match.swift | 6 ++-- PadelClub/Extensions/String+Extensions.swift | 9 ++++++ PadelClub/ViewModel/SearchViewModel.swift | 2 +- PadelClub/Views/Score/SetInputView.swift | 2 +- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/PadelClub/Data/Coredata/Persistence.swift b/PadelClub/Data/Coredata/Persistence.swift index e5953b6..56ac96e 100644 --- a/PadelClub/Data/Coredata/Persistence.swift +++ b/PadelClub/Data/Coredata/Persistence.swift @@ -99,6 +99,7 @@ class PersistenceController: NSObject { func batchInsertPlayers(_ importedPlayers: [FederalPlayer], importingDate: Date) async { guard !importedPlayers.isEmpty else { return } let context = newTaskContext() + context.performAndWait { context.transactionAuthor = PersistenceController.remoteDataImportAuthorName @@ -121,7 +122,16 @@ class PersistenceController: NSObject { // 1 var index = 0 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 let batchInsert = NSBatchInsertRequest( entity: ImportedPlayer.entity()) { (managedObject: NSManagedObject) -> Bool in @@ -133,15 +143,34 @@ class PersistenceController: NSObject { let data = imported[index] importedPlayer.license = data.license importedPlayer.ligueName = data.ligue + if fixApril2024 { + importedPlayer.ligueName?.replace(characters: replacementsCharacters) + } + importedPlayer.rank = Int64(data.rank) importedPlayer.points = data.points ?? 0 importedPlayer.assimilation = data.assimilation importedPlayer.country = data.country + if fixApril2024 { + importedPlayer.country?.replace(characters: replacementsCharacters) + } importedPlayer.tournamentCount = Int64(data.tournamentCount ?? 0) importedPlayer.lastName = data.lastName + if fixApril2024 { + importedPlayer.lastName?.replace(characters: replacementsCharacters) + } importedPlayer.firstName = data.firstName + if fixApril2024 { + importedPlayer.firstName?.replace(characters: replacementsCharacters) + } importedPlayer.fullName = data.firstName + " " + data.lastName + if fixApril2024 { + importedPlayer.fullName?.replace(characters: replacementsCharacters) + } importedPlayer.clubName = data.club + if fixApril2024 { + importedPlayer.clubName?.replace(characters: replacementsCharacters) + } importedPlayer.clubCode = data.clubCode.replaceCharactersFromSet(characterSet: .whitespaces) importedPlayer.male = data.isMale importedPlayer.importDate = importingDate diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index c703675..edca863 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -98,7 +98,16 @@ class GroupStage: ModelObject, Storable { func scoreLabel(forGroupStagePosition groupStagePosition: Int) -> String? { 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 { return nil } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index c3a9669..ae76ae3 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -423,12 +423,12 @@ class Match: ModelObject, Storable { 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()) var setDifference : Int = 0 - if endedSetsOne.count == 1 { + let zip = zip(endedSetsOne, endedSetsTwo) + if matchFormat.setsToWin == 1 { setDifference = endedSetsOne[0] - endedSetsTwo[0] } 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,+) return (setDifference * reverseValue, gameDifference * reverseValue) } diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 10f3a0e..9eca94f 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -141,3 +141,12 @@ extension String { extension LosslessStringConvertible { 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 }) + } + } +} diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 5a03c11..6e4d80c 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -118,7 +118,7 @@ class SearchViewModel: ObservableObject, Identifiable { } func wordsPredicates() -> NSPredicate? { - let words = words().filter({ $0.isEmpty }) + let words = words().filter({ $0.isEmpty == false }) switch words.count { case 2: let predicates = [ diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift index 724c1b1..ebcea2f 100644 --- a/PadelClub/Views/Score/SetInputView.swift +++ b/PadelClub/Views/Score/SetInputView.swift @@ -95,7 +95,7 @@ struct SetInputView: View { return [6,5] } if valueTeamOne == 5 && setFormat == .four { - return [3,2] + return [4,3] } } return setFormat.possibleValues