diff --git a/PadelClub/Views/Cashier/Event/EventView.swift b/PadelClub/Views/Cashier/Event/EventView.swift index 99ae840..cb0214e 100644 --- a/PadelClub/Views/Cashier/Event/EventView.swift +++ b/PadelClub/Views/Cashier/Event/EventView.swift @@ -18,6 +18,7 @@ enum EventDestination: Identifiable, Selectable, Equatable { case links case tournaments(Event) case cashier + case eventPlanning var id: String { return String(describing: self) @@ -33,6 +34,8 @@ enum EventDestination: Identifiable, Selectable, Equatable { return "Tournois" case .cashier: return "Finance" + case .eventPlanning: + return "Planning" } } @@ -42,7 +45,7 @@ enum EventDestination: Identifiable, Selectable, Equatable { return nil case .tournaments(let event): return event.tournaments.count - case .cashier: + case .cashier, .eventPlanning: return nil } } @@ -77,7 +80,7 @@ struct EventView: View { } func allDestinations() -> [EventDestination] { - [.club(event), .tournaments(event), .cashier] + [.club(event), .eventPlanning, .tournaments(event), .cashier] } var body: some View { @@ -90,6 +93,10 @@ struct EventView: View { switch selectedEventDestination { case .club(let event): EventClubSettingsView(event: event) + case .eventPlanning: + let allMatches = event.tournaments.flatMap { $0.allMatches() } + PlanningView(matches: allMatches, selectedScheduleDestination: .constant(.planning)) + .environment(\.matchViewStyle, .feedStyle) case .links: EventLinksView(event: event) case .tournaments(let event): diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 4223373..e34c906 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -256,13 +256,27 @@ struct CourtAvailabilityEditorView: View { struct DateAdjusterView: View { @Binding var date: Date + var time: Int? + var matchFormat: MatchFormat? var body: some View { - HStack { - _createButton(label: "-1h", timeOffset: -1, component: .hour) - _createButton(label: "-30m", timeOffset: -30, component: .minute) - _createButton(label: "+30m", timeOffset: 30, component: .minute) - _createButton(label: "+1h", timeOffset: 1, component: .hour) + HStack(spacing: 4) { + if let matchFormat { + _createButton(label: "-\(matchFormat.defaultEstimatedDuration)m", timeOffset: -matchFormat.defaultEstimatedDuration, component: .minute) + _createButton(label: "+\(matchFormat.defaultEstimatedDuration)m", timeOffset: +matchFormat.defaultEstimatedDuration, component: .minute) + _createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute) + _createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute) + } else if let time { + _createButton(label: "-\(time)m", timeOffset: -time, component: .minute) + _createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute) + _createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute) + _createButton(label: "+\(time)m", timeOffset: time, component: .minute) + } else { + _createButton(label: "-1h", timeOffset: -1, component: .hour) + _createButton(label: "-30m", timeOffset: -30, component: .minute) + _createButton(label: "+30m", timeOffset: 30, component: .minute) + _createButton(label: "+1h", timeOffset: 1, component: .hour) + } } .font(.headline) } @@ -272,6 +286,9 @@ struct DateAdjusterView: View { date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date }) { Text(label) + .lineLimit(1) + .font(.footnote) + .underline() .frame(maxWidth: .infinity) // Make buttons take equal space } .buttonStyle(.borderedProminent) diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index f9ac7b8..d3447f9 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -114,7 +114,6 @@ struct PlanningByCourtView: View { let match = _sortedMatches[index] Section { MatchRowView(match: match) - .matchViewStyle(.feedStyle) } header: { if let startDate = match.startDate { if index > 0 { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index b72441d..c964187 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -5,45 +5,45 @@ // Created by Razmig Sarkissian on 07/04/2024. // -import SwiftUI import LeStorage -import TipKit import PadelClubData +import SwiftUI +import TipKit struct PlanningView: View { - + @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament: Tournament @State private var selectedDay: Date? @Binding var selectedScheduleDestination: ScheduleDestination? @State private var filterOption: PlanningFilterOption = .byDefault @State private var showFinishedMatches: Bool = false @State private var enableMove: Bool = false - + @Environment(\.editMode) private var editMode + let allMatches: [Match] let timeSlotMoveOptionTip = TimeSlotMoveOptionTip() - + init(matches: [Match], selectedScheduleDestination: Binding) { self.allMatches = matches _selectedScheduleDestination = selectedScheduleDestination } - + var matches: [Match] { allMatches.filter({ showFinishedMatches || $0.endDate == nil }) } - - var timeSlots: [Date:[Match]] { + + var timeSlots: [Date: [Match]] { Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } } - - func days(timeSlots: [Date:[Match]]) -> [Date] { + + func days(timeSlots: [Date: [Match]]) -> [Date] { Set(timeSlots.keys.map { $0.startOfDay }).sorted() } - - func keys(timeSlots: [Date:[Match]]) -> [Date] { + + func keys(timeSlots: [Date: [Match]]) -> [Date] { timeSlots.keys.sorted() } - + private func _computedTitle(days: [Date]) -> String { if let selectedDay { return selectedDay.formatted(.dateTime.day().weekday().month()) @@ -55,6 +55,23 @@ struct PlanningView: View { } } } + + private func _confirmationMode() -> Bool { + enableMove || editMode?.wrappedValue == .active + } + + private var enableEditionBinding: Binding { + Binding { + editMode?.wrappedValue == .active + } set: { value in + if value { + editMode?.wrappedValue = .active + } else { + editMode?.wrappedValue = .inactive + } + } + + } var body: some View { let timeSlots = self.timeSlots @@ -62,119 +79,144 @@ struct PlanningView: View { let days = self.days(timeSlots: timeSlots) let matches = matches let notSlots = matches.allSatisfy({ $0.startDate == nil }) - BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay) - .environment(\.filterOption, filterOption) - .environment(\.showFinishedMatches, showFinishedMatches) - .environment(\.enableMove, enableMove) - .navigationTitle(Text(_computedTitle(days: days))) - .navigationBarBackButtonHidden(enableMove) - .toolbar(content: { - if days.count > 1 { - ToolbarTitleMenu { - Picker(selection: $selectedDay) { - Text("Tous les jours").tag(nil as Date?) - ForEach(days, id: \.self) { day in - if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { - Text("Sans horaire").tag(day as Date?) - } else { - Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) - } + BySlotView( + days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay + ) + .environment(\.filterOption, filterOption) + .environment(\.showFinishedMatches, showFinishedMatches) + .environment(\.enableMove, enableMove) + .navigationTitle(Text(_computedTitle(days: days))) + .navigationBarBackButtonHidden(_confirmationMode()) + .toolbar(content: { + if days.count > 1 { + ToolbarTitleMenu { + Picker(selection: $selectedDay) { + Text("Tous les jours").tag(nil as Date?) + ForEach(days, id: \.self) { day in + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire").tag(day as Date?) + } else { + Text(day.formatted(.dateTime.day().weekday().month())).tag( + day as Date?) } - } label: { - Text("Jour") } - .pickerStyle(.automatic) - .disabled(enableMove) + } label: { + Text("Jour") } + .pickerStyle(.automatic) + .disabled(_confirmationMode()) } - - if enableMove { - ToolbarItem(placement: .topBarLeading) { - Button("Annuler") { - enableMove = false - } + } + + if _confirmationMode() { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler") { + enableMove = false + enableEditionBinding.wrappedValue = false } - - ToolbarItem(placement: .topBarTrailing) { + } + if enableMove { + ToolbarItemGroup(placement: .topBarTrailing) { Button("Sauver") { - do { - try self.tournament.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches) - } catch { - Logger.error(error) + let groupByTournaments = allMatches.grouped { match in + match.currentTournament() } - + groupByTournaments.forEach { tournament, matches in + tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches) + } + enableMove = false } } - - } else { - - ToolbarItemGroup(placement: .topBarTrailing) { - if notSlots == false { + } + } else { + if notSlots == false { + ToolbarItemGroup(placement: .bottomBar) { + HStack { + CourtOptionsView(timeSlots: timeSlots, underlined: false) + Spacer() Toggle(isOn: $enableMove) { - Label("Déplacer", systemImage: "rectangle.2.swap") + Label { + Text("Déplacer") + } icon: { + Image(systemName: "rectangle.2.swap") + } } .popoverTip(timeSlotMoveOptionTip) + .disabled(_confirmationMode()) + Spacer() + Toggle(isOn: enableEditionBinding) { + Text("Modifier") + } + .disabled(_confirmationMode()) } - - Menu { - Section { - Picker(selection: $showFinishedMatches) { - Text("Afficher tous les matchs").tag(true) - Text("Masquer les matchs terminés").tag(false) - } label: { - Text("Option de filtrage") - } - .labelsHidden() - .pickerStyle(.inline) - } header: { + } + } + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + Section { + Picker(selection: $showFinishedMatches) { + Text("Afficher tous les matchs").tag(true) + Text("Masquer les matchs terminés").tag(false) + } label: { Text("Option de filtrage") } - - Divider() - - Section { - Picker(selection: $filterOption) { - ForEach(PlanningFilterOption.allCases) { - Text($0.localizedPlanningLabel()).tag($0) - } - } label: { - Text("Option de triage") + .labelsHidden() + .pickerStyle(.inline) + } header: { + Text("Option de filtrage") + } + + Divider() + + Section { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) } - .labelsHidden() - .pickerStyle(.inline) - } header: { + } label: { Text("Option de triage") - } - } label: { - Label("Trier", systemImage: "line.3.horizontal.decrease.circle") - .symbolVariant(filterOption == .byCourt || showFinishedMatches ? .fill : .none) + .labelsHidden() + .pickerStyle(.inline) + } header: { + Text("Option de triage") + } - + } label: { + Label("Trier", systemImage: "line.3.horizontal.decrease.circle") + .symbolVariant( + filterOption == .byCourt || showFinishedMatches ? .fill : .none) } + } - }) - .overlay { - if notSlots { - 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 - } + } + }) + .overlay { + if notSlots { + 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 } } } + } } - + struct BySlotView: View { - @Environment(Tournament.self) var tournament: Tournament @Environment(\.filterOption) private var filterOption @Environment(\.showFinishedMatches) private var showFinishedMatches @Environment(\.enableMove) private var enableMove + @Environment(\.editMode) private var editMode + @State private var selectedIds = Set() + @State private var showDateUpdateView: Bool = false + @State private var dateToUpdate: Date = Date() let days: [Date] let keys: [Date] @@ -184,15 +226,15 @@ struct PlanningView: View { let timeSlotMoveTip = TimeSlotMoveTip() var body: some View { - List { - + List(selection: $selectedIds) { if enableMove { TipView(timeSlotMoveTip) .tipStyle(tint: .logoYellow, asSection: true) } - + if !matches.allSatisfy({ $0.startDate == nil }) { - ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in + ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { + day in DaySectionView( day: day, keys: keys.filter({ $0.dayInt == day.dayInt }), @@ -202,15 +244,108 @@ struct PlanningView: View { } } } + .toolbar(content: { + if editMode?.wrappedValue == .active { + ToolbarItem(placement: .bottomBar) { + Button { + showDateUpdateView = true + } label: { + Text("Modifier la date des matchs sélectionnés") + } + .disabled(selectedIds.isEmpty) + } + } + }) + .sheet(isPresented: $showDateUpdateView, onDismiss: { + selectedIds.removeAll() + }) { + let selectedMatches = matches.filter({ selectedIds.contains($0.stringId) }) + DateUpdateView(selectedMatches: selectedMatches) + } } } + + struct DateUpdateView: View { + @Environment(\.dismiss) var dismiss + + let selectedMatches: [Match] + let selectedFormats: [MatchFormat] + @State private var dateToUpdate: Date + + init(selectedMatches: [Match]) { + self.selectedMatches = selectedMatches + self.selectedFormats = Array(Set(selectedMatches.map({ match in + match.matchFormat + }))) + _dateToUpdate = .init(wrappedValue: selectedMatches.first?.startDate ?? Date()) + } + + var body: some View { + NavigationStack { + List { + Section { + DatePicker(selection: $dateToUpdate) { + Text(dateToUpdate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + } + + Section { + DateAdjusterView(date: $dateToUpdate) + DateAdjusterView(date: $dateToUpdate, time: 10) + ForEach(selectedFormats, id: \.self) { matchFormat in + DateAdjusterView(date: $dateToUpdate, matchFormat: matchFormat) + } + } + + Section { + ForEach(selectedMatches) { match in + MatchRowView(match: match) + } + } header: { + Text("Matchs à modifier") + } + + } + .navigationTitle("Modification de la date") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar(content: { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + dismiss() + } + } + ToolbarItem(placement: .topBarTrailing) { + Button("Valider") { + _updateDate() + } + } + }) + } + } + + private func _updateDate() { + selectedMatches.forEach { match in + match.startDate = dateToUpdate + } + + let groupByTournaments = selectedMatches.grouped { match in + match.currentTournament() + } + groupByTournaments.forEach { tournament, matches in + tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches) + } + dismiss() + } + + } struct DaySectionView: View { - @Environment(Tournament.self) var tournament: Tournament @Environment(\.filterOption) private var filterOption @Environment(\.showFinishedMatches) private var showFinishedMatches @Environment(\.enableMove) private var enableMove + @Environment(\.editMode) private var editMode let day: Date let keys: [Date] @@ -222,31 +357,40 @@ struct PlanningView: View { ForEach(keys, id: \.self) { key in TimeSlotSectionView( key: key, - matches: timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) ?? [] + matches: timeSlots[key]?.sorted( + by: filterOption == .byDefault + ? \.computedOrder : \.courtIndexForSorting) ?? [] ) } .onMove(perform: enableMove ? moveSection : nil) } header: { HeaderView(day: day, timeSlots: timeSlots) } footer: { - if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { - Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") + VStack(alignment: .leading) { + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text( + "Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages." + ) + } + + CourtOptionsView(timeSlots: timeSlots, underlined: true) } } } - + func moveSection(from source: IndexSet, to destination: Int) { let daySlots = keys.filter { $0.dayInt == day.dayInt }.sorted() - + guard let sourceIdx = source.first, sourceIdx < daySlots.count, - destination <= daySlots.count else { + destination <= daySlots.count + else { return } - - // Create a mutable copy of the time slots for this day + + // Create a mutable copy of the time slots for this day var slotsToUpdate = daySlots - + let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination) // Perform the move in the array @@ -256,7 +400,7 @@ struct PlanningView: View { } else { slotsToUpdate.insert(sourceTime, at: destination) } - + // Update matches by swapping their startDates for index in updateRange { // Find the new time slot for these matches @@ -264,7 +408,7 @@ struct PlanningView: View { guard let newStartTime = daySlots[safe: index] else { continue } guard let matchesToUpdate = timeSlots[oldStartTime] else { continue } - // Update each match with the new start time + // Update each match with the new start time for match in matchesToUpdate { match.startDate = newStartTime } @@ -272,22 +416,47 @@ struct PlanningView: View { } } - struct TimeSlotSectionView: View { @Environment(\.enableMove) private var enableMove + @Environment(\.editMode) private var editMode let key: Date let matches: [Match] - + + @State private var isExpanded: Bool = false + @State private var showDateUpdateView: Bool = false + var body: some View { if !matches.isEmpty { if enableMove { TimeSlotHeaderView(key: key, matches: matches) } else { - DisclosureGroup { + DisclosureGroup(isExpanded: $isExpanded) { MatchListView(matches: matches) } label: { TimeSlotHeaderView(key: key, matches: matches) } + .contextMenu { + PlanningView.CourtOptionsView(timeSlots: [key: matches], underlined: false) + + Button { + showDateUpdateView = true + } label: { + Text("Modifier la date") + } + + } + .sheet(isPresented: $showDateUpdateView, onDismiss: { + }) { + PlanningView.DateUpdateView(selectedMatches: matches) + } + +// .onChange(of: editMode?.wrappedValue) { +// if editMode?.wrappedValue == .active, isExpanded == false { +// isExpanded = true +// } else if editMode?.wrappedValue == .inactive, isExpanded == true { +// isExpanded = false +// } +// } } } } @@ -297,7 +466,7 @@ struct PlanningView: View { let matches: [Match] var body: some View { - ForEach(matches) { match in + ForEach(matches, id: \.stringId) { match in NavigationLink { MatchDetailView(match: match) .matchViewStyle(.sectionedStandardStyle) @@ -309,6 +478,7 @@ struct PlanningView: View { } struct MatchRowView: View { + @Environment(\.matchViewStyle) private var matchViewStyle let match: Match var body: some View { @@ -319,15 +489,20 @@ struct PlanningView: View { } label: { if let groupStage = match.groupStageObject { Text(groupStage.groupStageTitle(.title)) + Text(match.matchTitle()) } else if let round = match.roundObject { Text(round.roundTitle()) + if round.index > 0 { + Text(match.matchTitle()) + } + } + if matchViewStyle == .feedStyle, let tournament = match.currentTournament() { + Text(tournament.tournamentTitle()) } - Text(match.matchTitle()) } } } - struct HeaderView: View { @Environment(\.filterOption) private var filterOption @Environment(\.showFinishedMatches) private var showFinishedMatches @@ -365,7 +540,6 @@ struct PlanningView: View { struct TimeSlotHeaderView: View { let key: Date let matches: [Match] - @Environment(Tournament.self) var tournament: Tournament var body: some View { LabeledContent { @@ -378,176 +552,251 @@ struct PlanningView: View { .font(.title) .fontWeight(.semibold) } + + let names = matches.sorted(by: \.computedOrder) + .compactMap({ $0.roundTitle() }) + .reduce(into: [String]()) { uniqueNames, name in + if !uniqueNames.contains(name) { + uniqueNames.append(name) + } + } + Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail) + // if matches.count <= matches.first?.courtCount() ?? { + // } else { + // Text(matches.count.formatted().appending(" matchs")) + // } + + } + } + } + + struct CourtOptionsView: View { + let timeSlots: [Date: [Match]] + let underlined: Bool + var allMatches: [Match] { + timeSlots.flatMap { $0.value } + } + + private func _removeCourts() { + allMatches.forEach { match in + match.courtIndex = nil + } + } + + private func _eventCourtCount() -> Int { timeSlots.first?.value.first?.currentTournament()?.eventObject()?.eventCourtCount() ?? 2 + } + + private func _save() { + let groupByTournaments = allMatches.grouped { match in + match.currentTournament() + } + groupByTournaments.forEach { tournament, matches in + tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches) + } + } + + var body: some View { + Menu { + Button("Supprimer") { + _removeCourts() + _save() + } - if matches.count <= tournament.courtCount { - let names = matches.sorted(by: \.computedOrder) - .compactMap({ $0.roundTitle() }) - .reduce(into: [String]()) { uniqueNames, name in - if !uniqueNames.contains(name) { - uniqueNames.append(name) + Button("Tirer au sort") { + _removeCourts() + + let eventCourtCount = _eventCourtCount() + + for slot in timeSlots { + var courtsAvailable = Array(0...eventCourtCount) + let matches = slot.value + matches.forEach { match in + if let rand = courtsAvailable.randomElement() { + match.courtIndex = rand + courtsAvailable.remove(elements: [rand]) } } - Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail) - } else { - Text(matches.count.formatted().appending(" matchs")) + } + _save() + } + Button("Fixer par ordre croissant") { + _removeCourts() + + let eventCourtCount = _eventCourtCount() + + for slot in timeSlots { + var courtsAvailable = Array(0.. Int { -// timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count -// } -// -// private func _timeSlotView(key: Date, matches: [Match]) -> some View { -// LabeledContent { -// Text(self._formattedMatchCount(matches.count)) -// } label: { -// if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { -// Text("Aucun horaire") -// } else { -// Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) -// } -// if matches.count <= tournament.courtCount { -// let names = matches.sorted(by: \.computedOrder) -// .compactMap({ $0.roundTitle() }) -// .reduce(into: [String]()) { uniqueNames, name in -// if !uniqueNames.contains(name) { -// uniqueNames.append(name) -// } -// } -// Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail) -// } else { -// Text(matches.count.formatted().appending(" matchs")) -// } -// } -// } -// -// fileprivate func _formattedMatchCount(_ count: Int) -> String { -// return "\(count.formatted()) match\(count.pluralSuffix)" -// } -// } + // struct BySlotView: View { + // @Environment(Tournament.self) var tournament: Tournament + // let days: [Date] + // let keys: [Date] + // let timeSlots: [Date:[Match]] + // let matches: [Match] + // let selectedDay: Date? + // let filterOption: PlanningFilterOption + // let showFinishedMatches: Bool + // + // var body: some View { + // List { + // if matches.allSatisfy({ $0.startDate == nil }) == false { + // ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in + // Section { + // ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in + // if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) { + // DisclosureGroup { + // ForEach(_matches) { match in + // NavigationLink { + // MatchDetailView(match: match) + // .matchViewStyle(.sectionedStandardStyle) + // + // } label: { + // LabeledContent { + // if let courtName = match.courtName() { + // Text(courtName) + // } + // } label: { + // if let groupStage = match.groupStageObject { + // Text(groupStage.groupStageTitle(.title)) + // } else if let round = match.roundObject { + // Text(round.roundTitle()) + // } + // Text(match.matchTitle()) + // } + // } + // } + // } label: { + // _timeSlotView(key: key, matches: _matches) + // } + // } + // } + // .onMove(perform: moveSection) + // } header: { + // HStack { + // if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + // Text("Sans horaire") + // } else { + // Text(day.formatted(.dateTime.day().weekday().month())) + // } + // Spacer() + // let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) + // if showFinishedMatches { + // Text(self._formattedMatchCount(count)) + // } else { + // Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") + // } + // } + // } footer: { + // if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + // Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") + // } + // } + // .headerProminence(.increased) + // } + // } + // } + // } + // + // func moveSection(from source: IndexSet, to destination: Int) { + // let daySlots = keys.filter { selectedDay == nil || $0.dayInt == selectedDay?.dayInt }.sorted() + // + // guard let sourceIdx = source.first, + // sourceIdx < daySlots.count, + // destination <= daySlots.count else { + // return + // } + // + // // Create a mutable copy of the time slots for this day + // var slotsToUpdate = daySlots + // + // let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination) - 1 + // print(updateRange) + // + // // Perform the move in the array + // let sourceTime = slotsToUpdate.remove(at: sourceIdx) + // if sourceIdx < destination { + // slotsToUpdate.insert(sourceTime, at: destination - 1) + // } else { + // slotsToUpdate.insert(sourceTime, at: destination) + // } + // + // // Update matches by swapping their startDates + // for index in updateRange { + // // Find the new time slot for these matches + // let oldStartTime = slotsToUpdate[index] + // let newStartTime = daySlots[index] + // guard let matchesToUpdate = timeSlots[oldStartTime] else { continue } + // print("moving", oldStartTime, "to", newStartTime) + // + // // Update each match with the new start time + // for match in matchesToUpdate { + // match.startDate = newStartTime + // } + // } + // + // try? self.tournament.tournamentStore?.matches.addOrUpdate(contentOfs: matches) + // } + // + // + // private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int { + // timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count + // } + // + // private func _timeSlotView(key: Date, matches: [Match]) -> some View { + // LabeledContent { + // Text(self._formattedMatchCount(matches.count)) + // } label: { + // if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { + // Text("Aucun horaire") + // } else { + // Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + // } + // if matches.count <= tournament.courtCount { + // let names = matches.sorted(by: \.computedOrder) + // .compactMap({ $0.roundTitle() }) + // .reduce(into: [String]()) { uniqueNames, name in + // if !uniqueNames.contains(name) { + // uniqueNames.append(name) + // } + // } + // Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail) + // } else { + // Text(matches.count.formatted().appending(" matchs")) + // } + // } + // } + // + // fileprivate func _formattedMatchCount(_ count: Int) -> String { + // return "\(count.formatted()) match\(count.pluralSuffix)" + // } + // } } enum PlanningFilterOption: Int, CaseIterable, Identifiable { var id: Int { self.rawValue } - + case byDefault case byCourt - + func localizedPlanningLabel() -> String { switch self { case .byCourt: @@ -558,7 +807,6 @@ enum PlanningFilterOption: Int, CaseIterable, Identifiable { } } - struct FilterOptionKey: EnvironmentKey { static let defaultValue: PlanningFilterOption = .byDefault }