fix stuff around stage

multistore
Razmig Sarkissian 2 years ago
parent 69dcc5a16d
commit bcd71ffa06
  1. 16
      PadelClub/Data/Round.swift
  2. 17
      PadelClub/Extensions/Date+Extensions.swift
  3. 48
      PadelClub/ViewModel/MatchScheduler.swift
  4. 2
      PadelClub/Views/Calling/CallView.swift
  5. 4
      PadelClub/Views/Components/RowButtonView.swift
  6. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  7. 6
      PadelClub/Views/Match/MatchSummaryView.swift
  8. 90
      PadelClub/Views/Planning/Components/DateUpdateManagerView.swift
  9. 15
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  10. 55
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  11. 50
      PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift
  12. 32
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  13. 36
      PadelClub/Views/Planning/PlanningSettingsView.swift
  14. 21
      PadelClub/Views/Planning/PlanningView.swift
  15. 37
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  16. 59
      PadelClub/Views/Planning/SchedulerView.swift
  17. 6
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift

@ -285,6 +285,18 @@ class Round: ModelObject, Storable {
} }
} }
func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? {
enabledMatches().last?.estimatedEndDate(additionalEstimationDuration)
}
func getLoserRoundStartDate() -> Date? {
loserRoundsAndChildren().first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate
}
func estimatedLoserRoundEndDate(_ additionalEstimationDuration: Int) -> Date? {
let lastMatch = loserRoundsAndChildren().last(where: { $0.isDisabled() == false })?.enabledMatches().last
return lastMatch?.estimatedEndDate(additionalEstimationDuration)
}
func disabledMatches() -> [Match] { func disabledMatches() -> [Match] {
_matches().filter({ $0.disabled }) _matches().filter({ $0.disabled })
@ -400,12 +412,12 @@ class Round: ModelObject, Storable {
return Store.main.findById(parent) return Store.main.findById(parent)
} }
func updateIfRequiredMatchFormat(_ updatedMatchFormat: MatchFormat, andLoserBracket: Bool) { func updateMatchFormat(_ updatedMatchFormat: MatchFormat, checkIfPossible: Bool, andLoserBracket: Bool) {
if updatedMatchFormat.weight < self.matchFormat.weight { if updatedMatchFormat.weight < self.matchFormat.weight {
updateMatchFormatAndAllMatches(updatedMatchFormat) updateMatchFormatAndAllMatches(updatedMatchFormat)
if andLoserBracket { if andLoserBracket {
loserRoundsAndChildren().forEach { round in loserRoundsAndChildren().forEach { round in
round.updateIfRequiredMatchFormat(updatedMatchFormat, andLoserBracket: true) round.updateMatchFormat(updatedMatchFormat, checkIfPossible: checkIfPossible, andLoserBracket: true)
} }
} }
} }

@ -37,7 +37,15 @@ enum TimeOfDay {
extension Date { extension Date {
func localizedDate() -> String { func localizedDate() -> String {
self.formatted(.dateTime.weekday().day().month()) + " à " + self.formatted(.dateTime.hour().minute()) self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute()
}
func formattedAsHourMinute() -> String {
formatted(.dateTime.hour().minute())
}
func formattedAsDate() -> String {
formatted(.dateTime.weekday().day(.twoDigits).month().year())
} }
var monthYearFormatted: String { var monthYearFormatted: String {
@ -193,6 +201,11 @@ extension Date {
let calendar = Calendar.current let calendar = Calendar.current
return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)! return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)!
} }
func atNine() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: 9, minute: 0, second: 0, of: self)!
}
} }
extension Date { extension Date {
@ -203,7 +216,7 @@ extension Date {
extension Date { extension Date {
func localizedTime() -> String { func localizedTime() -> String {
self.formatted(.dateTime.hour().minute()) self.formattedAsHourMinute()
} }
func localizedDay() -> String { func localizedDay() -> String {

@ -94,6 +94,43 @@ class MatchScheduler {
options.contains(.rotationDifferenceIsImportant) options.contains(.rotationDifferenceIsImportant)
} }
@discardableResult
func updateGroupStageSchedule(tournament: Tournament) -> Date {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
courtsUnavailability = tournament.eventObject?.courtsUnavailability
let matches = groupStages.flatMap({ $0._matches() })
matches.forEach({
$0.removeCourt()
$0.startDate = nil
})
var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate })
try? DataStore.shared.groupStages.addOrUpdate(contentOfs: groups)
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex)
}
}
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
return lastDate
}
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?) -> GroupStageMatchDispatcher { func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?) -> GroupStageMatchDispatcher {
let _groupStages = groupStages let _groupStages = groupStages
@ -464,7 +501,9 @@ class MatchScheduler {
} }
} }
func updateSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) { func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) {
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
courtsUnavailability = tournament.eventObject?.courtsUnavailability
let upperRounds = tournament.rounds() let upperRounds = tournament.rounds()
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
@ -567,4 +606,11 @@ class MatchScheduler {
dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate) dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate)
}) })
} }
func updateSchedule(tournament: Tournament) {
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
courtsUnavailability = tournament.eventObject?.courtsUnavailability
let lastDate = updateGroupStageSchedule(tournament: tournament)
updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
}
} }

