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.
482 lines
16 KiB
482 lines
16 KiB
//
|
|
// TeamRegistration.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by razmig on 10/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
|
|
@Observable
|
|
class TeamRegistration: ModelObject, Storable {
|
|
static func resourceName() -> String { "team-registrations" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
|
|
var id: String = Store.randomId()
|
|
var tournament: String
|
|
var groupStage: String?
|
|
var registrationDate: Date?
|
|
var callDate: Date?
|
|
var bracketPosition: Int?
|
|
var groupStagePosition: Int?
|
|
var comment: String?
|
|
var source: String?
|
|
var sourceValue: String?
|
|
var logo: String?
|
|
var name: String?
|
|
|
|
var walkOut: Bool = false
|
|
var wildCardBracket: Bool = false
|
|
var wildCardGroupStage: Bool = false
|
|
var weight: Int = 0
|
|
var lockedWeight: Int?
|
|
var confirmationDate: Date?
|
|
var qualified: Bool = false
|
|
var finalRanking: Int?
|
|
var pointsEarned: Int?
|
|
|
|
init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) {
|
|
self.tournament = tournament
|
|
self.groupStage = groupStage
|
|
self.registrationDate = registrationDate
|
|
self.callDate = callDate
|
|
self.bracketPosition = bracketPosition
|
|
self.groupStagePosition = groupStagePosition
|
|
self.comment = comment
|
|
self.source = source
|
|
self.sourceValue = sourceValue
|
|
self.logo = logo
|
|
self.name = name
|
|
self.walkOut = walkOut
|
|
self.wildCardBracket = wildCardBracket
|
|
self.wildCardGroupStage = wildCardGroupStage
|
|
self.weight = weight
|
|
self.lockedWeight = lockedWeight
|
|
self.confirmationDate = confirmationDate
|
|
self.qualified = qualified
|
|
}
|
|
|
|
func isSeedable() -> Bool {
|
|
bracketPosition == nil && groupStage == nil
|
|
}
|
|
|
|
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
|
|
let seedPosition = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding)
|
|
tournamentObject()?.resetTeamScores(in: bracketPosition)
|
|
bracketPosition = seedPosition
|
|
tournamentObject()?.updateTeamScores(in: bracketPosition)
|
|
}
|
|
|
|
func expectedSummonDate() -> Date? {
|
|
if let groupStageStartDate = groupStageObject()?.startDate {
|
|
return groupStageStartDate
|
|
} else if let roundMatchStartDate = initialMatch()?.startDate {
|
|
return roundMatchStartDate
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var initialWeight: Int {
|
|
lockedWeight ?? weight
|
|
}
|
|
|
|
func called() -> Bool {
|
|
callDate != nil
|
|
}
|
|
|
|
func confirmed() -> Bool {
|
|
confirmationDate != nil
|
|
}
|
|
|
|
func getPhoneNumbers() -> [String] {
|
|
return players().compactMap { $0.phoneNumber }.filter({ $0.isMobileNumber() })
|
|
}
|
|
|
|
func getMail() -> [String] {
|
|
let mails = players().compactMap({ $0.email })
|
|
return mails
|
|
}
|
|
|
|
func isImported() -> Bool {
|
|
unsortedPlayers().allSatisfy({ $0.isImported() })
|
|
}
|
|
|
|
func isWildCard() -> Bool {
|
|
wildCardBracket || wildCardGroupStage
|
|
}
|
|
|
|
func isPlaying() -> Bool {
|
|
currentMatch() != nil
|
|
}
|
|
|
|
func currentMatch() -> Match? {
|
|
teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
|
|
}
|
|
|
|
func teamScores() -> [TeamScore] {
|
|
Store.main.filter(isIncluded: { $0.teamRegistration == id })
|
|
}
|
|
|
|
var tournamentCategory: TournamentCategory {
|
|
tournamentObject()?.tournamentCategory ?? .men
|
|
}
|
|
|
|
@objc
|
|
var canonicalName: String {
|
|
players().map { $0.canonicalName }.joined(separator: " ")
|
|
}
|
|
|
|
func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
|
|
guard let codeClubOrClubName else { return true }
|
|
return unsortedPlayers().anySatisfy({
|
|
$0.clubName?.contains(codeClubOrClubName) == true || $0.clubName?.contains(codeClubOrClubName) == true
|
|
})
|
|
}
|
|
|
|
func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) {
|
|
setWeight(from: self.players(), inTournamentCategory: tournamentCategory)
|
|
}
|
|
|
|
func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String {
|
|
players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ")
|
|
}
|
|
|
|
func index(in teams: [TeamRegistration]) -> Int? {
|
|
teams.firstIndex(where: { $0.id == id })
|
|
}
|
|
|
|
func formattedSeed(in teams: [TeamRegistration]) -> String {
|
|
if let index = index(in: teams) {
|
|
return "#\(index + 1)"
|
|
} else {
|
|
return "###"
|
|
}
|
|
}
|
|
|
|
func contains(_ searchField: String) -> Bool {
|
|
unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true
|
|
}
|
|
|
|
func includes(_ players: [PlayerRegistration]) -> Bool {
|
|
players.allSatisfy { player in
|
|
includes(player)
|
|
}
|
|
}
|
|
|
|
func includes(_ player: PlayerRegistration) -> Bool {
|
|
unsortedPlayers().anySatisfy { _player in
|
|
_player.isSameAs(player)
|
|
}
|
|
}
|
|
|
|
func canPlay() -> Bool {
|
|
teamScores().isEmpty == false || players().allSatisfy({ $0.hasPaid() || $0.hasArrived })
|
|
}
|
|
|
|
func availableForSeedPick() -> Bool {
|
|
groupStage == nil && bracketPosition == nil
|
|
}
|
|
|
|
func inGroupStage() -> Bool {
|
|
groupStagePosition != nil
|
|
}
|
|
|
|
func inRound() -> Bool {
|
|
bracketPosition != nil
|
|
}
|
|
|
|
func resetPositions() {
|
|
groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
|
|
groupStage = nil
|
|
groupStagePosition = nil
|
|
tournamentObject()?.resetTeamScores(in: bracketPosition)
|
|
bracketPosition = nil
|
|
}
|
|
|
|
func pasteData() -> String {
|
|
[name, playersPasteData(), formattedInscriptionDate()].compactMap({ $0 }).joined(separator: "\n")
|
|
}
|
|
|
|
var computedRegistrationDate: Date {
|
|
registrationDate ?? .distantFuture
|
|
}
|
|
|
|
func formattedInscriptionDate() -> String? {
|
|
if let registrationDate {
|
|
return "Inscrit le " + registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func playersPasteData() -> String {
|
|
players().map { $0.pasteData() }.joined(separator: "\n")
|
|
}
|
|
|
|
|
|
func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) {
|
|
do {
|
|
try DataStore.shared.playerRegistrations.delete(contentOfs: unsortedPlayers())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
|
|
|
|
players.forEach { player in
|
|
player.teamRegistration = id
|
|
}
|
|
}
|
|
|
|
func qualifiedFromGroupStage() -> Bool {
|
|
groupStagePosition != nil && bracketPosition != nil
|
|
}
|
|
|
|
typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)
|
|
|
|
func replacementRange() -> TeamRange? {
|
|
guard let tournamentObject = tournamentObject() else { return nil }
|
|
guard let index = tournamentObject.indexOf(team: self) else { return nil }
|
|
let selectedSortedTeams = tournamentObject.selectedSortedTeams()
|
|
let left = selectedSortedTeams[safe: index - 1]
|
|
let right = selectedSortedTeams[safe: index + 1]
|
|
return (left: left, right: right)
|
|
}
|
|
|
|
func replacementRangeExtended() -> TeamRange? {
|
|
guard let tournamentObject = tournamentObject() else { return nil }
|
|
guard let groupStagePosition else { return nil }
|
|
let selectedSortedTeams = tournamentObject.selectedSortedTeams()
|
|
var left: TeamRegistration? = nil
|
|
if groupStagePosition == 0 {
|
|
left = tournamentObject.seeds().last
|
|
} else {
|
|
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight)
|
|
left = previousHat.last
|
|
}
|
|
var right: TeamRegistration? = nil
|
|
if groupStagePosition == tournamentObject.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 AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool
|
|
|
|
func players() -> [PlayerRegistration] {
|
|
Store.main.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in
|
|
let predicates: [AreInIncreasingOrder] = [
|
|
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
|
|
{ $0.rank ?? 0 < $1.rank ?? 0 },
|
|
{ $0.lastName < $1.lastName},
|
|
{ $0.firstName < $1.firstName }
|
|
]
|
|
|
|
for predicate in predicates {
|
|
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
|
|
continue
|
|
}
|
|
|
|
return predicate(lhs, rhs)
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
func unsortedPlayers() -> [PlayerRegistration] {
|
|
Store.main.filter { $0.teamRegistration == self.id }
|
|
}
|
|
|
|
func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) {
|
|
let significantPlayerCount = significantPlayerCount()
|
|
weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
|
|
}
|
|
|
|
func significantPlayerCount() -> Int {
|
|
tournamentObject()?.significantPlayerCount() ?? 2
|
|
}
|
|
|
|
func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] {
|
|
let players = unsortedPlayers()
|
|
if players.count >= 2 { return [] }
|
|
let s = players.compactMap { $0.sex?.rawValue }
|
|
var missing = tournamentCategory.mandatoryPlayerType()
|
|
s.forEach { i in
|
|
if let index = missing.firstIndex(of: i) {
|
|
missing.remove(at: index)
|
|
}
|
|
}
|
|
return missing
|
|
}
|
|
|
|
func unrankValue(for malePlayer: Bool) -> Int {
|
|
tournamentObject()?.unrankValue(for: malePlayer) ?? 100_000
|
|
}
|
|
|
|
func groupStageObject() -> GroupStage? {
|
|
guard let groupStage else { return nil }
|
|
return Store.main.findById(groupStage)
|
|
}
|
|
|
|
func initialRound() -> Round? {
|
|
guard let bracketPosition else { return nil }
|
|
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
|
|
return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == roundIndex }).first
|
|
}
|
|
|
|
func initialMatch() -> Match? {
|
|
guard let bracketPosition else { return nil }
|
|
guard let initialRoundObject = initialRound() else { return nil }
|
|
return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }).first
|
|
}
|
|
|
|
|
|
func tournamentObject() -> Tournament? {
|
|
Store.main.findById(tournament)
|
|
}
|
|
|
|
override func deleteDependencies() throws {
|
|
try Store.main.deleteDependencies(items: self.unsortedPlayers())
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case _id = "id"
|
|
case _tournament = "tournament"
|
|
case _groupStage = "groupStage"
|
|
case _registrationDate = "registrationDate"
|
|
case _callDate = "callDate"
|
|
case _bracketPosition = "bracketPosition"
|
|
case _groupStagePosition = "groupStagePosition"
|
|
case _comment = "comment"
|
|
case _source = "source"
|
|
case _sourceValue = "sourceValue"
|
|
case _logo = "logo"
|
|
case _name = "name"
|
|
case _wildCardBracket = "wildCardBracket"
|
|
case _wildCardGroupStage = "wildCardGroupStage"
|
|
case _weight = "weight"
|
|
case _walkOut = "walkOut"
|
|
case _lockedWeight = "lockedWeight"
|
|
case _confirmationDate = "confirmationDate"
|
|
case _qualified = "qualified"
|
|
case _finalRanking = "finalRanking"
|
|
case _pointsEarned = "pointsEarned"
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(id, forKey: ._id)
|
|
try container.encode(tournament, forKey: ._tournament)
|
|
|
|
if let groupStage = groupStage {
|
|
try container.encode(groupStage, forKey: ._groupStage)
|
|
} else {
|
|
try container.encodeNil(forKey: ._groupStage)
|
|
}
|
|
|
|
if let registrationDate = registrationDate {
|
|
try container.encode(registrationDate, forKey: ._registrationDate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._registrationDate)
|
|
}
|
|
|
|
if let callDate = callDate {
|
|
try container.encode(callDate, forKey: ._callDate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._callDate)
|
|
}
|
|
|
|
if let bracketPosition = bracketPosition {
|
|
try container.encode(bracketPosition, forKey: ._bracketPosition)
|
|
} else {
|
|
try container.encodeNil(forKey: ._bracketPosition)
|
|
}
|
|
|
|
if let groupStagePosition = groupStagePosition {
|
|
try container.encode(groupStagePosition, forKey: ._groupStagePosition)
|
|
} else {
|
|
try container.encodeNil(forKey: ._groupStagePosition)
|
|
}
|
|
|
|
if let comment = comment {
|
|
try container.encode(comment, forKey: ._comment)
|
|
} else {
|
|
try container.encodeNil(forKey: ._comment)
|
|
}
|
|
|
|
if let source = source {
|
|
try container.encode(source, forKey: ._source)
|
|
} else {
|
|
try container.encodeNil(forKey: ._source)
|
|
}
|
|
|
|
if let sourceValue = sourceValue {
|
|
try container.encode(sourceValue, forKey: ._sourceValue)
|
|
} else {
|
|
try container.encodeNil(forKey: ._sourceValue)
|
|
}
|
|
|
|
if let logo = logo {
|
|
try container.encode(logo, forKey: ._logo)
|
|
} else {
|
|
try container.encodeNil(forKey: ._logo)
|
|
}
|
|
|
|
if let name = name {
|
|
try container.encode(name, forKey: ._name)
|
|
} else {
|
|
try container.encodeNil(forKey: ._name)
|
|
}
|
|
|
|
try container.encode(walkOut, forKey: ._walkOut)
|
|
try container.encode(wildCardBracket, forKey: ._wildCardBracket)
|
|
try container.encode(wildCardGroupStage, forKey: ._wildCardGroupStage)
|
|
try container.encode(weight, forKey: ._weight)
|
|
|
|
if let lockedWeight = lockedWeight {
|
|
try container.encode(lockedWeight, forKey: ._lockedWeight)
|
|
} else {
|
|
try container.encodeNil(forKey: ._lockedWeight)
|
|
}
|
|
|
|
if let confirmationDate = confirmationDate {
|
|
try container.encode(confirmationDate, forKey: ._confirmationDate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._confirmationDate)
|
|
}
|
|
|
|
try container.encode(qualified, forKey: ._qualified)
|
|
|
|
if let finalRanking {
|
|
try container.encode(finalRanking, forKey: ._finalRanking)
|
|
} else {
|
|
try container.encodeNil(forKey: ._finalRanking)
|
|
}
|
|
|
|
if let pointsEarned {
|
|
try container.encode(pointsEarned, forKey: ._pointsEarned)
|
|
} else {
|
|
try container.encodeNil(forKey: ._pointsEarned)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension TeamRegistration: Hashable {
|
|
static func == (lhs: TeamRegistration, rhs: TeamRegistration) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|
|
|
|
enum TeamDataSource: Int, Codable {
|
|
case beachPadel
|
|
}
|
|
|