Update split

sync3
Laurent 6 months ago
parent 2aa0b55f78
commit 8f1c01afc3
  1. BIN
      .DS_Store
  2. 80
      PadelClubData.xcodeproj/xcshareddata/xcschemes/PadelClubData.xcscheme
  3. BIN
      PadelClubData/.DS_Store
  4. 57
      PadelClubData/Business.swift
  5. 27
      PadelClubData/Business/DayPeriod.swift
  6. 12
      PadelClubData/Business/SpinDrawable.swift
  7. 71
      PadelClubData/ContactManager.swift
  8. BIN
      PadelClubData/Data/.DS_Store
  9. 2
      PadelClubData/Data/AppSettings.swift
  10. 6
      PadelClubData/Data/Club.swift
  11. 2
      PadelClubData/Data/Court.swift
  12. 45
      PadelClubData/Data/CustomUser.swift
  13. 50
      PadelClubData/Data/DataStore.swift
  14. 8
      PadelClubData/Data/DateInterval.swift
  15. 6
      PadelClubData/Data/DrawLog.swift
  16. 2
      PadelClubData/Data/Event.swift
  17. 1
      PadelClubData/Data/Gen/BaseClub.swift
  18. 1
      PadelClubData/Data/Gen/BaseCourt.swift
  19. 59
      PadelClubData/Data/Gen/BaseCustomUser.swift
  20. 1
      PadelClubData/Data/Gen/BaseDateInterval.swift
  21. 1
      PadelClubData/Data/Gen/BaseDrawLog.swift
  22. 1
      PadelClubData/Data/Gen/BaseEvent.swift
  23. 1
      PadelClubData/Data/Gen/BaseGroupStage.swift
  24. 1
      PadelClubData/Data/Gen/BaseMatch.swift
  25. 1
      PadelClubData/Data/Gen/BaseMatchScheduler.swift
  26. 1
      PadelClubData/Data/Gen/BaseMonthData.swift
  27. 24
      PadelClubData/Data/Gen/BasePlayerRegistration.swift
  28. 1
      PadelClubData/Data/Gen/BasePurchase.swift
  29. 1
      PadelClubData/Data/Gen/BaseRound.swift
  30. 1
      PadelClubData/Data/Gen/BaseTeamRegistration.swift
  31. 1
      PadelClubData/Data/Gen/BaseTeamScore.swift
  32. 101
      PadelClubData/Data/Gen/BaseTournament.swift
  33. 1
      PadelClubData/Data/Gen/Club.json
  34. 41
      PadelClubData/Data/Gen/CustomUser.json
  35. 16
      PadelClubData/Data/Gen/PlayerRegistration.json
  36. 75
      PadelClubData/Data/Gen/Tournament.json
  37. 7
      PadelClubData/Data/Gen/generator.py
  38. 34
      PadelClubData/Data/GroupStage.swift
  39. 64
      PadelClubData/Data/Match.swift
  40. 32
      PadelClubData/Data/MatchScheduler.swift
  41. 2
      PadelClubData/Data/MonthData.swift
  42. 48
      PadelClubData/Data/PlayerRegistration.swift
  43. 4
      PadelClubData/Data/Purchase.swift
  44. 270
      PadelClubData/Data/Round.swift
  45. 60
      PadelClubData/Data/TeamRegistration.swift
  46. 6
      PadelClubData/Data/TeamScore.swift
  47. 503
      PadelClubData/Data/Tournament.swift
  48. 2
      PadelClubData/Data/TournamentLibrary.swift
  49. 16
      PadelClubData/Extensions/Array+Extensions.swift
  50. 69
      PadelClubData/Extensions/Color+Extensions.swift
  51. 13
      PadelClubData/Extensions/Date+Extensions.swift
  52. 0
      PadelClubData/Extensions/MySortDescriptor.swift
  53. 8
      PadelClubData/Extensions/NumberFormatter+Extensions.swift
  54. 1
      PadelClubData/Extensions/Sequence+Extensions.swift
  55. 5
      PadelClubData/Extensions/String+Crypto.swift
  56. 48
      PadelClubData/Extensions/String+Extensions.swift
  57. 16
      PadelClubData/Extensions/Tournament+Extensions.swift
  58. 10
      PadelClubData/Extensions/URL+Extensions.swift
  59. 27
      PadelClubData/Subscriptions/Guard.swift
  60. 4
      PadelClubData/Subscriptions/StoreItem.swift
  61. 141
      PadelClubData/Utils/ContactManager.swift
  62. 1
      PadelClubData/Utils/DisplayContext.swift
  63. 4
      PadelClubData/Utils/ExportFormat.swift
  64. 12
      PadelClubData/Utils/NetworkManager.swift
  65. 4
      PadelClubData/Utils/NetworkManagerError.swift
  66. 2
      PadelClubData/Utils/PListReader.swift
  67. 4
      PadelClubData/Utils/Patcher.swift
  68. 14
      PadelClubData/Utils/SourceFileManager.swift
  69. 2
      PadelClubData/Utils/URLs.swift
  70. 47
      PadelClubData/ViewModel/MatchDescriptor.swift
  71. 0
      PadelClubData/ViewModel/MatchSpot.swift
  72. 135
      PadelClubData/ViewModel/PadelRule.swift
  73. 0
      PadelClubData/ViewModel/Screen.swift
  74. 1
      PadelClubData/ViewModel/SeedInterval.swift
  75. 6
      PadelClubData/ViewModel/Selectable.swift
  76. 8
      PadelClubData/ViewModel/SetDescriptor.swift

BIN
.DS_Store vendored

Binary file not shown.

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4AD86BE2DAEB37900022F97"
BuildableName = "PadelClubData.framework"
BlueprintName = "PadelClubData"
ReferencedContainer = "container:PadelClubData.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4AD86C82DAEB37900022F97"
BuildableName = "PadelClubDataTests.xctest"
BlueprintName = "PadelClubDataTests"
ReferencedContainer = "container:PadelClubData.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4AD86BE2DAEB37900022F97"
BuildableName = "PadelClubData.framework"
BlueprintName = "PadelClubData"
ReferencedContainer = "container:PadelClubData.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

Binary file not shown.

@ -1,12 +1,36 @@
//
// OnlineRegistrationStatus.swift
// Business.swift
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
// Created by Laurent Morvillier on 30/04/2025.
//
import Foundation
public protocol SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String]
}
public enum DayPeriod: Int, CaseIterable, Identifiable, Codable {
public var id: Int { self.rawValue }
case all
case weekend
case week
public func localizedDayPeriodLabel() -> String {
switch self {
case .all:
return "n'importe"
case .week:
return "la semaine"
case .weekend:
return "le week-end"
}
}
}
public enum OnlineRegistrationStatus: Int {
case open = 1
case notEnabled = 2
@ -59,3 +83,32 @@ public enum OnlineRegistrationStatus: Int {
}
}
}
public enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable {
case all = -1
case male = 1
case female = 0
public var id: Int { rawValue }
public func icon() -> String {
switch self {
case .all:
return "Tous"
case .male:
return "Homme"
case .female:
return "Femme"
}
}
public var localizedPlayerLabel: String {
switch self {
case .female:
return "joueuse"
default:
return "joueur"
}
}
}

@ -1,27 +0,0 @@
//
// DayPeriod.swift
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Foundation
public enum DayPeriod: Int, CaseIterable, Identifiable, Codable {
public var id: Int { self.rawValue }
case all
case weekend
case week
public func localizedDayPeriodLabel() -> String {
switch self {
case .all:
return "n'importe"
case .week:
return "la semaine"
case .weekend:
return "le week-end"
}
}
}

@ -1,12 +0,0 @@
//
// SpinDrawable.swift
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Foundation
public protocol SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String]
}

@ -1,71 +0,0 @@
//
// ContactManager.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 19/09/2023.
//
import Foundation
import SwiftUI
import MessageUI
import LeStorage
public enum ContactManagerError: LocalizedError {
case mailFailed
case mailNotSent //no network no error
case messageFailed
case messageNotSent //no network no error
case calendarAccessDenied
case calendarEventSaveFailed
case noCalendarAvailable
case uncalledTeams([TeamRegistration])
var localizedDescription: String {
switch self {
case .mailFailed:
return "Le mail n'a pas été envoyé"
case .mailNotSent:
return "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer."
case .messageFailed:
return "Le SMS n'a pas été envoyé"
case .messageNotSent:
return "Le SMS n'a pas été envoyé"
case .uncalledTeams(let array):
let verb = array.count > 1 ? "peuvent" : "peut"
return "Attention, \(array.count) équipe\(array.count.pluralSuffix) ne \(verb) pas être contacté par la méthode choisie"
case .calendarAccessDenied:
return "Padel Club n'a pas accès à votre calendrier"
case .calendarEventSaveFailed:
return "Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier"
case .noCalendarAvailable:
return "Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi"
}
}
public static func getNetworkErrorMessage(sentError: ContactManagerError?, networkMonitorConnected: Bool) -> String {
var errors: [String] = []
if networkMonitorConnected == false {
errors.append("L'appareil n'est pas connecté à internet.")
}
if let sentError {
errors.append(sentError.localizedDescription)
}
return errors.joined(separator: "\n")
}
}
public enum ContactType: Identifiable {
case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?)
case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?)
public var id: Int {
switch self {
case .message: return 0
case .mail: return 1
}
}
public static let defaultAvailablePaymentMethods: String = "Règlement possible par chèque ou espèces."
}

