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.
372 lines
17 KiB
372 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 showPrintScreen: Bool = false
|
|
|
|
var upperRound: UpperRound
|
|
|
|
init(upperRound: UpperRound) {
|
|
self.upperRound = upperRound
|
|
let seeds = upperRound.round.seeds()
|
|
SlideToDeleteSeedTip.seeds = seeds.count
|
|
PrintTip.seeds = seeds.count
|
|
BracketEditTip.matchesHidden = upperRound.round.getDisabledMatches().count
|
|
}
|
|
|
|
var tournamentStore: TournamentStore {
|
|
return self.tournament.tournamentStore
|
|
}
|
|
|
|
private var spaceLeft: [Match] {
|
|
let displayableMatches: [Match] = self.upperRound.round.playedMatches()
|
|
return displayableMatches.filter { match in
|
|
match.teamScores.count == 1
|
|
}
|
|
}
|
|
|
|
private var seedSpaceLeft: [Match] {
|
|
let displayableMatches: [Match] = self.upperRound.round.playedMatches()
|
|
return displayableMatches.filter { match in
|
|
match.teamScores.count == 0
|
|
}
|
|
}
|
|
|
|
private var availableSeedGroup: SeedInterval? {
|
|
tournament.seedGroupAvailable(atRoundIndex: upperRound.round.index)
|
|
}
|
|
|
|
var showVisualDrawView: Binding<Bool> { Binding(
|
|
get: { selectedSeedGroup != nil },
|
|
set: {
|
|
if $0 == false {
|
|
selectedSeedGroup = nil
|
|
}
|
|
}
|
|
)}
|
|
|
|
var body: some View {
|
|
List {
|
|
let displayableMatches = upperRound.round.playedMatches().sorted(by: \.index)
|
|
if displayableMatches.isEmpty {
|
|
Section {
|
|
ContentUnavailableView("Aucun match dans cette manche", systemImage: "tennisball")
|
|
}
|
|
}
|
|
|
|
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())
|
|
} label: {
|
|
Text("Match\(leftToPlay.pluralSuffix) à jouer \(upperRound.title)")
|
|
}
|
|
} footer: {
|
|
Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement")
|
|
}
|
|
}
|
|
|
|
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.round.index > 0 {
|
|
let correspondingLoserRoundTitle = upperRound.round.correspondingLoserRoundTitle()
|
|
Section {
|
|
NavigationLink {
|
|
LoserRoundsView(upperBracketRound: upperRound)
|
|
.environment(tournament)
|
|
.navigationTitle(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(correspondingLoserRoundTitle)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Match de classement")
|
|
}
|
|
}
|
|
} else {
|
|
let availableSeeds = tournament.availableSeeds()
|
|
let availableQualifiedTeams = tournament.availableQualifiedTeams()
|
|
|
|
if availableSeeds.isEmpty == false, let availableSeedGroup {
|
|
Section {
|
|
RowButtonView("Placer \(availableSeedGroup.localizedInterval())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) {
|
|
Task {
|
|
tournament.setSeeds(inRoundIndex: upperRound.round.index, inSeedGroup: availableSeedGroup)
|
|
_save()
|
|
}
|
|
}
|
|
} 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.localizedInterval()) 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 {
|
|
let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false
|
|
let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft
|
|
if availableSeedSpot.isEmpty == false {
|
|
Section {
|
|
DisclosureGroup {
|
|
ForEach(availableQualifiedTeams) { team in
|
|
NavigationLink {
|
|
|
|
SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in
|
|
Task {
|
|
results.forEach { drawResult in
|
|
if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot {
|
|
team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false)
|
|
} else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match {
|
|
team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true)
|
|
}
|
|
}
|
|
_save()
|
|
}
|
|
}
|
|
} 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)
|
|
}
|
|
_save()
|
|
}
|
|
}
|
|
} 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)
|
|
}
|
|
_save()
|
|
}
|
|
}
|
|
} 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
|
|
}
|
|
} footer: {
|
|
if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true {
|
|
FooterButtonView("Désactiver", role: .destructive) {
|
|
match.disableMatch()
|
|
do {
|
|
try tournamentStore.matches.addOrUpdate(instance: match)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationDestination(isPresented: $showPrintScreen) {
|
|
PrintSettingsView(tournament: tournament)
|
|
}
|
|
.fullScreenCover(isPresented: showVisualDrawView) {
|
|
if let availableSeedGroup = selectedSeedGroup {
|
|
let seeds = tournament.seeds(inSeedGroup: availableSeedGroup)
|
|
let opposingSeeding = seedSpaceLeft.isEmpty ? true : false
|
|
let availableSeedSpot = opposingSeeding ? spaceLeft : seedSpaceLeft
|
|
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: opposingSeeding)
|
|
}
|
|
|
|
_save()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.headerProminence(.increased)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
|
|
if isEditingTournamentSeed.wrappedValue == true {
|
|
_save()
|
|
isEditingTournamentSeed.wrappedValue = false
|
|
} else {
|
|
isEditingTournamentSeed.wrappedValue = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func _save() {
|
|
do {
|
|
try self.tournamentStore.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 self.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
|
|
self.isEditingTournamentSeed.wrappedValue = false
|
|
}
|
|
}
|
|
}
|
|
|
|
//#Preview {
|
|
// RoundView(round: Round.mock())
|
|
// .environment(Tournament.mock())
|
|
//}
|
|
|
|
struct MatchSpot: SpinDrawable {
|
|
let match: Match
|
|
let teamPosition: TeamPosition
|
|
|
|
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
|
|
[match.roundTitle(), matchTitle(displayStyle: displayStyle)].compactMap { $0 }
|
|
}
|
|
|
|
func matchTitle(displayStyle: DisplayStyle) -> String {
|
|
[match.matchTitle(displayStyle), teamPositionLabel()].joined(separator: " - ")
|
|
}
|
|
|
|
func teamPositionLabel() -> String {
|
|
switch teamPosition {
|
|
case .one:
|
|
return "haut"
|
|
case .two:
|
|
return "bas"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Match {
|
|
func matchSpots() -> [MatchSpot] {
|
|
[MatchSpot(match: self, teamPosition: .one), MatchSpot(match: self, teamPosition: .two)]
|
|
}
|
|
}
|
|
|