Merge branch 'drawlog'

paca_championship
Raz 1 year ago
commit ee8a297f13
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 103
      PadelClub/Data/DrawLog.swift
  3. 25
      PadelClub/Data/Match.swift
  4. 19
      PadelClub/Data/MatchScheduler.swift
  5. 56
      PadelClub/Data/TeamRegistration.swift
  6. 132
      PadelClub/Data/Tournament.swift
  7. 2
      PadelClub/Data/TournamentStore.swift
  8. 9
      PadelClub/Utils/PadelRule.swift
  9. 12
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  10. 128
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  11. 69
      PadelClub/Views/Round/DrawLogsView.swift
  12. 107
      PadelClub/Views/Round/PreviewBracketPositionView.swift
  13. 90
      PadelClub/Views/Round/RoundSettingsView.swift
  14. 103
      PadelClub/Views/Round/RoundView.swift
  15. 85
      PadelClub/Views/Team/TeamRestingView.swift
  16. 90
      PadelClub/Views/Team/TeamRowView.swift
  17. 18
      PadelClubTests/ServerDataTests.swift

@ -424,6 +424,15 @@
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; };
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; }; FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; };
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; };
FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; };
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; };
@ -1066,6 +1075,9 @@
FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; }; FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; };
FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; }; FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; };
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; };
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = "<group>"; };
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = "<group>"; };
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
@ -1356,6 +1368,7 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */, FF6EC9012B94799200EA7F5A /* Coredata */,
FF6EC9022B9479B900EA7F5A /* Federal */, FF6EC9022B9479B900EA7F5A /* Federal */,
); );
@ -1871,6 +1884,8 @@
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */,
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */,
); );
path = Round; path = Round;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2331,6 +2346,7 @@
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */,
FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */,
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */,
FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */,
C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */,
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
@ -2391,6 +2407,7 @@
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2442,6 +2459,7 @@
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */,
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */,
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */,
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
@ -2605,6 +2623,7 @@
FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */, FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */,
FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */, FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */,
FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */, FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */,
FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */, FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */,
FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */, FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */,
FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */, FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */,
@ -2665,6 +2684,7 @@
FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */,
FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */,
FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */,
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */,
FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */, FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */,
FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */, FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2716,6 +2736,7 @@
FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */,
FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */,
FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */,
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */,
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */,
@ -2858,6 +2879,7 @@
FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */, FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */,
FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */, FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */,
FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */, FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */,
FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */, FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */,
FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */, FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */,
FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */, FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */,
@ -2918,6 +2940,7 @@
FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */,
FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */,
FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */,
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */,
FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */, FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */,
FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */, FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2969,6 +2992,7 @@
FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */,
FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */,
FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */,
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */,
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */,

@ -0,0 +1,103 @@
//
// DrawLog.swift
// PadelClub
//
// Created by razmig on 22/10/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
final class DrawLog: ModelObject, Storable {
static func resourceName() -> String { return "draw-logs" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
static var relationshipNames: [String] = []
var id: String = Store.randomId()
var tournament: String
var drawDate: Date = Date()
var drawSeed: Int
var drawMatchIndex: Int
var drawTeamPosition: TeamPosition
internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int, drawMatchIndex: Int, drawTeamPosition: TeamPosition) {
self.id = id
self.tournament = tournament
self.drawDate = drawDate
self.drawSeed = drawSeed
self.drawMatchIndex = drawMatchIndex
self.drawTeamPosition = drawTeamPosition
}
func tournamentObject() -> Tournament? {
Store.main.findById(self.tournament)
}
func computedBracketPosition() -> Int {
drawMatchIndex * 2 + drawTeamPosition.rawValue
}
func updateTeamBracketPosition(_ team: TeamRegistration) {
guard let match = drawMatch() else { return }
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition)
team.bracketPosition = seedPosition
tournamentObject()?.updateTeamScores(in: seedPosition)
}
func exportedDrawLog() -> String {
[drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ")
}
func localizedDrawSeedLabel() -> String {
return "Tête de série #\(drawSeed + 1)"
}
func localizedDrawLogLabel() -> String {
return [localizedDrawSeedLabel(), positionLabel()].joined(separator: " -> ")
}
func localizedDrawBranch() -> String {
drawTeamPosition.localizedBranchLabel()
}
func drawMatch() -> Match? {
let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex)
return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex })
}
func positionLabel() -> String {
return drawMatch()?.roundAndMatchTitle() ?? ""
}
func roundLabel() -> String {
return drawMatch()?.roundTitle() ?? ""
}
func matchLabel() -> String {
return drawMatch()?.matchTitle() ?? ""
}
var tournamentStore: TournamentStore {
return TournamentStore.instance(tournamentId: self.tournament)
}
override func deleteDependencies() throws {
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _tournament = "tournament"
case _drawDate = "drawDate"
case _drawSeed = "drawSeed"
case _drawMatchIndex = "drawMatchIndex"
case _drawTeamPosition = "drawTeamPosition"
}
func insertOnServer() throws {
self.tournamentStore.drawLogs.writeChangeAndInsertOnServer(instance: self)
}
}