Binary file not shown.

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
public final class AppSettings: MicroStorable {
final public class AppSettings: MicroStorable {
public var lastDataSource: String? = nil
public var didCreateAccount: Bool = false

@ -10,10 +10,8 @@ import SwiftUI
import LeStorage
@Observable
public final class Club: BaseClub {
static var copyServerResponse: Bool { return true }
final public class Club: BaseClub {
public func clubTitle(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide, .title:

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
public final class Court: BaseCourt {
final public class Court: BaseCourt {
static func == (lhs: Court, rhs: Court) -> Bool {
lhs.id == rhs.id

@ -8,12 +8,48 @@
import Foundation
import LeStorage
enum UserRight: Int, Codable {
public enum UserRight: Int, Codable {
case none = 0
case edition = 1
case creation = 2
}
public enum RegistrationPaymentMode: Int, Codable {
case disabled = 0
case corporate = 1
case noFee = 2
case stripe = 3
public static let stripeFixedFee = 0.25 // Fixed fee in euros
public static let stripePercentageFee = 0.014 // 1.4%
public func canEnableOnlinePayment() -> Bool {
switch self {
case .disabled:
return false
case .corporate:
return true
case .noFee:
return true
case .stripe:
return true
}
}
public func requiresStripe() -> Bool {
switch self {
case .disabled:
return false
case .corporate:
return true
case .noFee:
return true
case .stripe:
return true
}
}
}
@Observable
public class CustomUser: BaseCustomUser, UserBase {
@ -48,7 +84,7 @@ public class CustomUser: BaseCustomUser, UserBase {
//
// var deviceId: String?
init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) {
public init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) {
super.init(username: username, email: email, firstName: firstName, lastName: lastName, phone: phone, country: country, loserBracketMode: loserBracketMode)
@ -129,7 +165,10 @@ public class CustomUser: BaseCustomUser, UserBase {
}
}
// enum CodingKeys: String, CodingKey {
public func canEnableOnlinePayment() -> Bool {
registrationPaymentMode.canEnableOnlinePayment()
}
// public enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _lastUpdate = "lastUpdate"
// case _username = "username"

@ -39,7 +39,7 @@ public class DataStore: ObservableObject {
public fileprivate(set) var dateIntervals: SyncedCollection<DateInterval>
public fileprivate(set) var purchases: SyncedCollection<Purchase>
fileprivate var userStorage: StoredSingleton<CustomUser>
public var userStorage: StoredSingleton<CustomUser>
fileprivate var _temporaryLocalUser: OptionalStorage<CustomUser> = OptionalStorage(fileName: "tmp_local_user.json")
public fileprivate(set) var appSettingsStorage: MicroStorage<AppSettings> = MicroStorage(fileName: "appsettings.json")
@ -282,8 +282,16 @@ public class DataStore: ObservableObject {
// MARK: - Convenience
private var _lastRunningCheckDate: Date? = nil
private var _cachedRunningMatches: [Match]? = nil
public func runningMatches() -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastRunningCheckDate,
let cachedMatches = _cachedRunningMatches,
dateNow.timeIntervalSince(lastCheck) < 5 {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
@ -295,11 +303,22 @@ public class DataStore: ObservableObject {
runningMatches.append(contentsOf: matches)
}
}
return runningMatches
_lastRunningCheckDate = dateNow
_cachedRunningMatches = runningMatches
return _cachedRunningMatches!
}
private var _lastRunningAndNextCheckDate: Date? = nil
private var _cachedRunningAndNextMatches: [Match]? = nil
public func runningAndNextMatches() -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastRunningAndNextCheckDate,
let cachedMatches = _cachedRunningAndNextMatches,
dateNow.timeIntervalSince(lastCheck) < 5 {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
@ -310,11 +329,23 @@ public class DataStore: ObservableObject {
runningMatches.append(contentsOf: matches)
}
}
return runningMatches
_lastRunningAndNextCheckDate = dateNow
_cachedRunningAndNextMatches = runningMatches
return _cachedRunningAndNextMatches!
}
private var _lastEndCheckDate: Date? = nil
private var _cachedEndMatches: [Match]? = nil
public func endMatches() -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastEndCheckDate,
let cachedMatches = _cachedEndMatches,
dateNow.timeIntervalSince(lastCheck) < 5 {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
@ -325,7 +356,18 @@ public class DataStore: ObservableObject {
runningMatches.append(contentsOf: matches)
}
}
return runningMatches.sorted(by: \.endDate!, order: .descending)
_lastEndCheckDate = dateNow
_cachedEndMatches = runningMatches.sorted(by: \.endDate!, order: .descending)
return _cachedEndMatches!
}
public func subscriptionUnitlyPayedTournaments(after date: Date) -> Int {
return DataStore.shared.tournaments.count(where: { tournament in
tournament.creationDate > date &&
tournament.payment == .subscriptionUnit &&
tournament.isCanceled == false &&
tournament.isDeleted == false })
}
}

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
public final class DateInterval: BaseDateInterval {
final public class DateInterval: BaseDateInterval {
// static func resourceName() -> String { return "date-intervals" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
@ -45,15 +45,15 @@ public final class DateInterval: BaseDateInterval {
startDate..<endDate
}
func isSingleDay() -> Bool {
public func isSingleDay() -> Bool {
Calendar.current.isDate(startDate, inSameDayAs: endDate)
}
func isDateInside(_ date: Date) -> Bool {
public func isDateInside(_ date: Date) -> Bool {
date >= startDate && date <= endDate
}
func isDateOutside(_ date: Date) -> Bool {
public func isDateOutside(_ date: Date) -> Bool {
date <= startDate && date <= endDate && date >= startDate && date >= endDate
}

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
public final class DrawLog: BaseDrawLog, SideStorable {
final public class DrawLog: BaseDrawLog, SideStorable {
public func tournamentObject() -> Tournament? {
Store.main.findById(self.tournament)
@ -70,7 +70,7 @@ public final class DrawLog: BaseDrawLog, SideStorable {
return drawMatch()?.matchTitle() ?? ""
}
public var tournamentStore: TournamentStore? {
var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
@ -84,7 +84,7 @@ public enum DrawType: Int, Codable {
case groupStage
case court
func localizedDrawType() -> String {
public func localizedDrawType() -> String {
switch self {
case .seed:
return "Tête de série"

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
public final class Event: BaseEvent {
final public class Event: BaseEvent {
public init(creator: String? = nil, club: String? = nil, name: String? = nil, tenupId: String? = nil) {
super.init(creator: creator, club: club, name: name, tenupId: tenupId)

@ -10,6 +10,7 @@ public class BaseClub: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "clubs" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = true
public var id: String = Store.randomId()
public var creator: String? = nil

@ -10,6 +10,7 @@ public class BaseCourt: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "courts" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var index: Int = 0

@ -10,6 +10,7 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "users" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var username: String = ""
@ -32,8 +33,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
public var groupStageMatchFormatPreference: MatchFormat? = nil
public var loserBracketMatchFormatPreference: MatchFormat? = nil
public var loserBracketMode: LoserBracketMode = .automatic
public var disableRankingFederalRuling: Bool = false
public var deviceId: String? = nil
public var agents: [String] = []
public var userRole: Int? = nil
public var registrationPaymentMode: RegistrationPaymentMode = RegistrationPaymentMode.disabled
public var umpireCustomMail: String? = nil
public var umpireCustomContact: String? = nil
public var umpireCustomPhone: String? = nil
public var hideUmpireMail: Bool = false
public var hideUmpirePhone: Bool = true
public init(
id: String = Store.randomId(),
@ -57,8 +66,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
groupStageMatchFormatPreference: MatchFormat? = nil,
loserBracketMatchFormatPreference: MatchFormat? = nil,
loserBracketMode: LoserBracketMode = .automatic,
disableRankingFederalRuling: Bool = false,
deviceId: String? = nil,
agents: [String] = []
agents: [String] = [],
userRole: Int? = nil,
registrationPaymentMode: RegistrationPaymentMode = RegistrationPaymentMode.disabled,
umpireCustomMail: String? = nil,
umpireCustomContact: String? = nil,
umpireCustomPhone: String? = nil,
hideUmpireMail: Bool = false,
hideUmpirePhone: Bool = true
) {
super.init()
self.id = id
@ -82,8 +99,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
self.groupStageMatchFormatPreference = groupStageMatchFormatPreference
self.loserBracketMatchFormatPreference = loserBracketMatchFormatPreference
self.loserBracketMode = loserBracketMode
self.disableRankingFederalRuling = disableRankingFederalRuling
self.deviceId = deviceId
self.agents = agents
self.userRole = userRole
self.registrationPaymentMode = registrationPaymentMode
self.umpireCustomMail = umpireCustomMail
self.umpireCustomContact = umpireCustomContact
self.umpireCustomPhone = umpireCustomPhone
self.hideUmpireMail = hideUmpireMail
self.hideUmpirePhone = hideUmpirePhone
}
required public override init() {
super.init()
@ -111,8 +136,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference"
case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference"
case _loserBracketMode = "loserBracketMode"
case _disableRankingFederalRuling = "disableRankingFederalRuling"
case _deviceId = "deviceId"
case _agents = "agents"
case _userRole = "userRole"
case _registrationPaymentMode = "registrationPaymentMode"
case _umpireCustomMail = "umpireCustomMail"
case _umpireCustomContact = "umpireCustomContact"
case _umpireCustomPhone = "umpireCustomPhone"
case _hideUmpireMail = "hideUmpireMail"
case _hideUmpirePhone = "hideUmpirePhone"
}
required init(from decoder: Decoder) throws {
@ -138,8 +171,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
self.groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference) ?? nil
self.loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) ?? nil
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false
self.deviceId = try container.decodeIfPresent(String.self, forKey: ._deviceId) ?? nil
self.agents = try container.decodeIfPresent([String].self, forKey: ._agents) ?? []
self.userRole = try container.decodeIfPresent(Int.self, forKey: ._userRole) ?? nil
self.registrationPaymentMode = try container.decodeIfPresent(RegistrationPaymentMode.self, forKey: ._registrationPaymentMode) ?? RegistrationPaymentMode.disabled
self.umpireCustomMail = try container.decodeIfPresent(String.self, forKey: ._umpireCustomMail) ?? nil
self.umpireCustomContact = try container.decodeIfPresent(String.self, forKey: ._umpireCustomContact) ?? nil
self.umpireCustomPhone = try container.decodeIfPresent(String.self, forKey: ._umpireCustomPhone) ?? nil
self.hideUmpireMail = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpireMail) ?? false
self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true
try super.init(from: decoder)
}
@ -166,8 +207,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
try container.encode(self.groupStageMatchFormatPreference, forKey: ._groupStageMatchFormatPreference)
try container.encode(self.loserBracketMatchFormatPreference, forKey: ._loserBracketMatchFormatPreference)
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode)
try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling)
try container.encode(self.deviceId, forKey: ._deviceId)
try container.encode(self.agents, forKey: ._agents)
try container.encode(self.userRole, forKey: ._userRole)
try container.encode(self.registrationPaymentMode, forKey: ._registrationPaymentMode)
try container.encode(self.umpireCustomMail, forKey: ._umpireCustomMail)
try container.encode(self.umpireCustomContact, forKey: ._umpireCustomContact)
try container.encode(self.umpireCustomPhone, forKey: ._umpireCustomPhone)
try container.encode(self.hideUmpireMail, forKey: ._hideUmpireMail)
try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone)
try super.encode(to: encoder)
}
@ -194,8 +243,16 @@ public class BaseCustomUser: SyncedModelObject, SyncedStorable {
self.groupStageMatchFormatPreference = customuser.groupStageMatchFormatPreference
self.loserBracketMatchFormatPreference = customuser.loserBracketMatchFormatPreference
self.loserBracketMode = customuser.loserBracketMode
self.disableRankingFederalRuling = customuser.disableRankingFederalRuling
self.deviceId = customuser.deviceId
self.agents = customuser.agents
self.userRole = customuser.userRole
self.registrationPaymentMode = customuser.registrationPaymentMode
self.umpireCustomMail = customuser.umpireCustomMail
self.umpireCustomContact = customuser.umpireCustomContact
self.umpireCustomPhone = customuser.umpireCustomPhone
self.hideUmpireMail = customuser.hideUmpireMail
self.hideUmpirePhone = customuser.hideUmpirePhone
}
public static func relationships() -> [Relationship] {

@ -10,6 +10,7 @@ public class BaseDateInterval: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "date-intervals" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var event: String = ""

@ -10,6 +10,7 @@ public class BaseDrawLog: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "draw-logs" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var tournament: String = ""

@ -10,6 +10,7 @@ public class BaseEvent: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "events" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var creator: String? = nil

@ -10,6 +10,7 @@ public class BaseGroupStage: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "group-stages" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var tournament: String = ""

@ -10,6 +10,7 @@ public class BaseMatch: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "matches" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var round: String? = nil

@ -10,6 +10,7 @@ public class BaseMatchScheduler: BaseModelObject, Storable {
public static func resourceName() -> String { return "match-scheduler" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var tournament: String = ""

@ -10,6 +10,7 @@ public class BaseMonthData: BaseModelObject, Storable {
public static func resourceName() -> String { return "month-data" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var monthKey: String = ""

@ -10,6 +10,7 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "player-registrations" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var teamRegistration: String? = nil
@ -33,6 +34,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
public var coach: Bool = false
public var captain: Bool = false
public var registeredOnline: Bool = false
public var timeToConfirm: Date? = nil
public var registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting
public var paymentId: String? = nil
public init(
id: String = Store.randomId(),
@ -56,7 +60,10 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
hasArrived: Bool = false,
coach: Bool = false,
captain: Bool = false,
registeredOnline: Bool = false
registeredOnline: Bool = false,
timeToConfirm: Date? = nil,
registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting,
paymentId: String? = nil
) {
super.init()
self.id = id
@ -81,6 +88,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.coach = coach
self.captain = captain
self.registeredOnline = registeredOnline
self.timeToConfirm = timeToConfirm
self.registrationStatus = registrationStatus
self.paymentId = paymentId
}
required public override init() {
super.init()
@ -109,6 +119,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
case _coach = "coach"
case _captain = "captain"
case _registeredOnline = "registeredOnline"
case _timeToConfirm = "timeToConfirm"
case _registrationStatus = "registrationStatus"
case _paymentId = "paymentId"
}
required init(from decoder: Decoder) throws {
@ -135,6 +148,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.coach = try container.decodeIfPresent(Bool.self, forKey: ._coach) ?? false
self.captain = try container.decodeIfPresent(Bool.self, forKey: ._captain) ?? false
self.registeredOnline = try container.decodeIfPresent(Bool.self, forKey: ._registeredOnline) ?? false
self.timeToConfirm = try container.decodeIfPresent(Date.self, forKey: ._timeToConfirm) ?? nil
self.registrationStatus = try container.decodeIfPresent(PlayerRegistration.RegistrationStatus.self, forKey: ._registrationStatus) ?? PlayerRegistration.RegistrationStatus.waiting
self.paymentId = try container.decodeIfPresent(String.self, forKey: ._paymentId) ?? nil
try super.init(from: decoder)
}
@ -162,6 +178,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
try container.encode(self.coach, forKey: ._coach)
try container.encode(self.captain, forKey: ._captain)
try container.encode(self.registeredOnline, forKey: ._registeredOnline)
try container.encode(self.timeToConfirm, forKey: ._timeToConfirm)
try container.encode(self.registrationStatus, forKey: ._registrationStatus)
try container.encode(self.paymentId, forKey: ._paymentId)
try super.encode(to: encoder)
}
@ -194,6 +213,9 @@ public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
self.coach = playerregistration.coach
self.captain = playerregistration.captain
self.registeredOnline = playerregistration.registeredOnline
self.timeToConfirm = playerregistration.timeToConfirm
self.registrationStatus = playerregistration.registrationStatus
self.paymentId = playerregistration.paymentId
}
public static func relationships() -> [Relationship] {

@ -8,6 +8,7 @@ public class BasePurchase: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "purchases" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: UInt64 = 0
public var user: String = ""

@ -10,6 +10,7 @@ public class BaseRound: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "rounds" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var tournament: String = ""

@ -10,6 +10,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "team-registrations" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var tournament: String = ""

@ -10,6 +10,7 @@ public class BaseTeamScore: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "team-scores" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var match: String = ""

@ -10,6 +10,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "tournaments" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public static var copyServerResponse: Bool = false
public var id: String = Store.randomId()
public var event: String? = nil
@ -22,20 +23,20 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public var roundFormat: MatchFormat? = nil
public var loserRoundFormat: MatchFormat? = nil
public var groupStageSortMode: GroupStageOrderingMode = GroupStageOrderingMode.snake
public var groupStageCount: Int = 0
public var groupStageCount: Int = 4
public var rankSourceDate: Date? = nil
public var dayDuration: Int = 0
public var teamCount: Int = 0
public var dayDuration: Int = 1
public var teamCount: Int = 24
public var teamSorting: TeamSortingType = TeamSortingType.inscriptionDate
public var federalCategory: TournamentCategory = TournamentCategory.men
public var federalLevelCategory: TournamentLevel = TournamentLevel.unlisted
public var federalAgeCategory: FederalTournamentAge = FederalTournamentAge.unlisted
public var federalLevelCategory: TournamentLevel = TournamentLevel.p100
public var federalAgeCategory: FederalTournamentAge = FederalTournamentAge.senior
public var closedRegistrationDate: Date? = nil
public var groupStageAdditionalQualified: Int = 0
public var courtCount: Int = 2
public var prioritizeClubMembers: Bool = false
public var qualifiedPerGroupStage: Int = 0
public var teamsPerGroupStage: Int = 0
public var qualifiedPerGroupStage: Int = 1
public var teamsPerGroupStage: Int = 4
public var entryFee: Double? = nil
public var payment: TournamentPayment? = nil
public var additionalEstimationDuration: Int = 0
@ -70,6 +71,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public var hideUmpirePhone: Bool = true
public var disableRankingFederalRuling: Bool = false
public var teamCountLimit: Bool = true
public var enableOnlinePayment: Bool = false
public var onlinePaymentIsMandatory: Bool = false
public var enableOnlinePaymentRefund: Bool = false
public var refundDateLimit: Date? = nil
public var stripeAccountId: String? = nil
public var enableTimeToConfirm: Bool = false
public var isCorporateTournament: Bool = false
public var isTemplate: Bool = false
public init(
id: String = Store.randomId(),
@ -83,20 +92,20 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
roundFormat: MatchFormat? = nil,
loserRoundFormat: MatchFormat? = nil,
groupStageSortMode: GroupStageOrderingMode = GroupStageOrderingMode.snake,
groupStageCount: Int = 0,
groupStageCount: Int = 4,
rankSourceDate: Date? = nil,
dayDuration: Int = 0,
teamCount: Int = 0,
dayDuration: Int = 1,
teamCount: Int = 24,
teamSorting: TeamSortingType = TeamSortingType.inscriptionDate,
federalCategory: TournamentCategory = TournamentCategory.men,
federalLevelCategory: TournamentLevel = TournamentLevel.unlisted,
federalAgeCategory: FederalTournamentAge = FederalTournamentAge.unlisted,
federalLevelCategory: TournamentLevel = TournamentLevel.p100,
federalAgeCategory: FederalTournamentAge = FederalTournamentAge.senior,
closedRegistrationDate: Date? = nil,
groupStageAdditionalQualified: Int = 0,
courtCount: Int = 2,
prioritizeClubMembers: Bool = false,
qualifiedPerGroupStage: Int = 0,
teamsPerGroupStage: Int = 0,
qualifiedPerGroupStage: Int = 1,
teamsPerGroupStage: Int = 4,
entryFee: Double? = nil,
payment: TournamentPayment? = nil,
additionalEstimationDuration: Int = 0,
@ -130,7 +139,15 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
hideUmpireMail: Bool = false,
hideUmpirePhone: Bool = true,
disableRankingFederalRuling: Bool = false,
teamCountLimit: Bool = true
teamCountLimit: Bool = true,
enableOnlinePayment: Bool = false,
onlinePaymentIsMandatory: Bool = false,
enableOnlinePaymentRefund: Bool = false,
refundDateLimit: Date? = nil,
stripeAccountId: String? = nil,
enableTimeToConfirm: Bool = false,
isCorporateTournament: Bool = false,
isTemplate: Bool = false
) {
super.init()
self.id = id
@ -192,6 +209,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.hideUmpirePhone = hideUmpirePhone
self.disableRankingFederalRuling = disableRankingFederalRuling
self.teamCountLimit = teamCountLimit
self.enableOnlinePayment = enableOnlinePayment
self.onlinePaymentIsMandatory = onlinePaymentIsMandatory
self.enableOnlinePaymentRefund = enableOnlinePaymentRefund
self.refundDateLimit = refundDateLimit
self.stripeAccountId = stripeAccountId
self.enableTimeToConfirm = enableTimeToConfirm
self.isCorporateTournament = isCorporateTournament
self.isTemplate = isTemplate
}
required public override init() {
super.init()
@ -259,6 +284,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
case _hideUmpirePhone = "hideUmpirePhone"
case _disableRankingFederalRuling = "disableRankingFederalRuling"
case _teamCountLimit = "teamCountLimit"
case _enableOnlinePayment = "enableOnlinePayment"
case _onlinePaymentIsMandatory = "onlinePaymentIsMandatory"
case _enableOnlinePaymentRefund = "enableOnlinePaymentRefund"
case _refundDateLimit = "refundDateLimit"
case _stripeAccountId = "stripeAccountId"
case _enableTimeToConfirm = "enableTimeToConfirm"
case _isCorporateTournament = "isCorporateTournament"
case _isTemplate = "isTemplate"
}
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
@ -341,20 +374,20 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.roundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._roundFormat) ?? nil
self.loserRoundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserRoundFormat) ?? nil
self.groupStageSortMode = try container.decodeIfPresent(GroupStageOrderingMode.self, forKey: ._groupStageSortMode) ?? GroupStageOrderingMode.snake
self.groupStageCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageCount) ?? 0
self.groupStageCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageCount) ?? 4
self.rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate) ?? nil
self.dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) ?? 0
self.teamCount = try container.decodeIfPresent(Int.self, forKey: ._teamCount) ?? 0
self.dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) ?? 1
self.teamCount = try container.decodeIfPresent(Int.self, forKey: ._teamCount) ?? 24
self.teamSorting = try container.decodeIfPresent(TeamSortingType.self, forKey: ._teamSorting) ?? TeamSortingType.inscriptionDate
self.federalCategory = try container.decodeIfPresent(TournamentCategory.self, forKey: ._federalCategory) ?? TournamentCategory.men
self.federalLevelCategory = try container.decodeIfPresent(TournamentLevel.self, forKey: ._federalLevelCategory) ?? TournamentLevel.unlisted
self.federalAgeCategory = try container.decodeIfPresent(FederalTournamentAge.self, forKey: ._federalAgeCategory) ?? FederalTournamentAge.unlisted
self.federalLevelCategory = try container.decodeIfPresent(TournamentLevel.self, forKey: ._federalLevelCategory) ?? TournamentLevel.p100
self.federalAgeCategory = try container.decodeIfPresent(FederalTournamentAge.self, forKey: ._federalAgeCategory) ?? FederalTournamentAge.senior
self.closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate) ?? nil
self.groupStageAdditionalQualified = try container.decodeIfPresent(Int.self, forKey: ._groupStageAdditionalQualified) ?? 0
self.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2
self.prioritizeClubMembers = try container.decodeIfPresent(Bool.self, forKey: ._prioritizeClubMembers) ?? false
self.qualifiedPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._qualifiedPerGroupStage) ?? 0
self.teamsPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._teamsPerGroupStage) ?? 0
self.qualifiedPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._qualifiedPerGroupStage) ?? 1
self.teamsPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._teamsPerGroupStage) ?? 4
self.entryFee = try container.decodeIfPresent(Double.self, forKey: ._entryFee) ?? nil
self.payment = try Self._decodePayment(container: container)
self.additionalEstimationDuration = try container.decodeIfPresent(Int.self, forKey: ._additionalEstimationDuration) ?? 0
@ -389,6 +422,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true
self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false
self.teamCountLimit = try container.decodeIfPresent(Bool.self, forKey: ._teamCountLimit) ?? true
self.enableOnlinePayment = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlinePayment) ?? false
self.onlinePaymentIsMandatory = try container.decodeIfPresent(Bool.self, forKey: ._onlinePaymentIsMandatory) ?? false
self.enableOnlinePaymentRefund = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlinePaymentRefund) ?? false
self.refundDateLimit = try container.decodeIfPresent(Date.self, forKey: ._refundDateLimit) ?? nil
self.stripeAccountId = try container.decodeIfPresent(String.self, forKey: ._stripeAccountId) ?? nil
self.enableTimeToConfirm = try container.decodeIfPresent(Bool.self, forKey: ._enableTimeToConfirm) ?? false
self.isCorporateTournament = try container.decodeIfPresent(Bool.self, forKey: ._isCorporateTournament) ?? false
self.isTemplate = try container.decodeIfPresent(Bool.self, forKey: ._isTemplate) ?? false
try super.init(from: decoder)
}
@ -453,6 +494,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone)
try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling)
try container.encode(self.teamCountLimit, forKey: ._teamCountLimit)
try container.encode(self.enableOnlinePayment, forKey: ._enableOnlinePayment)
try container.encode(self.onlinePaymentIsMandatory, forKey: ._onlinePaymentIsMandatory)
try container.encode(self.enableOnlinePaymentRefund, forKey: ._enableOnlinePaymentRefund)
try container.encode(self.refundDateLimit, forKey: ._refundDateLimit)
try container.encode(self.stripeAccountId, forKey: ._stripeAccountId)
try container.encode(self.enableTimeToConfirm, forKey: ._enableTimeToConfirm)
try container.encode(self.isCorporateTournament, forKey: ._isCorporateTournament)
try container.encode(self.isTemplate, forKey: ._isTemplate)
try super.encode(to: encoder)
}
@ -522,6 +571,14 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.hideUmpirePhone = tournament.hideUmpirePhone
self.disableRankingFederalRuling = tournament.disableRankingFederalRuling
self.teamCountLimit = tournament.teamCountLimit
self.enableOnlinePayment = tournament.enableOnlinePayment
self.onlinePaymentIsMandatory = tournament.onlinePaymentIsMandatory
self.enableOnlinePaymentRefund = tournament.enableOnlinePaymentRefund
self.refundDateLimit = tournament.refundDateLimit
self.stripeAccountId = tournament.stripeAccountId
self.enableTimeToConfirm = tournament.enableTimeToConfirm
self.isCorporateTournament = tournament.isCorporateTournament
self.isTemplate = tournament.isTemplate
}
public static func relationships() -> [Relationship] {

@ -4,6 +4,7 @@
"name": "Club",
"synchronizable": true,
"observable": true,
"copy_server_response": "true",
"properties": [
{
"name": "id",

@ -119,6 +119,11 @@
"type": "LoserBracketMode",
"defaultValue": ".automatic"
},
{
"name": "disableRankingFederalRuling",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "deviceId",
"type": "String",
@ -129,6 +134,42 @@
"name": "agents",
"type": "[String]",
"defaultValue": "[]"
},
{
"name": "userRole",
"type": "Int",
"optional": true,
"defaultValue": "nil"
},
{
"name": "registrationPaymentMode",
"type": "RegistrationPaymentMode",
"defaultValue": "RegistrationPaymentMode.disabled"
},
{
"name": "umpireCustomMail",
"type": "String",
"optional": true
},
{
"name": "umpireCustomContact",
"type": "String",
"optional": true
},
{
"name": "umpireCustomPhone",
"type": "String",
"optional": true
},
{
"name": "hideUmpireMail",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "hideUmpirePhone",
"type": "Bool",
"defaultValue": "true"
}
]
}

@ -115,6 +115,22 @@
"name": "registeredOnline",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "timeToConfirm",
"type": "Date",
"optional": true
},
{
"name": "registrationStatus",
"type": "PlayerRegistration.RegistrationStatus",
"choices": "PlayerRegistration.RegistrationStatus",
"defaultValue": "PlayerRegistration.RegistrationStatus.waiting"
},
{
"name": "paymentId",
"type": "String",
"optional": true
}
]
}

