diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 8327def..f55faec 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -58,6 +58,7 @@ enum MatchSchedulerOption: Hashable { case randomizeCourts case rotationDifferenceIsImportant case shouldHandleUpperRoundSlice + case shouldEndRoundBeforeStartingNext } class MatchScheduler { @@ -69,6 +70,10 @@ class MatchScheduler { var upperBracketRotationDifference: Int = 1 var courtsUnavailability: [DateInterval]? = nil + func shouldEndRoundBeforeStartingNext() -> Bool { + options.contains(.shouldEndRoundBeforeStartingNext) + } + func shouldHandleUpperRoundSlice() -> Bool { options.contains(.shouldHandleUpperRoundSlice) } @@ -464,10 +469,18 @@ class MatchScheduler { let upperRounds = tournament.rounds() let allMatches = tournament.allMatches() - let rounds = upperRounds.map { - $0 - } + upperRounds.flatMap { - $0.loserRoundsAndChildren() + var rounds = [Round]() + + if shouldEndRoundBeforeStartingNext() { + rounds = upperRounds.flatMap { + [$0] + $0.loserRoundsAndChildren() + } + } else { + rounds = upperRounds.map { + $0 + } + upperRounds.flatMap { + $0.loserRoundsAndChildren() + } } var flattenedMatches = rounds.flatMap { round in diff --git a/PadelClub/Views/Components/BarButtonView.swift b/PadelClub/Views/Components/BarButtonView.swift index e70d164..7d52007 100644 --- a/PadelClub/Views/Components/BarButtonView.swift +++ b/PadelClub/Views/Components/BarButtonView.swift @@ -28,7 +28,7 @@ struct BarButtonView: View { Image(systemName: icon) .resizable() .scaledToFit() - .frame(minHeight: 28) + .frame(minHeight: 36) } .labelStyle(.iconOnly) } diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift index 1c30303..6ba8502 100644 --- a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -19,15 +19,25 @@ enum DateUpdate { struct DateUpdateManagerView: View { @Binding var startDate: Date @State private var dateUpdated: Bool = false - + + var duration: Int? var validateAction: () -> Void var body: some View { HStack { Menu { - Text("à demain 9h") - Text("à la prochaine rotation") - Text("à la précédente rotation") + 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() diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index f41b2a6..82b0606 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -17,6 +17,7 @@ struct CourtAvailabilitySettingsView: View { @State private var courtIndex: Int = 0 @State private var startDate: Date = Date() @State private var endDate: Date = Date() + @State private var editingSlot: DateInterval? var courtsUnavailability: [Int: [DateInterval]] { let groupedBy = Dictionary(grouping: event.courtsUnavailability, by: { dateInterval in @@ -55,6 +56,7 @@ struct CourtAvailabilitySettingsView: View { try? dataStore.dateIntervals.addOrUpdate(instance: duplicatedDateInterval) } Button("éditer") { + editingSlot = dateInterval courtIndex = dateInterval.courtIndex startDate = dateInterval.startDate endDate = dateInterval.endDate @@ -86,19 +88,23 @@ struct CourtAvailabilitySettingsView: View { } } .overlay { - ContentUnavailableView { - Label("Tous les terrains sont disponibles", systemImage: "checkmark.circle.fill") - } description: { - Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") - } actions: { - RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { - showingPopover = true + if courtsUnavailability.isEmpty { + ContentUnavailableView { + Label("Tous les terrains sont disponibles", systemImage: "checkmark.circle.fill").tint(.green) + } description: { + Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") + } actions: { + RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { + showingPopover = true + } } } } .toolbar { ToolbarItem(placement: .topBarTrailing) { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { + startDate = tournament.startDate + endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } } @@ -119,14 +125,21 @@ struct CourtAvailabilitySettingsView: View { } footer: { FooterButtonView("jour entier") { startDate = startDate.startOfDay - endDate = endDate.endOfDay() + endDate = startDate.endOfDay() } } } .toolbar { ButtonValidateView { - let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) - try? dataStore.dateIntervals.addOrUpdate(instance: dateInterval) + if editingSlot == nil { + let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) + try? dataStore.dateIntervals.addOrUpdate(instance: dateInterval) + } else { + editingSlot?.courtIndex = courtIndex + editingSlot?.endDate = endDate + editingSlot?.startDate = startDate + try? dataStore.dateIntervals.addOrUpdate(instance: editingSlot!) + } showingPopover = false } } diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift index db8860a..4ca4020 100644 --- a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -9,6 +9,7 @@ import SwiftUI struct GroupStageScheduleEditorView: View { @EnvironmentObject var dataStore: DataStore + @Environment(Tournament.self) var tournament: Tournament var groupStage: GroupStage @State private var startDate: Date @State private var dateUpdated: Bool = false @@ -33,17 +34,11 @@ struct GroupStageScheduleEditorView: View { } header: { Text(groupStage.groupStageTitle()) } footer: { - DateUpdateManagerView(startDate: $startDate) { + DateUpdateManagerView(startDate: $startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { groupStage.startDate = startDate _save() } } - - NavigationLink { - GroupStageView(groupStage: groupStage) - } label: { - Text("Voir la poule") - } } .onChange(of: groupStage.matchFormat) { _save() diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index 5b58990..103580a 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -34,7 +34,7 @@ struct LoserRoundScheduleEditorView: View { } header: { Text("Classement " + upperRound.roundTitle()) } footer: { - DateUpdateManagerView(startDate: $startDate) { + DateUpdateManagerView(startDate: $startDate, duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { _updateSchedule() } } diff --git a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift index f4f48dd..1ae2b46 100644 --- a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift @@ -51,8 +51,16 @@ struct LoserRoundStepScheduleEditorView: View { } header: { Text(round.selectionLabel()) } footer: { - DateUpdateManagerView(startDate: $startDate) { - _updateSchedule() + HStack { + DateUpdateManagerView(startDate: $startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + _updateSchedule() + } + + if round.startDate != nil { + FooterButtonView("retirer l'horaire") { + round.startDate = nil + } + } } } .headerProminence(.increased) diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index 2f24b72..04aca18 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -29,7 +29,7 @@ struct MatchScheduleEditorView: View { Text(match.matchTitle()) } } footer: { - DateUpdateManagerView(startDate: $startDate) { + DateUpdateManagerView(startDate: $startDate, duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { _updateSchedule() } } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index af77e8e..3e01a21 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -22,6 +22,7 @@ struct PlanningSettingsView: View { @State private var isScheduling: Bool = false @State private var schedulingDone: Bool = false @State private var showOptions: Bool = false + @State private var shouldEndBeforeStartNext: Bool = true init(tournament: Tournament) { self.tournament = tournament @@ -143,6 +144,10 @@ struct PlanningSettingsView: View { Text("Équilibrer les matchs d'une manche sur plusieurs tours") } + Toggle(isOn: $shouldEndBeforeStartNext) { + Text("Finir une manche et les matchs de classements avant de continuer") + } + Toggle(isOn: $upperBracketBreakTime) { Text("Tableau : tenir compte des pauses") } @@ -154,7 +159,7 @@ struct PlanningSettingsView: View { Toggle(isOn: $rotationDifferenceIsImportant) { Text("Forcer un créneau supplémentaire entre 2 phases") } - + LabeledContent { StepperView(count: $upperBracketRotationDifference, minimum: 0, maximum: 2) } label: { @@ -181,6 +186,10 @@ struct PlanningSettingsView: View { matchScheduler.courtsUnavailability = tournament.eventObject?.courtsUnavailability matchScheduler.options.removeAll() + if shouldEndBeforeStartNext { + matchScheduler.options.insert(.shouldEndRoundBeforeStartingNext) + } + if randomCourtDistribution { matchScheduler.options.insert(.randomizeCourts) } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index aa8f9cd..d134177 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -55,13 +55,22 @@ struct PlanningView: View { } } } header: { - Text(day.formatted(.dateTime.day().weekday().month().year())) + HStack { + Text(day.formatted(.dateTime.day().weekday().month())) + Spacer() + let count = _matchesCount(inDayInt: day.dayInt) + Text(count.formatted() + " match" + count.pluralSuffix) + } } .headerProminence(.increased) } } .navigationTitle("Programmation") } + + private func _matchesCount(inDayInt dayInt: Int) -> Int { + timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count + } private func _timeSlotView(key: Date, matches: [Match]) -> some View { LabeledContent { diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index 007b19e..2c329e2 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -29,18 +29,16 @@ struct RoundScheduleEditorView: View { } } footer: { HStack { - DateUpdateManagerView(startDate: $startDate) { + DateUpdateManagerView(startDate: $startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { _updateSchedule() } Spacer() - if let roundStartDate = round.startDate { - Button("horaire automatique") { + if round.startDate != nil { + FooterButtonView("retirer l'horaire") { round.startDate = nil } - .underline() - .buttonStyle(.borderless) } } } diff --git a/PadelClub/Views/Planning/SchedulerView.swift b/PadelClub/Views/Planning/SchedulerView.swift index ab4a982..06be9f4 100644 --- a/PadelClub/Views/Planning/SchedulerView.swift +++ b/PadelClub/Views/Planning/SchedulerView.swift @@ -50,6 +50,7 @@ struct SchedulerView: View { } else if let groupStage = schedulable as? GroupStage { GroupStageScheduleEditorView(groupStage: groupStage) + .environment(tournament) } } .navigationTitle(schedulable.titleLabel()) diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 3972a59..d805bbc 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -30,7 +30,8 @@ struct InscriptionManagerView: View { @State private var currentRankSourceDate: Date? @State private var confirmUpdateRank = false @State private var selectionSearchField: String? - + @State private var autoSelect: Bool = false + let slideToDeleteTip = SlideToDeleteTip() let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() let fileTip = InscriptionManagerFileInputTip() @@ -279,6 +280,7 @@ struct InscriptionManagerView: View { await MainActor.run { fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) pasteString = first + autoSelect = true } } } @@ -563,6 +565,24 @@ struct InscriptionManagerView: View { private func _buildingTeamView() -> some View { List(selection: $createdPlayerIds) { + if let pasteString { + + Section { + Text(pasteString) + } footer: { + HStack { + Text("contenu du presse-papier") + Spacer() + Button("effacer", role: .destructive) { + self.pasteString = nil + self.createdPlayers.removeAll() + self.createdPlayerIds.removeAll() + } + .buttonStyle(.borderless) + } + } + } + Section { ForEach(createdPlayerIds.sorted(), id: \.self) { id in if let p = createdPlayers.first(where: { $0.id == id }) { @@ -591,22 +611,6 @@ struct InscriptionManagerView: View { } if let pasteString { - - Section { - Text(pasteString) - } footer: { - HStack { - Text("contenu du presse-papier") - Spacer() - Button("effacer", role: .destructive) { - self.pasteString = nil - self.createdPlayers.removeAll() - self.createdPlayerIds.removeAll() - } - .buttonStyle(.borderless) - } - } - if fetchPlayers.isEmpty { ContentUnavailableView { Label("Aucun résultat", systemImage: "person.2.slash") @@ -634,13 +638,13 @@ struct InscriptionManagerView: View { } } .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here - if let pasteString, count == 2 { + if let pasteString, count == 2, autoSelect == true { fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in createdPlayerIds.insert(player.license!) } + autoSelect = false } } - .environment(\.editMode, Binding.constant(EditMode.active)) } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index fcb2342..07432a8 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -44,7 +44,7 @@ struct TournamentView: View { } } } footer: { - if tournament.inscriptionClosed() { + if tournament.inscriptionClosed() == false { Button { tournament.lockRegistration() _save()