@ -18,7 +18,7 @@ struct CallView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack { HStack {
if let startDate { if let startDate {
Text(startDate.formatted(.dateTime.hour().minute())) Text(startDate.formattedAsHourMinute())
} else { } else {
Text("Aucun horaire") Text("Aucun horaire")
} }

@ -77,7 +77,11 @@ struct RowButtonView: View {
} }
.overlay { .overlay {
if isLoading { if isLoading {
ZStack {
Color.master
ProgressView() ProgressView()
.tint(.white)
}
} }
} }
.disabled(isLoading) .disabled(isLoading)

@ -41,7 +41,7 @@ struct GroupStageView: View {
_groupStageView() _groupStageView()
} header: { } header: {
if let startDate = groupStage.startDate { if let startDate = groupStage.startDate {
Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formattedAsHourMinute())
} }
} footer: { } footer: {
HStack { HStack {

@ -174,7 +174,7 @@ struct MatchSummaryView: View {
// } // }
// } else { // } else {
// if let endDate = match.endDate { // if let endDate = match.endDate {
// Text(endDate.formatted(.dateTime.hour().minute())) // Text(endDate.formattedAsHourMinute())
// } // }
// } // }
// } // }
@ -203,9 +203,9 @@ struct MatchSummaryView: View {
// secondsComponent: Int64(endDate.timeIntervalSince(startDate)), // secondsComponent: Int64(endDate.timeIntervalSince(startDate)),
// attosecondsComponent: 0 // attosecondsComponent: 0
// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow)) // ).formatted(.units(allowed: [.hours, .minutes], width: .narrow))
// Text(startDate.formatted(.dateTime.hour().minute())) // Text(startDate.formattedAsHourMinute())
// } else if startDate.timeIntervalSinceNow < 0 { // } else if startDate.timeIntervalSinceNow < 0 {
// Text(startDate.formatted(.dateTime.hour().minute())) // Text(startDate.formattedAsHourMinute())
// } else { // } else {
// Text(startDate.formatted(.dateTime.day().month())) // Text(startDate.formatted(.dateTime.day().month()))
// } // }

@ -7,26 +7,44 @@
import SwiftUI import SwiftUI
enum DateUpdate { struct DatePickingView: View {
case nextRotation let title: String
case previousRotation
case tomorrowAtNine
case inMinutes(Int)
case afterRound(Round)
case afterGroupStage(GroupStage)
}
struct DateUpdateManagerView: View {
@Binding var startDate: Date @Binding var startDate: Date
@Binding var currentDate: Date? @Binding var currentDate: Date?
@State private var dateUpdated: Bool = false
var duration: Int? var duration: Int?
var validateAction: () -> Void var validateAction: (() async -> ())
@State private var confirmFollowingScheduleUpdate: Bool = false
@State private var updatingInProgress: Bool = false
var body: some View { var body: some View {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmFollowingScheduleUpdate {
RowButtonView("Modifier la suite du programme") {
updatingInProgress = true
await validateAction()
updatingInProgress = false
confirmFollowingScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmFollowingScheduleUpdate && updatingInProgress == false {
FooterButtonView("non, ne pas modifier la suite") {
currentDate = startDate
confirmFollowingScheduleUpdate = false
}
} else {
HStack { HStack {
Menu { Menu {
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") { Button("à demain 9h") {
startDate = startDate.tomorrowAtNine startDate = startDate.tomorrowAtNine
} }
@ -46,22 +64,50 @@ struct DateUpdateManagerView: View {
.buttonStyle(.borderless) .buttonStyle(.borderless)
Spacer() Spacer()
if dateUpdated { if currentDate != nil {
FooterButtonView("valider l'horaire") { FooterButtonView("retirer l'horaire bloqué") {
validateAction()
dateUpdated = false
}
} else if currentDate != nil {
FooterButtonView("retirer l'horaire") {
currentDate = nil currentDate = nil
} }
} }
} }
.font(.subheadline)
.buttonStyle(.borderless) .buttonStyle(.borderless)
}
}
.onChange(of: startDate) { .onChange(of: startDate) {
dateUpdated = true confirmFollowingScheduleUpdate = true
} }
.headerProminence(.increased)
} }
} }
struct MatchFormatPickingView: View {
@Binding var matchFormat: MatchFormat
var validateAction: (() async -> ())
@State private var confirmScheduleUpdate: Bool = false
@State private var updatingInProgress : Bool = false
var body: some View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
if confirmScheduleUpdate {
RowButtonView("Recalculer les horaires") {
updatingInProgress = true
await validateAction()
updatingInProgress = false
confirmScheduleUpdate = false
}
}
} footer: {
if confirmScheduleUpdate && updatingInProgress == false {
FooterButtonView("non, ne pas modifier les horaires") {
confirmScheduleUpdate = false
}
}
}
.headerProminence(.increased)
.onChange(of: matchFormat) {
confirmScheduleUpdate = true
}
}
}

@ -12,7 +12,6 @@ struct GroupStageScheduleEditorView: View {
@Bindable var groupStage: GroupStage @Bindable var groupStage: GroupStage
var tournament: Tournament var tournament: Tournament
@State private var startDate: Date @State private var startDate: Date
@State private var uuid: UUID = UUID()
init(groupStage: GroupStage, tournament: Tournament) { init(groupStage: GroupStage, tournament: Tournament) {
self.groupStage = groupStage self.groupStage = groupStage
@ -21,24 +20,12 @@ struct GroupStageScheduleEditorView: View {
} }
var body: some View { var body: some View {
Section { DatePickingView(title: groupStage.groupStageTitle(), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
//MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} header: {
Text(groupStage.groupStageTitle())
} footer: {
DateUpdateManagerView(startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
groupStage.startDate = startDate groupStage.startDate = startDate
_save() _save()
uuid = UUID()
}
} }
.id(uuid)
} }
private func _save() { private func _save() {
try? dataStore.groupStages.addOrUpdate(instance: groupStage) try? dataStore.groupStages.addOrUpdate(instance: groupStage)
} }

@ -9,71 +9,66 @@ import SwiftUI
struct LoserRoundScheduleEditorView: View { struct LoserRoundScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var upperRound: Round var upperRound: Round
var tournament: Tournament
var loserRounds: [Round] var loserRounds: [Round]
@State private var startDate: Date @State private var startDate: Date
@State private var matchFormat: MatchFormat @State private var matchFormat: MatchFormat
init(upperRound: Round) { init(upperRound: Round, tournament: Tournament) {
self.upperRound = upperRound self.upperRound = upperRound
self.tournament = tournament
let _loserRounds = upperRound.loserRounds() let _loserRounds = upperRound.loserRounds()
self.loserRounds = _loserRounds self.loserRounds = _loserRounds
self._startDate = State(wrappedValue: _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate ?? Date()) self._startDate = State(wrappedValue: _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate ?? tournament.startDate)
self._matchFormat = State(wrappedValue: _loserRounds.first?.matchFormat ?? upperRound.matchFormat) self._matchFormat = State(wrappedValue: _loserRounds.first?.matchFormat ?? upperRound.matchFormat)
} }
var body: some View { var body: some View {
List { List {
Section { MatchFormatPickingView(matchFormat: $matchFormat) {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) await _updateSchedule()
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} header: {
Text("Match de classement " + upperRound.roundTitle())
} footer: {
DateUpdateManagerView(startDate: $startDate, currentDate: .constant(nil), duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule()
} }
DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: .constant(nil), duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
await _updateSchedule()
} }
let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false }) let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false })
ForEach(enabledLoserRounds.indices, id: \.self) { index in ForEach(enabledLoserRounds.indices, id: \.self) { index in
let loserRound = enabledLoserRounds[index] let loserRound = enabledLoserRounds[index]
LoserRoundStepScheduleEditorView(stepIndex: index, round: loserRound, upperRound: upperRound) LoserRoundStepScheduleEditorView(stepIndex: index, round: loserRound, upperRound: upperRound, tournament: tournament)
.id(UUID()) .id(UUID())
} }
} }
.onChange(of: matchFormat) { .onChange(of: matchFormat) {
loserRounds.forEach { round in loserRounds.forEach { round in
round.updateMatchFormatAndAllMatches(matchFormat) round.updateMatchFormat(matchFormat, checkIfPossible: false, andLoserBracket: true)
//round.updateIfRequiredMatchFormat(matchFormat, andLoserBracket: true)
} }
_save() _save()
} }
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle("Horaires") .navigationTitle("Classement " + upperRound.roundTitle())
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
private func _updateSchedule() { private func _updateSchedule() async {
let matches = upperRound.loserRounds().flatMap({ round in // let matches = upperRound.loserRounds().flatMap({ round in
round.playedMatches() // round.playedMatches()
}) // })
upperRound.loserRounds().forEach({ round in // upperRound.loserRounds().forEach({ round in
round.resetFromRoundAllMatchesStartDate() // round.resetFromRoundAllMatchesStartDate()
}) // })
//
try? dataStore.matches.addOrUpdate(contentOfs: matches) // try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save() // _save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate) let loserRounds = upperRound.loserRounds().filter { $0.isDisabled() == false }
MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: loserRounds.first?.id, fromMatchId: nil, startDate: startDate)
loserRounds.first?.startDate = startDate
_save() _save()
upperRound.loserRounds().first?.startDate = startDate
} }
private func _save() { private func _save() {

@ -9,75 +9,57 @@ import SwiftUI
struct LoserRoundStepScheduleEditorView: View { struct LoserRoundStepScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var stepIndex: Int var stepIndex: Int
var round: Round var round: Round
var upperRound: Round var upperRound: Round
var tournament: Tournament
var matches: [Match] var matches: [Match]
@State private var startDate: Date @State private var startDate: Date
@State private var matchFormat: MatchFormat //@State private var matchFormat: MatchFormat
init(stepIndex: Int, round: Round, upperRound: Round) { init(stepIndex: Int, round: Round, upperRound: Round, tournament: Tournament) {
self.upperRound = upperRound self.upperRound = upperRound
self.tournament = tournament
self.round = round self.round = round
self.stepIndex = stepIndex self.stepIndex = stepIndex
let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.enabledMatches() }) let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.enabledMatches() })
self.matches = _matches self.matches = _matches
self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date()) self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? tournament.startDate)
self._matchFormat = State(wrappedValue: round.matchFormat) //self._matchFormat = State(wrappedValue: round.matchFormat)
} }
var body: some View { var body: some View {
@Bindable var round = round @Bindable var round = round
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) DatePickingView(title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
DatePicker(selection: $startDate) { await _updateSchedule()
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
} }
Section {
NavigationLink { NavigationLink {
List { List {
ForEach(matches) { match in ForEach(matches) { match in
if match.disabled == false { if match.disabled == false {
MatchScheduleEditorView(match: match) MatchScheduleEditorView(match: match, tournament: tournament)
} }
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle(round.selectionLabel()) .navigationTitle("Tour #\(stepIndex + 1)")
.environment(tournament) .environment(tournament)
} label: { } label: {
Text("Voir tous les matchs") Text("Voir tous les matchs du \((stepIndex + 1).ordinalFormatted()) tour")
}
} header: {
Text("Tour #\(stepIndex + 1)")
} footer: {
HStack {
DateUpdateManagerView(startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule()
}
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.onChange(of: matchFormat) {
round.updateMatchFormatAndAllMatches(matchFormat)
_save()
}
.onChange(of: round.startDate) { .onChange(of: round.startDate) {
_save() _save()
} }
} }
private func _updateSchedule() { private func _updateSchedule() async {
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
round.resetFromRoundAllMatchesStartDate()
})
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.startDate = startDate round.startDate = startDate
}) })

@ -8,39 +8,35 @@
import SwiftUI import SwiftUI
struct MatchScheduleEditorView: View { struct MatchScheduleEditorView: View {
@Environment(Tournament.self) var tournament: Tournament
@Bindable var match: Match @Bindable var match: Match
var tournament: Tournament
@State private var startDate: Date @State private var startDate: Date
init(match: Match) { init(match: Match, tournament: Tournament) {
self.match = match self.match = match
self._startDate = State(wrappedValue: match.startDate ?? Date()) self.tournament = tournament
self._startDate = State(wrappedValue: match.startDate ?? tournament.startDate)
} }
var body: some View { var title: String {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} header: {
if let round = match.roundObject { if let round = match.roundObject {
Text(round.roundTitle() + " " + match.matchTitle()) return round.roundTitle() + " " + match.matchTitle()
} else { } else {
Text(match.matchTitle()) return match.matchTitle()
} }
} footer: {
DateUpdateManagerView(startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule()
} }
var body: some View {
DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
await _updateSchedule()
} }
.headerProminence(.increased)
} }
private func _updateSchedule() { private func _updateSchedule() async {
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate) MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate)
} }
} }
#Preview { #Preview {
MatchScheduleEditorView(match: Match.mock()) MatchScheduleEditorView(match: Match.mock(), tournament: Tournament.mock())
} }

