// // 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 @State private var deletingDateMatchesDone: Bool = false @State private var deletingDone: 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).lineLimit(1) } 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("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 { deletingDateMatchesDone = false allMatches.forEach({ $0.startDate = nil $0.confirmed = false }) try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) deletingDateMatchesDone = true } 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 { deletingDone = false 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) deletingDone = true } 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)) } } if deletingDone { Label("Tous les horaires ont été supprimés", systemImage: "clock.badge.xmark") .toastFormatted() .deferredRendering(for: .seconds(2)) } if deletingDateMatchesDone { Label("Horaires des matchs supprimés", systemImage: "clock.badge.xmark") .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 { if value > 1 { Text("\(value.formatted()) poules commenceront en parallèle") } else { Text("une poule sera jouer à la fois") } } } .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.randomizeCourts) { Text("Distribuer les terrains au hasard") } } Section { Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) { Text("Remplir au maximum les terrains d'une rotation") } } footer: { Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") } Section { Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { Text("Équilibrer les matchs d'une manche") } } footer: { Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") } Section { Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { Text("Ne pas tenir compte des autres tournois") } } footer: { Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi est toujours considéré comme libre.") } Section { Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) { Text("Finir une manche, classement inclus avant de continuer") } } Section { Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) { Text("Tenir compte des temps de pause réglementaires") } } header: { Text("Tableau") } Section { Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) { Text("Tenir compte des temps de pause réglementaires") } } header: { Text("Classement") } Section { Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) { Text("Forcer une rotation d'attente 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) } footer: { Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.") } Section { LabeledContent { StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5) } label: { Text("Optimisation des créneaux") Text("Si libre plus de \(matchScheduler.timeDifferenceLimit) minutes") } } footer: { Text("Cette option essaie d'optimiser les créneaux disponibles à partir du moment où ils sont à priori 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()) //}