@ -164,22 +164,8 @@ defer {
} }
@discardableResult @discardableResult
func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int { func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int {
let matchIndex = index let matchIndex = index
var teamPosition : TeamPosition {
if let slot {
return slot
} else {
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
return teamPosition
}
}
previousMatch(teamPosition)?.disableMatch() previousMatch(teamPosition)?.disableMatch()
return matchIndex * 2 + teamPosition.rawValue return matchIndex * 2 + teamPosition.rawValue
} }
@ -247,6 +233,7 @@ defer {
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState() currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
} }
func resetScores() { func resetScores() {
@ -548,7 +535,7 @@ defer {
if endDate == nil { if endDate == nil {
endDate = Date() endDate = Date()
} }
teams().forEach({ $0.resetRestingTime() })
winningTeamId = teamScoreWinning.teamRegistration winningTeamId = teamScoreWinning.teamRegistration
losingTeamId = teamScoreWalkout.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
@ -571,6 +558,8 @@ defer {
teamOne?.hasArrived() teamOne?.hasArrived()
teamTwo?.hasArrived() teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
winningTeamId = teamOne?.id winningTeamId = teamOne?.id
losingTeamId = teamTwo?.id losingTeamId = teamTwo?.id
@ -937,6 +926,10 @@ defer {
(teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow
} }
func isValidSpot() -> Bool {
previousMatches().allSatisfy({ $0.isSeeded() == false })
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _round = "round" case _round = "round"

@ -493,8 +493,16 @@ final class MatchScheduler : ModelObject, Storable {
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty {
print("Handling break time conflicts or waiting for free courts") print("Handling break time conflicts or waiting for free courts")
let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }
let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
if let courtsUnavailability, previousEndDate != nil {
previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
if let courtsUnavailability, previousEndDateNoBreak != nil {
previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak }
@ -651,7 +659,12 @@ final class MatchScheduler : ModelObject, Storable {
} }
if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count {
print("All courts in rotation \(rotationIndex) are free") print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)")
}
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability)
return computedStartDateAndCourts.earliestFreeDate
} }
return minimumTargetedEndDate return minimumTargetedEndDate

@ -116,13 +116,39 @@ final class TeamRegistration: ModelObject, Storable {
} }
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding) var teamPosition : TeamPosition {
if let slot {
return slot
} else {
let matchIndex = match.index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
return teamPosition
}
}
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: teamPosition)
tournamentObject()?.resetTeamScores(in: bracketPosition) tournamentObject()?.resetTeamScores(in: bracketPosition)
self.bracketPosition = seedPosition self.bracketPosition = seedPosition
if groupStagePosition != nil && qualified == false { if groupStagePosition != nil && qualified == false {
qualified = true qualified = true
} }
tournamentObject()?.updateTeamScores(in: bracketPosition) if let tournament = tournamentObject() {
if let index = index(in: tournament.selectedSortedTeams()) {
let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition)
do {
try tournamentStore.drawLogs.addOrUpdate(instance: drawLog)
} catch {
Logger.error(error)
}
}
tournament.updateTeamScores(in: bracketPosition)
}
} }
func expectedSummonDate() -> Date? { func expectedSummonDate() -> Date? {
@ -532,8 +558,21 @@ final class TeamRegistration: ModelObject, Storable {
} }
} }
var _cachedRestingTime: (Bool, Date?)?
func restingTime() -> Date? { func restingTime() -> Date? {
matches().sorted(by: \.computedEndDateForSorting).last?.endDate if let _cachedRestingTime { return _cachedRestingTime.1 }
let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate
_cachedRestingTime = (true, restingTime)
return restingTime
}
func resetRestingTime() {
_cachedRestingTime = nil
}
var restingTimeForSorting: Date {
restingTime()!
} }
func teamNameLabel() -> String { func teamNameLabel() -> String {
@ -544,6 +583,17 @@ final class TeamRegistration: ModelObject, Storable {
} }
} }
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition
} else if let bracketPosition {
return true
} else if let drawMatchIndex {
return true
}
return false
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _tournament = "tournament" case _tournament = "tournament"