@ -66,7 +66,8 @@
},
{
"name": "groupStageCount",
"type": "Int"
"type": "Int",
"defaultValue": "4"
},
{
"name": "rankSourceDate",
@ -75,11 +76,13 @@
},
{
"name": "dayDuration",
"type": "Int"
"type": "Int",
"defaultValue": "1"
},
{
"name": "teamCount",
"type": "Int"
"type": "Int",
"defaultValue": "24"
},
{
"name": "teamSorting",
@ -94,12 +97,12 @@
{
"name": "federalLevelCategory",
"type": "TournamentLevel",
"defaultValue": "TournamentLevel.unlisted"
"defaultValue": "TournamentLevel.p100"
},
{
"name": "federalAgeCategory",
"type": "FederalTournamentAge",
"defaultValue": "FederalTournamentAge.unlisted"
"defaultValue": "FederalTournamentAge.senior"
},
{
"name": "closedRegistrationDate",
@ -108,7 +111,8 @@
},
{
"name": "groupStageAdditionalQualified",
"type": "Int"
"type": "Int",
"defaultValue": "0"
},
{
"name": "courtCount",
@ -117,15 +121,18 @@
},
{
"name": "prioritizeClubMembers",
"type": "Bool"
"type": "Bool",
"defaultValue": "false"
},
{
"name": "qualifiedPerGroupStage",
"type": "Int"
"type": "Int",
"defaultValue": "1"
},
{
"name": "teamsPerGroupStage",
"type": "Int"
"type": "Int",
"defaultValue": "4"
},
{
"name": "entryFee",
@ -255,12 +262,12 @@
{
"name": "minimumPlayerPerTeam",
"type": "Int",
"defaultValue": 2
"defaultValue": "2"
},
{
"name": "maximumPlayerPerTeam",
"type": "Int",
"defaultValue": 2
"defaultValue": "2"
},
{
"name": "information",
@ -295,14 +302,52 @@
{
"name": "disableRankingFederalRuling",
"type": "Bool",
"defaultValue": "false",
"optional": false
"defaultValue": "false"
},
{
"name": "teamCountLimit",
"type": "Bool",
"defaultValue": "true",
"optional": false
"defaultValue": "true"
},
{
"name": "enableOnlinePayment",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "onlinePaymentIsMandatory",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "enableOnlinePaymentRefund",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "refundDateLimit",
"type": "Date",
"optional": true
},
{
"name": "stripeAccountId",
"type": "String",
"optional": true
},
{
"name": "enableTimeToConfirm",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "isCorporateTournament",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "isTemplate",
"type": "Bool",
"defaultValue": "false"
}
]
}

@ -24,6 +24,7 @@ class SwiftModelGenerator:
resource = self.make_resource_name(model_name)
resource_name = model_data.get("resource_name", resource)
token_exempted = model_data.get("tokenExemptedMethods", [])
copy_server_response = model_data.get("copy_server_response", "false")
lines = ["// Generated by SwiftModelGenerator", "// Do not modify this file manually", ""]
@ -43,7 +44,7 @@ class SwiftModelGenerator:
lines.append("")
# Add SyncedStorable protocol requirements
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted))
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted, copy_server_response))
lines.append("")
# Properties
@ -371,7 +372,7 @@ class SwiftModelGenerator:
lines.append(" }")
return lines
def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str]) -> List[str]:
def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str], copy_server_response: str) -> List[str]:
"""Generate the static functions required by SyncedStorable protocol."""
# Convert HTTP methods to proper format
formatted_methods = [f".{method.lower()}" for method in token_exempted]
@ -380,6 +381,8 @@ class SwiftModelGenerator:
return [
f" public static func resourceName() -> String {{ return \"{resource_name}\" }}",
f" public static func tokenExemptedMethods() -> [HTTPMethod] {{ return [{methods_str}] }}",
f" public static var copyServerResponse: Bool = {copy_server_response}",
]
def _generate_relationships(self, model_name, properties: List[Dict[str, Any]]) -> List[str]:

@ -11,7 +11,7 @@ internal import Algorithms
import SwiftUI
@Observable
public final class GroupStage: BaseGroupStage, SideStorable {
final public class GroupStage: BaseGroupStage, SideStorable {
public var matchFormat: MatchFormat {
get {
@ -22,7 +22,7 @@ public final class GroupStage: BaseGroupStage, SideStorable {
}
}
var tournamentStore: TournamentStore? {
public var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
@ -65,11 +65,11 @@ public final class GroupStage: BaseGroupStage, SideStorable {
}
}
var computedOrder: Int {
public var computedOrder: Int {
index + step * 100
}
func isRunning() -> Bool { // at least a match has started
public func isRunning() -> Bool { // at least a match has started
_matches().anySatisfy({ $0.isRunning() })
}
@ -224,7 +224,7 @@ public final class GroupStage: BaseGroupStage, SideStorable {
}
}
// func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
// public func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
// guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
// let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
// if matches.isEmpty && nilIfEmpty { return nil }
@ -250,7 +250,7 @@ public final class GroupStage: BaseGroupStage, SideStorable {
matchIndexes.append(index)
}
}
return _matches().filter { matchIndexes.contains($0.index % matchCount) }
return _matches().filter { matchIndexes.contains($0.index%matchCount) }
}
public func initialStartDate(forTeam team: TeamRegistration) -> Date? {
@ -338,16 +338,16 @@ public final class GroupStage: BaseGroupStage, SideStorable {
}
func indexOf(_ matchIndex: Int) -> Int {
public func indexOf(_ matchIndex: Int) -> Int {
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex
}
func _matchUp(for matchIndex: Int) -> [Int] {
public func _matchUp(for matchIndex: Int) -> [Int] {
let combinations = Array((0..<size).combinations(ofCount: 2))
return combinations[safe: matchIndex%matchCount] ?? []
}
func returnMatchesSuffix(for matchIndex: Int) -> String {
public func returnMatchesSuffix(for matchIndex: Int) -> String {
if matchCount > 0 {
let count = _matches().count
if count > matchCount * 2 {
@ -362,7 +362,7 @@ public final class GroupStage: BaseGroupStage, SideStorable {
return ""
}
func localizedMatchUpLabel(for matchIndex: Int) -> String {
public func localizedMatchUpLabel(for matchIndex: Int) -> String {
let matchUp = _matchUp(for: matchIndex)
if let index = matchUp.first, let index2 = matchUp.last {
return "#\(index + 1) vs #\(index2 + 1)" + returnMatchesSuffix(for: matchIndex)
@ -371,11 +371,11 @@ public final class GroupStage: BaseGroupStage, SideStorable {
}
}
var matchCount: Int {
public var matchCount: Int {
(size * (size - 1)) / 2
}
func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
public func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
let _teams = _teams(for: matchIndex)
switch team {
case .one:
@ -455,7 +455,7 @@ public final class GroupStage: BaseGroupStage, SideStorable {
}
var scoreCache: [Int: TeamGroupStageScore] = [:]
public var scoreCache: [Int: TeamGroupStageScore] = [:]
public func computedScore(forTeam team: TeamRegistration, step: Int = 0) -> TeamGroupStageScore? {
guard let groupStagePositionAtStep = team.groupStagePositionAtStep(step) else {
@ -528,11 +528,11 @@ public final class GroupStage: BaseGroupStage, SideStorable {
}
// Clear the cache if necessary, for example when starting a new step or when matches update
func clearScoreCache() {
public func clearScoreCache() {
scoreCache.removeAll()
}
// func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
// public func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
// if sortedByScore {
// return unsortedTeams().compactMap({ team in
// scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!)
@ -622,8 +622,8 @@ public final class GroupStage: BaseGroupStage, SideStorable {
// step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0
// }
//
// func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
// public func encode(to encoder: Encoder) throws {
// public var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)
// try container.encode(storeId, forKey: ._storeId)

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
public final class Match: BaseMatch, SideStorable {
final public class Match: BaseMatch, SideStorable {
static func == (lhs: Match, rhs: Match) -> Bool {
lhs.id == rhs.id && lhs.startDate == rhs.startDate
@ -20,7 +20,7 @@ public final class Match: BaseMatch, SideStorable {
return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted()
}
var byeState: Bool = false
public var byeState: Bool = false
public init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) {
@ -88,7 +88,9 @@ defer {
#endif
if groupStage != nil {
return index
} else if let index = (matches ?? roundObject?.playedMatches().sorted(by: \.index))?.firstIndex(where: { $0.id == id }) {
} else if let matches, let index = matches.firstIndex(where: { $0.id == id }) {
return index
} else if let roundObject, roundObject.isUpperBracket(), let index = roundObject.playedMatches().firstIndex(where: { $0.id == id }) {
return index
}
return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
@ -278,7 +280,7 @@ defer {
return self.tournamentStore?.matches.first(where: { $0.round == roundObjectNextRound.id && $0.index == nextIndex })
}
func _toggleForwardMatchDisableState(_ state: Bool) {
public func _toggleForwardMatchDisableState(_ state: Bool) {
guard let roundObject else { return }
guard roundObject.parent != nil else { return }
guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return }
@ -320,7 +322,7 @@ defer {
// forwardMatch._toggleByeState(state)
}
func isSeededBy(team: TeamRegistration) -> Bool {
public func isSeededBy(team: TeamRegistration) -> Bool {
isSeededBy(team: team, inTeamPosition: .one) || isSeededBy(team: team, inTeamPosition: .two)
}
@ -337,36 +339,28 @@ defer {
public func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) {
//if disabled == state { return }
let tournamentStore = self.tournamentStore
let currentState = disabled
disabled = state
if disabled != currentState {
do {
try self.tournamentStore?.teamScores.delete(contentOfs: teamScores)
} catch {
Logger.error(error)
}
tournamentStore?.teamScores.delete(contentOfs: teamScores)
}
if state == true, state != currentState {
let teams = teams()
for team in teams {
if isSeededBy(team: team) {
team.bracketPosition = nil
self.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
}
}
}
//byeState = false
if state != currentState {
roundObject?._cachedSeedInterval = nil
roundObject?.invalidateCache()
name = nil
do {
try self.tournamentStore?.matches.addOrUpdate(instance: self)
} catch {
Logger.error(error)
}
tournamentStore?.matches.addOrUpdate(instance: self)
}
if single == false {
@ -413,15 +407,15 @@ defer {
[roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ")
}
func topPreviousRoundMatchIndex() -> Int {
public func topPreviousRoundMatchIndex() -> Int {
return index * 2 + 1
}
func bottomPreviousRoundMatchIndex() -> Int {
public func bottomPreviousRoundMatchIndex() -> Int {
return (index + 1) * 2
}
func topPreviousRoundMatch() -> Match? {
public func topPreviousRoundMatch() -> Match? {
guard let roundObject else { return nil }
let topPreviousRoundMatchIndex = topPreviousRoundMatchIndex()
let roundObjectPreviousRoundId = roundObject.previousRound()?.id
@ -430,7 +424,7 @@ defer {
})
}
func bottomPreviousRoundMatch() -> Match? {
public func bottomPreviousRoundMatch() -> Match? {
guard let roundObject else { return nil }
let bottomPreviousRoundMatchIndex = bottomPreviousRoundMatchIndex()
let roundObjectPreviousRoundId = roundObject.previousRound()?.id
@ -466,10 +460,10 @@ defer {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index)
}
guard let roundObject else { return index }
return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + indexInRound()
return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + RoundRule.matchIndexWithinRound(fromMatchIndex: index)
}
func previousMatches() -> [Match] {
public func previousMatches() -> [Match] {
guard let roundObject else { return [] }
guard let tournamentStore = self.tournamentStore else { return [] }
@ -613,7 +607,7 @@ defer {
}
}
func createTeamScores() -> [TeamScore] {
public func createTeamScores() -> [TeamScore] {
let teamOne = team(.one)
let teamTwo = team(.two)
let teams = [teamOne, teamTwo].compactMap({ $0 }).map { TeamScore(match: id, team: $0) }
@ -688,16 +682,16 @@ defer {
return currentTournament()?.courtCount ?? 1
}
func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool {
public func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool {
let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
return courtUsed.contains(courtIndex) == false
}
func courtIsPreferred(_ courtIndex: Int) -> Bool {
public func courtIsPreferred(_ courtIndex: Int) -> Bool {
return false
}
func allCourts() -> [Int] {
public func allCourts() -> [Int] {
let availableCourts = Array(0..<courtCount())
return availableCourts
}
@ -736,7 +730,7 @@ defer {
return startDate ?? .distantFuture
}
var computedEndDateForSorting: Date {
public var computedEndDateForSorting: Date {
return endDate ?? .distantFuture
}
@ -766,7 +760,7 @@ defer {
return round != nil
}
func walkoutTeam() -> [TeamRegistration] {
public func walkoutTeam() -> [TeamRegistration] {
//walkout 0 means real walkout, walkout 1 means lucky loser situation
return scores().filter({ $0.walkOut == 0 }).compactMap { $0.team }
}
@ -779,7 +773,7 @@ defer {
return groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject()
}
func tournamentId() -> String? {
public func tournamentId() -> String? {
return groupStageObject?.tournament ?? roundObject?.tournament
}
@ -807,7 +801,7 @@ defer {
// return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 }
}
func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? {
public func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? {
guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil }
var reverseValue = 1
if teamPosition == team(.two)?.groupStagePositionAtStep(step) {
@ -842,12 +836,12 @@ defer {
}
}
func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
public func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
guard let groupStageObject else { return nil }
return groupStageObject.team(teamPosition: team, inMatchIndex: index)
}
func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
public func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
guard let roundObject else { return nil }
let previousRound = roundObject.previousRound()
return roundObject.roundProjectedTeam(team, inMatch: self, previousRound: previousRound)
@ -995,7 +989,7 @@ defer {
public typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date)
func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] {
public func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] {
guard let tournament = currentTournament() else { return [] }
let startDate = Date().withoutSeconds()
if runningMatches.isEmpty {

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
public final class MatchScheduler: BaseMatchScheduler, SideStorable {
final public class MatchScheduler: BaseMatchScheduler, SideStorable {
//
// init(tournament: String,
// timeDifferenceLimit: Int = 5,
@ -63,7 +63,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
// TournamentStore.instance(tournamentId: self.tournament)
}
func tournamentObject() -> Tournament? {
public func tournamentObject() -> Tournament? {
return Store.main.findById(tournament)
}
@ -148,7 +148,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
return lastDate
}
func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
public func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
let _groupStages = groupStages
@ -272,7 +272,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
}
func rotationDifference(loserBracket: Bool) -> Int {
public func rotationDifference(loserBracket: Bool) -> Int {
if loserBracket {
return loserBracketRotationDifference
} else {
@ -280,7 +280,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
}
}
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
public func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
print("Evaluating match: \(match.roundAndMatchTitle()) in round: \(roundObject.roundTitle()) with index: \(match.index)")
if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate {
@ -372,11 +372,11 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
}
func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
public func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
}
func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] {
public func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] {
let byCourt = Dictionary(grouping: slots, by: { $0.courtIndex })
return (byCourt.keys.flatMap { courtIndex in
let matchesByCourt = byCourt[courtIndex]?.sorted(by: \.startDate)
@ -390,7 +390,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
)
}
func getAvailableCourts(from matches: [Match]) -> [(Int, Date)] {
public func getAvailableCourts(from matches: [Match]) -> [(Int, Date)] {
let validMatches = matches.filter({ $0.courtIndex != nil && $0.startDate != nil })
let byCourt = Dictionary(grouping: validMatches, by: { $0.courtIndex! })
return (byCourt.keys.flatMap { court in
@ -405,7 +405,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
)
}
func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
public func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
var slots = [TimeMatch]()
var _startDate: Date?
var rotationIndex = 0
@ -551,7 +551,7 @@ public final class MatchScheduler: BaseMatchScheduler, SideStorable {
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound)
}
func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date {
public func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date {
var matchPerRound = [String: Int]()
var minimumTargetedEndDate = rotationStartDate
@ -833,7 +833,7 @@ struct GroupStageTimeMatch {
let groupIndex: Int
}
struct TimeMatch {
public struct TimeMatch {
let matchID: String
let rotationIndex: Int
var courtIndex: Int
@ -841,7 +841,7 @@ struct TimeMatch {
var durationLeft: Int //in minutes
var minimumBreakTime: Int //in minutes
func estimatedEndDate(includeBreakTime: Bool) -> Date {
public func estimatedEndDate(includeBreakTime: Bool) -> Date {
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
return startDate.addingTimeInterval(minutesToAdd * 60.0)
}
@ -851,14 +851,14 @@ struct TimeMatch {
}
}
struct GroupStageMatchDispatcher {
public struct GroupStageMatchDispatcher {
let timedMatches: [GroupStageTimeMatch]
let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int
let groupLastRotation: [Int: Int]
}
struct MatchDispatcher {
public struct MatchDispatcher {
let timedMatches: [TimeMatch]
let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int
@ -866,7 +866,7 @@ struct MatchDispatcher {
}
extension Match {
func teamIds() -> [String] {
public func teamIds() -> [String] {
return teams().map { $0.id }
}
@ -878,7 +878,7 @@ extension Match {
matchUp().contains(id)
}
func matchUp() -> [String] {
public func matchUp() -> [String] {
guard let groupStageObject else {
return []
}

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
public final class MonthData: BaseMonthData {
final public class MonthData: BaseMonthData {
public init(monthKey: String) {
super.init()

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
public final class PlayerRegistration: BasePlayerRegistration, SideStorable {
final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
public func localizedSourceLabel() -> String {
switch source {
@ -26,7 +26,7 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
}
public init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false) {
public init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false, coach: Bool = false, captain: Bool = false, registeredOnline: Bool = false, timeToConfirm: Date? = nil, registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting, paymentId: String? = nil) {
super.init()
self.teamRegistration = teamRegistration
self.firstName = firstName
@ -46,6 +46,12 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable {
self.computedRank = computedRank
self.source = source
self.hasArrived = hasArrived
self.coach = coach
self.captain = captain
self.registeredOnline = registeredOnline
self.timeToConfirm = timeToConfirm
self.registrationStatus = registrationStatus
self.paymentId = paymentId
}
required init(from decoder: any Decoder) throws {
@ -89,7 +95,7 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable {
public func pasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator())
return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
}
@ -187,7 +193,7 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return "non classé" + (isMalePlayer() ? "" : "e")
}
}
public func setComputedRank(in tournament: Tournament) {
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000
switch tournament.tournamentCategory {
@ -212,11 +218,45 @@ public final class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
}
public func hasPaidOnline() -> Bool {
registrationStatus == .confirmed && paymentId != nil && paymentType == .creditCard
}
public func hasConfirmed() -> Bool {
registrationStatus == .confirmed
}
public func confirmRegistration() {
registrationStatus = .confirmed
}
public enum PlayerDataSource: Int, Codable {
case frenchFederation = 0
case beachPadel = 1
}
public enum RegistrationStatus: Int, Codable, CaseIterable, Identifiable {
case waiting = 0
case pending = 1
case confirmed = 2
case canceled = 3
public var id: Int { self.rawValue }
public func localizedRegistrationStatus() -> String {
switch self {
case .waiting:
return "En attente"
case .pending:
return "En cours"
case .confirmed:
return "Confirmé"
case .canceled:
return "Annulé"
}
}
}
public static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
switch playerRank {
case 0: return 0

@ -56,7 +56,7 @@ public class Purchase: BasePurchase {
// case expirationDate
// }
func isValid() -> Bool {
public func isValid() -> Bool {
guard self.revocationDate == nil else {
return false
}
@ -66,7 +66,7 @@ public class Purchase: BasePurchase {
return expiration > Date()
}
// func encode(to encoder: Encoder) throws {
// public func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(self.id, forKey: .id)

@ -10,10 +10,12 @@ import LeStorage
import SwiftUI
@Observable
public final class Round: BaseRound, SideStorable {
var _cachedSeedInterval: SeedInterval?
final public class Round: BaseRound, SideStorable {
private var _cachedSeedInterval: SeedInterval?
private var _cachedLoserRounds: [Round]?
private var _cachedLoserRoundsAndChildren: [Round]?
public init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) {
super.init(tournament: tournament, index: index, parent: parent, format: matchFormat, startDate: startDate, groupStageLoserBracket: groupStageLoserBracket, loserBracketMode: loserBracketMode)
@ -45,7 +47,16 @@ public final class Round: BaseRound, SideStorable {
public func tournamentObject() -> Tournament? {
return Store.main.findById(tournament)
}
public func _unsortedMatches(includeDisabled: Bool) -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
if includeDisabled {
return tournamentStore.matches.filter { $0.round == self.id }
} else {
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false }
}
}
public func _matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.round == self.id }.sorted(by: \.index)
@ -78,7 +89,20 @@ public final class Round: BaseRound, SideStorable {
return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false
}
}
public func upperMatches(upperRound: Round, match: Match) -> [Match] {
let matchIndex = match.index
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
return [upperRound.getMatch(atMatchIndexInRound: indexInRound * 2), upperRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 })
}
public func previousMatches(previousRound: Round, match: Match) -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter {
$0.round == previousRound.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex())
}
}
public func upperMatches(ofMatch match: Match) -> [Match] {
if parent != nil, previousRound() == nil, let parentRound {
let matchIndex = match.index
@ -164,12 +188,12 @@ public final class Round: BaseRound, SideStorable {
public func losers() -> [TeamRegistration] {
let teamIds: [String] = self._matches().compactMap { $0.losingTeamId }
let teamIds: [String] = self._unsortedMatches(includeDisabled: false).compactMap { $0.losingTeamId }
return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) }
}
public func winners() -> [TeamRegistration] {
let teamIds: [String] = self._matches().compactMap { $0.winningTeamId }
let teamIds: [String] = self._unsortedMatches(includeDisabled: false).compactMap { $0.winningTeamId }
return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) }
}
@ -302,10 +326,10 @@ defer {
public func enabledMatches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false }.sorted(by: \.index)
return tournamentStore.matches.filter { $0.disabled == false && $0.round == self.id }.sorted(by: \.index)
}
// func displayableMatches() -> [Match] {
// public func displayableMatches() -> [Match] {
//#if _DEBUG_TIME //DEBUGING TIME
// let start = Date()
// defer {
@ -353,17 +377,21 @@ defer {
public func loserRounds(forRoundIndex roundIndex: Int, loserRoundsAndChildren: [Round]) -> [Round] {
return loserRoundsAndChildren.filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount)
}
public func isEnabled() -> Bool {
return _unsortedMatches(includeDisabled: false).isEmpty == false
}
public func isDisabled() -> Bool {
return _matches().allSatisfy({ $0.disabled })
return _unsortedMatches(includeDisabled: true).allSatisfy({ $0.disabled })
}
public func isRankDisabled() -> Bool {
return _matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty })
return _unsortedMatches(includeDisabled: true).allSatisfy({ $0.disabled && $0.teamScores.isEmpty })
}
func resetFromRoundAllMatchesStartDate() {
_matches().forEach({
public func resetFromRoundAllMatchesStartDate() {
_unsortedMatches(includeDisabled: false).forEach({
$0.startDate = nil
})
loserRoundsAndChildren().forEach { round in
@ -372,8 +400,8 @@ defer {
nextRound()?.resetFromRoundAllMatchesStartDate()
}
func resetFromRoundAllMatchesStartDate(from match: Match) {
let matches = _matches()
public func resetFromRoundAllMatchesStartDate(from match: Match) {
let matches = _unsortedMatches(includeDisabled: false)
if let index = matches.firstIndex(where: { $0.id == match.id }) {
matches[index...].forEach { match in
match.startDate = nil
@ -386,10 +414,36 @@ defer {
}
public func getActiveLoserRound() -> Round? {
let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed()
return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first
// Get all loser rounds once
let allLoserRounds = loserRounds()
var lowestIndexRound: Round? = nil
let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index)
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount)
for currentIndex in 0..<roundCount { // Assuming no tournament has >100 rounds
// Find non-disabled round with current index
let roundAtIndex = allLoserRounds.first(where: { $0.index == currentIndex && $0.isEnabled() })
// No round at this index, we've checked all available rounds
if roundAtIndex == nil {
break
}
// Save the first non-disabled round we find (should be index 0)
if lowestIndexRound == nil {
lowestIndexRound = roundAtIndex
}
// If this round is active, return it immediately
if roundAtIndex!.hasStarted() && !roundAtIndex!.hasEnded() {
return roundAtIndex
}
}
// If no active round found, return the one with lowest index
return lowestIndexRound
}
public func enableRound() {
_toggleRound(disable: false)
}
@ -399,7 +453,7 @@ defer {
}
private func _toggleRound(disable: Bool) {
let _matches = _matches()
let _matches = _unsortedMatches(includeDisabled: true)
_matches.forEach { match in
match.disabled = disable
match.resetMatch()
@ -414,7 +468,7 @@ defer {
}
public var cumulativeMatchCount: Int {
var totalMatches = playedMatches().count
var totalMatches = _unsortedMatches(includeDisabled: false).count
if let parentRound {
totalMatches += parentRound.cumulativeMatchCount
}
@ -443,11 +497,12 @@ defer {
}
public func disabledMatches() -> [Match] {
return _matches().filter({ $0.disabled })
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true }
}
public func allLoserRoundMatches() -> [Match] {
loserRoundsAndChildren().flatMap({ $0._matches() })
loserRoundsAndChildren().flatMap({ $0._unsortedMatches(includeDisabled: false) })
}
public var theoryCumulativeMatchCount: Int {
@ -496,7 +551,7 @@ defer {
return seedInterval.localizedLabel(displayStyle)
}
func hasNextRound() -> Bool {
public func hasNextRound() -> Bool {
return nextRound()?.isRankDisabled() == false
}
@ -520,7 +575,7 @@ defer {
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func seedInterval(initialMode)", initialMode, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
print("func seedInterval(initialMode)", id, index, initialMode, _cachedSeedInterval, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
@ -560,15 +615,19 @@ defer {
if let previousRound = previousRound() {
if (previousRound.enabledMatches().isEmpty == false || initialMode) {
return previousRound.seedInterval(initialMode: initialMode)?.chunks()?.first
_cachedSeedInterval = previousRound.seedInterval(initialMode: initialMode)?.chunks()?.first
return _cachedSeedInterval
} else {
return previousRound.seedInterval(initialMode: initialMode)
_cachedSeedInterval = previousRound.seedInterval(initialMode: initialMode)
return _cachedSeedInterval
}
} else if let parentRound {
if parentRound.isUpperBracket() {
return parentRound.seedInterval(initialMode: initialMode)
_cachedSeedInterval = parentRound.seedInterval(initialMode: initialMode)
return _cachedSeedInterval
}
return parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last
_cachedSeedInterval = parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last
return _cachedSeedInterval
}
return nil
@ -599,18 +658,24 @@ defer {
tournamentObject?.updateTournamentState()
}
public func roundStatus() -> String {
let hasEnded = hasEnded()
if hasStarted() && hasEnded == false {
public func roundStatus(playedMatches: [Match]) -> String {
let hasEnded = playedMatches.anySatisfy({ $0.hasEnded() == false }) == false
let hasStarted = playedMatches.anySatisfy({ $0.hasStarted() })
if hasStarted && hasEnded == false {
return "en cours"
} else if hasEnded {
return "terminée"
} else if let tournamentObject = tournamentObject(), tournamentObject.groupStagesAreOver() == false {
return "en attente"
} else {
return "à démarrer"
}
}
public func loserRounds() -> [Round] {
if let _cachedLoserRounds {
return _cachedLoserRounds
}
guard let tournamentStore = self.tournamentStore else { return [] }
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
@ -620,12 +685,57 @@ defer {
}
#endif
return tournamentStore.rounds.filter( { $0.parent == id }).sorted(by: \.index).reversed()
// Filter first to reduce sorting work
let filteredRounds = tournamentStore.rounds.filter { $0.parent == id }
// Return empty array early if no rounds match
if filteredRounds.isEmpty {
return []
}
// Sort directly in descending order to avoid the separate reversed() call
_cachedLoserRounds = filteredRounds.sorted { $0.index > $1.index }
return _cachedLoserRounds!
}
public func loserRoundsAndChildren() -> [Round] {
let loserRounds = loserRounds()
return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() })
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func loserRoundsAndChildren: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
// Return cached result if available
if let cached = _cachedLoserRoundsAndChildren {
return cached
}
// Calculate result if cache is invalid or unavailable
let direct = loserRounds()
// Return quickly if there are no direct loser rounds
if direct.isEmpty {
// Update cache with empty result
_cachedLoserRoundsAndChildren = []
return []
}
// Pre-allocate capacity to avoid reallocations (estimate based on typical tournament structure)
var allRounds = direct
let estimatedChildrenCount = direct.count * 2 // Rough estimate
allRounds.reserveCapacity(estimatedChildrenCount)
// Collect all children rounds in one pass
for round in direct {
allRounds.append(contentsOf: round.loserRoundsAndChildren())
}
// Store result in cache
_cachedLoserRoundsAndChildren = allRounds
return allRounds
}
public func isUpperBracket() -> Bool {
@ -637,43 +747,106 @@ defer {
}
public func deleteLoserBracket() {
let loserRounds = loserRounds()
self.tournamentStore?.rounds.delete(contentOfs: loserRounds)
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func deleteLoserBracket: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
self.tournamentStore?.rounds.delete(contentOfs: self.loserRounds())
self.invalidateCache()
}
public func buildLoserBracket() {
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func buildLoserBracket: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard loserRounds().isEmpty else { return }
self.invalidateCache()
let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index)
guard currentRoundMatchCount > 1 else { return }
guard let tournamentStore else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount)
var loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat
let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat
// if let parentRound {
// loserBracketMatchFormat = tournamentObject()?.loserBracketSmartMatchFormat(parentRound.index)
// }
var titles = [String: String]()
let rounds = (0..<roundCount).map { //index 0 is the final
let round = Round(tournament: tournament, index: $0, matchFormat: loserBracketMatchFormat)
round.parent = id //parent
//titles[round.id] = round.roundTitle(initialMode: true)
return round
}
self.tournamentStore?.rounds.addOrUpdate(contentOfs: rounds)
tournamentStore.rounds.addOrUpdate(contentOfs: rounds)
let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount)
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, format: loserBracketMatchFormat, name: round.roundTitle(initialMode: true))
//let title = titles[round.id]
return Match(round: round.id, index: $0, format: loserBracketMatchFormat)
//initial mode let the roundTitle give a name without considering the playable match
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
tournamentStore.matches.addOrUpdate(contentOfs: matches)
loserRounds().forEach { round in
rounds.forEach { round in
round.buildLoserBracket()
}
}
public func disableUnplayedLoserBracketMatches() {
let m = self._matches()
if let previousRound = self.previousRound() {
m.forEach { match in
let prmc = previousRound.previousMatches(previousRound: previousRound, match: match).filter({
$0.disabled
}).count
let byeprmc = previousRound.previousMatches(previousRound: previousRound, match: match).filter({ $0.byeState }).count
if byeprmc == 2 {
match.disabled = false
} else {
if prmc == 2 {
match.disabled = true
if byeprmc == 1 {
match.byeState = true
}
} else if prmc == 1 {
match.byeState = true
match.disabled = true
} else {
match.disabled = false
}
}
}
} else if let upperRound = self.parentRound {
m.forEach { match in
let prmc = self.upperMatches(upperRound: upperRound, match: match).filter({ $0.disabled }).count
if prmc > 0 {
match.disabled = true
if upperRound.isUpperBracket(), prmc == 1 {
match.byeState = true
}
} else {
match.disabled = false
}
}
}
tournamentStore?.matches.addOrUpdate(contentOfs: m)
loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
public var parentRound: Round? {
guard let parent = parent else { return nil }
return self.tournamentStore?.rounds.findById(parent)
@ -704,12 +877,18 @@ defer {
}
public func updateMatchFormatOfAllMatches(_ updatedMatchFormat: MatchFormat) {
let playedMatches = _matches()
let playedMatches = _unsortedMatches(includeDisabled: true)
playedMatches.forEach { match in
match.matchFormat = updatedMatchFormat
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: playedMatches)
}
public func invalidateCache() {
_cachedLoserRounds = nil
_cachedSeedInterval = nil
_cachedLoserRoundsAndChildren = nil
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
let matches = self._matches()
@ -726,6 +905,7 @@ defer {
self.tournamentStore?.rounds.deleteDependencies(loserRounds, shouldBeSynchronized: shouldBeSynchronized)
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
@ -754,7 +934,7 @@ defer {
// loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
// }
//
// func encode(to encoder: Encoder) throws {
// public func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)
@ -773,7 +953,7 @@ defer {
func insertOnServer() {
self.tournamentStore?.rounds.writeChangeAndInsertOnServer(instance: self)
for match in self._matches() {
for match in self._unsortedMatches(includeDisabled: true) {
match.insertOnServer()
}
}

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
public final class TeamRegistration: BaseTeamRegistration, SideStorable {
final public class TeamRegistration: BaseTeamRegistration, SideStorable {
// static func resourceName() -> String { "team-registrations" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
@ -79,6 +79,20 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable {
players().anySatisfy({ $0.registeredOnline })
}
public func hasPaidOnline() -> Bool {
players().anySatisfy({ $0.hasPaidOnline() })
}
public func hasConfirmed() -> Bool {
players().allSatisfy({ $0.hasConfirmed() })
}
public func confirmRegistration() {
let players = players()
players.forEach({ $0.confirmRegistration() })
tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
}
public func unrankedOrUnknown() -> Bool {
players().anySatisfy({ $0.source == nil })
}
@ -129,7 +143,7 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized)
}
public func hasArrived(isHere: Bool = false) {
let unsortedPlayers = unsortedPlayers()
unsortedPlayers.forEach({ $0.hasArrived = !isHere })
@ -231,26 +245,26 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable {
return currentMatch() != nil
}
func currentMatch() -> Match? {
public func currentMatch() -> Match? {
return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
}
func teamScores() -> [TeamScore] {
public func teamScores() -> [TeamScore] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamScores.filter({ $0.teamRegistration == id })
}
func wins() -> [Match] {
public func wins() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.winningTeamId == id })
}
func loses() -> [Match] {
public func loses() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id })
}
func matches() -> [Match] {
public func matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id })
}
@ -264,7 +278,7 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable {
players().map { $0.canonicalName }.joined(separator: " ")
}
func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
public func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
guard let codeClubOrClubName else { return true }
return unsortedPlayers().anySatisfy({
$0.clubName?.contains(codeClubOrClubName) == true
@ -523,16 +537,25 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
}
func coaches() -> [PlayerRegistration] {
public func coaches() -> [PlayerRegistration] {
guard let store = self.tournamentStore else { return [] }
return store.playerRegistrations.filter { $0.coach }
}
func significantPlayerCount() -> Int {
public func setWeight(
from players: [PlayerRegistration],
inTournamentCategory tournamentCategory: TournamentCategory
) {
let significantPlayerCount = significantPlayerCount()
let sortedPlayers = players.sorted(by: \.computedRank, order: .ascending)
weight = (sortedPlayers.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
}
public func significantPlayerCount() -> Int {
return tournamentObject()?.significantPlayerCount() ?? 2
}
func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] {
public func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] {
let players = unsortedPlayers()
if players.count >= 2 { return [] }
let s = players.compactMap { $0.sex?.rawValue }
@ -545,7 +568,7 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable {
return missing
}
func unrankValue(for malePlayer: Bool) -> Int {
public func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000
}
@ -691,19 +714,6 @@ public final class TeamRegistration: BaseTeamRegistration, SideStorable {
playerRegistration.insertOnServer()
}
}
// MARK: - Refacto
public func setWeight(
from players: [PlayerRegistration],
inTournamentCategory tournamentCategory: TournamentCategory
) {
let significantPlayerCount = significantPlayerCount()
let sortedPlayers = players.sorted(by: \.computedRank, order: .ascending)
weight = (sortedPlayers.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
}
}

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
public final class TeamScore: BaseTeamScore, SideStorable {
final public class TeamScore: BaseTeamScore, SideStorable {
// static func resourceName() -> String { "team-scores" }
@ -68,7 +68,7 @@ public final class TeamScore: BaseTeamScore, SideStorable {
// MARK: - Computed dependencies
func matchObject() -> Match? {
public func matchObject() -> Match? {
return self.tournamentStore?.matches.findById(self.match)
}
@ -97,7 +97,7 @@ public final class TeamScore: BaseTeamScore, SideStorable {
// case _luckyLoser = "luckyLoser"
// }
//
// func encode(to encoder: Encoder) throws {
// public func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)

@ -10,14 +10,14 @@ import LeStorage
import SwiftUI
@Observable
public final class Tournament: BaseTournament {
final public class Tournament: BaseTournament {
//local variable
public var refreshInProgress: Bool = false
public var lastTeamRefresh: Date?
public var refreshRanking: Bool = false
func shouldRefreshTeams(forced: Bool) -> Bool {
public func shouldRefreshTeams(forced: Bool) -> Bool {
if forced {
return true
}
@ -27,98 +27,6 @@ public final class Tournament: BaseTournament {
@ObservationIgnored
public var navigationPath: [Screen] = []
// 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) {
// super.init()
// }
public init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, 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, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil,
umpireCustomMail: String? = nil,
umpireCustomContact: String? = nil,
umpireCustomPhone: String? = nil,
hideUmpireMail: Bool = false,
hideUmpirePhone: Bool = true,
disableRankingFederalRuling: Bool = false,
teamCountLimit: Bool = true
) {
super.init()
self.event = event
self.name = name
self.startDate = startDate
self.endDate = endDate
self.creationDate = creationDate
#if DEBUG
self.isPrivate = false
#else
self.isPrivate = isPrivate
#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.waitingListLimit = waitingListLimit
self.accountIsRequired = accountIsRequired
self.licenseIsRequired = licenseIsRequired
self.minimumPlayerPerTeam = minimumPlayerPerTeam
self.maximumPlayerPerTeam = maximumPlayerPerTeam
self.information = information
self.umpireCustomMail = umpireCustomMail
self.umpireCustomContact = umpireCustomContact
self.disableRankingFederalRuling = disableRankingFederalRuling
self.teamCountLimit = teamCountLimit
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
public var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.id)
@ -155,6 +63,7 @@ public final class Tournament: BaseTournament {
}
// MARK: - Computed Dependencies
public func unsortedTeams() -> [TeamRegistration] {
@ -208,7 +117,7 @@ public final class Tournament: BaseTournament {
return self.startDate
}
func canBePublished() -> Bool {
public func canBePublished() -> Bool {
switch state() {
case .build, .finished, .running:
return unsortedTeams().count > 3
@ -286,7 +195,7 @@ public final class Tournament: BaseTournament {
return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path)
}
func courtUsed(runningMatches: [Match]) -> [Int] {
public func courtUsed(runningMatches: [Match]) -> [Int] {
#if _DEBUGING_TIME //DEBUGING TIME
let start = Date()
defer {
@ -392,7 +301,7 @@ defer {
return seeds().filter { $0.isSeedable() }
}
func lastSeedRound() -> Int {
public func lastSeedRound() -> Int {
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
} else {
@ -400,16 +309,16 @@ defer {
}
}
func getRound(atRoundIndex roundIndex: Int) -> Round? {
public 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] {
public func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? []
}
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
public func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? []
}
public func availableSeedGroups(includeAll: Bool = false) -> [SeedInterval] {
@ -596,28 +505,55 @@ defer {
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
}
func matchesWithSpace() -> [Match] {
public func matchesWithSpace() -> [Match] {
getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? []
}
public 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
for round in rounds {
let playedMatches = round.playedMatches()
if withSeeds {
if round?.seeds().isEmpty == false {
// Optimization: If no matches have started in this round, return nil immediately
if !playedMatches.contains(where: { $0.hasStarted() }) {
return round
} else {
return nil
}
} else {
return round
if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) {
if withSeeds {
if !round.seeds().isEmpty {
return round
} else {
return nil
}
} else {
return round
}
}
}
return nil
}
public func getActiveRoundAndStatus() -> (Round, String)? {
let rounds: [Round] = self.rounds()
for round in rounds {
let playedMatches = round.playedMatches()
// Optimization: If no matches have started in this round, return nil immediately
if !playedMatches.contains(where: { $0.hasStarted() }) {
return (round, round.roundStatus(playedMatches: playedMatches))
}
if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) {
return (round, round.roundStatus(playedMatches: playedMatches))
}
}
return nil
}
public func getPlayedMatchDateIntervals(in event: Event) -> [DateInterval] {
let allMatches: [Match] = self.allMatches().filter { $0.courtIndex != nil && $0.startDate != nil }
return allMatches.map { match in
@ -634,7 +570,7 @@ defer {
return tournamentStore.matches.filter { $0.disabled == false }
}
func _allMatchesIncludingDisabled() -> [Match] {
public func _allMatchesIncludingDisabled() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return Array(tournamentStore.matches)
}
@ -642,7 +578,7 @@ defer {
public func rounds() -> [Round] {
guard let tournamentStore = self.tournamentStore else { return [] }
let rounds: [Round] = tournamentStore.rounds.filter { $0.isUpperBracket() }
return rounds.sorted(by: \.index).reversed()
return rounds.sorted { $0.index > $1.index }
}
public func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
@ -655,6 +591,7 @@ defer {
return waitingListTeams(in: teams, includingWalkOuts: false)
}
public func selectedSortedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
@ -765,7 +702,7 @@ defer {
public func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] {
var duplicates = [PlayerRegistration]()
Set(players.compactMap({ $0.licenceId })).forEach { licenceId in
let found = players.filter({ $0.licenceId == licenceId })
let found = players.filter({ $0.licenceId?.strippedLicense == licenceId.strippedLicense })
if found.count > 1 {
duplicates.append(found.first!)
}
@ -828,12 +765,6 @@ defer {
}
}
public func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? {
guard let seedIndex else { return nil }
return selectedSortedTeams()[safe: seedIndex]?.callDate
}
public func maximumCourtsPerGroupSage() -> Int {
if teamsPerGroupStage > 1 {
return min(teamsPerGroupStage / 2, courtCount)
@ -841,7 +772,7 @@ defer {
return max(1, courtCount)
}
}
public func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool {
guard let summonDate = team.callDate else { return true }
let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate
@ -900,6 +831,10 @@ defer {
return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
}
public func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? {
guard let seedIndex else { return nil }
return selectedSortedTeams()[safe: seedIndex]?.callDate
}
public static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
@ -1031,7 +966,7 @@ defer {
let groupStages = groupStages()
var baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified
if disableRankingFederalRuling == false {
if disableRankingFederalRuling == false, baseRank > 0 {
baseRank += qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - 1
}
let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 }))
@ -1096,6 +1031,19 @@ defer {
return rankings
}
public func refreshPointsEarned(assimilationLevel: TournamentLevel? = nil) {
guard let tournamentStore = self.tournamentStore else { return }
let tournamentLevel = assimilationLevel ?? tournamentLevel
let unsortedTeams = unsortedTeams()
unsortedTeams.forEach { team in
if let finalRanking = team.finalRanking {
team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: finalRanking - 1, count: teamCount)
}
}
tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
}
public func lockRegistration() {
closedRegistrationDate = Date()
@ -1119,6 +1067,17 @@ defer {
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public func updateWeights() {
let teams = self.unsortedTeams()
teams.forEach { team in
let players = team.unsortedPlayers()
players.forEach { $0.setComputedRank(in: self) }
team.setWeight(from: players, inTournamentCategory: tournamentCategory)
self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
}
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public func missingUnrankedValue() -> Bool {
return maleUnrankedValue == nil || femaleUnrankedValue == nil
}
@ -1136,9 +1095,9 @@ defer {
}
}
let displayStyleCategory = hideSenior ? .short : displayStyle
var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle)]
var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedCategoryLabel(displayStyle, ageCategory: federalAgeCategory)]
if displayStyle == .short {
levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedLabel(displayStyle)]
levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedCategoryLabel(displayStyle, ageCategory: federalAgeCategory)]
}
let array = levelCategory + [federalTournamentAge.localizedFederalAgeLabel(displayStyleCategory)]
let title: String = array.filter({ $0.isEmpty == false }).joined(separator: " ")
@ -1151,10 +1110,10 @@ defer {
public func localizedTournamentType() -> String {
switch tournamentLevel {
case .unlisted:
case .unlisted, .championship:
return tournamentLevel.localizedLevelLabel(.short)
default:
return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedLabel(.short)
return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedCategoryLabel(.short, ageCategory: federalAgeCategory)
}
}
@ -1185,6 +1144,7 @@ defer {
return groupStageCount * qualifiedPerGroupStage
}
public func availableQualifiedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
@ -1225,7 +1185,7 @@ defer {
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
}
func groupStageLoserBracketAreOver() -> Bool {
public func groupStageLoserBracketAreOver() -> Bool {
guard let groupStageLoserBracket = groupStageLoserBracket() else {
return true
}
@ -1337,8 +1297,8 @@ defer {
cut = TeamRegistration.TeamRange(availableSeeds.first, availableSeeds.last)
}
if let round = getActiveRound() {
return ([round.roundTitle(.short), round.roundStatus()].joined(separator: " ").lowercased(), description, cut)
if let roundAndStatus = getActiveRoundAndStatus() {
return ([roundAndStatus.0.roundTitle(.short), roundAndStatus.1].joined(separator: " ").lowercased(), description, cut)
} else {
return ("", description, nil)
}
@ -1353,15 +1313,16 @@ defer {
let cut : TeamRegistration.TeamRange? = isAnimation() ? nil : TeamRegistration.TeamRange(groupStageTeams.first, groupStageTeams.last)
let runningGroupStages = groupStages().filter({ $0.isRunning() })
if groupStagesAreOver() { return ("terminées", cut) }
let groupStages = groupStages()
let runningGroupStages = groupStages.filter({ $0.isRunning() })
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)
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)
}
@ -1393,6 +1354,23 @@ defer {
}
}
public func addEmptyTeamRegistration(_ count: Int) {
guard let tournamentStore = self.tournamentStore else { return }
let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
}
public func buildGroupStages() {
guard groupStages().isEmpty, let tournamentStore = self.tournamentStore else {
return
@ -1597,6 +1575,20 @@ defer {
}
}
public var tournamentCategory: TournamentCategory {
get {
federalCategory
}
set {
if federalCategory != newValue {
federalCategory = newValue
updateWeights()
} else {
federalCategory = newValue
}
}
}
public var tournamentLevel: TournamentLevel {
get {
federalLevelCategory
@ -1638,9 +1630,108 @@ defer {
return groupStageMatchFormat
}
}
public func initSettings(templateTournament: Tournament?) {
setupDefaultPrivateSettings(templateTournament: templateTournament)
setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings
if let templateTournament {
setupRegistrationSettings(templateTournament: templateTournament)
}
setupFederalSettings()
}
public 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
}
}
public 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
}
public func setupDefaultPrivateSettings(templateTournament: Tournament?) {
#if DEBUG
self.isPrivate = false
self.publishTeams = true
self.publishSummons = true
self.publishBrackets = true
self.publishGroupStages = true
self.publishRankings = true
self.publishTournament = true
#else
var shouldBePrivate = templateTournament?.isPrivate ?? true
if Guard.main.currentPlan == .monthlyUnlimited {
shouldBePrivate = false
} else if Guard.main.purchasedTransactions.isEmpty == false {
shouldBePrivate = false
}
self.isPrivate = shouldBePrivate
#endif
}
public func setupUmpireSettings(defaultTournament: Tournament? = nil) {
if let defaultTournament {
self.umpireCustomMail = defaultTournament.umpireCustomMail
self.umpireCustomPhone = defaultTournament.umpireCustomPhone
self.umpireCustomContact = defaultTournament.umpireCustomContact
self.hideUmpireMail = defaultTournament.hideUmpireMail
self.hideUmpirePhone = defaultTournament.hideUmpirePhone
self.disableRankingFederalRuling = defaultTournament.disableRankingFederalRuling
self.loserBracketMode = defaultTournament.loserBracketMode
} else {
let user = DataStore.shared.user
self.umpireCustomMail = user.umpireCustomMail
self.umpireCustomPhone = user.umpireCustomPhone
self.umpireCustomContact = user.umpireCustomContact
self.hideUmpireMail = user.hideUmpireMail
self.hideUmpirePhone = user.hideUmpirePhone
self.disableRankingFederalRuling = user.disableRankingFederalRuling
self.loserBracketMode = user.loserBracketMode
}
}
public func setupRegistrationSettings(templateTournament: Tournament) {
self.enableOnlineRegistration = templateTournament.enableOnlineRegistration
self.accountIsRequired = templateTournament.accountIsRequired
self.licenseIsRequired = templateTournament.licenseIsRequired
self.minimumPlayerPerTeam = templateTournament.minimumPlayerPerTeam
self.maximumPlayerPerTeam = templateTournament.maximumPlayerPerTeam
self.waitingListLimit = templateTournament.waitingListLimit
self.teamCountLimit = templateTournament.teamCountLimit
self.enableOnlinePayment = templateTournament.enableOnlinePayment
self.onlinePaymentIsMandatory = templateTournament.onlinePaymentIsMandatory
self.enableOnlinePaymentRefund = templateTournament.enableOnlinePaymentRefund
self.stripeAccountId = templateTournament.stripeAccountId
self.enableTimeToConfirm = templateTournament.enableTimeToConfirm
self.isCorporateTournament = templateTournament.isCorporateTournament
if self.registrationDateLimit == nil, templateTournament.registrationDateLimit != nil {
self.registrationDateLimit = startDate.truncateMinutesAndSeconds()
}
self.openingRegistrationDate = templateTournament.openingRegistrationDate != nil ? creationDate.truncateMinutesAndSeconds() : nil
self.refundDateLimit = templateTournament.enableOnlinePaymentRefund ? startDate.truncateMinutesAndSeconds() : nil
}
public func onlineRegistrationCanBeEnabled() -> Bool {
isAnimation() == false
true
// isAnimation() == false
}
public func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
@ -1662,7 +1753,7 @@ defer {
}
}
func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
public func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
tournamentLevel == build.level
&& tournamentCategory == build.category
&& federalTournamentAge == build.age
@ -1824,7 +1915,7 @@ defer {
}
public func allLoserRoundMatches() -> [Match] {
rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) }
rounds().flatMap { $0.allLoserRoundMatches() }
}
public func seedsCount() -> Int {
@ -1914,47 +2005,44 @@ defer {
}
public 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..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, format: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore?.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
await MainActor.run {
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()
let tournamentStore = self.tournamentStore
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, format: round.matchFormat)
if let nextRound, let followingMatch = tournamentStore?.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
return match
}
tournamentStore?.rounds.addOrUpdate(instance: round)
tournamentStore?.matches.addOrUpdate(contentOfs: matches)
if round.index < 5 {
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
return match
}
do {
try tournamentStore?.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore?.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
@ -2018,11 +2106,19 @@ defer {
// MARK: - Status
public func shouldTournamentBeOver() async -> Bool {
return false
if hasEnded() {
return true
}
if hasStarted() == false {
return false
}
if hasStarted(), self.startDate.timeIntervalSinceNow > -3600*24 {
return false
}
if tournamentStore?.store.fileCollectionsAllLoaded() == false {
return false
}
#if _DEBUGING_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -2057,6 +2153,10 @@ defer {
unsortedTeams().filter({ $0.hasRegisteredOnline() })
}
public func paidOnlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasPaidOnline() })
}
public func shouldWarnOnlineRegistrationUpdates() -> Bool {
enableOnlineRegistration && onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false
}
@ -2064,12 +2164,17 @@ defer {
public func refreshTeamList(forced: Bool) async {
guard StoreCenter.main.isAuthenticated else { return }
guard tournamentStore?.store.fileCollectionsAllLoaded() == true else { return }
guard shouldRefreshTeams(forced: forced), refreshInProgress == false, enableOnlineRegistration, hasEnded() == false else { return }
guard shouldRefreshTeams(forced: forced), refreshInProgress == false else { return }
if forced == false {
guard enableOnlineRegistration, hasEnded() == false else {
return
}
}
refreshInProgress = true
do {
// try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
// try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true)
// try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
//try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
refreshInProgress = false
lastTeamRefresh = Date()
} catch {
@ -2079,6 +2184,11 @@ defer {
}
}
public func mailSubject() -> String {
let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), clubName].compactMap({ $0 }).joined(separator: " | ")
return subject
}
// MARK: -
func insertOnServer() throws {
@ -2104,42 +2214,12 @@ defer {
}
// MARK: - Payments & Crypto
public enum PaymentError: Error {
case cantPayTournament
}
// MARK: - Refacto
public var tournamentCategory: TournamentCategory {
get {
federalCategory
}
set {
if federalCategory != newValue {
federalCategory = newValue
updateWeights()
} else {
federalCategory = newValue
}
}
}
func updateWeights() {
let teams = self.unsortedTeams()
teams.forEach { team in
let players = team.unsortedPlayers()
players.forEach { $0.setComputedRank(in: self) }
team.setWeight(from: players, inTournamentCategory: tournamentCategory)
self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
}
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
}
extension Bool {
@ -2151,7 +2231,7 @@ extension Bool {
return Int.random(in: (5...9))
}
}
static func decodeInt(_ int: Int) -> Bool {
public static func decodeInt(_ int: Int) -> Bool {
switch int {
case (0...4):
return true
@ -2197,6 +2277,17 @@ extension Bool {
// }
//}
public extension Tournament {
static func getTemplateTournament() -> Tournament? {
return DataStore.shared.tournaments.filter { $0.isTemplate && $0.isDeleted == false }.sorted(by: \.startDate, order: .descending).first
}
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)
}
}
/// Warning: if the enum has more than 10 cases, the payment algo is broken
public enum TournamentPayment: Int, CaseIterable {

@ -8,7 +8,7 @@
import Foundation
import LeStorage
class TournamentLibrary {
public class TournamentLibrary {
static let shared: TournamentLibrary = TournamentLibrary()

@ -30,8 +30,19 @@ public extension Array {
}
}
public extension Array where Element: Equatable {
/// Remove first collection element that is equal to the given `object` or `element`:
mutating func remove(elements: [Element]) {
elements.forEach {
if let index = firstIndex(of: $0) {
remove(at: index)
}
}
}
}
extension Array where Element: CustomStringConvertible {
public extension Array where Element: CustomStringConvertible {
func customJoined(separator: String, lastSeparator: String) -> String {
switch count {
case 0:
@ -48,7 +59,8 @@ extension Array where Element: CustomStringConvertible {
}
}
extension Dictionary where Key == Int, Value == [String] {
public extension Dictionary where Key == Int, Value == [String] {
mutating func setOrAppend(_ element: String?, at key: Int) {
// Check if the element is nil; do nothing if it is
guard let element = element else {

@ -0,0 +1,69 @@
//
// Color+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 27/03/2024.
//
import SwiftUI
import UIKit
public extension Color {
func variation(withHueOffset hueOffset: Double = 0, saturationFactor: Double = 0.4, brightnessFactor: Double = 0.8, opacity: Double = 0.5) -> Color {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
UIColor(self).getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
// Apply adjustments
hue += CGFloat(hueOffset)
saturation *= CGFloat(saturationFactor)
brightness *= CGFloat(brightnessFactor)
alpha *= CGFloat(opacity)
// Clamp values
hue = max(0, min(hue, 1))
saturation = max(0, min(saturation, 1))
brightness = max(0, min(brightness, 1))
alpha = max(0, min(alpha, 1))
return Color(hue: Double(hue), saturation: Double(saturation), brightness: Double(brightness), opacity: Double(alpha))
}
}
public extension UIColor {
/// Initialises NSColor from a hexadecimal string. Color is clear if string is invalid.
/// - Parameter fromHex: supported formats are "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA", with or without the # character
convenience init(fromHex:String) {
var r = 0, g = 0, b = 0, a = 255
let offset = fromHex.hasPrefix("#") ? 1 : 0
let ch = fromHex.map{$0}
switch(ch.count - offset) {
case 8:
a = 16 * (ch[offset+6].hexDigitValue ?? 0) + (ch[offset+7].hexDigitValue ?? 0)
fallthrough
case 6:
r = 16 * (ch[offset+0].hexDigitValue ?? 0) + (ch[offset+1].hexDigitValue ?? 0)
g = 16 * (ch[offset+2].hexDigitValue ?? 0) + (ch[offset+3].hexDigitValue ?? 0)
b = 16 * (ch[offset+4].hexDigitValue ?? 0) + (ch[offset+5].hexDigitValue ?? 0)
break
case 4:
a = 16 * (ch[offset+3].hexDigitValue ?? 0) + (ch[offset+3].hexDigitValue ?? 0)
fallthrough
case 3: // Three digit #0D3 is the same as six digit #00DD33
r = 16 * (ch[offset+0].hexDigitValue ?? 0) + (ch[offset+0].hexDigitValue ?? 0)
g = 16 * (ch[offset+1].hexDigitValue ?? 0) + (ch[offset+1].hexDigitValue ?? 0)
b = 16 * (ch[offset+2].hexDigitValue ?? 0) + (ch[offset+2].hexDigitValue ?? 0)
break
default:
a = 0
break
}
self.init(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: CGFloat(a)/255)
}
}

@ -6,6 +6,7 @@
//
import Foundation
public enum TimeOfDay {
case morning
case noon
@ -56,6 +57,10 @@ public extension Date {
formatted(.dateTime.weekday().day(.twoDigits).month().year())
}
var dateFormatted: String {
formatted(.dateTime.day(.twoDigits).month(.twoDigits).year(.twoDigits))
}
var monthYearFormatted: String {
formatted(.dateTime.month(.wide).year(.defaultDigits))
}
@ -110,7 +115,7 @@ public extension Date {
}
static var firstDayOfWeek = Calendar.current.firstWeekday
static var capitalizedFirstLettersOfWeekdays: [String] {
static var capitalizedFirstLettersOfWeekdays: [String] = {
let calendar = Calendar.current
// let weekdays = calendar.shortWeekdaySymbols
@ -129,9 +134,9 @@ public extension Date {
}
}
return weekdays.map { $0.capitalized }
}
}()
static var fullMonthNames: [String] {
static var fullMonthNames: [String] = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
@ -140,7 +145,7 @@ public extension Date {
let date = Calendar.current.date(from: DateComponents(year: 2000, month: month, day: 1))
return date.map { dateFormatter.string(from: $0) }
}
}
}()
var startOfMonth: Date {
Calendar.current.dateInterval(of: .month, for: self)!.start

@ -8,13 +8,13 @@
import Foundation
public extension NumberFormatter {
static var ordinal: NumberFormatter {
static var ordinal: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
return formatter
}
}()
static var standard: NumberFormatter {
static var standard: NumberFormatter = {
return NumberFormatter()
}
}()
}

@ -29,7 +29,6 @@ public extension Sequence {
}
public extension Sequence {
func concurrentForEach(
_ operation: @escaping (Element) async throws -> Void
) async throws {

@ -8,14 +8,14 @@
import Foundation
import CryptoKit
enum CryptoError: Error {
public enum CryptoError: Error {
case invalidUTF8
case cantConvertUTF8
case invalidBase64String
case nilSeal
}
extension Data {
public extension Data {
func encrypt(pass: String) throws -> Data {
let key = try self._createSymmetricKey(fromString: pass)
@ -44,3 +44,4 @@ extension Data {
}
}

@ -163,8 +163,50 @@ public extension String {
}
func licencesFound() -> [String] {
let matches = self.matches(of: /[1-9][0-9]{5,7}/)
return matches.map { String(self[$0.range]) }
// First try to find licenses with format: 5-8 digits followed by optional letter
let precisePattern = /[1-9][0-9]{5,7}[ ]?[A-Za-z]?/
let preciseMatches = self.matches(of: precisePattern)
let preciseResults = preciseMatches.map { String(self[$0.range]).trimmingCharacters(in: .whitespaces) }
// If we find potential licenses with the precise pattern
if !preciseResults.isEmpty {
// Filter to only include those with trailing letters
let licensesWithLetters = preciseResults.filter {
let lastChar = $0.last
return lastChar != nil && lastChar!.isLetter
}
print("🎫 Found \(preciseResults.count) potential licenses, filtering to \(licensesWithLetters.count) with trailing letters")
// If we have licenses with letters, validate them
if !licensesWithLetters.isEmpty {
let validLicenses = licensesWithLetters.filter { $0.isLicenseNumber }
// If we have valid licenses, return the numeric part of each
if !validLicenses.isEmpty {
let numericLicenses = validLicenses.map { license -> String in
// Extract just the numeric part (all characters except the last letter)
if let lastChar = license.last, lastChar.isLetter {
return String(license.dropLast())
}
return license
}
if numericLicenses.isEmpty == false {
print("🎫 Found valid licenses: \(validLicenses), returning numeric parts: \(numericLicenses)")
return numericLicenses
}
}
}
}
// Fallback to just number pattern if we didn't find good matches
let numberPattern = /[1-9][0-9]{5,7}/
let numberMatches = self.matches(of: numberPattern)
let numberResults = numberMatches.map { String(self[$0.range]) }
print("🎫 Falling back to number-only pattern, found: \(numberResults)")
return numberResults
}
}
@ -261,7 +303,7 @@ public extension String {
/// Formats the birthdate string into "DD/MM/YYYY".
/// - Returns: A formatted birthdate string, or the original string if parsing fails.
public func formattedAsBirthdate() -> String {
func formattedAsBirthdate() -> String {
if let parsedDate = self.parseAsBirthdate() {
let outputFormatter = DateFormatter()
outputFormatter.dateFormat = "dd/MM/yyyy" // Desired output format

@ -1,16 +0,0 @@
//
// Tournament+Extensions.swift
// PadelClubData
//
// Created by Laurent Morvillier on 17/04/2025.
//
import Foundation
extension Tournament {
public 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)
}
}

@ -9,13 +9,13 @@ import Foundation
public extension URL {
public static var savedDateFormatter: DateFormatter = {
static var savedDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "DD/MM/yyyy"
return df
}()
public static var importDateFormatter: DateFormatter = {
static var importDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "MM-yyyy"
return df
@ -89,7 +89,7 @@ public extension URL {
return nil
}
public func fftImportingMaleUnrankValue() -> Int? {
func fftImportingMaleUnrankValue() -> Int? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
@ -107,7 +107,7 @@ public extension URL {
return nil
}
public func fileModelIdentifier() -> String? {
func fileModelIdentifier() -> String? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
@ -125,7 +125,7 @@ public extension URL {
return nil
}
public func fftImportingUncomplete() -> Int? {
func fftImportingUncomplete() -> Int? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil

@ -8,6 +8,7 @@
import Foundation
import StoreKit
import LeStorage
import Combine
@available(iOS 15, *)
@objc public class Guard: NSObject {
@ -20,7 +21,7 @@ import LeStorage
var updateListenerTask: Task<Void, Never>? = nil
fileprivate let _freeTournaments: Int = 3
public let freeTournaments: Int = 3
override init() {
@ -34,6 +35,7 @@ import LeStorage
} catch {
Logger.error(error)
}
Logger.log("plan = \(String(describing: currentBestPurchase?.productId))")
}
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil)
@ -263,8 +265,8 @@ import LeStorage
return TournamentPayment.unlimited
case .fivePerMonth:
if let purchaseDate = self.currentBestPurchase?.purchaseDate {
let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > purchaseDate && $0.payment == .subscriptionUnit && $0.isCanceled == false }
if tournaments.count < StoreItem.five {
let count = DataStore.shared.subscriptionUnitlyPayedTournaments(after: purchaseDate)
if count < StoreItem.five {
return TournamentPayment.subscriptionUnit
}
}
@ -277,7 +279,7 @@ import LeStorage
fileprivate func _paymentWithoutSubscription() -> TournamentPayment? {
let freelyPayed: Int = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count
if freelyPayed < self._freeTournaments {
if freelyPayed < self.freeTournaments {
return TournamentPayment.free
}
let tournamentCreditCount: Int = self._purchasedTournamentCount()
@ -291,12 +293,14 @@ import LeStorage
public var remainingTournaments: Int {
let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == TournamentPayment.unit }.count
let tournamentCreditCount = self._purchasedTournamentCount()
// let notDeletedTournamentCount = DataStore.shared.tournaments.filter { $0.isDeleted == false }.count
// Logger.log("unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ")
return tournamentCreditCount - unitlyPayed
}
public var remainingFreeTournaments: Int {
let freelyPayed = DataStore.shared.tournaments.filter { $0.payment == TournamentPayment.free }.count
return self.freeTournaments - freelyPayed
}
func disconnect() {
let purchases = DataStore.shared.purchases
purchases.reset()
@ -305,20 +309,21 @@ import LeStorage
}
public struct PurchaseRow: Identifiable {
public var id: UInt64
public var name: String
public var item: StoreItem
public var quantity: Int?
public var expirationDate: Date?
public var remainingCount: Int? = nil
public init(id: UInt64, name: String, item: StoreItem, quantity: Int? = nil) {
public init(id: UInt64, name: String, item: StoreItem, quantity: Int? = nil, expirationDate: Date? = nil, remainingCount: Int? = nil) {
self.id = id
self.name = name
self.item = item
self.quantity = quantity
self.expirationDate = expirationDate
self.remainingCount = remainingCount
}
}
fileprivate extension StoreKit.Transaction {

@ -13,9 +13,9 @@ public enum StoreItem: String, Identifiable, CaseIterable {
case unit = "app.padelclub.tournament.unit"
#if DEBUG
static let five: Int = 2
public static let five: Int = 2
#else
static let five: Int = 5
public static let five: Int = 5
#endif
public var id: String { return self.rawValue }

@ -0,0 +1,141 @@
//
// ContactManager.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 19/09/2023.
//
import Foundation
import SwiftUI
import MessageUI
import LeStorage
public enum ContactManagerError: LocalizedError {
case mailFailed
case mailNotSent //no network no error
case messageFailed
case messageNotSent //no network no error
case calendarAccessDenied
case calendarEventSaveFailed
case noCalendarAvailable
case uncalledTeams([TeamRegistration])
public var localizedDescription: String {
switch self {
case .mailFailed:
return "Le mail n'a pas été envoyé"
case .mailNotSent:
return "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer."
case .messageFailed:
return "Le SMS n'a pas été envoyé"
case .messageNotSent:
return "Le SMS n'a pas été envoyé"
case .uncalledTeams(let array):
let verb = array.count > 1 ? "peuvent" : "peut"
return "Attention, \(array.count) équipe\(array.count.pluralSuffix) ne \(verb) pas être contacté par la méthode choisie"
case .calendarAccessDenied:
return "Padel Club n'a pas accès à votre calendrier"
case .calendarEventSaveFailed:
return "Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier"
case .noCalendarAvailable:
return "Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi"
}
}
public static func getNetworkErrorMessage(sentError: ContactManagerError?, networkMonitorConnected: Bool) -> String {
var errors: [String] = []
if networkMonitorConnected == false {
errors.append("L'appareil n'est pas connecté à internet.")
}
if let sentError {
errors.append(sentError.localizedDescription)
}
return errors.joined(separator: "\n")
}
}
public enum ContactType: Identifiable {
case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?)
case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?)
public var id: Int {
switch self {
case .message: return 0
case .mail: return 1
}
}
}
public extension ContactType {
static let defaultCustomMessage: String =
"""
Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me confirmer votre présence avec votre nom et de prévenir votre partenaire.
"""
static let defaultAvailablePaymentMethods: String = "Règlement possible par chèque ou espèces."
static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let clubName = tournament?.clubName ?? ""
var text = tournamentCustomMessage
let date = startDate ?? tournament?.startDate ?? Date()
if let tournament {
text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.title, hideSenior: true))
text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage)
}
text = text.replacingOccurrences(of: "#club", with: clubName)
text = text.replacingOccurrences(of: "#manche", with: roundLabel.lowercased())
text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))")
text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))")
let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament)
text = text.replacingOccurrences(of: "#signature", with: signature)
return text
}
static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, reSummon: Bool = false) -> String {
let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage
if useFullCustomMessage {
return callingCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel)
}
let date = startDate ?? tournament?.startDate ?? Date()
let clubName = tournament?.clubName ?? ""
let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament)
let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s"
var entryFeeMessage: String? {
(DataStore.shared.user.summonsDisplayEntryFee) ? tournament?.entryFeeMessage : nil
}
var linkMessage: String? {
if let tournament, tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString {
return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink)
} else {
return nil
}
}
var computedMessage: String {
[entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n")
}
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"
if let tournament {
return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)"
} else {
return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)"
}
}
}

@ -27,6 +27,7 @@ public enum SummoningDisplayContext {
}
public struct DeviceHelper {
public static func isBigScreen() -> Bool {
switch UIDevice.current.userInterfaceIdiom {
case .pad: // iPads

@ -13,7 +13,7 @@ public enum ExportFormat: Int, Identifiable, CaseIterable {
case rawText
case csv
var suffix: String {
public var suffix: String {
switch self {
case .rawText:
return "txt"
@ -31,7 +31,7 @@ public enum ExportFormat: Int, Identifiable, CaseIterable {
}
}
func newLineSeparator(_ count: Int = 1) -> String {
public func newLineSeparator(_ count: Int = 1) -> String {
return Array(repeating: "\n", count: count).joined()
}
}

@ -7,10 +7,10 @@
import Foundation
class NetworkManager {
static let shared: NetworkManager = NetworkManager()
public class NetworkManager {
public static let shared: NetworkManager = NetworkManager()
func removeRankingData(lastDateString: String, fileName: String) {
public func removeRankingData(lastDateString: String, fileName: String) {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
@ -35,7 +35,7 @@ class NetworkManager {
// }
// }
//
func formatDateForHTTPHeader(_ date: Date) -> String {
public func formatDateForHTTPHeader(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
formatter.locale = Locale(identifier: "en_US_POSIX")
@ -45,7 +45,7 @@ class NetworkManager {
}
@discardableResult
func downloadRankingData(lastDateString: String, fileName: String) async throws -> Int? {
public func downloadRankingData(lastDateString: String, fileName: String) async throws -> Int? {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
@ -93,7 +93,7 @@ class NetworkManager {
return nil
}
func checkFileCreationDate(filePath: String) throws -> Date? {
public func checkFileCreationDate(filePath: String) throws -> Date? {
let fileManager = FileManager.default
let attributes = try fileManager.attributesOfItem(atPath: filePath)
return attributes[.creationDate] as? Date

@ -7,7 +7,7 @@
import Foundation
enum NetworkManagerError: LocalizedError {
public enum NetworkManagerError: LocalizedError {
case maintenance
case fileNotYetAvailable
case mailFailed
@ -17,7 +17,7 @@ enum NetworkManagerError: LocalizedError {
case fileNotModified
case fileNotDownloaded(Int)
var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .maintenance:
return "Le site de la FFT est en maintenance"

@ -25,7 +25,7 @@ public class PListReader {
print("Failed to read plist file at path: \(plistPath)")
}
} else {
print("Plist file '\(plist)' not found in bundle")
print("Plist file 'Data.plist' not found in bundle")
}
return nil

@ -11,7 +11,7 @@ import LeStorage
public enum ManualPatch: String {
case disconnect
var id: String {
public var id: String {
return "padelclub.app.manual.patch.\(self.rawValue)"
}
}
@ -63,7 +63,7 @@ enum Patch: String, CaseIterable {
public class AutomaticPatcher {
static func applyAllWhenApplicable() {
public static func applyAllWhenApplicable() {
for patch in Patch.allCases {
self.patchIfPossible(patch)
}

@ -11,7 +11,7 @@ import LeStorage
public class SourceFileManager {
public static let shared = SourceFileManager()
init() {
public init() {
createDirectoryIfNeeded(directoryURL: rankingSourceDirectory)
#if targetEnvironment(simulator)
createDirectoryIfNeeded(directoryURL: anonymousSourceDirectory)
@ -21,7 +21,7 @@ public class SourceFileManager {
public let rankingSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "rankings")
public let anonymousSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "anonymous")
func createDirectoryIfNeeded(directoryURL: URL) {
public func createDirectoryIfNeeded(directoryURL: URL) {
let fileManager = FileManager.default
do {
// Check if the directory exists
@ -37,7 +37,7 @@ public class SourceFileManager {
}
}
var lastDataSource: String? {
public var lastDataSource: String? {
DataStore.shared.appSettings.lastDataSource
}
@ -54,14 +54,14 @@ public class SourceFileManager {
// }
}
func _removeAllData(fromDate current: Date) {
public func _removeAllData(fromDate current: Date) {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
files.forEach { fileName in
NetworkManager.shared.removeRankingData(lastDateString: lastStringDate, fileName: fileName)
}
}
actor SourceFileDownloadTracker {
var _downloadedFileStatus : Int? = nil
@ -134,7 +134,7 @@ public class SourceFileManager {
}
}
func monthsBetweenDates(startDateString: String, endDateString: String) -> [String] {
public func monthsBetweenDates(startDateString: String, endDateString: String) -> [String] {
let dateFormatter = URL.importDateFormatter
guard let startDate = dateFormatter.date(from: startDateString),
@ -207,7 +207,7 @@ public class SourceFileManager {
return allFiles(isManPlayer)
}
static func isDateAfterUrlImportDate(date: Date, dateString: String) -> Bool {
public static func isDateAfterUrlImportDate(date: Date, dateString: String) -> Bool {
guard let importDate = URL.importDateFormatter.date(from: dateString) else {
return false
}

@ -71,7 +71,7 @@ public enum PageLink: String, Identifiable, CaseIterable {
rawValue
}
var path: String {
public var path: String {
switch self {
case .matches:
return ""

@ -10,7 +10,6 @@ import SwiftUI
import Combine
public class MatchDescriptor: ObservableObject {
@Published public var matchFormat: MatchFormat
@Published public var setDescriptors: [SetDescriptor]
public var court: Int = 1
@ -22,29 +21,6 @@ public class MatchDescriptor: ObservableObject {
public let colorTeamOne: Color = .teal
public let colorTeamTwo: Color = .indigo
public init(match: Match? = nil) {
self.match = match
if let groupStage = match?.groupStageObject {
self.matchFormat = groupStage.matchFormat
self.setDescriptors = [SetDescriptor(setFormat: groupStage.matchFormat.setFormat)]
} else {
let format = match?.matchFormat ?? match?.currentTournament()?.matchFormat ?? .defaultFormatForMatchType(.groupStage)
self.matchFormat = format
self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)]
}
let teamOne = match?.team(.one)
let teamTwo = match?.team(.two)
self.teamLabelOne = teamOne?.teamLabel(.wide, twoLines: true) ?? ""
self.teamLabelTwo = teamTwo?.teamLabel(.wide, twoLines: true) ?? ""
if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score {
self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in
SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat)
})
}
}
public var teamOneSetupIsActive: Bool {
if hasEnded && showSetInputView == false && showTieBreakInputView == false {
return false
@ -95,6 +71,29 @@ public class MatchDescriptor: ObservableObject {
return setDescriptors.anySatisfy({ $0.showTieBreakInputView })
}
public init(match: Match? = nil) {
self.match = match
if let groupStage = match?.groupStageObject {
self.matchFormat = groupStage.matchFormat
self.setDescriptors = [SetDescriptor(setFormat: groupStage.matchFormat.setFormat)]
} else {
let format = match?.matchFormat ?? match?.currentTournament()?.matchFormat ?? .defaultFormatForMatchType(.groupStage)
self.matchFormat = format
self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)]
}
let teamOne = match?.team(.one)
let teamTwo = match?.team(.two)
self.teamLabelOne = teamOne?.teamLabel(.wide, twoLines: true) ?? ""
self.teamLabelTwo = teamTwo?.teamLabel(.wide, twoLines: true) ?? ""
if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score {
self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in
SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat)
})
}
}
public var teamOneScores: [String] {
setDescriptors.compactMap { $0.getValue(teamPosition: .one) }
}

@ -34,7 +34,6 @@ public protocol TournamentBuildHolder: Identifiable {
}
public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
public var uniqueId: String = Store.randomId()
public var id: String { uniqueId }
public let category: TournamentCategory
@ -55,8 +54,8 @@ public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identif
computedLabel(displayStyle)
}
var identifier: String {
level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel()
public var identifier: String {
level.localizedLevelLabel()+":"+category.localizedCategoryLabel(ageCategory: age)+":"+age.localizedFederalAgeLabel()
}
func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
@ -64,12 +63,12 @@ public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identif
return localizedLabel(displayStyle) + " " + localizedAge(displayStyle)
}
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle)
public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
level.localizedLevelLabel(displayStyle) + " " + category.localizedCategoryLabel(displayStyle, ageCategory: age)
}
func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String {
level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle)
public func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String {
level.localizedLevelLabel(displayStyle) + " " + category.localizedCategoryLabel(displayStyle, ageCategory: age)
}
func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String {
@ -77,7 +76,7 @@ public struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identif
}
}
public extension TournamentBuild {
extension TournamentBuild {
init?(category: String, level: String, age: FederalTournamentAge = .senior) {
guard let levelFound = TournamentLevel.allCases.first(where: { $0.localizedLevelLabel() == level }) else { return nil }
@ -104,7 +103,6 @@ public enum FederalTournamentType: String, Hashable, Codable, CaseIterable, Iden
case championnatParPaire = "L"
public var id: String { self.rawValue }
public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .tournoi:
@ -266,13 +264,13 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
case .unlisted:
return displayStyle == .title ? "Aucune" : ""
case .a11_12:
return "11/12 ans"
return "U12"
case .a13_14:
return "13/14 ans"
return "U14"
case .a15_16:
return "15/16 ans"
return "U16"
case .a17_18:
return "17/18 ans"
return "U18"
case .senior:
return displayStyle == .short ? "" : "Senior"
case .a45:
@ -307,6 +305,27 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
return age >= 55
}
}
public func isChildCategory() -> Bool {
switch self {
case .unlisted:
return false
case .a11_12:
return true
case .a13_14:
return true
case .a15_16:
return true
case .a17_18:
return true
case .senior:
return false
case .a45:
return false
case .a55:
return false
}
}
}
public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
@ -325,9 +344,9 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
self.init(rawValue: value)
}
public static var assimilationAllCases: [TournamentLevel] {
public static var assimilationAllCases: [TournamentLevel] = {
return [.p25, .p100, .p250, .p500, .p1000, .p1500, .p2000]
}
}()
public var entryFee: Double? {
switch self {
@ -404,6 +423,15 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
}
}
public func haveDeadlines() -> Bool {
switch self {
case .p500, .p1000, .p1500, .p2000:
return true
default:
return false
}
}
public func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge) -> Int {
switch self {
case .p25:
@ -504,6 +532,10 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
public var defaultTeamSortingType: TeamSortingType {
switch self {
case .championship:
return .inscriptionDate
case .unlisted:
return .inscriptionDate
case .p25, .p100, .p250:
return .inscriptionDate
default:
@ -555,7 +587,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
public var coachingIsAuthorized: Bool {
switch self {
case .p500, .p1000, .p1500, .p2000:
case .p500, .p1000, .p1500, .p2000, .championship:
return true
default:
return false
@ -564,6 +596,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
public func points(for rank: Int, count: Int) -> Int {
if self == .unlisted { return 0 }
if self == .championship { return 0 }
let points = points(for: count)
if rank >= points.count {
return points.last!
@ -913,26 +946,46 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab
}
}
public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
public func localizedCategoryLabel(_ displayStyle: DisplayStyle = .wide, ageCategory: FederalTournamentAge? = nil) -> String {
switch self {
case .unlisted:
return displayStyle == .title ? "Aucune" : ""
case .men:
switch displayStyle {
case .title:
if ageCategory?.isChildCategory() == true {
return "Garçons"
}
return "Hommes"
case .wide:
if ageCategory?.isChildCategory() == true {
return "Garçons"
}
return "Hommes"
case .short:
if ageCategory?.isChildCategory() == true {
return "G"
}
return "H"
}
case .women:
switch displayStyle {
case .title:
if ageCategory?.isChildCategory() == true {
return "Filles"
}
return "Dames"
case .wide:
if ageCategory?.isChildCategory() == true {
return "Filles"
}
return "Dames"
case .short:
if ageCategory?.isChildCategory() == true {
return "F"
}
return "D"
}
case .mix:
@ -959,35 +1012,6 @@ public enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiab
}
}
public enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable {
case all = -1
case male = 1
case female = 0
public var id: Int { rawValue }
public func icon() -> String {
switch self {
case .all:
return "Tous"
case .male:
return "Homme"
case .female:
return "Femme"
}
}
public var localizedPlayerLabel: String {
switch self {
case .female:
return "joueuse"
default:
return "joueur"
}
}
}
public enum GroupStageOrderingMode: Int, Hashable, Codable, CaseIterable, Identifiable {
case random
@ -1000,7 +1024,6 @@ public enum GroupStageOrderingMode: Int, Hashable, Codable, CaseIterable, Identi
}
public var id: Int { self.rawValue }
public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .random:
@ -1305,9 +1328,9 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
}
}
public static var allCases: [MatchFormat] {
public static var allCases: [MatchFormat] = {
[.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint]
}
}()
public func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition {
scoreTeamOne >= scoreTeamTwo ? .one : .two
@ -1384,7 +1407,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
}
}
var estimatedTimeWithBreak: Int {
public var estimatedTimeWithBreak: Int {
estimatedDuration + breakTime.breakTime
}
@ -1472,6 +1495,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
public var format: String {
shortFormat + (isFederal ? "" : " (non officiel)")
}
public var shortFormat: String {
switch self {
case .twoSets:
@ -1585,7 +1609,6 @@ public enum Format: Int, Hashable, Codable {
return "tie-break en 15"
}
}
public var isTiebreak: Bool {
switch self {
case .normal:
@ -1736,20 +1759,20 @@ public enum RoundRule {
}
}
static func teamsInFirstRound(forTeams teams: Int) -> Int {
public static func teamsInFirstRound(forTeams teams: Int) -> Int {
Int(pow(2.0, ceil(log2(Double(teams)))))
}
static func numberOfMatches(forTeams teams: Int) -> Int {
public static func numberOfMatches(forTeams teams: Int) -> Int {
teamsInFirstRound(forTeams: teams) - 1
}
static func numberOfRounds(forTeams teams: Int) -> Int {
public static func numberOfRounds(forTeams teams: Int) -> Int {
if teams == 0 { return 0 }
return Int(log2(Double(teamsInFirstRound(forTeams: teams))))
}
static func matchIndex(fromRoundIndex roundIndex: Int) -> Int {
public static func matchIndex(fromRoundIndex roundIndex: Int) -> Int {
guard roundIndex >= 0 else {
return -1 // Invalid round index
}
@ -1757,7 +1780,7 @@ public enum RoundRule {
return (1 << roundIndex) - 1
}
static func matchIndex(fromBracketPosition: Int) -> Int {
public static func matchIndex(fromBracketPosition: Int) -> Int {
roundIndex(fromMatchIndex: fromBracketPosition / 2) + fromBracketPosition%2
}

@ -8,7 +8,6 @@
import Foundation
public struct SeedInterval: Hashable, Comparable {
public let first: Int
public let last: Int

@ -9,7 +9,7 @@ import Foundation
import SwiftUI
import TipKit
protocol Selectable {
public protocol Selectable {
func selectionLabel(index: Int) -> String
func badgeValue() -> Int?
func badgeImage() -> Badge?
@ -19,7 +19,7 @@ protocol Selectable {
func associatedTip() -> (any Tip)?
}
extension Selectable {
public extension Selectable {
func associatedTip() -> (any Tip)? {
return nil
}
@ -71,7 +71,7 @@ struct SelectionTipViewModifier: ViewModifier {
}
}
extension View {
public extension View {
func selectableTipViewModifier(selectable: Selectable, action: @escaping () -> Void) -> some View {
modifier(SelectionTipViewModifier(selectable: selectable, action: action))
}

@ -17,11 +17,11 @@ public struct SetDescriptor: Identifiable, Equatable {
public var showSetInputView: Bool = true
public var showTieBreakInputView: Bool = false
public var isTeamOneSet: Bool {
public var isTeamOneSet: Bool {
return valueTeamOne != nil || tieBreakValueTeamOne != nil
}
public var hasEnded: Bool {
public var hasEnded: Bool {
if let valueTeamTwo, let valueTeamOne {
return setFormat.hasEnded(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else {
@ -29,7 +29,7 @@ public struct SetDescriptor: Identifiable, Equatable {
}
}
public var winner: TeamPosition? {
public var winner: TeamPosition? {
if let valueTeamTwo, let valueTeamOne, valueTeamOne != valueTeamTwo {
return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else {
@ -37,7 +37,7 @@ public struct SetDescriptor: Identifiable, Equatable {
}
}
public var shouldTieBreak: Bool {
public var shouldTieBreak: Bool {
setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0)
}
Loading…
Cancel
Save