parent
faaeb416c9
commit
512cad69e5
@ -0,0 +1,251 @@ |
||||
// |
||||
// MatchScheduler.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 08/04/2024. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
struct TimeMatch { |
||||
let matchID: String |
||||
let rotationIndex: Int |
||||
var courtIndex: Int |
||||
let groupIndex: Int |
||||
} |
||||
|
||||
struct MatchDispatcher { |
||||
let timedMatches: [TimeMatch] |
||||
let freeCourtPerRotation: [Int: [Int]] |
||||
let rotationCount: Int |
||||
let groupLastRotation: [Int: Int] |
||||
} |
||||
|
||||
extension Match { |
||||
func teamIds() -> [String] { |
||||
return teams().map { $0.id } |
||||
} |
||||
|
||||
func containsTeamId(_ id: String) -> Bool { |
||||
teamIds().contains(id) |
||||
} |
||||
} |
||||
|
||||
class MatchScheduler { |
||||
static let shared = MatchScheduler() |
||||
|
||||
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> MatchDispatcher { |
||||
|
||||
let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate } |
||||
|
||||
// Get the maximum count of matches in any group |
||||
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 |
||||
|
||||
// Use zip and flatMap to flatten matches in the desired order |
||||
let flattenedMatches = (0..<maxMatchesCount).flatMap { index in |
||||
_groupStages.compactMap { group in |
||||
// Use optional subscript to safely access matches |
||||
let playedMatches = group.playedMatches() |
||||
return playedMatches.indices.contains(index) ? playedMatches[index] : nil |
||||
} |
||||
} |
||||
|
||||
var slots = [TimeMatch]() |
||||
var availableMatchs = flattenedMatches |
||||
var rotationIndex = 0 |
||||
var teamsPerRotation = [Int: [String]]() |
||||
var freeCourtPerRotation = [Int: [Int]]() |
||||
var groupLastRotation = [Int: Int]() |
||||
|
||||
while slots.count < flattenedMatches.count { |
||||
teamsPerRotation[rotationIndex] = [] |
||||
freeCourtPerRotation[rotationIndex] = [] |
||||
let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }.map { ($0.groupIndex, 1) } |
||||
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +) |
||||
var rotationMatches = Array(availableMatchs.filter({ match in |
||||
teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true |
||||
}).prefix(numberOfCourtsAvailablePerRotation)) |
||||
|
||||
if rotationIndex > 0 { |
||||
rotationMatches = rotationMatches.sorted(by: { |
||||
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { |
||||
return $0.groupStageObject!.index < $1.groupStageObject!.index |
||||
} else { |
||||
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 |
||||
} |
||||
}) |
||||
} |
||||
|
||||
(0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in |
||||
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) |
||||
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 )) |
||||
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) |
||||
rotationMatches.removeAll(where: { $0.id == first.id }) |
||||
availableMatchs.removeAll(where: { $0.id == first.id }) |
||||
if let index = first.groupStageObject?.index { |
||||
groupLastRotation[index] = rotationIndex |
||||
} |
||||
} else { |
||||
freeCourtPerRotation[rotationIndex]!.append(courtIndex) |
||||
} |
||||
} |
||||
|
||||
rotationIndex += 1 |
||||
} |
||||
|
||||
var organizedSlots = [TimeMatch]() |
||||
for i in 0..<rotationIndex { |
||||
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() |
||||
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted |
||||
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) |
||||
|
||||
for j in 0..<matches.count { |
||||
matches[j].courtIndex = courts[j] |
||||
organizedSlots.append(matches[j]) |
||||
} |
||||
} |
||||
|
||||
|
||||
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) |
||||
} |
||||
|
||||
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int) -> Bool { |
||||
print(roundObject.roundTitle(), match.matchTitle()) |
||||
let previousMatches = roundObject.precedentMatches(ofMatch: match) |
||||
if previousMatches.isEmpty { return true } |
||||
|
||||
let previousMatchSlots = slots.filter({ slot in |
||||
previousMatches.map { $0.id }.contains(slot.matchID) |
||||
}) |
||||
|
||||
if previousMatchSlots.isEmpty { |
||||
if previousMatches.filter({ $0.disabled == false }).allSatisfy({ $0.startDate != nil }) { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count { |
||||
if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + (roundObject.loser == nil ? 1 : 0) < rotationIndex }) |
||||
return previousMatchIsInPreviousRotation |
||||
|
||||
} |
||||
|
||||
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0) -> MatchDispatcher { |
||||
|
||||
var slots = [TimeMatch]() |
||||
var availableMatchs = flattenedMatches |
||||
var rotationIndex = 0 |
||||
var freeCourtPerRotation = [Int: [Int]]() |
||||
var groupLastRotation = [Int: Int]() |
||||
|
||||
while slots.count < flattenedMatches.count { |
||||
freeCourtPerRotation[rotationIndex] = [] |
||||
var matchPerRound = [Int: Int]() |
||||
var availableCourt = numberOfCourtsAvailablePerRotation |
||||
if rotationIndex == 0 { |
||||
availableCourt = availableCourt - initialOccupiedCourt |
||||
} |
||||
(0..<availableCourt).forEach { courtIndex in |
||||
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) |
||||
|
||||
if let first = availableMatchs.first(where: { match in |
||||
let roundObject = match.roundObject! |
||||
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex) |
||||
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, numberOfCourtsAvailablePerRotation > 1, let nextMatch = match.next() { |
||||
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex) { |
||||
return true |
||||
} else { |
||||
return false |
||||
} |
||||
} |
||||
|
||||
if (matchPerRound[roundObject.index] ?? 0)%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == numberOfCourtsAvailablePerRotation - 1 { |
||||
return false |
||||
} |
||||
|
||||
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 |
||||
} |
||||
} |
||||
slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index )) |
||||
availableMatchs.removeAll(where: { $0.id == first.id }) |
||||
if let index = first.roundObject?.index { |
||||
groupLastRotation[index] = rotationIndex |
||||
} |
||||
} else { |
||||
freeCourtPerRotation[rotationIndex]!.append(courtIndex) |
||||
} |
||||
} |
||||
|
||||
rotationIndex += 1 |
||||
} |
||||
|
||||
var organizedSlots = [TimeMatch]() |
||||
for i in 0..<rotationIndex { |
||||
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() |
||||
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted |
||||
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) |
||||
|
||||
for j in 0..<matches.count { |
||||
matches[j].courtIndex = courts[j] |
||||
organizedSlots.append(matches[j]) |
||||
} |
||||
} |
||||
|
||||
|
||||
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) |
||||
} |
||||
|
||||
func updateSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, randomizeCourts: Bool, startDate: Date) { |
||||
|
||||
let upperRounds = tournament.rounds() |
||||
var roundIndex = 0 |
||||
if let roundId { |
||||
roundIndex = upperRounds.firstIndex(where: { $0.id == roundId }) ?? 0 |
||||
} |
||||
|
||||
let rounds = upperRounds.flatMap { |
||||
[$0] + $0.loserRoundsAndChildren() |
||||
} |
||||
|
||||
var flattenedMatches = rounds[roundIndex...].flatMap { round in |
||||
round._matches().filter({ $0.disabled == false }).sorted(by: \.index) |
||||
} |
||||
|
||||
if let matchId, let matchIndex = flattenedMatches.firstIndex(where: { $0.id == matchId }) { |
||||
flattenedMatches = Array(flattenedMatches[matchIndex...]) |
||||
} |
||||
|
||||
flattenedMatches.forEach({ $0.startDate = nil }) |
||||
|
||||
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, initialOccupiedCourt: 0) |
||||
|
||||
roundDispatch.timedMatches.forEach { matchSchedule in |
||||
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { |
||||
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 |
||||
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd) |
||||
match.setCourt(matchSchedule.courtIndex + 1) |
||||
} |
||||
} |
||||
|
||||
try? DataStore.shared.matches.addOrUpdate(contentOfs: flattenedMatches) |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,48 @@ |
||||
// |
||||
// GroupStageScheduleEditorView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 07/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct GroupStageScheduleEditorView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
var groupStage: GroupStage |
||||
|
||||
var body: some View { |
||||
@Bindable var groupStage = groupStage |
||||
List { |
||||
Section { |
||||
MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat) |
||||
} |
||||
|
||||
Section { |
||||
Text("Modifier l'horaire") |
||||
} |
||||
|
||||
RowButtonView("Convoquer") { |
||||
|
||||
} |
||||
|
||||
NavigationLink { |
||||
GroupStageView(groupStage: groupStage) |
||||
} label: { |
||||
Text("Voir la poule") |
||||
} |
||||
} |
||||
.onChange(of: groupStage.matchFormat) { |
||||
_save() |
||||
} |
||||
} |
||||
|
||||
|
||||
private func _save() { |
||||
try? dataStore.groupStages.addOrUpdate(instance: groupStage) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
GroupStageScheduleEditorView(groupStage: GroupStage.mock()) |
||||
} |
||||
@ -0,0 +1,40 @@ |
||||
// |
||||
// MatchScheduleEditorView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 10/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct MatchScheduleEditorView: View { |
||||
@Environment(Tournament.self) var tournament: Tournament |
||||
var match: Match |
||||
@State private var startDate: Date |
||||
|
||||
init(match: Match) { |
||||
self.match = match |
||||
self._startDate = State(wrappedValue: match.startDate ?? Date()) |
||||
} |
||||
|
||||
var body: some View { |
||||
Section { |
||||
DatePicker(selection: $startDate) { |
||||
Text(startDate.formatted(.dateTime.weekday())) |
||||
} |
||||
RowButtonView("Modifier") { |
||||
_updateSchedule() |
||||
} |
||||
} header: { |
||||
Text(match.matchTitle()) |
||||
} |
||||
} |
||||
|
||||
private func _updateSchedule() { |
||||
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, randomizeCourts: true, startDate: startDate) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
MatchScheduleEditorView(match: Match.mock()) |
||||
} |
||||
@ -0,0 +1,159 @@ |
||||
// |
||||
// PlanningSettingsView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 07/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
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 groupStageCourtCount: Int |
||||
|
||||
init(tournament: Tournament) { |
||||
self.tournament = tournament |
||||
self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1) |
||||
} |
||||
|
||||
var body: some View { |
||||
@Bindable var tournament = tournament |
||||
List { |
||||
Section { |
||||
DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate) |
||||
Stepper(value: $tournament.dayDuration, in: 1...1_000) { |
||||
HStack { |
||||
Text("Durée") |
||||
Spacer() |
||||
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) |
||||
} |
||||
} |
||||
} header: { |
||||
Text("Démarrage et durée du tournoi") |
||||
} footer: { |
||||
Text("todo: Expliquer ce que ca fait") |
||||
} |
||||
|
||||
Section { |
||||
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) |
||||
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount) |
||||
|
||||
NavigationLink { |
||||
|
||||
} label: { |
||||
Text("Disponibilité des terrains") |
||||
} |
||||
} |
||||
|
||||
Section { |
||||
|
||||
Toggle(isOn: $randomCourtDistribution) { |
||||
Text("Distribuer les terrains au hasard") |
||||
} |
||||
|
||||
RowButtonView("Horaire intelligent", role: .destructive) { |
||||
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 |
||||
let groupStages = tournament.groupStages() |
||||
let numberOfCourtsAvailablePerRotation: Int = min(tournament.courtCount, groupStageCourtCount * groupStages.count) |
||||
|
||||
let matchScheduler = MatchScheduler.shared |
||||
|
||||
let matches = tournament.groupStages().flatMap({ $0._matches() }) |
||||
matches.forEach({ $0.startDate = nil }) |
||||
|
||||
var times = Set(groupStages.compactMap { $0.startDate }) |
||||
if times.isEmpty { |
||||
groupStages.forEach({ $0.startDate = tournament.startDate }) |
||||
times.insert(tournament.startDate) |
||||
try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) |
||||
} |
||||
|
||||
var lastDate : Date? = nil |
||||
times.forEach { time in |
||||
let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groupStages, startingDate: time, randomizeCourts: randomCourtDistribution) |
||||
|
||||
dispatch.timedMatches.forEach { matchSchedule in |
||||
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { |
||||
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 |
||||
if let startDate = match.groupStageObject?.startDate { |
||||
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd) |
||||
lastDate = match.startDate?.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60) |
||||
} |
||||
match.setCourt(matchSchedule.courtIndex + 1) |
||||
} |
||||
} |
||||
} |
||||
try? dataStore.matches.addOrUpdate(contentOfs: matches) |
||||
let upperRounds = tournament.rounds() |
||||
let rounds = upperRounds.flatMap { |
||||
[$0] + $0.loserRoundsAndChildren() |
||||
} |
||||
|
||||
let flattenedMatches = rounds.flatMap { round in |
||||
round._matches().filter({ $0.disabled == false }).sorted(by: \.index) |
||||
} |
||||
|
||||
flattenedMatches.forEach({ $0.startDate = nil }) |
||||
|
||||
let roundDispatch = matchScheduler.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomCourtDistribution) |
||||
|
||||
roundDispatch.timedMatches.forEach { matchSchedule in |
||||
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { |
||||
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60 |
||||
if let lastDate { |
||||
match.startDate = lastDate.addingTimeInterval(timeIntervalToAdd) |
||||
} |
||||
match.setCourt(matchSchedule.courtIndex + 1) |
||||
} |
||||
} |
||||
|
||||
try? dataStore.matches.addOrUpdate(contentOfs: flattenedMatches) |
||||
|
||||
|
||||
scheduleSetup = true |
||||
} |
||||
|
||||
if scheduleSetup { |
||||
HStack { |
||||
Image(systemName: "checkmark") |
||||
} |
||||
} |
||||
} |
||||
|
||||
Section { |
||||
NavigationLink { |
||||
|
||||
} label: { |
||||
Text("Modifier le message de convocation") |
||||
} |
||||
} |
||||
} |
||||
.onChange(of: groupStageCourtCount) { |
||||
tournament.groupStageCourtCount = groupStageCourtCount |
||||
_save() |
||||
} |
||||
.onChange(of: tournament.startDate) { |
||||
_save() |
||||
} |
||||
.onChange(of: tournament.courtCount) { |
||||
_save() |
||||
} |
||||
.onChange(of: tournament.groupStageCourtCount) { |
||||
_save() |
||||
} |
||||
.onChange(of: tournament.dayDuration) { |
||||
_save() |
||||
} |
||||
} |
||||
|
||||
private func _save() { |
||||
try? dataStore.tournaments.addOrUpdate(instance: tournament) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
PlanningSettingsView(tournament: Tournament.mock()) |
||||
} |
||||
@ -0,0 +1,149 @@ |
||||
// |
||||
// PlanningView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 07/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct PlanningView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
@Environment(\.editMode) private var editMode |
||||
|
||||
let matches: [Match] |
||||
@State private var timeSlots: [Date:[Match]] |
||||
@State private var days: [Date] |
||||
@State private var keys: [Date] |
||||
|
||||
init(matches: [Match]) { |
||||
self.matches = matches |
||||
let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } |
||||
_timeSlots = State(wrappedValue: timeSlots) |
||||
_days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted()) |
||||
_keys = State(wrappedValue: timeSlots.keys.sorted()) |
||||
} |
||||
|
||||
var body: some View { |
||||
List { |
||||
ForEach(days, id: \.self) { day in |
||||
Section { |
||||
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in |
||||
if let _matches = timeSlots[key] { |
||||
if editMode?.wrappedValue.isEditing == true { |
||||
HStack { |
||||
VStack(alignment: .leading) { |
||||
let index = keys.firstIndex(of: key) |
||||
Button { |
||||
let previousKey = keys[index! - 1] |
||||
let previousMatches = timeSlots[previousKey] |
||||
previousMatches?.forEach { match in |
||||
match.startDate = key |
||||
} |
||||
_matches.forEach { match in |
||||
match.startDate = previousKey |
||||
} |
||||
_update() |
||||
} label: { |
||||
Image(systemName: "arrow.up") |
||||
} |
||||
.buttonStyle(.bordered) |
||||
.disabled(index == 0) |
||||
Button { |
||||
let nextKey = keys[index! + 1] |
||||
let nextMatches = timeSlots[nextKey] |
||||
nextMatches?.forEach { match in |
||||
match.startDate = key |
||||
} |
||||
_matches.forEach { match in |
||||
match.startDate = nextKey |
||||
} |
||||
_update() |
||||
} label: { |
||||
Image(systemName: "arrow.down") |
||||
} |
||||
.buttonStyle(.bordered) |
||||
.disabled(index == keys.count - 1) |
||||
} |
||||
VStack(alignment: .leading) { |
||||
LabeledContent { |
||||
Text(_matches.count.formatted() + " match" + _matches.count.pluralSuffix) |
||||
} label: { |
||||
Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle) |
||||
} |
||||
|
||||
ForEach(_matches) { match in |
||||
LabeledContent { |
||||
|
||||
} label: { |
||||
if let groupStage = match.groupStageObject { |
||||
Text(groupStage.groupStageTitle()) |
||||
} else if let round = match.roundObject { |
||||
Text(round.roundTitle()) |
||||
} |
||||
Text(match.matchTitle()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
DisclosureGroup { |
||||
ForEach(_matches) { match in |
||||
NavigationLink { |
||||
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) |
||||
} label: { |
||||
if let groupStage = match.groupStageObject { |
||||
Text(groupStage.groupStageTitle()) |
||||
} else if let round = match.roundObject { |
||||
Text(round.roundTitle()) |
||||
} |
||||
Text(match.matchTitle()) |
||||
} |
||||
} |
||||
} label: { |
||||
_timeSlotView(key: key, matches: _matches) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} header: { |
||||
Text(day.formatted(.dateTime.day().weekday().month().year())) |
||||
} |
||||
.headerProminence(.increased) |
||||
} |
||||
} |
||||
.toolbar { |
||||
EditButton() |
||||
} |
||||
.onChange(of: isEditing) { old, new in |
||||
if old == true && new == false { |
||||
print("save") |
||||
try? dataStore.matches.addOrUpdate(contentOfs: matches) |
||||
} |
||||
} |
||||
.navigationTitle("Programmation") |
||||
} |
||||
|
||||
private func _update() { |
||||
let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } |
||||
self.timeSlots = timeSlots |
||||
self.days = Set(timeSlots.keys.map { $0.startOfDay }).sorted() |
||||
self.keys = timeSlots.keys.sorted() |
||||
} |
||||
|
||||
private var isEditing: Bool { |
||||
editMode?.wrappedValue.isEditing == true |
||||
} |
||||
|
||||
private func _timeSlotView(key: Date, matches: [Match]) -> some View { |
||||
LabeledContent { |
||||
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) |
||||
} label: { |
||||
Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle) |
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
PlanningView(matches: []) |
||||
} |
||||
@ -0,0 +1,58 @@ |
||||
// |
||||
// RoundScheduleEditorView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 07/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct RoundScheduleEditorView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
@Environment(Tournament.self) var tournament: Tournament |
||||
|
||||
var round: Round |
||||
@State private var startDate: Date |
||||
|
||||
init(round: Round) { |
||||
self.round = round |
||||
self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date()) |
||||
} |
||||
|
||||
var body: some View { |
||||
@Bindable var round = round |
||||
List { |
||||
Section { |
||||
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) |
||||
} |
||||
|
||||
Section { |
||||
DatePicker(selection: $startDate) { |
||||
Text(startDate.formatted(.dateTime.weekday())) |
||||
} |
||||
RowButtonView("Modifier") { |
||||
_updateSchedule() |
||||
} |
||||
} |
||||
|
||||
ForEach(round.playedMatches()) { match in |
||||
MatchScheduleEditorView(match: match) |
||||
} |
||||
} |
||||
.onChange(of: round.matchFormat) { |
||||
_save() |
||||
} |
||||
} |
||||
|
||||
private func _updateSchedule() { |
||||
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, randomizeCourts: true, startDate: startDate) |
||||
} |
||||
|
||||
private func _save() { |
||||
try? dataStore.rounds.addOrUpdate(instance: round) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
RoundScheduleEditorView(round: Round.mock()) |
||||
} |
||||
@ -0,0 +1,66 @@ |
||||
// |
||||
// SchedulerView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 07/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
extension GroupStage: Schedulable {} |
||||
extension Round: Schedulable {} |
||||
|
||||
struct SchedulerView: View { |
||||
var tournament: Tournament |
||||
|
||||
var body: some View { |
||||
List { |
||||
ForEach(tournament.groupStages()) { |
||||
_schedulerView($0) |
||||
} |
||||
ForEach(tournament.rounds()) { round in |
||||
_schedulerView(round) |
||||
ForEach(round.loserRoundsAndChildren()) { loserRound in |
||||
if round.isDisabled() == false { |
||||
_schedulerView(loserRound) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.headerProminence(.increased) |
||||
} |
||||
|
||||
func _schedulerView(_ schedulable: any Schedulable) -> some View { |
||||
Section { |
||||
NavigationLink { |
||||
Group { |
||||
if let round = schedulable as? Round { |
||||
RoundScheduleEditorView(round: round) |
||||
.environment(tournament) |
||||
} |
||||
else if let groupStage = schedulable as? GroupStage { |
||||
GroupStageScheduleEditorView(groupStage: groupStage) |
||||
} |
||||
} |
||||
.navigationTitle(schedulable.selectionLabel()) |
||||
} label: { |
||||
LabeledContent { |
||||
Text(schedulable.matchFormat.format).font(.largeTitle) |
||||
} label: { |
||||
if let startDate = schedulable.startDate { |
||||
Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle) |
||||
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) |
||||
} else { |
||||
Text("Aucun horaire") |
||||
} |
||||
} |
||||
} |
||||
} header: { |
||||
Text(schedulable.selectionLabel()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
SchedulerView(tournament: Tournament.mock()) |
||||
} |
||||
@ -0,0 +1,91 @@ |
||||
// |
||||
// TournamentScheduleView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 07/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
protocol Schedulable: Selectable, Identifiable { |
||||
var startDate: Date? { get set } |
||||
var matchFormat: MatchFormat { get set } |
||||
func playedMatches() -> [Match] |
||||
} |
||||
|
||||
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) |
||||
} |
||||
} |
||||
|
||||
case planning |
||||
case schedule |
||||
case groupStage(GroupStage) |
||||
case round(Round) |
||||
|
||||
func selectionLabel() -> String { |
||||
switch self { |
||||
case .schedule: |
||||
return "Horaires" |
||||
case .planning: |
||||
return "Progr." |
||||
case .groupStage(let groupStage): |
||||
return groupStage.selectionLabel() |
||||
case .round(let round): |
||||
return round.selectionLabel() |
||||
} |
||||
} |
||||
|
||||
func badgeValue() -> Int? { |
||||
nil |
||||
} |
||||
} |
||||
|
||||
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 body: some View { |
||||
VStack(spacing: 0) { |
||||
GenericDestinationPickerView(selectedDestination: $selectedScheduleDestination, destinations: allDestinations, nilDestinationIsValid: true) |
||||
switch selectedScheduleDestination { |
||||
case .none: |
||||
PlanningSettingsView(tournament: tournament) |
||||
.navigationTitle("Réglages") |
||||
case .some(let selectedSchedule): |
||||
switch selectedSchedule { |
||||
case .groupStage(let groupStage): |
||||
Text("ok") |
||||
case .round(let round): |
||||
Text("ok") |
||||
case .planning: |
||||
PlanningView(matches: tournament.allMatches()) |
||||
case .schedule: |
||||
SchedulerView(tournament: tournament) |
||||
} |
||||
} |
||||
} |
||||
.navigationBarTitleDisplayMode(.inline) |
||||
.toolbarBackground(.visible, for: .navigationBar) |
||||
.navigationTitle("Horaires") |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
TournamentScheduleView(tournament: Tournament.mock()) |
||||
} |
||||
Loading…
Reference in new issue