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.
297 lines
12 KiB
297 lines
12 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) {
|
|
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.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)
|
|
}
|
|
|
|
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)
|
|
.font(.footnote)
|
|
.underline()
|
|
.frame(maxWidth: .infinity) // Make buttons take equal space
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.master)
|
|
}
|
|
}
|
|
|