@ -179,11 +179,9 @@ struct PlanningSettingsView: View {
} }
private func _setupSchedule() async { private func _setupSchedule() async {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matchScheduler = MatchScheduler.shared let matchScheduler = MatchScheduler.shared
matchScheduler.courtsUnavailability = tournament.eventObject?.courtsUnavailability
matchScheduler.options.removeAll() matchScheduler.options.removeAll()
if shouldEndBeforeStartNext { if shouldEndBeforeStartNext {
@ -214,35 +212,7 @@ struct PlanningSettingsView: View {
matchScheduler.upperBracketRotationDifference = upperBracketRotationDifference matchScheduler.upperBracketRotationDifference = upperBracketRotationDifference
matchScheduler.timeDifferenceLimit = timeDifferenceLimit matchScheduler.timeDifferenceLimit = timeDifferenceLimit
let matches = tournament.groupStages().flatMap({ $0._matches() }) matchScheduler.updateSchedule(tournament: tournament)
matches.forEach({
$0.removeCourt()
$0.startDate = nil
})
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)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex)
}
}
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
} }
private func _save() { private func _save() {

@ -12,12 +12,14 @@ struct PlanningView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
let matches: [Match] let matches: [Match]
@Binding var selectedScheduleDestination: ScheduleDestination?
@State private var timeSlots: [Date:[Match]] @State private var timeSlots: [Date:[Match]]
@State private var days: [Date] @State private var days: [Date]
@State private var keys: [Date] @State private var keys: [Date]
init(matches: [Match]) { init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) {
self.matches = matches self.matches = matches
_selectedScheduleDestination = selectedScheduleDestination
let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture }
_timeSlots = State(wrappedValue: timeSlots) _timeSlots = State(wrappedValue: timeSlots)
_days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted()) _days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted())
@ -26,6 +28,7 @@ struct PlanningView: View {
var body: some View { var body: some View {
List { List {
if matches.allSatisfy({ $0.startDate == nil }) == false {
ForEach(days, id: \.self) { day in ForEach(days, id: \.self) { day in
Section { Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
@ -65,6 +68,20 @@ struct PlanningView: View {
.headerProminence(.increased) .headerProminence(.increased)
} }
} }
}
.overlay {
if matches.allSatisfy({ $0.startDate == nil }) {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {
Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi")
} actions: {
RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil
}
}
}
}
.navigationTitle("Programmation") .navigationTitle("Programmation")
} }
@ -83,5 +100,5 @@ struct PlanningView: View {
} }
#Preview { #Preview {
PlanningView(matches: []) PlanningView(matches: [], selectedScheduleDestination: .constant(nil))
} }

