multistore
Razmig Sarkissian 2 years ago
parent 10df57a72e
commit ebac3974c9
  1. 10
      PadelClub/Manager/PadelRule.swift
  2. 86
      PadelClub/ViewModel/MatchScheduler.swift
  3. 3
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  4. 80
      PadelClub/Views/Planning/PlanningSettingsView.swift
  5. 6
      PadelClub/Views/Planning/PlanningView.swift
  6. 3
      PadelClub/Views/Tournament/Shared/DateBoxView.swift
  7. 2
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

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

@ -18,12 +18,10 @@ struct TimeMatch {
let matchID: String let matchID: String
let rotationIndex: Int let rotationIndex: Int
var courtIndex: Int var courtIndex: Int
let groupIndex: Int
var startDate: Date var startDate: Date
var durationLeft: Int //in minutes var durationLeft: Int //in minutes
var minimumBreakTime: Int //in minutes var minimumBreakTime: Int //in minutes
var courtLocked: Bool = false
var freeCourt: Bool = false
func estimatedEndDate(includeBreakTime: Bool) -> Date { func estimatedEndDate(includeBreakTime: Bool) -> Date {
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
return startDate.addingTimeInterval(minutesToAdd * 60.0) return startDate.addingTimeInterval(minutesToAdd * 60.0)
@ -41,7 +39,6 @@ struct MatchDispatcher {
let timedMatches: [TimeMatch] let timedMatches: [TimeMatch]
let freeCourtPerRotation: [Int: [Int]] let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int let rotationCount: Int
let groupLastRotation: [Int: Int]
} }
extension Match { extension Match {
@ -59,7 +56,7 @@ class MatchScheduler {
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> GroupStageMatchDispatcher { 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 // Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
@ -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? { func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
} }
@ -207,37 +191,21 @@ class MatchScheduler {
) )
} }
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, dispatcherStartDate: Date) -> MatchDispatcher {
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0, dispatcherStartDate: Date) -> MatchDispatcher {
var slots = [TimeMatch]() var slots = [TimeMatch]()
var availableMatchs = flattenedMatches var availableMatchs = flattenedMatches
var rotationIndex = 0 var rotationIndex = 0
var freeCourtPerRotation = [Int: [Int]]() var freeCourtPerRotation = [Int: [Int]]()
var groupLastRotation = [Int: Int]()
var courts = [Int]() var courts = [Int]()
var timeToAdd = 0.0
while availableMatchs.count > 0 { while availableMatchs.count > 0 {
freeCourtPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = []
var matchPerRound = [Int: Int]() var matchPerRound = [Int: Int]()
var availableCourt = numberOfCourtsAvailablePerRotation var availableCourt = numberOfCourtsAvailablePerRotation
if rotationIndex == 0 {
availableCourt = availableCourt - initialOccupiedCourt
}
courts = (0..<availableCourt).map { $0 } courts = (0..<availableCourt).map { $0 }
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate 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 }
// })
courts.sort() courts.sort()
print("courts available at rotation \(rotationIndex)", courts) print("courts available at rotation \(rotationIndex)", courts)
print("rotationStartDate", rotationStartDate) print("rotationStartDate", rotationStartDate)
@ -270,35 +238,24 @@ class MatchScheduler {
} }
} }
// 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) dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation)
rotationIndex += 1 rotationIndex += 1
} }
// var organizedSlots = [TimeMatch]() var organizedSlots = [TimeMatch]()
// for i in 0..<rotationIndex { for i in 0..<rotationIndex {
// let courtsSorted = slots.filter({ $0.rotationIndex == i && $0.courtLocked == false }).map { $0.courtIndex }.sorted() let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
// let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
// var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex))
//
// for j in 0..<matches.count { for j in 0..<matches.count {
// matches[j].courtIndex = courts[j] matches[j].courtIndex = courts[j]
// organizedSlots.append(matches[j]) organizedSlots.append(matches[j])
// } }
// } }
//
//
return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex)
} }
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) { func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) {
@ -333,7 +290,7 @@ class MatchScheduler {
matchPerRound[first.roundObject!.index] = 1 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) let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime)
slots.append(timeMatch) slots.append(timeMatch)
availableMatchs.removeAll(where: { $0.id == first.id }) availableMatchs.removeAll(where: { $0.id == first.id })
} else { } else {
@ -344,9 +301,6 @@ class MatchScheduler {
if freeCourtPerRotation[rotationIndex]!.count == availableCourts { if freeCourtPerRotation[rotationIndex]!.count == availableCourts {
freeCourtPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = []
let courtsUsed = getNextEarliestAvailableDate(from: slots) let courtsUsed = getNextEarliestAvailableDate(from: slots)
print(courtsUsed)
let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
availableDate <= minimumTargetedEndDate availableDate <= minimumTargetedEndDate
}.sorted(by: \.1).map { $0.0 } }.sorted(by: \.1).map { $0.0 }
@ -380,7 +334,7 @@ class MatchScheduler {
flattenedMatches.forEach({ $0.startDate = nil }) flattenedMatches.forEach({ $0.startDate = nil })
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, initialOccupiedCourt: 0, dispatcherStartDate: startDate) let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, dispatcherStartDate: startDate)
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 }) {

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

@ -55,42 +55,7 @@ struct PlanningSettingsView: View {
} }
RowButtonView("Horaire intelligent", role: .destructive) { RowButtonView("Horaire intelligent", role: .destructive) {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 _setupSchedule()
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = min(tournament.courtCount, groupStageCourtCount * groupStages.count)
let matchScheduler = MatchScheduler.shared
let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil })
var times = Set(groupStages.compactMap { $0.startDate })
if times.isEmpty {
groupStages.forEach({ $0.startDate = tournament.startDate })
times.insert(tournament.startDate)
try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
}
var lastDate : Date? = nil
times.forEach { time in
let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groupStages, startingDate: time, randomizeCourts: randomCourtDistribution)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd)
lastDate = match.startDate?.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex + 1)
}
}
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, randomizeCourts: randomCourtDistribution, startDate: lastDate ?? tournament.startDate)
scheduleSetup = true
} }
if scheduleSetup { if scheduleSetup {
@ -126,6 +91,49 @@ struct PlanningSettingsView: View {
} }
} }
private func _setupSchedule() {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matchScheduler = MatchScheduler.shared
let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil })
// var times = Set(groupStages.compactMap { $0.startDate }.filter { $0 >= tournament.startDate } )
// if times.isEmpty {
// groupStages.forEach({ $0.startDate = tournament.startDate })
// times.insert(tournament.startDate)
// try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
// }
var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate })
try? dataStore.groupStages.addOrUpdate(contentOfs: groups)
let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate, 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() { private func _save() {
try? dataStore.tournaments.addOrUpdate(instance: tournament) try? dataStore.tournaments.addOrUpdate(instance: tournament)
} }

@ -91,6 +91,11 @@ struct PlanningView: View {
ForEach(_matches) { match in ForEach(_matches) { match in
NavigationLink { NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: {
LabeledContent {
if let court = match.court {
Text(court)
}
} label: { } label: {
if let groupStage = match.groupStageObject { if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle()) Text(groupStage.groupStageTitle())
@ -100,6 +105,7 @@ struct PlanningView: View {
Text(match.matchTitle()) Text(match.matchTitle())
} }
} }
}
} label: { } label: {
_timeSlotView(key: key, matches: _matches) _timeSlotView(key: key, matches: _matches)
} }

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

@ -28,7 +28,7 @@ struct TournamentCellView: View {
private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View { private func _buildView(_ build: any TournamentBuildHolder, existingTournament: Tournament?) -> some View {
HStack { HStack {
DateBoxView(date: tournament.startDate, displayStyle: displayStyle) DateBoxView(date: tournament.startDate, displayStyle: .short)
Rectangle() Rectangle()
.fill(color) .fill(color)
.frame(width: 2) .frame(width: 2)

Loading…
Cancel
Save