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/PlanningSettingsView.swift

327 lines
14 KiB

//
// PlanningSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
import LeStorage
struct PlanningSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Bindable var tournament: Tournament
@Bindable var matchScheduler: MatchScheduler
@State private var groupStageChunkCount: Int
@State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false
@State private var showOptions: Bool = false
@State private var issueFound: Bool = false
@State private var parallelType: Bool = false
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
init(tournament: Tournament) {
self.tournament = tournament
if let matchScheduler = tournament.matchScheduler() {
self.matchScheduler = matchScheduler
if matchScheduler.groupStageChunkCount != nil {
_parallelType = .init(wrappedValue: true)
_groupStageChunkCount = State(wrappedValue: matchScheduler.groupStageChunkCount!)
} else {
_groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue())
}
} else {
self.matchScheduler = MatchScheduler(tournament: tournament.id)
self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue())
}
}
var body: some View {
List {
if tournament.payment == nil {
SubscriptionInfoView()
}
Section {
DatePicker(selection: $tournament.startDate) {
Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized)
}
LabeledContent {
StepperView(count: $tournament.dayDuration, minimum: 1)
} label: {
Text("Durée")
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix)
}
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
if let event = tournament.eventObject() {
NavigationLink {
CourtAvailabilitySettingsView(event: event)
.environment(tournament)
} label: {
Text("Préciser les indisponibilités des terrains")
}
}
} footer: {
if let club = tournament.club() {
if tournament.courtCount < club.courtCount {
let plural = tournament.courtCount.pluralSuffix
let verb = tournament.courtCount > 1 ? "seront" : "sera"
Text("En réduisant les terrains maximum, seul\(plural) le\(plural) \(tournament.courtCount) premier\(plural) terrain\(plural) \(verb) utilisé\(plural)") + Text(", par contre, si vous gardez le nombre de terrains du club, vous pourrez plutôt préciser quel terrain n'est pas disponible.")
} else if tournament.courtCount > club.courtCount {
let isCreatedByUser = club.hasBeenCreated(by: StoreCenter.main.userId)
Button {
do {
club.courtCount = tournament.courtCount
try dataStore.clubs.addOrUpdate(instance: club)
} catch {
Logger.error(error)
}
} label: {
if isCreatedByUser {
Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.")
+ Text("Mettre à jour le club ?").underline().foregroundStyle(.master)
} else {
Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed)
}
}
.buttonStyle(.plain)
.disabled(isCreatedByUser == false)
}
}
}
if issueFound {
Text("Padel Club n'a pas réussi à définir un horaire pour tous les matchs de ce tournoi, à cause de la programmation d'autres tournois ou de l'indisponibilité des terrains.")
.foregroundStyle(.logoRed)
}
Section {
NavigationLink {
_optionsView()
} label: {
Text("Voir les options avancées")
}
}
let allMatches = tournament.allMatches()
let allGroupStages = tournament.groupStages()
let allRounds = tournament.allRounds()
let matchesWithDate = allMatches.filter({ $0.startDate != nil })
let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil })
let roundsWithDate = allRounds.filter({ $0.startDate != nil })
if matchesWithDate.isEmpty == false || groupStagesWithDate.isEmpty == false || roundsWithDate.isEmpty == false {
Section {
RowButtonView("Supprimer les horaires des matches", role: .destructive) {
do {
allMatches.forEach({
$0.startDate = nil
$0.confirmed = false
})
try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches)
} catch {
Logger.error(error)
}
}
} footer: {
Text("Garde les horaires définis pour les poules et les manches.")
}
Section {
RowButtonView("Supprimer tous les horaires", role: .destructive) {
do {
allMatches.forEach({
$0.startDate = nil
$0.confirmed = false
})
try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches)
allGroupStages.forEach({ $0.startDate = nil })
try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages)
allRounds.forEach({ $0.startDate = nil })
try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds)
} catch {
Logger.error(error)
}
}
}
}
Section {
if groupStagesWithDate.isEmpty == false {
Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.")
}
if roundsWithDate.isEmpty == false {
Text("Des dates de démarrages ont été indiqué pour les manches et seront prises en compte.")
}
RowButtonView("Horaire intelligent", role: .destructive) {
await MainActor.run {
issueFound = false
schedulingDone = false
}
self.issueFound = await _setupSchedule()
await MainActor.run {
_save()
schedulingDone = true
}
}
} footer: {
Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline()
}
}
.headerProminence(.increased)
.onAppear {
do {
try self.tournamentStore.matchSchedulers.addOrUpdate(instance: matchScheduler)
} catch {
Logger.error(error)
}
}
.overlay(alignment: .bottom) {
if schedulingDone {
if issueFound {
Label("Horaires mis à jour", systemImage: "xmark.circle.fill")
.toastFormatted()
.deferredRendering(for: .seconds(2))
} else {
Label("Horaires mis à jour", systemImage: "checkmark.circle.fill")
.toastFormatted()
.deferredRendering(for: .seconds(2))
}
}
}
.onChange(of: tournament.startDate) {
_save()
}
.onChange(of: tournament.courtCount) {
_save()
}
.onChange(of: tournament.dayDuration) {
_save()
}
}
@ViewBuilder
private func _optionsView() -> some View {
List {
if tournament.groupStageCount > 0 {
Section {
Picker(selection: $parallelType) {
Text("Auto.").tag(false)
Text("Manuel").tag(true)
} label: {
Text("Poules en parallèle")
let value = tournament.getGroupStageChunkValue()
if parallelType == false, value > 1 {
Text("\(value.formatted()) poules commenceront en parallèle")
}
}
.onChange(of: parallelType) {
if parallelType {
groupStageChunkCount = tournament.getGroupStageChunkValue()
} else {
matchScheduler.groupStageChunkCount = nil
}
}
if parallelType {
TournamentFieldsManagerView(localizedStringKey: "Poule en parallèle", count: $groupStageChunkCount, max: tournament.groupStageCount)
.onChange(of: groupStageChunkCount) {
matchScheduler.groupStageChunkCount = groupStageChunkCount
}
}
} footer: {
Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.")
}
}
Section {
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) {
Text("Ne pas tenir compte des autres tournois")
}
Toggle(isOn: $matchScheduler.randomizeCourts) {
Text("Distribuer les terrains au hasard")
}
Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) {
Text("Équilibrer les matchs d'une manche sur plusieurs tours")
}
Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) {
Text("Finir une manche, classement inclus avant de continuer")
}
}
Section {
Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) {
Text("Tenir compte des pauses")
Text("Tableau")
}
Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) {
Text("Tenir compte des pauses")
Text("Classement")
}
}
Section {
Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) {
Text("Forcer un créneau supplémentaire entre 2 phases")
}
LabeledContent {
StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
Text("Tableau")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
LabeledContent {
StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
Text("Classement")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
}
Section {
LabeledContent {
StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5)
} label: {
Text("Optimisation des créneaux")
Text("Si libre plus de \(matchScheduler.timeDifferenceLimit) minutes")
}
}
}
.navigationTitle("Options avancées")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _setupSchedule() async -> Bool {
return matchScheduler.updateSchedule(tournament: tournament)
}
private func _save() {
do {
try self.tournamentStore.matchSchedulers.addOrUpdate(instance: matchScheduler)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
//#Preview {
// PlanningSettingsView(tournament: Tournament.mock())
//}