@ -9,32 +9,30 @@ import SwiftUI
struct RoundScheduleEditorView: View { struct RoundScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var round: Round var round: Round
var tournament: Tournament
@State private var startDate: Date @State private var startDate: Date
init(round: Round) { init(round: Round, tournament: Tournament) {
self.round = round self.round = round
self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date()) self.tournament = tournament
self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? tournament.startDate)
} }
var body: some View { var body: some View {
@Bindable var round = round @Bindable var round = round
List { List {
Section { MatchFormatPickingView(matchFormat: $round.matchFormat) {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) await _updateSchedule()
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} footer: {
DateUpdateManagerView(startDate: $startDate, currentDate: $round.startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule()
} }
DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $round.startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
await _updateSchedule()
} }
ForEach(round.playedMatches()) { match in ForEach(round.playedMatches()) { match in
MatchScheduleEditorView(match: match) MatchScheduleEditorView(match: match, tournament: tournament)
.id(UUID()) .id(UUID())
} }
} }
@ -48,22 +46,15 @@ struct RoundScheduleEditorView: View {
round.updateMatchFormatOfAllMatches(round.matchFormat) round.updateMatchFormatOfAllMatches(round.matchFormat)
for index in (0..<round.index).reversed() { for index in (0..<round.index).reversed() {
if let upperRound = upperRounds.filter({ $0.index == index }).first { if let upperRound = upperRounds.filter({ $0.index == index }).first {
upperRound.updateIfRequiredMatchFormat(round.matchFormat, andLoserBracket: false) upperRound.updateMatchFormat(round.matchFormat, checkIfPossible: true, andLoserBracket: false)
} }
} }
_save() _save()
} }
} }
private func _updateSchedule() { private func _updateSchedule() async {
let matches = round._matches() MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
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)
round.startDate = startDate round.startDate = startDate
_save() _save()
} }
@ -74,5 +65,5 @@ struct RoundScheduleEditorView: View {
} }
#Preview { #Preview {
RoundScheduleEditorView(round: Round.mock()) RoundScheduleEditorView(round: Round.mock(), tournament: Tournament.mock())
} }

