add planning feature

newoffer2025
Raz 6 months ago
parent ecdc46a968
commit 12142cde37
  1. 11
      PadelClub/Views/Cashier/Event/EventView.swift
  2. 27
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  3. 1
      PadelClub/Views/Planning/PlanningByCourtView.swift
  4. 756
      PadelClub/Views/Planning/PlanningView.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):

@ -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)

@ -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 {

@ -5,20 +5,20 @@
// 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()
@ -32,15 +32,15 @@ struct PlanningView: View {
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()
}
@ -56,125 +56,167 @@ struct PlanningView: View {
}
}
private func _confirmationMode() -> Bool {
enableMove || editMode?.wrappedValue == .active
}
private var enableEditionBinding: Binding<Bool> {
Binding {
editMode?.wrappedValue == .active
} set: { value in
if value {
editMode?.wrappedValue = .active
} else {
editMode?.wrappedValue = .inactive
}
}
}
var body: some View {
let timeSlots = self.timeSlots
let keys = self.keys(timeSlots: timeSlots)
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")
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de filtrage")
}
Divider()
Divider()
Section {
Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0)
}
} label: {
Text("Option de triage")
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<String>()
@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,15 +357,23 @@ 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)
}
}
}
@ -240,11 +383,12 @@ struct PlanningView: View {
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)
@ -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 {
@ -379,167 +553,242 @@ struct PlanningView: View {
.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)
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()
}
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..<eventCourtCount)
let matches = slot.value
for i in 0..<matches.count {
if !courtsAvailable.isEmpty {
let court = courtsAvailable.removeFirst()
matches[i].courtIndex = court
}
}
}
_save()
}
} label: {
Text("Terrains")
.underline(underlined)
}
}
}
// 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)"
// }
// }
// 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 {
@ -558,7 +807,6 @@ enum PlanningFilterOption: Int, CaseIterable, Identifiable {
}
}
struct FilterOptionKey: EnvironmentKey {
static let defaultValue: PlanningFilterOption = .byDefault
}

Loading…
Cancel
Save