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.
655 lines
28 KiB
655 lines
28 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
|
|
@State private var deletingDateMatchesDone: Bool = false
|
|
@State private var deletingDone: Bool = false
|
|
@State private var presentFormatHelperView: 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, courtsAvailable: Set(tournament.courtsAvailable()))
|
|
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: {
|
|
LabeledContent {
|
|
Text(event.courtsUnavailability.count.formatted())
|
|
} label: {
|
|
Text("Créneaux d'indisponibilités")
|
|
}
|
|
}
|
|
}
|
|
|
|
NavigationLink {
|
|
MultiCourtPickerView(matchScheduler: matchScheduler)
|
|
.environment(tournament)
|
|
} label: {
|
|
LabeledContent {
|
|
Text(matchScheduler.courtsAvailable.count.formatted() + "/" + tournament.courtCount.formatted())
|
|
} label: {
|
|
Text("Sélection des terrains")
|
|
if matchScheduler.courtsAvailable.count > tournament.courtCount {
|
|
Text("Attention !")
|
|
.tint(.red)
|
|
}
|
|
}
|
|
}
|
|
} 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)
|
|
}
|
|
|
|
let event = tournament.eventObject()
|
|
Section {
|
|
NavigationLink {
|
|
_optionsView()
|
|
} label: {
|
|
Text("Voir plus d'options intelligentes")
|
|
}
|
|
|
|
if let event, event.tournaments.count > 1 {
|
|
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) {
|
|
Text("Ne pas tenir compte des autres tournois")
|
|
}
|
|
}
|
|
} footer: {
|
|
if let event, event.tournaments.count > 1 {
|
|
Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.")
|
|
}
|
|
}
|
|
|
|
_smartView()
|
|
}
|
|
.navigationTitle("Réglages")
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button {
|
|
presentFormatHelperView = true
|
|
} label: {
|
|
Text("Aide-mémoire")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $presentFormatHelperView) {
|
|
NavigationStack {
|
|
MatchFormatGuideView()
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
Button("Retour", role: .cancel) {
|
|
presentFormatHelperView = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.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) {
|
|
matchScheduler.courtsAvailable = Set(tournament.courtsAvailable())
|
|
_save()
|
|
}
|
|
.onChange(of: tournament.dayDuration) {
|
|
_save()
|
|
}
|
|
}
|
|
|
|
private func _localizedFooterMessage(groupStagesWithDateIsEmpty: Bool, roundsWithDateIsEmpty: Bool) -> String {
|
|
let base = "Supprime les horaires des matchs restants non démarrés."
|
|
let extend = " Garde les horaires définis pour les "
|
|
if groupStagesWithDateIsEmpty && roundsWithDateIsEmpty {
|
|
return base
|
|
} else if groupStagesWithDateIsEmpty, roundsWithDateIsEmpty == false {
|
|
return base + extend + "manches du tableau."
|
|
} else if roundsWithDateIsEmpty, groupStagesWithDateIsEmpty == false {
|
|
return base + extend + "poules."
|
|
} else {
|
|
return base + extend + "poules et les manches du tableau."
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _smartView() -> some View {
|
|
let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false })
|
|
let allGroupStages = tournament.allGroupStages()
|
|
let allRounds = tournament.allRounds()
|
|
let matchesWithDate = allMatches.filter({ $0.startDate != nil })
|
|
|
|
let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate)
|
|
|
|
let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament)
|
|
|
|
_formatPerDayView(matchCountPerDay: countedSet)
|
|
|
|
let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil })
|
|
let roundsWithDate = allRounds.filter({ $0.startDate != nil })
|
|
if matchesWithDate.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(_localizedFooterMessage(groupStagesWithDateIsEmpty: groupStagesWithDate.isEmpty, roundsWithDateIsEmpty: roundsWithDate.isEmpty))
|
|
}
|
|
}
|
|
|
|
if groupStagesWithDate.isEmpty == false {
|
|
Section {
|
|
RowButtonView("Supprimer les horaires des poules", role: .destructive) {
|
|
do {
|
|
deletingDone = false
|
|
allGroupStages.forEach({ $0.startDate = nil })
|
|
try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages)
|
|
|
|
deletingDone = true
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if roundsWithDate.isEmpty == false {
|
|
Section {
|
|
RowButtonView("Supprimer les horaires du tableau", role: .destructive) {
|
|
do {
|
|
deletingDone = false
|
|
allRounds.forEach({ $0.startDate = nil })
|
|
try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds)
|
|
deletingDone = true
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
} footer: {
|
|
Text("Supprime les horaires définis pour les manches du tableau.")
|
|
}
|
|
}
|
|
|
|
if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false {
|
|
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)
|
|
}
|
|
}
|
|
} footer: {
|
|
Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.")
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
Section {
|
|
RowButtonView("Debug delete all dates", role: .destructive) {
|
|
do {
|
|
deletingDone = false
|
|
tournament.allMatches().forEach({
|
|
$0.startDate = nil
|
|
$0.endDate = nil
|
|
$0.confirmed = false
|
|
})
|
|
try self.tournamentStore.matches.addOrUpdate(contentOfs: tournament.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)
|
|
}
|
|
}
|
|
} footer: {
|
|
Text("Supprime les horaires des matchs, les horaires définis pour les poules et les manches du tableau.")
|
|
}
|
|
#endif
|
|
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
@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 en parallèle")
|
|
} else {
|
|
Text("une poule sera jouée à 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.simultaneousStart) {
|
|
Text("Démarrage simultané")
|
|
}
|
|
} footer: {
|
|
Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.")
|
|
}
|
|
}
|
|
|
|
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.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)
|
|
}
|
|
}
|
|
|
|
private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] {
|
|
var matchesByDay = [Date: [Match]]()
|
|
let calendar = Calendar.current
|
|
|
|
for match in matches {
|
|
// Extract day/month/year and create a date with only these components
|
|
let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting)
|
|
let strippedDate = calendar.date(from: components)!
|
|
|
|
// Group matches by the strippedDate (only day/month/year)
|
|
if matchesByDay[strippedDate] == nil {
|
|
matchesByDay[strippedDate] = []
|
|
}
|
|
|
|
let shouldIncludeMatch: Bool
|
|
switch match.matchType {
|
|
case .groupStage:
|
|
shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.groupStage != nil }.compactMap { $0.groupStage }.contains(match.groupStage!)
|
|
case .bracket:
|
|
shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.round != nil }.compactMap { $0.round }.contains(match.round!)
|
|
case .loserBracket:
|
|
shouldIncludeMatch = true
|
|
}
|
|
|
|
if shouldIncludeMatch {
|
|
matchesByDay[strippedDate]!.append(match)
|
|
}
|
|
}
|
|
|
|
return matchesByDay
|
|
}
|
|
|
|
private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] {
|
|
let days = matchesByDay.keys
|
|
var matchCountPerDay = [Date: NSCountedSet]()
|
|
|
|
for day in days {
|
|
if let matches = matchesByDay[day] {
|
|
var groupStageCount = 0
|
|
let countedSet = NSCountedSet()
|
|
|
|
for match in matches {
|
|
switch match.matchType {
|
|
case .groupStage:
|
|
if let groupStage = match.groupStageObject {
|
|
if groupStageCount < groupStage.size - 1 {
|
|
groupStageCount = groupStage.size - 1
|
|
}
|
|
}
|
|
case .bracket:
|
|
countedSet.add(match.matchFormat)
|
|
case .loserBracket:
|
|
break
|
|
}
|
|
}
|
|
|
|
if groupStageCount > 0 {
|
|
for _ in 0..<groupStageCount {
|
|
countedSet.add(tournament.groupStageMatchFormat)
|
|
}
|
|
}
|
|
|
|
if let loserRounds = matches.filter({ $0.round != nil }).filter({ $0.roundObject?.parent == nil }).sorted(by: \.computedStartDateForSorting).last?.roundObject?.loserRounds() {
|
|
|
|
let ids = matches.map { $0.id }
|
|
for loserRound in loserRounds {
|
|
if let first = loserRound.playedMatches().first {
|
|
if ids.contains(first.id) {
|
|
countedSet.add(first.matchFormat)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
matchCountPerDay[day] = countedSet
|
|
}
|
|
}
|
|
|
|
return matchCountPerDay
|
|
}
|
|
|
|
private func _formatPerDayView(matchCountPerDay: [Date: NSCountedSet]) -> some View {
|
|
ForEach(Array(matchCountPerDay.keys).sorted(), id: \.self) { date in
|
|
if let countedSet = matchCountPerDay[date] {
|
|
Section {
|
|
let totalMatches = countedSet.totalCount()
|
|
ForEach(Array(countedSet).compactMap { $0 as? MatchFormat }, id: \.self) { matchFormat in
|
|
|
|
let count = countedSet.count(for: matchFormat)
|
|
let totalForThisFormat = matchFormat.maximumMatchPerDay(for: totalMatches)
|
|
let error = count > totalForThisFormat
|
|
// Presenting LabeledContent for each match format and its count
|
|
LabeledContent {
|
|
Image(systemName: error ? "exclamationmark.triangle" : "checkmark")
|
|
.font(.title3)
|
|
.foregroundStyle(error ? .red : .green)
|
|
} label: {
|
|
let label : String = "\(count) match\(count.pluralSuffix) en \(matchFormat.format)"
|
|
let optionA : String = "aucun match possible à ce format"
|
|
let optionB : String = "pas plus de " + totalForThisFormat.formatted() + " match\(totalForThisFormat.pluralSuffix) à ce format"
|
|
let subtitle : String = (totalForThisFormat == 0) ? optionA : optionB
|
|
Text(label)
|
|
Text(subtitle)
|
|
}
|
|
}
|
|
} header: {
|
|
Text(date.formatted(.dateTime.weekday(.abbreviated).day(.twoDigits).month(.abbreviated)))
|
|
} footer: {
|
|
let totalMatches = countedSet.totalCount()
|
|
Text("Une équipe jouera potentiellement jusqu'à \(totalMatches) match\(totalMatches.pluralSuffix) ce jour.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to format date to string (you can customize the format)
|
|
private func _formattedDate(_ date: Date) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium // Set the date style (e.g., Oct 12, 2024)
|
|
return formatter.string(from: date)
|
|
}
|
|
|
|
|
|
}
|
|
|
|
// Extension to compute the total count in an NSCountedSet
|
|
extension NSCountedSet {
|
|
func totalCount() -> Int {
|
|
var total = 0
|
|
for element in self {
|
|
total += self.count(for: element)
|
|
}
|
|
return total
|
|
}
|
|
}
|
|
|
|
//#Preview {
|
|
// PlanningSettingsView(tournament: Tournament.mock())
|
|
//}
|
|
|