@ -1,5 +1,5 @@
// //
// Tournament.swift // swift
// PadelClub // PadelClub
// //
// Created by Laurent Morvillier on 02/02/2024. // Created by Laurent Morvillier on 02/02/2024.
@ -335,6 +335,12 @@ final class Tournament : ModelObject, Storable {
override func deleteDependencies() throws { override func deleteDependencies() throws {
let store = self.tournamentStore let store = self.tournamentStore
let drawLogs = self.tournamentStore.drawLogs
for drawLog in drawLogs {
try drawLog.deleteDependencies()
}
store.drawLogs.deleteDependencies(drawLogs)
let teams = self.tournamentStore.teamRegistrations let teams = self.tournamentStore.teamRegistrations
for team in teams { for team in teams {
try team.deleteDependencies() try team.deleteDependencies()
@ -552,7 +558,7 @@ defer {
return endDate != nil return endDate != nil
} }
func state() -> Tournament.State { func state() -> State {
if self.isCanceled == true { if self.isCanceled == true {
return .canceled return .canceled
} }
@ -2257,6 +2263,128 @@ defer {
rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) }
} }
func seedsCount() -> Int {
selectedSortedTeams().count - groupStageSpots()
}
func lastDrawnDate() -> Date? {
drawLogs().last?.drawDate
}
func drawLogs() -> [DrawLog] {
self.tournamentStore.drawLogs.sorted(by: \.drawDate)
}
func seedSpotsLeft() -> Bool {
let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false })
if alreadySeededRounds.isEmpty { return true }
let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() }
return spotsLeft.isEmpty == false
}
func isRoundValidForSeeding(roundIndex: Int) -> Bool {
if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) {
return roundIndex >= lastRoundWithSeeds.index
} else {
return true
}
}
func updateSeedsBracketPosition() async {
await removeAllSeeds()
let drawLogs = drawLogs().reversed()
let seeds = seeds()
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
} catch {
Logger.error(error)
}
}
func removeAllSeeds() async {
unsortedTeams().forEach({ team in
team.bracketPosition = nil
})
let ts = allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
allRounds().forEach({ round in
round.enableRound()
})
}
func addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
func exportedDrawLogs() -> String {
var logs : [String] = ["Journal des tirages\n\n"]
logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n"))
return logs.joined()
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {

@ -26,6 +26,7 @@ class TournamentStore: Store, ObservableObject {
fileprivate(set) var teamScores: StoredCollection<TeamScore> = StoredCollection.placeholder() fileprivate(set) var teamScores: StoredCollection<TeamScore> = StoredCollection.placeholder()
fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder() fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder()
fileprivate(set) var drawLogs: StoredCollection<DrawLog> = StoredCollection.placeholder()
convenience init(tournament: Tournament) { convenience init(tournament: Tournament) {
self.init(identifier: tournament.id, parameter: "tournament") self.init(identifier: tournament.id, parameter: "tournament")
@ -51,6 +52,7 @@ class TournamentStore: Store, ObservableObject {
self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed) self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed) self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed) self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed)
self.drawLogs = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.loadCollectionsFromServerIfNoFile() self.loadCollectionsFromServerIfNoFile()

@ -996,6 +996,15 @@ enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
return shortName return shortName
} }
} }
func localizedBranchLabel() -> String {
switch self {
case .one:
return "Branche du haut"
case .two:
return "Branche du bas"
}
}
} }
enum SetFormat: Int, Hashable, Codable { enum SetFormat: Int, Hashable, Codable {

@ -78,16 +78,8 @@ struct PlayerBlockView: View {
} }
} }
if displayRestingTime, let restingTime = team?.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) { if displayRestingTime, let team {
if restingTime > -300 { TeamRowView.TeamRestingView(team: team)
Text("vient de finir")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("en repos depuis " + value)
.font(.footnote)
.foregroundStyle(.secondary)
}
} }
} }
.bold(hasWon) .bold(hasWon)

