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/CourtAvailabilitySettingsVi...

359 lines
15 KiB

//
// CourtAvailabilitySettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import SwiftUI
import LeStorage
import PadelClubData
struct CourtAvailabilitySettingsView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
let event: Event
@State private var showingPopover: Bool = false
@State private var editingSlot: PadelClubData.DateInterval?
var courtsUnavailability: [Int: [PadelClubData.DateInterval]] {
let groupedBy = Dictionary(grouping: event.courtsUnavailability, by: { dateInterval in
return dateInterval.courtIndex
})
return groupedBy
}
var body: some View {
List {
let keys = courtsUnavailability.keys.sorted()
ForEach(keys, id: \.self) { key in
if let dates = courtsUnavailability[key] {
Section {
ForEach(dates.sorted(by: \.startDate)) { dateInterval in
Menu {
Button("dupliquer") {
let duplicatedDateInterval = DateInterval(event: event.id, courtIndex: (dateInterval.courtIndex+1)%tournament.courtCount, startDate: dateInterval.startDate, endDate: dateInterval.endDate)
do {
try dataStore.dateIntervals.addOrUpdate(instance: duplicatedDateInterval)
} catch {
Logger.error(error)
}
}
Button("éditer") {
editingSlot = dateInterval
}
Button("effacer", role: .destructive) {
do {
try dataStore.dateIntervals.delete(instance: dateInterval)
} catch {
Logger.error(error)
}
}
} label: {
HStack {
VStack(alignment: .leading, spacing: 0) {
Text(dateInterval.startDate.localizedTime()).font(.largeTitle)
Text(dateInterval.startDate.localizedDay()).font(.caption)
}
Spacer()
VStack {
Image(systemName: "arrowshape.forward.fill")
.tint(.master)
Text("indisponible").foregroundStyle(.logoRed).font(.caption)
}
Spacer()
VStack(alignment: .trailing, spacing: 0) {
Text(dateInterval.endDate.localizedTime()).font(.largeTitle)
Text(dateInterval.endDate.localizedDay()).font(.caption)
}
}
}
.swipeActions {
Button(role: .destructive) {
do {
try dataStore.dateIntervals.delete(instance: dateInterval)
} catch {
Logger.error(error)
}
} label: {
LabelDelete()
}
}
.buttonStyle(.plain)
}
} header: {
HStack {
Text(Court.courtIndexedTitle(atIndex: key))
Spacer()
if let courtName = tournament.courtNameIfAvailable(atIndex: key) {
Text(courtName)
}
}
}
.headerProminence(.increased)
}
}
}
.overlay {
if courtsUnavailability.isEmpty {
ContentUnavailableView {
Label("Tous les pistes sont disponibles", systemImage: "checkmark.circle.fill").tint(.green)
} description: {
Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs pistes, 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) {
if #available(iOS 26.0, *) {
BarButtonView("Ajouter une indisponibilité", icon: "plus") {
showingPopover = true
}
} else {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
showingPopover = true
}
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible")
.sheet(isPresented: $showingPopover) {
CourtAvailabilityEditorView(event: event)
}
.sheet(item: $editingSlot) { editingSlot in
CourtAvailabilityEditorView(editingSlot: editingSlot, event: event)
}
}
}
struct CourtPicker: View {
@Environment(Tournament.self) var tournament: Tournament
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text(tournament.courtName(atIndex: $0))
}
}
}
}
struct CourtAvailabilityEditorView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
var editingSlot: PadelClubData.DateInterval?
let event: Event
@State private var courtIndex: Int
@State private var startDate: Date
@State private var endDate: Date
init(editingSlot: PadelClubData.DateInterval, event: Event) {
self.editingSlot = editingSlot
self.event = event
_courtIndex = .init(wrappedValue: editingSlot.courtIndex)
_startDate = .init(wrappedValue: editingSlot.startDate)
_endDate = .init(wrappedValue: editingSlot.endDate)
}
init(event: Event) {
self.event = event
_courtIndex = .init(wrappedValue: 0)
let startDate = event.eventStartDate()
_startDate = .init(wrappedValue: event.eventStartDate())
_endDate = .init(wrappedValue: startDate.addingTimeInterval(5400))
}
var body: some View {
NavigationStack {
Form {
Section {
CourtPicker(title: "Piste", selection: $courtIndex, maxCourt: tournament.courtCount)
}
Section {
DatePicker("Début", selection: $startDate)
.onChange(of: startDate) {
if endDate < startDate {
endDate = startDate.addingTimeInterval(90*60)
}
}
DatePicker("Fin", selection: $endDate)
.onChange(of: endDate) {
if startDate > endDate {
startDate = endDate.addingTimeInterval(-90*60)
}
}
} footer: {
FooterButtonView("jour entier") {
startDate = startDate.startOfDay
endDate = startDate.tomorrowAtNine.startOfDay
}
}
Section {
DateAdjusterView(date: $startDate)
} header: {
Text("Modifier rapidement l'horaire de début")
}
Section {
DateAdjusterView(date: $endDate)
} header: {
Text("Modifier rapidement l'horaire de fin")
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView {
if editingSlot == nil {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
do {
try dataStore.dateIntervals.addOrUpdate(instance: dateInterval)
} catch {
Logger.error(error)
}
} else {
editingSlot?.courtIndex = courtIndex
editingSlot?.endDate = endDate
editingSlot?.startDate = startDate
do {
try dataStore.dateIntervals.addOrUpdate(instance: editingSlot!)
} catch {
Logger.error(error)
}
}
dismiss()
}
}
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle(_navigationTitle())
.tint(.master)
}
}
private func _navigationTitle() -> String {
editingSlot == nil ? "Nouveau créneau" : "Édition du créneau"
}
}
struct DateAdjusterView: View {
@Binding var date: Date
var time: Int?
var matchFormat: MatchFormat?
var body: some View {
HStack(spacing: 4) {
if let matchFormat {
_createButton(label: "-\(matchFormat.getEstimatedDuration())m", timeOffset: -matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "+\(matchFormat.getEstimatedDuration())m", timeOffset: +matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute)
Divider()
_createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute)
} else if let time {
_createButton(label: "-\(time)m", timeOffset: -time, component: .minute)
Divider()
_createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute)
Divider()
_createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute)
Divider()
_createButton(label: "+\(time)m", timeOffset: time, component: .minute)
} else {
_createButton(label: "-1h", timeOffset: -60, component: .minute)
Divider()
_createButton(label: "-30m", timeOffset: -30, component: .minute)
Divider()
_createButton(label: "+30m", timeOffset: 30, component: .minute)
Divider()
_createButton(label: "+1h", timeOffset: 60, component: .minute)
}
}
.font(.headline)
}
private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View {
Button(action: {
date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date
}) {
Text(label)
.lineLimit(1)
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderless)
.tint(.master)
}
}
struct StepAdjusterView: View {
@Binding var step: Int
var time: Int?
var matchFormat: MatchFormat?
var body: some View {
HStack(spacing: 4) {
if let matchFormat {
_createButton(label: "-\(matchFormat.getEstimatedDuration())m", timeOffset: -matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "+\(matchFormat.getEstimatedDuration())m", timeOffset: +matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute)
Divider()
_createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute)
} else if let time {
_createButton(label: "-\(time)m", timeOffset: -time, component: .minute)
Divider()
_createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute)
Divider()
_createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute)
Divider()
_createButton(label: "+\(time)m", timeOffset: time, component: .minute)
} else {
_createButton(label: "-1h", timeOffset: -60, component: .minute)
Divider()
_createButton(label: "-30m", timeOffset: -30, component: .minute)
Divider()
_createButton(label: "+30m", timeOffset: 30, component: .minute)
Divider()
_createButton(label: "+1h", timeOffset: 60, component: .minute)
}
}
.font(.headline)
}
private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View {
Button(action: {
step += timeOffset
}) {
Text(label)
.lineLimit(1)
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderless)
.tint(.master)
}
}