// // 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) 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) 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) 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..) 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] { #if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func tournament unsortedTeams", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif 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] let allLicenseIds = allLicenseIds() for (index, team) in selectedSortedTeams.enumerated() { print("pasting team index \(index)") teamPaste.append(team.pasteData(exportFormat, index + 1, allPlayers: allLicenseIds)) } return teamPaste.joined(separator: exportFormat.newLineSeparator()) } } func allLicenseIds() -> [(String, String)] { players().compactMap({ if let id = $0.licenceId?.strippedLicense, let clubCode = $0.team()?.clubCode { return (id, clubCode) } else { return nil } }) } 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 //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() 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 //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] = _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] { #if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func unsortedTeamsWithoutWO", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return self.tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false } // return Store.main.filter { $0.tournament == self.id && $0.walkOut == false } } func walkoutTeams() -> [TeamRegistration] { #if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func walkoutTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif 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] { #if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func unsortedPlayers", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif 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 { return 0 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] = [.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", 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", 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", 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", 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 //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func tournament finishedMatches", 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 = Set() 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.. 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 { #if DEBUG_TIME //DEBUGING 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 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) } let players = unsortedPlayers() try await players.concurrentForEach { player in try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0) } } func verifyEQ() async throws { #if DEBUG_TIME //DEBUGING 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 = URL.importDateFormatter.date(from: "08-2024") else { return } let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } let sources = dataURLs.map { CSVParser(url: $0) } let teams = selectedSortedTeams().prefix(24) let players = teams.flatMap({ $0.unsortedPlayers() }) try await players.concurrentForEach { player in try await player.verifyEQ(from: sources) } } 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 //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.. 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.. 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, 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] { 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] = [.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.. 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.. Int { self.tournamentStore.groupStages.sorted(by: \.step).last?.step ?? 0 } func generateSmartLoserGroupStageBracket() { guard let groupStageLoserBracket = groupStageLoserBracket() else { return } for i in qualifiedPerGroupStage.. [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.. 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.. 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 } }