@ -15,9 +15,6 @@ struct CourtAvailabilitySettingsView: View {
let event: Event let event: Event
@State private var showingPopover: Bool = false @State private var showingPopover: Bool = false
@State private var courtIndex: Int = 0
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
@State private var editingSlot: DateInterval? @State private var editingSlot: DateInterval?
var courtsUnavailability: [Int: [DateInterval]] { var courtsUnavailability: [Int: [DateInterval]] {
@ -45,10 +42,6 @@ struct CourtAvailabilitySettingsView: View {
} }
Button("éditer") { Button("éditer") {
editingSlot = dateInterval editingSlot = dateInterval
courtIndex = dateInterval.courtIndex
startDate = dateInterval.startDate
endDate = dateInterval.endDate
showingPopover = true
} }
Button("effacer", role: .destructive) { Button("effacer", role: .destructive) {
do { do {
@ -110,8 +103,6 @@ struct CourtAvailabilitySettingsView: View {
Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.")
} actions: { } actions: {
RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
} }
@ -120,8 +111,6 @@ struct CourtAvailabilitySettingsView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
} }
@ -130,6 +119,58 @@ struct CourtAvailabilitySettingsView: View {
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible") .navigationTitle("Créneau indisponible")
.sheet(isPresented: $showingPopover) { .sheet(isPresented: $showingPopover) {
CourtAvailabilityEditorView(event: event)
}
.sheet(item: $editingSlot) { editingSlot in
CourtAvailabilityEditorView(editingSlot: editingSlot, event: event)
}
}
}
struct CourtPicker: View {
@Environment(Tournament.self) var tournament: Tournament
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text(tournament.courtName(atIndex: $0))
}
}
}
}
struct CourtAvailabilityEditorView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
var editingSlot: DateInterval?
let event: Event
@State private var courtIndex: Int
@State private var startDate: Date
@State private var endDate: Date
init(editingSlot: DateInterval, event: Event) {
self.editingSlot = editingSlot
self.event = event
_courtIndex = .init(wrappedValue: editingSlot.courtIndex)
_startDate = .init(wrappedValue: editingSlot.startDate)
_endDate = .init(wrappedValue: editingSlot.endDate)
}
init(event: Event) {
self.event = event
_courtIndex = .init(wrappedValue: 0)
let startDate = event.eventStartDate()
_startDate = .init(wrappedValue: event.eventStartDate())
_endDate = .init(wrappedValue: startDate.addingTimeInterval(5400))
}
var body: some View {
NavigationStack { NavigationStack {
Form { Form {
Section { Section {
@ -153,9 +194,21 @@ struct CourtAvailabilitySettingsView: View {
} footer: { } footer: {
FooterButtonView("jour entier") { FooterButtonView("jour entier") {
startDate = startDate.startOfDay startDate = startDate.startOfDay
endDate = startDate.endOfDay() endDate = startDate.tomorrowAtNine.startOfDay
} }
} }
Section {
DateAdjusterView(date: $startDate)
} header: {
Text("Modifier rapidement l'horaire de début")
}
Section {
DateAdjusterView(date: $endDate)
} header: {
Text("Modifier rapidement l'horaire de fin")
}
} }
.toolbar { .toolbar {
ButtonValidateView { ButtonValidateView {
@ -176,40 +229,49 @@ struct CourtAvailabilitySettingsView: View {
Logger.error(error) Logger.error(error)
} }
} }
showingPopover = false
dismiss()
}
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
} }
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau") .navigationTitle(_navigationTitle())
.tint(.master) .tint(.master)
} }
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1
}
} }
private func _navigationTitle() -> String {
editingSlot == nil ? "Nouveau créneau" : "Édition du créneau"
} }
} }
struct CourtPicker: View { struct DateAdjusterView: View {
@Environment(Tournament.self) var tournament: Tournament @Binding var date: Date
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View { var body: some View {
Picker(title, selection: $selection) { HStack {
ForEach(0..<maxCourt, id: \.self) { _createButton(label: "-1h", timeOffset: -1, component: .hour)
Text(tournament.courtName(atIndex: $0)) _createButton(label: "-30m", timeOffset: -30, component: .minute)
_createButton(label: "+30m", timeOffset: 30, component: .minute)
_createButton(label: "+1h", timeOffset: 1, component: .hour)
} }
.font(.headline)
} }
private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View {
Button(action: {
date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date
}) {
Text(label)
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderedProminent)
.tint(.master)
} }
} }
//#Preview {
// CourtAvailabilitySettingsView(event: Event.mock())
//}

