You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Views/Planning/PlanningView.swift

246 lines
10 KiB

//
// PlanningView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
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
let allMatches: [Match]
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) {
self.allMatches = matches
_selectedScheduleDestination = selectedScheduleDestination
}
var matches: [Match] {
allMatches.filter({ showFinishedMatches || $0.endDate == nil })
}
var timeSlots: [Date:[Match]] {
Dictionary(grouping: matches) { $0.startDate ?? .distantFuture }
}
func days(timeSlots: [Date:[Match]]) -> [Date] {
Set(timeSlots.keys.map { $0.startOfDay }).sorted()
}
func keys(timeSlots: [Date:[Match]]) -> [Date] {
timeSlots.keys.sorted()
}
enum PlanningFilterOption: Int, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case byDefault
case byCourt
func localizedPlanningLabel() -> String {
switch self {
case .byCourt:
return "Par terrain"
case .byDefault:
return "Par ordre des matchs"
}
}
}
private func _computedTitle(days: [Date]) -> String {
if let selectedDay {
return selectedDay.formatted(.dateTime.day().weekday().month())
} else {
if days.count > 1 {
return "Tous les jours"
} else {
return "Horaires"
}
}
}
var body: some View {
let timeSlots = self.timeSlots
let keys = self.keys(timeSlots: timeSlots)
let days = self.days(timeSlots: timeSlots)
let matches = matches
BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay, filterOption: filterOption, showFinishedMatches: showFinishedMatches)
.navigationTitle(Text(_computedTitle(days: days)))
.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)
}
}
ToolbarItemGroup(placement: .topBarTrailing) {
Menu {
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)
} label: {
Label("Filtrer", systemImage: "clock.badge.checkmark")
.symbolVariant(showFinishedMatches ? .fill : .none)
}
Menu {
Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0)
}
} label: {
Text("Option de triage")
}
.labelsHidden()
.pickerStyle(.inline)
} label: {
Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(filterOption == .byCourt ? .fill : .none)
}
}
})
.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
}
}
}
}
}
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)
}
}
}
} 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)
}
}
}
}
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)"
}
}
}
//#Preview {
// PlanningView(matches: [], selectedScheduleDestination: .constant(nil))
//}