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/Calling/BracketCallingView.swift

215 lines
8.5 KiB

//
// BracketCallingView.swift
// PadelClub
//
// Created by razmig on 15/10/2024.
//
import SwiftUI
import LeStorage
import PadelClubData
struct BracketCallingView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@State private var initialSeedRound: Int = 0
@State private var initialSeedCount: Int = 0
let tournamentRounds: [Round]
let teams: [TeamRegistration]
init(tournament: Tournament) {
let rounds = tournament.rounds()
self.tournamentRounds = rounds
self.teams = tournament.availableSeeds()
if tournament.initialSeedRound == 0, rounds.count > 0 {
let index = rounds.count - 1
_initialSeedRound = .init(wrappedValue: index)
_initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index))
} else if tournament.initialSeedRound < rounds.count {
_initialSeedRound = .init(wrappedValue: tournament.initialSeedRound)
_initialSeedCount = .init(wrappedValue: tournament.initialSeedCount)
} else if rounds.count > 0 {
let index = rounds.count - 1
_initialSeedRound = .init(wrappedValue: index)
_initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index))
}
}
var initialRound: Round {
tournamentRounds.first(where: { $0.index == initialSeedRound })!
}
func filteredRounds() -> [Round] {
tournamentRounds.filter({ $0.index >= initialSeedRound }).reversed()
}
func seedCount(forRoundIndex roundIndex: Int) -> Int {
if roundIndex < initialSeedRound { return 0 }
if roundIndex == initialSeedRound {
return initialSeedCount
}
let seedCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let previousSeedCount = self.seedCount(forRoundIndex: roundIndex - 1)
let total = seedCount - previousSeedCount
if total < 0 { return 0 }
return total
}
func seeds(forRoundIndex roundIndex: Int) -> [TeamRegistration] {
let previousSeeds: Int = (initialSeedRound..<roundIndex).map { seedCount(forRoundIndex: $0) }.reduce(0, +)
if roundIndex == tournamentRounds.count - 1 {
return Array(teams.dropFirst(previousSeeds))
} else {
return Array(teams.dropFirst(previousSeeds).prefix(seedCount(forRoundIndex: roundIndex)))
}
}
var body: some View {
List {
let uncalledTeams = teams.filter({ $0.callDate == nil })
if uncalledTeams.isEmpty == false {
NavigationLink {
TeamsCallingView(teams: uncalledTeams)
.environment(tournament)
} label: {
LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted())
}
}
PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
Section {
Picker(selection: $initialSeedRound) {
ForEach(tournamentRounds) {
Text($0.roundTitle()).tag($0.index)
}
} label: {
Text("Premier tour")
}
.onChange(of: initialSeedRound) {
initialSeedCount = RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)
}
LabeledContent {
StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound))
} label: {
Text("Têtes de série")
}
} footer: {
Text("Permet de convoquer par tour du tableau sans avoir tirer au sort les tétes de série. Vous pourrez ensuite confirmer leur horaire plus précis si le tour se joue sur plusieurs rotations. Les équipes ne peuvent pas être considéré comme convoqué au bon horaire en dehors de cet écran tant qu'elles n'ont pas été placé dans le tableau.")
}
ForEach(filteredRounds()) { round in
let seeds = seeds(forRoundIndex: round.index)
let startDate = ([round.startDate] + round.playedMatches().map { $0.startDate }).compacted().min()
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) == false })
if seeds.isEmpty == false {
Section {
NavigationLink {
_roundView(round: round, seeds: seeds)
.environment(tournament)
} label: {
CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: startDate, title: "convoquées")
}
} header: {
Text(round.roundTitle())
} footer: {
if let startDate {
CallView(teams: seeds, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle())
}
}
}
}
}
.onDisappear(perform: {
tournament.initialSeedCount = initialSeedCount
tournament.initialSeedRound = initialSeedRound
_save()
})
.headerProminence(.increased)
.navigationTitle("Pré-convocation")
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
@ViewBuilder
private func _roundView(round: Round, seeds: [TeamRegistration]) -> some View {
List {
let uncalledTeams = seeds.filter({ $0.callDate == nil })
if uncalledTeams.isEmpty == false {
NavigationLink {
TeamsCallingView(teams: uncalledTeams)
.environment(tournament)
} label: {
LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted())
}
}
let startDate = round.startDate ?? round.playedMatches().first?.startDate
let badCalled = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) })
if badCalled.isEmpty == false {
Section {
ForEach(badCalled) { team in
TeamCallView(team: team)
}
} header: {
HStack {
Text("Mauvais horaire")
Spacer()
Text(badCalled.count.formatted() + " équipe\(badCalled.count.pluralSuffix)")
}
} footer: {
if let startDate {
CallView(teams: badCalled, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle())
}
}
}
Section {
ForEach(seeds) { team in
TeamCallView(team: team)
}
} header: {
HStack {
Text(round.roundTitle())
Spacer()
Text(seeds.count.formatted() + " équipe\(seeds.count.pluralSuffix)")
}
}
}
.overlay {
if seeds.isEmpty {
ContentUnavailableView {
Label("Aucune équipe dans ce tour", systemImage: "clock.badge.questionmark")
} description: {
Text("Padel Club n'a pas réussi à déterminer quelles équipes jouent ce tour.")
} actions: {
// RowButtonView("Horaire intelligent") {
// selectedScheduleDestination = nil
// }
}
}
}
.headerProminence(.increased)
.navigationTitle(round.roundTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
//#Preview {
// SeedsCallingView()
//}