@ -0,0 +1,69 @@
//
// DrawLogsView.swift
// PadelClub
//
// Created by razmig on 22/10/2024.
//
import SwiftUI
import LeStorage
struct DrawLogsView: View {
@Environment(Tournament.self) var tournament
var drawLogs: [DrawLog] {
tournament.drawLogs().reversed()
}
var body: some View {
List {
ForEach(drawLogs) { drawLog in
HStack {
VStack(alignment: .leading) {
Text(drawLog.localizedDrawSeedLabel())
Text(drawLog.drawDate.localizedDate())
.font(.footnote)
.foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text(drawLog.positionLabel()).lineLimit(1).truncationMode(.middle)
Text(drawLog.localizedDrawBranch())
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
}
.overlay(content: {
if drawLogs.isEmpty {
ContentUnavailableView("Aucun tirage", systemImage: "dice", description: Text("Aucun tirage au sort n'a été effectué."))
}
})
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) {
Menu {
ShareLink(item: tournament.exportedDrawLogs()) {
Label("Partager les tirages", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
Divider()
Button("Tout effacer", role: .destructive) {
do {
try tournament.tournamentStore.drawLogs.deleteAll()
} catch {
Logger.error(error)
}
}
} label: {
LabelOptions()
}
}
})
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Journal des tirages")
}
}

@ -0,0 +1,107 @@
//
// PreviewBracketPositionView.swift
// PadelClub
//
// Created by razmig on 23/10/2024.
//
import SwiftUI
struct PreviewBracketPositionView: View {
let seeds: [TeamRegistration]
let drawLogs: [DrawLog]
@State private var filterOption: PreviewBracketPositionFilterOption = .difference
enum PreviewBracketPositionFilterOption: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case all
case difference
case summon
func isDisplayable(_ team: TeamRegistration, drawLog: DrawLog?) -> Bool {
switch self {
case .all:
true
case .difference:
team.isDifferentPosition(drawLog?.computedBracketPosition()) == true
case .summon:
team.callDate != drawLog?.drawMatch()?.startDate
}
}
}
var body: some View {
List {
Section {
ForEach(seeds.indices, id: \.self) { seedIndex in
let seed = seeds[seedIndex]
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
if filterOption.isDisplayable(seed, drawLog: drawLog) {
HStack {
VStack(alignment: .leading) {
Text("Tête de série #\(seedIndex + 1)").font(.caption)
TeamRowView.TeamView(team: seed)
TeamRowView.TeamCallDateView(team: seed)
}
Spacer()
if let drawLog {
VStack(alignment: .trailing) {
Text(drawLog.roundLabel()).lineLimit(1).truncationMode(.middle).font(.caption)
Text(drawLog.matchLabel()).lineLimit(1).truncationMode(.middle)
Text(drawLog.localizedDrawBranch())
if let expectedDate = drawLog.drawMatch()?.startDate {
Text(expectedDate.localizedDate())
.font(.caption)
} else {
Text("Aucun horaire")
.font(.caption)
}
}
}
}
.listRowView(isActive: true, color: seed.isDifferentPosition(drawLog?.computedBracketPosition()) ? .logoRed : .green, hideColorVariation: true)
}
}
} header: {
Picker(selection: $filterOption) {
Text("Tous").tag(PreviewBracketPositionFilterOption.all)
Text("Changements").tag(PreviewBracketPositionFilterOption.difference)
Text("Convoc.").tag(PreviewBracketPositionFilterOption.summon)
} label: {
Text("Filter")
}
.labelsHidden()
.pickerStyle(.segmented)
.textCase(nil)
}
}
.overlay(content: {
if seeds.isEmpty {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Aucun tête de série dans le tournoi."))
} else if filterOption == .difference, noSeedHasDifferentPlace() {
ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau."))
} else if filterOption == .summon, noSeedHasDifferentSummon() {
ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau."))
}
})
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Aperçu du tableau tiré")
}
func noSeedHasDifferentPlace() -> Bool {
seeds.enumerated().allSatisfy({ seedIndex, seed in
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
return seed.isDifferentPosition(drawLog?.computedBracketPosition()) == false
})
}
func noSeedHasDifferentSummon() -> Bool {
seeds.enumerated().allSatisfy({ seedIndex, seed in
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
return seed.callDate == drawLog?.drawMatch()?.startDate
})
}
}

@ -74,6 +74,32 @@ struct RoundSettingsView: View {
} }
} }
let previewAvailable = tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount() && tournament.lastDrawnDate() != nil && tournament.seedSpotsLeft()
Section {
NavigationLink {
DrawLogsView()
.environment(tournament)
} label: {
Text("Gestionnaire des tirages au sort")
}
if previewAvailable {
NavigationLink {
PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs())
} label: {
Text("Aperçu du repositionnement")
}
RowButtonView("Replacer toutes les têtes de série", role: .destructive) {
await tournament.updateSeedsBracketPosition()
}
}
} footer: {
if previewAvailable {
Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.")
}
}
// Section { // Section {
// RowButtonView("Enabled", role: .destructive) { // RowButtonView("Enabled", role: .destructive) {
// let allMatches = tournament._allMatchesIncludingDisabled() // let allMatches = tournament._allMatchesIncludingDisabled()
@ -127,72 +153,12 @@ struct RoundSettingsView: View {
} }
private func _removeAllSeeds() async { private func _removeAllSeeds() async {
tournament.unsortedTeams().forEach({ team in await tournament.removeAllSeeds()
team.bracketPosition = nil
})
let ts = tournament.allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch {
Logger.error(error)
}
tournament.allRounds().forEach({ round in
round.enableRound()
})
self.isEditingTournamentSeed.wrappedValue = true self.isEditingTournamentSeed.wrappedValue = true
} }
private func _addNewRound(_ roundIndex: Int) async { private func _addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) await tournament.addNewRound(roundIndex)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournament.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
} }
private func _removeRound(_ lastRound: Round) async { private func _removeRound(_ lastRound: Round) async {

@ -37,14 +37,14 @@ struct RoundView: View {
let displayableMatches: [Match] = self.upperRound.round.playedMatches() let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in return displayableMatches.filter { match in
match.teamScores.count == 1 match.teamScores.count == 1
} }.filter({ $0.isValidSpot() })
} }
private var seedSpaceLeft: [Match] { private var seedSpaceLeft: [Match] {
let displayableMatches: [Match] = self.upperRound.round.playedMatches() let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in return displayableMatches.filter { match in
match.teamScores.count == 0 match.teamScores.count == 0
} }.filter({ $0.isValidSpot() })
} }
private var availableSeedGroup: SeedInterval? { private var availableSeedGroup: SeedInterval? {
@ -121,6 +121,7 @@ struct RoundView: View {
} }
} }
} else { } else {
let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index)
let availableSeeds = tournament.availableSeeds() let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableQualifiedTeams = tournament.availableQualifiedTeams()
@ -152,7 +153,6 @@ struct RoundView: View {
if availableQualifiedTeams.isEmpty == false { if availableQualifiedTeams.isEmpty == false {
let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : 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 let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft
if availableSeedSpot.isEmpty == false {
Section { Section {
DisclosureGroup { DisclosureGroup {
ForEach(availableQualifiedTeams) { team in ForEach(availableQualifiedTeams) { team in
@ -174,65 +174,25 @@ struct RoundView: View {
} label: { } label: {
TeamRowView(team: team, displayCallDate: false) TeamRowView(team: team, displayCallDate: false)
} }
.disabled(availableSeedSpot.isEmpty || isRoundValidForSeeding == false)
} }
} label: { } label: {
Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count)
} }
} header: { } header: {
Text("Tirage au sort visuel d'un qualifié").font(.subheadline) Text("Tirage au sort visuel d'un qualifié").font(.subheadline)
} footer: {
if availableSeedSpot.isEmpty || isRoundValidForSeeding == false {
Text("Aucune place disponible !")
.foregroundStyle(.red)
} }
} }
} }
if availableSeeds.isEmpty == false { if availableSeeds.isEmpty == false {
if seedSpaceLeft.isEmpty == false { let spots = (seedSpaceLeft.isEmpty == false) ? seedSpaceLeft : spaceLeft
Section { let opposingSeeding = (seedSpaceLeft.isEmpty == false) ? false : true
DisclosureGroup { _drawSection(availableSeeds: availableSeeds, spots: spots, opposingSeeding: opposingSeeding, isRoundValidForSeeding: isRoundValidForSeeding)
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(seeds: [team])
}
}
} 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(seeds: [team])
}
}
} 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)
}
}
} }
} }
@ -259,11 +219,11 @@ struct RoundView: View {
} }
} }
#if DEBUG #if DEBUG
Spacer() Spacer()
Text(match.index.formatted() + " " + match.teamScores.count.formatted()) Text(match.index.formatted() + " " + match.teamScores.count.formatted())
#endif #endif
} }
} footer: { } footer: {
if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true { if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true {
@ -399,12 +359,39 @@ struct RoundView: View {
} }
} }
} }
}
//#Preview { private func _drawSection(availableSeeds: [TeamRegistration], spots: [Match], opposingSeeding: Bool, isRoundValidForSeeding: Bool) -> some View {
// RoundView(round: Round.mock()) Section {
// .environment(Tournament.mock()) DisclosureGroup {
//} ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: spots) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: spots[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding)
}
_save(seeds: [team])
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
.disabled(spots.isEmpty || isRoundValidForSeeding == 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)
} footer: {
if spots.isEmpty || isRoundValidForSeeding == false {
Text("Aucune place disponible ! Ajouter une manche via les réglages du tableau.")
.foregroundStyle(.red)
}
}
}
}
struct MatchSpot: SpinDrawable { struct MatchSpot: SpinDrawable {
let match: Match let match: Match

@ -9,87 +9,57 @@ import SwiftUI
struct TeamRestingView: View { struct TeamRestingView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var sortingMode: SortingMode = .restingTime @State private var displayMode: DisplayMode = .teams
@State private var selectedCourt: Int? @State private var selectedCourt: Int?
@State private var readyMatches: [Match] = [] @State private var readyMatches: [Match] = []
@State private var matchesLeft: [Match] = [] @State private var matchesLeft: [Match] = []
@State private var teams: [TeamRegistration] = []
enum SortingMode: Int, Identifiable, CaseIterable { enum DisplayMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
case index case teams
case restingTime case restingTime
case court
func localizedSortingModeLabel() -> String { func localizedSortingModeLabel() -> String {
switch self { switch self {
case .index: case .teams:
return "Ordre" return "Équipes"
case .court:
return "Terrain"
case .restingTime: case .restingTime:
return "Repos" return "Matchs"
} }
} }
} }
var sortingModeCases: [SortingMode] {
var sortingModes = [SortingMode]()
sortingModes.append(.index)
sortingModes.append(.restingTime)
sortingModes.append(.court)
return sortingModes
}
func contentUnavailableDescriptionLabel() -> String { func contentUnavailableDescriptionLabel() -> String {
switch sortingMode { switch displayMode {
case .index:
return "Ce tournoi n'a aucun match prêt à démarrer"
case .restingTime: case .restingTime:
return "Ce tournoi n'a aucun match prêt à démarrer" return "Ce tournoi n'a aucun match prêt à démarrer"
case .court: case .teams:
return "Ce tournoi n'a aucun match prêt à démarrer" return "Ce tournoi n'a aucune équipe ayant déjà terminé un match."
} }
} }
var sortedMatches: [Match] { var sortedMatches: [Match] {
switch sortingMode {
case .index:
return readyMatches
case .restingTime:
return readyMatches.sorted(by: \.restingTimeForSorting) return readyMatches.sorted(by: \.restingTimeForSorting)
case .court:
return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending)
} }
var sortedTeams: [TeamRegistration] {
return teams
} }
var body: some View { var body: some View {
List { List {
Section { Section {
Picker(selection: $selectedCourt) { switch displayMode {
Text("Aucun").tag(nil as Int?) case .teams:
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in if sortedTeams.isEmpty == false {
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex as Int?) ForEach(sortedTeams) { team in
} TeamRowView(team: team, displayRestingTime: true)
} label: {
Text("Sur le terrain")
} }
// } else {
// Toggle(isOn: $checkCanPlay) { ContentUnavailableView("Aucune équipe en repos", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
// if isFree {
// Text("Vérifier le paiement ou la présence")
// } else {
// Text("Vérifier la présence")
// }
// }
// } footer: {
// if isFree {
// Text("Masque les matchs où un ou plusieurs joueurs qui ne sont pas encore arrivé")
// } else {
// Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé")
// }
} }
case .restingTime:
Section {
if sortedMatches.isEmpty == false { if sortedMatches.isEmpty == false {
ForEach(sortedMatches) { match in ForEach(sortedMatches) { match in
MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt)
@ -97,23 +67,23 @@ struct TeamRestingView: View {
} else { } else {
ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
} }
}
} header: { } header: {
Picker(selection: $sortingMode) { Picker(selection: $displayMode) {
ForEach(sortingModeCases) { sortingMode in ForEach(DisplayMode.allCases) { sortingMode in
Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode) Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode)
} }
} label: { } label: {
Text("Méthode de tri") Text("Affichage")
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.segmented) .pickerStyle(.segmented)
} }
.headerProminence(.increased)
.textCase(nil) .textCase(nil)
} }
.toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Temps de repos")
.navigationTitle("Match à suivre")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.onAppear { .onAppear {
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
let matchesLeft = tournament.matchesLeft(allMatches) let matchesLeft = tournament.matchesLeft(allMatches)
@ -121,6 +91,7 @@ struct TeamRestingView: View {
let readyMatches = tournament.readyMatches(allMatches) let readyMatches = tournament.readyMatches(allMatches)
self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.matchesLeft = matchesLeft self.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)
} }
} }
} }