@ -29,8 +29,11 @@ struct SchedulerView: View {
List { List {
switch destination { switch destination {
case .scheduleGroupStage: case .scheduleGroupStage:
Section { MatchFormatPickingView(matchFormat: $tournament.groupStageMatchFormat) {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $tournament.groupStageMatchFormat) Task {
MatchScheduler.shared.updateSchedule(tournament: tournament)
}
}
.onChange(of: tournament.groupStageMatchFormat) { .onChange(of: tournament.groupStageMatchFormat) {
let groupStages = tournament.groupStages() let groupStages = tournament.groupStages()
groupStages.forEach { groupStage in groupStages.forEach { groupStage in
@ -39,13 +42,7 @@ struct SchedulerView: View {
try? dataStore.tournaments.addOrUpdate(instance: tournament) try? dataStore.tournaments.addOrUpdate(instance: tournament)
try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
} }
// } footer: {
// if tournament.groupStageMatchFormat.weight > tournament.groupStageSmartMatchFormat().weight {
// FooterButtonView("passer en " + tournament.groupStageSmartMatchFormat().format) {
// tournament.groupStageMatchFormat = tournament.groupStageSmartMatchFormat()
// }
// }
}
ForEach(tournament.groupStages()) { ForEach(tournament.groupStages()) {
GroupStageScheduleEditorView(groupStage: $0, tournament: tournament) GroupStageScheduleEditorView(groupStage: $0, tournament: tournament)
.id(UUID()) .id(UUID())
@ -59,36 +56,64 @@ struct SchedulerView: View {
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.monospacedDigit()
} }
@ViewBuilder
func _roundView(_ round: Round) -> some View { func _roundView(_ round: Round) -> some View {
Section { Section {
NavigationLink { NavigationLink {
RoundScheduleEditorView(round: round) RoundScheduleEditorView(round: round, tournament: tournament)
.environment(tournament)
.navigationTitle(round.titleLabel()) .navigationTitle(round.titleLabel())
.environment(tournament)
} label: { } label: {
LabeledContent { LabeledContent {
Text(round.matchFormat.format).font(.largeTitle) Text(round.matchFormat.format).font(.largeTitle)
} label: { } label: {
if let startDate = round.getStartDate() { if let startDate = round.getStartDate() {
Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle) HStack {
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) Text(startDate.formattedAsHourMinute()).font(.largeTitle)
if let estimatedEndDate = round.estimatedEndDate(tournament.additionalEstimationDuration) {
Image(systemName: "arrowshape.forward.fill")
Text(estimatedEndDate.formattedAsHourMinute()).font(.largeTitle)
}
}
Text(startDate.formattedAsDate())
} else { } else {
Text("Aucun horaire") Text("Aucun horaire")
} }
} }
} }
} header: {
Text(round.titleLabel())
}
Section {
NavigationLink { NavigationLink {
LoserRoundScheduleEditorView(upperRound: round) LoserRoundScheduleEditorView(upperRound: round, tournament: tournament)
.environment(tournament) .environment(tournament)
} label: { } label: {
Text("Match de classement \(round.roundTitle(.short))") LabeledContent {
let count = round.loserRounds().filter({ $0.isDisabled() == false }).count
Text(count.formatted() + " tour" + count.pluralSuffix)
} label: {
if let startDate = round.getLoserRoundStartDate() {
HStack {
Text(startDate.formattedAsHourMinute()).font(.title3)
if let estimatedEndDate = round.estimatedLoserRoundEndDate(tournament.additionalEstimationDuration) {
Image(systemName: "arrowshape.forward.fill")
Text(estimatedEndDate.formattedAsHourMinute()).font(.title3)
}
}
Text(startDate.formattedAsDate())
} else {
Text("Aucun horaire")
}
}
} }
} header: { } header: {
Text(round.titleLabel()) Text("Match de classement \(round.roundTitle(.short))")
} }
.headerProminence(.increased)
} }
} }

@ -34,7 +34,7 @@ enum ScheduleDestination: String, Identifiable, Selectable {
case .scheduleBracket: case .scheduleBracket:
return "Tableau" return "Tableau"
case .planning: case .planning:
return "Programmation" return "Prog."
} }
} }
@ -50,7 +50,7 @@ enum ScheduleDestination: String, Identifiable, Selectable {
struct TournamentScheduleView: View { struct TournamentScheduleView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedScheduleDestination: ScheduleDestination? = nil @State private var selectedScheduleDestination: ScheduleDestination? = .planning
let allDestinations: [ScheduleDestination] let allDestinations: [ScheduleDestination]
init(tournament: Tournament) { init(tournament: Tournament) {
@ -79,7 +79,7 @@ struct TournamentScheduleView: View {
case .scheduleBracket: case .scheduleBracket:
SchedulerView(tournament: tournament, destination: selectedSchedule) SchedulerView(tournament: tournament, destination: selectedSchedule)
case .planning: case .planning:
PlanningView(matches: tournament.allMatches()) PlanningView(matches: tournament.allMatches(), selectedScheduleDestination: $selectedScheduleDestination)
} }
} }
} }

Loading…
Cancel
Save