// // 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 } // MARK: - Computed dependencies func unsortedPlayers() -> [PlayerRegistration] { Store.main.filter { $0.teamRegistration == self.id } } // MARK: - override func deleteDependencies() throws { DataStore.shared.playerRegistrations.deleteDependencies(self.unsortedPlayers()) } func hasArrived() { let unsortedPlayers = unsortedPlayers() unsortedPlayers.forEach({ $0.hasArrived = true }) do { try DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) } catch { Logger.error(error) } } 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 Store.main.filter(isIncluded: { $0.teamRegistration == id }) } func wins() -> [Match] { return Store.main.filter(isIncluded: { $0.winningTeamId == id }) } func loses() -> [Match] { return Store.main.filter(isIncluded: { $0.losingTeamId == id }) } func matches() -> [Match] { return Store.main.filter(isIncluded: { $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 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 = Set(arrayOfIds.sorted()) let searchedIds = Set(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted()) return ids.hashValue == searchedIds.hashValue } func includes(_ players: [PlayerRegistration]) -> Bool { return players.allSatisfy { player in includes(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, inTournamentCategory tournamentCategory: TournamentCategory) { let previousPlayers = Set(unsortedPlayers()) let playersToRemove = previousPlayers.subtracting(players) do { try DataStore.shared.playerRegistrations.delete(contentOfs: playersToRemove) } 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 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 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? { 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() throws { try DataStore.shared.teamRegistrations.writeChangeAndInsertOnServer(instance: self) for playerRegistration in self.unsortedPlayers() { try 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 }