@ -12,50 +12,93 @@ struct TeamRowView: View {
var team: TeamRegistration var team: TeamRegistration
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false var displayCallDate: Bool = false
var displayRestingTime: Bool = false
var body: some View { var body: some View {
LabeledContent { LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { TeamHeadlineView(team: team)
if let groupStage = team.groupStageObject() { TeamView(team: team)
HStack { }
Text(groupStage.groupStageTitle(.title)) if displayCallDate {
if let finalPosition = groupStage.finalPosition(ofTeam: team) { TeamCallDateView(team: team)
Text((finalPosition + 1).ordinalFormatted()) }
if displayRestingTime {
TeamRestingView(team: team)
} }
} }
} else if let round = team.initialRound() {
Text(round.roundTitle(.wide))
} }
if let wildcardLabel = team.wildcardLabel() { struct TeamRestingView: View {
Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption) let team: TeamRegistration
@ViewBuilder
var body: some View {
if let restingTime = team.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) {
if restingTime > -300 {
Text("vient de finir")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("en repos depuis " + value)
.font(.footnote)
.foregroundStyle(.secondary)
}
} }
} }
}
struct TeamView: View {
let team: TeamRegistration
var body: some View {
if let name = team.name, name.isEmpty == false { if let name = team.name, name.isEmpty == false {
Text(name).foregroundStyle(.secondary).font(.footnote) Text(name).foregroundStyle(.secondary).font(.footnote)
if team.players().isEmpty { if team.players().isEmpty {
Text("Aucun joueur") Text("Aucun joueur")
} else { } else {
ForEach(team.players()) { player in CompactTeamView(team: team)
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
} }
} else { } else {
if team.players().isEmpty == false { if team.players().isEmpty == false {
ForEach(team.players()) { player in CompactTeamView(team: team)
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
} else { } else {
Text("Place réservée") Text("Place réservée")
Text("Place réservée") Text("Place réservée")
} }
} }
} }
if displayCallDate { }
struct TeamHeadlineView: View {
let team: TeamRegistration
var body: some View {
HStack {
if let groupStage = team.groupStageObject() {
HStack {
Text(groupStage.groupStageTitle(.title))
if let finalPosition = groupStage.finalPosition(ofTeam: team) {
Text((finalPosition + 1).ordinalFormatted())
}
}
} else if let round = team.initialRound() {
Text(round.roundTitle(.wide))
}
if let wildcardLabel = team.wildcardLabel() {
Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption)
}
}
}
}
struct TeamCallDateView: View {
let team: TeamRegistration
var body: some View {
if let callDate = team.callDate { if let callDate = team.callDate {
Text("Déjà convoquée \(callDate.localizedDate())") Text("Déjà convoquée \(callDate.localizedDate())")
.foregroundStyle(.logoRed) .foregroundStyle(.logoRed)
@ -69,9 +112,14 @@ struct TeamRowView: View {
} }
} }
} }
struct CompactTeamView: View {
let team: TeamRegistration
var body: some View {
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
}
} }
} }
//#Preview {
// TeamRowView(team: TeamRegistration.mock())
//}

@ -369,4 +369,22 @@ final class ServerDataTests: XCTestCase {
} }
func testDrawLog() async throws {
let tournament: [Tournament] = try await StoreCenter.main.service().get()
guard let tournamentId = tournament.first?.id else {
assertionFailure("missing tournament in database")
return
}
let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two)
let d: DrawLog = try await StoreCenter.main.service().post(drawLog)
assert(d.tournament == drawLog.tournament)
assert(d.drawDate.formatted() == drawLog.drawDate.formatted())
assert(d.drawSeed == drawLog.drawSeed)
assert(d.drawTeamPosition == drawLog.drawTeamPosition)
assert(d.drawMatchIndex == drawLog.drawMatchIndex)
}
} }

Loading…
Cancel
Save