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/Round/RoundView.swift

359 lines
17 KiB

//
// RoundView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 30/03/2024.
//
import SwiftUI
import LeStorage
import TipKit
struct RoundView: View {
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State private var selectedSeedGroup: SeedInterval?
@State private var spaceLeft: [Match] = []
@State private var seedSpaceLeft: [Match] = []
@State private var availableSeedGroup: SeedInterval?
@State private var showPrintScreen: Bool = false
var upperRound: UpperRound
private func _getAvailableSeedGroup() async {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _getAvailableSeedGroup of: ", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: upperRound.round.index)
}
private func _getSpaceLeft() async {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _getSpaceLeft of: ", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
self.spaceLeft.removeAll()
self.seedSpaceLeft.removeAll()
let displayableMatches: [Match] = self.upperRound.round.displayableMatches()
displayableMatches.forEach { match in
let count: Int = match.teamScores.count
if count == 0 {
seedSpaceLeft.append(match)
} else if count == 1 {
spaceLeft.append(match)
}
}
}
var showVisualDrawView: Binding<Bool> { Binding(
get: { selectedSeedGroup != nil },
set: {
if $0 == false {
selectedSeedGroup = nil
}
}
)}
var body: some View {
List {
let displayableMatches = upperRound.round.displayableMatches().sorted(by: \.index)
if displayableMatches.isEmpty {
Section {
ContentUnavailableView("Aucun match dans cette manche", systemImage: "tennisball")
}
} else if isEditingTournamentSeed.wrappedValue == false {
//(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue })
let printTip = PrintTip()
TipView(printTip) { actions in
showPrintScreen = true
}
.tipStyle(tint: .master, asSection: true)
if upperRound.loserRounds.isEmpty == false {
Section {
NavigationLink {
LoserRoundsView(upperBracketRound: upperRound)
.environment(tournament)
.navigationTitle(upperRound.correspondingLoserRoundTitle)
} label: {
LabeledContent {
let status = upperRound.status()
if status.0 == status.1 {
Image(systemName: "checkmark").foregroundStyle(.green)
} else {
Text("\(status.0) terminé\(status.0.pluralSuffix) sur \(status.1)")
}
} label: {
Text(upperRound.correspondingLoserRoundTitle)
}
}
}
}
} else {
let disabledMatchesCount = BracketEditTip.matchesHidden
if disabledMatchesCount > 0 {
let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle())
TipView(bracketTip).tipStyle(tint: .green, asSection: true)
Section {
let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount)
LabeledContent {
Text(leftToPlay.formatted()).font(.largeTitle)
} label: {
Text("Match\(leftToPlay.pluralSuffix) à jouer \(upperRound.title)")
Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement")
}
}
}
let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams()
if availableSeeds.isEmpty == false, let availableSeedGroup {
Section {
RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) {
tournament.setSeeds(inRoundIndex: upperRound.round.index, inSeedGroup: availableSeedGroup)
await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
await _getSpaceLeft()
await _getAvailableSeedGroup()
}
} footer: {
if availableSeedGroup.isFixed() == false {
Text("Le tirage au sort ne sera pas visuel. Toutes les équipes de ce chapeau seront tirées.")
}
}
if (availableSeedGroup.isFixed() == false) {
Section {
RowButtonView("Tirage au sort \(availableSeedGroup.localizedLabel()) visuel") {
self.selectedSeedGroup = availableSeedGroup
}
} footer: {
Text("Le tirage au sort sera visuel et automatique, n'hésitez pas à enregistrer une vidéo de votre écran. Toutes les équipes de ce chapeau seront tirées les unes après les autres.")
}
}
}
if availableQualifiedTeams.isEmpty == false {
if spaceLeft.isEmpty == false {
Section {
DisclosureGroup {
ForEach(availableQualifiedTeams) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: spaceLeft) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
}
await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
await _getSpaceLeft()
await _getAvailableSeedGroup()
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
}
} label: {
Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count)
}
} header: {
Text("Tirage au sort visuel d'un qualifié").font(.subheadline)
}
}
}
if availableSeeds.isEmpty == false {
if seedSpaceLeft.isEmpty == false {
Section {
DisclosureGroup {
ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false)
}
await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
await _getSpaceLeft()
await _getAvailableSeedGroup()
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
}
} else if spaceLeft.isEmpty == false {
Section {
DisclosureGroup {
ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: spaceLeft) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
}
await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
await _getSpaceLeft()
await _getAvailableSeedGroup()
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
}
}
}
}
if isEditingTournamentSeed.wrappedValue == true {
let slideToDelete = SlideToDeleteSeedTip()
TipView(slideToDelete).tipStyle(tint: .logoRed, asSection: true)
}
ForEach(displayableMatches) { match in
let matchTitle = match.matchTitle(.short, inMatches: displayableMatches)
Section {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle, title: matchTitle)
} header: {
HStack {
Text(upperRound.round.roundTitle(.wide))
if upperRound.round.index > 0 {
Text(matchTitle)
} else {
let tournamentTeamCount = tournament.teamCount
if let seedIntervalPointRange = upperRound.round.seedInterval()?.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournamentTeamCount) {
Spacer()
Text(seedIntervalPointRange)
.font(.caption)
}
}
#if DEBUG
Spacer()
Text(match.teamScores.count.formatted())
#endif
}
}
}
}
.navigationDestination(isPresented: $showPrintScreen) {
PrintSettingsView(tournament: tournament)
}
.onAppear {
Task {
await _prepareRound()
}
let seeds = upperRound.round.seeds()
SlideToDeleteSeedTip.seeds = seeds.count
PrintTip.seeds = seeds.count
BracketEditTip.matchesHidden = upperRound.round.getDisabledMatches().count
}
.fullScreenCover(isPresented: showVisualDrawView) {
if let availableSeedGroup = selectedSeedGroup {
let seeds = tournament.seeds(inSeedGroup: availableSeedGroup)
let availableSeedSpot = tournament.availableSeedSpot(inRoundIndex: upperRound.round.index)
NavigationStack {
SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true) { draws in
Task {
draws.forEach { drawResult in
seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: false)
}
await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
}
}
}
}
}
.headerProminence(.increased)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
if isEditingTournamentSeed.wrappedValue == true {
Task {
await _save()
}
}
isEditingTournamentSeed.wrappedValue.toggle()
}
}
}
}
private func _save() async {
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch {
Logger.error(error)
}
//todo should be done server side
let rounds = tournament.rounds()
rounds.forEach { round in
let matches = round.playedMatches()
matches.forEach { match in
match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches))
}
}
let allRoundMatches = tournament.allRoundMatches()
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: allRoundMatches)
} catch {
Logger.error(error)
}
}
private func _prepareRound() async {
Task {
await _getSpaceLeft()
}
Task {
await _getAvailableSeedGroup()
}
}
}
//#Preview {
// RoundView(round: Round.mock())
// .environment(Tournament.mock())
//}