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.
458 lines
17 KiB
458 lines
17 KiB
//
|
|
// Tournament+Extensions.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Laurent Morvillier on 15/04/2025.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import PadelClubData
|
|
import LeStorage
|
|
|
|
extension Tournament {
|
|
|
|
func setupFederalSettings() {
|
|
teamSorting = tournamentLevel.defaultTeamSortingType
|
|
groupStageMatchFormat = groupStageSmartMatchFormat()
|
|
loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
|
|
matchFormat = roundSmartMatchFormat(5)
|
|
entryFee = tournamentLevel.entryFee
|
|
registrationDateLimit = deadline(for: .inscription)
|
|
if enableOnlineRegistration, isAnimation() == false {
|
|
accountIsRequired = true
|
|
licenseIsRequired = true
|
|
}
|
|
}
|
|
|
|
func customizeUsingPreferences() {
|
|
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
|
|
tournament.tournamentLevel == self.tournamentLevel
|
|
&& tournament.tournamentCategory == self.tournamentCategory
|
|
&& tournament.federalTournamentAge == self.federalTournamentAge
|
|
&& tournament.hasEnded() == true
|
|
&& tournament.isCanceled == false
|
|
&& tournament.isDeleted == false
|
|
}).sorted(by: \.endDate!, order: .descending).first else {
|
|
return
|
|
}
|
|
|
|
self.dayDuration = lastTournamentWithSameBuild.dayDuration
|
|
self.teamCount = (lastTournamentWithSameBuild.teamCount / 2) * 2
|
|
self.enableOnlineRegistration = lastTournamentWithSameBuild.enableOnlineRegistration
|
|
}
|
|
|
|
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
|
|
let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date(), name: name)
|
|
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
|
|
players.forEach { player in
|
|
player.teamRegistration = team.id
|
|
}
|
|
if isAnimation() {
|
|
if team.weight == 0 {
|
|
team.weight = unsortedTeams().count
|
|
}
|
|
}
|
|
return team
|
|
}
|
|
|
|
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 addEmptyTeamRegistration(_ count: Int) {
|
|
|
|
guard let tournamentStore = self.tournamentStore else { return }
|
|
|
|
let teams = (0..<count).map { _ in
|
|
let team = TeamRegistration(tournament: id, registrationDate: Date())
|
|
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
|
|
return team
|
|
}
|
|
|
|
do {
|
|
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func addWildCard(_ count: Int, _ type: MatchType) {
|
|
let wcs = (0..<count).map { _ in
|
|
let team = TeamRegistration(tournament: id, registrationDate: Date())
|
|
if type == .bracket {
|
|
team.wildCardBracket = true
|
|
} else {
|
|
team.wildCardGroupStage = true
|
|
}
|
|
|
|
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
|
|
team.weight += 200_000
|
|
return team
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: wcs)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func teamsRanked() -> [TeamRegistration] {
|
|
let selected = selectedSortedTeams().filter({ $0.finalRanking != nil })
|
|
return selected.sorted(by: \.finalRanking!, order: .ascending)
|
|
}
|
|
|
|
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 homonyms(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
players.filter({ $0.hasHomonym() })
|
|
}
|
|
|
|
func payIfNecessary() throws {
|
|
if self.payment != nil { return }
|
|
if let payment = Guard.main.paymentForNewTournament() {
|
|
self.payment = payment
|
|
DataStore.shared.tournaments.addOrUpdate(instance: self)
|
|
return
|
|
}
|
|
throw PaymentError.cantPayTournament
|
|
}
|
|
|
|
func cutLabelColor(index: Int?, teamCount: Int?) -> Color {
|
|
guard let index else { return Color.grayNotUniversal }
|
|
let _teamCount = teamCount ?? selectedSortedTeams().count
|
|
let groupStageCut = groupStageCut()
|
|
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut)
|
|
if index < bracketCut {
|
|
return Color.mint
|
|
} else if index - bracketCut < groupStageCut && _teamCount > 0 {
|
|
return Color.indigo
|
|
} else {
|
|
return Color.grayNotUniversal
|
|
}
|
|
}
|
|
|
|
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 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 inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
if startDate.isInCurrentYear() == false {
|
|
return []
|
|
}
|
|
return players.filter { player in
|
|
return isPlayerRankInadequate(player: player)
|
|
}
|
|
}
|
|
|
|
func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
if startDate.isInCurrentYear() == false {
|
|
return []
|
|
}
|
|
return players.filter { player in
|
|
return isPlayerAgeInadequate(player: player)
|
|
}
|
|
}
|
|
|
|
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
|
|
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)
|
|
if isAnimation() {
|
|
if newTeam.weight == 0 {
|
|
newTeam.weight = team.index(in: teams) ?? 0
|
|
}
|
|
}
|
|
teamsToImport.append(newTeam)
|
|
}
|
|
}
|
|
|
|
if let tournamentStore = self.tournamentStore {
|
|
tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport)
|
|
let playersToImport = teams.flatMap { $0.players }
|
|
tournamentStore.playerRegistrations.addOrUpdate(contentOfs: playersToImport)
|
|
}
|
|
|
|
if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty {
|
|
setGroupStage(randomize: groupStageSortMode == .random)
|
|
}
|
|
}
|
|
|
|
func registrationIssues(selectedTeams: [TeamRegistration]) async -> Int {
|
|
let players : [PlayerRegistration] = unsortedPlayers()
|
|
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 updateRank(to newDate: Date?, forceRefreshLockWeight: Bool, providedSources: [CSVParser]?) async throws {
|
|
refreshRanking = true
|
|
#if DEBUG_TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
|
|
guard let newDate else { return }
|
|
rankSourceDate = newDate
|
|
|
|
// Fetch current month data only once
|
|
var monthData = currentMonthData()
|
|
|
|
if monthData == nil {
|
|
async let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
|
|
async let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
|
|
|
|
let formatted = URL.importDateFormatter.string(from: newDate)
|
|
let newMonthData = MonthData(monthKey: formatted)
|
|
|
|
newMonthData.maleUnrankedValue = await lastRankMan
|
|
newMonthData.femaleUnrankedValue = await lastRankWoman
|
|
DataStore.shared.monthData.addOrUpdate(instance: newMonthData)
|
|
monthData = newMonthData
|
|
}
|
|
|
|
let lastRankMan = monthData?.maleUnrankedValue
|
|
let lastRankWoman = monthData?.femaleUnrankedValue
|
|
|
|
var chunkedParsers: [CSVParser] = []
|
|
if let providedSources {
|
|
chunkedParsers = providedSources
|
|
} else {
|
|
// Fetch only the required files
|
|
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
|
|
guard !dataURLs.isEmpty else { return } // Early return if no files found
|
|
|
|
let sources = dataURLs.map { CSVParser(url: $0) }
|
|
chunkedParsers = try await chunkAllSources(sources: sources, size: 10000)
|
|
}
|
|
|
|
let players = unsortedPlayers()
|
|
for player in players {
|
|
let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan
|
|
try await player.updateRank(from: chunkedParsers, lastRank: lastRank)
|
|
player.setComputedRank(in: self)
|
|
}
|
|
|
|
if providedSources == nil {
|
|
try chunkedParsers.forEach { chunk in
|
|
try FileManager.default.removeItem(at: chunk.url)
|
|
}
|
|
}
|
|
|
|
tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
|
|
|
|
let unsortedTeams = unsortedTeams()
|
|
unsortedTeams.forEach { team in
|
|
team.setWeight(from: team.players(), inTournamentCategory: tournamentCategory)
|
|
if forceRefreshLockWeight {
|
|
team.lockedWeight = team.weight
|
|
}
|
|
}
|
|
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
|
|
refreshRanking = false
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
return Tournament(rankSourceDate: rankSourceDate, currencyCode: Locale.defaultCurrency())
|
|
}
|
|
|
|
}
|
|
|
|
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.localizedCategoryLabel(ageCategory: build.age), build.age.localizedFederalAgeLabel()].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 {
|
|
public func buildHolderTitle(_ displayStyle: DisplayStyle) -> String {
|
|
tournamentTitle(.short)
|
|
}
|
|
|
|
public var category: TournamentCategory {
|
|
tournamentCategory
|
|
}
|
|
|
|
public var level: TournamentLevel {
|
|
tournamentLevel
|
|
}
|
|
|
|
public var age: FederalTournamentAge {
|
|
federalTournamentAge
|
|
}
|
|
}
|
|
|
|
// MARK: - UI extensions
|
|
|
|
extension Tournament {
|
|
|
|
public var shouldShowPaymentInfo: Bool {
|
|
if self.payment != nil {
|
|
return false
|
|
}
|
|
switch self.state() {
|
|
case .initial, .build, .running:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
//extension Tournament {
|
|
// func deadline(for type: TournamentDeadlineType) -> Date? {
|
|
// guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
|
|
//
|
|
// let daysOffset = type.daysOffset(level: tournamentLevel)
|
|
// if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
|
|
// let startOfDay = Calendar.current.startOfDay(for: date)
|
|
// return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
|
|
// }
|
|
// return nil
|
|
// }
|
|
//}
|
|
|