You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Data/TeamRegistration.swift

569 lines
20 KiB

//
// TeamRegistration.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
@Observable
final class TeamRegistration: ModelObject, Storable {
static func resourceName() -> String { "team-registrations" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
static var relationshipNames: [String] = []
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
}
var tournamentStore: TournamentStore {
return TournamentStore.instance(tournamentId: self.tournament)
}
// MARK: - Computed dependencies
func unsortedPlayers() -> [PlayerRegistration] {
return self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }
}
// MARK: -
override func deleteDependencies() throws {
let unsortedPlayers = unsortedPlayers()
for player in unsortedPlayers {
try player.deleteDependencies()
}
self.tournamentStore.playerRegistrations.deleteDependencies(unsortedPlayers)
let teamScores = teamScores()
for teamScore in teamScores {
try teamScore.deleteDependencies()
}
self.tournamentStore.teamScores.deleteDependencies(teamScores)
}
func hasArrived(isHere: Bool = false) {
let unsortedPlayers = unsortedPlayers()
unsortedPlayers.forEach({ $0.hasArrived = !isHere })
do {
try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
} catch {
Logger.error(error)
}
}
func isHere() -> Bool {
let unsortedPlayers = unsortedPlayers()
return unsortedPlayers.allSatisfy({ $0.hasArrived })
}
func isSeedable() -> Bool {
bracketPosition == nil && groupStage == nil
}
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding)
tournamentObject()?.resetTeamScores(in: bracketPosition)
self.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 {
return lockedWeight ?? weight
}
func called() -> Bool {
return callDate != nil
}
func confirmed() -> Bool {
return 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 {
return unsortedPlayers().allSatisfy({ $0.isImported() })
}
func isWildCard() -> Bool {
return wildCardBracket || wildCardGroupStage
}
func isPlaying() -> Bool {
return currentMatch() != nil
}
func currentMatch() -> Match? {
return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
}
func teamScores() -> [TeamScore] {
return self.tournamentStore.teamScores.filter({ $0.teamRegistration == id })
}
func wins() -> [Match] {
return self.tournamentStore.matches.filter({ $0.winningTeamId == id })
}
func loses() -> [Match] {
return self.tournamentStore.matches.filter({ $0.losingTeamId == id })
}
func matches() -> [Match] {
return self.tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == 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) {
self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory)
}
func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String {
return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ")
}
func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String {
[displayTeamName ? name : nil, displayRank ? seedIndex() : nil, displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel()].compactMap({ $0 }).joined(separator: " ")
}
func seedIndex() -> String? {
guard let tournament = tournamentObject() else { return nil }
guard let index = index(in: tournament.selectedSortedTeams()) else { return nil }
return "(\(index + 1))"
}
func index(in teams: [TeamRegistration]) -> Int? {
return 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 {
return unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true
}
func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool {
let arrayOfIds : [String] = unsortedPlayers().compactMap({ $0.licenceId?.strippedLicense?.canonicalVersion })
let ids : Set<String> = Set<String>(arrayOfIds.sorted())
let searchedIds = Set<String>(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted())
return ids.hashValue == searchedIds.hashValue
}
func includes(players: [PlayerRegistration]) -> Bool {
let unsortedPlayers = unsortedPlayers()
guard players.count == unsortedPlayers.count else { return false }
return players.allSatisfy { player in
unsortedPlayers.anySatisfy { _player in
_player.isSameAs(player)
}
}
}
func includes(player: PlayerRegistration) -> Bool {
return unsortedPlayers().anySatisfy { _player in
_player.isSameAs(player)
}
}
func canPlay() -> Bool {
return matches().isEmpty == false || players().allSatisfy({ $0.hasPaid() || $0.hasArrived })
}
func availableForSeedPick() -> Bool {
return groupStage == nil && bracketPosition == nil
}
func inGroupStage() -> Bool {
return groupStagePosition != nil
}
func inRound() -> Bool {
return bracketPosition != nil
}
func resetGroupeStagePosition() {
groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
groupStage = nil
groupStagePosition = nil
}
func resetBracketPosition() {
guard let bracketPosition else { return }
guard let tournamentObject = tournamentObject() else { return }
if let match = tournamentObject.match(for: bracketPosition) {
let teamScores = match.teamScores.filter({ $0.teamRegistration != self.id })
tournamentObject.resetTeamScores(in: bracketPosition, outsideOf: teamScores)
}
self.bracketPosition = nil
}
func resetPositions() {
resetGroupeStagePosition()
resetBracketPosition()
}
func pasteData() -> String {
[playersPasteData(), formattedInscriptionDate(), name].compactMap({ $0 }).joined(separator: "\n")
}
var computedRegistrationDate: Date {
return 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 {
return players().map { $0.pasteData() }.joined(separator: "\n")
}
func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) {
let previousPlayers = Set(unsortedPlayers())
let playersToRemove = previousPlayers.subtracting(players)
do {
try self.tournamentStore.playerRegistrations.delete(contentOfs: playersToRemove)
} catch {
Logger.error(error)
}
setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in
player.teamRegistration = id
}
}
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] {
self.tournamentStore.playerRegistrations.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 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 {
return 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 {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 100_000
}
func groupStageObject() -> GroupStage? {
guard let groupStage else { return nil }
return self.tournamentStore.groupStages.findById(groupStage)
}
func initialRound() -> Round? {
guard let bracketPosition else { return nil }
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
return self.tournamentStore.rounds.first(where: { $0.index == roundIndex })
}
func initialMatch() -> Match? {
guard let bracketPosition else { return nil }
guard let initialRoundObject = initialRound() else { return nil }
return self.tournamentStore.matches.first(where: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 })
}
func tournamentObject() -> Tournament? {
return Store.main.findById(tournament)
}
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)
}
}
func insertOnServer() {
self.tournamentStore.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
for playerRegistration in self.unsortedPlayers() {
playerRegistration.insertOnServer()
}
}
}
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
}