diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 3ae6dc4..10b5b0d 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -285,7 +285,19 @@ 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] { _matches().filter({ $0.disabled }) } @@ -400,12 +412,12 @@ class Round: ModelObject, Storable { 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 { updateMatchFormatAndAllMatches(updatedMatchFormat) if andLoserBracket { loserRoundsAndChildren().forEach { round in - round.updateIfRequiredMatchFormat(updatedMatchFormat, andLoserBracket: true) + round.updateMatchFormat(updatedMatchFormat, checkIfPossible: checkIfPossible, andLoserBracket: true) } } } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index bac5698..15c7272 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -37,7 +37,15 @@ enum TimeOfDay { extension Date { 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 { @@ -193,6 +201,11 @@ extension Date { let calendar = Calendar.current 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 { @@ -203,7 +216,7 @@ extension Date { extension Date { func localizedTime() -> String { - self.formatted(.dateTime.hour().minute()) + self.formattedAsHourMinute() } func localizedDay() -> String { diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 7a06ecf..65cb618 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -94,6 +94,43 @@ class MatchScheduler { 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 { let _groupStages = groupStages @@ -464,8 +501,10 @@ 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 allMatches = tournament.allMatches() @@ -567,4 +606,11 @@ class MatchScheduler { 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) + } } diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index fb1e6c9..1126f31 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -18,7 +18,7 @@ struct CallView: View { VStack(spacing: 0) { HStack { if let startDate { - Text(startDate.formatted(.dateTime.hour().minute())) + Text(startDate.formattedAsHourMinute()) } else { Text("Aucun horaire") } diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift index bc7edc5..df2ea2e 100644 --- a/PadelClub/Views/Components/RowButtonView.swift +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -77,7 +77,11 @@ struct RowButtonView: View { } .overlay { if isLoading { - ProgressView() + ZStack { + Color.master + ProgressView() + .tint(.white) + } } } .disabled(isLoading) diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index e74439d..7833a3c 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -41,7 +41,7 @@ struct GroupStageView: View { _groupStageView() } header: { 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: { HStack { diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 3e95299..9b62708 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -174,7 +174,7 @@ struct MatchSummaryView: View { // } // } else { // 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)), // attosecondsComponent: 0 // ).formatted(.units(allowed: [.hours, .minutes], width: .narrow)) -// Text(startDate.formatted(.dateTime.hour().minute())) +// Text(startDate.formattedAsHourMinute()) // } else if startDate.timeIntervalSinceNow < 0 { -// Text(startDate.formatted(.dateTime.hour().minute())) +// Text(startDate.formattedAsHourMinute()) // } else { // Text(startDate.formatted(.dateTime.day().month())) // } diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift index 8c8d96c..51e8c29 100644 --- a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -7,61 +7,107 @@ import SwiftUI -enum DateUpdate { - case nextRotation - case previousRotation - case tomorrowAtNine - case inMinutes(Int) - case afterRound(Round) - case afterGroupStage(GroupStage) -} - -struct DateUpdateManagerView: View { +struct DatePickingView: View { + let title: String @Binding var startDate: Date @Binding var currentDate: Date? - - @State private var dateUpdated: Bool = false 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 { - HStack { - Menu { - Button("à demain 9h") { - startDate = startDate.tomorrowAtNine + 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 } - - if let duration { - Button("à la prochaine rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + } header: { + Text(title) + } footer: { + if confirmFollowingScheduleUpdate && updatingInProgress == false { + FooterButtonView("non, ne pas modifier la suite") { + currentDate = startDate + confirmFollowingScheduleUpdate = false + } + } else { + HStack { + Menu { + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() } - Button("à la précédente rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * -60) + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } } } - } label: { - Text("décaler") - .underline() + .buttonStyle(.borderless) } - .buttonStyle(.borderless) - Spacer() - - if dateUpdated { - FooterButtonView("valider l'horaire") { - validateAction() - dateUpdated = false + } + .onChange(of: startDate) { + 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 } - } else if currentDate != nil { - FooterButtonView("retirer l'horaire") { - currentDate = nil + } + } footer: { + if confirmScheduleUpdate && updatingInProgress == false { + FooterButtonView("non, ne pas modifier les horaires") { + confirmScheduleUpdate = false } } } - .font(.subheadline) - .buttonStyle(.borderless) - .onChange(of: startDate) { - dateUpdated = true + .headerProminence(.increased) + .onChange(of: matchFormat) { + confirmScheduleUpdate = true } } } - diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift index 3c2994a..7f299a8 100644 --- a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -12,7 +12,6 @@ struct GroupStageScheduleEditorView: View { @Bindable var groupStage: GroupStage var tournament: Tournament @State private var startDate: Date - @State private var uuid: UUID = UUID() init(groupStage: GroupStage, tournament: Tournament) { self.groupStage = groupStage @@ -21,24 +20,12 @@ struct GroupStageScheduleEditorView: View { } var body: some View { - Section { - //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 - _save() - uuid = UUID() - } + DatePickingView(title: groupStage.groupStageTitle(), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + groupStage.startDate = startDate + _save() } - .id(uuid) } - - + private func _save() { try? dataStore.groupStages.addOrUpdate(instance: groupStage) } diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index bd6e485..2b35159 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -9,71 +9,66 @@ import SwiftUI struct LoserRoundScheduleEditorView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament: Tournament var upperRound: Round + var tournament: Tournament var loserRounds: [Round] @State private var startDate: Date @State private var matchFormat: MatchFormat - init(upperRound: Round) { + init(upperRound: Round, tournament: Tournament) { self.upperRound = upperRound + self.tournament = tournament let _loserRounds = upperRound.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) } var body: some View { List { - Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) - 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() - } + MatchFormatPickingView(matchFormat: $matchFormat) { + await _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 }) ForEach(enabledLoserRounds.indices, id: \.self) { index in let loserRound = enabledLoserRounds[index] - LoserRoundStepScheduleEditorView(stepIndex: index, round: loserRound, upperRound: upperRound) + LoserRoundStepScheduleEditorView(stepIndex: index, round: loserRound, upperRound: upperRound, tournament: tournament) .id(UUID()) } } .onChange(of: matchFormat) { loserRounds.forEach { round in - round.updateMatchFormatAndAllMatches(matchFormat) - //round.updateIfRequiredMatchFormat(matchFormat, andLoserBracket: true) + round.updateMatchFormat(matchFormat, checkIfPossible: false, andLoserBracket: true) } _save() } .headerProminence(.increased) - .navigationTitle("Horaires") + .navigationTitle("Classement " + upperRound.roundTitle()) .toolbarBackground(.visible, for: .navigationBar) .navigationBarTitleDisplayMode(.inline) } - private func _updateSchedule() { - let matches = upperRound.loserRounds().flatMap({ round in - round.playedMatches() - }) - upperRound.loserRounds().forEach({ round in - round.resetFromRoundAllMatchesStartDate() - }) + private func _updateSchedule() async { +// let matches = upperRound.loserRounds().flatMap({ round in +// round.playedMatches() +// }) +// upperRound.loserRounds().forEach({ round in +// round.resetFromRoundAllMatchesStartDate() +// }) +// +// try? dataStore.matches.addOrUpdate(contentOfs: matches) +// _save() - try? dataStore.matches.addOrUpdate(contentOfs: matches) + 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() - - MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate) - _save() - - upperRound.loserRounds().first?.startDate = startDate } private func _save() { diff --git a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift index f775b32..1904942 100644 --- a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift @@ -9,75 +9,57 @@ import SwiftUI struct LoserRoundStepScheduleEditorView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament: Tournament var stepIndex: Int var round: Round var upperRound: Round + var tournament: Tournament var matches: [Match] @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.tournament = tournament self.round = round self.stepIndex = stepIndex let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.enabledMatches() }) self.matches = _matches - self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date()) - self._matchFormat = State(wrappedValue: round.matchFormat) + self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? tournament.startDate) + //self._matchFormat = State(wrappedValue: round.matchFormat) } var body: some View { @Bindable var round = round + + DatePickingView(title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + await _updateSchedule() + } + Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) - DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) - } NavigationLink { List { ForEach(matches) { match in if match.disabled == false { - MatchScheduleEditorView(match: match) + MatchScheduleEditorView(match: match, tournament: tournament) } } } .headerProminence(.increased) - .navigationTitle(round.selectionLabel()) + .navigationTitle("Tour #\(stepIndex + 1)") .environment(tournament) } label: { - Text("Voir tous les matchs") - } - - } header: { - Text("Tour #\(stepIndex + 1)") - } footer: { - HStack { - DateUpdateManagerView(startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { - _updateSchedule() - } + Text("Voir tous les matchs du \((stepIndex + 1).ordinalFormatted()) tour") } } .headerProminence(.increased) - .onChange(of: matchFormat) { - round.updateMatchFormatAndAllMatches(matchFormat) - _save() - } .onChange(of: round.startDate) { _save() } } - private func _updateSchedule() { - upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in - round.resetFromRoundAllMatchesStartDate() - }) - - try? dataStore.matches.addOrUpdate(contentOfs: matches) - _save() - - MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + private func _updateSchedule() async { + MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in round.startDate = startDate }) diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index dcf987c..7f45a07 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -8,39 +8,35 @@ import SwiftUI struct MatchScheduleEditorView: View { - @Environment(Tournament.self) var tournament: Tournament @Bindable var match: Match + var tournament: Tournament @State private var startDate: Date - init(match: Match) { + init(match: Match, tournament: Tournament) { self.match = match - self._startDate = State(wrappedValue: match.startDate ?? Date()) + self.tournament = tournament + self._startDate = State(wrappedValue: match.startDate ?? tournament.startDate) + } + + var title: String { + if let round = match.roundObject { + return round.roundTitle() + " " + match.matchTitle() + } else { + return match.matchTitle() + } } var body: some View { - Section { - DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) - } - } header: { - if let round = match.roundObject { - Text(round.roundTitle() + " " + match.matchTitle()) - } else { - Text(match.matchTitle()) - } - } footer: { - DateUpdateManagerView(startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { - _updateSchedule() - } + DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + await _updateSchedule() } - .headerProminence(.increased) } - private func _updateSchedule() { - MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate) + private func _updateSchedule() async { + MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate) } } #Preview { - MatchScheduleEditorView(match: Match.mock()) + MatchScheduleEditorView(match: Match.mock(), tournament: Tournament.mock()) } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 3e01a21..0252129 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -179,11 +179,9 @@ struct PlanningSettingsView: View { } private func _setupSchedule() async { - let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 - let groupStages = tournament.groupStages() - let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount + let matchScheduler = MatchScheduler.shared - matchScheduler.courtsUnavailability = tournament.eventObject?.courtsUnavailability + matchScheduler.options.removeAll() if shouldEndBeforeStartNext { @@ -214,35 +212,7 @@ struct PlanningSettingsView: View { matchScheduler.upperBracketRotationDifference = upperBracketRotationDifference matchScheduler.timeDifferenceLimit = timeDifferenceLimit - let matches = tournament.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.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) + matchScheduler.updateSchedule(tournament: tournament) } private func _save() { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index d134177..dabc0e9 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -12,12 +12,14 @@ struct PlanningView: View { @Environment(Tournament.self) var tournament: Tournament let matches: [Match] + @Binding var selectedScheduleDestination: ScheduleDestination? @State private var timeSlots: [Date:[Match]] @State private var days: [Date] @State private var keys: [Date] - init(matches: [Match]) { + init(matches: [Match], selectedScheduleDestination: Binding) { self.matches = matches + _selectedScheduleDestination = selectedScheduleDestination let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } _timeSlots = State(wrappedValue: timeSlots) _days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted()) @@ -26,43 +28,58 @@ struct PlanningView: View { 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] { - DisclosureGroup { - ForEach(_matches) { match in - NavigationLink { - MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) - } label: { - LabeledContent { - if let courtName = match.courtName() { - Text(courtName) - } + if matches.allSatisfy({ $0.startDate == nil }) == false { + ForEach(days, id: \.self) { day in + Section { + ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in + if let _matches = timeSlots[key] { + 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()) + LabeledContent { + if let courtName = match.courtName() { + Text(courtName) + } + } 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: { + _timeSlotView(key: key, matches: _matches) } - } label: { - _timeSlotView(key: key, matches: _matches) } } + } header: { + HStack { + Text(day.formatted(.dateTime.day().weekday().month())) + Spacer() + let count = _matchesCount(inDayInt: day.dayInt) + Text(count.formatted() + " match" + count.pluralSuffix) + } } - } header: { - HStack { - Text(day.formatted(.dateTime.day().weekday().month())) - Spacer() - let count = _matchesCount(inDayInt: day.dayInt) - Text(count.formatted() + " match" + count.pluralSuffix) + .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 } } - .headerProminence(.increased) } } .navigationTitle("Programmation") @@ -83,5 +100,5 @@ struct PlanningView: View { } #Preview { - PlanningView(matches: []) + PlanningView(matches: [], selectedScheduleDestination: .constant(nil)) } diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index bece59e..d9149da 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -9,32 +9,30 @@ import SwiftUI struct RoundScheduleEditorView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament: Tournament var round: Round + var tournament: Tournament @State private var startDate: Date - init(round: Round) { + init(round: Round, tournament: Tournament) { 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 { @Bindable var round = round List { - Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) - 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() - } + MatchFormatPickingView(matchFormat: $round.matchFormat) { + await _updateSchedule() + } + + DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $round.startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + await _updateSchedule() } ForEach(round.playedMatches()) { match in - MatchScheduleEditorView(match: match) + MatchScheduleEditorView(match: match, tournament: tournament) .id(UUID()) } } @@ -48,22 +46,15 @@ struct RoundScheduleEditorView: View { round.updateMatchFormatOfAllMatches(round.matchFormat) for index in (0.. tournament.groupStageSmartMatchFormat().weight { -// FooterButtonView("passer en " + tournament.groupStageSmartMatchFormat().format) { -// tournament.groupStageMatchFormat = tournament.groupStageSmartMatchFormat() -// } -// } + MatchFormatPickingView(matchFormat: $tournament.groupStageMatchFormat) { + Task { + MatchScheduler.shared.updateSchedule(tournament: tournament) + } + } + .onChange(of: tournament.groupStageMatchFormat) { + let groupStages = tournament.groupStages() + groupStages.forEach { groupStage in + groupStage.updateMatchFormat(tournament.groupStageMatchFormat) + } + try? dataStore.tournaments.addOrUpdate(instance: tournament) + try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages) } + ForEach(tournament.groupStages()) { GroupStageScheduleEditorView(groupStage: $0, tournament: tournament) .id(UUID()) @@ -59,36 +56,64 @@ struct SchedulerView: View { } } .headerProminence(.increased) + .monospacedDigit() } + @ViewBuilder func _roundView(_ round: Round) -> some View { Section { NavigationLink { - RoundScheduleEditorView(round: round) - .environment(tournament) + RoundScheduleEditorView(round: round, tournament: tournament) .navigationTitle(round.titleLabel()) + .environment(tournament) } label: { LabeledContent { Text(round.matchFormat.format).font(.largeTitle) } label: { if let startDate = round.getStartDate() { - Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle) - Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) + HStack { + 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 { Text("Aucun horaire") } } } + } header: { + Text(round.titleLabel()) + } + + Section { NavigationLink { - LoserRoundScheduleEditorView(upperRound: round) + LoserRoundScheduleEditorView(upperRound: round, tournament: tournament) .environment(tournament) } 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: { - Text(round.titleLabel()) + Text("Match de classement \(round.roundTitle(.short))") } - .headerProminence(.increased) } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 82bc8cb..c6cfa8c 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -34,7 +34,7 @@ enum ScheduleDestination: String, Identifiable, Selectable { case .scheduleBracket: return "Tableau" case .planning: - return "Programmation" + return "Prog." } } @@ -50,7 +50,7 @@ enum ScheduleDestination: String, Identifiable, Selectable { struct TournamentScheduleView: View { var tournament: Tournament - @State private var selectedScheduleDestination: ScheduleDestination? = nil + @State private var selectedScheduleDestination: ScheduleDestination? = .planning let allDestinations: [ScheduleDestination] init(tournament: Tournament) { @@ -79,7 +79,7 @@ struct TournamentScheduleView: View { case .scheduleBracket: SchedulerView(tournament: tournament, destination: selectedSchedule) case .planning: - PlanningView(matches: tournament.allMatches()) + PlanningView(matches: tournament.allMatches(), selectedScheduleDestination: $selectedScheduleDestination) } } }