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.
2821 lines
115 KiB
2821 lines
115 KiB
//
|
|
// swift
|
|
// PadelClub
|
|
//
|
|
// Created by Laurent Morvillier on 02/02/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
import SwiftUI
|
|
|
|
@Observable
|
|
final class Tournament : ModelObject, Storable {
|
|
static func resourceName() -> String { "tournaments" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
static func filterByStoreIdentifier() -> Bool { return false }
|
|
static var relationshipNames: [String] = []
|
|
|
|
var id: String = Store.randomId()
|
|
var event: String?
|
|
var name: String?
|
|
var startDate: Date
|
|
var endDate: Date?
|
|
private(set) var creationDate: Date
|
|
var isPrivate: Bool
|
|
private(set) var groupStageFormat: MatchFormat?
|
|
private(set) var roundFormat: MatchFormat?
|
|
private(set) var loserRoundFormat: MatchFormat?
|
|
var groupStageSortMode: GroupStageOrderingMode
|
|
var groupStageCount: Int
|
|
var rankSourceDate: Date?
|
|
var dayDuration: Int
|
|
var teamCount: Int
|
|
var teamSorting: TeamSortingType
|
|
var federalCategory: TournamentCategory
|
|
var federalLevelCategory: TournamentLevel
|
|
var federalAgeCategory: FederalTournamentAge
|
|
var closedRegistrationDate: Date?
|
|
var groupStageAdditionalQualified: Int
|
|
var courtCount: Int = 2
|
|
var prioritizeClubMembers: Bool
|
|
var qualifiedPerGroupStage: Int
|
|
var teamsPerGroupStage: Int
|
|
var entryFee: Double?
|
|
var payment: TournamentPayment? = nil
|
|
var additionalEstimationDuration: Int = 0
|
|
var isDeleted: Bool = false
|
|
var isCanceled: Bool = false
|
|
var publishTeams: Bool = false
|
|
//var publishWaitingList: Bool = false
|
|
var publishSummons: Bool = false
|
|
var publishGroupStages: Bool = false
|
|
var publishBrackets: Bool = false
|
|
var shouldVerifyGroupStage: Bool = false
|
|
var shouldVerifyBracket: Bool = false
|
|
var hideTeamsWeight: Bool = false
|
|
var publishTournament: Bool = false
|
|
var hidePointsEarned: Bool = false
|
|
var publishRankings: Bool = false
|
|
var loserBracketMode: LoserBracketMode = .automatic
|
|
var initialSeedRound: Int = 0
|
|
var initialSeedCount: Int = 0
|
|
var enableOnlineRegistration: Bool = false
|
|
var registrationDateLimit: Date? = nil
|
|
var openingRegistrationDate: Date? = nil
|
|
var targetTeamCount: Int? = nil
|
|
var waitingListLimit: Int? = nil
|
|
var accountIsRequired: Bool = true
|
|
var licenseIsRequired: Bool = true
|
|
var minimumPlayerPerTeam: Int = 2
|
|
var maximumPlayerPerTeam: Int = 2
|
|
var information: String? = nil
|
|
|
|
@ObservationIgnored
|
|
var navigationPath: [Screen] = []
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case _id = "id"
|
|
case _event = "event"
|
|
case _creator = "creator"
|
|
case _name = "name"
|
|
case _startDate = "startDate"
|
|
case _endDate = "endDate"
|
|
case _creationDate = "creationDate"
|
|
case _isPrivate = "isPrivate"
|
|
case _groupStageFormat = "groupStageFormat"
|
|
case _roundFormat = "roundFormat"
|
|
case _loserRoundFormat = "loserRoundFormat"
|
|
case _groupStageSortMode = "groupStageSortMode"
|
|
case _groupStageCount = "groupStageCount"
|
|
case _rankSourceDate = "rankSourceDate"
|
|
case _dayDuration = "dayDuration"
|
|
case _teamCount = "teamCount"
|
|
case _teamSorting = "teamSorting"
|
|
case _federalCategory = "federalCategory"
|
|
case _federalLevelCategory = "federalLevelCategory"
|
|
case _federalAgeCategory = "federalAgeCategory"
|
|
case _seedCount = "seedCount"
|
|
case _closedRegistrationDate = "closedRegistrationDate"
|
|
case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
|
|
case _courtCount = "courtCount"
|
|
case _prioritizeClubMembers = "prioritizeClubMembers"
|
|
case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
|
|
case _teamsPerGroupStage = "teamsPerGroupStage"
|
|
case _entryFee = "entryFee"
|
|
case _additionalEstimationDuration = "additionalEstimationDuration"
|
|
case _isDeleted = "isDeleted"
|
|
case _isCanceled = "localId"
|
|
case _payment = "globalId"
|
|
case _publishTeams = "publishTeams"
|
|
//case _publishWaitingList = "publishWaitingList"
|
|
case _publishSummons = "publishSummons"
|
|
case _publishGroupStages = "publishGroupStages"
|
|
case _publishBrackets = "publishBrackets"
|
|
case _shouldVerifyGroupStage = "shouldVerifyGroupStage"
|
|
case _shouldVerifyBracket = "shouldVerifyBracket"
|
|
case _hideTeamsWeight = "hideTeamsWeight"
|
|
case _publishTournament = "publishTournament"
|
|
case _hidePointsEarned = "hidePointsEarned"
|
|
case _publishRankings = "publishRankings"
|
|
case _loserBracketMode = "loserBracketMode"
|
|
case _initialSeedRound = "initialSeedRound"
|
|
case _initialSeedCount = "initialSeedCount"
|
|
case _enableOnlineRegistration = "enableOnlineRegistration"
|
|
case _registrationDateLimit = "registrationDateLimit"
|
|
case _openingRegistrationDate = "openingRegistrationDate"
|
|
case _targetTeamCount = "targetTeamCount"
|
|
case _waitingListLimit = "waitingListLimit"
|
|
case _accountIsRequired = "accountIsRequired"
|
|
case _licenseIsRequired = "licenseIsRequired"
|
|
case _minimumPlayerPerTeam = "minimumPlayerPerTeam"
|
|
case _maximumPlayerPerTeam = "maximumPlayerPerTeam"
|
|
case _information = "information"
|
|
|
|
}
|
|
|
|
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, targetTeamCount: Int? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) {
|
|
self.event = event
|
|
self.name = name
|
|
self.startDate = startDate
|
|
self.endDate = endDate
|
|
self.creationDate = creationDate
|
|
#if DEBUG
|
|
self.isPrivate = false
|
|
#else
|
|
self.isPrivate = Guard.main.purchasedTransactions.isEmpty
|
|
#endif
|
|
self.groupStageFormat = groupStageFormat
|
|
self.roundFormat = roundFormat
|
|
self.loserRoundFormat = loserRoundFormat
|
|
self.groupStageSortMode = groupStageSortMode
|
|
self.groupStageCount = groupStageCount
|
|
self.rankSourceDate = rankSourceDate
|
|
self.dayDuration = dayDuration
|
|
self.teamCount = teamCount
|
|
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
|
|
self.federalCategory = federalCategory
|
|
self.federalLevelCategory = federalLevelCategory
|
|
self.federalAgeCategory = federalAgeCategory
|
|
self.closedRegistrationDate = closedRegistrationDate
|
|
self.groupStageAdditionalQualified = groupStageAdditionalQualified
|
|
self.courtCount = courtCount
|
|
self.prioritizeClubMembers = prioritizeClubMembers
|
|
self.qualifiedPerGroupStage = qualifiedPerGroupStage
|
|
self.teamsPerGroupStage = teamsPerGroupStage
|
|
self.entryFee = entryFee
|
|
self.additionalEstimationDuration = additionalEstimationDuration
|
|
self.isDeleted = isDeleted
|
|
#if DEBUG
|
|
self.publishTeams = true
|
|
self.publishSummons = true
|
|
self.publishBrackets = true
|
|
self.publishGroupStages = true
|
|
self.publishRankings = true
|
|
self.publishTournament = true
|
|
#else
|
|
self.publishTeams = publishTeams
|
|
self.publishSummons = publishSummons
|
|
self.publishBrackets = publishBrackets
|
|
self.publishGroupStages = publishGroupStages
|
|
self.publishRankings = publishRankings
|
|
self.publishTournament = publishTournament
|
|
#endif
|
|
self.shouldVerifyBracket = shouldVerifyBracket
|
|
self.shouldVerifyGroupStage = shouldVerifyGroupStage
|
|
self.hideTeamsWeight = hideTeamsWeight
|
|
self.hidePointsEarned = hidePointsEarned
|
|
self.loserBracketMode = loserBracketMode
|
|
self.initialSeedRound = initialSeedRound
|
|
self.initialSeedCount = initialSeedCount
|
|
self.enableOnlineRegistration = enableOnlineRegistration
|
|
self.registrationDateLimit = registrationDateLimit
|
|
self.openingRegistrationDate = openingRegistrationDate
|
|
self.targetTeamCount = targetTeamCount
|
|
self.waitingListLimit = waitingListLimit
|
|
|
|
self.accountIsRequired = accountIsRequired
|
|
self.licenseIsRequired = licenseIsRequired
|
|
self.minimumPlayerPerTeam = minimumPlayerPerTeam
|
|
self.maximumPlayerPerTeam = maximumPlayerPerTeam
|
|
self.information = information
|
|
}
|
|
|
|
required init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try container.decode(String.self, forKey: ._id)
|
|
event = try container.decodeIfPresent(String.self, forKey: ._event)
|
|
name = try container.decodeIfPresent(String.self, forKey: ._name)
|
|
startDate = try container.decode(Date.self, forKey: ._startDate)
|
|
endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate)
|
|
creationDate = try container.decode(Date.self, forKey: ._creationDate)
|
|
isPrivate = try container.decode(Bool.self, forKey: ._isPrivate)
|
|
groupStageFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageFormat)
|
|
roundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._roundFormat)
|
|
loserRoundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserRoundFormat)
|
|
groupStageSortMode = try container.decode(GroupStageOrderingMode.self, forKey: ._groupStageSortMode)
|
|
groupStageCount = try container.decode(Int.self, forKey: ._groupStageCount)
|
|
rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate)
|
|
dayDuration = try container.decode(Int.self, forKey: ._dayDuration)
|
|
teamCount = try container.decode(Int.self, forKey: ._teamCount)
|
|
teamSorting = try container.decode(TeamSortingType.self, forKey: ._teamSorting)
|
|
federalCategory = try container.decode(TournamentCategory.self, forKey: ._federalCategory)
|
|
federalLevelCategory = try container.decode(TournamentLevel.self, forKey: ._federalLevelCategory)
|
|
federalAgeCategory = try container.decode(FederalTournamentAge.self, forKey: ._federalAgeCategory)
|
|
closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate)
|
|
groupStageAdditionalQualified = try container.decode(Int.self, forKey: ._groupStageAdditionalQualified)
|
|
courtCount = try container.decode(Int.self, forKey: ._courtCount)
|
|
prioritizeClubMembers = try container.decode(Bool.self, forKey: ._prioritizeClubMembers)
|
|
qualifiedPerGroupStage = try container.decode(Int.self, forKey: ._qualifiedPerGroupStage)
|
|
teamsPerGroupStage = try container.decode(Int.self, forKey: ._teamsPerGroupStage)
|
|
entryFee = try container.decodeIfPresent(Double.self, forKey: ._entryFee)
|
|
payment = try Tournament._decodePayment(container: container)
|
|
additionalEstimationDuration = try container.decode(Int.self, forKey: ._additionalEstimationDuration)
|
|
isDeleted = try container.decode(Bool.self, forKey: ._isDeleted)
|
|
isCanceled = try Tournament._decodeCanceled(container: container)
|
|
publishTeams = try container.decodeIfPresent(Bool.self, forKey: ._publishTeams) ?? false
|
|
publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false
|
|
publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false
|
|
publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false
|
|
shouldVerifyBracket = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyBracket) ?? false
|
|
shouldVerifyGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyGroupStage) ?? false
|
|
hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false
|
|
publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
|
|
hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false
|
|
publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false
|
|
loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
|
|
initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0
|
|
initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0
|
|
enableOnlineRegistration = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlineRegistration) ?? false
|
|
registrationDateLimit = try container.decodeIfPresent(Date.self, forKey: ._registrationDateLimit)
|
|
openingRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._openingRegistrationDate)
|
|
targetTeamCount = try container.decodeIfPresent(Int.self, forKey: ._targetTeamCount)
|
|
waitingListLimit = try container.decodeIfPresent(Int.self, forKey: ._waitingListLimit)
|
|
accountIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._accountIsRequired) ?? true
|
|
licenseIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._licenseIsRequired) ?? true
|
|
minimumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._minimumPlayerPerTeam) ?? 2
|
|
maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2
|
|
|
|
information = try container.decodeIfPresent(String.self, forKey: ._information)
|
|
}
|
|
|
|
fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
|
|
|
|
fileprivate static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
|
|
let data = try container.decodeIfPresent(Data.self, forKey: ._payment)
|
|
|
|
if let data {
|
|
do {
|
|
let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)
|
|
let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
|
|
return TournamentPayment(rawValue: sequence[18])
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
fileprivate static func _decodeCanceled(container: KeyedDecodingContainer<CodingKeys>) throws -> Bool {
|
|
let data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled)
|
|
if let data {
|
|
do {
|
|
let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)
|
|
let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
|
|
return Bool.decodeInt(sequence[18])
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(id, forKey: ._id)
|
|
try container.encode(event, forKey: ._event)
|
|
try container.encode(name, forKey: ._name)
|
|
|
|
try container.encode(startDate, forKey: ._startDate)
|
|
try container.encode(endDate, forKey: ._endDate)
|
|
|
|
try container.encode(creationDate, forKey: ._creationDate)
|
|
try container.encode(isPrivate, forKey: ._isPrivate)
|
|
|
|
try container.encode(groupStageFormat, forKey: ._groupStageFormat)
|
|
try container.encode(roundFormat, forKey: ._roundFormat)
|
|
try container.encode(loserRoundFormat, forKey: ._loserRoundFormat)
|
|
|
|
try container.encode(groupStageSortMode, forKey: ._groupStageSortMode)
|
|
try container.encode(groupStageCount, forKey: ._groupStageCount)
|
|
|
|
try container.encode(rankSourceDate, forKey: ._rankSourceDate)
|
|
|
|
try container.encode(dayDuration, forKey: ._dayDuration)
|
|
try container.encode(teamCount, forKey: ._teamCount)
|
|
try container.encode(teamSorting, forKey: ._teamSorting)
|
|
try container.encode(federalCategory, forKey: ._federalCategory)
|
|
try container.encode(federalLevelCategory, forKey: ._federalLevelCategory)
|
|
try container.encode(federalAgeCategory, forKey: ._federalAgeCategory)
|
|
|
|
try container.encode(closedRegistrationDate, forKey: ._closedRegistrationDate)
|
|
|
|
try container.encode(groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified)
|
|
try container.encode(courtCount, forKey: ._courtCount)
|
|
try container.encode(prioritizeClubMembers, forKey: ._prioritizeClubMembers)
|
|
try container.encode(qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage)
|
|
try container.encode(teamsPerGroupStage, forKey: ._teamsPerGroupStage)
|
|
try container.encode(entryFee, forKey: ._entryFee)
|
|
|
|
try self._encodePayment(container: &container)
|
|
try container.encode(additionalEstimationDuration, forKey: ._additionalEstimationDuration)
|
|
try container.encode(isDeleted, forKey: ._isDeleted)
|
|
try self._encodeIsCanceled(container: &container)
|
|
try container.encode(publishTeams, forKey: ._publishTeams)
|
|
try container.encode(publishSummons, forKey: ._publishSummons)
|
|
try container.encode(publishBrackets, forKey: ._publishBrackets)
|
|
try container.encode(publishGroupStages, forKey: ._publishGroupStages)
|
|
try container.encode(shouldVerifyBracket, forKey: ._shouldVerifyBracket)
|
|
try container.encode(shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage)
|
|
try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight)
|
|
try container.encode(publishTournament, forKey: ._publishTournament)
|
|
try container.encode(hidePointsEarned, forKey: ._hidePointsEarned)
|
|
try container.encode(publishRankings, forKey: ._publishRankings)
|
|
try container.encode(loserBracketMode, forKey: ._loserBracketMode)
|
|
try container.encode(initialSeedRound, forKey: ._initialSeedRound)
|
|
try container.encode(initialSeedCount, forKey: ._initialSeedCount)
|
|
try container.encode(enableOnlineRegistration, forKey: ._enableOnlineRegistration)
|
|
try container.encode(registrationDateLimit, forKey: ._registrationDateLimit)
|
|
try container.encode(openingRegistrationDate, forKey: ._openingRegistrationDate)
|
|
try container.encode(targetTeamCount, forKey: ._targetTeamCount)
|
|
try container.encode(waitingListLimit, forKey: ._waitingListLimit)
|
|
|
|
try container.encode(accountIsRequired, forKey: ._accountIsRequired)
|
|
try container.encode(licenseIsRequired, forKey: ._licenseIsRequired)
|
|
try container.encode(minimumPlayerPerTeam, forKey: ._minimumPlayerPerTeam)
|
|
try container.encode(maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam)
|
|
try container.encode(information, forKey: ._information)
|
|
}
|
|
|
|
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
|
|
|
|
guard let payment else {
|
|
try container.encodeNil(forKey: ._payment)
|
|
return
|
|
}
|
|
|
|
let max: Int = TournamentPayment.allCases.count
|
|
var sequence = (1...18).map { _ in Int.random(in: (0..<max)) }
|
|
sequence.append(payment.rawValue)
|
|
sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0..<max ))} )
|
|
|
|
let stringCombo: [String] = sequence.map { $0.formatted() }
|
|
let joined: String = stringCombo.joined(separator: "")
|
|
if let data = joined.data(using: .utf8) {
|
|
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
|
|
try container.encodeIfPresent(encryped, forKey: ._payment)
|
|
}
|
|
|
|
}
|
|
|
|
func _encodeIsCanceled(container: inout KeyedEncodingContainer<CodingKeys>) throws {
|
|
|
|
let max: Int = 9
|
|
var sequence = (1...18).map { _ in Int.random(in: (0...max)) }
|
|
sequence.append(self.isCanceled.encodedValue)
|
|
sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0...max ))} )
|
|
|
|
let stringCombo: [String] = sequence.map { $0.formatted() }
|
|
let joined: String = stringCombo.joined(separator: "")
|
|
if let data = joined.data(using: .utf8) {
|
|
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
|
|
try container.encode(encryped, forKey: ._isCanceled)
|
|
}
|
|
}
|
|
|
|
var tournamentStore: TournamentStore {
|
|
return TournamentStore.instance(tournamentId: self.id)
|
|
}
|
|
|
|
override func deleteDependencies() throws {
|
|
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
|
|
for team in teams {
|
|
try team.deleteDependencies()
|
|
}
|
|
store.teamRegistrations.deleteDependencies(teams)
|
|
|
|
let groups = self.tournamentStore.groupStages
|
|
for group in groups {
|
|
try group.deleteDependencies()
|
|
}
|
|
store.groupStages.deleteDependencies(groups)
|
|
|
|
let rounds = self.tournamentStore.rounds
|
|
for round in rounds {
|
|
try round.deleteDependencies()
|
|
}
|
|
store.rounds.deleteDependencies(rounds)
|
|
|
|
store.matchSchedulers.deleteDependencies(self._matchSchedulers())
|
|
}
|
|
|
|
// MARK: - Computed Dependencies
|
|
|
|
func unsortedTeams() -> [TeamRegistration] {
|
|
return Array(self.tournamentStore.teamRegistrations)
|
|
}
|
|
|
|
func groupStages(atStep step: Int = 0) -> [GroupStage] {
|
|
let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
|
|
return groupStages.sorted(by: \.index)
|
|
}
|
|
|
|
func allGroupStages() -> [GroupStage] {
|
|
return self.tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder)
|
|
}
|
|
|
|
func allRounds() -> [Round] {
|
|
return Array(self.tournamentStore.rounds)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
/// Warning: if the enum has more than 10 cases, the payment algo is broken
|
|
enum TournamentPayment: Int, CaseIterable {
|
|
case free, unit, subscriptionUnit, unlimited
|
|
|
|
var isSubscription: Bool {
|
|
switch self {
|
|
case .subscriptionUnit, .unlimited:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
enum State {
|
|
case initial
|
|
case build
|
|
case running
|
|
case canceled
|
|
case finished
|
|
}
|
|
|
|
func eventLabel() -> String {
|
|
if let event = eventObject(), let name = event.name {
|
|
return name
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func publishedTournamentDate() -> Date {
|
|
return min(creationDate.tomorrowAtNine, startDate)
|
|
}
|
|
|
|
func publishedTeamsDate() -> Date {
|
|
return self.startDate
|
|
}
|
|
|
|
func canBePublished() -> Bool {
|
|
switch state() {
|
|
case .build, .finished, .running:
|
|
return unsortedTeams().count > 3
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isTournamentPublished() -> Bool {
|
|
return (Date() >= publishedTournamentDate()) || publishTournament
|
|
}
|
|
|
|
func areTeamsPublished() -> Bool {
|
|
return Date() >= startDate || publishTeams
|
|
}
|
|
|
|
func areSummonsPublished() -> Bool {
|
|
return Date() >= startDate || publishSummons
|
|
}
|
|
|
|
fileprivate func _publishedDateFromMatches(_ matches: [Match]) -> Date? {
|
|
let startDates: [Date] = matches.compactMap { $0.startDate }
|
|
let sortedDates: [Date] = startDates.sorted()
|
|
|
|
if let first: Date = sortedDates.first?.atEightAM() {
|
|
if first.isEarlierThan(startDate) {
|
|
return startDate
|
|
} else {
|
|
return first
|
|
}
|
|
} else {
|
|
return startDate
|
|
}
|
|
}
|
|
|
|
func publishedGroupStagesDate() -> Date? {
|
|
let matches: [Match] = self.groupStages().flatMap { $0.playedMatches() }
|
|
return self._publishedDateFromMatches(matches)
|
|
}
|
|
|
|
func areGroupStagesPublished() -> Bool {
|
|
if publishGroupStages { return true }
|
|
if let publishedGroupStagesDate = publishedGroupStagesDate() {
|
|
return Date() >= publishedGroupStagesDate
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func publishedBracketsDate() -> Date? {
|
|
let matches: [Match] = self.rounds().flatMap { $0.playedMatches() }
|
|
return self._publishedDateFromMatches(matches)
|
|
}
|
|
|
|
func areBracketsPublished() -> Bool {
|
|
if publishBrackets { return true }
|
|
if let publishedBracketsDate = publishedBracketsDate() {
|
|
return Date() >= publishedBracketsDate
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func shareURL(_ pageLink: PageLink = .matches) -> URL? {
|
|
if pageLink == .clubBroadcast {
|
|
let club = club()
|
|
print("club", club)
|
|
print("club broadcast code", club?.broadcastCode)
|
|
if let club, let broadcastCode = club.broadcastCode {
|
|
return URLs.main.url.appending(path: "c/\(broadcastCode)")
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path)
|
|
}
|
|
|
|
func courtUsed(runningMatches: [Match]) -> [Int] {
|
|
#if _DEBUGING_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
|
|
}
|
|
|
|
func hasStarted() -> Bool {
|
|
return startDate <= Date()
|
|
}
|
|
|
|
func eventObject() -> Event? {
|
|
guard let event else { return nil }
|
|
return Store.main.findById(event)
|
|
}
|
|
|
|
func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String {
|
|
let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams()
|
|
switch exportFormat {
|
|
case .rawText:
|
|
return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2))
|
|
case .csv:
|
|
let headers = ["N°", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids"].joined(separator: exportFormat.separator())
|
|
var teamPaste = [headers]
|
|
for (index, team) in selectedSortedTeams.enumerated() {
|
|
teamPaste.append(team.pasteData(exportFormat, index + 1))
|
|
}
|
|
return teamPaste.joined(separator: exportFormat.newLineSeparator())
|
|
case .championship:
|
|
let headers = [
|
|
"Horodateur",
|
|
"alertCount",
|
|
"alertDescripion",
|
|
|
|
"poids",
|
|
"joker",
|
|
|
|
"playerCount",
|
|
"nveqCount",
|
|
"NC Non vérifié",
|
|
|
|
"Adresse e-mail",
|
|
"Code Club",
|
|
"Nom du Club",
|
|
"Catégorie",
|
|
"Numéro d'Equipe",
|
|
// "Nom du Capitaine (Il doit être licencié FFT 2025)",
|
|
// "Numéro du Téléphone",
|
|
// "E-mail",
|
|
// "Nom du Correspondant",
|
|
// "Numéro du Téléphone",
|
|
// "E-mail",
|
|
"JOUEUR 1 - Nom",
|
|
"JOUEUR 1 - Prénom",
|
|
"JOUEUR 1 - Vérif",
|
|
"JOUEUR 1 - Licence",
|
|
"JOUEUR 1 - Ranking",
|
|
"JOUEUR 1 - Statut",
|
|
"JOUEUR 2 - Nom",
|
|
"JOUEUR 2 - Prénom",
|
|
"JOUEUR 2 - Vérif",
|
|
"JOUEUR 2 - Licence",
|
|
"JOUEUR 2 - Ranking",
|
|
"JOUEUR 2 - Statut",
|
|
"JOUEUR 3 - Nom",
|
|
"JOUEUR 3 - Prénom",
|
|
"JOUEUR 3 - Vérif",
|
|
"JOUEUR 3 - Licence",
|
|
"JOUEUR 3 - Ranking",
|
|
"JOUEUR 3 - Statut",
|
|
"JOUEUR 4 - Nom",
|
|
"JOUEUR 4 - Prénom",
|
|
"JOUEUR 4 - Vérif",
|
|
"JOUEUR 4 - Licence",
|
|
"JOUEUR 4 - Ranking",
|
|
"JOUEUR 4 - Statut",
|
|
"JOUEUR 5 - Nom",
|
|
"JOUEUR 5 - Prénom",
|
|
"JOUEUR 5 - Vérif",
|
|
"JOUEUR 5 - Licence",
|
|
"JOUEUR 5 - Ranking",
|
|
"JOUEUR 5 - Statut",
|
|
"JOUEUR 6 - Nom",
|
|
"JOUEUR 6 - Prénom",
|
|
"JOUEUR 6 - Vérif",
|
|
"JOUEUR 6 - Licence",
|
|
"JOUEUR 6 - Ranking",
|
|
"JOUEUR 6 - Statut",
|
|
"JOUEUR 7 - Nom",
|
|
"JOUEUR 7 - Prénom",
|
|
"JOUEUR 7 - Vérif",
|
|
"JOUEUR 7 - Licence",
|
|
"JOUEUR 7 - Ranking",
|
|
"JOUEUR 7 - Statut",
|
|
"JOUEUR 8 - Nom",
|
|
"JOUEUR 8 - Prénom",
|
|
"JOUEUR 8 - Vérif",
|
|
"JOUEUR 8 - Licence",
|
|
"JOUEUR 8 - Ranking",
|
|
"JOUEUR 8 - Statut",
|
|
"JOUEUR 9 - Nom",
|
|
"JOUEUR 9 - Prénom",
|
|
"JOUEUR 9 - Vérif",
|
|
"JOUEUR 9 - Licence",
|
|
"JOUEUR 9 - Ranking",
|
|
"JOUEUR 9 - Statut",
|
|
"JOUEUR 10 - Nom",
|
|
"JOUEUR 10 - Prénom",
|
|
"JOUEUR 10 - Vérif",
|
|
"JOUEUR 10 - Licence",
|
|
"JOUEUR 10 - Ranking",
|
|
"JOUEUR 10 - Statut",
|
|
].joined(separator: exportFormat.separator())
|
|
var teamPaste = [headers]
|
|
for (index, team) in selectedSortedTeams.enumerated() {
|
|
teamPaste.append(team.pasteData(exportFormat, index + 1))
|
|
}
|
|
return teamPaste.joined(separator: exportFormat.newLineSeparator())
|
|
}
|
|
}
|
|
|
|
func club() -> Club? {
|
|
return eventObject()?.clubObject()
|
|
}
|
|
|
|
func locationLabel(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
if let club = club() {
|
|
switch displayStyle {
|
|
case .wide, .title:
|
|
return club.name
|
|
case .short:
|
|
return club.acronym
|
|
}
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func hasEnded() -> Bool {
|
|
return endDate != nil
|
|
}
|
|
|
|
func state() -> State {
|
|
if self.isCanceled == true {
|
|
return .canceled
|
|
}
|
|
|
|
if self.hasEnded() { return .finished }
|
|
|
|
let isBuild = (groupStageCount > 0 && groupStages().isEmpty == false)
|
|
|| rounds().isEmpty == false
|
|
|
|
if isBuild && startDate <= Date() { return .running }
|
|
|
|
if isBuild {
|
|
return .build
|
|
}
|
|
return .initial
|
|
}
|
|
|
|
func seededTeams() -> [TeamRegistration] {
|
|
return selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil })
|
|
}
|
|
|
|
func groupStageTeams() -> [TeamRegistration] {
|
|
return selectedSortedTeams().filter({ $0.groupStagePosition != nil })
|
|
}
|
|
|
|
func groupStageSpots() -> Int {
|
|
return groupStages().map { $0.size }.reduce(0,+)
|
|
}
|
|
|
|
func seeds() -> [TeamRegistration] {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
let seeds = max(selectedSortedTeams.count - groupStageSpots() , 0)
|
|
return Array(selectedSortedTeams.prefix(seeds))
|
|
}
|
|
|
|
func availableSeeds() -> [TeamRegistration] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func availableSeeds()", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
|
|
|
|
return seeds().filter { $0.isSeedable() }
|
|
}
|
|
|
|
func lastSeedRound() -> Int {
|
|
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
|
|
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func getRound(atRoundIndex roundIndex: Int) -> Round? {
|
|
return self.tournamentStore.rounds.first(where: { $0.index == roundIndex })
|
|
// return Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first
|
|
}
|
|
|
|
func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
|
|
return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? []
|
|
}
|
|
|
|
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
|
|
return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? []
|
|
}
|
|
|
|
func availableSeedGroups() -> [SeedInterval] {
|
|
let seeds = seeds()
|
|
var availableSeedGroup = Set<SeedInterval>()
|
|
for (index, seed) in seeds.enumerated() {
|
|
if seed.isSeedable(), let seedGroup = seedGroup(for: index) {
|
|
availableSeedGroup.insert(seedGroup)
|
|
}
|
|
}
|
|
return availableSeedGroup.sorted(by: <)
|
|
}
|
|
|
|
func seedGroup(for alreadySetupSeeds: Int) -> SeedInterval? {
|
|
switch alreadySetupSeeds {
|
|
case 0...1:
|
|
return SeedInterval(first: 1, last: 2)
|
|
case 2...3:
|
|
return SeedInterval(first: 3, last: 4)
|
|
case 4...7:
|
|
return SeedInterval(first: 5, last: 8)
|
|
case 8...15:
|
|
return SeedInterval(first: 9, last: 16)
|
|
case 16...23:
|
|
return SeedInterval(first: 17, last: 24)
|
|
case 24...31:
|
|
return SeedInterval(first: 25, last: 32)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func availableSeedGroup() -> SeedInterval? {
|
|
let seeds = seeds()
|
|
if let firstIndex = seeds.firstIndex(where: { $0.isSeedable() }) {
|
|
guard let seedGroup = seedGroup(for: firstIndex) else { return nil }
|
|
return seedGroup
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func randomSeed(fromSeedGroup seedGroup: SeedInterval) -> TeamRegistration? {
|
|
let availableSeeds = seeds(inSeedGroup: seedGroup)
|
|
return availableSeeds.randomElement()
|
|
}
|
|
|
|
func seeds(inSeedGroup seedGroup: SeedInterval) -> [TeamRegistration] {
|
|
let availableSeedInSeedGroup = (seedGroup.last - seedGroup.first) + 1
|
|
let availableSeeds = seeds().dropFirst(seedGroup.first - 1).prefix(availableSeedInSeedGroup).filter({ $0.isSeedable() })
|
|
return availableSeeds
|
|
}
|
|
|
|
func seedGroupAvailable(atRoundIndex roundIndex: Int) -> SeedInterval? {
|
|
if let availableSeedGroup = availableSeedGroup() {
|
|
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: availableSeedGroup)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func seedGroupAvailable(atRoundIndex roundIndex: Int, availableSeedGroup: SeedInterval) -> SeedInterval? {
|
|
|
|
if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() {
|
|
|
|
if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup }
|
|
let availableSeeds = seeds(inSeedGroup: availableSeedGroup)
|
|
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
|
|
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
|
|
|
|
if availableSeedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
|
|
print("availableSeedGroup == SeedInterval(first: 3, last: 4)")
|
|
return availableSeedGroup
|
|
}
|
|
|
|
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
|
|
return availableSeedGroup
|
|
} else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count {
|
|
return availableSeedGroup
|
|
} else if let chunks = availableSeedGroup.chunks() {
|
|
if let chunk = chunks.first(where: { seedInterval in
|
|
seedInterval.first >= self.seededTeams().count
|
|
}) {
|
|
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) {
|
|
if seedGroup == SeedInterval(first: 1, last: 2) {
|
|
let seeds = seeds()
|
|
if let matches = getRound(atRoundIndex: roundIndex)?.playedMatches() {
|
|
if let lastMatch = matches.last {
|
|
seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false)
|
|
}
|
|
if let firstMatch = matches.first {
|
|
seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false)
|
|
}
|
|
}
|
|
} else {
|
|
|
|
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
|
|
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
|
|
let availableSeeds = seeds(inSeedGroup: seedGroup)
|
|
|
|
if seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
|
|
var spots = [Match]()
|
|
spots.append(availableSeedSpot[1])
|
|
spots.append(availableSeedSpot[4])
|
|
spots = spots.shuffled()
|
|
for (index, seed) in availableSeeds.enumerated() {
|
|
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
|
|
}
|
|
} else {
|
|
if availableSeeds.count <= availableSeedSpot.count {
|
|
let spots = availableSeedSpot.shuffled()
|
|
for (index, seed) in availableSeeds.enumerated() {
|
|
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
|
|
}
|
|
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
|
|
|
|
let spots = availableSeedOpponentSpot.shuffled()
|
|
for (index, seed) in availableSeeds.enumerated() {
|
|
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
|
|
}
|
|
} else if let chunks = seedGroup.chunks() {
|
|
if let chunk = chunks.first(where: { seedInterval in
|
|
seedInterval.first >= self.seededTeams().count
|
|
}) {
|
|
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
func inscriptionClosed() -> Bool {
|
|
closedRegistrationDate != nil
|
|
}
|
|
|
|
func getActiveGroupStage(atStep step: Int = 0) -> GroupStage? {
|
|
let groupStages = groupStages(atStep: step)
|
|
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
|
|
}
|
|
|
|
func matchesWithSpace() -> [Match] {
|
|
getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? []
|
|
}
|
|
|
|
func getActiveRound(withSeeds: Bool = false) -> Round? {
|
|
let rounds: [Round] = self.rounds()
|
|
let unfinishedRounds: [Round] = rounds.filter { $0.hasStarted() && $0.hasEnded() == false }
|
|
let sortedRounds: [Round] = unfinishedRounds.sorted(by: \.index).reversed()
|
|
|
|
let round = sortedRounds.first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first
|
|
|
|
if withSeeds {
|
|
if round?.seeds().isEmpty == false {
|
|
return round
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
return round
|
|
}
|
|
}
|
|
|
|
func getPlayedMatchDateIntervals(in event: Event) -> [DateInterval] {
|
|
let allMatches: [Match] = self.allMatches().filter { $0.courtIndex != nil && $0.startDate != nil }
|
|
return allMatches.map { match in
|
|
DateInterval(event: event.id, courtIndex: match.courtIndex!, startDate: match.startDate!, endDate: match.estimatedEndDate(additionalEstimationDuration)!)
|
|
}
|
|
}
|
|
|
|
func allRoundMatches() -> [Match] {
|
|
return allRounds().flatMap { $0._matches() }
|
|
}
|
|
|
|
func allMatches() -> [Match] {
|
|
return self.tournamentStore.matches.filter { $0.disabled == false }
|
|
}
|
|
|
|
func _allMatchesIncludingDisabled() -> [Match] {
|
|
return Array(self.tournamentStore.matches)
|
|
}
|
|
|
|
func rounds() -> [Round] {
|
|
let rounds: [Round] = self.tournamentStore.rounds.filter { $0.isUpperBracket() }
|
|
return rounds.sorted(by: \.index).reversed()
|
|
}
|
|
|
|
func sortedTeams() -> [TeamRegistration] {
|
|
let teams = selectedSortedTeams()
|
|
return teams + waitingListTeams(in: teams, includingWalkOuts: true)
|
|
}
|
|
|
|
func waitingListSortedTeams() -> [TeamRegistration] {
|
|
let teams = selectedSortedTeams()
|
|
return waitingListTeams(in: teams, includingWalkOuts: false)
|
|
}
|
|
|
|
|
|
func selectedSortedTeams() -> [TeamRegistration] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
var _sortedTeams : [TeamRegistration] = []
|
|
var _teams = unsortedTeams().filter({ $0.isOutOfTournament() == false })
|
|
|
|
if let closedRegistrationDate {
|
|
_teams = _teams.filter({ team in
|
|
if let registrationDate = team.registrationDate {
|
|
return registrationDate <= closedRegistrationDate
|
|
} else {
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting()
|
|
|
|
let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.id)], order: .ascending)
|
|
|
|
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
|
|
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
|
|
let groupStageSpots: Int = self.groupStageSpots()
|
|
var bracketSeeds: Int = min(teamCount, _teams.count) - groupStageSpots - wcBracket.count
|
|
var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
|
|
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
|
|
if bracketSeeds < 0 { bracketSeeds = 0 }
|
|
|
|
if prioritizeClubMembers {
|
|
|
|
var bracketTeams: [TeamRegistration] = []
|
|
bracketTeams.append(contentsOf: _completeTeams.filter { $0.hasMemberOfClub(clubName) })
|
|
|
|
let others: [TeamRegistration] = _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }
|
|
let sortedOthers: [TeamRegistration] = others.sorted(using: defaultSorting, order: .ascending)
|
|
bracketTeams.append(contentsOf: sortedOthers)
|
|
|
|
bracketTeams = bracketTeams
|
|
.prefix(bracketSeeds)
|
|
.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
bracketTeams.append(contentsOf: wcBracket)
|
|
|
|
// let bracketTeams: [TeamRegistration] = (_completeTeams.filter { $0.hasMemberOfClub(clubName) } + _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
|
|
|
|
let groupStageTeamsNoFiltering = Set(_completeTeams).subtracting(bracketTeams)
|
|
let groupStageTeams = (groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) } + groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
|
|
|
|
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
} else {
|
|
let bracketTeams = _completeTeams.prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
|
|
let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
|
|
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
}
|
|
return _sortedTeams
|
|
}
|
|
|
|
func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] {
|
|
let waitingList = Set(unsortedTeams()).subtracting(teams)
|
|
let waitings = waitingList.filter { $0.isOutOfTournament() == false }.sorted(using: _defaultSorting(), order: .ascending)
|
|
let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
|
|
if includingWalkOuts {
|
|
return waitings + walkOuts
|
|
} else {
|
|
return waitings
|
|
}
|
|
}
|
|
|
|
func bracketCut(teamCount: Int) -> Int {
|
|
return max(0, teamCount - groupStageCut())
|
|
}
|
|
|
|
func groupStageCut() -> Int {
|
|
return groupStageSpots()
|
|
}
|
|
|
|
func cutLabel(index: Int, teamCount: Int?) -> String {
|
|
let _teamCount = teamCount ?? selectedSortedTeams().count
|
|
let bracketCut = bracketCut(teamCount: _teamCount)
|
|
if index < bracketCut {
|
|
return "Tableau"
|
|
} else if index - bracketCut < groupStageCut() && _teamCount > 0 {
|
|
return "Poule"
|
|
} else {
|
|
return "Attente"
|
|
}
|
|
}
|
|
|
|
func cutLabelColor(index: Int?, teamCount: Int?) -> Color {
|
|
guard let index else { return Color.gray }
|
|
let _teamCount = teamCount ?? selectedSortedTeams().count
|
|
let bracketCut = bracketCut(teamCount: _teamCount)
|
|
if index < bracketCut {
|
|
return Color.mint
|
|
} else if index - bracketCut < groupStageCut() && _teamCount > 0 {
|
|
return Color.cyan
|
|
} else {
|
|
return Color.gray
|
|
}
|
|
}
|
|
|
|
func unsortedTeamsWithoutWO() -> [TeamRegistration] {
|
|
return self.tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false }
|
|
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
|
|
}
|
|
|
|
func walkoutTeams() -> [TeamRegistration] {
|
|
return self.tournamentStore.teamRegistrations.filter { $0.walkOut == true }
|
|
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == true }
|
|
}
|
|
|
|
func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
var duplicates = [PlayerRegistration]()
|
|
Set(players.compactMap({ $0.licenceId })).forEach { licenceId in
|
|
let found = players.filter({ $0.licenceId == licenceId })
|
|
if found.count > 1 {
|
|
duplicates.append(found.first!)
|
|
}
|
|
}
|
|
return duplicates
|
|
}
|
|
|
|
func homonyms(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
players.filter({ $0.hasHomonym() })
|
|
}
|
|
|
|
func unsortedPlayers() -> [PlayerRegistration] {
|
|
return Array(self.tournamentStore.playerRegistrations)
|
|
}
|
|
|
|
func selectedPlayers() -> [PlayerRegistration] {
|
|
return self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank)
|
|
}
|
|
|
|
func paidSelectedPlayers(type: PlayerRegistration.PlayerPaymentType) -> Double? {
|
|
if let entryFee {
|
|
return Double(self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }.filter { $0.paymentType == type }.count) * entryFee
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func players() -> [PlayerRegistration] {
|
|
return self.tournamentStore.playerRegistrations.sorted(by: \.computedRank)
|
|
}
|
|
|
|
func unrankValue(for malePlayer: Bool) -> Int? {
|
|
switch tournamentCategory {
|
|
case .unlisted:
|
|
return nil
|
|
case .men:
|
|
return maleUnrankedValue
|
|
case .women:
|
|
return femaleUnrankedValue
|
|
case .mix:
|
|
return malePlayer ? maleUnrankedValue : femaleUnrankedValue
|
|
}
|
|
}
|
|
|
|
//todo
|
|
var clubName: String? {
|
|
return self.eventObject()?.clubObject()?.name
|
|
}
|
|
|
|
//todo
|
|
func significantPlayerCount() -> Int {
|
|
return 6
|
|
}
|
|
|
|
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
if startDate.isInCurrentYear() == false {
|
|
return []
|
|
}
|
|
return players.filter { player in
|
|
return isPlayerRankInadequate(player: player)
|
|
}
|
|
}
|
|
|
|
func isPlayerRankInadequate(player: PlayerHolder) -> Bool {
|
|
guard let rank = player.getRank() else { return false }
|
|
let _rank = player.male ? rank : rank + PlayerRegistration.addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0)
|
|
if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
if startDate.isInCurrentYear() == false {
|
|
return []
|
|
}
|
|
return players.filter { player in
|
|
return isPlayerAgeInadequate(player: player)
|
|
}
|
|
}
|
|
|
|
func isPlayerAgeInadequate(player: PlayerHolder) -> Bool {
|
|
guard let computedAge = player.computedAge else { return false }
|
|
if federalTournamentAge.isAgeValid(age: computedAge) == false {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func licenseYearValidity() -> Int {
|
|
if startDate.get(.month) > 8 {
|
|
return startDate.get(.year) + 1
|
|
} else {
|
|
return startDate.get(.year)
|
|
}
|
|
}
|
|
|
|
func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] {
|
|
let licenseYearValidity = self.licenseYearValidity()
|
|
return players.filter({ player in
|
|
if player.isImported() {
|
|
// Player is marked as imported: check if the license is valid
|
|
return !player.isValidLicenseNumber(year: licenseYearValidity)
|
|
} else {
|
|
// Player is not imported: validate license and handle `isImported` flag for non-imported players
|
|
let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true
|
|
let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false
|
|
|
|
// If global `isImported` is true, check license number as well
|
|
let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity)
|
|
|
|
return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag
|
|
}
|
|
})
|
|
}
|
|
|
|
func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? {
|
|
guard let seedIndex else { return nil }
|
|
return selectedSortedTeams()[safe: seedIndex]?.callDate
|
|
}
|
|
|
|
func importTeams(_ teams: [FileImportManager.TeamHolder]) {
|
|
var teamsToImport = [TeamRegistration]()
|
|
let players = players().filter { $0.licenceId != nil }
|
|
teams.forEach { team in
|
|
if let previousTeam = team.previousTeam {
|
|
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory)
|
|
teamsToImport.append(previousTeam)
|
|
} else {
|
|
var registrationDate = team.registrationDate ?? team.teamChampionship?.getRegistrationDate()
|
|
if let previousPlayer = players.first(where: { player in
|
|
let ids = team.players.compactMap({ $0.licenceId })
|
|
return ids.contains(player.licenceId!)
|
|
}), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate {
|
|
registrationDate = previousTeamRegistrationDate
|
|
}
|
|
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name ?? team.teamChampionship?.teamIndex)
|
|
newTeam.clubCode = team.teamChampionship?.clubCode
|
|
newTeam.clubName = team.teamChampionship?.clubName
|
|
newTeam.registratonMail = team.teamChampionship?.registrationMail
|
|
if isAnimation() {
|
|
if newTeam.weight == 0 {
|
|
newTeam.weight = team.index(in: teams) ?? 0
|
|
}
|
|
}
|
|
teamsToImport.append(newTeam)
|
|
}
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
do {
|
|
try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players })
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
|
|
if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty {
|
|
setGroupStage(randomize: groupStageSortMode == .random)
|
|
}
|
|
}
|
|
|
|
func maximumCourtsPerGroupSage() -> Int {
|
|
if teamsPerGroupStage > 1 {
|
|
return min(teamsPerGroupStage / 2, courtCount)
|
|
} else {
|
|
return max(1, courtCount)
|
|
}
|
|
}
|
|
|
|
func registrationIssues() async -> Int {
|
|
let players : [PlayerRegistration] = unsortedPlayers()
|
|
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
|
|
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
|
|
let duplicates : [PlayerRegistration] = duplicates(in: players)
|
|
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
|
|
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players)
|
|
let homonyms = homonyms(in: players)
|
|
let ageInadequatePlayers = ageInadequatePlayers(in: players)
|
|
let isImported = players.anySatisfy({ $0.isImported() })
|
|
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported)
|
|
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
|
|
let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams, includingWalkOuts: true)
|
|
let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil })
|
|
let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil })
|
|
|
|
return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count
|
|
}
|
|
|
|
func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool {
|
|
guard let summonDate = team.callDate else { return true }
|
|
let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate
|
|
guard let expectedSummonDate else { return true }
|
|
return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame
|
|
}
|
|
|
|
func groupStagesMatches(atStep step: Int = 0) -> [Match] {
|
|
return groupStages(atStep: step).flatMap({ $0._matches() })
|
|
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
|
|
}
|
|
|
|
static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)]
|
|
|
|
static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending)
|
|
}
|
|
|
|
static func runningMatches(_ allMatches: [Match]) -> [Match] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
|
|
}
|
|
|
|
static func readyMatches(_ allMatches: [Match]) -> [Match] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
|
|
}
|
|
|
|
static func matchesLeft(_ allMatches: [Match]) -> [Match] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
|
|
}
|
|
|
|
|
|
static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
if let limit {
|
|
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(limit))
|
|
} else {
|
|
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed())
|
|
}
|
|
}
|
|
|
|
func teamsRanked() -> [TeamRegistration] {
|
|
let selected = selectedSortedTeams().filter({ $0.finalRanking != nil })
|
|
return selected.sorted(by: \.finalRanking!, order: .ascending)
|
|
}
|
|
|
|
private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) {
|
|
for key in dictionary.keys {
|
|
if var stringArray = dictionary[key] {
|
|
// Remove all instances of each string in stringsToRemove
|
|
stringArray.removeAll { stringsToRemove.contains($0) }
|
|
dictionary[key] = stringArray
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
func finalRanking() async -> [Int: [String]] {
|
|
var teams: [Int: [String]] = [:]
|
|
var ids: Set<String> = Set<String>()
|
|
let rounds = rounds()
|
|
let lastStep = lastStep()
|
|
if rounds.isEmpty, lastStep > 0 {
|
|
let groupStages = groupStages(atStep: lastStep)
|
|
|
|
for groupStage in groupStages {
|
|
let groupStageTeams = groupStage.teams(true)
|
|
for teamIndex in 0..<groupStageTeams.count {
|
|
teams[groupStage.index * groupStage.size + 1 + teamIndex] = [groupStageTeams[teamIndex].id]
|
|
}
|
|
}
|
|
} else {
|
|
|
|
let final = rounds.last?.playedMatches().last
|
|
if let winner = final?.winningTeamId {
|
|
teams[1] = [winner]
|
|
ids.insert(winner)
|
|
}
|
|
if let finalist = final?.losingTeamId {
|
|
teams[2] = [finalist]
|
|
ids.insert(finalist)
|
|
}
|
|
|
|
let others: [Round] = rounds.flatMap { round in
|
|
let losers = round.losers()
|
|
let minimumFinalPosition = round.seedInterval()?.last ?? teamCount
|
|
if teams[minimumFinalPosition] == nil {
|
|
teams[minimumFinalPosition] = losers.map { $0.id }
|
|
} else {
|
|
teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id })
|
|
}
|
|
|
|
print("round", round.roundTitle())
|
|
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
|
|
print(rounds.count, rounds.map { $0.roundTitle() })
|
|
return rounds
|
|
}.compactMap({ $0 })
|
|
|
|
others.forEach { round in
|
|
print("round", round.roundTitle())
|
|
if let interval = round.seedInterval() {
|
|
print("interval", interval.localizedInterval())
|
|
let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() }
|
|
print("playedMatches", playedMatches.count)
|
|
let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false })
|
|
print("winners", winners.count)
|
|
let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false })
|
|
print("losers", losers.count)
|
|
if winners.isEmpty {
|
|
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
|
|
if disabledIds.isEmpty == false {
|
|
_removeStrings(from: &teams, stringsToRemove: disabledIds)
|
|
teams[interval.last] = disabledIds
|
|
let teamNames : [String] = disabledIds.compactMap {
|
|
let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0)
|
|
return t
|
|
}.map { $0.canonicalName }
|
|
print("winners.isEmpty", "\(interval.last) : ", teamNames)
|
|
disabledIds.forEach {
|
|
ids.insert($0)
|
|
}
|
|
}
|
|
} else {
|
|
if winners.isEmpty == false {
|
|
_removeStrings(from: &teams, stringsToRemove: winners)
|
|
teams[interval.first + winners.count - 1] = winners
|
|
let teamNames : [String] = winners.compactMap {
|
|
let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0)
|
|
return t
|
|
}.map { $0.canonicalName }
|
|
print("winners", "\(interval.last + winners.count - 1) : ", teamNames)
|
|
winners.forEach { ids.insert($0) }
|
|
}
|
|
|
|
if losers.isEmpty == false {
|
|
_removeStrings(from: &teams, stringsToRemove: losers)
|
|
teams[interval.first + winners.count] = losers
|
|
let loserTeamNames : [String] = losers.compactMap {
|
|
let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0)
|
|
return t
|
|
}.map { $0.canonicalName }
|
|
print("losers", "\(interval.first + winners.count) : ", loserTeamNames)
|
|
losers.forEach { ids.insert($0) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() {
|
|
groupStageLoserBracketPlayedMatches.forEach({ match in
|
|
if match.hasEnded() {
|
|
let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count
|
|
teams.setOrAppend(match.winningTeamId, at: match.index)
|
|
teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount)
|
|
}
|
|
})
|
|
}
|
|
|
|
let groupStages = groupStages()
|
|
let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified
|
|
let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 }))
|
|
groupStages.forEach { groupStage in
|
|
let groupStageTeams = groupStage.teams(true)
|
|
for (index, team) in groupStageTeams.enumerated() {
|
|
if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false {
|
|
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
|
|
|
|
let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0)
|
|
if let existingTeams = teams[_index] {
|
|
teams[_index] = existingTeams + [team.id]
|
|
} else {
|
|
teams[_index] = [team.id]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return teams
|
|
}
|
|
|
|
func setRankings(finalRanks: [Int: [String]]) async -> [Int: [TeamRegistration]] {
|
|
var rankings: [Int: [TeamRegistration]] = [:]
|
|
|
|
finalRanks.keys.sorted().forEach { rank in
|
|
if let rankedTeamIds = finalRanks[rank] {
|
|
let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
|
|
rankings[rank] = teams
|
|
}
|
|
}
|
|
|
|
rankings.keys.sorted().forEach { rank in
|
|
if let rankedTeams = rankings[rank] {
|
|
rankedTeams.forEach { team in
|
|
team.finalRanking = rank
|
|
team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: rank - 1, count: teamCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
|
|
return rankings
|
|
}
|
|
|
|
func lockRegistration() {
|
|
closedRegistrationDate = Date()
|
|
let count = selectedSortedTeams().count
|
|
if teamCount != count {
|
|
teamCount = count
|
|
}
|
|
let teams = unsortedTeams()
|
|
teams.forEach { team in
|
|
team.lockedWeight = team.weight
|
|
}
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func unlockRegistration() {
|
|
closedRegistrationDate = nil
|
|
let teams = unsortedTeams()
|
|
teams.forEach { team in
|
|
team.lockedWeight = nil
|
|
}
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
|
|
func updateWeights() {
|
|
let teams = self.unsortedTeams()
|
|
teams.forEach { team in
|
|
let players = team.unsortedPlayers()
|
|
players.forEach { $0.setComputedRank(in: self) }
|
|
team.setWeight(from: players.sorted(by: \.computedRank), inTournamentCategory: tournamentCategory)
|
|
do {
|
|
try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func updateRank(to newDate: Date?) async throws {
|
|
guard let newDate else { return }
|
|
rankSourceDate = newDate
|
|
|
|
if currentMonthData() == nil {
|
|
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
|
|
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
|
|
await MainActor.run {
|
|
let formatted: String = URL.importDateFormatter.string(from: newDate)
|
|
let monthData: MonthData = MonthData(monthKey: formatted)
|
|
monthData.maleUnrankedValue = lastRankMan
|
|
monthData.femaleUnrankedValue = lastRankWoman
|
|
do {
|
|
try DataStore.shared.monthData.addOrUpdate(instance: monthData)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
let lastRankMan = currentMonthData()?.maleUnrankedValue
|
|
let lastRankWoman = currentMonthData()?.femaleUnrankedValue
|
|
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
|
|
let sources = dataURLs.map { CSVParser(url: $0) }
|
|
|
|
try await unsortedPlayers().concurrentForEach { player in
|
|
try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0)
|
|
}
|
|
}
|
|
|
|
func missingUnrankedValue() -> Bool {
|
|
return maleUnrankedValue == nil || femaleUnrankedValue == nil
|
|
}
|
|
|
|
func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? {
|
|
return unsortedTeams().first(where: { $0.includes(players: players) })
|
|
}
|
|
|
|
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
if tournamentLevel == .unlisted, displayStyle == .title {
|
|
if let name {
|
|
return name
|
|
} else {
|
|
return tournamentLevel.localizedLevelLabel(.title)
|
|
}
|
|
}
|
|
let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ")
|
|
if displayStyle == .wide, let name {
|
|
return [title, name].joined(separator: " - ")
|
|
} else {
|
|
return title
|
|
}
|
|
}
|
|
|
|
func localizedTournamentType() -> String {
|
|
switch tournamentLevel {
|
|
case .unlisted:
|
|
return tournamentLevel.localizedLevelLabel(.short)
|
|
default:
|
|
return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedLabel(.short)
|
|
}
|
|
}
|
|
|
|
func hideWeight() -> Bool {
|
|
return hideTeamsWeight || tournamentLevel.hideWeight()
|
|
}
|
|
|
|
func isAnimation() -> Bool {
|
|
federalLevelCategory == .unlisted
|
|
}
|
|
|
|
func subtitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
return name ?? ""
|
|
}
|
|
|
|
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
switch displayStyle {
|
|
case .title:
|
|
startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
|
|
case .wide:
|
|
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
|
|
case .short:
|
|
startDate.formatted(date: .numeric, time: .omitted)
|
|
}
|
|
}
|
|
|
|
func qualifiedFromGroupStage() -> Int {
|
|
return groupStageCount * qualifiedPerGroupStage
|
|
}
|
|
|
|
|
|
func availableQualifiedTeams() -> [TeamRegistration] {
|
|
#if _DEBUG_TIME //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func availableQualifiedTeams()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
|
|
return unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil })
|
|
}
|
|
|
|
func qualifiedTeams() -> [TeamRegistration] {
|
|
return unsortedTeams().filter({ $0.qualified })
|
|
}
|
|
|
|
func moreQualifiedToDraw() -> Int {
|
|
return max((qualifiedFromGroupStage() + groupStageAdditionalQualified) - qualifiedTeams().count, 0)
|
|
}
|
|
|
|
func missingQualifiedFromGroupStages() -> [TeamRegistration] {
|
|
if groupStageAdditionalQualified > 0 && groupStagesAreOver() {
|
|
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
|
|
groupStage.teams(true)[safe: qualifiedPerGroupStage]
|
|
}
|
|
.filter({ $0.qualified == false })
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
func groupStagesAreOver(atStep: Int = 0) -> Bool {
|
|
let groupStages = groupStages(atStep: atStep)
|
|
guard groupStages.isEmpty == false else {
|
|
return true
|
|
}
|
|
return groupStages.allSatisfy({ $0.hasEnded() })
|
|
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
|
|
}
|
|
|
|
func groupStageLoserBracketAreOver() -> Bool {
|
|
guard let groupStageLoserBracket = groupStageLoserBracket() else {
|
|
return true
|
|
}
|
|
return groupStageLoserBracket.hasEnded()
|
|
}
|
|
|
|
fileprivate func _paymentMethodMessage() -> String? {
|
|
return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods
|
|
}
|
|
|
|
var entryFeeMessage: String {
|
|
if let entryFee {
|
|
let message: String = "Inscription : \(entryFee.formatted(.currency(code: Locale.defaultCurrency()))) par joueur."
|
|
return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n")
|
|
} else {
|
|
return "Inscription : gratuite."
|
|
}
|
|
}
|
|
|
|
func umpireMail() -> [String]? {
|
|
return [DataStore.shared.user.email]
|
|
}
|
|
|
|
func earnings() -> Double {
|
|
if let entryFee {
|
|
return Double(selectedPlayers().filter { $0.hasPaid() }.count) * entryFee
|
|
} else {
|
|
return 0.0
|
|
}
|
|
}
|
|
|
|
func paidCompletion() -> Double {
|
|
let selectedPlayers = selectedPlayers()
|
|
if selectedPlayers.isEmpty { return 0 }
|
|
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count)
|
|
}
|
|
|
|
func presenceStatus() -> Double {
|
|
let selectedPlayers = selectedPlayers()
|
|
if selectedPlayers.isEmpty { return 0 }
|
|
return Double(selectedPlayers.filter { $0.hasArrived }.count) / Double(selectedPlayers.count)
|
|
}
|
|
|
|
typealias TournamentStatus = (label:String, completion: String)
|
|
func cashierStatus() async -> TournamentStatus {
|
|
let selectedPlayers = selectedPlayers()
|
|
var filteredPlayers = [PlayerRegistration]()
|
|
var wording = ""
|
|
if isFree() {
|
|
wording = "présent"
|
|
filteredPlayers = selectedPlayers.filter({ $0.hasArrived })
|
|
} else {
|
|
wording = "encaissé"
|
|
filteredPlayers = selectedPlayers.filter({ $0.hasPaid() })
|
|
}
|
|
// let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés"
|
|
let label = "\(filteredPlayers.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs \(wording)\(filteredPlayers.count.pluralSuffix)"
|
|
let completion = (Double(filteredPlayers.count) / Double(selectedPlayers.count))
|
|
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
|
|
return TournamentStatus(label: label, completion: completionLabel)
|
|
}
|
|
|
|
func scheduleStatus() async -> TournamentStatus {
|
|
let allMatches = allMatches()
|
|
let ready = allMatches.filter({ $0.startDate != nil })
|
|
// let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés"
|
|
let label = "\(ready.count.formatted()) / \(allMatches.count.formatted()) matchs programmés"
|
|
let completion = (Double(ready.count) / Double(allMatches.count))
|
|
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
|
|
return TournamentStatus(label: label, completion: completionLabel)
|
|
}
|
|
|
|
func callStatus() async -> TournamentStatus {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false }
|
|
let justCalled = selectedSortedTeams.filter { $0.called() }
|
|
|
|
let label = "\(justCalled.count.formatted()) / \(selectedSortedTeams.count.formatted()) (\(called.count.formatted()) au bon horaire)"
|
|
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
|
|
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
|
|
return TournamentStatus(label: label, completion: completionLabel)
|
|
}
|
|
|
|
func confirmedSummonStatus() async -> TournamentStatus {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
let called = selectedSortedTeams.filter { $0.confirmationDate != nil }
|
|
let label = "\(called.count.formatted()) / \(selectedSortedTeams.count.formatted()) confirmées"
|
|
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
|
|
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
|
|
return TournamentStatus(label: label, completion: completionLabel)
|
|
}
|
|
|
|
func bracketStatus() async -> (status: String, description: String?, cut: TeamRegistration.TeamRange?) {
|
|
let availableSeeds = availableSeeds()
|
|
var description: String? = nil
|
|
if availableSeeds.isEmpty == false {
|
|
description = "placer \(availableSeeds.count) équipe\(availableSeeds.count.pluralSuffix)"
|
|
}
|
|
if description == nil {
|
|
let availableQualifiedTeams = availableQualifiedTeams()
|
|
if availableQualifiedTeams.isEmpty == false {
|
|
description = "placer \(availableQualifiedTeams.count) qualifié" + availableQualifiedTeams.count.pluralSuffix
|
|
}
|
|
}
|
|
|
|
var cut: TeamRegistration.TeamRange? = nil
|
|
if description == nil && isAnimation() == false {
|
|
cut = TeamRegistration.TeamRange(availableSeeds.first, availableSeeds.last)
|
|
}
|
|
|
|
if let round = getActiveRound() {
|
|
return ([round.roundTitle(.short), round.roundStatus()].joined(separator: " ").lowercased(), description, cut)
|
|
} else {
|
|
return ("", description, nil)
|
|
}
|
|
}
|
|
|
|
func groupStageStatus() async -> (status: String, cut: TeamRegistration.TeamRange?) {
|
|
let groupStageTeams = groupStageTeams()
|
|
let groupStageTeamsCount = groupStageTeams.count
|
|
if groupStageTeamsCount == 0 || groupStageTeamsCount != groupStageSpots() {
|
|
return ("à compléter", nil)
|
|
}
|
|
|
|
let cut : TeamRegistration.TeamRange? = isAnimation() ? nil : TeamRegistration.TeamRange(groupStageTeams.first, groupStageTeams.last)
|
|
|
|
let runningGroupStages = groupStages().filter({ $0.isRunning() })
|
|
if groupStagesAreOver() { return ("terminées", cut) }
|
|
if runningGroupStages.isEmpty {
|
|
|
|
let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false })
|
|
if ongoingGroupStages.isEmpty == false {
|
|
return ("Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut)
|
|
}
|
|
return (groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix, cut)
|
|
} else {
|
|
return ("Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut)
|
|
}
|
|
}
|
|
|
|
func settingsDescriptionLocalizedLabel() -> String {
|
|
[courtCount.formatted() + " terrain\(courtCount.pluralSuffix)", entryFeeMessage].joined(separator: ", ")
|
|
}
|
|
|
|
func structureDescriptionLocalizedLabel() -> String {
|
|
let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil
|
|
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
|
|
}
|
|
|
|
func deleteAndBuildEverything(preset: PadelTournamentStructurePreset = .manual) {
|
|
resetBracketPosition()
|
|
deleteStructure()
|
|
deleteGroupStages()
|
|
|
|
switch preset {
|
|
case .doubleGroupStage:
|
|
buildGroupStages()
|
|
addNewGroupStageStep()
|
|
qualifiedPerGroupStage = 0
|
|
groupStageAdditionalQualified = 0
|
|
default:
|
|
buildGroupStages()
|
|
buildBracket()
|
|
}
|
|
}
|
|
|
|
func addWildCardIfNeeded(_ count: Int, _ type: MatchType) {
|
|
let currentCount = selectedSortedTeams().filter({
|
|
if type == .bracket {
|
|
return $0.wildCardBracket
|
|
} else {
|
|
return $0.wildCardGroupStage
|
|
}
|
|
}).count
|
|
|
|
if currentCount < count {
|
|
let _diff = count - currentCount
|
|
addWildCard(_diff, type)
|
|
}
|
|
}
|
|
|
|
func addWildCard(_ count: Int, _ type: MatchType) {
|
|
let wcs = (0..<count).map { _ in
|
|
let team = TeamRegistration(tournament: id)
|
|
if type == .bracket {
|
|
team.wildCardBracket = true
|
|
} else {
|
|
team.wildCardGroupStage = true
|
|
}
|
|
|
|
return team
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: wcs)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func addEmptyTeamRegistration(_ count: Int) {
|
|
let teams = (0..<count).map { _ in
|
|
let team = TeamRegistration(tournament: id)
|
|
return team
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func buildGroupStages() {
|
|
guard groupStages().isEmpty else {
|
|
return
|
|
}
|
|
|
|
var _groupStages = [GroupStage]()
|
|
for index in 0..<groupStageCount {
|
|
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, matchFormat: groupStageSmartMatchFormat())
|
|
_groupStages.append(groupStage)
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.groupStages.addOrUpdate(contentOfs: _groupStages)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
refreshGroupStages()
|
|
}
|
|
|
|
func bracketTeamCount() -> Int {
|
|
let bracketTeamCount = teamCount - (teamsPerGroupStage - qualifiedPerGroupStage) * groupStageCount + (groupStageAdditionalQualified * (groupStageCount > 0 ? 1 : 0))
|
|
return bracketTeamCount
|
|
}
|
|
|
|
func buildBracket() {
|
|
guard rounds().isEmpty else { return }
|
|
let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount())
|
|
|
|
let rounds = (0..<roundCount).map { //index 0 is the final
|
|
return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
|
|
}
|
|
|
|
if rounds.isEmpty {
|
|
return
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount())
|
|
|
|
let matches = (0..<matchCount).map { //0 is final match
|
|
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
|
|
let round = rounds[roundIndex]
|
|
return Match(round: round.id, index: $0, matchFormat: round.matchFormat, name: Match.setServerTitle(upperRound: round, matchIndex: RoundRule.matchIndexWithinRound(fromMatchIndex: $0)))
|
|
}
|
|
|
|
print(matches.map {
|
|
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
|
|
})
|
|
|
|
do {
|
|
try self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
rounds.forEach { round in
|
|
round.buildLoserBracket()
|
|
}
|
|
}
|
|
|
|
func match(for bracketPosition: Int?) -> Match? {
|
|
guard let bracketPosition else { return nil }
|
|
let matchIndex = bracketPosition / 2
|
|
let roundIndex = RoundRule.roundIndex(fromMatchIndex: matchIndex)
|
|
if let round: Round = self.getRound(atRoundIndex: roundIndex) {
|
|
return self.tournamentStore.matches.first(where: { $0.round == round.id && $0.index == matchIndex })
|
|
// return Store.main.filter(isIncluded: { $0.round == round.id && $0.index == matchIndex }).first
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resetTeamScores(in matchOfBracketPosition: Int?, outsideOf: [TeamScore] = []) {
|
|
guard let match = match(for: matchOfBracketPosition) else { return }
|
|
match.resetTeamScores(outsideOf: outsideOf)
|
|
}
|
|
|
|
func updateTeamScores(in matchOfBracketPosition: Int?) {
|
|
guard let match = match(for: matchOfBracketPosition) else { return }
|
|
match.updateTeamScores()
|
|
}
|
|
|
|
func deleteStructure() {
|
|
do {
|
|
try self.tournamentStore.rounds.delete(contentOfs: rounds())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func resetBracketPosition() {
|
|
unsortedTeams().forEach({ $0.bracketPosition = nil })
|
|
}
|
|
|
|
func deleteGroupStages() {
|
|
do {
|
|
try self.tournamentStore.groupStages.delete(contentOfs: allGroupStages())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func refreshGroupStages(keepExistingMatches: Bool = false) {
|
|
unsortedTeams().forEach { team in
|
|
team.groupStage = nil
|
|
team.groupStagePosition = nil
|
|
}
|
|
|
|
if groupStageCount > 0 {
|
|
switch groupStageOrderingMode {
|
|
case .random:
|
|
setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
|
|
case .snake:
|
|
setGroupStage(randomize: false, keepExistingMatches: keepExistingMatches)
|
|
case .swiss:
|
|
setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setGroupStage(randomize: Bool, keepExistingMatches: Bool = false) {
|
|
let groupStages = groupStages()
|
|
let numberOfBracketsAsInt = groupStages.count
|
|
// let teamsPerBracket = teamsPerBracket
|
|
if groupStageCount != numberOfBracketsAsInt {
|
|
deleteGroupStages()
|
|
buildGroupStages()
|
|
} else {
|
|
setGroupStageTeams(randomize: randomize)
|
|
groupStages.forEach { $0.buildMatches(keepExistingMatches: keepExistingMatches) }
|
|
}
|
|
}
|
|
|
|
func setGroupStageTeams(randomize: Bool) {
|
|
let groupStages = groupStages()
|
|
let max = groupStages.map { $0.size }.reduce(0,+)
|
|
var chunks = selectedSortedTeams().suffix(max).chunked(into: groupStageCount)
|
|
for (index, _) in chunks.enumerated() {
|
|
if randomize {
|
|
chunks[index].shuffle()
|
|
} else if index % 2 != 0 {
|
|
chunks[index].reverse()
|
|
}
|
|
|
|
print("Equipes \(chunks[index].map { $0.weight })")
|
|
for (jIndex, _) in chunks[index].enumerated() {
|
|
print("Position \(index + 1) Poule \(groupStages[jIndex].index)")
|
|
chunks[index][jIndex].groupStage = groupStages[jIndex].id
|
|
chunks[index][jIndex].groupStagePosition = index
|
|
}
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func isFree() -> Bool {
|
|
return entryFee == nil || entryFee == 0
|
|
}
|
|
|
|
func indexOf(team: TeamRegistration) -> Int? {
|
|
return selectedSortedTeams().firstIndex(where: { $0.id == team.id })
|
|
}
|
|
|
|
func labelIndexOf(team: TeamRegistration) -> String? {
|
|
if let teamIndex = indexOf(team: team) {
|
|
return "Tête de série #" + (teamIndex + 1).formatted()
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
|
|
let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name)
|
|
team.setWeight(from: players.sorted(by: \.computedRank), inTournamentCategory: tournamentCategory)
|
|
players.forEach { player in
|
|
player.teamRegistration = team.id
|
|
}
|
|
if isAnimation() {
|
|
if team.weight == 0 {
|
|
team.weight = unsortedTeams().count
|
|
}
|
|
}
|
|
return team
|
|
}
|
|
|
|
var matchFormat: MatchFormat {
|
|
get {
|
|
roundFormat ?? .defaultFormatForMatchType(.bracket)
|
|
}
|
|
set {
|
|
roundFormat = newValue
|
|
}
|
|
}
|
|
|
|
var groupStageMatchFormat: MatchFormat {
|
|
get {
|
|
groupStageFormat ?? .defaultFormatForMatchType(.groupStage)
|
|
}
|
|
set {
|
|
groupStageFormat = newValue
|
|
}
|
|
}
|
|
|
|
var loserBracketMatchFormat: MatchFormat {
|
|
get {
|
|
loserRoundFormat ?? .defaultFormatForMatchType(.loserBracket)
|
|
}
|
|
set {
|
|
loserRoundFormat = newValue
|
|
}
|
|
}
|
|
|
|
var groupStageOrderingMode: GroupStageOrderingMode {
|
|
get {
|
|
groupStageSortMode
|
|
}
|
|
set {
|
|
groupStageSortMode = newValue
|
|
}
|
|
}
|
|
|
|
var tournamentCategory: TournamentCategory {
|
|
get {
|
|
federalCategory
|
|
}
|
|
set {
|
|
if federalCategory != newValue {
|
|
federalCategory = newValue
|
|
updateWeights()
|
|
} else {
|
|
federalCategory = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
var tournamentLevel: TournamentLevel {
|
|
get {
|
|
federalLevelCategory
|
|
}
|
|
set {
|
|
federalLevelCategory = newValue
|
|
teamSorting = newValue.defaultTeamSortingType
|
|
groupStageMatchFormat = groupStageSmartMatchFormat()
|
|
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
|
|
matchFormat = roundSmartMatchFormat(5)
|
|
}
|
|
}
|
|
|
|
var federalTournamentAge: FederalTournamentAge {
|
|
get {
|
|
federalAgeCategory
|
|
}
|
|
set {
|
|
federalAgeCategory = newValue
|
|
}
|
|
}
|
|
|
|
func loserBracketSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
|
|
let format = tournamentLevel.federalFormatForLoserBracketRound(roundIndex)
|
|
if tournamentLevel == .p25 { return .superTie }
|
|
if format.rank < loserBracketMatchFormat.rank {
|
|
return format
|
|
} else {
|
|
return loserBracketMatchFormat
|
|
}
|
|
}
|
|
|
|
func groupStageSmartMatchFormat() -> MatchFormat {
|
|
let format = tournamentLevel.federalFormatForGroupStage()
|
|
if tournamentLevel == .p25 { return .superTie }
|
|
if format.rank < groupStageMatchFormat.rank {
|
|
return format
|
|
} else {
|
|
return groupStageMatchFormat
|
|
}
|
|
}
|
|
|
|
func setupFederalSettings() {
|
|
teamSorting = tournamentLevel.defaultTeamSortingType
|
|
groupStageMatchFormat = groupStageSmartMatchFormat()
|
|
loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
|
|
matchFormat = roundSmartMatchFormat(5)
|
|
entryFee = tournamentLevel.entryFee
|
|
}
|
|
|
|
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
|
|
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
|
|
if tournamentLevel == .p25 { return .superTie }
|
|
if format.rank < matchFormat.rank {
|
|
return format
|
|
} else {
|
|
return matchFormat
|
|
}
|
|
}
|
|
|
|
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
|
|
switch teamSorting {
|
|
case .rank:
|
|
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
|
|
case .inscriptionDate:
|
|
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
|
|
}
|
|
}
|
|
|
|
func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
|
|
tournamentLevel == build.level
|
|
&& tournamentCategory == build.category
|
|
&& federalTournamentAge == build.age
|
|
}
|
|
|
|
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.id)]
|
|
|
|
private func _matchSchedulers() -> [MatchScheduler] {
|
|
return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id }
|
|
// DataStore.shared.matchSchedulers.filter(isIncluded: { $0.tournament == self.id })
|
|
}
|
|
|
|
func matchScheduler() -> MatchScheduler? {
|
|
return self._matchSchedulers().first
|
|
}
|
|
|
|
func courtsAvailable() -> [Int] {
|
|
(0..<courtCount).map { $0 }
|
|
}
|
|
|
|
func currentMonthData() -> MonthData? {
|
|
guard let rankSourceDate else { return nil }
|
|
let dateString = URL.importDateFormatter.string(from: rankSourceDate)
|
|
return DataStore.shared.monthData.first(where: { $0.monthKey == dateString })
|
|
}
|
|
|
|
var maleUnrankedValue: Int? {
|
|
return currentMonthData()?.maleUnrankedValue
|
|
}
|
|
|
|
var femaleUnrankedValue: Int? {
|
|
return currentMonthData()?.femaleUnrankedValue
|
|
}
|
|
|
|
func courtNameIfAvailable(atIndex courtIndex: Int) -> String? {
|
|
return club()?.customizedCourts.first(where: { $0.index == courtIndex })?.name
|
|
}
|
|
|
|
func courtName(atIndex courtIndex: Int) -> String {
|
|
return courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex)
|
|
}
|
|
|
|
func tournamentWinner() -> TeamRegistration? {
|
|
let finals: Round? = self.tournamentStore.rounds.first(where: { $0.index == 0 && $0.isUpperBracket() })
|
|
return finals?.playedMatches().first?.winner()
|
|
}
|
|
|
|
func getGroupStageChunkValue() -> Int {
|
|
if groupStageCount > 0 && teamsPerGroupStage >= 2 {
|
|
let result = courtCount / (teamsPerGroupStage / 2)
|
|
let remainder = courtCount % (teamsPerGroupStage / 2)
|
|
let value = remainder == 0 ? result : result + 1
|
|
return min(groupStageCount, value)
|
|
} else {
|
|
return 1
|
|
}
|
|
}
|
|
|
|
func replacementRangeExtended(groupStagePosition: Int) -> TeamRegistration.TeamRange? {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
var left: TeamRegistration? = nil
|
|
if groupStagePosition == 0 {
|
|
left = seeds().last
|
|
} else {
|
|
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight)
|
|
left = previousHat.last
|
|
}
|
|
var right: TeamRegistration? = nil
|
|
if groupStagePosition == teamsPerGroupStage - 1 {
|
|
right = nil
|
|
} else {
|
|
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition + 1 }).sorted(by: \.weight)
|
|
right = previousHat.first
|
|
}
|
|
return (left: left, right: right)
|
|
}
|
|
|
|
|
|
typealias TeamPlacementIssue = (shouldBeInIt: [String], shouldNotBeInIt: [String])
|
|
func groupStageTeamPlacementIssue() -> TeamPlacementIssue {
|
|
let selected = selectedSortedTeams()
|
|
let allTeams = unsortedTeams()
|
|
let newGroup = selected.suffix(groupStageSpots())
|
|
let currentGroup = allTeams.filter({ $0.groupStagePosition != nil })
|
|
let selectedIds = newGroup.map { $0.id }
|
|
let groupIds = currentGroup.map { $0.id }
|
|
let shouldBeInIt = Set(selectedIds).subtracting(groupIds)
|
|
let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds)
|
|
return (Array(shouldBeInIt), Array(shouldNotBeInIt))
|
|
}
|
|
|
|
func bracketTeamPlacementIssue() -> TeamPlacementIssue {
|
|
let selected = selectedSortedTeams()
|
|
let allTeams = unsortedTeams()
|
|
let seedCount = max(selected.count - groupStageSpots(), 0)
|
|
let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified })
|
|
let currentGroup = allTeams.filter({ $0.bracketPosition != nil })
|
|
let selectedIds = newGroup.map { $0.id }
|
|
let groupStageTeamsInBracket = selected.filter({ $0.qualified == false && $0.inGroupStage() && $0.inRound() })
|
|
let groupIds = currentGroup.map { $0.id } + groupStageTeamsInBracket.map { $0.id }
|
|
let shouldBeInIt = Set(selectedIds).subtracting(groupIds)
|
|
let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds)
|
|
return (Array(shouldBeInIt), Array(shouldNotBeInIt))
|
|
}
|
|
|
|
func groupStageLoserBracket() -> Round? {
|
|
tournamentStore.rounds.first(where: { $0.groupStageLoserBracket })
|
|
}
|
|
|
|
func groupStageLoserBracketsInitialPlace() -> Int {
|
|
return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1
|
|
}
|
|
|
|
func addNewGroupStageStep() {
|
|
let lastStep = lastStep() + 1
|
|
for i in 0..<teamsPerGroupStage {
|
|
let gs = GroupStage(tournament: id, index: i, size: groupStageCount, step: lastStep)
|
|
do {
|
|
try tournamentStore.groupStages.addOrUpdate(instance: gs)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
groupStages(atStep: 1).forEach { $0.buildMatches() }
|
|
}
|
|
|
|
func lastStep() -> Int {
|
|
self.tournamentStore.groupStages.sorted(by: \.step).last?.step ?? 0
|
|
}
|
|
|
|
func generateSmartLoserGroupStageBracket() {
|
|
guard let groupStageLoserBracket = groupStageLoserBracket() else { return }
|
|
for i in qualifiedPerGroupStage..<teamsPerGroupStage {
|
|
groupStages().chunked(into: 2).forEach { gss in
|
|
let placeCount = i * 2 + 1
|
|
let match = Match(round: groupStageLoserBracket.id, index: placeCount, matchFormat: groupStageLoserBracket.matchFormat)
|
|
match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place")
|
|
do {
|
|
try tournamentStore.matches.addOrUpdate(instance: match)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] {
|
|
print("rang \(i)")
|
|
print(score1.teamLabel(.short), "vs", score2.teamLabel(.short))
|
|
|
|
match.setLuckyLoser(team: score1, teamPosition: .one)
|
|
match.setLuckyLoser(team: score2, teamPosition: .two)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func updateTournamentState() {
|
|
Task {
|
|
if hasEnded() {
|
|
let fr = await finalRanking()
|
|
_ = await setRankings(finalRanks: fr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func allLoserRoundMatches() -> [Match] {
|
|
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()
|
|
}
|
|
|
|
|
|
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
|
|
guard let source = eventObject()?.courtsUnavailability else { return false }
|
|
let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex })
|
|
return courtLockedSchedule.anySatisfy({ dateInterval in
|
|
let range = startDate..<endDate
|
|
return dateInterval.range.overlaps(range)
|
|
})
|
|
}
|
|
|
|
func getOnlineRegistrationStatus() -> OnlineRegistrationStatus {
|
|
if hasStarted() {
|
|
return .inProgress
|
|
}
|
|
if closedRegistrationDate != nil {
|
|
return .ended
|
|
}
|
|
if endDate != nil {
|
|
return .endedWithResults
|
|
}
|
|
|
|
let now = Date()
|
|
|
|
if let openingRegistrationDate = openingRegistrationDate {
|
|
let timezonedDateTime = openingRegistrationDate // Assuming dates are already in local timezone
|
|
if now < timezonedDateTime {
|
|
return .notStarted
|
|
}
|
|
}
|
|
|
|
if let registrationDateLimit = registrationDateLimit {
|
|
let timezonedDateTime = registrationDateLimit // Assuming dates are already in local timezone
|
|
if now > timezonedDateTime {
|
|
return .ended
|
|
}
|
|
}
|
|
|
|
if let targetTeamCount = targetTeamCount {
|
|
// Get all team registrations excluding walk_outs
|
|
let currentTeamCount = unsortedTeamsWithoutWO().count
|
|
|
|
if currentTeamCount >= targetTeamCount {
|
|
if let waitingListLimit = waitingListLimit {
|
|
let waitingListCount = currentTeamCount - targetTeamCount
|
|
if waitingListCount >= waitingListLimit {
|
|
return .waitingListFull
|
|
}
|
|
}
|
|
return .waitingListPossible
|
|
}
|
|
}
|
|
|
|
return .open
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
func insertOnServer() throws {
|
|
|
|
DataStore.shared.tournaments.writeChangeAndInsertOnServer(instance: self)
|
|
|
|
for teamRegistration in self.tournamentStore.teamRegistrations {
|
|
teamRegistration.insertOnServer()
|
|
}
|
|
for groupStage in self.tournamentStore.groupStages {
|
|
groupStage.insertOnServer()
|
|
}
|
|
for round in self.tournamentStore.rounds {
|
|
round.insertOnServer()
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Payments & Crypto
|
|
|
|
enum PaymentError: Error {
|
|
case cantPayTournament
|
|
}
|
|
|
|
func payIfNecessary() throws {
|
|
if self.payment != nil { return }
|
|
if let payment = Guard.main.paymentForNewTournament() {
|
|
self.payment = payment
|
|
try DataStore.shared.tournaments.addOrUpdate(instance: self)
|
|
return
|
|
}
|
|
throw PaymentError.cantPayTournament
|
|
}
|
|
|
|
}
|
|
|
|
fileprivate extension Bool {
|
|
var encodedValue: Int {
|
|
switch self {
|
|
case true:
|
|
return Int.random(in: (0...4))
|
|
case false:
|
|
return Int.random(in: (5...9))
|
|
}
|
|
}
|
|
static func decodeInt(_ int: Int) -> Bool {
|
|
switch int {
|
|
case (0...4):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
//extension Tournament {
|
|
// enum CodingKeys: String, CodingKey {
|
|
// case _id = "id"
|
|
// case _event = "event"
|
|
// case _name = "name"
|
|
// case _startDate = "startDate"
|
|
// case _endDate = "endDate"
|
|
// case _creationDate = "creationDate"
|
|
// case _isPrivate = "isPrivate"
|
|
// case _groupStageFormat = "groupStageFormat"
|
|
// case _roundFormat = "roundFormat"
|
|
// case _loserRoundFormat = "loserRoundFormat"
|
|
// case _groupStageSortMode = "groupStageSortMode"
|
|
// case _groupStageCount = "groupStageCount"
|
|
// case _rankSourceDate = "rankSourceDate"
|
|
// case _dayDuration = "dayDuration"
|
|
// case _teamCount = "teamCount"
|
|
// case _teamSorting = "teamSorting"
|
|
// case _federalCategory = "federalCategory"
|
|
// case _federalLevelCategory = "federalLevelCategory"
|
|
// case _federalAgeCategory = "federalAgeCategory"
|
|
// case _groupStageCourtCount = "groupStageCourtCount"
|
|
// case _closedRegistrationDate = "closedRegistrationDate"
|
|
// case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
|
|
// case _courtCount = "courtCount"
|
|
// case _prioritizeClubMembers = "prioritizeClubMembers"
|
|
// case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
|
|
// case _teamsPerGroupStage = "teamsPerGroupStage"
|
|
// case _entryFee = "entryFee"
|
|
// case _additionalEstimationDuration = "additionalEstimationDuration"
|
|
// case _isDeleted = "isDeleted"
|
|
// case _isCanceled = "localId"
|
|
// case _payment = "globalId"
|
|
// }
|
|
//}
|
|
|
|
extension Tournament: Hashable {
|
|
static func == (lhs: Tournament, rhs: Tournament) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|
|
|
|
extension Tournament: FederalTournamentHolder {
|
|
|
|
func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String {
|
|
if isAnimation() {
|
|
if let name {
|
|
return name.trunc(length: DeviceHelper.charLength())
|
|
} else if build.age == .unlisted, build.category == .unlisted {
|
|
return build.level.localizedLevelLabel(.title)
|
|
} else {
|
|
return build.level.localizedLevelLabel(displayStyle)
|
|
}
|
|
}
|
|
return build.level.localizedLevelLabel(displayStyle)
|
|
}
|
|
|
|
var codeClub: String? {
|
|
club()?.code
|
|
}
|
|
|
|
var holderId: String { id }
|
|
|
|
func clubLabel() -> String {
|
|
locationLabel()
|
|
}
|
|
|
|
func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String {
|
|
if isAnimation() {
|
|
if displayAgeAndCategory(forBuild: build) == false {
|
|
return [build.category.localizedLabel(), build.age.localizedLabel()].filter({ $0.isEmpty == false }).joined(separator: " ")
|
|
} else if name != nil {
|
|
return build.level.localizedLevelLabel(.title)
|
|
} else {
|
|
return ""
|
|
}
|
|
} else {
|
|
return subtitle()
|
|
}
|
|
}
|
|
|
|
var tournaments: [any TournamentBuildHolder] {
|
|
[
|
|
self
|
|
]
|
|
}
|
|
|
|
var dayPeriod: DayPeriod {
|
|
let day = startDate.get(.weekday)
|
|
switch day {
|
|
case 2...6:
|
|
return .week
|
|
default:
|
|
return .weekend
|
|
}
|
|
}
|
|
|
|
func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool {
|
|
if isAnimation() {
|
|
if let name, name.count < DeviceHelper.maxCharacter() {
|
|
return true
|
|
} else if build.age == .unlisted, build.category == .unlisted {
|
|
return true
|
|
} else {
|
|
return DeviceHelper.isBigScreen()
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
extension Tournament: TournamentBuildHolder {
|
|
func buildHolderTitle(_ displayStyle: DisplayStyle) -> String {
|
|
tournamentTitle(.short)
|
|
}
|
|
|
|
var category: TournamentCategory {
|
|
tournamentCategory
|
|
}
|
|
|
|
var level: TournamentLevel {
|
|
tournamentLevel
|
|
}
|
|
|
|
var age: FederalTournamentAge {
|
|
federalTournamentAge
|
|
}
|
|
}
|
|
|
|
extension Tournament {
|
|
static func newEmptyInstance() -> Tournament {
|
|
let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource
|
|
var _mostRecentDateAvailable: Date? {
|
|
guard let lastDataSource else { return nil }
|
|
return URL.importDateFormatter.date(from: lastDataSource)
|
|
}
|
|
|
|
let rankSourceDate = _mostRecentDateAvailable
|
|
let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false }
|
|
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
|
|
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
|
|
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
|
|
//creator: DataStore.shared.user?.id
|
|
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode)
|
|
}
|
|
|
|
static func fake() -> Tournament {
|
|
return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil)
|
|
}
|
|
|
|
}
|
|
|
|
extension Tournament {
|
|
func deadline(for type: TournamentDeadlineType) -> Date? {
|
|
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
|
|
|
|
if let date = Calendar.current.date(byAdding: .day, value: type.daysOffset, to: startDate) {
|
|
let startOfDay = Calendar.current.startOfDay(for: date)
|
|
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|