Compare commits
No commits in common. 'main' and 'online_payment' have entirely different histories.
main
...
online_pay
@ -1,8 +0,0 @@ |
|||||||
## Padel Club |
|
||||||
|
|
||||||
This is the main directory of a Swift app that helps padel tournament organizers. |
|
||||||
The project is structured around three projects linked in the PadelClub.xcworkspace: |
|
||||||
- PadelClub: this one, which mostly contains the UI for the project |
|
||||||
- PadelClubData: the business logic for the app |
|
||||||
- LeStorage: a local storage with a synchronization layer |
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,111 @@ |
|||||||
|
// |
||||||
|
// AppSettings.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 26/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class AppSettings: MicroStorable { |
||||||
|
|
||||||
|
var lastDataSource: String? = nil |
||||||
|
var didCreateAccount: Bool = false |
||||||
|
var didRegisterAccount: Bool = false |
||||||
|
|
||||||
|
//search tournament stuff |
||||||
|
var tournamentCategories: Set<TournamentCategory.ID> |
||||||
|
var tournamentLevels: Set<TournamentLevel.ID> |
||||||
|
var tournamentAges: Set<FederalTournamentAge.ID> |
||||||
|
var tournamentTypes: Set<FederalTournamentType.ID> |
||||||
|
var startDate: Date |
||||||
|
var endDate: Date |
||||||
|
var city: String |
||||||
|
var distance: Double |
||||||
|
var sortingOption: String |
||||||
|
var nationalCup: Bool |
||||||
|
var dayDuration: Int? |
||||||
|
var dayPeriod: DayPeriod |
||||||
|
|
||||||
|
func lastDataSourceDate() -> Date? { |
||||||
|
guard let lastDataSource else { return nil } |
||||||
|
return URL.importDateFormatter.date(from: lastDataSource) |
||||||
|
} |
||||||
|
|
||||||
|
func localizedLastDataSource() -> String? { |
||||||
|
guard let lastDataSource else { return nil } |
||||||
|
guard let date = URL.importDateFormatter.date(from: lastDataSource) else { return nil } |
||||||
|
|
||||||
|
return date.monthYearFormatted |
||||||
|
} |
||||||
|
|
||||||
|
func resetSearch() { |
||||||
|
tournamentAges = Set() |
||||||
|
tournamentTypes = Set() |
||||||
|
tournamentLevels = Set() |
||||||
|
tournamentCategories = Set() |
||||||
|
city = "" |
||||||
|
distance = 30 |
||||||
|
startDate = Date() |
||||||
|
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||||
|
sortingOption = "dateDebut+asc" |
||||||
|
nationalCup = false |
||||||
|
dayDuration = nil |
||||||
|
dayPeriod = .all |
||||||
|
} |
||||||
|
|
||||||
|
required init() { |
||||||
|
tournamentAges = Set() |
||||||
|
tournamentTypes = Set() |
||||||
|
tournamentLevels = Set() |
||||||
|
tournamentCategories = Set() |
||||||
|
city = "" |
||||||
|
distance = 30 |
||||||
|
startDate = Date() |
||||||
|
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||||
|
sortingOption = "dateDebut+asc" |
||||||
|
nationalCup = false |
||||||
|
dayDuration = nil |
||||||
|
dayPeriod = .all |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
lastDataSource = try container.decodeIfPresent(String.self, forKey: ._lastDataSource) |
||||||
|
didCreateAccount = try container.decodeIfPresent(Bool.self, forKey: ._didCreateAccount) ?? false |
||||||
|
didRegisterAccount = try container.decodeIfPresent(Bool.self, forKey: ._didRegisterAccount) ?? false |
||||||
|
tournamentCategories = try container.decodeIfPresent(Set<TournamentCategory.ID>.self, forKey: ._tournamentCategories) ?? Set() |
||||||
|
tournamentLevels = try container.decodeIfPresent(Set<TournamentLevel.ID>.self, forKey: ._tournamentLevels) ?? Set() |
||||||
|
tournamentAges = try container.decodeIfPresent(Set<FederalTournamentAge.ID>.self, forKey: ._tournamentAges) ?? Set() |
||||||
|
tournamentTypes = try container.decodeIfPresent(Set<FederalTournamentType.ID>.self, forKey: ._tournamentTypes) ?? Set() |
||||||
|
startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date() |
||||||
|
endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||||
|
city = try container.decodeIfPresent(String.self, forKey: ._city) ?? "" |
||||||
|
distance = try container.decodeIfPresent(Double.self, forKey: ._distance) ?? 30 |
||||||
|
sortingOption = try container.decodeIfPresent(String.self, forKey: ._sortingOption) ?? "dateDebut+asc" |
||||||
|
nationalCup = try container.decodeIfPresent(Bool.self, forKey: ._nationalCup) ?? false |
||||||
|
dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) |
||||||
|
dayPeriod = try container.decodeIfPresent(DayPeriod.self, forKey: ._dayPeriod) ?? .all |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _lastDataSource = "lastDataSource" |
||||||
|
case _didCreateAccount = "didCreateAccount" |
||||||
|
case _didRegisterAccount = "didRegisterAccount" |
||||||
|
case _tournamentCategories = "tournamentCategories" |
||||||
|
case _tournamentLevels = "tournamentLevels" |
||||||
|
case _tournamentAges = "tournamentAges" |
||||||
|
case _tournamentTypes = "tournamentTypes" |
||||||
|
case _startDate = "startDate" |
||||||
|
case _endDate = "endDate" |
||||||
|
case _city = "city" |
||||||
|
case _distance = "distance" |
||||||
|
case _sortingOption = "sortingOption" |
||||||
|
case _nationalCup = "nationalCup" |
||||||
|
case _dayDuration = "dayDuration" |
||||||
|
case _dayPeriod = "dayPeriod" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,115 @@ |
|||||||
|
// |
||||||
|
// Club.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 02/02/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import SwiftUI |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class Club: BaseClub { |
||||||
|
|
||||||
|
func clubTitle(_ displayStyle: DisplayStyle = .wide) -> String { |
||||||
|
switch displayStyle { |
||||||
|
case .wide, .title: |
||||||
|
return name |
||||||
|
case .short: |
||||||
|
return acronym |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func shareURL() -> URL? { |
||||||
|
return URL(string: URLs.main.url.appending(path: "?club=\(id)").absoluteString.removingPercentEncoding!) |
||||||
|
} |
||||||
|
|
||||||
|
var customizedCourts: [Court] { |
||||||
|
DataStore.shared.courts.filter { $0.club == self.id }.sorted(by: \.index) |
||||||
|
} |
||||||
|
|
||||||
|
override func deleteDependencies() { |
||||||
|
let customizedCourts = self.customizedCourts |
||||||
|
for customizedCourt in customizedCourts { |
||||||
|
customizedCourt.deleteDependencies() |
||||||
|
} |
||||||
|
DataStore.shared.courts.deleteDependencies(customizedCourts) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
extension Club { |
||||||
|
var isValid: Bool { |
||||||
|
name.isEmpty == false && name.count > 3 |
||||||
|
} |
||||||
|
|
||||||
|
func automaticShortName() -> String { |
||||||
|
name.acronym() |
||||||
|
} |
||||||
|
|
||||||
|
enum AcronymMode: String, CaseIterable { |
||||||
|
case automatic = "Automatique" |
||||||
|
case custom = "Personalisée" |
||||||
|
} |
||||||
|
|
||||||
|
func shortNameMode() -> AcronymMode { |
||||||
|
(acronym.isEmpty || acronym == automaticShortName()) ? .automatic : .custom |
||||||
|
} |
||||||
|
|
||||||
|
func hasTenupId() -> Bool { |
||||||
|
code != nil |
||||||
|
} |
||||||
|
|
||||||
|
func federalLink() -> URL? { |
||||||
|
guard let code else { return nil } |
||||||
|
return URL(string: "https://tenup.fft.fr/club/\(code)") |
||||||
|
} |
||||||
|
|
||||||
|
func courtName(atIndex courtIndex: Int) -> String { |
||||||
|
courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex) |
||||||
|
} |
||||||
|
|
||||||
|
func courtNameIfAvailable(atIndex courtIndex: Int) -> String? { |
||||||
|
customizedCourts.first(where: { $0.index == courtIndex })?.name |
||||||
|
} |
||||||
|
|
||||||
|
func update(fromClub club: Club) { |
||||||
|
self.acronym = club.acronym |
||||||
|
self.name = club.name |
||||||
|
self.phone = club.phone |
||||||
|
self.code = club.code |
||||||
|
self.address = club.address |
||||||
|
self.city = club.city |
||||||
|
self.zipCode = club.zipCode |
||||||
|
self.latitude = club.latitude |
||||||
|
self.longitude = club.longitude |
||||||
|
} |
||||||
|
|
||||||
|
func hasBeenCreated(by creatorId: String?) -> Bool { |
||||||
|
return creatorId == creator || creator == nil || self.relatedUser == creatorId |
||||||
|
} |
||||||
|
|
||||||
|
func isFavorite() -> Bool { |
||||||
|
return DataStore.shared.user.clubs.contains(where: { $0 == self.id }) |
||||||
|
} |
||||||
|
|
||||||
|
static func findOrCreate(name: String, code: String?, city: String? = nil, zipCode: String? = nil) -> Club { |
||||||
|
|
||||||
|
/* |
||||||
|
|
||||||
|
identify a club : code, name, ?? |
||||||
|
|
||||||
|
*/ |
||||||
|
let club: Club? = DataStore.shared.clubs.first(where: { (code == nil && $0.name == name && $0.city == city && $0.zipCode == zipCode) || code != nil && $0.code == code }) |
||||||
|
|
||||||
|
if let club { |
||||||
|
return club |
||||||
|
} else { |
||||||
|
let club = Club(creator: StoreCenter.main.userId, name: name, acronym: name.acronym(), code: code, city: city, zipCode: zipCode) |
||||||
|
club.relatedUser = StoreCenter.main.userId |
||||||
|
return club |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
// |
||||||
|
// Court.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 23/04/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import SwiftUI |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class Court: BaseCourt { |
||||||
|
|
||||||
|
static func == (lhs: Court, rhs: Court) -> Bool { |
||||||
|
lhs.id == rhs.id |
||||||
|
} |
||||||
|
|
||||||
|
init(index: Int, club: String, name: String? = nil, exitAllowed: Bool = false, indoor: Bool = false) { |
||||||
|
|
||||||
|
super.init() |
||||||
|
|
||||||
|
self.index = index |
||||||
|
self.lastUpdate = Date() |
||||||
|
self.club = club |
||||||
|
self.name = name |
||||||
|
self.exitAllowed = exitAllowed |
||||||
|
self.indoor = indoor |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
func courtTitle() -> String { |
||||||
|
self.name ?? courtIndexTitle() |
||||||
|
} |
||||||
|
|
||||||
|
func courtIndexTitle() -> String { |
||||||
|
Self.courtIndexedTitle(atIndex: index) |
||||||
|
} |
||||||
|
|
||||||
|
static func courtIndexedTitle(atIndex index: Int) -> String { |
||||||
|
("Terrain #" + (index + 1).formatted()) |
||||||
|
} |
||||||
|
|
||||||
|
func clubObject() -> Club? { |
||||||
|
Store.main.findById(club) |
||||||
|
} |
||||||
|
|
||||||
|
override func deleteDependencies() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,316 @@ |
|||||||
|
// |
||||||
|
// User.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 21/02/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
enum UserRight: Int, Codable { |
||||||
|
case none = 0 |
||||||
|
case edition = 1 |
||||||
|
case creation = 2 |
||||||
|
} |
||||||
|
|
||||||
|
enum RegistrationPaymentMode: Int, Codable { |
||||||
|
case disabled = 0 |
||||||
|
case corporate = 1 |
||||||
|
case noFee = 2 |
||||||
|
case stripe = 3 |
||||||
|
|
||||||
|
static let stripeFixedFee = 0.25 // Fixed fee in euros |
||||||
|
static let stripePercentageFee = 0.014 // 1.4% |
||||||
|
|
||||||
|
func canEnableOnlinePayment() -> Bool { |
||||||
|
switch self { |
||||||
|
case .disabled: |
||||||
|
return false |
||||||
|
case .corporate: |
||||||
|
return true |
||||||
|
case .noFee: |
||||||
|
return true |
||||||
|
case .stripe: |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func requiresStripe() -> Bool { |
||||||
|
switch self { |
||||||
|
case .disabled: |
||||||
|
return false |
||||||
|
case .corporate: |
||||||
|
return true |
||||||
|
case .noFee: |
||||||
|
return true |
||||||
|
case .stripe: |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Observable |
||||||
|
class CustomUser: BaseCustomUser, UserBase { |
||||||
|
|
||||||
|
// static func resourceName() -> String { "users" } |
||||||
|
// static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] } |
||||||
|
// static func filterByStoreIdentifier() -> Bool { return false } |
||||||
|
// static var relationshipNames: [String] = [] |
||||||
|
// |
||||||
|
// public var id: String = Store.randomId() |
||||||
|
// var lastUpdate: Date |
||||||
|
// public var username: String |
||||||
|
// public var email: String |
||||||
|
// var clubs: [String] = [] |
||||||
|
// var umpireCode: String? |
||||||
|
// var licenceId: String? |
||||||
|
// var firstName: String |
||||||
|
// var lastName: String |
||||||
|
// var phone: String? |
||||||
|
// var country: String? |
||||||
|
// |
||||||
|
// var summonsMessageBody : String? = nil |
||||||
|
// var summonsMessageSignature: String? = nil |
||||||
|
// var summonsAvailablePaymentMethods: String? = nil |
||||||
|
// var summonsDisplayFormat: Bool = false |
||||||
|
// var summonsDisplayEntryFee: Bool = false |
||||||
|
// var summonsUseFullCustomMessage: Bool = false |
||||||
|
// var matchFormatsDefaultDuration: [MatchFormat: Int]? = nil |
||||||
|
// var bracketMatchFormatPreference: MatchFormat? |
||||||
|
// var groupStageMatchFormatPreference: MatchFormat? |
||||||
|
// var loserBracketMatchFormatPreference: MatchFormat? |
||||||
|
// var loserBracketMode: LoserBracketMode = .automatic |
||||||
|
// |
||||||
|
// var deviceId: String? |
||||||
|
|
||||||
|
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) |
||||||
|
|
||||||
|
|
||||||
|
// self.lastUpdate = Date() |
||||||
|
// self.username = username |
||||||
|
// self.firstName = firstName |
||||||
|
// self.lastName = lastName |
||||||
|
// self.email = email |
||||||
|
// self.phone = phone |
||||||
|
// self.country = country |
||||||
|
// self.loserBracketMode = loserBracketMode |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
public func uuid() throws -> UUID { |
||||||
|
if let uuid = UUID(uuidString: self.id) { |
||||||
|
return uuid |
||||||
|
} |
||||||
|
throw UUIDError.cantConvertString(string: self.id) |
||||||
|
} |
||||||
|
|
||||||
|
func currentPlayerData() -> ImportedPlayer? { |
||||||
|
guard let licenceId = self.licenceId?.strippedLicense else { return nil } |
||||||
|
let federalContext = PersistenceController.shared.localContainer.viewContext |
||||||
|
let fetchRequest = ImportedPlayer.fetchRequest() |
||||||
|
let predicate = NSPredicate(format: "license == %@", licenceId) |
||||||
|
fetchRequest.predicate = predicate |
||||||
|
return try? federalContext.fetch(fetchRequest).first |
||||||
|
} |
||||||
|
|
||||||
|
func defaultSignature(_ tournament: Tournament?) -> String { |
||||||
|
let fullName = tournament?.umpireCustomContact ?? fullName() |
||||||
|
return "Sportivement,\n\(fullName), votre JAP." |
||||||
|
} |
||||||
|
|
||||||
|
func fullName() -> String { |
||||||
|
[firstName, lastName].joined(separator: " ") |
||||||
|
} |
||||||
|
|
||||||
|
func hasTenupClubs() -> Bool { |
||||||
|
self.clubsObjects().filter({ $0.code != nil }).isEmpty == false |
||||||
|
} |
||||||
|
|
||||||
|
func hasFavoriteClubsAndCreatedClubs() -> Bool { |
||||||
|
clubsObjects(includeCreated: true).isEmpty == false |
||||||
|
} |
||||||
|
|
||||||
|
func setUserClub(_ userClub: Club) { |
||||||
|
self.clubs.insert(userClub.id, at: 0) |
||||||
|
} |
||||||
|
|
||||||
|
func clubsObjects(includeCreated: Bool = false) -> [Club] { |
||||||
|
return DataStore.shared.clubs.filter({ (includeCreated && $0.creator == id) || clubs.contains($0.id) }) |
||||||
|
} |
||||||
|
|
||||||
|
func createdClubsObjectsNotFavorite() -> [Club] { |
||||||
|
return DataStore.shared.clubs.filter({ ($0.creator == id) && clubs.contains($0.id) == false }) |
||||||
|
} |
||||||
|
|
||||||
|
func saveMatchFormatsDefaultDuration(_ matchFormat: MatchFormat, estimatedDuration: Int) { |
||||||
|
if estimatedDuration == matchFormat.defaultEstimatedDuration { |
||||||
|
matchFormatsDefaultDuration?.removeValue(forKey: matchFormat) |
||||||
|
} else { |
||||||
|
matchFormatsDefaultDuration = matchFormatsDefaultDuration ?? [MatchFormat: Int]() |
||||||
|
matchFormatsDefaultDuration?[matchFormat] = estimatedDuration |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func addClub(_ club: Club) { |
||||||
|
if !self.clubs.contains(where: { $0.id == club.id }) { |
||||||
|
self.clubs.append(club.id) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getSummonsMessageSignature() -> String? { |
||||||
|
if let summonsMessageSignature, summonsMessageSignature.isEmpty == false { |
||||||
|
return summonsMessageSignature |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func canEnableOnlinePayment() -> Bool { |
||||||
|
registrationPaymentMode.canEnableOnlinePayment() |
||||||
|
} |
||||||
|
// enum CodingKeys: String, CodingKey { |
||||||
|
// case _id = "id" |
||||||
|
// case _lastUpdate = "lastUpdate" |
||||||
|
// case _username = "username" |
||||||
|
// case _email = "email" |
||||||
|
// case _clubs = "clubs" |
||||||
|
// case _umpireCode = "umpireCode" |
||||||
|
// case _licenceId = "licenceId" |
||||||
|
// case _firstName = "firstName" |
||||||
|
// case _lastName = "lastName" |
||||||
|
// case _phone = "phone" |
||||||
|
// case _country = "country" |
||||||
|
// case _summonsMessageBody = "summonsMessageBody" |
||||||
|
// case _summonsMessageSignature = "summonsMessageSignature" |
||||||
|
// case _summonsAvailablePaymentMethods = "summonsAvailablePaymentMethods" |
||||||
|
// case _summonsDisplayFormat = "summonsDisplayFormat" |
||||||
|
// case _summonsDisplayEntryFee = "summonsDisplayEntryFee" |
||||||
|
// case _summonsUseFullCustomMessage = "summonsUseFullCustomMessage" |
||||||
|
// case _matchFormatsDefaultDuration = "matchFormatsDefaultDuration" |
||||||
|
// case _bracketMatchFormatPreference = "bracketMatchFormatPreference" |
||||||
|
// case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" |
||||||
|
// case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" |
||||||
|
// case _deviceId = "deviceId" |
||||||
|
// case _loserBracketMode = "loserBracketMode" |
||||||
|
// } |
||||||
|
// |
||||||
|
// public required init(from decoder: Decoder) throws { |
||||||
|
// let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
// |
||||||
|
// // Required properties |
||||||
|
// id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
// lastUpdate = try container.decodeIfPresent(Date.self, forKey: ._lastUpdate) ?? Date() |
||||||
|
// username = try container.decode(String.self, forKey: ._username) |
||||||
|
// email = try container.decode(String.self, forKey: ._email) |
||||||
|
// firstName = try container.decode(String.self, forKey: ._firstName) |
||||||
|
// lastName = try container.decode(String.self, forKey: ._lastName) |
||||||
|
// |
||||||
|
// // Optional properties |
||||||
|
// clubs = try container.decodeIfPresent([String].self, forKey: ._clubs) ?? [] |
||||||
|
// umpireCode = try container.decodeIfPresent(String.self, forKey: ._umpireCode) |
||||||
|
// licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId) |
||||||
|
// phone = try container.decodeIfPresent(String.self, forKey: ._phone) |
||||||
|
// country = try container.decodeIfPresent(String.self, forKey: ._country) |
||||||
|
// |
||||||
|
// // Summons-related properties |
||||||
|
// summonsMessageBody = try container.decodeIfPresent(String.self, forKey: ._summonsMessageBody) |
||||||
|
// summonsMessageSignature = try container.decodeIfPresent(String.self, forKey: ._summonsMessageSignature) |
||||||
|
// summonsAvailablePaymentMethods = try container.decodeIfPresent(String.self, forKey: ._summonsAvailablePaymentMethods) |
||||||
|
// summonsDisplayFormat = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayFormat) ?? false |
||||||
|
// summonsDisplayEntryFee = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayEntryFee) ?? false |
||||||
|
// summonsUseFullCustomMessage = try container.decodeIfPresent(Bool.self, forKey: ._summonsUseFullCustomMessage) ?? false |
||||||
|
// |
||||||
|
// // Match-related properties |
||||||
|
// matchFormatsDefaultDuration = try container.decodeIfPresent([MatchFormat: Int].self, forKey: ._matchFormatsDefaultDuration) |
||||||
|
// bracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._bracketMatchFormatPreference) |
||||||
|
// groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference) |
||||||
|
// loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) |
||||||
|
// loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic |
||||||
|
// } |
||||||
|
// |
||||||
|
// func encode(to encoder: Encoder) throws { |
||||||
|
// var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
// |
||||||
|
// try container.encode(id, forKey: ._id) |
||||||
|
// try container.encode(lastUpdate, forKey: ._lastUpdate) |
||||||
|
// try container.encode(username, forKey: ._username) |
||||||
|
// try container.encode(email, forKey: ._email) |
||||||
|
// try container.encode(clubs, forKey: ._clubs) |
||||||
|
// |
||||||
|
// try container.encode(umpireCode, forKey: ._umpireCode) |
||||||
|
// try container.encode(licenceId, forKey: ._licenceId) |
||||||
|
// try container.encode(firstName, forKey: ._firstName) |
||||||
|
// try container.encode(lastName, forKey: ._lastName) |
||||||
|
// try container.encode(phone, forKey: ._phone) |
||||||
|
// try container.encode(country, forKey: ._country) |
||||||
|
// try container.encode(summonsMessageBody, forKey: ._summonsMessageBody) |
||||||
|
// try container.encode(summonsMessageSignature, forKey: ._summonsMessageSignature) |
||||||
|
// try container.encode(summonsAvailablePaymentMethods, forKey: ._summonsAvailablePaymentMethods) |
||||||
|
// try container.encode(summonsDisplayFormat, forKey: ._summonsDisplayFormat) |
||||||
|
// try container.encode(summonsDisplayEntryFee, forKey: ._summonsDisplayEntryFee) |
||||||
|
// try container.encode(summonsUseFullCustomMessage, forKey: ._summonsUseFullCustomMessage) |
||||||
|
// |
||||||
|
// try container.encode(matchFormatsDefaultDuration, forKey: ._matchFormatsDefaultDuration) |
||||||
|
// try container.encode(bracketMatchFormatPreference, forKey: ._bracketMatchFormatPreference) |
||||||
|
// try container.encode(groupStageMatchFormatPreference, forKey: ._groupStageMatchFormatPreference) |
||||||
|
// try container.encode(loserBracketMatchFormatPreference, forKey: ._loserBracketMatchFormatPreference) |
||||||
|
// try container.encode(deviceId, forKey: ._deviceId) |
||||||
|
// |
||||||
|
// try container.encode(loserBracketMode, forKey: ._loserBracketMode) |
||||||
|
// } |
||||||
|
|
||||||
|
static func placeHolder() -> CustomUser { |
||||||
|
return CustomUser(username: "", email: "", firstName: "", lastName: "", phone: nil, country: nil) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
class UserCreationForm: CustomUser, UserPasswordBase { |
||||||
|
|
||||||
|
init(user: CustomUser, username: String, password: String, firstName: String, lastName: String, email: String, phone: String?, country: String?) { |
||||||
|
self.password = password |
||||||
|
super.init(username: username, email: email, firstName: firstName, lastName: lastName, phone: phone, country: country) |
||||||
|
|
||||||
|
self.summonsMessageBody = user.summonsMessageBody |
||||||
|
self.summonsMessageSignature = user.summonsMessageSignature |
||||||
|
self.summonsAvailablePaymentMethods = user.summonsAvailablePaymentMethods |
||||||
|
self.summonsDisplayFormat = user.summonsDisplayFormat |
||||||
|
self.summonsDisplayEntryFee = user.summonsDisplayEntryFee |
||||||
|
self.summonsUseFullCustomMessage = user.summonsUseFullCustomMessage |
||||||
|
self.matchFormatsDefaultDuration = user.matchFormatsDefaultDuration |
||||||
|
self.bracketMatchFormatPreference = user.bracketMatchFormatPreference |
||||||
|
self.groupStageMatchFormatPreference = user.groupStageMatchFormatPreference |
||||||
|
self.loserBracketMatchFormatPreference = user.loserBracketMatchFormatPreference |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
fatalError("init(from:) has not been implemented") |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
self.password = "" |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
public var password: String |
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey { |
||||||
|
case password |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
try super.encode(to: encoder) |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.password, forKey: .password) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,395 @@ |
|||||||
|
// |
||||||
|
// DataStore.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 02/02/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
class DataStore: ObservableObject { |
||||||
|
|
||||||
|
static let shared = DataStore() |
||||||
|
|
||||||
|
@Published var user: CustomUser = CustomUser.placeHolder() { |
||||||
|
didSet { |
||||||
|
let loggedUser = StoreCenter.main.isAuthenticated |
||||||
|
|
||||||
|
if loggedUser { |
||||||
|
if self.user.id != self.userStorage.item()?.id { |
||||||
|
self.userStorage.setItemNoSync(self.user) |
||||||
|
StoreCenter.main.initialSynchronization(clear: false) |
||||||
|
self._fixMissingClubCreatorIfNecessary(self.clubs) |
||||||
|
self._fixMissingEventCreatorIfNecessary(self.events) |
||||||
|
} |
||||||
|
} else { |
||||||
|
self._temporaryLocalUser.item = self.user |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fileprivate(set) var tournaments: SyncedCollection<Tournament> |
||||||
|
fileprivate(set) var clubs: SyncedCollection<Club> |
||||||
|
fileprivate(set) var courts: SyncedCollection<Court> |
||||||
|
fileprivate(set) var events: SyncedCollection<Event> |
||||||
|
fileprivate(set) var monthData: StoredCollection<MonthData> |
||||||
|
fileprivate(set) var dateIntervals: SyncedCollection<DateInterval> |
||||||
|
fileprivate(set) var purchases: SyncedCollection<Purchase> |
||||||
|
|
||||||
|
var userStorage: StoredSingleton<CustomUser> |
||||||
|
|
||||||
|
fileprivate var _temporaryLocalUser: OptionalStorage<CustomUser> = OptionalStorage(fileName: "tmp_local_user.json") |
||||||
|
fileprivate(set) var appSettingsStorage: MicroStorage<AppSettings> = MicroStorage(fileName: "appsettings.json") |
||||||
|
|
||||||
|
var appSettings: AppSettings { |
||||||
|
appSettingsStorage.item |
||||||
|
} |
||||||
|
|
||||||
|
init() { |
||||||
|
let store = Store.main |
||||||
|
StoreCenter.main.blackListUserName("apple-test") |
||||||
|
|
||||||
|
// let secureScheme = true |
||||||
|
let domain: String = URLs.activationHost.rawValue |
||||||
|
|
||||||
|
#if DEBUG |
||||||
|
if let secure = PListReader.readBool(plist: "local", key: "secure_server"), |
||||||
|
let domain = PListReader.readString(plist: "local", key: "server_domain") { |
||||||
|
StoreCenter.main.configureURLs(secureScheme: secure, domain: domain) |
||||||
|
} else { |
||||||
|
StoreCenter.main.configureURLs(secureScheme: true, domain: domain) |
||||||
|
} |
||||||
|
#else |
||||||
|
StoreCenter.main.configureURLs(secureScheme: true, domain: domain) |
||||||
|
#endif |
||||||
|
|
||||||
|
StoreCenter.main.logsFailedAPICalls() |
||||||
|
|
||||||
|
var synchronized: Bool = true |
||||||
|
|
||||||
|
#if DEBUG |
||||||
|
if let sync = PListReader.readBool(plist: "local", key: "synchronized") { |
||||||
|
synchronized = sync |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
StoreCenter.main.forceNoSynchronization = !synchronized |
||||||
|
|
||||||
|
|
||||||
|
let indexed: Bool = true |
||||||
|
self.clubs = store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.courts = store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.tournaments = store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.events = store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.dateIntervals = store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.userStorage = store.registerObject(synchronized: synchronized) |
||||||
|
self.purchases = Store.main.registerSynchronizedCollection(inMemory: true) |
||||||
|
|
||||||
|
self.monthData = store.registerCollection(indexed: indexed) |
||||||
|
|
||||||
|
// Load ApiCallCollection, making them restart at launch and deletable on disconnect |
||||||
|
StoreCenter.main.loadApiCallCollection(type: GroupStage.self) |
||||||
|
StoreCenter.main.loadApiCallCollection(type: Round.self) |
||||||
|
StoreCenter.main.loadApiCallCollection(type: PlayerRegistration.self) |
||||||
|
StoreCenter.main.loadApiCallCollection(type: TeamRegistration.self) |
||||||
|
StoreCenter.main.loadApiCallCollection(type: Match.self) |
||||||
|
StoreCenter.main.loadApiCallCollection(type: TeamScore.self) |
||||||
|
StoreCenter.main.loadApiCallCollection(type: DrawLog.self) |
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil) |
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidUpdate), name: NSNotification.Name.CollectionDidChange, object: nil) |
||||||
|
NotificationCenter.default.addObserver( |
||||||
|
self, |
||||||
|
selector: #selector(_willEnterForegroundNotification), |
||||||
|
name: UIScene.willEnterForegroundNotification, |
||||||
|
object: nil) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
deinit { |
||||||
|
NotificationCenter.default.removeObserver(self) |
||||||
|
} |
||||||
|
|
||||||
|
func saveUser() { |
||||||
|
if user.username.count > 0 { |
||||||
|
self.userStorage.update() |
||||||
|
} else { |
||||||
|
self._temporaryLocalUser.item = self.user |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@objc func collectionDidLoad(notification: Notification) { |
||||||
|
|
||||||
|
if let userSingleton: StoredSingleton<CustomUser> = notification.object as? StoredSingleton<CustomUser> { |
||||||
|
self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder() |
||||||
|
} else if let clubsCollection: SyncedCollection<Club> = notification.object as? SyncedCollection<Club> { |
||||||
|
self._fixMissingClubCreatorIfNecessary(clubsCollection) |
||||||
|
} else if let eventsCollection: SyncedCollection<Event> = notification.object as? SyncedCollection<Event> { |
||||||
|
self._fixMissingEventCreatorIfNecessary(eventsCollection) |
||||||
|
} |
||||||
|
|
||||||
|
if Store.main.fileCollectionsAllLoaded() { |
||||||
|
AutomaticPatcher.applyAllWhenApplicable() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
fileprivate func _fixMissingClubCreatorIfNecessary(_ clubsCollection: SyncedCollection<Club>) { |
||||||
|
for club in clubsCollection { |
||||||
|
if let userId = StoreCenter.main.userId, club.creator == nil { |
||||||
|
club.creator = userId |
||||||
|
self.userStorage.item()?.addClub(club) |
||||||
|
self.userStorage.update() |
||||||
|
clubsCollection.writeChangeAndInsertOnServer(instance: club) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fileprivate func _fixMissingEventCreatorIfNecessary(_ eventsCollection: SyncedCollection<Event>) { |
||||||
|
for event in eventsCollection { |
||||||
|
if let userId = StoreCenter.main.userId, event.creator == nil { |
||||||
|
event.creator = userId |
||||||
|
do { |
||||||
|
try event.insertOnServer() |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@objc func collectionDidUpdate(notification: Notification) { |
||||||
|
self.objectWillChange.send() |
||||||
|
} |
||||||
|
|
||||||
|
@objc func _willEnterForegroundNotification() { |
||||||
|
Task { |
||||||
|
try await self.purchases.loadDataFromServerIfAllowed(clear: true) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func disconnect() { |
||||||
|
|
||||||
|
Task { |
||||||
|
if await StoreCenter.main.hasPendingAPICalls() { |
||||||
|
// todo qu'est ce qu'on fait des API Call ? |
||||||
|
} |
||||||
|
|
||||||
|
do { |
||||||
|
let services = try StoreCenter.main.service() |
||||||
|
try await services.logout() |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
|
||||||
|
DispatchQueue.main.async { |
||||||
|
self._localDisconnect() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func deleteAccount() { |
||||||
|
|
||||||
|
Task { |
||||||
|
do { |
||||||
|
let services = try StoreCenter.main.service() |
||||||
|
try await services.deleteAccount() |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
|
||||||
|
DispatchQueue.main.async { |
||||||
|
self._localDisconnect() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func deleteTournament(_ tournament: Tournament) { |
||||||
|
let event = tournament.eventObject() |
||||||
|
let isLastTournament = event?.tournaments.count == 1 |
||||||
|
self.tournaments.delete(instance: tournament) |
||||||
|
if let event, isLastTournament { |
||||||
|
self.events.delete(instance: event) |
||||||
|
} |
||||||
|
StoreCenter.main.destroyStore(identifier: tournament.id) |
||||||
|
} |
||||||
|
|
||||||
|
fileprivate func _localDisconnect() { |
||||||
|
|
||||||
|
let tournamendIds: [String] = self.tournaments.map { $0.id } |
||||||
|
|
||||||
|
TournamentLibrary.shared.reset() |
||||||
|
|
||||||
|
self.tournaments.reset() |
||||||
|
self.clubs.reset() |
||||||
|
self.courts.reset() |
||||||
|
self.events.reset() |
||||||
|
self.dateIntervals.reset() |
||||||
|
self.userStorage.reset() |
||||||
|
self.purchases.reset() |
||||||
|
|
||||||
|
Guard.main.disconnect() |
||||||
|
|
||||||
|
StoreCenter.main.disconnect() |
||||||
|
|
||||||
|
for tournament in tournamendIds { |
||||||
|
StoreCenter.main.destroyStore(identifier: tournament.id) |
||||||
|
} |
||||||
|
|
||||||
|
self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder() |
||||||
|
self.user.clubs.removeAll() |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func tryUserUpdate(userCopy: CustomUser) async throws { |
||||||
|
try await self.userStorage.tryPutBeforeUpdating(userCopy) |
||||||
|
} |
||||||
|
|
||||||
|
// func copyToLocalServer(tournament: Tournament) { |
||||||
|
// |
||||||
|
// Task { |
||||||
|
// do { |
||||||
|
// |
||||||
|
// if let url = PListReader.readString(plist: "local", key: "local_server"), |
||||||
|
// let login = PListReader.readString(plist: "local", key: "username"), |
||||||
|
// let pass = PListReader.readString(plist: "local", key: "password") { |
||||||
|
// let service = Services(url: url) |
||||||
|
// let _: CustomUser = try await service.login(username: login, password: pass) |
||||||
|
// |
||||||
|
// tournament.event = nil |
||||||
|
// _ = try await service.post(tournament) |
||||||
|
// |
||||||
|
// for groupStage in tournament.groupStages() { |
||||||
|
// _ = try await service.post(groupStage) |
||||||
|
// } |
||||||
|
// for round in tournament.rounds() { |
||||||
|
// try await self._insertRoundAndChildren(round: round, service: service) |
||||||
|
// } |
||||||
|
// for teamRegistration in tournament.unsortedTeams() { |
||||||
|
// _ = try await service.post(teamRegistration) |
||||||
|
// for playerRegistration in teamRegistration.unsortedPlayers() { |
||||||
|
// _ = try await service.post(playerRegistration) |
||||||
|
// } |
||||||
|
// } |
||||||
|
// for groupStage in tournament.groupStages() { |
||||||
|
// for match in groupStage._matches() { |
||||||
|
// try await self._insertMatch(match: match, service: service) |
||||||
|
// } |
||||||
|
// } |
||||||
|
// for round in tournament.allRounds() { |
||||||
|
// for match in round._matches() { |
||||||
|
// try await self._insertMatch(match: match, service: service) |
||||||
|
// } |
||||||
|
// } |
||||||
|
// |
||||||
|
// } |
||||||
|
// } catch { |
||||||
|
// Logger.error(error) |
||||||
|
// } |
||||||
|
// } |
||||||
|
// |
||||||
|
// } |
||||||
|
// |
||||||
|
// fileprivate func _insertRoundAndChildren(round: Round, service: Services) async throws { |
||||||
|
// _ = try await service.post(round) |
||||||
|
// for loserRound in round.loserRounds() { |
||||||
|
// try await self._insertRoundAndChildren(round: loserRound, service: service) |
||||||
|
// } |
||||||
|
// } |
||||||
|
// |
||||||
|
// fileprivate func _insertMatch(match: Match, service: Services) async throws { |
||||||
|
// _ = try await service.post(match) |
||||||
|
// for teamScore in match.teamScores { |
||||||
|
// _ = try await service.post(teamScore) |
||||||
|
// } |
||||||
|
// |
||||||
|
// } |
||||||
|
|
||||||
|
// MARK: - Convenience |
||||||
|
|
||||||
|
private var _lastRunningCheckDate: Date? = nil |
||||||
|
private var _cachedRunningMatches: [Match]? = nil |
||||||
|
|
||||||
|
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] = [] |
||||||
|
for tournament in lastTournaments { |
||||||
|
if let store = tournament.tournamentStore { |
||||||
|
let matches = store.matches.filter { match in |
||||||
|
match.disabled == false && match.isRunning() |
||||||
|
} |
||||||
|
runningMatches.append(contentsOf: matches) |
||||||
|
} |
||||||
|
} |
||||||
|
_lastRunningCheckDate = dateNow |
||||||
|
_cachedRunningMatches = runningMatches |
||||||
|
return _cachedRunningMatches! |
||||||
|
} |
||||||
|
|
||||||
|
private var _lastRunningAndNextCheckDate: Date? = nil |
||||||
|
private var _cachedRunningAndNextMatches: [Match]? = nil |
||||||
|
|
||||||
|
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] = [] |
||||||
|
for tournament in lastTournaments { |
||||||
|
if let store = tournament.tournamentStore { |
||||||
|
let matches = store.matches.filter { match in |
||||||
|
match.disabled == false && match.startDate != nil && match.endDate == nil } |
||||||
|
runningMatches.append(contentsOf: matches) |
||||||
|
} |
||||||
|
} |
||||||
|
_lastRunningAndNextCheckDate = dateNow |
||||||
|
_cachedRunningAndNextMatches = runningMatches |
||||||
|
return _cachedRunningAndNextMatches! |
||||||
|
} |
||||||
|
|
||||||
|
private var _lastEndCheckDate: Date? = nil |
||||||
|
private var _cachedEndMatches: [Match]? = nil |
||||||
|
|
||||||
|
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] = [] |
||||||
|
for tournament in lastTournaments { |
||||||
|
if let store = tournament.tournamentStore { |
||||||
|
let matches = store.matches.filter { match in |
||||||
|
match.disabled == false && match.hasEnded() } |
||||||
|
runningMatches.append(contentsOf: matches) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_lastEndCheckDate = dateNow |
||||||
|
_cachedEndMatches = runningMatches.sorted(by: \.endDate!, order: .descending) |
||||||
|
return _cachedEndMatches! |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,76 @@ |
|||||||
|
// |
||||||
|
// DateInterval.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 19/04/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import SwiftUI |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class DateInterval: BaseDateInterval { |
||||||
|
|
||||||
|
// static func resourceName() -> String { return "date-intervals" } |
||||||
|
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
// static func filterByStoreIdentifier() -> Bool { return false } |
||||||
|
// static var relationshipNames: [String] = [] |
||||||
|
// |
||||||
|
// var id: String = Store.randomId() |
||||||
|
// var lastUpdate: Date |
||||||
|
// var event: String |
||||||
|
// var courtIndex: Int |
||||||
|
// var startDate: Date |
||||||
|
// var endDate: Date |
||||||
|
|
||||||
|
internal init(event: String, courtIndex: Int, startDate: Date, endDate: Date) { |
||||||
|
super.init(event: event, courtIndex: courtIndex, startDate: startDate, endDate: endDate) |
||||||
|
// self.lastUpdate = Date() |
||||||
|
// self.event = event |
||||||
|
// self.courtIndex = courtIndex |
||||||
|
// self.startDate = startDate |
||||||
|
// self.endDate = endDate |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: any Decoder) throws { |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
var range: Range<Date> { |
||||||
|
startDate..<endDate |
||||||
|
} |
||||||
|
|
||||||
|
func isSingleDay() -> Bool { |
||||||
|
Calendar.current.isDate(startDate, inSameDayAs: endDate) |
||||||
|
} |
||||||
|
|
||||||
|
func isDateInside(_ date: Date) -> Bool { |
||||||
|
date >= startDate && date <= endDate |
||||||
|
} |
||||||
|
|
||||||
|
func isDateOutside(_ date: Date) -> Bool { |
||||||
|
date <= startDate && date <= endDate && date >= startDate && date >= endDate |
||||||
|
} |
||||||
|
|
||||||
|
override func deleteDependencies() { |
||||||
|
} |
||||||
|
|
||||||
|
// enum CodingKeys: String, CodingKey { |
||||||
|
// case _id = "id" |
||||||
|
// case _lastUpdate = "lastUpdate" |
||||||
|
// case _event = "event" |
||||||
|
// case _courtIndex = "courtIndex" |
||||||
|
// case _startDate = "startDate" |
||||||
|
// case _endDate = "endDate" |
||||||
|
// } |
||||||
|
|
||||||
|
func insertOnServer() throws { |
||||||
|
DataStore.shared.dateIntervals.writeChangeAndInsertOnServer(instance: self) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
// |
||||||
|
// DrawLog.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by razmig on 22/10/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import SwiftUI |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class DrawLog: BaseDrawLog, SideStorable { |
||||||
|
|
||||||
|
func tournamentObject() -> Tournament? { |
||||||
|
Store.main.findById(self.tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func computedBracketPosition() -> Int { |
||||||
|
drawMatchIndex * 2 + drawTeamPosition.rawValue |
||||||
|
} |
||||||
|
|
||||||
|
func updateTeamBracketPosition(_ team: TeamRegistration) { |
||||||
|
guard let match = drawMatch() else { return } |
||||||
|
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition) |
||||||
|
team.bracketPosition = seedPosition |
||||||
|
tournamentObject()?.updateTeamScores(in: seedPosition) |
||||||
|
} |
||||||
|
|
||||||
|
func exportedDrawLog() -> String { |
||||||
|
[drawType.localizedDrawType(), drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].filter({ $0.isEmpty == false }).joined(separator: " ") |
||||||
|
} |
||||||
|
|
||||||
|
func localizedDrawSeedLabel() -> String { |
||||||
|
return "\(drawType.localizedDrawType()) #\(drawSeed + 1)" |
||||||
|
} |
||||||
|
|
||||||
|
func localizedDrawLogLabel() -> String { |
||||||
|
return [localizedDrawSeedLabel(), positionLabel()].filter({ $0.isEmpty == false }).joined(separator: " -> ") |
||||||
|
} |
||||||
|
|
||||||
|
func localizedDrawBranch() -> String { |
||||||
|
switch drawType { |
||||||
|
case .seed: |
||||||
|
return drawTeamPosition.localizedBranchLabel() |
||||||
|
default: |
||||||
|
return "" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func drawMatch() -> Match? { |
||||||
|
switch drawType { |
||||||
|
case .seed: |
||||||
|
let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex) |
||||||
|
return tournamentStore?.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex }) |
||||||
|
default: |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func positionLabel() -> String { |
||||||
|
return drawMatch()?.roundAndMatchTitle() ?? "" |
||||||
|
} |
||||||
|
|
||||||
|
func roundLabel() -> String { |
||||||
|
return drawMatch()?.roundTitle() ?? "" |
||||||
|
} |
||||||
|
|
||||||
|
func matchLabel() -> String { |
||||||
|
return drawMatch()?.matchTitle() ?? "" |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentStore: TournamentStore? { |
||||||
|
return TournamentLibrary.shared.store(tournamentId: self.tournament) |
||||||
|
} |
||||||
|
|
||||||
|
override func deleteDependencies() { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
enum DrawType: Int, Codable { |
||||||
|
case seed |
||||||
|
case groupStage |
||||||
|
case court |
||||||
|
|
||||||
|
func localizedDrawType() -> String { |
||||||
|
switch self { |
||||||
|
case .seed: |
||||||
|
return "Tête de série" |
||||||
|
case .groupStage: |
||||||
|
return "Poule" |
||||||
|
case .court: |
||||||
|
return "Terrain" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,100 @@ |
|||||||
|
// |
||||||
|
// Event_v2.swift |
||||||
|
// Padel Tournament |
||||||
|
// |
||||||
|
// Created by razmig on 10/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class Event: BaseEvent { |
||||||
|
|
||||||
|
internal init(creator: String? = nil, club: String? = nil, name: String? = nil, tenupId: String? = nil) { |
||||||
|
super.init(creator: creator, club: club, name: name, tenupId: tenupId) |
||||||
|
self.relatedUser = creator |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
override func deleteDependencies() { |
||||||
|
let tournaments = self.tournaments |
||||||
|
for tournament in tournaments { |
||||||
|
tournament.deleteDependencies() |
||||||
|
} |
||||||
|
|
||||||
|
DataStore.shared.tournaments.deleteDependencies(tournaments) |
||||||
|
|
||||||
|
let courtsUnavailabilities = self.courtsUnavailability |
||||||
|
for courtsUnavailability in courtsUnavailabilities { |
||||||
|
courtsUnavailability.deleteDependencies() |
||||||
|
} |
||||||
|
DataStore.shared.dateIntervals.deleteDependencies(courtsUnavailabilities) |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - Computed dependencies |
||||||
|
|
||||||
|
var tournaments: [Tournament] { |
||||||
|
DataStore.shared.tournaments.filter { $0.event == self.id && $0.isDeleted == false } |
||||||
|
} |
||||||
|
|
||||||
|
func clubObject() -> Club? { |
||||||
|
guard let club else { return nil } |
||||||
|
return Store.main.findById(club) |
||||||
|
} |
||||||
|
|
||||||
|
var courtsUnavailability: [DateInterval] { |
||||||
|
DataStore.shared.dateIntervals.filter({ $0.event == id }) |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - |
||||||
|
|
||||||
|
func eventCourtCount() -> Int { |
||||||
|
tournaments.map { $0.courtCount }.max() ?? 2 |
||||||
|
} |
||||||
|
|
||||||
|
func eventStartDate() -> Date { |
||||||
|
tournaments.map { $0.startDate }.min() ?? Date() |
||||||
|
} |
||||||
|
|
||||||
|
func eventDayDuration() -> Int { |
||||||
|
tournaments.map { $0.dayDuration }.max() ?? 1 |
||||||
|
} |
||||||
|
|
||||||
|
func eventTitle() -> String { |
||||||
|
if let name, name.isEmpty == false { |
||||||
|
return name |
||||||
|
} else { |
||||||
|
return "Événement" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func existingBuild(_ build: any TournamentBuildHolder) -> Tournament? { |
||||||
|
tournaments.first(where: { $0.isSameBuild(build) }) |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentsCourtsUsed(exluding tournamentId: String) -> [DateInterval] { |
||||||
|
tournaments.filter { $0.id != tournamentId }.flatMap({ tournament in |
||||||
|
tournament.getPlayedMatchDateIntervals(in: self) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func insertOnServer() throws { |
||||||
|
DataStore.shared.events.writeChangeAndInsertOnServer(instance: self) |
||||||
|
for tournament in self.tournaments { |
||||||
|
try tournament.insertOnServer() |
||||||
|
} |
||||||
|
for dataInterval in self.courtsUnavailability { |
||||||
|
try dataInterval.insertOnServer() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,150 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseClub: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "clubs" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = true |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var creator: String? = nil |
||||||
|
var name: String = "" |
||||||
|
var acronym: String = "" |
||||||
|
var phone: String? = nil |
||||||
|
var code: String? = nil |
||||||
|
var address: String? = nil |
||||||
|
var city: String? = nil |
||||||
|
var zipCode: String? = nil |
||||||
|
var latitude: Double? = nil |
||||||
|
var longitude: Double? = nil |
||||||
|
var courtCount: Int = 2 |
||||||
|
var broadcastCode: String? = nil |
||||||
|
var timezone: String? = TimeZone.current.identifier |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
creator: String? = nil, |
||||||
|
name: String = "", |
||||||
|
acronym: String = "", |
||||||
|
phone: String? = nil, |
||||||
|
code: String? = nil, |
||||||
|
address: String? = nil, |
||||||
|
city: String? = nil, |
||||||
|
zipCode: String? = nil, |
||||||
|
latitude: Double? = nil, |
||||||
|
longitude: Double? = nil, |
||||||
|
courtCount: Int = 2, |
||||||
|
broadcastCode: String? = nil, |
||||||
|
timezone: String? = TimeZone.current.identifier |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.creator = creator |
||||||
|
self.name = name |
||||||
|
self.acronym = acronym |
||||||
|
self.phone = phone |
||||||
|
self.code = code |
||||||
|
self.address = address |
||||||
|
self.city = city |
||||||
|
self.zipCode = zipCode |
||||||
|
self.latitude = latitude |
||||||
|
self.longitude = longitude |
||||||
|
self.courtCount = courtCount |
||||||
|
self.broadcastCode = broadcastCode |
||||||
|
self.timezone = timezone |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _creator = "creator" |
||||||
|
case _name = "name" |
||||||
|
case _acronym = "acronym" |
||||||
|
case _phone = "phone" |
||||||
|
case _code = "code" |
||||||
|
case _address = "address" |
||||||
|
case _city = "city" |
||||||
|
case _zipCode = "zipCode" |
||||||
|
case _latitude = "latitude" |
||||||
|
case _longitude = "longitude" |
||||||
|
case _courtCount = "courtCount" |
||||||
|
case _broadcastCode = "broadcastCode" |
||||||
|
case _timezone = "timezone" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.creator = try container.decodeIfPresent(String.self, forKey: ._creator) ?? nil |
||||||
|
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? "" |
||||||
|
self.acronym = try container.decodeIfPresent(String.self, forKey: ._acronym) ?? "" |
||||||
|
self.phone = try container.decodeIfPresent(String.self, forKey: ._phone) ?? nil |
||||||
|
self.code = try container.decodeIfPresent(String.self, forKey: ._code) ?? nil |
||||||
|
self.address = try container.decodeIfPresent(String.self, forKey: ._address) ?? nil |
||||||
|
self.city = try container.decodeIfPresent(String.self, forKey: ._city) ?? nil |
||||||
|
self.zipCode = try container.decodeIfPresent(String.self, forKey: ._zipCode) ?? nil |
||||||
|
self.latitude = try container.decodeIfPresent(Double.self, forKey: ._latitude) ?? nil |
||||||
|
self.longitude = try container.decodeIfPresent(Double.self, forKey: ._longitude) ?? nil |
||||||
|
self.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2 |
||||||
|
self.broadcastCode = try container.decodeIfPresent(String.self, forKey: ._broadcastCode) ?? nil |
||||||
|
self.timezone = try container.decodeIfPresent(String.self, forKey: ._timezone) ?? TimeZone.current.identifier |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.creator, forKey: ._creator) |
||||||
|
try container.encode(self.name, forKey: ._name) |
||||||
|
try container.encode(self.acronym, forKey: ._acronym) |
||||||
|
try container.encode(self.phone, forKey: ._phone) |
||||||
|
try container.encode(self.code, forKey: ._code) |
||||||
|
try container.encode(self.address, forKey: ._address) |
||||||
|
try container.encode(self.city, forKey: ._city) |
||||||
|
try container.encode(self.zipCode, forKey: ._zipCode) |
||||||
|
try container.encode(self.latitude, forKey: ._latitude) |
||||||
|
try container.encode(self.longitude, forKey: ._longitude) |
||||||
|
try container.encode(self.courtCount, forKey: ._courtCount) |
||||||
|
try container.encode(self.broadcastCode, forKey: ._broadcastCode) |
||||||
|
try container.encode(self.timezone, forKey: ._timezone) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func creatorValue() -> CustomUser? { |
||||||
|
guard let creator = self.creator else { return nil } |
||||||
|
return Store.main.findById(creator) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let club = other as? BaseClub else { return } |
||||||
|
self.id = club.id |
||||||
|
self.creator = club.creator |
||||||
|
self.name = club.name |
||||||
|
self.acronym = club.acronym |
||||||
|
self.phone = club.phone |
||||||
|
self.code = club.code |
||||||
|
self.address = club.address |
||||||
|
self.city = club.city |
||||||
|
self.zipCode = club.zipCode |
||||||
|
self.latitude = club.latitude |
||||||
|
self.longitude = club.longitude |
||||||
|
self.courtCount = club.courtCount |
||||||
|
self.broadcastCode = club.broadcastCode |
||||||
|
self.timezone = club.timezone |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: CustomUser.self, keyPath: \BaseClub.creator), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseCourt: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "courts" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var index: Int = 0 |
||||||
|
var club: String = "" |
||||||
|
var name: String? = nil |
||||||
|
var exitAllowed: Bool = false |
||||||
|
var indoor: Bool = false |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
index: Int = 0, |
||||||
|
club: String = "", |
||||||
|
name: String? = nil, |
||||||
|
exitAllowed: Bool = false, |
||||||
|
indoor: Bool = false |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.index = index |
||||||
|
self.club = club |
||||||
|
self.name = name |
||||||
|
self.exitAllowed = exitAllowed |
||||||
|
self.indoor = indoor |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _index = "index" |
||||||
|
case _club = "club" |
||||||
|
case _name = "name" |
||||||
|
case _exitAllowed = "exitAllowed" |
||||||
|
case _indoor = "indoor" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0 |
||||||
|
self.club = try container.decodeIfPresent(String.self, forKey: ._club) ?? "" |
||||||
|
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil |
||||||
|
self.exitAllowed = try container.decodeIfPresent(Bool.self, forKey: ._exitAllowed) ?? false |
||||||
|
self.indoor = try container.decodeIfPresent(Bool.self, forKey: ._indoor) ?? false |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.index, forKey: ._index) |
||||||
|
try container.encode(self.club, forKey: ._club) |
||||||
|
try container.encode(self.name, forKey: ._name) |
||||||
|
try container.encode(self.exitAllowed, forKey: ._exitAllowed) |
||||||
|
try container.encode(self.indoor, forKey: ._indoor) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func clubValue() -> Club? { |
||||||
|
return Store.main.findById(club) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let court = other as? BaseCourt else { return } |
||||||
|
self.id = court.id |
||||||
|
self.index = court.index |
||||||
|
self.club = court.club |
||||||
|
self.name = court.name |
||||||
|
self.exitAllowed = court.exitAllowed |
||||||
|
self.indoor = court.indoor |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Club.self, keyPath: \BaseCourt.club), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,262 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseCustomUser: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "users" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var username: String = "" |
||||||
|
var email: String = "" |
||||||
|
var clubs: [String] = [] |
||||||
|
var umpireCode: String? = nil |
||||||
|
var licenceId: String? = nil |
||||||
|
var firstName: String = "" |
||||||
|
var lastName: String = "" |
||||||
|
var phone: String? = nil |
||||||
|
var country: String? = nil |
||||||
|
var summonsMessageBody: String? = nil |
||||||
|
var summonsMessageSignature: String? = nil |
||||||
|
var summonsAvailablePaymentMethods: String? = nil |
||||||
|
var summonsDisplayFormat: Bool = false |
||||||
|
var summonsDisplayEntryFee: Bool = false |
||||||
|
var summonsUseFullCustomMessage: Bool = false |
||||||
|
var matchFormatsDefaultDuration: [MatchFormat: Int]? = nil |
||||||
|
var bracketMatchFormatPreference: MatchFormat? = nil |
||||||
|
var groupStageMatchFormatPreference: MatchFormat? = nil |
||||||
|
var loserBracketMatchFormatPreference: MatchFormat? = nil |
||||||
|
var loserBracketMode: LoserBracketMode = .automatic |
||||||
|
var disableRankingFederalRuling: Bool = false |
||||||
|
var deviceId: String? = nil |
||||||
|
var agents: [String] = [] |
||||||
|
var userRole: Int? = nil |
||||||
|
var registrationPaymentMode: RegistrationPaymentMode = RegistrationPaymentMode.disabled |
||||||
|
var umpireCustomMail: String? = nil |
||||||
|
var umpireCustomContact: String? = nil |
||||||
|
var umpireCustomPhone: String? = nil |
||||||
|
var hideUmpireMail: Bool = false |
||||||
|
var hideUmpirePhone: Bool = true |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
username: String = "", |
||||||
|
email: String = "", |
||||||
|
clubs: [String] = [], |
||||||
|
umpireCode: String? = nil, |
||||||
|
licenceId: String? = nil, |
||||||
|
firstName: String = "", |
||||||
|
lastName: String = "", |
||||||
|
phone: String? = nil, |
||||||
|
country: String? = nil, |
||||||
|
summonsMessageBody: String? = nil, |
||||||
|
summonsMessageSignature: String? = nil, |
||||||
|
summonsAvailablePaymentMethods: String? = nil, |
||||||
|
summonsDisplayFormat: Bool = false, |
||||||
|
summonsDisplayEntryFee: Bool = false, |
||||||
|
summonsUseFullCustomMessage: Bool = false, |
||||||
|
matchFormatsDefaultDuration: [MatchFormat: Int]? = nil, |
||||||
|
bracketMatchFormatPreference: MatchFormat? = nil, |
||||||
|
groupStageMatchFormatPreference: MatchFormat? = nil, |
||||||
|
loserBracketMatchFormatPreference: MatchFormat? = nil, |
||||||
|
loserBracketMode: LoserBracketMode = .automatic, |
||||||
|
disableRankingFederalRuling: Bool = false, |
||||||
|
deviceId: String? = nil, |
||||||
|
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 |
||||||
|
self.username = username |
||||||
|
self.email = email |
||||||
|
self.clubs = clubs |
||||||
|
self.umpireCode = umpireCode |
||||||
|
self.licenceId = licenceId |
||||||
|
self.firstName = firstName |
||||||
|
self.lastName = lastName |
||||||
|
self.phone = phone |
||||||
|
self.country = country |
||||||
|
self.summonsMessageBody = summonsMessageBody |
||||||
|
self.summonsMessageSignature = summonsMessageSignature |
||||||
|
self.summonsAvailablePaymentMethods = summonsAvailablePaymentMethods |
||||||
|
self.summonsDisplayFormat = summonsDisplayFormat |
||||||
|
self.summonsDisplayEntryFee = summonsDisplayEntryFee |
||||||
|
self.summonsUseFullCustomMessage = summonsUseFullCustomMessage |
||||||
|
self.matchFormatsDefaultDuration = matchFormatsDefaultDuration |
||||||
|
self.bracketMatchFormatPreference = bracketMatchFormatPreference |
||||||
|
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() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _username = "username" |
||||||
|
case _email = "email" |
||||||
|
case _clubs = "clubs" |
||||||
|
case _umpireCode = "umpireCode" |
||||||
|
case _licenceId = "licenceId" |
||||||
|
case _firstName = "firstName" |
||||||
|
case _lastName = "lastName" |
||||||
|
case _phone = "phone" |
||||||
|
case _country = "country" |
||||||
|
case _summonsMessageBody = "summonsMessageBody" |
||||||
|
case _summonsMessageSignature = "summonsMessageSignature" |
||||||
|
case _summonsAvailablePaymentMethods = "summonsAvailablePaymentMethods" |
||||||
|
case _summonsDisplayFormat = "summonsDisplayFormat" |
||||||
|
case _summonsDisplayEntryFee = "summonsDisplayEntryFee" |
||||||
|
case _summonsUseFullCustomMessage = "summonsUseFullCustomMessage" |
||||||
|
case _matchFormatsDefaultDuration = "matchFormatsDefaultDuration" |
||||||
|
case _bracketMatchFormatPreference = "bracketMatchFormatPreference" |
||||||
|
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 { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.username = try container.decodeIfPresent(String.self, forKey: ._username) ?? "" |
||||||
|
self.email = try container.decodeIfPresent(String.self, forKey: ._email) ?? "" |
||||||
|
self.clubs = try container.decodeIfPresent([String].self, forKey: ._clubs) ?? [] |
||||||
|
self.umpireCode = try container.decodeIfPresent(String.self, forKey: ._umpireCode) ?? nil |
||||||
|
self.licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId) ?? nil |
||||||
|
self.firstName = try container.decodeIfPresent(String.self, forKey: ._firstName) ?? "" |
||||||
|
self.lastName = try container.decodeIfPresent(String.self, forKey: ._lastName) ?? "" |
||||||
|
self.phone = try container.decodeIfPresent(String.self, forKey: ._phone) ?? nil |
||||||
|
self.country = try container.decodeIfPresent(String.self, forKey: ._country) ?? nil |
||||||
|
self.summonsMessageBody = try container.decodeIfPresent(String.self, forKey: ._summonsMessageBody) ?? nil |
||||||
|
self.summonsMessageSignature = try container.decodeIfPresent(String.self, forKey: ._summonsMessageSignature) ?? nil |
||||||
|
self.summonsAvailablePaymentMethods = try container.decodeIfPresent(String.self, forKey: ._summonsAvailablePaymentMethods) ?? nil |
||||||
|
self.summonsDisplayFormat = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayFormat) ?? false |
||||||
|
self.summonsDisplayEntryFee = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayEntryFee) ?? false |
||||||
|
self.summonsUseFullCustomMessage = try container.decodeIfPresent(Bool.self, forKey: ._summonsUseFullCustomMessage) ?? false |
||||||
|
self.matchFormatsDefaultDuration = try container.decodeIfPresent([MatchFormat: Int].self, forKey: ._matchFormatsDefaultDuration) ?? nil |
||||||
|
self.bracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._bracketMatchFormatPreference) ?? nil |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.username, forKey: ._username) |
||||||
|
try container.encode(self.email, forKey: ._email) |
||||||
|
try container.encode(self.clubs, forKey: ._clubs) |
||||||
|
try container.encode(self.umpireCode, forKey: ._umpireCode) |
||||||
|
try container.encode(self.licenceId, forKey: ._licenceId) |
||||||
|
try container.encode(self.firstName, forKey: ._firstName) |
||||||
|
try container.encode(self.lastName, forKey: ._lastName) |
||||||
|
try container.encode(self.phone, forKey: ._phone) |
||||||
|
try container.encode(self.country, forKey: ._country) |
||||||
|
try container.encode(self.summonsMessageBody, forKey: ._summonsMessageBody) |
||||||
|
try container.encode(self.summonsMessageSignature, forKey: ._summonsMessageSignature) |
||||||
|
try container.encode(self.summonsAvailablePaymentMethods, forKey: ._summonsAvailablePaymentMethods) |
||||||
|
try container.encode(self.summonsDisplayFormat, forKey: ._summonsDisplayFormat) |
||||||
|
try container.encode(self.summonsDisplayEntryFee, forKey: ._summonsDisplayEntryFee) |
||||||
|
try container.encode(self.summonsUseFullCustomMessage, forKey: ._summonsUseFullCustomMessage) |
||||||
|
try container.encode(self.matchFormatsDefaultDuration, forKey: ._matchFormatsDefaultDuration) |
||||||
|
try container.encode(self.bracketMatchFormatPreference, forKey: ._bracketMatchFormatPreference) |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let customuser = other as? BaseCustomUser else { return } |
||||||
|
self.id = customuser.id |
||||||
|
self.username = customuser.username |
||||||
|
self.email = customuser.email |
||||||
|
self.clubs = customuser.clubs |
||||||
|
self.umpireCode = customuser.umpireCode |
||||||
|
self.licenceId = customuser.licenceId |
||||||
|
self.firstName = customuser.firstName |
||||||
|
self.lastName = customuser.lastName |
||||||
|
self.phone = customuser.phone |
||||||
|
self.country = customuser.country |
||||||
|
self.summonsMessageBody = customuser.summonsMessageBody |
||||||
|
self.summonsMessageSignature = customuser.summonsMessageSignature |
||||||
|
self.summonsAvailablePaymentMethods = customuser.summonsAvailablePaymentMethods |
||||||
|
self.summonsDisplayFormat = customuser.summonsDisplayFormat |
||||||
|
self.summonsDisplayEntryFee = customuser.summonsDisplayEntryFee |
||||||
|
self.summonsUseFullCustomMessage = customuser.summonsUseFullCustomMessage |
||||||
|
self.matchFormatsDefaultDuration = customuser.matchFormatsDefaultDuration |
||||||
|
self.bracketMatchFormatPreference = customuser.bracketMatchFormatPreference |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,80 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseDateInterval: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "date-intervals" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var event: String = "" |
||||||
|
var courtIndex: Int = 0 |
||||||
|
var startDate: Date = Date() |
||||||
|
var endDate: Date = Date() |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
event: String = "", |
||||||
|
courtIndex: Int = 0, |
||||||
|
startDate: Date = Date(), |
||||||
|
endDate: Date = Date() |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.event = event |
||||||
|
self.courtIndex = courtIndex |
||||||
|
self.startDate = startDate |
||||||
|
self.endDate = endDate |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _event = "event" |
||||||
|
case _courtIndex = "courtIndex" |
||||||
|
case _startDate = "startDate" |
||||||
|
case _endDate = "endDate" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.event = try container.decodeIfPresent(String.self, forKey: ._event) ?? "" |
||||||
|
self.courtIndex = try container.decodeIfPresent(Int.self, forKey: ._courtIndex) ?? 0 |
||||||
|
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date() |
||||||
|
self.endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? Date() |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.event, forKey: ._event) |
||||||
|
try container.encode(self.courtIndex, forKey: ._courtIndex) |
||||||
|
try container.encode(self.startDate, forKey: ._startDate) |
||||||
|
try container.encode(self.endDate, forKey: ._endDate) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let dateinterval = other as? BaseDateInterval else { return } |
||||||
|
self.id = dateinterval.id |
||||||
|
self.event = dateinterval.event |
||||||
|
self.courtIndex = dateinterval.courtIndex |
||||||
|
self.startDate = dateinterval.startDate |
||||||
|
self.endDate = dateinterval.endDate |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,100 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseDrawLog: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "draw-logs" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var tournament: String = "" |
||||||
|
var drawDate: Date = Date() |
||||||
|
var drawSeed: Int = 0 |
||||||
|
var drawMatchIndex: Int = 0 |
||||||
|
var drawTeamPosition: TeamPosition = TeamPosition.one |
||||||
|
var drawType: DrawType = DrawType.seed |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
tournament: String = "", |
||||||
|
drawDate: Date = Date(), |
||||||
|
drawSeed: Int = 0, |
||||||
|
drawMatchIndex: Int = 0, |
||||||
|
drawTeamPosition: TeamPosition = TeamPosition.one, |
||||||
|
drawType: DrawType = DrawType.seed |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.tournament = tournament |
||||||
|
self.drawDate = drawDate |
||||||
|
self.drawSeed = drawSeed |
||||||
|
self.drawMatchIndex = drawMatchIndex |
||||||
|
self.drawTeamPosition = drawTeamPosition |
||||||
|
self.drawType = drawType |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _tournament = "tournament" |
||||||
|
case _drawDate = "drawDate" |
||||||
|
case _drawSeed = "drawSeed" |
||||||
|
case _drawMatchIndex = "drawMatchIndex" |
||||||
|
case _drawTeamPosition = "drawTeamPosition" |
||||||
|
case _drawType = "drawType" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.tournament = try container.decodeIfPresent(String.self, forKey: ._tournament) ?? "" |
||||||
|
self.drawDate = try container.decodeIfPresent(Date.self, forKey: ._drawDate) ?? Date() |
||||||
|
self.drawSeed = try container.decodeIfPresent(Int.self, forKey: ._drawSeed) ?? 0 |
||||||
|
self.drawMatchIndex = try container.decodeIfPresent(Int.self, forKey: ._drawMatchIndex) ?? 0 |
||||||
|
self.drawTeamPosition = try container.decodeIfPresent(TeamPosition.self, forKey: ._drawTeamPosition) ?? TeamPosition.one |
||||||
|
self.drawType = try container.decodeIfPresent(DrawType.self, forKey: ._drawType) ?? DrawType.seed |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.tournament, forKey: ._tournament) |
||||||
|
try container.encode(self.drawDate, forKey: ._drawDate) |
||||||
|
try container.encode(self.drawSeed, forKey: ._drawSeed) |
||||||
|
try container.encode(self.drawMatchIndex, forKey: ._drawMatchIndex) |
||||||
|
try container.encode(self.drawTeamPosition, forKey: ._drawTeamPosition) |
||||||
|
try container.encode(self.drawType, forKey: ._drawType) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentValue() -> Tournament? { |
||||||
|
return Store.main.findById(tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let drawlog = other as? BaseDrawLog else { return } |
||||||
|
self.id = drawlog.id |
||||||
|
self.tournament = drawlog.tournament |
||||||
|
self.drawDate = drawlog.drawDate |
||||||
|
self.drawSeed = drawlog.drawSeed |
||||||
|
self.drawMatchIndex = drawlog.drawMatchIndex |
||||||
|
self.drawTeamPosition = drawlog.drawTeamPosition |
||||||
|
self.drawType = drawlog.drawType |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Tournament.self, keyPath: \BaseDrawLog.tournament), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,100 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseEvent: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "events" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var creator: String? = nil |
||||||
|
var club: String? = nil |
||||||
|
var creationDate: Date = Date() |
||||||
|
var name: String? = nil |
||||||
|
var tenupId: String? = nil |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
creator: String? = nil, |
||||||
|
club: String? = nil, |
||||||
|
creationDate: Date = Date(), |
||||||
|
name: String? = nil, |
||||||
|
tenupId: String? = nil |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.creator = creator |
||||||
|
self.club = club |
||||||
|
self.creationDate = creationDate |
||||||
|
self.name = name |
||||||
|
self.tenupId = tenupId |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _creator = "creator" |
||||||
|
case _club = "club" |
||||||
|
case _creationDate = "creationDate" |
||||||
|
case _name = "name" |
||||||
|
case _tenupId = "tenupId" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.creator = try container.decodeIfPresent(String.self, forKey: ._creator) ?? nil |
||||||
|
self.club = try container.decodeIfPresent(String.self, forKey: ._club) ?? nil |
||||||
|
self.creationDate = try container.decodeIfPresent(Date.self, forKey: ._creationDate) ?? Date() |
||||||
|
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil |
||||||
|
self.tenupId = try container.decodeIfPresent(String.self, forKey: ._tenupId) ?? nil |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.creator, forKey: ._creator) |
||||||
|
try container.encode(self.club, forKey: ._club) |
||||||
|
try container.encode(self.creationDate, forKey: ._creationDate) |
||||||
|
try container.encode(self.name, forKey: ._name) |
||||||
|
try container.encode(self.tenupId, forKey: ._tenupId) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func creatorValue() -> CustomUser? { |
||||||
|
guard let creator = self.creator else { return nil } |
||||||
|
return Store.main.findById(creator) |
||||||
|
} |
||||||
|
|
||||||
|
func clubValue() -> Club? { |
||||||
|
guard let club = self.club else { return nil } |
||||||
|
return Store.main.findById(club) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let event = other as? BaseEvent else { return } |
||||||
|
self.id = event.id |
||||||
|
self.creator = event.creator |
||||||
|
self.club = event.club |
||||||
|
self.creationDate = event.creationDate |
||||||
|
self.name = event.name |
||||||
|
self.tenupId = event.tenupId |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: CustomUser.self, keyPath: \BaseEvent.creator), |
||||||
|
Relationship(type: Club.self, keyPath: \BaseEvent.club), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,107 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseGroupStage: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "group-stages" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var tournament: String = "" |
||||||
|
var index: Int = 0 |
||||||
|
var size: Int = 0 |
||||||
|
var format: MatchFormat? = nil |
||||||
|
var startDate: Date? = nil |
||||||
|
var name: String? = nil |
||||||
|
var step: Int = 0 |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
tournament: String = "", |
||||||
|
index: Int = 0, |
||||||
|
size: Int = 0, |
||||||
|
format: MatchFormat? = nil, |
||||||
|
startDate: Date? = nil, |
||||||
|
name: String? = nil, |
||||||
|
step: Int = 0 |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.tournament = tournament |
||||||
|
self.index = index |
||||||
|
self.size = size |
||||||
|
self.format = format |
||||||
|
self.startDate = startDate |
||||||
|
self.name = name |
||||||
|
self.step = step |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _tournament = "tournament" |
||||||
|
case _index = "index" |
||||||
|
case _size = "size" |
||||||
|
case _format = "format" |
||||||
|
case _startDate = "startDate" |
||||||
|
case _name = "name" |
||||||
|
case _step = "step" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.tournament = try container.decodeIfPresent(String.self, forKey: ._tournament) ?? "" |
||||||
|
self.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0 |
||||||
|
self.size = try container.decodeIfPresent(Int.self, forKey: ._size) ?? 0 |
||||||
|
self.format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) ?? nil |
||||||
|
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? nil |
||||||
|
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil |
||||||
|
self.step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0 |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.tournament, forKey: ._tournament) |
||||||
|
try container.encode(self.index, forKey: ._index) |
||||||
|
try container.encode(self.size, forKey: ._size) |
||||||
|
try container.encode(self.format, forKey: ._format) |
||||||
|
try container.encode(self.startDate, forKey: ._startDate) |
||||||
|
try container.encode(self.name, forKey: ._name) |
||||||
|
try container.encode(self.step, forKey: ._step) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentValue() -> Tournament? { |
||||||
|
return Store.main.findById(tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let groupstage = other as? BaseGroupStage else { return } |
||||||
|
self.id = groupstage.id |
||||||
|
self.tournament = groupstage.tournament |
||||||
|
self.index = groupstage.index |
||||||
|
self.size = groupstage.size |
||||||
|
self.format = groupstage.format |
||||||
|
self.startDate = groupstage.startDate |
||||||
|
self.name = groupstage.name |
||||||
|
self.step = groupstage.step |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Tournament.self, keyPath: \BaseGroupStage.tournament), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,156 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseMatch: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "matches" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var round: String? = nil |
||||||
|
var groupStage: String? = nil |
||||||
|
var startDate: Date? = nil |
||||||
|
var endDate: Date? = nil |
||||||
|
var index: Int = 0 |
||||||
|
var format: MatchFormat? = nil |
||||||
|
var servingTeamId: String? = nil |
||||||
|
var winningTeamId: String? = nil |
||||||
|
var losingTeamId: String? = nil |
||||||
|
var name: String? = nil |
||||||
|
var disabled: Bool = false |
||||||
|
var courtIndex: Int? = nil |
||||||
|
var confirmed: Bool = false |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
round: String? = nil, |
||||||
|
groupStage: String? = nil, |
||||||
|
startDate: Date? = nil, |
||||||
|
endDate: Date? = nil, |
||||||
|
index: Int = 0, |
||||||
|
format: MatchFormat? = nil, |
||||||
|
servingTeamId: String? = nil, |
||||||
|
winningTeamId: String? = nil, |
||||||
|
losingTeamId: String? = nil, |
||||||
|
name: String? = nil, |
||||||
|
disabled: Bool = false, |
||||||
|
courtIndex: Int? = nil, |
||||||
|
confirmed: Bool = false |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.round = round |
||||||
|
self.groupStage = groupStage |
||||||
|
self.startDate = startDate |
||||||
|
self.endDate = endDate |
||||||
|
self.index = index |
||||||
|
self.format = format |
||||||
|
self.servingTeamId = servingTeamId |
||||||
|
self.winningTeamId = winningTeamId |
||||||
|
self.losingTeamId = losingTeamId |
||||||
|
self.name = name |
||||||
|
self.disabled = disabled |
||||||
|
self.courtIndex = courtIndex |
||||||
|
self.confirmed = confirmed |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _round = "round" |
||||||
|
case _groupStage = "groupStage" |
||||||
|
case _startDate = "startDate" |
||||||
|
case _endDate = "endDate" |
||||||
|
case _index = "index" |
||||||
|
case _format = "format" |
||||||
|
case _servingTeamId = "servingTeamId" |
||||||
|
case _winningTeamId = "winningTeamId" |
||||||
|
case _losingTeamId = "losingTeamId" |
||||||
|
case _name = "name" |
||||||
|
case _disabled = "disabled" |
||||||
|
case _courtIndex = "courtIndex" |
||||||
|
case _confirmed = "confirmed" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.round = try container.decodeIfPresent(String.self, forKey: ._round) ?? nil |
||||||
|
self.groupStage = try container.decodeIfPresent(String.self, forKey: ._groupStage) ?? nil |
||||||
|
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? nil |
||||||
|
self.endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? nil |
||||||
|
self.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0 |
||||||
|
self.format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) ?? nil |
||||||
|
self.servingTeamId = try container.decodeIfPresent(String.self, forKey: ._servingTeamId) ?? nil |
||||||
|
self.winningTeamId = try container.decodeIfPresent(String.self, forKey: ._winningTeamId) ?? nil |
||||||
|
self.losingTeamId = try container.decodeIfPresent(String.self, forKey: ._losingTeamId) ?? nil |
||||||
|
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil |
||||||
|
self.disabled = try container.decodeIfPresent(Bool.self, forKey: ._disabled) ?? false |
||||||
|
self.courtIndex = try container.decodeIfPresent(Int.self, forKey: ._courtIndex) ?? nil |
||||||
|
self.confirmed = try container.decodeIfPresent(Bool.self, forKey: ._confirmed) ?? false |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.round, forKey: ._round) |
||||||
|
try container.encode(self.groupStage, forKey: ._groupStage) |
||||||
|
try container.encode(self.startDate, forKey: ._startDate) |
||||||
|
try container.encode(self.endDate, forKey: ._endDate) |
||||||
|
try container.encode(self.index, forKey: ._index) |
||||||
|
try container.encode(self.format, forKey: ._format) |
||||||
|
try container.encode(self.servingTeamId, forKey: ._servingTeamId) |
||||||
|
try container.encode(self.winningTeamId, forKey: ._winningTeamId) |
||||||
|
try container.encode(self.losingTeamId, forKey: ._losingTeamId) |
||||||
|
try container.encode(self.name, forKey: ._name) |
||||||
|
try container.encode(self.disabled, forKey: ._disabled) |
||||||
|
try container.encode(self.courtIndex, forKey: ._courtIndex) |
||||||
|
try container.encode(self.confirmed, forKey: ._confirmed) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func roundValue() -> Round? { |
||||||
|
guard let round = self.round else { return nil } |
||||||
|
return self.store?.findById(round) |
||||||
|
} |
||||||
|
|
||||||
|
func groupStageValue() -> GroupStage? { |
||||||
|
guard let groupStage = self.groupStage else { return nil } |
||||||
|
return self.store?.findById(groupStage) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let match = other as? BaseMatch else { return } |
||||||
|
self.id = match.id |
||||||
|
self.round = match.round |
||||||
|
self.groupStage = match.groupStage |
||||||
|
self.startDate = match.startDate |
||||||
|
self.endDate = match.endDate |
||||||
|
self.index = match.index |
||||||
|
self.format = match.format |
||||||
|
self.servingTeamId = match.servingTeamId |
||||||
|
self.winningTeamId = match.winningTeamId |
||||||
|
self.losingTeamId = match.losingTeamId |
||||||
|
self.name = match.name |
||||||
|
self.disabled = match.disabled |
||||||
|
self.courtIndex = match.courtIndex |
||||||
|
self.confirmed = match.confirmed |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Round.self, keyPath: \BaseMatch.round), |
||||||
|
Relationship(type: GroupStage.self, keyPath: \BaseMatch.groupStage), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,163 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseMatchScheduler: BaseModelObject, Storable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "match-scheduler" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var tournament: String = "" |
||||||
|
var timeDifferenceLimit: Int = 5 |
||||||
|
var loserBracketRotationDifference: Int = 0 |
||||||
|
var upperBracketRotationDifference: Int = 1 |
||||||
|
var accountUpperBracketBreakTime: Bool = true |
||||||
|
var accountLoserBracketBreakTime: Bool = false |
||||||
|
var randomizeCourts: Bool = true |
||||||
|
var rotationDifferenceIsImportant: Bool = false |
||||||
|
var shouldHandleUpperRoundSlice: Bool = false |
||||||
|
var shouldEndRoundBeforeStartingNext: Bool = true |
||||||
|
var groupStageChunkCount: Int? = nil |
||||||
|
var overrideCourtsUnavailability: Bool = false |
||||||
|
var shouldTryToFillUpCourtsAvailable: Bool = false |
||||||
|
var courtsAvailable: Set<Int> = Set<Int>() |
||||||
|
var simultaneousStart: Bool = true |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
tournament: String = "", |
||||||
|
timeDifferenceLimit: Int = 5, |
||||||
|
loserBracketRotationDifference: Int = 0, |
||||||
|
upperBracketRotationDifference: Int = 1, |
||||||
|
accountUpperBracketBreakTime: Bool = true, |
||||||
|
accountLoserBracketBreakTime: Bool = false, |
||||||
|
randomizeCourts: Bool = true, |
||||||
|
rotationDifferenceIsImportant: Bool = false, |
||||||
|
shouldHandleUpperRoundSlice: Bool = false, |
||||||
|
shouldEndRoundBeforeStartingNext: Bool = true, |
||||||
|
groupStageChunkCount: Int? = nil, |
||||||
|
overrideCourtsUnavailability: Bool = false, |
||||||
|
shouldTryToFillUpCourtsAvailable: Bool = false, |
||||||
|
courtsAvailable: Set<Int> = Set<Int>(), |
||||||
|
simultaneousStart: Bool = true |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.tournament = tournament |
||||||
|
self.timeDifferenceLimit = timeDifferenceLimit |
||||||
|
self.loserBracketRotationDifference = loserBracketRotationDifference |
||||||
|
self.upperBracketRotationDifference = upperBracketRotationDifference |
||||||
|
self.accountUpperBracketBreakTime = accountUpperBracketBreakTime |
||||||
|
self.accountLoserBracketBreakTime = accountLoserBracketBreakTime |
||||||
|
self.randomizeCourts = randomizeCourts |
||||||
|
self.rotationDifferenceIsImportant = rotationDifferenceIsImportant |
||||||
|
self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice |
||||||
|
self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext |
||||||
|
self.groupStageChunkCount = groupStageChunkCount |
||||||
|
self.overrideCourtsUnavailability = overrideCourtsUnavailability |
||||||
|
self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable |
||||||
|
self.courtsAvailable = courtsAvailable |
||||||
|
self.simultaneousStart = simultaneousStart |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _tournament = "tournament" |
||||||
|
case _timeDifferenceLimit = "timeDifferenceLimit" |
||||||
|
case _loserBracketRotationDifference = "loserBracketRotationDifference" |
||||||
|
case _upperBracketRotationDifference = "upperBracketRotationDifference" |
||||||
|
case _accountUpperBracketBreakTime = "accountUpperBracketBreakTime" |
||||||
|
case _accountLoserBracketBreakTime = "accountLoserBracketBreakTime" |
||||||
|
case _randomizeCourts = "randomizeCourts" |
||||||
|
case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant" |
||||||
|
case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice" |
||||||
|
case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext" |
||||||
|
case _groupStageChunkCount = "groupStageChunkCount" |
||||||
|
case _overrideCourtsUnavailability = "overrideCourtsUnavailability" |
||||||
|
case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" |
||||||
|
case _courtsAvailable = "courtsAvailable" |
||||||
|
case _simultaneousStart = "simultaneousStart" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.tournament = try container.decodeIfPresent(String.self, forKey: ._tournament) ?? "" |
||||||
|
self.timeDifferenceLimit = try container.decodeIfPresent(Int.self, forKey: ._timeDifferenceLimit) ?? 5 |
||||||
|
self.loserBracketRotationDifference = try container.decodeIfPresent(Int.self, forKey: ._loserBracketRotationDifference) ?? 0 |
||||||
|
self.upperBracketRotationDifference = try container.decodeIfPresent(Int.self, forKey: ._upperBracketRotationDifference) ?? 1 |
||||||
|
self.accountUpperBracketBreakTime = try container.decodeIfPresent(Bool.self, forKey: ._accountUpperBracketBreakTime) ?? true |
||||||
|
self.accountLoserBracketBreakTime = try container.decodeIfPresent(Bool.self, forKey: ._accountLoserBracketBreakTime) ?? false |
||||||
|
self.randomizeCourts = try container.decodeIfPresent(Bool.self, forKey: ._randomizeCourts) ?? true |
||||||
|
self.rotationDifferenceIsImportant = try container.decodeIfPresent(Bool.self, forKey: ._rotationDifferenceIsImportant) ?? false |
||||||
|
self.shouldHandleUpperRoundSlice = try container.decodeIfPresent(Bool.self, forKey: ._shouldHandleUpperRoundSlice) ?? false |
||||||
|
self.shouldEndRoundBeforeStartingNext = try container.decodeIfPresent(Bool.self, forKey: ._shouldEndRoundBeforeStartingNext) ?? true |
||||||
|
self.groupStageChunkCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageChunkCount) ?? nil |
||||||
|
self.overrideCourtsUnavailability = try container.decodeIfPresent(Bool.self, forKey: ._overrideCourtsUnavailability) ?? false |
||||||
|
self.shouldTryToFillUpCourtsAvailable = try container.decodeIfPresent(Bool.self, forKey: ._shouldTryToFillUpCourtsAvailable) ?? false |
||||||
|
self.courtsAvailable = try container.decodeIfPresent(Set<Int>.self, forKey: ._courtsAvailable) ?? Set<Int>() |
||||||
|
self.simultaneousStart = try container.decodeIfPresent(Bool.self, forKey: ._simultaneousStart) ?? true |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.tournament, forKey: ._tournament) |
||||||
|
try container.encode(self.timeDifferenceLimit, forKey: ._timeDifferenceLimit) |
||||||
|
try container.encode(self.loserBracketRotationDifference, forKey: ._loserBracketRotationDifference) |
||||||
|
try container.encode(self.upperBracketRotationDifference, forKey: ._upperBracketRotationDifference) |
||||||
|
try container.encode(self.accountUpperBracketBreakTime, forKey: ._accountUpperBracketBreakTime) |
||||||
|
try container.encode(self.accountLoserBracketBreakTime, forKey: ._accountLoserBracketBreakTime) |
||||||
|
try container.encode(self.randomizeCourts, forKey: ._randomizeCourts) |
||||||
|
try container.encode(self.rotationDifferenceIsImportant, forKey: ._rotationDifferenceIsImportant) |
||||||
|
try container.encode(self.shouldHandleUpperRoundSlice, forKey: ._shouldHandleUpperRoundSlice) |
||||||
|
try container.encode(self.shouldEndRoundBeforeStartingNext, forKey: ._shouldEndRoundBeforeStartingNext) |
||||||
|
try container.encode(self.groupStageChunkCount, forKey: ._groupStageChunkCount) |
||||||
|
try container.encode(self.overrideCourtsUnavailability, forKey: ._overrideCourtsUnavailability) |
||||||
|
try container.encode(self.shouldTryToFillUpCourtsAvailable, forKey: ._shouldTryToFillUpCourtsAvailable) |
||||||
|
try container.encode(self.courtsAvailable, forKey: ._courtsAvailable) |
||||||
|
try container.encode(self.simultaneousStart, forKey: ._simultaneousStart) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentValue() -> Tournament? { |
||||||
|
return Store.main.findById(tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let matchscheduler = other as? BaseMatchScheduler else { return } |
||||||
|
self.id = matchscheduler.id |
||||||
|
self.tournament = matchscheduler.tournament |
||||||
|
self.timeDifferenceLimit = matchscheduler.timeDifferenceLimit |
||||||
|
self.loserBracketRotationDifference = matchscheduler.loserBracketRotationDifference |
||||||
|
self.upperBracketRotationDifference = matchscheduler.upperBracketRotationDifference |
||||||
|
self.accountUpperBracketBreakTime = matchscheduler.accountUpperBracketBreakTime |
||||||
|
self.accountLoserBracketBreakTime = matchscheduler.accountLoserBracketBreakTime |
||||||
|
self.randomizeCourts = matchscheduler.randomizeCourts |
||||||
|
self.rotationDifferenceIsImportant = matchscheduler.rotationDifferenceIsImportant |
||||||
|
self.shouldHandleUpperRoundSlice = matchscheduler.shouldHandleUpperRoundSlice |
||||||
|
self.shouldEndRoundBeforeStartingNext = matchscheduler.shouldEndRoundBeforeStartingNext |
||||||
|
self.groupStageChunkCount = matchscheduler.groupStageChunkCount |
||||||
|
self.overrideCourtsUnavailability = matchscheduler.overrideCourtsUnavailability |
||||||
|
self.shouldTryToFillUpCourtsAvailable = matchscheduler.shouldTryToFillUpCourtsAvailable |
||||||
|
self.courtsAvailable = matchscheduler.courtsAvailable |
||||||
|
self.simultaneousStart = matchscheduler.simultaneousStart |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Tournament.self, keyPath: \BaseMatchScheduler.tournament), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,122 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseMonthData: BaseModelObject, Storable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "month-data" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var monthKey: String = "" |
||||||
|
var creationDate: Date = Date() |
||||||
|
var maleUnrankedValue: Int? = nil |
||||||
|
var femaleUnrankedValue: Int? = nil |
||||||
|
var maleCount: Int? = nil |
||||||
|
var femaleCount: Int? = nil |
||||||
|
var anonymousCount: Int? = nil |
||||||
|
var incompleteMode: Bool = false |
||||||
|
var dataModelIdentifier: String? = nil |
||||||
|
var fileModelIdentifier: String? = nil |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
monthKey: String = "", |
||||||
|
creationDate: Date = Date(), |
||||||
|
maleUnrankedValue: Int? = nil, |
||||||
|
femaleUnrankedValue: Int? = nil, |
||||||
|
maleCount: Int? = nil, |
||||||
|
femaleCount: Int? = nil, |
||||||
|
anonymousCount: Int? = nil, |
||||||
|
incompleteMode: Bool = false, |
||||||
|
dataModelIdentifier: String? = nil, |
||||||
|
fileModelIdentifier: String? = nil |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.monthKey = monthKey |
||||||
|
self.creationDate = creationDate |
||||||
|
self.maleUnrankedValue = maleUnrankedValue |
||||||
|
self.femaleUnrankedValue = femaleUnrankedValue |
||||||
|
self.maleCount = maleCount |
||||||
|
self.femaleCount = femaleCount |
||||||
|
self.anonymousCount = anonymousCount |
||||||
|
self.incompleteMode = incompleteMode |
||||||
|
self.dataModelIdentifier = dataModelIdentifier |
||||||
|
self.fileModelIdentifier = fileModelIdentifier |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _monthKey = "monthKey" |
||||||
|
case _creationDate = "creationDate" |
||||||
|
case _maleUnrankedValue = "maleUnrankedValue" |
||||||
|
case _femaleUnrankedValue = "femaleUnrankedValue" |
||||||
|
case _maleCount = "maleCount" |
||||||
|
case _femaleCount = "femaleCount" |
||||||
|
case _anonymousCount = "anonymousCount" |
||||||
|
case _incompleteMode = "incompleteMode" |
||||||
|
case _dataModelIdentifier = "dataModelIdentifier" |
||||||
|
case _fileModelIdentifier = "fileModelIdentifier" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.monthKey = try container.decodeIfPresent(String.self, forKey: ._monthKey) ?? "" |
||||||
|
self.creationDate = try container.decodeIfPresent(Date.self, forKey: ._creationDate) ?? Date() |
||||||
|
self.maleUnrankedValue = try container.decodeIfPresent(Int.self, forKey: ._maleUnrankedValue) ?? nil |
||||||
|
self.femaleUnrankedValue = try container.decodeIfPresent(Int.self, forKey: ._femaleUnrankedValue) ?? nil |
||||||
|
self.maleCount = try container.decodeIfPresent(Int.self, forKey: ._maleCount) ?? nil |
||||||
|
self.femaleCount = try container.decodeIfPresent(Int.self, forKey: ._femaleCount) ?? nil |
||||||
|
self.anonymousCount = try container.decodeIfPresent(Int.self, forKey: ._anonymousCount) ?? nil |
||||||
|
self.incompleteMode = try container.decodeIfPresent(Bool.self, forKey: ._incompleteMode) ?? false |
||||||
|
self.dataModelIdentifier = try container.decodeIfPresent(String.self, forKey: ._dataModelIdentifier) ?? nil |
||||||
|
self.fileModelIdentifier = try container.decodeIfPresent(String.self, forKey: ._fileModelIdentifier) ?? nil |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.monthKey, forKey: ._monthKey) |
||||||
|
try container.encode(self.creationDate, forKey: ._creationDate) |
||||||
|
try container.encode(self.maleUnrankedValue, forKey: ._maleUnrankedValue) |
||||||
|
try container.encode(self.femaleUnrankedValue, forKey: ._femaleUnrankedValue) |
||||||
|
try container.encode(self.maleCount, forKey: ._maleCount) |
||||||
|
try container.encode(self.femaleCount, forKey: ._femaleCount) |
||||||
|
try container.encode(self.anonymousCount, forKey: ._anonymousCount) |
||||||
|
try container.encode(self.incompleteMode, forKey: ._incompleteMode) |
||||||
|
try container.encode(self.dataModelIdentifier, forKey: ._dataModelIdentifier) |
||||||
|
try container.encode(self.fileModelIdentifier, forKey: ._fileModelIdentifier) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let monthdata = other as? BaseMonthData else { return } |
||||||
|
self.id = monthdata.id |
||||||
|
self.monthKey = monthdata.monthKey |
||||||
|
self.creationDate = monthdata.creationDate |
||||||
|
self.maleUnrankedValue = monthdata.maleUnrankedValue |
||||||
|
self.femaleUnrankedValue = monthdata.femaleUnrankedValue |
||||||
|
self.maleCount = monthdata.maleCount |
||||||
|
self.femaleCount = monthdata.femaleCount |
||||||
|
self.anonymousCount = monthdata.anonymousCount |
||||||
|
self.incompleteMode = monthdata.incompleteMode |
||||||
|
self.dataModelIdentifier = monthdata.dataModelIdentifier |
||||||
|
self.fileModelIdentifier = monthdata.fileModelIdentifier |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,227 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BasePlayerRegistration: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "player-registrations" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var teamRegistration: String? = nil |
||||||
|
var firstName: String = "" |
||||||
|
var lastName: String = "" |
||||||
|
var licenceId: String? = nil |
||||||
|
var rank: Int? = nil |
||||||
|
var paymentType: PlayerPaymentType? = nil |
||||||
|
var sex: PlayerSexType? = nil |
||||||
|
var tournamentPlayed: Int? = nil |
||||||
|
var points: Double? = nil |
||||||
|
var clubName: String? = nil |
||||||
|
var ligueName: String? = nil |
||||||
|
var assimilation: String? = nil |
||||||
|
var phoneNumber: String? = nil |
||||||
|
var email: String? = nil |
||||||
|
var birthdate: String? = nil |
||||||
|
var computedRank: Int = 0 |
||||||
|
var source: PlayerRegistration.PlayerDataSource? = nil |
||||||
|
var hasArrived: Bool = false |
||||||
|
var coach: Bool = false |
||||||
|
var captain: Bool = false |
||||||
|
var registeredOnline: Bool = false |
||||||
|
var timeToConfirm: Date? = nil |
||||||
|
var registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting |
||||||
|
var paymentId: String? = nil |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
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.id = id |
||||||
|
self.teamRegistration = teamRegistration |
||||||
|
self.firstName = firstName |
||||||
|
self.lastName = lastName |
||||||
|
self.licenceId = licenceId |
||||||
|
self.rank = rank |
||||||
|
self.paymentType = paymentType |
||||||
|
self.sex = sex |
||||||
|
self.tournamentPlayed = tournamentPlayed |
||||||
|
self.points = points |
||||||
|
self.clubName = clubName |
||||||
|
self.ligueName = ligueName |
||||||
|
self.assimilation = assimilation |
||||||
|
self.phoneNumber = phoneNumber |
||||||
|
self.email = email |
||||||
|
self.birthdate = birthdate |
||||||
|
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 public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _teamRegistration = "teamRegistration" |
||||||
|
case _firstName = "firstName" |
||||||
|
case _lastName = "lastName" |
||||||
|
case _licenceId = "licenceId" |
||||||
|
case _rank = "rank" |
||||||
|
case _paymentType = "paymentType" |
||||||
|
case _sex = "sex" |
||||||
|
case _tournamentPlayed = "tournamentPlayed" |
||||||
|
case _points = "points" |
||||||
|
case _clubName = "clubName" |
||||||
|
case _ligueName = "ligueName" |
||||||
|
case _assimilation = "assimilation" |
||||||
|
case _phoneNumber = "phoneNumber" |
||||||
|
case _email = "email" |
||||||
|
case _birthdate = "birthdate" |
||||||
|
case _computedRank = "computedRank" |
||||||
|
case _source = "source" |
||||||
|
case _hasArrived = "hasArrived" |
||||||
|
case _coach = "coach" |
||||||
|
case _captain = "captain" |
||||||
|
case _registeredOnline = "registeredOnline" |
||||||
|
case _timeToConfirm = "timeToConfirm" |
||||||
|
case _registrationStatus = "registrationStatus" |
||||||
|
case _paymentId = "paymentId" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.teamRegistration = try container.decodeIfPresent(String.self, forKey: ._teamRegistration) ?? nil |
||||||
|
self.firstName = try container.decodeIfPresent(String.self, forKey: ._firstName) ?? "" |
||||||
|
self.lastName = try container.decodeIfPresent(String.self, forKey: ._lastName) ?? "" |
||||||
|
self.licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId) ?? nil |
||||||
|
self.rank = try container.decodeIfPresent(Int.self, forKey: ._rank) ?? nil |
||||||
|
self.paymentType = try container.decodeIfPresent(PlayerPaymentType.self, forKey: ._paymentType) ?? nil |
||||||
|
self.sex = try container.decodeIfPresent(PlayerSexType.self, forKey: ._sex) ?? nil |
||||||
|
self.tournamentPlayed = try container.decodeIfPresent(Int.self, forKey: ._tournamentPlayed) ?? nil |
||||||
|
self.points = try container.decodeIfPresent(Double.self, forKey: ._points) ?? nil |
||||||
|
self.clubName = try container.decodeIfPresent(String.self, forKey: ._clubName) ?? nil |
||||||
|
self.ligueName = try container.decodeIfPresent(String.self, forKey: ._ligueName) ?? nil |
||||||
|
self.assimilation = try container.decodeIfPresent(String.self, forKey: ._assimilation) ?? nil |
||||||
|
self.phoneNumber = try container.decodeIfPresent(String.self, forKey: ._phoneNumber) ?? nil |
||||||
|
self.email = try container.decodeIfPresent(String.self, forKey: ._email) ?? nil |
||||||
|
self.birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate) ?? nil |
||||||
|
self.computedRank = try container.decodeIfPresent(Int.self, forKey: ._computedRank) ?? 0 |
||||||
|
self.source = try container.decodeIfPresent(PlayerRegistration.PlayerDataSource.self, forKey: ._source) ?? nil |
||||||
|
self.hasArrived = try container.decodeIfPresent(Bool.self, forKey: ._hasArrived) ?? false |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.teamRegistration, forKey: ._teamRegistration) |
||||||
|
try container.encode(self.firstName, forKey: ._firstName) |
||||||
|
try container.encode(self.lastName, forKey: ._lastName) |
||||||
|
try container.encode(self.licenceId, forKey: ._licenceId) |
||||||
|
try container.encode(self.rank, forKey: ._rank) |
||||||
|
try container.encode(self.paymentType, forKey: ._paymentType) |
||||||
|
try container.encode(self.sex, forKey: ._sex) |
||||||
|
try container.encode(self.tournamentPlayed, forKey: ._tournamentPlayed) |
||||||
|
try container.encode(self.points, forKey: ._points) |
||||||
|
try container.encode(self.clubName, forKey: ._clubName) |
||||||
|
try container.encode(self.ligueName, forKey: ._ligueName) |
||||||
|
try container.encode(self.assimilation, forKey: ._assimilation) |
||||||
|
try container.encode(self.phoneNumber, forKey: ._phoneNumber) |
||||||
|
try container.encode(self.email, forKey: ._email) |
||||||
|
try container.encode(self.birthdate, forKey: ._birthdate) |
||||||
|
try container.encode(self.computedRank, forKey: ._computedRank) |
||||||
|
try container.encode(self.source, forKey: ._source) |
||||||
|
try container.encode(self.hasArrived, forKey: ._hasArrived) |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
func teamRegistrationValue() -> TeamRegistration? { |
||||||
|
guard let teamRegistration = self.teamRegistration else { return nil } |
||||||
|
return Store.main.findById(teamRegistration) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let playerregistration = other as? BasePlayerRegistration else { return } |
||||||
|
self.id = playerregistration.id |
||||||
|
self.teamRegistration = playerregistration.teamRegistration |
||||||
|
self.firstName = playerregistration.firstName |
||||||
|
self.lastName = playerregistration.lastName |
||||||
|
self.licenceId = playerregistration.licenceId |
||||||
|
self.rank = playerregistration.rank |
||||||
|
self.paymentType = playerregistration.paymentType |
||||||
|
self.sex = playerregistration.sex |
||||||
|
self.tournamentPlayed = playerregistration.tournamentPlayed |
||||||
|
self.points = playerregistration.points |
||||||
|
self.clubName = playerregistration.clubName |
||||||
|
self.ligueName = playerregistration.ligueName |
||||||
|
self.assimilation = playerregistration.assimilation |
||||||
|
self.phoneNumber = playerregistration.phoneNumber |
||||||
|
self.email = playerregistration.email |
||||||
|
self.birthdate = playerregistration.birthdate |
||||||
|
self.computedRank = playerregistration.computedRank |
||||||
|
self.source = playerregistration.source |
||||||
|
self.hasArrived = playerregistration.hasArrived |
||||||
|
self.coach = playerregistration.coach |
||||||
|
self.captain = playerregistration.captain |
||||||
|
self.registeredOnline = playerregistration.registeredOnline |
||||||
|
self.timeToConfirm = playerregistration.timeToConfirm |
||||||
|
self.registrationStatus = playerregistration.registrationStatus |
||||||
|
self.paymentId = playerregistration.paymentId |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: TeamRegistration.self, keyPath: \BasePlayerRegistration.teamRegistration), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,99 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
class BasePurchase: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "purchases" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: UInt64 = 0 |
||||||
|
var user: String = "" |
||||||
|
var purchaseDate: Date = Date() |
||||||
|
var productId: String = "" |
||||||
|
var quantity: Int? = nil |
||||||
|
var revocationDate: Date? = nil |
||||||
|
var expirationDate: Date? = nil |
||||||
|
|
||||||
|
init( |
||||||
|
id: UInt64 = 0, |
||||||
|
user: String = "", |
||||||
|
purchaseDate: Date = Date(), |
||||||
|
productId: String = "", |
||||||
|
quantity: Int? = nil, |
||||||
|
revocationDate: Date? = nil, |
||||||
|
expirationDate: Date? = nil |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.user = user |
||||||
|
self.purchaseDate = purchaseDate |
||||||
|
self.productId = productId |
||||||
|
self.quantity = quantity |
||||||
|
self.revocationDate = revocationDate |
||||||
|
self.expirationDate = expirationDate |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case id = "id" |
||||||
|
case user = "user" |
||||||
|
case purchaseDate = "purchaseDate" |
||||||
|
case productId = "productId" |
||||||
|
case quantity = "quantity" |
||||||
|
case revocationDate = "revocationDate" |
||||||
|
case expirationDate = "expirationDate" |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(UInt64.self, forKey: .id) ?? 0 |
||||||
|
self.user = try container.decodeEncrypted(key: .user) |
||||||
|
self.purchaseDate = try container.decodeIfPresent(Date.self, forKey: .purchaseDate) ?? Date() |
||||||
|
self.productId = try container.decodeIfPresent(String.self, forKey: .productId) ?? "" |
||||||
|
self.quantity = try container.decodeIfPresent(Int.self, forKey: .quantity) ?? nil |
||||||
|
self.revocationDate = try container.decodeIfPresent(Date.self, forKey: .revocationDate) ?? nil |
||||||
|
self.expirationDate = try container.decodeIfPresent(Date.self, forKey: .expirationDate) ?? nil |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: .id) |
||||||
|
try container.encodeAndEncryptIfPresent(self.user.data(using: .utf8), forKey: .user) |
||||||
|
try container.encode(self.purchaseDate, forKey: .purchaseDate) |
||||||
|
try container.encode(self.productId, forKey: .productId) |
||||||
|
try container.encode(self.quantity, forKey: .quantity) |
||||||
|
try container.encode(self.revocationDate, forKey: .revocationDate) |
||||||
|
try container.encode(self.expirationDate, forKey: .expirationDate) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func userValue() -> CustomUser? { |
||||||
|
return Store.main.findById(user) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let purchase = other as? BasePurchase else { return } |
||||||
|
self.id = purchase.id |
||||||
|
self.user = purchase.user |
||||||
|
self.purchaseDate = purchase.purchaseDate |
||||||
|
self.productId = purchase.productId |
||||||
|
self.quantity = purchase.quantity |
||||||
|
self.revocationDate = purchase.revocationDate |
||||||
|
self.expirationDate = purchase.expirationDate |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: CustomUser.self, keyPath: \BasePurchase.user), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,107 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseRound: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "rounds" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var tournament: String = "" |
||||||
|
var index: Int = 0 |
||||||
|
var parent: String? = nil |
||||||
|
var format: MatchFormat? = nil |
||||||
|
var startDate: Date? = nil |
||||||
|
var groupStageLoserBracket: Bool = false |
||||||
|
var loserBracketMode: LoserBracketMode = .automatic |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
tournament: String = "", |
||||||
|
index: Int = 0, |
||||||
|
parent: String? = nil, |
||||||
|
format: MatchFormat? = nil, |
||||||
|
startDate: Date? = nil, |
||||||
|
groupStageLoserBracket: Bool = false, |
||||||
|
loserBracketMode: LoserBracketMode = .automatic |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.tournament = tournament |
||||||
|
self.index = index |
||||||
|
self.parent = parent |
||||||
|
self.format = format |
||||||
|
self.startDate = startDate |
||||||
|
self.groupStageLoserBracket = groupStageLoserBracket |
||||||
|
self.loserBracketMode = loserBracketMode |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _tournament = "tournament" |
||||||
|
case _index = "index" |
||||||
|
case _parent = "parent" |
||||||
|
case _format = "format" |
||||||
|
case _startDate = "startDate" |
||||||
|
case _groupStageLoserBracket = "groupStageLoserBracket" |
||||||
|
case _loserBracketMode = "loserBracketMode" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.tournament = try container.decodeIfPresent(String.self, forKey: ._tournament) ?? "" |
||||||
|
self.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0 |
||||||
|
self.parent = try container.decodeIfPresent(String.self, forKey: ._parent) ?? nil |
||||||
|
self.format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) ?? nil |
||||||
|
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? nil |
||||||
|
self.groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false |
||||||
|
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.tournament, forKey: ._tournament) |
||||||
|
try container.encode(self.index, forKey: ._index) |
||||||
|
try container.encode(self.parent, forKey: ._parent) |
||||||
|
try container.encode(self.format, forKey: ._format) |
||||||
|
try container.encode(self.startDate, forKey: ._startDate) |
||||||
|
try container.encode(self.groupStageLoserBracket, forKey: ._groupStageLoserBracket) |
||||||
|
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentValue() -> Tournament? { |
||||||
|
return Store.main.findById(tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let round = other as? BaseRound else { return } |
||||||
|
self.id = round.id |
||||||
|
self.tournament = round.tournament |
||||||
|
self.index = round.index |
||||||
|
self.parent = round.parent |
||||||
|
self.format = round.format |
||||||
|
self.startDate = round.startDate |
||||||
|
self.groupStageLoserBracket = round.groupStageLoserBracket |
||||||
|
self.loserBracketMode = round.loserBracketMode |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Tournament.self, keyPath: \BaseRound.tournament), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,199 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseTeamRegistration: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "team-registrations" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var tournament: String = "" |
||||||
|
var groupStage: String? = nil |
||||||
|
var registrationDate: Date? = nil |
||||||
|
var callDate: Date? = nil |
||||||
|
var bracketPosition: Int? = nil |
||||||
|
var groupStagePosition: Int? = nil |
||||||
|
var comment: String? = nil |
||||||
|
var source: String? = nil |
||||||
|
var sourceValue: String? = nil |
||||||
|
var logo: String? = nil |
||||||
|
var name: String? = nil |
||||||
|
var walkOut: Bool = false |
||||||
|
var wildCardBracket: Bool = false |
||||||
|
var wildCardGroupStage: Bool = false |
||||||
|
var weight: Int = 0 |
||||||
|
var lockedWeight: Int? = nil |
||||||
|
var confirmationDate: Date? = nil |
||||||
|
var qualified: Bool = false |
||||||
|
var finalRanking: Int? = nil |
||||||
|
var pointsEarned: Int? = nil |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
tournament: String = "", |
||||||
|
groupStage: String? = nil, |
||||||
|
registrationDate: Date? = nil, |
||||||
|
callDate: Date? = nil, |
||||||
|
bracketPosition: Int? = nil, |
||||||
|
groupStagePosition: Int? = nil, |
||||||
|
comment: String? = nil, |
||||||
|
source: String? = nil, |
||||||
|
sourceValue: String? = nil, |
||||||
|
logo: String? = nil, |
||||||
|
name: String? = nil, |
||||||
|
walkOut: Bool = false, |
||||||
|
wildCardBracket: Bool = false, |
||||||
|
wildCardGroupStage: Bool = false, |
||||||
|
weight: Int = 0, |
||||||
|
lockedWeight: Int? = nil, |
||||||
|
confirmationDate: Date? = nil, |
||||||
|
qualified: Bool = false, |
||||||
|
finalRanking: Int? = nil, |
||||||
|
pointsEarned: Int? = nil |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.tournament = tournament |
||||||
|
self.groupStage = groupStage |
||||||
|
self.registrationDate = registrationDate |
||||||
|
self.callDate = callDate |
||||||
|
self.bracketPosition = bracketPosition |
||||||
|
self.groupStagePosition = groupStagePosition |
||||||
|
self.comment = comment |
||||||
|
self.source = source |
||||||
|
self.sourceValue = sourceValue |
||||||
|
self.logo = logo |
||||||
|
self.name = name |
||||||
|
self.walkOut = walkOut |
||||||
|
self.wildCardBracket = wildCardBracket |
||||||
|
self.wildCardGroupStage = wildCardGroupStage |
||||||
|
self.weight = weight |
||||||
|
self.lockedWeight = lockedWeight |
||||||
|
self.confirmationDate = confirmationDate |
||||||
|
self.qualified = qualified |
||||||
|
self.finalRanking = finalRanking |
||||||
|
self.pointsEarned = pointsEarned |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _tournament = "tournament" |
||||||
|
case _groupStage = "groupStage" |
||||||
|
case _registrationDate = "registrationDate" |
||||||
|
case _callDate = "callDate" |
||||||
|
case _bracketPosition = "bracketPosition" |
||||||
|
case _groupStagePosition = "groupStagePosition" |
||||||
|
case _comment = "comment" |
||||||
|
case _source = "source" |
||||||
|
case _sourceValue = "sourceValue" |
||||||
|
case _logo = "logo" |
||||||
|
case _name = "name" |
||||||
|
case _walkOut = "walkOut" |
||||||
|
case _wildCardBracket = "wildCardBracket" |
||||||
|
case _wildCardGroupStage = "wildCardGroupStage" |
||||||
|
case _weight = "weight" |
||||||
|
case _lockedWeight = "lockedWeight" |
||||||
|
case _confirmationDate = "confirmationDate" |
||||||
|
case _qualified = "qualified" |
||||||
|
case _finalRanking = "finalRanking" |
||||||
|
case _pointsEarned = "pointsEarned" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.tournament = try container.decodeIfPresent(String.self, forKey: ._tournament) ?? "" |
||||||
|
self.groupStage = try container.decodeIfPresent(String.self, forKey: ._groupStage) ?? nil |
||||||
|
self.registrationDate = try container.decodeIfPresent(Date.self, forKey: ._registrationDate) ?? nil |
||||||
|
self.callDate = try container.decodeIfPresent(Date.self, forKey: ._callDate) ?? nil |
||||||
|
self.bracketPosition = try container.decodeIfPresent(Int.self, forKey: ._bracketPosition) ?? nil |
||||||
|
self.groupStagePosition = try container.decodeIfPresent(Int.self, forKey: ._groupStagePosition) ?? nil |
||||||
|
self.comment = try container.decodeIfPresent(String.self, forKey: ._comment) ?? nil |
||||||
|
self.source = try container.decodeIfPresent(String.self, forKey: ._source) ?? nil |
||||||
|
self.sourceValue = try container.decodeIfPresent(String.self, forKey: ._sourceValue) ?? nil |
||||||
|
self.logo = try container.decodeIfPresent(String.self, forKey: ._logo) ?? nil |
||||||
|
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil |
||||||
|
self.walkOut = try container.decodeIfPresent(Bool.self, forKey: ._walkOut) ?? false |
||||||
|
self.wildCardBracket = try container.decodeIfPresent(Bool.self, forKey: ._wildCardBracket) ?? false |
||||||
|
self.wildCardGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._wildCardGroupStage) ?? false |
||||||
|
self.weight = try container.decodeIfPresent(Int.self, forKey: ._weight) ?? 0 |
||||||
|
self.lockedWeight = try container.decodeIfPresent(Int.self, forKey: ._lockedWeight) ?? nil |
||||||
|
self.confirmationDate = try container.decodeIfPresent(Date.self, forKey: ._confirmationDate) ?? nil |
||||||
|
self.qualified = try container.decodeIfPresent(Bool.self, forKey: ._qualified) ?? false |
||||||
|
self.finalRanking = try container.decodeIfPresent(Int.self, forKey: ._finalRanking) ?? nil |
||||||
|
self.pointsEarned = try container.decodeIfPresent(Int.self, forKey: ._pointsEarned) ?? nil |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.tournament, forKey: ._tournament) |
||||||
|
try container.encode(self.groupStage, forKey: ._groupStage) |
||||||
|
try container.encode(self.registrationDate, forKey: ._registrationDate) |
||||||
|
try container.encode(self.callDate, forKey: ._callDate) |
||||||
|
try container.encode(self.bracketPosition, forKey: ._bracketPosition) |
||||||
|
try container.encode(self.groupStagePosition, forKey: ._groupStagePosition) |
||||||
|
try container.encode(self.comment, forKey: ._comment) |
||||||
|
try container.encode(self.source, forKey: ._source) |
||||||
|
try container.encode(self.sourceValue, forKey: ._sourceValue) |
||||||
|
try container.encode(self.logo, forKey: ._logo) |
||||||
|
try container.encode(self.name, forKey: ._name) |
||||||
|
try container.encode(self.walkOut, forKey: ._walkOut) |
||||||
|
try container.encode(self.wildCardBracket, forKey: ._wildCardBracket) |
||||||
|
try container.encode(self.wildCardGroupStage, forKey: ._wildCardGroupStage) |
||||||
|
try container.encode(self.weight, forKey: ._weight) |
||||||
|
try container.encode(self.lockedWeight, forKey: ._lockedWeight) |
||||||
|
try container.encode(self.confirmationDate, forKey: ._confirmationDate) |
||||||
|
try container.encode(self.qualified, forKey: ._qualified) |
||||||
|
try container.encode(self.finalRanking, forKey: ._finalRanking) |
||||||
|
try container.encode(self.pointsEarned, forKey: ._pointsEarned) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func groupStageValue() -> GroupStage? { |
||||||
|
guard let groupStage = self.groupStage else { return nil } |
||||||
|
return self.store?.findById(groupStage) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let teamregistration = other as? BaseTeamRegistration else { return } |
||||||
|
self.id = teamregistration.id |
||||||
|
self.tournament = teamregistration.tournament |
||||||
|
self.groupStage = teamregistration.groupStage |
||||||
|
self.registrationDate = teamregistration.registrationDate |
||||||
|
self.callDate = teamregistration.callDate |
||||||
|
self.bracketPosition = teamregistration.bracketPosition |
||||||
|
self.groupStagePosition = teamregistration.groupStagePosition |
||||||
|
self.comment = teamregistration.comment |
||||||
|
self.source = teamregistration.source |
||||||
|
self.sourceValue = teamregistration.sourceValue |
||||||
|
self.logo = teamregistration.logo |
||||||
|
self.name = teamregistration.name |
||||||
|
self.walkOut = teamregistration.walkOut |
||||||
|
self.wildCardBracket = teamregistration.wildCardBracket |
||||||
|
self.wildCardGroupStage = teamregistration.wildCardGroupStage |
||||||
|
self.weight = teamregistration.weight |
||||||
|
self.lockedWeight = teamregistration.lockedWeight |
||||||
|
self.confirmationDate = teamregistration.confirmationDate |
||||||
|
self.qualified = teamregistration.qualified |
||||||
|
self.finalRanking = teamregistration.finalRanking |
||||||
|
self.pointsEarned = teamregistration.pointsEarned |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: GroupStage.self, keyPath: \BaseTeamRegistration.groupStage), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,99 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseTeamScore: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "team-scores" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var match: String = "" |
||||||
|
var teamRegistration: String? = nil |
||||||
|
var score: String? = nil |
||||||
|
var walkOut: Int? = nil |
||||||
|
var luckyLoser: Int? = nil |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
match: String = "", |
||||||
|
teamRegistration: String? = nil, |
||||||
|
score: String? = nil, |
||||||
|
walkOut: Int? = nil, |
||||||
|
luckyLoser: Int? = nil |
||||||
|
) { |
||||||
|
super.init() |
||||||
|
self.id = id |
||||||
|
self.match = match |
||||||
|
self.teamRegistration = teamRegistration |
||||||
|
self.score = score |
||||||
|
self.walkOut = walkOut |
||||||
|
self.luckyLoser = luckyLoser |
||||||
|
} |
||||||
|
required public override init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _match = "match" |
||||||
|
case _teamRegistration = "teamRegistration" |
||||||
|
case _score = "score" |
||||||
|
case _walkOut = "walkOut" |
||||||
|
case _luckyLoser = "luckyLoser" |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.match = try container.decodeIfPresent(String.self, forKey: ._match) ?? "" |
||||||
|
self.teamRegistration = try container.decodeIfPresent(String.self, forKey: ._teamRegistration) ?? nil |
||||||
|
self.score = try container.decodeIfPresent(String.self, forKey: ._score) ?? nil |
||||||
|
self.walkOut = try container.decodeIfPresent(Int.self, forKey: ._walkOut) ?? nil |
||||||
|
self.luckyLoser = try container.decodeIfPresent(Int.self, forKey: ._luckyLoser) ?? nil |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.match, forKey: ._match) |
||||||
|
try container.encode(self.teamRegistration, forKey: ._teamRegistration) |
||||||
|
try container.encode(self.score, forKey: ._score) |
||||||
|
try container.encode(self.walkOut, forKey: ._walkOut) |
||||||
|
try container.encode(self.luckyLoser, forKey: ._luckyLoser) |
||||||
|
try super.encode(to: encoder) |
||||||
|
} |
||||||
|
|
||||||
|
func matchValue() -> Match? { |
||||||
|
return self.store?.findById(match) |
||||||
|
} |
||||||
|
|
||||||
|
func teamRegistrationValue() -> TeamRegistration? { |
||||||
|
guard let teamRegistration = self.teamRegistration else { return nil } |
||||||
|
return self.store?.findById(teamRegistration) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let teamscore = other as? BaseTeamScore else { return } |
||||||
|
self.id = teamscore.id |
||||||
|
self.match = teamscore.match |
||||||
|
self.teamRegistration = teamscore.teamRegistration |
||||||
|
self.score = teamscore.score |
||||||
|
self.walkOut = teamscore.walkOut |
||||||
|
self.luckyLoser = teamscore.luckyLoser |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Match.self, keyPath: \BaseTeamScore.match), |
||||||
|
Relationship(type: TeamRegistration.self, keyPath: \BaseTeamScore.teamRegistration), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,590 @@ |
|||||||
|
// Generated by SwiftModelGenerator |
||||||
|
// Do not modify this file manually |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
class BaseTournament: SyncedModelObject, SyncedStorable { |
||||||
|
|
||||||
|
static func resourceName() -> String { return "tournaments" } |
||||||
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
static var copyServerResponse: Bool = false |
||||||
|
|
||||||
|
var id: String = Store.randomId() |
||||||
|
var event: String? = nil |
||||||
|
var name: String? = nil |
||||||
|
var startDate: Date = Date() |
||||||
|
var endDate: Date? = nil |
||||||
|
var creationDate: Date = Date() |
||||||
|
var isPrivate: Bool = false |
||||||
|
var groupStageFormat: MatchFormat? = nil |
||||||
|
var roundFormat: MatchFormat? = nil |
||||||
|
var loserRoundFormat: MatchFormat? = nil |
||||||
|
var groupStageSortMode: GroupStageOrderingMode = GroupStageOrderingMode.snake |
||||||
|
var groupStageCount: Int = 4 |
||||||
|
var rankSourceDate: Date? = nil |
||||||
|
var dayDuration: Int = 1 |
||||||
|
var teamCount: Int = 24 |
||||||
|
var teamSorting: TeamSortingType = TeamSortingType.inscriptionDate |
||||||
|
var federalCategory: TournamentCategory = TournamentCategory.men |
||||||
|
var federalLevelCategory: TournamentLevel = TournamentLevel.p100 |
||||||
|
var federalAgeCategory: FederalTournamentAge = FederalTournamentAge.senior |
||||||
|
var closedRegistrationDate: Date? = nil |
||||||
|
var groupStageAdditionalQualified: Int = 0 |
||||||
|
var courtCount: Int = 2 |
||||||
|
var prioritizeClubMembers: Bool = false |
||||||
|
var qualifiedPerGroupStage: Int = 1 |
||||||
|
var teamsPerGroupStage: Int = 4 |
||||||
|
var entryFee: Double? = nil |
||||||
|
var payment: TournamentPayment? = nil |
||||||
|
var additionalEstimationDuration: Int = 0 |
||||||
|
var isDeleted: Bool = false |
||||||
|
var isCanceled: Bool = false |
||||||
|
var publishTeams: Bool = false |
||||||
|
var publishSummons: Bool = false |
||||||
|
var publishGroupStages: Bool = false |
||||||
|
var publishBrackets: Bool = false |
||||||
|
var shouldVerifyGroupStage: Bool = false |
||||||
|
var shouldVerifyBracket: Bool = false |
||||||
|
var hideTeamsWeight: Bool = false |
||||||
|
var publishTournament: Bool = false |
||||||
|
var hidePointsEarned: Bool = false |
||||||
|
var publishRankings: Bool = false |
||||||
|
var loserBracketMode: LoserBracketMode = .automatic |
||||||
|
var initialSeedRound: Int = 0 |
||||||
|
var initialSeedCount: Int = 0 |
||||||
|
var enableOnlineRegistration: Bool = false |
||||||
|
var registrationDateLimit: Date? = nil |
||||||
|
var openingRegistrationDate: Date? = nil |
||||||
|
var waitingListLimit: Int? = nil |
||||||
|
var accountIsRequired: Bool = true |
||||||
|
var licenseIsRequired: Bool = true |
||||||
|
var minimumPlayerPerTeam: Int = 2 |
||||||
|
var maximumPlayerPerTeam: Int = 2 |
||||||
|
var information: String? = nil |
||||||
|
var umpireCustomMail: String? = nil |
||||||
|
var umpireCustomContact: String? = nil |
||||||
|
var umpireCustomPhone: String? = nil |
||||||
|
var hideUmpireMail: Bool = false |
||||||
|
var hideUmpirePhone: Bool = true |
||||||
|
var disableRankingFederalRuling: Bool = false |
||||||
|
var teamCountLimit: Bool = true |
||||||
|
var enableOnlinePayment: Bool = false |
||||||
|
var onlinePaymentIsMandatory: Bool = false |
||||||
|
var enableOnlinePaymentRefund: Bool = false |
||||||
|
var refundDateLimit: Date? = nil |
||||||
|
var stripeAccountId: String? = nil |
||||||
|
var enableTimeToConfirm: Bool = false |
||||||
|
var isCorporateTournament: Bool = false |
||||||
|
var isTemplate: Bool = false |
||||||
|
|
||||||
|
init( |
||||||
|
id: String = Store.randomId(), |
||||||
|
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 = GroupStageOrderingMode.snake, |
||||||
|
groupStageCount: Int = 4, |
||||||
|
rankSourceDate: Date? = nil, |
||||||
|
dayDuration: Int = 1, |
||||||
|
teamCount: Int = 24, |
||||||
|
teamSorting: TeamSortingType = TeamSortingType.inscriptionDate, |
||||||
|
federalCategory: TournamentCategory = TournamentCategory.men, |
||||||
|
federalLevelCategory: TournamentLevel = TournamentLevel.p100, |
||||||
|
federalAgeCategory: FederalTournamentAge = FederalTournamentAge.senior, |
||||||
|
closedRegistrationDate: Date? = nil, |
||||||
|
groupStageAdditionalQualified: Int = 0, |
||||||
|
courtCount: Int = 2, |
||||||
|
prioritizeClubMembers: Bool = false, |
||||||
|
qualifiedPerGroupStage: Int = 1, |
||||||
|
teamsPerGroupStage: Int = 4, |
||||||
|
entryFee: Double? = nil, |
||||||
|
payment: TournamentPayment? = nil, |
||||||
|
additionalEstimationDuration: Int = 0, |
||||||
|
isDeleted: Bool = false, |
||||||
|
isCanceled: Bool = false, |
||||||
|
publishTeams: Bool = false, |
||||||
|
publishSummons: Bool = false, |
||||||
|
publishGroupStages: Bool = false, |
||||||
|
publishBrackets: Bool = false, |
||||||
|
shouldVerifyGroupStage: Bool = false, |
||||||
|
shouldVerifyBracket: 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, |
||||||
|
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 |
||||||
|
self.event = event |
||||||
|
self.name = name |
||||||
|
self.startDate = startDate |
||||||
|
self.endDate = endDate |
||||||
|
self.creationDate = creationDate |
||||||
|
self.isPrivate = isPrivate |
||||||
|
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 |
||||||
|
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.payment = payment |
||||||
|
self.additionalEstimationDuration = additionalEstimationDuration |
||||||
|
self.isDeleted = isDeleted |
||||||
|
self.isCanceled = isCanceled |
||||||
|
self.publishTeams = publishTeams |
||||||
|
self.publishSummons = publishSummons |
||||||
|
self.publishGroupStages = publishGroupStages |
||||||
|
self.publishBrackets = publishBrackets |
||||||
|
self.shouldVerifyGroupStage = shouldVerifyGroupStage |
||||||
|
self.shouldVerifyBracket = shouldVerifyBracket |
||||||
|
self.hideTeamsWeight = hideTeamsWeight |
||||||
|
self.publishTournament = publishTournament |
||||||
|
self.hidePointsEarned = hidePointsEarned |
||||||
|
self.publishRankings = publishRankings |
||||||
|
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.umpireCustomPhone = umpireCustomPhone |
||||||
|
self.hideUmpireMail = hideUmpireMail |
||||||
|
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() |
||||||
|
} |
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case isCanceled = "isCanceled" |
||||||
|
case payment = "payment" |
||||||
|
case _id = "id" |
||||||
|
case _event = "event" |
||||||
|
case _name = "name" |
||||||
|
case _startDate = "startDate" |
||||||
|
case _endDate = "endDate" |
||||||
|
case _creationDate = "creationDate" |
||||||
|
case _isPrivate = "isPrivate" |
||||||
|
case _groupStageFormat = "groupStageFormat" |
||||||
|
case _roundFormat = "roundFormat" |
||||||
|
case _loserRoundFormat = "loserRoundFormat" |
||||||
|
case _groupStageSortMode = "groupStageSortMode" |
||||||
|
case _groupStageCount = "groupStageCount" |
||||||
|
case _rankSourceDate = "rankSourceDate" |
||||||
|
case _dayDuration = "dayDuration" |
||||||
|
case _teamCount = "teamCount" |
||||||
|
case _teamSorting = "teamSorting" |
||||||
|
case _federalCategory = "federalCategory" |
||||||
|
case _federalLevelCategory = "federalLevelCategory" |
||||||
|
case _federalAgeCategory = "federalAgeCategory" |
||||||
|
case _closedRegistrationDate = "closedRegistrationDate" |
||||||
|
case _groupStageAdditionalQualified = "groupStageAdditionalQualified" |
||||||
|
case _courtCount = "courtCount" |
||||||
|
case _prioritizeClubMembers = "prioritizeClubMembers" |
||||||
|
case _qualifiedPerGroupStage = "qualifiedPerGroupStage" |
||||||
|
case _teamsPerGroupStage = "teamsPerGroupStage" |
||||||
|
case _entryFee = "entryFee" |
||||||
|
case _payment = "globalId" |
||||||
|
case _additionalEstimationDuration = "additionalEstimationDuration" |
||||||
|
case _isDeleted = "isDeleted" |
||||||
|
case _isCanceled = "localId" |
||||||
|
case _publishTeams = "publishTeams" |
||||||
|
case _publishSummons = "publishSummons" |
||||||
|
case _publishGroupStages = "publishGroupStages" |
||||||
|
case _publishBrackets = "publishBrackets" |
||||||
|
case _shouldVerifyGroupStage = "shouldVerifyGroupStage" |
||||||
|
case _shouldVerifyBracket = "shouldVerifyBracket" |
||||||
|
case _hideTeamsWeight = "hideTeamsWeight" |
||||||
|
case _publishTournament = "publishTournament" |
||||||
|
case _hidePointsEarned = "hidePointsEarned" |
||||||
|
case _publishRankings = "publishRankings" |
||||||
|
case _loserBracketMode = "loserBracketMode" |
||||||
|
case _initialSeedRound = "initialSeedRound" |
||||||
|
case _initialSeedCount = "initialSeedCount" |
||||||
|
case _enableOnlineRegistration = "enableOnlineRegistration" |
||||||
|
case _registrationDateLimit = "registrationDateLimit" |
||||||
|
case _openingRegistrationDate = "openingRegistrationDate" |
||||||
|
case _waitingListLimit = "waitingListLimit" |
||||||
|
case _accountIsRequired = "accountIsRequired" |
||||||
|
case _licenseIsRequired = "licenseIsRequired" |
||||||
|
case _minimumPlayerPerTeam = "minimumPlayerPerTeam" |
||||||
|
case _maximumPlayerPerTeam = "maximumPlayerPerTeam" |
||||||
|
case _information = "information" |
||||||
|
case _umpireCustomMail = "umpireCustomMail" |
||||||
|
case _umpireCustomContact = "umpireCustomContact" |
||||||
|
case _umpireCustomPhone = "umpireCustomPhone" |
||||||
|
case _hideUmpireMail = "hideUmpireMail" |
||||||
|
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? { |
||||||
|
var data = try container.decodeIfPresent(Data.self, forKey: ._payment) |
||||||
|
if data == nil { |
||||||
|
data = try container.decodeIfPresent(Data.self, forKey: .payment) |
||||||
|
} |
||||||
|
|
||||||
|
if let data { |
||||||
|
do { |
||||||
|
let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue) |
||||||
|
let sequence = decoded.compactMap { NumberFormatter.standard.number(from: String($0))?.intValue } |
||||||
|
return TournamentPayment(rawValue: sequence[18]) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
private func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws { |
||||||
|
guard let payment else { |
||||||
|
try container.encodeNil(forKey: ._payment) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
let max: Int = TournamentPayment.allCases.count |
||||||
|
var sequence = (1...18).map { _ in Int.random(in: (0..<max)) } |
||||||
|
sequence.append(payment.rawValue) |
||||||
|
sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0..<max ))} ) |
||||||
|
|
||||||
|
let stringCombo: [String] = sequence.map { $0.formatted() } |
||||||
|
let joined: String = stringCombo.joined(separator: "") |
||||||
|
if let data = joined.data(using: .utf8) { |
||||||
|
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue) |
||||||
|
try container.encodeIfPresent(encryped, forKey: ._payment) |
||||||
|
} |
||||||
|
} |
||||||
|
private static func _decodeIscanceled(container: KeyedDecodingContainer<CodingKeys>) throws -> Bool { |
||||||
|
var data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled) |
||||||
|
if data == nil { |
||||||
|
data = try container.decodeIfPresent(Data.self, forKey: .isCanceled) |
||||||
|
} |
||||||
|
if let data { |
||||||
|
do { |
||||||
|
let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue) |
||||||
|
let sequence = decoded.compactMap { NumberFormatter.standard.number(from: String($0))?.intValue } |
||||||
|
return Bool.decodeInt(sequence[18]) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
private func _encodeIscanceled(container: inout KeyedEncodingContainer<CodingKeys>) throws { |
||||||
|
let max: Int = 9 |
||||||
|
var sequence = (1...18).map { _ in Int.random(in: (0...max)) } |
||||||
|
sequence.append(self.isCanceled.encodedValue) |
||||||
|
sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0...max ))} ) |
||||||
|
|
||||||
|
let stringCombo: [String] = sequence.map { $0.formatted() } |
||||||
|
let joined: String = stringCombo.joined(separator: "") |
||||||
|
if let data = joined.data(using: .utf8) { |
||||||
|
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue) |
||||||
|
try container.encode(encryped, forKey: ._isCanceled) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() |
||||||
|
self.event = try container.decodeIfPresent(String.self, forKey: ._event) ?? nil |
||||||
|
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil |
||||||
|
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date() |
||||||
|
self.endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? nil |
||||||
|
self.creationDate = try container.decodeIfPresent(Date.self, forKey: ._creationDate) ?? Date() |
||||||
|
self.isPrivate = try container.decodeIfPresent(Bool.self, forKey: ._isPrivate) ?? false |
||||||
|
self.groupStageFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageFormat) ?? nil |
||||||
|
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) ?? 4 |
||||||
|
self.rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate) ?? nil |
||||||
|
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.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) ?? 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 |
||||||
|
self.isDeleted = try container.decodeIfPresent(Bool.self, forKey: ._isDeleted) ?? false |
||||||
|
self.isCanceled = try Self._decodeIscanceled(container: container) |
||||||
|
self.publishTeams = try container.decodeIfPresent(Bool.self, forKey: ._publishTeams) ?? false |
||||||
|
self.publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false |
||||||
|
self.publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false |
||||||
|
self.publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false |
||||||
|
self.shouldVerifyGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyGroupStage) ?? false |
||||||
|
self.shouldVerifyBracket = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyBracket) ?? false |
||||||
|
self.hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false |
||||||
|
self.publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false |
||||||
|
self.hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false |
||||||
|
self.publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false |
||||||
|
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic |
||||||
|
self.initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0 |
||||||
|
self.initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0 |
||||||
|
self.enableOnlineRegistration = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlineRegistration) ?? false |
||||||
|
self.registrationDateLimit = try container.decodeIfPresent(Date.self, forKey: ._registrationDateLimit) ?? nil |
||||||
|
self.openingRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._openingRegistrationDate) ?? nil |
||||||
|
self.waitingListLimit = try container.decodeIfPresent(Int.self, forKey: ._waitingListLimit) ?? nil |
||||||
|
self.accountIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._accountIsRequired) ?? true |
||||||
|
self.licenseIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._licenseIsRequired) ?? true |
||||||
|
self.minimumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._minimumPlayerPerTeam) ?? 2 |
||||||
|
self.maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2 |
||||||
|
self.information = try container.decodeIfPresent(String.self, forKey: ._information) ?? nil |
||||||
|
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 |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
override func encode(to encoder: Encoder) throws { |
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
try container.encode(self.id, forKey: ._id) |
||||||
|
try container.encode(self.event, forKey: ._event) |
||||||
|
try container.encode(self.name, forKey: ._name) |
||||||
|
try container.encode(self.startDate, forKey: ._startDate) |
||||||
|
try container.encode(self.endDate, forKey: ._endDate) |
||||||
|
try container.encode(self.creationDate, forKey: ._creationDate) |
||||||
|
try container.encode(self.isPrivate, forKey: ._isPrivate) |
||||||
|
try container.encode(self.groupStageFormat, forKey: ._groupStageFormat) |
||||||
|
try container.encode(self.roundFormat, forKey: ._roundFormat) |
||||||
|
try container.encode(self.loserRoundFormat, forKey: ._loserRoundFormat) |
||||||
|
try container.encode(self.groupStageSortMode, forKey: ._groupStageSortMode) |
||||||
|
try container.encode(self.groupStageCount, forKey: ._groupStageCount) |
||||||
|
try container.encode(self.rankSourceDate, forKey: ._rankSourceDate) |
||||||
|
try container.encode(self.dayDuration, forKey: ._dayDuration) |
||||||
|
try container.encode(self.teamCount, forKey: ._teamCount) |
||||||
|
try container.encode(self.teamSorting, forKey: ._teamSorting) |
||||||
|
try container.encode(self.federalCategory, forKey: ._federalCategory) |
||||||
|
try container.encode(self.federalLevelCategory, forKey: ._federalLevelCategory) |
||||||
|
try container.encode(self.federalAgeCategory, forKey: ._federalAgeCategory) |
||||||
|
try container.encode(self.closedRegistrationDate, forKey: ._closedRegistrationDate) |
||||||
|
try container.encode(self.groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified) |
||||||
|
try container.encode(self.courtCount, forKey: ._courtCount) |
||||||
|
try container.encode(self.prioritizeClubMembers, forKey: ._prioritizeClubMembers) |
||||||
|
try container.encode(self.qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage) |
||||||
|
try container.encode(self.teamsPerGroupStage, forKey: ._teamsPerGroupStage) |
||||||
|
try container.encode(self.entryFee, forKey: ._entryFee) |
||||||
|
try _encodePayment(container: &container) |
||||||
|
try container.encode(self.additionalEstimationDuration, forKey: ._additionalEstimationDuration) |
||||||
|
try container.encode(self.isDeleted, forKey: ._isDeleted) |
||||||
|
try _encodeIscanceled(container: &container) |
||||||
|
try container.encode(self.publishTeams, forKey: ._publishTeams) |
||||||
|
try container.encode(self.publishSummons, forKey: ._publishSummons) |
||||||
|
try container.encode(self.publishGroupStages, forKey: ._publishGroupStages) |
||||||
|
try container.encode(self.publishBrackets, forKey: ._publishBrackets) |
||||||
|
try container.encode(self.shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage) |
||||||
|
try container.encode(self.shouldVerifyBracket, forKey: ._shouldVerifyBracket) |
||||||
|
try container.encode(self.hideTeamsWeight, forKey: ._hideTeamsWeight) |
||||||
|
try container.encode(self.publishTournament, forKey: ._publishTournament) |
||||||
|
try container.encode(self.hidePointsEarned, forKey: ._hidePointsEarned) |
||||||
|
try container.encode(self.publishRankings, forKey: ._publishRankings) |
||||||
|
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode) |
||||||
|
try container.encode(self.initialSeedRound, forKey: ._initialSeedRound) |
||||||
|
try container.encode(self.initialSeedCount, forKey: ._initialSeedCount) |
||||||
|
try container.encode(self.enableOnlineRegistration, forKey: ._enableOnlineRegistration) |
||||||
|
try container.encode(self.registrationDateLimit, forKey: ._registrationDateLimit) |
||||||
|
try container.encode(self.openingRegistrationDate, forKey: ._openingRegistrationDate) |
||||||
|
try container.encode(self.waitingListLimit, forKey: ._waitingListLimit) |
||||||
|
try container.encode(self.accountIsRequired, forKey: ._accountIsRequired) |
||||||
|
try container.encode(self.licenseIsRequired, forKey: ._licenseIsRequired) |
||||||
|
try container.encode(self.minimumPlayerPerTeam, forKey: ._minimumPlayerPerTeam) |
||||||
|
try container.encode(self.maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam) |
||||||
|
try container.encode(self.information, forKey: ._information) |
||||||
|
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 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) |
||||||
|
} |
||||||
|
|
||||||
|
func eventValue() -> Event? { |
||||||
|
guard let event = self.event else { return nil } |
||||||
|
return Store.main.findById(event) |
||||||
|
} |
||||||
|
|
||||||
|
func copy(from other: any Storable) { |
||||||
|
guard let tournament = other as? BaseTournament else { return } |
||||||
|
self.id = tournament.id |
||||||
|
self.event = tournament.event |
||||||
|
self.name = tournament.name |
||||||
|
self.startDate = tournament.startDate |
||||||
|
self.endDate = tournament.endDate |
||||||
|
self.creationDate = tournament.creationDate |
||||||
|
self.isPrivate = tournament.isPrivate |
||||||
|
self.groupStageFormat = tournament.groupStageFormat |
||||||
|
self.roundFormat = tournament.roundFormat |
||||||
|
self.loserRoundFormat = tournament.loserRoundFormat |
||||||
|
self.groupStageSortMode = tournament.groupStageSortMode |
||||||
|
self.groupStageCount = tournament.groupStageCount |
||||||
|
self.rankSourceDate = tournament.rankSourceDate |
||||||
|
self.dayDuration = tournament.dayDuration |
||||||
|
self.teamCount = tournament.teamCount |
||||||
|
self.teamSorting = tournament.teamSorting |
||||||
|
self.federalCategory = tournament.federalCategory |
||||||
|
self.federalLevelCategory = tournament.federalLevelCategory |
||||||
|
self.federalAgeCategory = tournament.federalAgeCategory |
||||||
|
self.closedRegistrationDate = tournament.closedRegistrationDate |
||||||
|
self.groupStageAdditionalQualified = tournament.groupStageAdditionalQualified |
||||||
|
self.courtCount = tournament.courtCount |
||||||
|
self.prioritizeClubMembers = tournament.prioritizeClubMembers |
||||||
|
self.qualifiedPerGroupStage = tournament.qualifiedPerGroupStage |
||||||
|
self.teamsPerGroupStage = tournament.teamsPerGroupStage |
||||||
|
self.entryFee = tournament.entryFee |
||||||
|
self.payment = tournament.payment |
||||||
|
self.additionalEstimationDuration = tournament.additionalEstimationDuration |
||||||
|
self.isDeleted = tournament.isDeleted |
||||||
|
self.isCanceled = tournament.isCanceled |
||||||
|
self.publishTeams = tournament.publishTeams |
||||||
|
self.publishSummons = tournament.publishSummons |
||||||
|
self.publishGroupStages = tournament.publishGroupStages |
||||||
|
self.publishBrackets = tournament.publishBrackets |
||||||
|
self.shouldVerifyGroupStage = tournament.shouldVerifyGroupStage |
||||||
|
self.shouldVerifyBracket = tournament.shouldVerifyBracket |
||||||
|
self.hideTeamsWeight = tournament.hideTeamsWeight |
||||||
|
self.publishTournament = tournament.publishTournament |
||||||
|
self.hidePointsEarned = tournament.hidePointsEarned |
||||||
|
self.publishRankings = tournament.publishRankings |
||||||
|
self.loserBracketMode = tournament.loserBracketMode |
||||||
|
self.initialSeedRound = tournament.initialSeedRound |
||||||
|
self.initialSeedCount = tournament.initialSeedCount |
||||||
|
self.enableOnlineRegistration = tournament.enableOnlineRegistration |
||||||
|
self.registrationDateLimit = tournament.registrationDateLimit |
||||||
|
self.openingRegistrationDate = tournament.openingRegistrationDate |
||||||
|
self.waitingListLimit = tournament.waitingListLimit |
||||||
|
self.accountIsRequired = tournament.accountIsRequired |
||||||
|
self.licenseIsRequired = tournament.licenseIsRequired |
||||||
|
self.minimumPlayerPerTeam = tournament.minimumPlayerPerTeam |
||||||
|
self.maximumPlayerPerTeam = tournament.maximumPlayerPerTeam |
||||||
|
self.information = tournament.information |
||||||
|
self.umpireCustomMail = tournament.umpireCustomMail |
||||||
|
self.umpireCustomContact = tournament.umpireCustomContact |
||||||
|
self.umpireCustomPhone = tournament.umpireCustomPhone |
||||||
|
self.hideUmpireMail = tournament.hideUmpireMail |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
static func relationships() -> [Relationship] { |
||||||
|
return [ |
||||||
|
Relationship(type: Event.self, keyPath: \BaseTournament.event), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "Club", |
||||||
|
"synchronizable": true, |
||||||
|
"observable": true, |
||||||
|
"copy_server_response": "true", |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "creator", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil", |
||||||
|
"foreignKey": "CustomUser" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "\"\"" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "acronym", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "\"\"" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "phone", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "code", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "address", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "city", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "zipCode", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "latitude", |
||||||
|
"type": "Double", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "longitude", |
||||||
|
"type": "Double", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "courtCount", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "2" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "broadcastCode", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "timezone", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "TimeZone.current.identifier" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "Court", |
||||||
|
"synchronizable": true, |
||||||
|
"observable": true, |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "index", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "club", |
||||||
|
"type": "String", |
||||||
|
"foreignKey": "Club" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "exitAllowed", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "indoor", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
} |
||||||
|
], |
||||||
|
"tokenExemptedMethods": [] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,177 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "CustomUser", |
||||||
|
"resource_name": "users", |
||||||
|
"synchronizable": true, |
||||||
|
"observable": true, |
||||||
|
"tokenExemptedMethods": ["post"], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "username", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "email", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "clubs", |
||||||
|
"type": "[String]", |
||||||
|
"defaultValue": "[]" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "umpireCode", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "licenceId", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "firstName", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "lastName", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "phone", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "country", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "summonsMessageBody", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "summonsMessageSignature", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "summonsAvailablePaymentMethods", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "summonsDisplayFormat", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "summonsDisplayEntryFee", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "summonsUseFullCustomMessage", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "matchFormatsDefaultDuration", |
||||||
|
"type": "[MatchFormat: Int]", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "bracketMatchFormatPreference", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStageMatchFormatPreference", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "loserBracketMatchFormatPreference", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "loserBracketMode", |
||||||
|
"type": "LoserBracketMode", |
||||||
|
"defaultValue": ".automatic" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "disableRankingFederalRuling", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "deviceId", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"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" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "DateInterval", |
||||||
|
"synchronizable": true, |
||||||
|
"observable": true, |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "event", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "courtIndex", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "startDate", |
||||||
|
"type": "Date" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "endDate", |
||||||
|
"type": "Date" |
||||||
|
} |
||||||
|
], |
||||||
|
"tokenExemptedMethods": [] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
|
||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "DrawLog", |
||||||
|
"synchronizable": true, |
||||||
|
"sideStorable": true, |
||||||
|
"observable": true, |
||||||
|
"relationshipNames": [], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "tournament", |
||||||
|
"type": "String", |
||||||
|
"foreignKey": "Tournament" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "drawDate", |
||||||
|
"type": "Date", |
||||||
|
"defaultValue": "Date()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "drawSeed", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "drawMatchIndex", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "drawTeamPosition", |
||||||
|
"type": "TeamPosition", |
||||||
|
"defaultValue": "TeamPosition.one" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "drawType", |
||||||
|
"type": "DrawType", |
||||||
|
"defaultValue": "DrawType.seed" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "Event", |
||||||
|
"synchronizable": true, |
||||||
|
"observable": true, |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "creator", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil", |
||||||
|
"foreignKey": "CustomUser" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "club", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil", |
||||||
|
"foreignKey": "Club" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "creationDate", |
||||||
|
"type": "Date", |
||||||
|
"defaultValue": "Date()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "tenupId", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
} |
||||||
|
], |
||||||
|
"tokenExemptedMethods": [] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "GroupStage", |
||||||
|
"synchronizable": true, |
||||||
|
"observable": true, |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "tournament", |
||||||
|
"type": "String", |
||||||
|
"foreignKey": "Tournament" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "index", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "size", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "format", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "startDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "step", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
} |
||||||
|
], |
||||||
|
"tokenExemptedMethods": [] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "Match", |
||||||
|
"synchronizable": true, |
||||||
|
"observable": true, |
||||||
|
"tokenExemptedMethods": [], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "round", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"foreignKey": "Round*" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStage", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"foreignKey": "GroupStage*" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "startDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "endDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "index", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "format", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "servingTeamId", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "winningTeamId", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "losingTeamId", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "disabled", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "courtIndex", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "confirmed", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,91 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "MatchScheduler", |
||||||
|
"resource_name": "match-scheduler", |
||||||
|
"synchronizable": false, |
||||||
|
"observable": true, |
||||||
|
"tokenExemptedMethods": [], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "tournament", |
||||||
|
"type": "String", |
||||||
|
"foreignKey": "Tournament" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "timeDifferenceLimit", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "5" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "loserBracketRotationDifference", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "upperBracketRotationDifference", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "accountUpperBracketBreakTime", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "true" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "accountLoserBracketBreakTime", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "randomizeCourts", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "true" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "rotationDifferenceIsImportant", |
||||||
|
"type": "Bool" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "shouldHandleUpperRoundSlice", |
||||||
|
"type": "Bool" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "shouldEndRoundBeforeStartingNext", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "true" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStageChunkCount", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "overrideCourtsUnavailability", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "shouldTryToFillUpCourtsAvailable", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "courtsAvailable", |
||||||
|
"type": "Set<Int>", |
||||||
|
"defaultValue": "Set<Int>()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "simultaneousStart", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "true" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,71 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "MonthData", |
||||||
|
"resource_name": "month-data", |
||||||
|
"synchronizable": false, |
||||||
|
"observable": true, |
||||||
|
"tokenExemptedMethods": [], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "monthKey", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "creationDate", |
||||||
|
"type": "Date" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "maleUnrankedValue", |
||||||
|
"type": "Int", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "femaleUnrankedValue", |
||||||
|
"type": "Int", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "maleCount", |
||||||
|
"type": "Int", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "femaleCount", |
||||||
|
"type": "Int", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "anonymousCount", |
||||||
|
"type": "Int", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "incompleteMode", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "dataModelIdentifier", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "fileModelIdentifier", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,138 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "PlayerRegistration", |
||||||
|
"synchronizable": true, |
||||||
|
"sideStorable": true, |
||||||
|
"observable": true, |
||||||
|
"relationshipNames": ["teamRegistration"], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "teamRegistration", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"foreignKey": "TeamRegistration" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "firstName", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "lastName", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "licenceId", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "rank", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "paymentType", |
||||||
|
"type": "PlayerPaymentType", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "sex", |
||||||
|
"type": "PlayerSexType", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "tournamentPlayed", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "points", |
||||||
|
"type": "Double", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "clubName", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "ligueName", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "assimilation", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "phoneNumber", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "email", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "birthdate", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "computedRank", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "source", |
||||||
|
"type": "PlayerRegistration.PlayerDataSource", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "hasArrived", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "coach", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "captain", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"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 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "Purchase", |
||||||
|
"synchronizable": true, |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "UInt64", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "user", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "\"\"", |
||||||
|
"encryption": "standard", |
||||||
|
"foreignKey": "CustomUser" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "purchaseDate", |
||||||
|
"type": "Date" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "productId", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "\"\"" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "quantity", |
||||||
|
"type": "Int", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "revocationDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "expirationDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil" |
||||||
|
} |
||||||
|
], |
||||||
|
"tokenExemptedMethods": [], |
||||||
|
"relationshipNames": [] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "Round", |
||||||
|
"synchronizable": true, |
||||||
|
"sideStorable": true, |
||||||
|
"observable": true, |
||||||
|
"relationshipNames": [], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "tournament", |
||||||
|
"type": "String", |
||||||
|
"foreignKey": "Tournament" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "index", |
||||||
|
"type": "Int" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "parent", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "format", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"private": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "startDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStageLoserBracket", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "loserBracketMode", |
||||||
|
"type": "LoserBracketMode", |
||||||
|
"defaultValue": ".automatic" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,118 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "TeamRegistration", |
||||||
|
"synchronizable": true, |
||||||
|
"sideStorable": true, |
||||||
|
"observable": true, |
||||||
|
"relationshipNames": [], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "tournament", |
||||||
|
"type": "String" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStage", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"foreignKey": "GroupStage*" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "registrationDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "callDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "bracketPosition", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStagePosition", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "comment", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "source", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "sourceValue", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "logo", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "walkOut", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "wildCardBracket", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "wildCardGroupStage", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "weight", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "lockedWeight", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "confirmationDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "qualified", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "finalRanking", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "pointsEarned", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "TeamScore", |
||||||
|
"synchronizable": true, |
||||||
|
"sideStorable": true, |
||||||
|
"observable": true, |
||||||
|
"relationshipNames": ["match"], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "match", |
||||||
|
"type": "String", |
||||||
|
"foreignKey": "Match*" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "teamRegistration", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"foreignKey": "TeamRegistration*" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "score", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "walkOut", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "luckyLoser", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,355 @@ |
|||||||
|
{ |
||||||
|
"models": [ |
||||||
|
{ |
||||||
|
"name": "Tournament", |
||||||
|
"synchronizable": true, |
||||||
|
"copyable": true, |
||||||
|
"observable": true, |
||||||
|
"relationshipNames": [], |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "id", |
||||||
|
"type": "String", |
||||||
|
"defaultValue": "Store.randomId()" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "event", |
||||||
|
"type": "String", |
||||||
|
"optional": true, |
||||||
|
"foreignKey": "Event" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "startDate", |
||||||
|
"type": "Date" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "endDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "creationDate", |
||||||
|
"type": "Date", |
||||||
|
"private": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "isPrivate", |
||||||
|
"type": "Bool" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStageFormat", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"private": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "roundFormat", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"private": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "loserRoundFormat", |
||||||
|
"type": "MatchFormat", |
||||||
|
"optional": true, |
||||||
|
"private": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStageSortMode", |
||||||
|
"type": "GroupStageOrderingMode", |
||||||
|
"defaultValue": "GroupStageOrderingMode.snake" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStageCount", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "4" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "rankSourceDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "dayDuration", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "teamCount", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "24" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "teamSorting", |
||||||
|
"type": "TeamSortingType", |
||||||
|
"defaultValue": "TeamSortingType.inscriptionDate" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "federalCategory", |
||||||
|
"type": "TournamentCategory", |
||||||
|
"defaultValue": "TournamentCategory.men" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "federalLevelCategory", |
||||||
|
"type": "TournamentLevel", |
||||||
|
"defaultValue": "TournamentLevel.p100" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "federalAgeCategory", |
||||||
|
"type": "FederalTournamentAge", |
||||||
|
"defaultValue": "FederalTournamentAge.senior" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "closedRegistrationDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "groupStageAdditionalQualified", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "courtCount", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "2" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "prioritizeClubMembers", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "qualifiedPerGroupStage", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "teamsPerGroupStage", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "4" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "entryFee", |
||||||
|
"type": "Double", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "payment", |
||||||
|
"type": "TournamentPayment", |
||||||
|
"optional": true, |
||||||
|
"defaultValue": "nil", |
||||||
|
"encryption": "tournament_payment", |
||||||
|
"codingKey": "globalId" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "additionalEstimationDuration", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "isDeleted", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "isCanceled", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false", |
||||||
|
"encryption": "tournament_iscanceled", |
||||||
|
"codingKey": "localId" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "publishTeams", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "publishSummons", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "publishGroupStages", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "publishBrackets", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "shouldVerifyGroupStage", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "shouldVerifyBracket", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "hideTeamsWeight", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "publishTournament", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "hidePointsEarned", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "publishRankings", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "loserBracketMode", |
||||||
|
"type": "LoserBracketMode", |
||||||
|
"defaultValue": ".automatic" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "initialSeedRound", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "initialSeedCount", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "enableOnlineRegistration", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "registrationDateLimit", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "openingRegistrationDate", |
||||||
|
"type": "Date", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "waitingListLimit", |
||||||
|
"type": "Int", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "accountIsRequired", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "true" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "licenseIsRequired", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "true" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "minimumPlayerPerTeam", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "2" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "maximumPlayerPerTeam", |
||||||
|
"type": "Int", |
||||||
|
"defaultValue": "2" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "information", |
||||||
|
"type": "String", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"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" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "disableRankingFederalRuling", |
||||||
|
"type": "Bool", |
||||||
|
"defaultValue": "false" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "teamCountLimit", |
||||||
|
"type": "Bool", |
||||||
|
"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" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,593 @@ |
|||||||
|
import json |
||||||
|
import re |
||||||
|
import os |
||||||
|
from pathlib import Path |
||||||
|
from typing import Dict, List, Any |
||||||
|
import argparse |
||||||
|
import sys |
||||||
|
import logging |
||||||
|
from datetime import datetime |
||||||
|
import inflect |
||||||
|
|
||||||
|
class SwiftModelGenerator: |
||||||
|
def __init__(self, json_data: Dict[str, Any]): |
||||||
|
self.data = json_data |
||||||
|
self.pluralizer = inflect.engine() |
||||||
|
|
||||||
|
def generate_model(self, model_data: Dict[str, Any]) -> str: |
||||||
|
model_name = model_data["name"] |
||||||
|
is_sync = model_data.get("synchronizable", False) |
||||||
|
is_observable = model_data.get("observable", False) |
||||||
|
properties = model_data["properties"] |
||||||
|
|
||||||
|
# Get protocol specific configurations |
||||||
|
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", ""] |
||||||
|
|
||||||
|
# Import statement |
||||||
|
lines.append("import Foundation") |
||||||
|
lines.append("import LeStorage") |
||||||
|
if is_observable: |
||||||
|
lines.append("import SwiftUI") |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Class declaration |
||||||
|
if is_observable: |
||||||
|
lines.append("@Observable") |
||||||
|
protocol = "SyncedStorable" if is_sync else "Storable" |
||||||
|
base_class = "SyncedModelObject" if is_sync else "BaseModelObject" |
||||||
|
lines.append(f"class Base{model_name}: {base_class}, {protocol} {{") |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Add SyncedStorable protocol requirements |
||||||
|
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted, copy_server_response)) |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Properties |
||||||
|
for prop in properties: |
||||||
|
swift_type = prop["type"] |
||||||
|
if prop.get("optional", False): |
||||||
|
swift_type += "?" |
||||||
|
default_value = prop.get("defaultValue", "nil" if prop.get("optional", False) else self._get_default_value(swift_type)) |
||||||
|
lines.append(f" var {prop['name']}: {swift_type} = {default_value}") |
||||||
|
|
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Add constructor |
||||||
|
lines.extend(self._generate_constructor(model_name, properties)) |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# CodingKeys |
||||||
|
lines.extend(self._generate_coding_keys(model_name, properties, is_observable)) |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Encryption methods |
||||||
|
encrypted_props = [p for p in properties if "encryption" in p] |
||||||
|
if encrypted_props: |
||||||
|
lines.extend(self._generate_encryption_methods(properties)) |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Codable implementation |
||||||
|
lines.extend(self._generate_decoder(model_name, properties, is_observable, is_sync)) |
||||||
|
lines.append("") |
||||||
|
lines.extend(self._generate_encoder(properties, is_observable, is_sync)) |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Foreign Key convenience |
||||||
|
foreign_key_methods = self._generate_foreign_key_methods(properties) |
||||||
|
if foreign_key_methods: |
||||||
|
lines.extend(foreign_key_methods) |
||||||
|
# lines.append("") |
||||||
|
|
||||||
|
# Copy method |
||||||
|
lines.extend(self._generate_copy_method(model_name, properties)) |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
# Add relationships function |
||||||
|
lines.extend(self._generate_relationships(model_name, properties)) |
||||||
|
lines.append("") |
||||||
|
|
||||||
|
lines.append("}") |
||||||
|
return "\n".join(lines) |
||||||
|
|
||||||
|
def _generate_constructor(self, model_name: str, properties: List[Dict[str, Any]]) -> List[str]: |
||||||
|
"""Generate a constructor with all properties as parameters with default values.""" |
||||||
|
|
||||||
|
lines = [" init("] |
||||||
|
|
||||||
|
# Generate parameter list |
||||||
|
params = [] |
||||||
|
for prop in properties: |
||||||
|
name = prop['name'] |
||||||
|
type_name = prop['type'] |
||||||
|
is_optional = prop.get("optional", False) |
||||||
|
|
||||||
|
# Always include a default value |
||||||
|
default_value = prop.get("defaultValue") |
||||||
|
if default_value is None: |
||||||
|
if is_optional: |
||||||
|
default_value = "nil" |
||||||
|
else: |
||||||
|
default_value = self._get_default_value(type_name) |
||||||
|
|
||||||
|
# Format the parameter with its type and default value |
||||||
|
param = f" {name}: {type_name}" |
||||||
|
if is_optional: |
||||||
|
param += "?" |
||||||
|
param += f" = {default_value}" |
||||||
|
params.append(param) |
||||||
|
|
||||||
|
# Join parameters with commas |
||||||
|
lines.extend([f"{param}," for param in params[:-1]]) |
||||||
|
lines.append(f"{params[-1]}") # Last parameter without comma |
||||||
|
|
||||||
|
# Constructor body |
||||||
|
lines.extend([ |
||||||
|
" ) {", |
||||||
|
" super.init()", |
||||||
|
]) |
||||||
|
|
||||||
|
# Property assignments |
||||||
|
for prop in properties: |
||||||
|
name = prop['name'] |
||||||
|
lines.append(f" self.{name} = {name}") |
||||||
|
|
||||||
|
lines.append(" }") |
||||||
|
|
||||||
|
lines.extend([ |
||||||
|
" required public override init() {", |
||||||
|
" super.init()", |
||||||
|
" }", |
||||||
|
|
||||||
|
]) |
||||||
|
|
||||||
|
|
||||||
|
return lines |
||||||
|
|
||||||
|
def _generate_foreign_key_methods(self, properties: List[Dict[str, Any]]) -> List[str]: |
||||||
|
lines = [] |
||||||
|
for prop in properties: |
||||||
|
if "foreignKey" in prop: |
||||||
|
foreign_key = prop["foreignKey"] |
||||||
|
prop_name = prop["name"] |
||||||
|
method_name = f"{prop_name}Value" |
||||||
|
is_optional = prop.get("optional", False) |
||||||
|
|
||||||
|
lines.extend([f" func {method_name}() -> {foreign_key.rstrip('*')}? {{"]) |
||||||
|
|
||||||
|
if is_optional: |
||||||
|
lines.extend([ |
||||||
|
f" guard let {prop_name} = self.{prop_name} else {{ return nil }}" |
||||||
|
]) |
||||||
|
|
||||||
|
if foreign_key.endswith("*"): |
||||||
|
foreign_key = foreign_key[:-1] # Remove the asterisk |
||||||
|
lines.extend([ |
||||||
|
f" return self.store?.findById({prop_name})" |
||||||
|
]) |
||||||
|
else: |
||||||
|
lines.extend([ |
||||||
|
f" return Store.main.findById({prop_name})" |
||||||
|
]) |
||||||
|
|
||||||
|
lines.extend([" }", ""]) # Close the method and add a blank line |
||||||
|
return lines |
||||||
|
|
||||||
|
def _generate_coding_keys(self, model_name: str, properties: List[Dict[str, Any]], is_observable: bool) -> List[str]: |
||||||
|
lines = [" enum CodingKeys: String, CodingKey {"] |
||||||
|
|
||||||
|
if model_name == 'Tournament': |
||||||
|
lines.append(" case isCanceled = \"isCanceled\"") |
||||||
|
lines.append(" case payment = \"payment\"") |
||||||
|
|
||||||
|
for prop in properties: |
||||||
|
name = prop['name'] |
||||||
|
# Add underscore prefix to case name if observable |
||||||
|
case_name = f"_{name}" if is_observable else name |
||||||
|
|
||||||
|
# Use custom codingKey if provided, otherwise use the property name |
||||||
|
coding_key_value = prop.get("codingKey", name) |
||||||
|
|
||||||
|
lines.append(f" case {case_name} = \"{coding_key_value}\"") |
||||||
|
|
||||||
|
lines.append(" }") |
||||||
|
return lines |
||||||
|
|
||||||
|
def _generate_encryption_methods(self, properties: List[Dict[str, Any]]) -> List[str]: |
||||||
|
lines = [] |
||||||
|
for prop in properties: |
||||||
|
if "encryption" in prop: |
||||||
|
name = prop['name'] |
||||||
|
enc_type = prop['encryption'] |
||||||
|
if enc_type == "tournament_payment": |
||||||
|
lines.extend([ |
||||||
|
f" private static func _decode{name.capitalize()}(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {{", |
||||||
|
f" var data = try container.decodeIfPresent(Data.self, forKey: ._{name})", |
||||||
|
" if data == nil {", |
||||||
|
" data = try container.decodeIfPresent(Data.self, forKey: .payment)", |
||||||
|
" }", |
||||||
|
" ", |
||||||
|
" if let data {", |
||||||
|
" do {", |
||||||
|
" let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)", |
||||||
|
" let sequence = decoded.compactMap { NumberFormatter.standard.number(from: String($0))?.intValue }", |
||||||
|
" return TournamentPayment(rawValue: sequence[18])", |
||||||
|
" } catch {", |
||||||
|
" Logger.error(error)", |
||||||
|
" }", |
||||||
|
" }", |
||||||
|
" return nil", |
||||||
|
" }", |
||||||
|
"", |
||||||
|
f" private func _encode{name.capitalize()}(container: inout KeyedEncodingContainer<CodingKeys>) throws {{", |
||||||
|
f" guard let {name} else {{", |
||||||
|
f" try container.encodeNil(forKey: ._{name})", |
||||||
|
" return", |
||||||
|
" }", |
||||||
|
" ", |
||||||
|
" let max: Int = TournamentPayment.allCases.count", |
||||||
|
" var sequence = (1...18).map { _ in Int.random(in: (0..<max)) }", |
||||||
|
f" sequence.append({name}.rawValue)", |
||||||
|
" sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0..<max ))} )", |
||||||
|
"", |
||||||
|
" let stringCombo: [String] = sequence.map { $0.formatted() }", |
||||||
|
" let joined: String = stringCombo.joined(separator: \"\")", |
||||||
|
" if let data = joined.data(using: .utf8) {", |
||||||
|
" let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)", |
||||||
|
f" try container.encodeIfPresent(encryped, forKey: ._{name})", |
||||||
|
" }", |
||||||
|
" }" |
||||||
|
]) |
||||||
|
elif enc_type == "tournament_iscanceled": |
||||||
|
lines.extend([ |
||||||
|
f" private static func _decode{name.capitalize()}(container: KeyedDecodingContainer<CodingKeys>) throws -> Bool {{", |
||||||
|
f" var data = try container.decodeIfPresent(Data.self, forKey: ._{name})", |
||||||
|
" if data == nil {", |
||||||
|
" data = try container.decodeIfPresent(Data.self, forKey: .isCanceled)", |
||||||
|
" }", |
||||||
|
" if let data {", |
||||||
|
" do {", |
||||||
|
" let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)", |
||||||
|
" let sequence = decoded.compactMap { NumberFormatter.standard.number(from: String($0))?.intValue }", |
||||||
|
" return Bool.decodeInt(sequence[18])", |
||||||
|
" } catch {", |
||||||
|
" Logger.error(error)", |
||||||
|
" }", |
||||||
|
" }", |
||||||
|
" return false", |
||||||
|
" }", |
||||||
|
"", |
||||||
|
f" private func _encode{name.capitalize()}(container: inout KeyedEncodingContainer<CodingKeys>) throws {{", |
||||||
|
" let max: Int = 9", |
||||||
|
" var sequence = (1...18).map { _ in Int.random(in: (0...max)) }", |
||||||
|
f" sequence.append(self.{name}.encodedValue)", |
||||||
|
" sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0...max ))} )", |
||||||
|
" ", |
||||||
|
" let stringCombo: [String] = sequence.map { $0.formatted() }", |
||||||
|
" let joined: String = stringCombo.joined(separator: \"\")", |
||||||
|
" if let data = joined.data(using: .utf8) {", |
||||||
|
" let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)", |
||||||
|
f" try container.encode(encryped, forKey: ._{name})", |
||||||
|
" }", |
||||||
|
" }" |
||||||
|
]) |
||||||
|
return lines |
||||||
|
|
||||||
|
def _generate_decoder(self, model_name: str, properties: List[Dict[str, Any]], is_observable: bool, is_sync: bool = False) -> List[str]: |
||||||
|
lines = [" required init(from decoder: Decoder) throws {", |
||||||
|
" let container = try decoder.container(keyedBy: CodingKeys.self)"] |
||||||
|
|
||||||
|
for prop in properties: |
||||||
|
name = prop['name'] |
||||||
|
type_name = prop['type'] |
||||||
|
is_optional = prop.get("optional", False) |
||||||
|
default_value = prop.get("defaultValue", "nil" if is_optional else self._get_default_value(type_name)) |
||||||
|
option = prop.get("option") |
||||||
|
|
||||||
|
# Use the correct case reference based on observable flag |
||||||
|
case_ref = f"_{name}" if is_observable else name |
||||||
|
|
||||||
|
if "encryption" in prop: |
||||||
|
enc_type = prop['encryption'] |
||||||
|
if enc_type == "standard": |
||||||
|
if is_optional: |
||||||
|
lines.append(f" self.{name} = try container.decodeEncryptedIfPresent(key: .{case_ref})") |
||||||
|
else: |
||||||
|
lines.append(f" self.{name} = try container.decodeEncrypted(key: .{case_ref})") |
||||||
|
elif enc_type in ["tournament_payment", "tournament_iscanceled"]: |
||||||
|
lines.append(f" self.{name} = try Self._decode{name.capitalize()}(container: container)") |
||||||
|
else: |
||||||
|
# Handle Date with fractional option |
||||||
|
if type_name == "Date" and option == "fractional": |
||||||
|
if is_optional: |
||||||
|
lines.append(f" if let dateString = try container.decodeIfPresent(String.self, forKey: .{case_ref}) {{") |
||||||
|
lines.append(f" self.{name} = Date.iso8601FractionalFormatter.date(from: dateString)") |
||||||
|
lines.append(" } else {") |
||||||
|
lines.append(f" self.{name} = {default_value}") |
||||||
|
lines.append(" }") |
||||||
|
else: |
||||||
|
lines.append(f" let dateString = try container.decode(String.self, forKey: .{case_ref})") |
||||||
|
lines.append(f" self.{name} = Date.iso8601FractionalFormatter.date(from: dateString) ?? {default_value}") |
||||||
|
else: |
||||||
|
lines.append(f" self.{name} = try container.decodeIfPresent({type_name}.self, forKey: .{case_ref}) ?? {default_value}") |
||||||
|
|
||||||
|
lines.append(" try super.init(from: decoder)") |
||||||
|
|
||||||
|
lines.append(" }") |
||||||
|
return lines |
||||||
|
|
||||||
|
def _generate_encoder(self, properties: List[Dict[str, Any]], is_observable: bool, is_sync: bool = False) -> List[str]: |
||||||
|
lines = [" override func encode(to encoder: Encoder) throws {", |
||||||
|
" var container = encoder.container(keyedBy: CodingKeys.self)"] |
||||||
|
|
||||||
|
for prop in properties: |
||||||
|
name = prop['name'] |
||||||
|
type_name = prop['type'] |
||||||
|
is_optional = prop.get('optional', False) |
||||||
|
option = prop.get("option") |
||||||
|
|
||||||
|
# Use the correct case reference based on observable flag |
||||||
|
case_ref = f"_{name}" if is_observable else name |
||||||
|
|
||||||
|
if "encryption" in prop: |
||||||
|
enc_type = prop['encryption'] |
||||||
|
if enc_type == "standard": |
||||||
|
if is_optional: |
||||||
|
lines.append(f" try container.encodeAndEncryptIfPresent(self.{name}?.data(using: .utf8), forKey: .{case_ref})") |
||||||
|
else: |
||||||
|
lines.append(f" try container.encodeAndEncryptIfPresent(self.{name}.data(using: .utf8), forKey: .{case_ref})") |
||||||
|
elif enc_type in ["tournament_payment", "tournament_iscanceled"]: |
||||||
|
lines.append(f" try _encode{name.capitalize()}(container: &container)") |
||||||
|
else: |
||||||
|
if type_name == "Date" and option == "fractional": |
||||||
|
if is_optional: |
||||||
|
lines.append(f" if let date = self.{name} {{") |
||||||
|
lines.append(f" try container.encode(Date.iso8601FractionalFormatter.string(from: date), forKey: .{case_ref})") |
||||||
|
lines.append(" } else {") |
||||||
|
lines.append(f" try container.encodeNil(forKey: .{case_ref})") |
||||||
|
lines.append(" }") |
||||||
|
else: |
||||||
|
lines.append(f" try container.encode(Date.iso8601FractionalFormatter.string(from: self.{name}), forKey: .{case_ref})") |
||||||
|
else: |
||||||
|
lines.append(f" try container.encode(self.{name}, forKey: .{case_ref})") |
||||||
|
|
||||||
|
lines.append(" try super.encode(to: encoder)") |
||||||
|
lines.append(" }") |
||||||
|
return lines |
||||||
|
|
||||||
|
def _generate_copy_method(self, model_name: str, properties: List[Dict[str, Any]]) -> List[str]: |
||||||
|
|
||||||
|
model_variable = model_name.lower() |
||||||
|
lines = [f" func copy(from other: any Storable) {{"] |
||||||
|
lines.append(f" guard let {model_variable} = other as? Base{model_name} else {{ return }}") |
||||||
|
|
||||||
|
for prop in properties: |
||||||
|
name = prop['name'] |
||||||
|
lines.append(f" self.{name} = {model_variable}.{name}") |
||||||
|
|
||||||
|
lines.append(" }") |
||||||
|
return lines |
||||||
|
|
||||||
|
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] |
||||||
|
methods_str = ", ".join(formatted_methods) if formatted_methods else "" |
||||||
|
|
||||||
|
return [ |
||||||
|
f" static func resourceName() -> String {{ return \"{resource_name}\" }}", |
||||||
|
f" static func tokenExemptedMethods() -> [HTTPMethod] {{ return [{methods_str}] }}", |
||||||
|
f" static var copyServerResponse: Bool = {copy_server_response}", |
||||||
|
] |
||||||
|
|
||||||
|
def _generate_relationships(self, model_name, properties: List[Dict[str, Any]]) -> List[str]: |
||||||
|
# Find all properties with foreign keys |
||||||
|
foreign_key_props = [p for p in properties if "foreignKey" in p] |
||||||
|
|
||||||
|
if not foreign_key_props: |
||||||
|
# If no foreign keys, return empty array |
||||||
|
return [ |
||||||
|
" static func relationships() -> [Relationship] {", |
||||||
|
" return []", |
||||||
|
" }" |
||||||
|
] |
||||||
|
|
||||||
|
lines = [ |
||||||
|
" static func relationships() -> [Relationship] {", |
||||||
|
" return [" |
||||||
|
] |
||||||
|
|
||||||
|
# Generate relationship entries |
||||||
|
for prop in foreign_key_props: |
||||||
|
name = prop["name"] |
||||||
|
foreign_key = prop["foreignKey"].rstrip('*') # Remove asterisk if present |
||||||
|
lines.append(f" Relationship(type: {foreign_key}.self, keyPath: \\Base{model_name}.{name}),") |
||||||
|
|
||||||
|
# Close the array and function |
||||||
|
lines.extend([ |
||||||
|
" ]", |
||||||
|
" }" |
||||||
|
]) |
||||||
|
|
||||||
|
return lines |
||||||
|
|
||||||
|
def _get_default_value(self, type_name: str) -> str: |
||||||
|
"""Get default value for non-optional types""" |
||||||
|
if "String" in type_name: |
||||||
|
return "\"\"" |
||||||
|
elif "Int" in type_name: |
||||||
|
return "0" |
||||||
|
elif "Double" in type_name: |
||||||
|
return "0.0" |
||||||
|
elif "Bool" in type_name: |
||||||
|
return "false" |
||||||
|
elif "Date" in type_name: |
||||||
|
return "Date()" |
||||||
|
else: |
||||||
|
return "nil" # For any other type |
||||||
|
|
||||||
|
def get_output_filename(self, model_name: str) -> str: |
||||||
|
"""Generate the output filename for a model.""" |
||||||
|
return f"Base{model_name}.swift" |
||||||
|
|
||||||
|
def make_resource_name(self, text): |
||||||
|
p = inflect.engine() |
||||||
|
# Split camelCase into words |
||||||
|
words = re.findall('[A-Z][^A-Z]*', text) |
||||||
|
|
||||||
|
if not words: |
||||||
|
words = [text] |
||||||
|
|
||||||
|
words = [word.lower() for word in words] |
||||||
|
words[-1] = p.plural(words[-1]) |
||||||
|
return '-'.join(words) |
||||||
|
|
||||||
|
def process_directory(input_dir: str, output_dir: str, logger: logging.Logger, dry_run: bool = False) -> int: |
||||||
|
"""Process all JSON files in the input directory and generate Swift models.""" |
||||||
|
try: |
||||||
|
input_path = validate_directory(input_dir) |
||||||
|
if not dry_run: |
||||||
|
output_path = validate_directory(output_dir, create=True) |
||||||
|
|
||||||
|
json_files = list(input_path.glob("*.json")) |
||||||
|
if not json_files: |
||||||
|
logger.warning(f"No JSON files found in '{input_dir}'") |
||||||
|
return 0 |
||||||
|
|
||||||
|
logger.info(f"Found {len(json_files)} JSON files to process") |
||||||
|
successful_files = 0 |
||||||
|
|
||||||
|
for json_file in json_files: |
||||||
|
try: |
||||||
|
with open(json_file, 'r') as f: |
||||||
|
json_data = json.load(f) |
||||||
|
|
||||||
|
generator = SwiftModelGenerator(json_data) |
||||||
|
|
||||||
|
# Generate each model in the JSON file |
||||||
|
for model in json_data["models"]: |
||||||
|
model_name = model["name"] |
||||||
|
swift_code = generator.generate_model(model) |
||||||
|
|
||||||
|
if dry_run: |
||||||
|
logger.info(f"Would generate Base{model_name}.swift") |
||||||
|
continue |
||||||
|
|
||||||
|
# Write to output file with Base prefix |
||||||
|
output_file = output_path / generator.get_output_filename(model_name) |
||||||
|
with open(output_file, 'w') as f: |
||||||
|
f.write(swift_code) |
||||||
|
|
||||||
|
logger.info(f"Generated Base{model_name}.swift") |
||||||
|
|
||||||
|
successful_files += 1 |
||||||
|
|
||||||
|
except json.JSONDecodeError as e: |
||||||
|
logger.error(f"Error parsing {json_file.name}: {e}") |
||||||
|
except KeyError as e: |
||||||
|
logger.error(f"Missing required key in {json_file.name}: {e}") |
||||||
|
except Exception as e: |
||||||
|
logger.error(f"Error processing {json_file.name}: {e}") |
||||||
|
|
||||||
|
return successful_files |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
logger.error(f"Fatal error: {e}") |
||||||
|
return 0 |
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(verbose: bool) -> logging.Logger: |
||||||
|
"""Configure logging based on verbosity level.""" |
||||||
|
logger = logging.getLogger('SwiftModelGenerator') |
||||||
|
handler = logging.StreamHandler() |
||||||
|
|
||||||
|
if verbose: |
||||||
|
logger.setLevel(logging.DEBUG) |
||||||
|
handler.setFormatter(logging.Formatter( |
||||||
|
'%(asctime)s - %(levelname)s - %(message)s' |
||||||
|
)) |
||||||
|
else: |
||||||
|
logger.setLevel(logging.INFO) |
||||||
|
handler.setFormatter(logging.Formatter('%(message)s')) |
||||||
|
|
||||||
|
logger.addHandler(handler) |
||||||
|
return logger |
||||||
|
|
||||||
|
def validate_directory(path: str, create: bool = False) -> Path: |
||||||
|
"""Validate and optionally create a directory.""" |
||||||
|
dir_path = Path(path) |
||||||
|
if dir_path.exists(): |
||||||
|
if not dir_path.is_dir(): |
||||||
|
raise ValueError(f"'{path}' exists but is not a directory") |
||||||
|
elif create: |
||||||
|
dir_path.mkdir(parents=True) |
||||||
|
else: |
||||||
|
raise ValueError(f"Directory '{path}' does not exist") |
||||||
|
return dir_path |
||||||
|
|
||||||
|
def main(): |
||||||
|
parser = argparse.ArgumentParser( |
||||||
|
description="Generate Swift model classes from JSON definitions", |
||||||
|
epilog="Example: %(prog)s -i ./model_definitions -o ../MyProject/Models" |
||||||
|
) |
||||||
|
|
||||||
|
parser.add_argument( |
||||||
|
"-i", "--input-dir", |
||||||
|
required=True, |
||||||
|
help="Directory containing JSON model definitions" |
||||||
|
) |
||||||
|
|
||||||
|
parser.add_argument( |
||||||
|
"-o", "--output-dir", |
||||||
|
required=True, |
||||||
|
help="Directory where Swift files will be generated" |
||||||
|
) |
||||||
|
|
||||||
|
parser.add_argument( |
||||||
|
"-v", "--verbose", |
||||||
|
action="store_true", |
||||||
|
help="Enable verbose output" |
||||||
|
) |
||||||
|
|
||||||
|
parser.add_argument( |
||||||
|
"--dry-run", |
||||||
|
action="store_true", |
||||||
|
help="Show what would be generated without actually creating files" |
||||||
|
) |
||||||
|
|
||||||
|
parser.add_argument( |
||||||
|
"--version", |
||||||
|
action="version", |
||||||
|
version="%(prog)s 1.0.0" |
||||||
|
) |
||||||
|
|
||||||
|
args = parser.parse_args() |
||||||
|
|
||||||
|
# Setup logging |
||||||
|
logger = setup_logging(args.verbose) |
||||||
|
|
||||||
|
# Process the directories |
||||||
|
start_time = datetime.now() |
||||||
|
logger.info("Starting model generation...") |
||||||
|
|
||||||
|
successful_files = process_directory( |
||||||
|
args.input_dir, |
||||||
|
args.output_dir, |
||||||
|
logger, |
||||||
|
args.dry_run |
||||||
|
) |
||||||
|
|
||||||
|
# Report results |
||||||
|
duration = datetime.now() - start_time |
||||||
|
logger.info(f"\nGeneration completed in {duration.total_seconds():.2f} seconds") |
||||||
|
logger.info(f"Successfully processed {successful_files} files") |
||||||
|
|
||||||
|
# Return appropriate exit code |
||||||
|
sys.exit(0 if successful_files > 0 else 1) |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
||||||
@ -0,0 +1,684 @@ |
|||||||
|
// |
||||||
|
// GroupStage.swift |
||||||
|
// Padel Tournament |
||||||
|
// |
||||||
|
// Created by razmig on 10/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import Algorithms |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class GroupStage: BaseGroupStage, SideStorable { |
||||||
|
|
||||||
|
var matchFormat: MatchFormat { |
||||||
|
get { |
||||||
|
format ?? .defaultFormatForMatchType(.groupStage) |
||||||
|
} |
||||||
|
set { |
||||||
|
format = newValue |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentStore: TournamentStore? { |
||||||
|
return TournamentLibrary.shared.store(tournamentId: self.tournament) |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - Computed dependencies |
||||||
|
|
||||||
|
func _matches() -> [Match] { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return [] } |
||||||
|
return tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index) |
||||||
|
// Store.main.filter { $0.groupStage == self.id } |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentObject() -> Tournament? { |
||||||
|
Store.main.findById(self.tournament) |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - |
||||||
|
|
||||||
|
func teamAt(groupStagePosition: Int) -> TeamRegistration? { |
||||||
|
if step > 0 { |
||||||
|
return teams().first(where: { $0.groupStagePositionAtStep(step) == groupStagePosition }) |
||||||
|
} |
||||||
|
return teams().first(where: { $0.groupStagePosition == groupStagePosition }) |
||||||
|
} |
||||||
|
|
||||||
|
func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String { |
||||||
|
if let name { return name } |
||||||
|
|
||||||
|
var stepLabel = "" |
||||||
|
if step > 0 { |
||||||
|
stepLabel = " (" + (step + 1).ordinalFormatted(feminine: true) + " phase)" |
||||||
|
} |
||||||
|
|
||||||
|
switch displayStyle { |
||||||
|
case .title: |
||||||
|
return "Poule \(index + 1)" + stepLabel |
||||||
|
case .wide: |
||||||
|
return "Poule \(index + 1)" |
||||||
|
case .short: |
||||||
|
return "#\(index + 1)" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var computedOrder: Int { |
||||||
|
index + step * 100 |
||||||
|
} |
||||||
|
|
||||||
|
func isRunning() -> Bool { // at least a match has started |
||||||
|
_matches().anySatisfy({ $0.isRunning() }) |
||||||
|
} |
||||||
|
|
||||||
|
func hasStarted() -> Bool { // meaning at least one match is over |
||||||
|
_matches().filter { $0.hasEnded() }.isEmpty == false |
||||||
|
} |
||||||
|
|
||||||
|
func hasEnded() -> Bool { |
||||||
|
let _matches = _matches() |
||||||
|
if _matches.isEmpty { return false } |
||||||
|
//guard teams().count == size else { return false } |
||||||
|
return _matches.anySatisfy { $0.hasEnded() == false } == false |
||||||
|
} |
||||||
|
|
||||||
|
fileprivate func _createMatch(index: Int) -> Match { |
||||||
|
let match: Match = Match(groupStage: self.id, |
||||||
|
index: index, |
||||||
|
format: self.matchFormat, |
||||||
|
name: self.localizedMatchUpLabel(for: index)) |
||||||
|
match.store = self.store |
||||||
|
// print("_createMatch(index)", index) |
||||||
|
return match |
||||||
|
} |
||||||
|
|
||||||
|
func removeReturnMatches(onlyLast: Bool = false) { |
||||||
|
|
||||||
|
var returnMatches = _matches().filter({ $0.index >= matchCount }) |
||||||
|
if onlyLast { |
||||||
|
let matchPhaseCount = matchPhaseCount - 1 |
||||||
|
returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount }) |
||||||
|
} |
||||||
|
do { |
||||||
|
try self.tournamentStore?.matches.delete(contentOfs: returnMatches) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var matchPhaseCount: Int { |
||||||
|
let count = _matches().count |
||||||
|
if matchCount > 0 { |
||||||
|
return count / matchCount |
||||||
|
} else { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func addReturnMatches() { |
||||||
|
var teamScores = [TeamScore]() |
||||||
|
var matches = [Match]() |
||||||
|
let matchPhaseCount = matchPhaseCount |
||||||
|
for i in 0..<_numberOfMatchesToBuild() { |
||||||
|
let newMatch = self._createMatch(index: i + matchCount * matchPhaseCount) |
||||||
|
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) |
||||||
|
teamScores.append(contentsOf: newMatch.createTeamScores()) |
||||||
|
matches.append(newMatch) |
||||||
|
} |
||||||
|
|
||||||
|
self.tournamentStore?.matches.addOrUpdate(contentOfs: matches) |
||||||
|
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores) |
||||||
|
} |
||||||
|
|
||||||
|
func buildMatches(keepExistingMatches: Bool = false) { |
||||||
|
var teamScores = [TeamScore]() |
||||||
|
var matches = [Match]() |
||||||
|
clearScoreCache() |
||||||
|
|
||||||
|
if keepExistingMatches == false { |
||||||
|
_removeMatches() |
||||||
|
|
||||||
|
for i in 0..<_numberOfMatchesToBuild() { |
||||||
|
let newMatch = self._createMatch(index: i) |
||||||
|
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) |
||||||
|
teamScores.append(contentsOf: newMatch.createTeamScores()) |
||||||
|
matches.append(newMatch) |
||||||
|
} |
||||||
|
} else { |
||||||
|
for match in _matches() { |
||||||
|
match.resetTeamScores(outsideOf: []) |
||||||
|
teamScores.append(contentsOf: match.createTeamScores()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
do { |
||||||
|
try self.tournamentStore?.matches.addOrUpdate(contentOfs: matches) |
||||||
|
try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func playedMatches() -> [Match] { |
||||||
|
let ordered = _matches() |
||||||
|
let order = _matchOrder() |
||||||
|
let matchCount = max(1, matchCount) |
||||||
|
let count = ordered.count / matchCount |
||||||
|
if ordered.isEmpty == false && ordered.count % order.count == 0 { |
||||||
|
let repeatedArray = (0..<count).flatMap { i in |
||||||
|
order.map { $0 + i * order.count } |
||||||
|
} |
||||||
|
let result = repeatedArray.map { ordered[$0] } |
||||||
|
return result |
||||||
|
} else { |
||||||
|
return ordered |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func orderedIndexOfMatch(_ match: Match) -> Int { |
||||||
|
_matchOrder()[safe: match.index] ?? match.index |
||||||
|
} |
||||||
|
|
||||||
|
func updateGroupStageState() { |
||||||
|
clearScoreCache() |
||||||
|
|
||||||
|
if hasEnded(), let tournament = tournamentObject() { |
||||||
|
|
||||||
|
let teams = teams(true) |
||||||
|
for (index, team) in teams.enumerated() { |
||||||
|
team.qualified = index < tournament.qualifiedPerGroupStage |
||||||
|
if team.bracketPosition != nil && team.qualified == false { |
||||||
|
tournamentObject()?.shouldVerifyBracket = true |
||||||
|
} |
||||||
|
} |
||||||
|
try self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) |
||||||
|
if let tournamentObject = tournamentObject() { |
||||||
|
try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject) |
||||||
|
} |
||||||
|
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) |
||||||
|
|
||||||
|
let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0) |
||||||
|
let nextStepGroupStages = tournament.groupStages(atStep: 1) |
||||||
|
let groupStagesAreOverAtSecondStep = tournament.groupStagesAreOver(atStep: 1) |
||||||
|
|
||||||
|
if groupStagesAreOverAtFirstStep, nextStepGroupStages.isEmpty || groupStagesAreOverAtSecondStep == true, tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty { |
||||||
|
tournament.endDate = Date() |
||||||
|
DataStore.shared.tournaments.addOrUpdate(instance: tournament) |
||||||
|
} |
||||||
|
|
||||||
|
tournament.updateTournamentState() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? { |
||||||
|
if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) { |
||||||
|
let hideSetDifference = matchFormat.setsToWin == 1 |
||||||
|
let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " set" + scoreData.setDifference.pluralSuffix |
||||||
|
let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " jeu" + scoreData.gameDifference.localizedPluralSuffix("x") |
||||||
|
return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference) |
||||||
|
// return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 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 } |
||||||
|
// let wins = matches.filter { $0.winningTeamId == team.id }.count |
||||||
|
// let loses = matches.filter { $0.losingTeamId == team.id }.count |
||||||
|
// let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } |
||||||
|
// let setDifference = differences.map { $0.set }.reduce(0,+) |
||||||
|
// let gameDifference = differences.map { $0.game }.reduce(0,+) |
||||||
|
// return (team, wins, loses, setDifference, gameDifference) |
||||||
|
// /* |
||||||
|
// • 2 points par rencontre gagnée |
||||||
|
// • 1 point par rencontre perdue |
||||||
|
// • -1 point en cas de rencontre perdue par disqualification (scores de 6/0 6/0 attribués aux trois matchs) |
||||||
|
// • -2 points en cas de rencontre perdu par WO (scores de 6/0 6/0 attribués aux trois matchs) |
||||||
|
// */ |
||||||
|
// } |
||||||
|
// |
||||||
|
func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] { |
||||||
|
let combos = Array((0..<size).combinations(ofCount: 2)) |
||||||
|
var matchIndexes = [Int]() |
||||||
|
for (index, combo) in combos.enumerated() { |
||||||
|
if combo.contains(groupStagePosition) { //team is playing |
||||||
|
matchIndexes.append(index) |
||||||
|
} |
||||||
|
} |
||||||
|
return _matches().filter { matchIndexes.contains($0.index%matchCount) } |
||||||
|
} |
||||||
|
|
||||||
|
func initialStartDate(forTeam team: TeamRegistration) -> Date? { |
||||||
|
guard let groupStagePosition = team.groupStagePositionAtStep(step) else { return nil } |
||||||
|
return matches(forGroupStagePosition: groupStagePosition).compactMap({ $0.startDate }).sorted().first ?? startDate |
||||||
|
} |
||||||
|
|
||||||
|
func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? { |
||||||
|
if groupStagePosition == againstPosition { return nil } |
||||||
|
let combos = Array((0..<size).combinations(ofCount: 2)) |
||||||
|
var matchIndexes = [Int]() |
||||||
|
for (index, combo) in combos.enumerated() { |
||||||
|
if combo.contains(groupStagePosition) && combo.contains(againstPosition) { //teams are playing |
||||||
|
matchIndexes.append(index) |
||||||
|
} |
||||||
|
} |
||||||
|
return _matches().first(where: { matchIndexes.contains($0.index) }) |
||||||
|
} |
||||||
|
|
||||||
|
func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { |
||||||
|
#if _DEBUG_TIME //DEBUGING TIME |
||||||
|
let start = Date() |
||||||
|
defer { |
||||||
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||||
|
print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||||
|
} |
||||||
|
#endif |
||||||
|
return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting) |
||||||
|
} |
||||||
|
|
||||||
|
func runningMatches(playedMatches: [Match]) -> [Match] { |
||||||
|
#if _DEBUG_TIME //DEBUGING TIME |
||||||
|
let start = Date() |
||||||
|
defer { |
||||||
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||||
|
print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||||
|
} |
||||||
|
#endif |
||||||
|
return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) |
||||||
|
} |
||||||
|
|
||||||
|
func readyMatches(playedMatches: [Match]) -> [Match] { |
||||||
|
#if _DEBUG_TIME //DEBUGING TIME |
||||||
|
let start = Date() |
||||||
|
defer { |
||||||
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||||
|
print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||||
|
} |
||||||
|
#endif |
||||||
|
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) |
||||||
|
} |
||||||
|
|
||||||
|
func finishedMatches(playedMatches: [Match]) -> [Match] { |
||||||
|
#if _DEBUG_TIME //DEBUGING TIME |
||||||
|
let start = Date() |
||||||
|
defer { |
||||||
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||||
|
print("func group stage finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||||
|
} |
||||||
|
#endif |
||||||
|
return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() |
||||||
|
} |
||||||
|
|
||||||
|
func isReturnMatchEnabled() -> Bool { |
||||||
|
_matches().count > matchCount |
||||||
|
} |
||||||
|
|
||||||
|
private func _matchOrder() -> [Int] { |
||||||
|
var order: [Int] |
||||||
|
|
||||||
|
switch size { |
||||||
|
case 3: |
||||||
|
order = [1, 2, 0] |
||||||
|
case 4: |
||||||
|
order = [2, 3, 1, 4, 5, 0] |
||||||
|
case 5: |
||||||
|
order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] |
||||||
|
case 6: |
||||||
|
order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] |
||||||
|
default: |
||||||
|
order = [] |
||||||
|
} |
||||||
|
|
||||||
|
return order |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
func indexOf(_ matchIndex: Int) -> Int { |
||||||
|
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex |
||||||
|
} |
||||||
|
|
||||||
|
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 { |
||||||
|
if matchCount > 0 { |
||||||
|
let count = _matches().count |
||||||
|
if count > matchCount * 2 { |
||||||
|
return " - vague \((matchIndex / matchCount) + 1)" |
||||||
|
} |
||||||
|
|
||||||
|
if matchIndex >= matchCount { |
||||||
|
return " - retour" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
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) |
||||||
|
} else { |
||||||
|
return "--" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var matchCount: Int { |
||||||
|
(size * (size - 1)) / 2 |
||||||
|
} |
||||||
|
|
||||||
|
func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { |
||||||
|
let _teams = _teams(for: matchIndex) |
||||||
|
switch team { |
||||||
|
case .one: |
||||||
|
return _teams.first ?? nil |
||||||
|
case .two: |
||||||
|
return _teams.last ?? nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private func _teams(for matchIndex: Int) -> [TeamRegistration?] { |
||||||
|
let combinations = Array(0..<size).combinations(ofCount: 2).map {$0} |
||||||
|
return combinations[safe: matchIndex%matchCount]?.map { teamAt(groupStagePosition: $0) } ?? [] |
||||||
|
} |
||||||
|
|
||||||
|
private func _removeMatches() { |
||||||
|
self.tournamentStore?.matches.delete(contentOfs: _matches()) |
||||||
|
} |
||||||
|
|
||||||
|
private func _numberOfMatchesToBuild() -> Int { |
||||||
|
(size * (size - 1)) / 2 |
||||||
|
} |
||||||
|
|
||||||
|
func unsortedPlayers() -> [PlayerRegistration] { |
||||||
|
unsortedTeams().flatMap({ $0.unsortedPlayers() }) |
||||||
|
} |
||||||
|
|
||||||
|
typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool |
||||||
|
|
||||||
|
typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) |
||||||
|
|
||||||
|
fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { |
||||||
|
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() |
||||||
|
let combos = Array((0..<size).combinations(ofCount: 2)) |
||||||
|
let matchIndexes = combos.enumerated().compactMap { $0.element == indexes ? $0.offset : nil } |
||||||
|
let matches = _matches().filter { matchIndexes.contains($0.index) } |
||||||
|
if matches.count > 1 { |
||||||
|
let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!) |
||||||
|
let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!) |
||||||
|
|
||||||
|
let teamsSorted = [scoreA, scoreB].sorted { (lhs, rhs) in |
||||||
|
let predicates: [TeamScoreAreInIncreasingOrder] = [ |
||||||
|
{ $0.wins < $1.wins }, |
||||||
|
{ $0.setDifference < $1.setDifference }, |
||||||
|
{ $0.gameDifference < $1.gameDifference}, |
||||||
|
{ [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } |
||||||
|
] |
||||||
|
|
||||||
|
for predicate in predicates { |
||||||
|
if !predicate(lhs, rhs) && !predicate(rhs, lhs) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
return predicate(lhs, rhs) |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
}.map({ $0.team }) |
||||||
|
|
||||||
|
return teamsSorted.first == teamPosition |
||||||
|
} else { |
||||||
|
|
||||||
|
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) { |
||||||
|
return teamPosition.id == match.losingTeamId |
||||||
|
} else { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func unsortedTeams() -> [TeamRegistration] { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return [] } |
||||||
|
if step > 0 { |
||||||
|
return tournamentStore.groupStages.filter({ $0.step == step - 1 }).compactMap({ $0.teams(true)[safe: index] }) |
||||||
|
} |
||||||
|
return tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
var scoreCache: [Int: TeamGroupStageScore] = [:] |
||||||
|
|
||||||
|
func computedScore(forTeam team: TeamRegistration, step: Int = 0) -> TeamGroupStageScore? { |
||||||
|
guard let groupStagePositionAtStep = team.groupStagePositionAtStep(step) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
if let cachedScore = scoreCache[groupStagePositionAtStep] { |
||||||
|
return cachedScore |
||||||
|
} else { |
||||||
|
let score = _score(forGroupStagePosition: groupStagePositionAtStep) |
||||||
|
if let score = score { |
||||||
|
scoreCache[groupStagePositionAtStep] = score |
||||||
|
} |
||||||
|
return score |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { |
||||||
|
if sortedByScore { |
||||||
|
return unsortedTeams().compactMap({ team in |
||||||
|
// Check cache or use provided scores, otherwise calculate and store in cache |
||||||
|
scores?.first(where: { $0.team.id == team.id }) ?? { |
||||||
|
return computedScore(forTeam: team, step: step) |
||||||
|
}() |
||||||
|
}).sorted { (lhs, rhs) in |
||||||
|
let predicates: [TeamScoreAreInIncreasingOrder] = [ |
||||||
|
{ $0.wins < $1.wins }, |
||||||
|
{ $0.setDifference < $1.setDifference }, |
||||||
|
{ $0.gameDifference < $1.gameDifference}, |
||||||
|
{ self._headToHead($0.team, $1.team) }, |
||||||
|
{ [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } |
||||||
|
] |
||||||
|
|
||||||
|
for predicate in predicates { |
||||||
|
if !predicate(lhs, rhs) && !predicate(rhs, lhs) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
return predicate(lhs, rhs) |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
}.map({ $0.team }).reversed() |
||||||
|
} else { |
||||||
|
return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { |
||||||
|
// Check if the score for this position is already cached |
||||||
|
if let cachedScore = scoreCache[groupStagePosition] { |
||||||
|
return cachedScore |
||||||
|
} |
||||||
|
|
||||||
|
guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } |
||||||
|
let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) |
||||||
|
if matches.isEmpty && nilIfEmpty { return nil } |
||||||
|
let score = calculateScore(for: team, matches: matches, groupStagePosition: groupStagePosition) |
||||||
|
scoreCache[groupStagePosition] = score |
||||||
|
return score |
||||||
|
} |
||||||
|
|
||||||
|
private func calculateScore(for team: TeamRegistration, matches: [Match], groupStagePosition: Int) -> TeamGroupStageScore { |
||||||
|
let wins = matches.filter { $0.winningTeamId == team.id }.count |
||||||
|
let loses = matches.filter { $0.losingTeamId == team.id }.count |
||||||
|
let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } |
||||||
|
let setDifference = differences.map { $0.set }.reduce(0,+) |
||||||
|
let gameDifference = differences.map { $0.game }.reduce(0,+) |
||||||
|
|
||||||
|
return (team, wins, loses, setDifference, gameDifference) |
||||||
|
} |
||||||
|
|
||||||
|
// Clear the cache if necessary, for example when starting a new step or when matches update |
||||||
|
func clearScoreCache() { |
||||||
|
scoreCache.removeAll() |
||||||
|
} |
||||||
|
|
||||||
|
// 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)!) |
||||||
|
// }).sorted { (lhs, rhs) in |
||||||
|
// // Calculate intermediate values once and reuse them |
||||||
|
// let lhsWins = lhs.wins |
||||||
|
// let rhsWins = rhs.wins |
||||||
|
// let lhsSetDifference = lhs.setDifference |
||||||
|
// let rhsSetDifference = rhs.setDifference |
||||||
|
// let lhsGameDifference = lhs.gameDifference |
||||||
|
// let rhsGameDifference = rhs.gameDifference |
||||||
|
// let lhsHeadToHead = self._headToHead(lhs.team, rhs.team) |
||||||
|
// let rhsHeadToHead = self._headToHead(rhs.team, lhs.team) |
||||||
|
// let lhsGroupStagePosition = lhs.team.groupStagePositionAtStep(self.step)! |
||||||
|
// let rhsGroupStagePosition = rhs.team.groupStagePositionAtStep(self.step)! |
||||||
|
// |
||||||
|
// // Define comparison predicates in the same order |
||||||
|
// let predicates: [(Bool, Bool)] = [ |
||||||
|
// (lhsWins < rhsWins, lhsWins > rhsWins), |
||||||
|
// (lhsSetDifference < rhsSetDifference, lhsSetDifference > rhsSetDifference), |
||||||
|
// (lhsGameDifference < rhsGameDifference, lhsGameDifference > rhsGameDifference), |
||||||
|
// (lhsHeadToHead, rhsHeadToHead), |
||||||
|
// (lhsGroupStagePosition > rhsGroupStagePosition, lhsGroupStagePosition < rhsGroupStagePosition) |
||||||
|
// ] |
||||||
|
// |
||||||
|
// // Iterate over predicates and return as soon as a valid comparison is found |
||||||
|
// for (lhsPredicate, rhsPredicate) in predicates { |
||||||
|
// if lhsPredicate { return true } |
||||||
|
// if rhsPredicate { return false } |
||||||
|
// } |
||||||
|
// |
||||||
|
// return false |
||||||
|
// }.map({ $0.team }).reversed() |
||||||
|
// } else { |
||||||
|
// return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
func updateMatchFormat(_ updatedMatchFormat: MatchFormat) { |
||||||
|
self.matchFormat = updatedMatchFormat |
||||||
|
self.updateAllMatchesFormat() |
||||||
|
} |
||||||
|
|
||||||
|
func updateAllMatchesFormat() { |
||||||
|
let playedMatches = playedMatches() |
||||||
|
playedMatches.forEach { match in |
||||||
|
match.matchFormat = matchFormat |
||||||
|
} |
||||||
|
self.tournamentStore?.matches.addOrUpdate(contentOfs: playedMatches) |
||||||
|
} |
||||||
|
|
||||||
|
func pasteData() -> String { |
||||||
|
var data: [String] = [] |
||||||
|
data.append(self.groupStageTitle()) |
||||||
|
teams().forEach { team in |
||||||
|
data.append(team.teamLabelRanked(displayRank: true, displayTeamName: true)) |
||||||
|
} |
||||||
|
|
||||||
|
return data.joined(separator: "\n") |
||||||
|
} |
||||||
|
|
||||||
|
func finalPosition(ofTeam team: TeamRegistration) -> Int? { |
||||||
|
guard hasEnded() else { return nil } |
||||||
|
return teams(true).firstIndex(of: team) |
||||||
|
} |
||||||
|
|
||||||
|
override func deleteDependencies() { |
||||||
|
let matches = self._matches() |
||||||
|
for match in matches { |
||||||
|
match.deleteDependencies() |
||||||
|
} |
||||||
|
self.tournamentStore?.matches.deleteDependencies(matches) |
||||||
|
} |
||||||
|
|
||||||
|
// init(from decoder: Decoder) throws { |
||||||
|
// let container = try decoder.container(keyedBy: CodingKeys.self) |
||||||
|
// |
||||||
|
// id = try container.decode(String.self, forKey: ._id) |
||||||
|
// storeId = try container.decode(String.self, forKey: ._storeId) |
||||||
|
// lastUpdate = try container.decode(Date.self, forKey: ._lastUpdate) |
||||||
|
// tournament = try container.decode(String.self, forKey: ._tournament) |
||||||
|
// index = try container.decode(Int.self, forKey: ._index) |
||||||
|
// size = try container.decode(Int.self, forKey: ._size) |
||||||
|
// format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) |
||||||
|
// startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) |
||||||
|
// name = try container.decodeIfPresent(String.self, forKey: ._name) |
||||||
|
// step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0 |
||||||
|
// } |
||||||
|
// |
||||||
|
// func encode(to encoder: Encoder) throws { |
||||||
|
// var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
// |
||||||
|
// try container.encode(id, forKey: ._id) |
||||||
|
// try container.encode(storeId, forKey: ._storeId) |
||||||
|
// try container.encode(lastUpdate, forKey: ._lastUpdate) |
||||||
|
// try container.encode(tournament, forKey: ._tournament) |
||||||
|
// try container.encode(index, forKey: ._index) |
||||||
|
// try container.encode(size, forKey: ._size) |
||||||
|
// try container.encode(format, forKey: ._format) |
||||||
|
// try container.encode(startDate, forKey: ._startDate) |
||||||
|
// try container.encode(name, forKey: ._name) |
||||||
|
// try container.encode(step, forKey: ._step) |
||||||
|
// } |
||||||
|
|
||||||
|
func insertOnServer() { |
||||||
|
self.tournamentStore?.groupStages.writeChangeAndInsertOnServer(instance: self) |
||||||
|
for match in self._matches() { |
||||||
|
match.insertOnServer() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
extension GroupStage { |
||||||
|
enum CodingKeys: String, CodingKey { |
||||||
|
case _id = "id" |
||||||
|
case _storeId = "storeId" |
||||||
|
case _lastUpdate = "lastUpdate" |
||||||
|
case _tournament = "tournament" |
||||||
|
case _index = "index" |
||||||
|
case _size = "size" |
||||||
|
case _format = "format" |
||||||
|
case _startDate = "startDate" |
||||||
|
case _name = "name" |
||||||
|
case _step = "step" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension GroupStage: Selectable { |
||||||
|
func selectionLabel(index: Int) -> String { |
||||||
|
groupStageTitle() |
||||||
|
} |
||||||
|
|
||||||
|
func badgeValue() -> Int? { |
||||||
|
return runningMatches(playedMatches: _matches()).count |
||||||
|
} |
||||||
|
|
||||||
|
func badgeValueColor() -> Color? { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func badgeImage() -> Badge? { |
||||||
|
if teams().count < size { |
||||||
|
return .xmark |
||||||
|
} else { |
||||||
|
return hasEnded() ? .checkmark : nil |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,888 @@ |
|||||||
|
// |
||||||
|
// MatchScheduler.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 08/04/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class MatchScheduler: BaseMatchScheduler, SideStorable { |
||||||
|
// |
||||||
|
// init(tournament: String, |
||||||
|
// timeDifferenceLimit: Int = 5, |
||||||
|
// loserBracketRotationDifference: Int = 0, |
||||||
|
// upperBracketRotationDifference: Int = 1, |
||||||
|
// accountUpperBracketBreakTime: Bool = true, |
||||||
|
// accountLoserBracketBreakTime: Bool = false, |
||||||
|
// randomizeCourts: Bool = true, |
||||||
|
// rotationDifferenceIsImportant: Bool = false, |
||||||
|
// shouldHandleUpperRoundSlice: Bool = false, |
||||||
|
// shouldEndRoundBeforeStartingNext: Bool = true, |
||||||
|
//<<<<<<< HEAD |
||||||
|
// groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) { |
||||||
|
// super.init() |
||||||
|
//======= |
||||||
|
// groupStageChunkCount: Int? = nil, |
||||||
|
// overrideCourtsUnavailability: Bool = false, |
||||||
|
// shouldTryToFillUpCourtsAvailable: Bool = true, |
||||||
|
// courtsAvailable: Set<Int> = Set<Int>(), |
||||||
|
// simultaneousStart: Bool = true) { |
||||||
|
//>>>>>>> main |
||||||
|
// self.tournament = tournament |
||||||
|
// self.timeDifferenceLimit = timeDifferenceLimit |
||||||
|
// self.loserBracketRotationDifference = loserBracketRotationDifference |
||||||
|
// self.upperBracketRotationDifference = upperBracketRotationDifference |
||||||
|
// self.accountUpperBracketBreakTime = accountUpperBracketBreakTime |
||||||
|
// self.accountLoserBracketBreakTime = accountLoserBracketBreakTime |
||||||
|
// self.randomizeCourts = randomizeCourts |
||||||
|
// self.rotationDifferenceIsImportant = rotationDifferenceIsImportant |
||||||
|
// self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice |
||||||
|
// self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext |
||||||
|
// self.groupStageChunkCount = groupStageChunkCount |
||||||
|
// self.overrideCourtsUnavailability = overrideCourtsUnavailability |
||||||
|
// self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable |
||||||
|
// self.courtsAvailable = courtsAvailable |
||||||
|
// self.simultaneousStart = simultaneousStart |
||||||
|
// } |
||||||
|
|
||||||
|
var courtsUnavailability: [DateInterval]? { |
||||||
|
guard let event = tournamentObject()?.eventObject() else { return nil } |
||||||
|
return event.courtsUnavailability + (overrideCourtsUnavailability ? [] : event.tournamentsCourtsUsed(exluding: tournament)) |
||||||
|
} |
||||||
|
|
||||||
|
var additionalEstimationDuration : Int { |
||||||
|
return tournamentObject()?.additionalEstimationDuration ?? 0 |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentStore: TournamentStore? { |
||||||
|
return TournamentLibrary.shared.store(tournamentId: self.tournament) |
||||||
|
// TournamentStore.instance(tournamentId: self.tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentObject() -> Tournament? { |
||||||
|
return Store.main.findById(tournament) |
||||||
|
} |
||||||
|
|
||||||
|
@discardableResult |
||||||
|
func updateGroupStageSchedule(tournament: Tournament, specificGroupStage: GroupStage? = nil, atStep step: Int = 0, startDate: Date? = nil) -> Date { |
||||||
|
let computedGroupStageChunkCount = groupStageChunkCount ?? tournament.getGroupStageChunkValue() |
||||||
|
var groupStages: [GroupStage] = tournament.groupStages(atStep: step) |
||||||
|
if let specificGroupStage { |
||||||
|
groupStages = [specificGroupStage] |
||||||
|
} |
||||||
|
|
||||||
|
let matches = groupStages.flatMap { $0._matches() } |
||||||
|
matches.forEach({ |
||||||
|
$0.removeCourt() |
||||||
|
$0.startDate = nil |
||||||
|
$0.confirmed = false |
||||||
|
}) |
||||||
|
|
||||||
|
var lastDate : Date = startDate ?? tournament.startDate |
||||||
|
let times = Set(groupStages.compactMap { $0.startDate }).sorted() |
||||||
|
|
||||||
|
if let first = times.first { |
||||||
|
if first.isEarlierThan(tournament.startDate) { |
||||||
|
tournament.startDate = first |
||||||
|
do { |
||||||
|
try DataStore.shared.tournaments.addOrUpdate(instance: tournament) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
times.forEach({ time in |
||||||
|
if lastDate.isEarlierThan(time) { |
||||||
|
lastDate = time |
||||||
|
} |
||||||
|
let groups = groupStages.filter({ $0.startDate == time }) |
||||||
|
let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate) |
||||||
|
|
||||||
|
dispatch.timedMatches.forEach { matchSchedule in |
||||||
|
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { |
||||||
|
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) |
||||||
|
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60 |
||||||
|
if let startDate = match.groupStageObject?.startDate { |
||||||
|
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) |
||||||
|
match.startDate = matchStartDate |
||||||
|
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60) |
||||||
|
} |
||||||
|
match.setCourt(matchSchedule.courtIndex) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
groupStages.filter({ $0.startDate == nil || times.contains($0.startDate!) == false }).chunked(into: computedGroupStageChunkCount).forEach { groups in |
||||||
|
groups.forEach({ $0.startDate = lastDate }) |
||||||
|
do { |
||||||
|
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groups) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
|
||||||
|
let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate) |
||||||
|
|
||||||
|
dispatch.timedMatches.forEach { matchSchedule in |
||||||
|
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { |
||||||
|
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) |
||||||
|
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60 |
||||||
|
if let startDate = match.groupStageObject?.startDate { |
||||||
|
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) |
||||||
|
match.startDate = matchStartDate |
||||||
|
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60) |
||||||
|
} |
||||||
|
match.setCourt(matchSchedule.courtIndex) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
do { |
||||||
|
try self.tournamentStore?.matches.addOrUpdate(contentOfs: matches) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
return lastDate |
||||||
|
} |
||||||
|
|
||||||
|
func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { |
||||||
|
|
||||||
|
let _groupStages = groupStages |
||||||
|
|
||||||
|
// Get the maximum count of matches in any group |
||||||
|
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 |
||||||
|
var flattenedMatches = [Match]() |
||||||
|
if simultaneousStart { |
||||||
|
// Flatten matches in a round-robin order by cycling through each group |
||||||
|
flattenedMatches = (0..<maxMatchesCount).flatMap { index in |
||||||
|
_groupStages.compactMap { group in |
||||||
|
// Safely access matches, return nil if index is out of bounds |
||||||
|
let playedMatches = group.playedMatches() |
||||||
|
return playedMatches.indices.contains(index) ? playedMatches[index] : nil |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
flattenedMatches = _groupStages.flatMap({ $0.playedMatches() }) |
||||||
|
} |
||||||
|
|
||||||
|
var slots = [GroupStageTimeMatch]() |
||||||
|
var availableMatches = flattenedMatches |
||||||
|
var rotationIndex = 0 |
||||||
|
var teamsPerRotation = [Int: [String]]() // Tracks teams assigned to each rotation |
||||||
|
var freeCourtPerRotation = [Int: [Int]]() // Tracks free courts per rotation |
||||||
|
var groupLastRotation = [Int: Int]() // Tracks the last rotation each group was involved in |
||||||
|
let courtsUnavailability = courtsUnavailability |
||||||
|
|
||||||
|
while slots.count < flattenedMatches.count { |
||||||
|
print("Starting rotation \(rotationIndex) with \(availableMatches.count) matches left") |
||||||
|
teamsPerRotation[rotationIndex] = [] |
||||||
|
freeCourtPerRotation[rotationIndex] = [] |
||||||
|
|
||||||
|
let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 } |
||||||
|
.map { ($0.groupIndex, 1) } |
||||||
|
|
||||||
|
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +) |
||||||
|
var rotationMatches = Array(availableMatches.filter({ match in |
||||||
|
// Check if all teams from the match are not already scheduled in the current rotation |
||||||
|
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) }) |
||||||
|
if !teamsAvailable { |
||||||
|
print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)") |
||||||
|
} |
||||||
|
return teamsAvailable |
||||||
|
})) |
||||||
|
|
||||||
|
if rotationIndex > 0, simultaneousStart == false { |
||||||
|
rotationMatches = rotationMatches.sorted(by: { |
||||||
|
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { |
||||||
|
return $0.groupStageObject!.index < $1.groupStageObject!.index |
||||||
|
} else { |
||||||
|
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
courtsAvailable.forEach { courtIndex in |
||||||
|
print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)") |
||||||
|
if let first = rotationMatches.first(where: { match in |
||||||
|
let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) |
||||||
|
let timeIntervalToAdd = Double(rotationIndex) * Double(estimatedDuration) * 60 |
||||||
|
let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd) |
||||||
|
|
||||||
|
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability) |
||||||
|
|
||||||
|
if courtsUnavailable.contains(courtIndex) { |
||||||
|
print("Court \(courtIndex) is unavailable at \(rotationStartDate)") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) }) |
||||||
|
if !teamsAvailable { |
||||||
|
print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
print("Match \(match.roundAndMatchTitle()) is available for court \(courtIndex) at \(rotationStartDate)") |
||||||
|
return true |
||||||
|
}) { |
||||||
|
let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index) |
||||||
|
|
||||||
|
print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)") |
||||||
|
|
||||||
|
slots.append(timeMatch) |
||||||
|
teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp()) |
||||||
|
rotationMatches.removeAll(where: { $0.id == first.id }) |
||||||
|
availableMatches.removeAll(where: { $0.id == first.id }) |
||||||
|
|
||||||
|
if let index = first.groupStageObject?.index { |
||||||
|
groupLastRotation[index] = rotationIndex |
||||||
|
} |
||||||
|
} else { |
||||||
|
print("No available matches for court \(courtIndex) in rotation \(rotationIndex), adding to free court list") |
||||||
|
freeCourtPerRotation[rotationIndex]!.append(courtIndex) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
rotationIndex += 1 |
||||||
|
} |
||||||
|
|
||||||
|
print("All matches scheduled. Total rotations: \(rotationIndex)") |
||||||
|
|
||||||
|
// Organize slots and ensure courts are randomized or sorted |
||||||
|
var organizedSlots = [GroupStageTimeMatch]() |
||||||
|
for i in 0..<rotationIndex { |
||||||
|
let courtsSorted: [Int] = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() |
||||||
|
let courts: [Int] = randomizeCourts ? courtsSorted.shuffled() : courtsSorted |
||||||
|
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) |
||||||
|
|
||||||
|
for j in 0..<matches.count { |
||||||
|
matches[j].courtIndex = courts[j] |
||||||
|
organizedSlots.append(matches[j]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return GroupStageMatchDispatcher( |
||||||
|
timedMatches: organizedSlots, |
||||||
|
freeCourtPerRotation: freeCourtPerRotation, |
||||||
|
rotationCount: rotationIndex, |
||||||
|
groupLastRotation: groupLastRotation |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
func rotationDifference(loserBracket: Bool) -> Int { |
||||||
|
if loserBracket { |
||||||
|
return loserBracketRotationDifference |
||||||
|
} else { |
||||||
|
return upperBracketRotationDifference |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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 { |
||||||
|
print("Cannot start at \(targetedStartDate), earlier than round start date \(roundStartDate)") |
||||||
|
if targetedStartDate == minimumTargetedEndDate { |
||||||
|
print("Updating minimumTargetedEndDate to roundStartDate: \(roundStartDate)") |
||||||
|
minimumTargetedEndDate = roundStartDate |
||||||
|
} else { |
||||||
|
print("Setting minimumTargetedEndDate to the earlier of \(roundStartDate) and \(minimumTargetedEndDate)") |
||||||
|
minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate) |
||||||
|
} |
||||||
|
print("Returning false: Match cannot start earlier than the round start date.") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
let previousMatches = roundObject.precedentMatches(ofMatch: match) |
||||||
|
if previousMatches.isEmpty { |
||||||
|
print("No ancestors matches for this match, returning true. (eg beginning of tournament 1st bracket") |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
let previousMatchSlots = slots.filter { previousMatches.map { $0.id }.contains($0.matchID) } |
||||||
|
|
||||||
|
if previousMatchSlots.isEmpty { |
||||||
|
if previousMatches.filter({ !$0.disabled }).allSatisfy({ $0.startDate != nil }) { |
||||||
|
print("All previous matches have start dates, returning true.") |
||||||
|
return true |
||||||
|
} |
||||||
|
print("Some previous matches are pending, returning false.") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if previousMatches.filter({ !$0.disabled }).count > previousMatchSlots.count { |
||||||
|
if previousMatches.filter({ !$0.disabled }).anySatisfy({ $0.startDate != nil }) { |
||||||
|
print("Some previous matches started, returning true.") |
||||||
|
return true |
||||||
|
} |
||||||
|
print("Not enough previous matches have started, returning false.") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
var includeBreakTime = false |
||||||
|
if accountLoserBracketBreakTime && roundObject.isLoserBracket() { |
||||||
|
includeBreakTime = true |
||||||
|
print("Including break time for loser bracket.") |
||||||
|
} |
||||||
|
|
||||||
|
if accountUpperBracketBreakTime && !roundObject.isLoserBracket() { |
||||||
|
includeBreakTime = true |
||||||
|
print("Including break time for upper bracket.") |
||||||
|
} |
||||||
|
|
||||||
|
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy { |
||||||
|
$0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex |
||||||
|
} |
||||||
|
|
||||||
|
if previousMatchIsInPreviousRotation { |
||||||
|
print("All previous matches are from earlier rotations, returning true.") |
||||||
|
} else { |
||||||
|
print("Some previous matches are from the current rotation.") |
||||||
|
} |
||||||
|
|
||||||
|
guard let minimumPossibleEndDate = previousMatchSlots.map({ |
||||||
|
$0.estimatedEndDate(includeBreakTime: includeBreakTime) |
||||||
|
}).max() else { |
||||||
|
print("No valid previous match end date, returning \(previousMatchIsInPreviousRotation).") |
||||||
|
return previousMatchIsInPreviousRotation |
||||||
|
} |
||||||
|
|
||||||
|
if targetedStartDate >= minimumPossibleEndDate { |
||||||
|
if rotationDifferenceIsImportant { |
||||||
|
print("Targeted start date is after the minimum possible end date and rotation difference is important, returning \(previousMatchIsInPreviousRotation).") |
||||||
|
return previousMatchIsInPreviousRotation |
||||||
|
} else { |
||||||
|
print("Targeted start date is after the minimum possible end date, returning true.") |
||||||
|
return true |
||||||
|
} |
||||||
|
} else { |
||||||
|
if targetedStartDate == minimumTargetedEndDate { |
||||||
|
print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)") |
||||||
|
minimumTargetedEndDate = minimumPossibleEndDate |
||||||
|
} else { |
||||||
|
print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") |
||||||
|
minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) |
||||||
|
} |
||||||
|
print("Targeted start date \(targetedStartDate) is before the minimum possible end date, returning false. \(minimumTargetedEndDate)") |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { |
||||||
|
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() |
||||||
|
} |
||||||
|
|
||||||
|
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) |
||||||
|
let lastMatch = matchesByCourt?.last |
||||||
|
var results = [(Int, Date)]() |
||||||
|
if let courtFreeDate = lastMatch?.estimatedEndDate(includeBreakTime: false) { |
||||||
|
results.append((courtIndex, courtFreeDate)) |
||||||
|
} |
||||||
|
return results |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!) |
||||||
|
let lastMatch = matchesByCourt?.last |
||||||
|
var results = [(Int, Date)]() |
||||||
|
if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) { |
||||||
|
results.append((court, courtFreeDate)) |
||||||
|
} |
||||||
|
return results |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { |
||||||
|
var slots = [TimeMatch]() |
||||||
|
var _startDate: Date? |
||||||
|
var rotationIndex = 0 |
||||||
|
var availableMatchs = flattenedMatches.filter({ $0.startDate == nil }) |
||||||
|
let courtsUnavailability = courtsUnavailability |
||||||
|
var issueFound: Bool = false |
||||||
|
|
||||||
|
// Log start of the function |
||||||
|
print("Starting roundDispatcher with \(availableMatchs.count) matches and \(courtsAvailable) courts available") |
||||||
|
|
||||||
|
flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in |
||||||
|
if _startDate == nil { |
||||||
|
_startDate = match.startDate |
||||||
|
} else if match.startDate! > _startDate! { |
||||||
|
_startDate = match.startDate |
||||||
|
rotationIndex += 1 |
||||||
|
} |
||||||
|
|
||||||
|
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime) |
||||||
|
slots.append(timeMatch) |
||||||
|
} |
||||||
|
|
||||||
|
if !slots.isEmpty { |
||||||
|
rotationIndex += 1 |
||||||
|
} |
||||||
|
|
||||||
|
var freeCourtPerRotation = [Int: [Int]]() |
||||||
|
var courts = initialCourts ?? Array(courtsAvailable) |
||||||
|
var shouldStartAtDispatcherDate = rotationIndex > 0 |
||||||
|
var suitableDate: Date? |
||||||
|
|
||||||
|
while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 { |
||||||
|
freeCourtPerRotation[rotationIndex] = [] |
||||||
|
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) |
||||||
|
|
||||||
|
var rotationStartDate: Date |
||||||
|
if previousRotationSlots.isEmpty && rotationIndex > 0 { |
||||||
|
let computedSuitableDate = slots.sorted(by: \.computedEndDateForSorting).last?.computedEndDateForSorting |
||||||
|
print("Previous rotation was empty, find a suitable rotationStartDate \(suitableDate)") |
||||||
|
rotationStartDate = suitableDate ?? computedSuitableDate ?? dispatcherStartDate |
||||||
|
} else { |
||||||
|
rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate |
||||||
|
} |
||||||
|
|
||||||
|
if shouldStartAtDispatcherDate { |
||||||
|
rotationStartDate = dispatcherStartDate |
||||||
|
shouldStartAtDispatcherDate = false |
||||||
|
} else { |
||||||
|
courts = rotationIndex == 0 ? courts : Array(courtsAvailable) |
||||||
|
} |
||||||
|
courts.sort() |
||||||
|
|
||||||
|
// Log courts availability and start date |
||||||
|
print("Courts available at rotation \(rotationIndex): \(courts)") |
||||||
|
print("Rotation start date: \(rotationStartDate)") |
||||||
|
|
||||||
|
// Check for court availability and break time conflicts |
||||||
|
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { |
||||||
|
print("Handling break time conflicts or waiting for free courts") |
||||||
|
let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } |
||||||
|
var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) |
||||||
|
var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) |
||||||
|
|
||||||
|
if let courtsUnavailability, previousEndDate != nil { |
||||||
|
previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate |
||||||
|
} |
||||||
|
|
||||||
|
if let courtsUnavailability, previousEndDateNoBreak != nil { |
||||||
|
previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate |
||||||
|
} |
||||||
|
|
||||||
|
let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } |
||||||
|
|
||||||
|
if let previousEndDate, let previousEndDateNoBreak { |
||||||
|
let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate) |
||||||
|
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) |
||||||
|
print("Difference with break: \(differenceWithBreak), without break: \(differenceWithoutBreak)") |
||||||
|
|
||||||
|
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) |
||||||
|
var difference = differenceWithBreak |
||||||
|
|
||||||
|
if differenceWithBreak <= 0, accountUpperBracketBreakTime == false { |
||||||
|
difference = differenceWithoutBreak |
||||||
|
} else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { |
||||||
|
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) |
||||||
|
} |
||||||
|
|
||||||
|
print("Final difference to evaluate: \(difference)") |
||||||
|
|
||||||
|
if (difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate) || difference < 0 { |
||||||
|
print(""" |
||||||
|
Adjusting rotation start: |
||||||
|
- Initial rotationStartDate: \(rotationStartDate) |
||||||
|
- Adjusted by difference: \(difference) |
||||||
|
- Adjusted rotationStartDate: \(rotationStartDate.addingTimeInterval(-difference)) |
||||||
|
- PreviousEndDate: \(previousEndDate) |
||||||
|
""") |
||||||
|
|
||||||
|
courts.removeAll(where: { freeCourtPreviousRotation.contains($0) }) |
||||||
|
freeCourtPerRotation[rotationIndex] = courts |
||||||
|
courts = freeCourtPreviousRotation |
||||||
|
rotationStartDate = rotationStartDate.addingTimeInterval(-difference) |
||||||
|
} |
||||||
|
} |
||||||
|
} else if let firstMatch = availableMatchs.first { |
||||||
|
let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration) |
||||||
|
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) |
||||||
|
|
||||||
|
if Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))).isEmpty { |
||||||
|
print("Issue: All courts unavailable in this rotation") |
||||||
|
if let courtsUnavailability { |
||||||
|
let computedStartDateAndCourts = getFirstFreeCourt(startDate: rotationStartDate, duration: duration, courts: courts, courtsUnavailability: courtsUnavailability) |
||||||
|
rotationStartDate = computedStartDateAndCourts.earliestFreeDate |
||||||
|
courts = computedStartDateAndCourts.availableCourts |
||||||
|
} else { |
||||||
|
issueFound = true |
||||||
|
} |
||||||
|
} else { |
||||||
|
courts = Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Dispatch courts and schedule matches |
||||||
|
suitableDate = dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) |
||||||
|
rotationIndex += 1 |
||||||
|
} |
||||||
|
|
||||||
|
// Organize matches in slots |
||||||
|
var organizedSlots = [TimeMatch]() |
||||||
|
for i in 0..<rotationIndex { |
||||||
|
let courtsSorted = slots.filter { $0.rotationIndex == i }.map { $0.courtIndex }.sorted() |
||||||
|
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted |
||||||
|
var matches = slots.filter { $0.rotationIndex == i }.sorted(using: .keyPath(\.courtIndex)) |
||||||
|
|
||||||
|
for j in 0..<matches.count { |
||||||
|
matches[j].courtIndex = courts[j] |
||||||
|
organizedSlots.append(matches[j]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches") |
||||||
|
|
||||||
|
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 { |
||||||
|
var matchPerRound = [String: Int]() |
||||||
|
var minimumTargetedEndDate = rotationStartDate |
||||||
|
|
||||||
|
// Log dispatch attempt |
||||||
|
print("Dispatching courts for rotation \(rotationIndex) with start date \(rotationStartDate) and available courts \(courts.sorted())") |
||||||
|
|
||||||
|
for (courtPosition, courtIndex) in courts.sorted().enumerated() { |
||||||
|
if let firstMatch = availableMatchs.first(where: { match in |
||||||
|
print("Trying to find a match for court \(courtIndex) in rotation \(rotationIndex)") |
||||||
|
|
||||||
|
let roundObject = match.roundObject! |
||||||
|
let duration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) |
||||||
|
|
||||||
|
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) |
||||||
|
|
||||||
|
if courtsUnavailable.contains(courtIndex) { |
||||||
|
print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) |
||||||
|
|
||||||
|
if !canBePlayed { |
||||||
|
print("Returning false: Match \(match.roundAndMatchTitle()) can't be played due to constraints.") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
let currentRotationSameRoundMatches = matchPerRound[roundObject.id] ?? 0 |
||||||
|
let roundMatchesCount = roundObject.playedMatches().count |
||||||
|
|
||||||
|
if shouldHandleUpperRoundSlice { |
||||||
|
if roundObject.parent == nil, roundObject.index > 1, currentRotationSameRoundMatches == 0, courts.count - matchPerRound.count < 2 { |
||||||
|
return false |
||||||
|
} |
||||||
|
else if roundObject.parent == nil, roundObject.index == 0, matchPerRound.isEmpty == false { |
||||||
|
return false |
||||||
|
} |
||||||
|
else if roundObject.parent == nil, roundObject.index == 1, currentRotationSameRoundMatches >= 0, courtsAvailable.count > 0 { |
||||||
|
return true |
||||||
|
} |
||||||
|
else if roundObject.parent == nil && roundMatchesCount > courts.count && currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { |
||||||
|
print("Returning false: Too many matches already played in the current rotation for round \(roundObject.roundTitle()).") |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let indexInRound = match.indexInRound() |
||||||
|
|
||||||
|
|
||||||
|
if shouldTryToFillUpCourtsAvailable == false { |
||||||
|
if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() { |
||||||
|
|
||||||
|
var nextMinimumTargetedEndDate = minimumTargetedEndDate |
||||||
|
if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &nextMinimumTargetedEndDate) { |
||||||
|
print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") |
||||||
|
return true |
||||||
|
} else { |
||||||
|
print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).") |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).") |
||||||
|
return canBePlayed |
||||||
|
}) { |
||||||
|
print("Found match: \(firstMatch.roundAndMatchTitle()) for court \(courtIndex) at \(rotationStartDate)") |
||||||
|
|
||||||
|
matchPerRound[firstMatch.roundObject!.id, default: 0] += 1 |
||||||
|
|
||||||
|
let timeMatch = TimeMatch( |
||||||
|
matchID: firstMatch.id, |
||||||
|
rotationIndex: rotationIndex, |
||||||
|
courtIndex: courtIndex, |
||||||
|
startDate: rotationStartDate, |
||||||
|
durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration), |
||||||
|
minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime |
||||||
|
) |
||||||
|
|
||||||
|
slots.append(timeMatch) |
||||||
|
availableMatchs.removeAll(where: { $0.id == firstMatch.id }) |
||||||
|
} else { |
||||||
|
print("No suitable match found for court \(courtIndex) in rotation \(rotationIndex). Adding court to freeCourtPerRotation.") |
||||||
|
freeCourtPerRotation[rotationIndex]?.append(courtIndex) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { |
||||||
|
print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)") |
||||||
|
} |
||||||
|
|
||||||
|
if let courtsUnavailability { |
||||||
|
let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability) |
||||||
|
return computedStartDateAndCourts.earliestFreeDate |
||||||
|
} |
||||||
|
|
||||||
|
return minimumTargetedEndDate |
||||||
|
} |
||||||
|
|
||||||
|
@discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool { |
||||||
|
|
||||||
|
let upperRounds: [Round] = tournament.rounds() |
||||||
|
let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) |
||||||
|
|
||||||
|
var rounds = [Round]() |
||||||
|
|
||||||
|
if let groupStageLoserBracketRound = tournament.groupStageLoserBracket() { |
||||||
|
rounds.append(groupStageLoserBracketRound) |
||||||
|
} |
||||||
|
|
||||||
|
if shouldEndRoundBeforeStartingNext { |
||||||
|
rounds.append(contentsOf: upperRounds.flatMap { |
||||||
|
[$0] + $0.loserRoundsAndChildren() |
||||||
|
}) |
||||||
|
} else { |
||||||
|
rounds.append(contentsOf: upperRounds.map { |
||||||
|
$0 |
||||||
|
} + upperRounds.flatMap { |
||||||
|
$0.loserRoundsAndChildren() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
let flattenedMatches = rounds.flatMap { round in |
||||||
|
round._matches().filter({ $0.disabled == false && $0.hasEnded() == false && $0.hasStarted() == false }).sorted(by: \.index) |
||||||
|
} |
||||||
|
|
||||||
|
flattenedMatches.forEach({ |
||||||
|
if (roundId == nil && matchId == nil) || $0.startDate?.isEarlierThan(startDate) == false { |
||||||
|
$0.startDate = nil |
||||||
|
$0.removeCourt() |
||||||
|
$0.confirmed = false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// if let roundId { |
||||||
|
// if let round : Round = Store.main.findById(roundId) { |
||||||
|
// let matches = round._matches().filter({ $0.disabled == false }).sorted(by: \.index) |
||||||
|
// round.resetFromRoundAllMatchesStartDate() |
||||||
|
// flattenedMatches = matches + flattenedMatches |
||||||
|
// } |
||||||
|
// |
||||||
|
// } else if let matchId { |
||||||
|
// if let match : Match = Store.main.findById(matchId) { |
||||||
|
// if let round = match.roundObject { |
||||||
|
// round.resetFromRoundAllMatchesStartDate(from: match) |
||||||
|
// } |
||||||
|
// flattenedMatches = [match] + flattenedMatches |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
if let roundId, let matchId { |
||||||
|
//todo |
||||||
|
if let index = flattenedMatches.firstIndex(where: { $0.round == roundId && $0.id == matchId }) { |
||||||
|
flattenedMatches[index...].forEach { |
||||||
|
$0.startDate = nil |
||||||
|
$0.removeCourt() |
||||||
|
$0.confirmed = false |
||||||
|
} |
||||||
|
} |
||||||
|
} else if let roundId { |
||||||
|
//todo |
||||||
|
if let index = flattenedMatches.firstIndex(where: { $0.round == roundId }) { |
||||||
|
flattenedMatches[index...].forEach { |
||||||
|
$0.startDate = nil |
||||||
|
$0.removeCourt() |
||||||
|
$0.confirmed = false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
let matches: [Match] = allMatches.filter { $0.startDate?.isEarlierThan(startDate) == true && $0.startDate?.dayInt == startDate.dayInt } |
||||||
|
let usedCourts = getAvailableCourts(from: matches) |
||||||
|
let initialCourts: [Int] = usedCourts.filter { (court, availableDate) in |
||||||
|
availableDate <= startDate |
||||||
|
}.sorted(by: \.1).compactMap { $0.0 } |
||||||
|
|
||||||
|
let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts |
||||||
|
|
||||||
|
print("initial available courts at beginning: \(courts ?? [])") |
||||||
|
|
||||||
|
let roundDispatch = self.roundDispatcher(flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts) |
||||||
|
|
||||||
|
roundDispatch.timedMatches.forEach { matchSchedule in |
||||||
|
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { |
||||||
|
match.startDate = matchSchedule.startDate |
||||||
|
match.setCourt(matchSchedule.courtIndex) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
do { |
||||||
|
try self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
|
||||||
|
return roundDispatch.issueFound |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
func courtsUnavailable(startDate: Date, duration: Int, courtsUnavailability: [DateInterval]?) -> [Int] { |
||||||
|
let endDate = startDate.addingTimeInterval(Double(duration) * 60) |
||||||
|
guard let courtsUnavailability else { return [] } |
||||||
|
let groupedBy = Dictionary(grouping: courtsUnavailability, by: { $0.courtIndex }) |
||||||
|
let courts = groupedBy.keys |
||||||
|
return courts.filter { |
||||||
|
courtUnavailable(courtIndex: $0, from: startDate, to: endDate, source: courtsUnavailability) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date, source: [DateInterval]) -> Bool { |
||||||
|
let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex }) |
||||||
|
return courtLockedSchedule.anySatisfy({ dateInterval in |
||||||
|
let range = startDate..<endDate |
||||||
|
return dateInterval.range.overlaps(range) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func getFirstFreeCourt(startDate: Date, duration: Int, courts: [Int], courtsUnavailability: [DateInterval]) -> (earliestFreeDate: Date, availableCourts: [Int]) { |
||||||
|
var earliestEndDate: Date? |
||||||
|
var availableCourtsAtEarliest: [Int] = [] |
||||||
|
|
||||||
|
// Iterate through each court and find the earliest time it becomes free |
||||||
|
for courtIndex in courts { |
||||||
|
let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex } |
||||||
|
var isAvailable = true |
||||||
|
|
||||||
|
for interval in unavailabilityForCourt { |
||||||
|
if interval.startDate <= startDate && interval.endDate > startDate { |
||||||
|
isAvailable = false |
||||||
|
if let currentEarliest = earliestEndDate { |
||||||
|
earliestEndDate = min(currentEarliest, interval.endDate) |
||||||
|
} else { |
||||||
|
earliestEndDate = interval.endDate |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If the court is available at the start date, add it to the list of available courts |
||||||
|
if isAvailable { |
||||||
|
availableCourtsAtEarliest.append(courtIndex) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If there are no unavailable courts, return the original start date and all courts |
||||||
|
if let earliestEndDate = earliestEndDate { |
||||||
|
// Find which courts will be available at the earliest free date |
||||||
|
let courtsAvailableAtEarliest = courts.filter { courtIndex in |
||||||
|
let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex } |
||||||
|
return unavailabilityForCourt.allSatisfy { $0.endDate <= earliestEndDate } |
||||||
|
} |
||||||
|
return (earliestFreeDate: earliestEndDate, availableCourts: courtsAvailableAtEarliest) |
||||||
|
} else { |
||||||
|
// If no courts were unavailable, all courts are available at the start date |
||||||
|
return (earliestFreeDate: startDate.addingTimeInterval(Double(duration) * 60), availableCourts: courts) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func updateSchedule(tournament: Tournament) -> Bool { |
||||||
|
if tournament.courtCount < courtsAvailable.count { |
||||||
|
courtsAvailable = Set(tournament.courtsAvailable()) |
||||||
|
} |
||||||
|
var lastDate = tournament.startDate |
||||||
|
if tournament.groupStageCount > 0 { |
||||||
|
lastDate = updateGroupStageSchedule(tournament: tournament) |
||||||
|
} |
||||||
|
if tournament.groupStages(atStep: 1).isEmpty == false { |
||||||
|
lastDate = updateGroupStageSchedule(tournament: tournament, atStep: 1, startDate: lastDate) |
||||||
|
} |
||||||
|
return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct GroupStageTimeMatch { |
||||||
|
let matchID: String |
||||||
|
let rotationIndex: Int |
||||||
|
var courtIndex: Int |
||||||
|
let groupIndex: Int |
||||||
|
} |
||||||
|
|
||||||
|
struct TimeMatch { |
||||||
|
let matchID: String |
||||||
|
let rotationIndex: Int |
||||||
|
var courtIndex: Int |
||||||
|
var startDate: Date |
||||||
|
var durationLeft: Int //in minutes |
||||||
|
var minimumBreakTime: Int //in minutes |
||||||
|
|
||||||
|
func estimatedEndDate(includeBreakTime: Bool) -> Date { |
||||||
|
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) |
||||||
|
return startDate.addingTimeInterval(minutesToAdd * 60.0) |
||||||
|
} |
||||||
|
|
||||||
|
var computedEndDateForSorting: Date { |
||||||
|
estimatedEndDate(includeBreakTime: false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct GroupStageMatchDispatcher { |
||||||
|
let timedMatches: [GroupStageTimeMatch] |
||||||
|
let freeCourtPerRotation: [Int: [Int]] |
||||||
|
let rotationCount: Int |
||||||
|
let groupLastRotation: [Int: Int] |
||||||
|
} |
||||||
|
|
||||||
|
struct MatchDispatcher { |
||||||
|
let timedMatches: [TimeMatch] |
||||||
|
let freeCourtPerRotation: [Int: [Int]] |
||||||
|
let rotationCount: Int |
||||||
|
let issueFound: Bool |
||||||
|
} |
||||||
|
|
||||||
|
extension Match { |
||||||
|
func teamIds() -> [String] { |
||||||
|
return teams().map { $0.id } |
||||||
|
} |
||||||
|
|
||||||
|
func containsTeamId(_ id: String) -> Bool { |
||||||
|
return teamIds().contains(id) |
||||||
|
} |
||||||
|
|
||||||
|
func containsTeamIndex(_ id: String) -> Bool { |
||||||
|
matchUp().contains(id) |
||||||
|
} |
||||||
|
|
||||||
|
func matchUp() -> [String] { |
||||||
|
guard let groupStageObject else { |
||||||
|
return [] |
||||||
|
} |
||||||
|
|
||||||
|
return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" } |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
// |
||||||
|
// MockData.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 20/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
extension Court { |
||||||
|
static func mock() -> Court { |
||||||
|
Court(index: 0, club: "", name: "Test") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Event { |
||||||
|
static func mock() -> Event { |
||||||
|
Event() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Club { |
||||||
|
static func mock() -> Club { |
||||||
|
Club(name: "AUC", acronym: "AUC") |
||||||
|
} |
||||||
|
|
||||||
|
static func newEmptyInstance() -> Club { |
||||||
|
Club(name: "", acronym: "") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension GroupStage { |
||||||
|
static func mock() -> GroupStage { |
||||||
|
GroupStage(tournament: "", index: 0, size: 4) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Round { |
||||||
|
static func mock() -> Round { |
||||||
|
Round(tournament: "", index: 0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Tournament { |
||||||
|
static func mock() -> Tournament { |
||||||
|
return Tournament(groupStageSortMode: .snake, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Match { |
||||||
|
static func mock() -> Match { |
||||||
|
return Match(index: 0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension TeamRegistration { |
||||||
|
static func mock() -> TeamRegistration { |
||||||
|
return TeamRegistration(tournament: "") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension PlayerRegistration { |
||||||
|
static func mock() -> PlayerRegistration { |
||||||
|
return PlayerRegistration(firstName: "Raz", lastName: "Shark", sex: .male) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
// |
||||||
|
// PlayerPaymentType.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 11/02/2025. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
enum PlayerPaymentType: Int, CaseIterable, Identifiable, Codable { |
||||||
|
|
||||||
|
init?(rawValue: Int?) { |
||||||
|
guard let value = rawValue else { return nil } |
||||||
|
self.init(rawValue: value) |
||||||
|
} |
||||||
|
|
||||||
|
var id: Self { |
||||||
|
self |
||||||
|
} |
||||||
|
|
||||||
|
case cash = 0 |
||||||
|
case lydia = 1 |
||||||
|
case gift = 2 |
||||||
|
case check = 3 |
||||||
|
case paylib = 4 |
||||||
|
case bankTransfer = 5 |
||||||
|
case clubHouse = 6 |
||||||
|
case creditCard = 7 |
||||||
|
case forfeit = 8 |
||||||
|
|
||||||
|
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { |
||||||
|
switch self { |
||||||
|
case .check: |
||||||
|
return "Chèque" |
||||||
|
case .cash: |
||||||
|
return "Cash" |
||||||
|
case .lydia: |
||||||
|
return "Lydia" |
||||||
|
case .paylib: |
||||||
|
return "Paylib" |
||||||
|
case .bankTransfer: |
||||||
|
return "Virement" |
||||||
|
case .clubHouse: |
||||||
|
return "Clubhouse" |
||||||
|
case .creditCard: |
||||||
|
return "CB" |
||||||
|
case .forfeit: |
||||||
|
return "Forfait" |
||||||
|
case .gift: |
||||||
|
return "Offert" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,510 @@ |
|||||||
|
// |
||||||
|
// PlayerRegistration.swift |
||||||
|
// Padel Tournament |
||||||
|
// |
||||||
|
// Created by razmig on 10/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class PlayerRegistration: BasePlayerRegistration, SideStorable { |
||||||
|
|
||||||
|
func localizedSourceLabel() -> String { |
||||||
|
switch source { |
||||||
|
case .frenchFederation: |
||||||
|
return "base fédérale" |
||||||
|
case .beachPadel: |
||||||
|
return "beach-padel" |
||||||
|
case nil: |
||||||
|
if registeredOnline { |
||||||
|
return "à vérifier vous-même" |
||||||
|
} else { |
||||||
|
return "créé par vous-même" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
self.lastName = lastName |
||||||
|
self.licenceId = licenceId |
||||||
|
self.rank = rank |
||||||
|
self.paymentType = paymentType |
||||||
|
self.sex = sex |
||||||
|
self.tournamentPlayed = tournamentPlayed |
||||||
|
self.points = points |
||||||
|
self.clubName = clubName |
||||||
|
self.ligueName = ligueName |
||||||
|
self.assimilation = assimilation |
||||||
|
self.phoneNumber = phoneNumber |
||||||
|
self.email = email |
||||||
|
self.birthdate = birthdate |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
internal init(importedPlayer: ImportedPlayer) { |
||||||
|
super.init() |
||||||
|
self.teamRegistration = "" |
||||||
|
self.firstName = (importedPlayer.firstName ?? "").prefixTrimmed(50).capitalized |
||||||
|
self.lastName = (importedPlayer.lastName ?? "").prefixTrimmed(50).uppercased() |
||||||
|
self.licenceId = importedPlayer.license?.prefixTrimmed(50) ?? nil |
||||||
|
self.rank = Int(importedPlayer.rank) |
||||||
|
self.sex = importedPlayer.male ? .male : .female |
||||||
|
self.tournamentPlayed = importedPlayer.tournamentPlayed |
||||||
|
self.points = importedPlayer.getPoints() |
||||||
|
self.clubName = importedPlayer.clubName?.prefixTrimmed(200) |
||||||
|
self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200) |
||||||
|
self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50) |
||||||
|
self.source = .frenchFederation |
||||||
|
self.birthdate = importedPlayer.birthYear?.prefixTrimmed(50) |
||||||
|
} |
||||||
|
|
||||||
|
internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { |
||||||
|
super.init() |
||||||
|
let _lastName = federalData[0].trimmed.uppercased() |
||||||
|
let _firstName = federalData[1].trimmed.capitalized |
||||||
|
if _lastName.isEmpty && _firstName.isEmpty { return nil } |
||||||
|
lastName = _lastName.prefixTrimmed(50) |
||||||
|
firstName = _firstName.prefixTrimmed(50) |
||||||
|
birthdate = federalData[2].formattedAsBirthdate().prefixTrimmed(50) |
||||||
|
licenceId = federalData[3].prefixTrimmed(50) |
||||||
|
clubName = federalData[4].prefixTrimmed(200) |
||||||
|
let stringRank = federalData[5] |
||||||
|
if stringRank.isEmpty { |
||||||
|
rank = nil |
||||||
|
} else { |
||||||
|
rank = Int(stringRank) |
||||||
|
} |
||||||
|
let _email = federalData[6] |
||||||
|
if _email.isEmpty == false { |
||||||
|
self.email = _email.prefixTrimmed(50) |
||||||
|
} |
||||||
|
let _phoneNumber = federalData[7] |
||||||
|
if _phoneNumber.isEmpty == false { |
||||||
|
self.phoneNumber = _phoneNumber.prefixTrimmed(50) |
||||||
|
} |
||||||
|
|
||||||
|
source = .beachPadel |
||||||
|
if sexUnknown { |
||||||
|
if sex == 1 && FileImportManager.shared.foundInWomenData(license: federalData[3]) { |
||||||
|
self.sex = .female |
||||||
|
} else if FileImportManager.shared.foundInMenData(license: federalData[3]) { |
||||||
|
self.sex = .male |
||||||
|
} else { |
||||||
|
self.sex = nil |
||||||
|
} |
||||||
|
} else { |
||||||
|
self.sex = PlayerSexType(rawValue: sex) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: any Decoder) throws { |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentStore: TournamentStore? { |
||||||
|
guard let storeId else { |
||||||
|
fatalError("missing store id for \(String(describing: type(of: self)))") |
||||||
|
} |
||||||
|
return TournamentLibrary.shared.store(tournamentId: storeId) |
||||||
|
// if let store = self.store as? TournamentStore { |
||||||
|
// return store |
||||||
|
// } |
||||||
|
} |
||||||
|
|
||||||
|
var computedAge: Int? { |
||||||
|
if let birthdate { |
||||||
|
let components = birthdate.components(separatedBy: "/") |
||||||
|
if let age = components.last, let ageInt = Int(age) { |
||||||
|
let year = Calendar.current.getSportAge() |
||||||
|
|
||||||
|
if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier |
||||||
|
if ageInt < 23 { |
||||||
|
return year - 2000 - ageInt |
||||||
|
} else { |
||||||
|
return year - 2000 + 100 - ageInt |
||||||
|
} |
||||||
|
} else { //si l'année est représenté sur 4 chiffres |
||||||
|
return year - ageInt |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func pasteData(_ exportFormat: ExportFormat = .rawText) -> String { |
||||||
|
switch exportFormat { |
||||||
|
case .rawText: |
||||||
|
return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator()) |
||||||
|
case .csv: |
||||||
|
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func isPlaying() -> Bool { |
||||||
|
team()?.isPlaying() == true |
||||||
|
} |
||||||
|
|
||||||
|
func contains(_ searchField: String) -> Bool { |
||||||
|
let nameComponents = searchField.canonicalVersion.split(separator: " ") |
||||||
|
|
||||||
|
if nameComponents.count > 1 { |
||||||
|
let pairs = nameComponents.pairs() |
||||||
|
return pairs.contains(where: { |
||||||
|
(firstName.canonicalVersion.localizedCaseInsensitiveContains(String($0)) && |
||||||
|
lastName.canonicalVersion.localizedCaseInsensitiveContains(String($1))) || |
||||||
|
(firstName.canonicalVersion.localizedCaseInsensitiveContains(String($1)) && |
||||||
|
lastName.canonicalVersion.localizedCaseInsensitiveContains(String($0))) |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return nameComponents.contains { component in |
||||||
|
firstName.canonicalVersion.localizedCaseInsensitiveContains(component) || |
||||||
|
lastName.canonicalVersion.localizedCaseInsensitiveContains(component) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func isSameAs(_ player: PlayerRegistration) -> Bool { |
||||||
|
firstName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline.canonicalVersion) == .orderedSame && |
||||||
|
lastName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline.canonicalVersion) == .orderedSame |
||||||
|
} |
||||||
|
|
||||||
|
func tournament() -> Tournament? { |
||||||
|
guard let tournament = team()?.tournament else { return nil } |
||||||
|
return Store.main.findById(tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func team() -> TeamRegistration? { |
||||||
|
guard let teamRegistration else { return nil } |
||||||
|
return self.tournamentStore?.teamRegistrations.findById(teamRegistration) |
||||||
|
} |
||||||
|
|
||||||
|
func isHere() -> Bool { |
||||||
|
hasArrived |
||||||
|
} |
||||||
|
|
||||||
|
func hasPaid() -> Bool { |
||||||
|
paymentType != nil |
||||||
|
} |
||||||
|
|
||||||
|
func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String { |
||||||
|
switch displayStyle { |
||||||
|
case .wide, .title: |
||||||
|
return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized |
||||||
|
case .short: |
||||||
|
let names = lastName.components(separatedBy: .whitespaces) |
||||||
|
if lastName.components(separatedBy: .whitespaces).count > 1 { |
||||||
|
if let firstLongWord = names.first(where: { $0.count > 3 }) { |
||||||
|
return firstLongWord.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "." |
||||||
|
} |
||||||
|
} |
||||||
|
return lastName.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "." |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func isImported() -> Bool { |
||||||
|
source == .beachPadel |
||||||
|
} |
||||||
|
|
||||||
|
func unrankedOrUnknown() -> Bool { |
||||||
|
source == nil |
||||||
|
} |
||||||
|
|
||||||
|
func isValidLicenseNumber(year: Int) -> Bool { |
||||||
|
guard let licenceId else { return false } |
||||||
|
guard licenceId.isLicenseNumber else { return false } |
||||||
|
guard licenceId.suffix(6) == "(\(year))" else { return false } |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
@objc |
||||||
|
var canonicalName: String { |
||||||
|
playerLabel().folding(options: .diacriticInsensitive, locale: .current).lowercased() |
||||||
|
} |
||||||
|
|
||||||
|
func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { |
||||||
|
if let rank, rank > 0 { |
||||||
|
if rank != computedRank { |
||||||
|
return computedRank.formatted() + " (" + rank.formatted() + ")" |
||||||
|
} else { |
||||||
|
return rank.formatted() |
||||||
|
} |
||||||
|
} else { |
||||||
|
return "non classé" + (isMalePlayer() ? "" : "e") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func updateRank(from sources: [CSVParser], lastRank: Int?) async throws { |
||||||
|
#if DEBUG_TIME |
||||||
|
let start = Date() |
||||||
|
defer { |
||||||
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||||
|
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
if let dataFound = try await history(from: sources) { |
||||||
|
await MainActor.run { |
||||||
|
rank = dataFound.rankValue?.toInt() |
||||||
|
points = dataFound.points |
||||||
|
tournamentPlayed = dataFound.tournamentCountValue?.toInt() |
||||||
|
} |
||||||
|
} else if let dataFound = try await historyFromName(from: sources) { |
||||||
|
await MainActor.run { |
||||||
|
rank = dataFound.rankValue?.toInt() |
||||||
|
points = dataFound.points |
||||||
|
tournamentPlayed = dataFound.tournamentCountValue?.toInt() |
||||||
|
} |
||||||
|
} else { |
||||||
|
await MainActor.run { |
||||||
|
rank = lastRank |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func history(from sources: [CSVParser]) async throws -> Line? { |
||||||
|
#if DEBUG_TIME |
||||||
|
let start = Date() |
||||||
|
defer { |
||||||
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||||
|
print("func history()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
guard let license = licenceId?.strippedLicense else { |
||||||
|
return nil // Do NOT call historyFromName here, let updateRank handle it |
||||||
|
} |
||||||
|
|
||||||
|
let filteredSources = sources.filter { $0.maleData == isMalePlayer() } |
||||||
|
|
||||||
|
return await withTaskGroup(of: Line?.self) { group in |
||||||
|
for source in filteredSources { |
||||||
|
group.addTask { |
||||||
|
guard !Task.isCancelled else { return nil } |
||||||
|
return try? await source.first { $0.rawValue.contains(";\(license);") } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for await result in group { |
||||||
|
if let result { |
||||||
|
group.cancelAll() // Stop other tasks as soon as we find a match |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func historyFromName(from sources: [CSVParser]) async throws -> Line? { |
||||||
|
#if DEBUG |
||||||
|
let start = Date() |
||||||
|
defer { |
||||||
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
||||||
|
print("func historyFromName()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
let filteredSources = sources.filter { $0.maleData == isMalePlayer() } |
||||||
|
let normalizedLastName = lastName.canonicalVersionWithPunctuation |
||||||
|
let normalizedFirstName = firstName.canonicalVersionWithPunctuation |
||||||
|
|
||||||
|
return await withTaskGroup(of: Line?.self) { group in |
||||||
|
for source in filteredSources { |
||||||
|
group.addTask { |
||||||
|
guard !Task.isCancelled else { print("Cancelled"); return nil } |
||||||
|
return try? await source.first { |
||||||
|
let lineValue = $0.rawValue.canonicalVersionWithPunctuation |
||||||
|
return lineValue.contains(";\(normalizedLastName);\(normalizedFirstName);") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for await result in group { |
||||||
|
if let result { |
||||||
|
group.cancelAll() // Stop other tasks as soon as we find a match |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func setComputedRank(in tournament: Tournament) { |
||||||
|
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000 |
||||||
|
switch tournament.tournamentCategory { |
||||||
|
case .men: |
||||||
|
computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) |
||||||
|
default: |
||||||
|
computedRank = currentRank |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func isMalePlayer() -> Bool { |
||||||
|
sex == .male |
||||||
|
} |
||||||
|
|
||||||
|
func validateLicenceId(_ year: Int) { |
||||||
|
if let currentLicenceId = licenceId { |
||||||
|
if currentLicenceId.trimmed.hasSuffix("(\(year-1))") { |
||||||
|
self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)") |
||||||
|
} else if let computedLicense = currentLicenceId.strippedLicense?.computedLicense { |
||||||
|
self.licenceId = computedLicense + " (\(year))" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func hasHomonym() -> Bool { |
||||||
|
let federalContext = PersistenceController.shared.localContainer.viewContext |
||||||
|
let fetchRequest = ImportedPlayer.fetchRequest() |
||||||
|
let predicate = NSPredicate(format: "firstName == %@ && lastName == %@", firstName, lastName) |
||||||
|
fetchRequest.predicate = predicate |
||||||
|
|
||||||
|
do { |
||||||
|
let count = try federalContext.count(for: fetchRequest) |
||||||
|
return count > 1 |
||||||
|
} catch { |
||||||
|
|
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func hasPaidOnline() -> Bool { |
||||||
|
registrationStatus == .confirmed && paymentId != nil && paymentType == .creditCard |
||||||
|
} |
||||||
|
|
||||||
|
func hasConfirmed() -> Bool { |
||||||
|
registrationStatus == .confirmed |
||||||
|
} |
||||||
|
|
||||||
|
func confirmRegistration() { |
||||||
|
registrationStatus = .confirmed |
||||||
|
} |
||||||
|
|
||||||
|
enum PlayerDataSource: Int, Codable { |
||||||
|
case frenchFederation = 0 |
||||||
|
case beachPadel = 1 |
||||||
|
} |
||||||
|
|
||||||
|
enum RegistrationStatus: Int, Codable, CaseIterable, Identifiable { |
||||||
|
case waiting = 0 |
||||||
|
case pending = 1 |
||||||
|
case confirmed = 2 |
||||||
|
case canceled = 3 |
||||||
|
|
||||||
|
var id: Int { self.rawValue } |
||||||
|
|
||||||
|
func localizedRegistrationStatus() -> String { |
||||||
|
switch self { |
||||||
|
case .waiting: |
||||||
|
return "En attente" |
||||||
|
case .pending: |
||||||
|
return "En cours" |
||||||
|
case .confirmed: |
||||||
|
return "Confirmé" |
||||||
|
case .canceled: |
||||||
|
return "Annulé" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { |
||||||
|
switch playerRank { |
||||||
|
case 0: return 0 |
||||||
|
case womanMax: return manMax - womanMax |
||||||
|
case manMax: return 0 |
||||||
|
default: |
||||||
|
return TournamentCategory.femaleInMaleAssimilationAddition(playerRank) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func insertOnServer() { |
||||||
|
self.tournamentStore?.playerRegistrations.writeChangeAndInsertOnServer(instance: self) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
extension PlayerRegistration: PlayerHolder { |
||||||
|
func getAssimilatedAsMaleRank() -> Int? { |
||||||
|
nil |
||||||
|
} |
||||||
|
|
||||||
|
func getFirstName() -> String { |
||||||
|
firstName |
||||||
|
} |
||||||
|
|
||||||
|
func getLastName() -> String { |
||||||
|
lastName |
||||||
|
} |
||||||
|
|
||||||
|
func getPoints() -> Double? { |
||||||
|
self.points |
||||||
|
} |
||||||
|
|
||||||
|
func getRank() -> Int? { |
||||||
|
rank |
||||||
|
} |
||||||
|
|
||||||
|
func isUnranked() -> Bool { |
||||||
|
rank == nil |
||||||
|
} |
||||||
|
|
||||||
|
func formattedRank() -> String { |
||||||
|
self.rankLabel() |
||||||
|
} |
||||||
|
|
||||||
|
func formattedLicense() -> String { |
||||||
|
if let licenceId { return licenceId.computedLicense } |
||||||
|
return "aucune licence" |
||||||
|
} |
||||||
|
|
||||||
|
var male: Bool { |
||||||
|
isMalePlayer() |
||||||
|
} |
||||||
|
|
||||||
|
func getBirthYear() -> Int? { |
||||||
|
nil |
||||||
|
} |
||||||
|
|
||||||
|
func getProgression() -> Int { |
||||||
|
0 |
||||||
|
} |
||||||
|
|
||||||
|
func getComputedRank() -> Int? { |
||||||
|
computedRank |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
enum PlayerDataSource: Int, Codable { |
||||||
|
case frenchFederation = 0 |
||||||
|
case beachPadel = 1 |
||||||
|
} |
||||||
|
|
||||||
|
enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable { |
||||||
|
init?(rawValue: Int?) { |
||||||
|
guard let value = rawValue else { return nil } |
||||||
|
self.init(rawValue: value) |
||||||
|
} |
||||||
|
|
||||||
|
var id: Self { |
||||||
|
self |
||||||
|
} |
||||||
|
|
||||||
|
case female = 0 |
||||||
|
case male = 1 |
||||||
|
} |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
# Procédure de création de classe |
||||||
|
Dans Swift: |
||||||
|
- Dans Data > Gen > créer un nouveau fichier json pour la classe |
||||||
|
- Le paramètre "foreignKey" permet de générer une méthode qui récupère l'objet parent. Ajouter une étoile permet d'indiquer que l'on cherche l'objet dans le Store de l'objet et non le Store.main. |
||||||
|
- Pour générer les fichiers, on se place dans le répertoire de generator.py et on lance la commande : python generator.py -i . -o . |
||||||
|
- il faut avoir inflect: pip install inflect |
||||||
|
|
||||||
|
Dans Django: |
||||||
|
- Les modèles de base doivent étendre BaseModel |
||||||
|
- Les modèles stockés dans des répertoires doivent étendre SideStoreModel |
||||||
|
- Les classes d'admin doivent étendre SyncedObjectAdmin |
||||||
|
- Les ForeignKey doivent toujours avoir on_delete=models.SET_NULL |
||||||
|
- Pour se faciliter la vie dans l'admin, on veut que les delete effacent les enfants malgré tout. Il faut donc implémenter la méthode delete_dependencies(self) de la classe |
||||||
|
|
||||||
|
|
||||||
|
# Procédure d'ajout de champ dans une classe |
||||||
|
|
||||||
|
Dans Swift: |
||||||
|
- Ouvrir le fichier .json correspondant à la classe |
||||||
|
- Regénérer la classe, voir ci-dessus pour la commande |
||||||
|
- Ouvrir **ServerDataTests** et ajouter un test sur le champ |
||||||
|
- Pour que les tests sur les dates fonctionnent, on peut tester date.formatted() par exemple |
||||||
|
|
||||||
|
Dans Django: |
||||||
|
- Ajouter le champ dans la classe |
||||||
|
- Si c'est une ForeignKey, *toujours* mettre un related_name sinon la synchro casse |
||||||
|
- Si c'est un champ dans **CustomUser**: |
||||||
|
- Ajouter le champ à la méthode fields_for_update |
||||||
|
- Ajouter le champ dans UserSerializer > create > create_user dans serializers.py |
||||||
|
- L'ajouter aussi dans admin.py si nécéssaire |
||||||
|
- Faire le *makemigrations* + *migrate* |
||||||
|
|
||||||
|
Note: Les nouvelles classes de model doivent étendre BaseModel ou SideStoreModel |
||||||
|
|
||||||
|
Enfin, revenir dans Xcode, ouvrir ServerDataTests et lancer le test mis à jour |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,779 @@ |
|||||||
|
// |
||||||
|
// TeamRegistration.swift |
||||||
|
// Padel Tournament |
||||||
|
// |
||||||
|
// Created by razmig on 10/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class TeamRegistration: BaseTeamRegistration, SideStorable { |
||||||
|
|
||||||
|
// static func resourceName() -> String { "team-registrations" } |
||||||
|
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
// static func filterByStoreIdentifier() -> Bool { return true } |
||||||
|
// static var relationshipNames: [String] = [] |
||||||
|
// |
||||||
|
// var id: String = Store.randomId() |
||||||
|
// var lastUpdate: Date |
||||||
|
// var tournament: String |
||||||
|
// var groupStage: String? |
||||||
|
// var registrationDate: Date? |
||||||
|
// var callDate: Date? |
||||||
|
// var bracketPosition: Int? |
||||||
|
// var groupStagePosition: Int? |
||||||
|
// var comment: String? |
||||||
|
// var source: String? |
||||||
|
// var sourceValue: String? |
||||||
|
// var logo: String? |
||||||
|
// var name: String? |
||||||
|
// |
||||||
|
// var walkOut: Bool = false |
||||||
|
// var wildCardBracket: Bool = false |
||||||
|
// var wildCardGroupStage: Bool = false |
||||||
|
// var weight: Int = 0 |
||||||
|
// var lockedWeight: Int? |
||||||
|
// var confirmationDate: Date? |
||||||
|
// var qualified: Bool = false |
||||||
|
// var finalRanking: Int? |
||||||
|
// var pointsEarned: Int? |
||||||
|
// |
||||||
|
// var storeId: String? = nil |
||||||
|
|
||||||
|
init( |
||||||
|
tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, |
||||||
|
callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, |
||||||
|
comment: String? = nil, source: String? = nil, sourceValue: String? = nil, |
||||||
|
logo: String? = nil, name: String? = nil, walkOut: Bool = false, |
||||||
|
wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, |
||||||
|
lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false |
||||||
|
) { |
||||||
|
|
||||||
|
super.init() |
||||||
|
|
||||||
|
// self.storeId = tournament |
||||||
|
self.tournament = tournament |
||||||
|
self.groupStage = groupStage |
||||||
|
self.registrationDate = registrationDate ?? Date() |
||||||
|
self.callDate = callDate |
||||||
|
self.bracketPosition = bracketPosition |
||||||
|
self.groupStagePosition = groupStagePosition |
||||||
|
self.comment = comment |
||||||
|
self.source = source |
||||||
|
self.sourceValue = sourceValue |
||||||
|
self.logo = logo |
||||||
|
self.name = name |
||||||
|
self.walkOut = walkOut |
||||||
|
self.wildCardBracket = wildCardBracket |
||||||
|
self.wildCardGroupStage = wildCardGroupStage |
||||||
|
self.weight = weight |
||||||
|
self.lockedWeight = lockedWeight |
||||||
|
self.confirmationDate = confirmationDate |
||||||
|
self.qualified = qualified |
||||||
|
} |
||||||
|
|
||||||
|
func hasRegisteredOnline() -> Bool { |
||||||
|
players().anySatisfy({ $0.registeredOnline }) |
||||||
|
} |
||||||
|
|
||||||
|
func hasPaidOnline() -> Bool { |
||||||
|
players().anySatisfy({ $0.hasPaidOnline() }) |
||||||
|
} |
||||||
|
|
||||||
|
func hasConfirmed() -> Bool { |
||||||
|
players().allSatisfy({ $0.hasConfirmed() }) |
||||||
|
} |
||||||
|
|
||||||
|
func confirmRegistration() { |
||||||
|
let players = players() |
||||||
|
players.forEach({ $0.confirmRegistration() }) |
||||||
|
tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) |
||||||
|
} |
||||||
|
|
||||||
|
func unrankedOrUnknown() -> Bool { |
||||||
|
players().anySatisfy({ $0.source == nil }) |
||||||
|
} |
||||||
|
|
||||||
|
func isOutOfTournament() -> Bool { |
||||||
|
walkOut |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: any Decoder) throws { |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentStore: TournamentStore? { |
||||||
|
return TournamentLibrary.shared.store(tournamentId: self.tournament) |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - Computed dependencies |
||||||
|
|
||||||
|
func unsortedPlayers() -> [PlayerRegistration] { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return [] } |
||||||
|
return tournamentStore.playerRegistrations.filter { |
||||||
|
$0.teamRegistration == self.id && $0.coach == false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - |
||||||
|
|
||||||
|
func deleteTeamScores() { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return } |
||||||
|
let ts = tournamentStore.teamScores.filter({ $0.teamRegistration == id }) |
||||||
|
tournamentStore.teamScores.delete(contentOfs: ts) |
||||||
|
} |
||||||
|
|
||||||
|
override func deleteDependencies() { |
||||||
|
let unsortedPlayers = unsortedPlayers() |
||||||
|
for player in unsortedPlayers { |
||||||
|
player.deleteDependencies() |
||||||
|
} |
||||||
|
self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers) |
||||||
|
|
||||||
|
let teamScores = teamScores() |
||||||
|
for teamScore in teamScores { |
||||||
|
teamScore.deleteDependencies() |
||||||
|
} |
||||||
|
self.tournamentStore?.teamScores.deleteDependencies(teamScores) |
||||||
|
} |
||||||
|
|
||||||
|
func hasArrived(isHere: Bool = false) { |
||||||
|
let unsortedPlayers = unsortedPlayers() |
||||||
|
unsortedPlayers.forEach({ $0.hasArrived = !isHere }) |
||||||
|
self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) |
||||||
|
} |
||||||
|
|
||||||
|
func isHere() -> Bool { |
||||||
|
let unsortedPlayers = unsortedPlayers() |
||||||
|
if unsortedPlayers.isEmpty { return false } |
||||||
|
return unsortedPlayers.allSatisfy({ $0.hasArrived }) |
||||||
|
} |
||||||
|
|
||||||
|
func isSeedable() -> Bool { |
||||||
|
bracketPosition == nil && groupStage == nil |
||||||
|
} |
||||||
|
|
||||||
|
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { |
||||||
|
var teamPosition: TeamPosition { |
||||||
|
if let slot { |
||||||
|
return slot |
||||||
|
} else { |
||||||
|
let matchIndex = match.index |
||||||
|
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) |
||||||
|
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) |
||||||
|
let isUpper = |
||||||
|
RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) |
||||||
|
< (numberOfMatches / 2) |
||||||
|
var teamPosition = slot ?? (isUpper ? .one : .two) |
||||||
|
if opposingSeeding { |
||||||
|
teamPosition = slot ?? (isUpper ? .two : .one) |
||||||
|
} |
||||||
|
return teamPosition |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) |
||||||
|
tournamentObject()?.resetTeamScores(in: bracketPosition) |
||||||
|
self.bracketPosition = seedPosition |
||||||
|
if groupStagePosition != nil && qualified == false { |
||||||
|
qualified = true |
||||||
|
} |
||||||
|
if let tournament = tournamentObject() { |
||||||
|
if let index = index(in: tournament.selectedSortedTeams()) { |
||||||
|
let drawLog = DrawLog( |
||||||
|
tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, |
||||||
|
drawTeamPosition: teamPosition, drawType: .seed) |
||||||
|
do { |
||||||
|
try tournamentStore?.drawLogs.addOrUpdate(instance: drawLog) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
tournament.updateTeamScores(in: bracketPosition) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func expectedSummonDate() -> Date? { |
||||||
|
if let groupStageStartDate = groupStageObject()?.startDate { |
||||||
|
return groupStageStartDate |
||||||
|
} else if let roundMatchStartDate = initialMatch()?.startDate { |
||||||
|
return roundMatchStartDate |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
var initialWeight: Int { |
||||||
|
return lockedWeight ?? weight |
||||||
|
} |
||||||
|
|
||||||
|
func called() -> Bool { |
||||||
|
return callDate != nil |
||||||
|
} |
||||||
|
|
||||||
|
func confirmed() -> Bool { |
||||||
|
return confirmationDate != nil |
||||||
|
} |
||||||
|
|
||||||
|
func getPhoneNumbers() -> [String] { |
||||||
|
return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false }) |
||||||
|
} |
||||||
|
|
||||||
|
func getMail() -> [String] { |
||||||
|
let mails = players().compactMap({ $0.email }) |
||||||
|
return mails |
||||||
|
} |
||||||
|
|
||||||
|
func isImported() -> Bool { |
||||||
|
let unsortedPlayers = unsortedPlayers() |
||||||
|
if unsortedPlayers.isEmpty { return false } |
||||||
|
|
||||||
|
return unsortedPlayers.allSatisfy({ $0.isImported() }) |
||||||
|
} |
||||||
|
|
||||||
|
func isWildCard() -> Bool { |
||||||
|
return wildCardBracket || wildCardGroupStage |
||||||
|
} |
||||||
|
|
||||||
|
func isPlaying() -> Bool { |
||||||
|
return currentMatch() != nil |
||||||
|
} |
||||||
|
|
||||||
|
func currentMatch() -> Match? { |
||||||
|
return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() }) |
||||||
|
} |
||||||
|
|
||||||
|
func teamScores() -> [TeamScore] { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return [] } |
||||||
|
return tournamentStore.teamScores.filter({ $0.teamRegistration == id }) |
||||||
|
} |
||||||
|
|
||||||
|
func wins() -> [Match] { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return [] } |
||||||
|
return tournamentStore.matches.filter({ $0.winningTeamId == id }) |
||||||
|
} |
||||||
|
|
||||||
|
func loses() -> [Match] { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return [] } |
||||||
|
return tournamentStore.matches.filter({ $0.losingTeamId == id }) |
||||||
|
} |
||||||
|
|
||||||
|
func matches() -> [Match] { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return [] } |
||||||
|
return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id }) |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentCategory: TournamentCategory { |
||||||
|
tournamentObject()?.tournamentCategory ?? .men |
||||||
|
} |
||||||
|
|
||||||
|
@objc |
||||||
|
var canonicalName: String { |
||||||
|
players().map { $0.canonicalName }.joined(separator: " ") |
||||||
|
} |
||||||
|
|
||||||
|
func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool { |
||||||
|
guard let codeClubOrClubName else { return true } |
||||||
|
return unsortedPlayers().anySatisfy({ |
||||||
|
$0.clubName?.contains(codeClubOrClubName) == true |
||||||
|
|| $0.clubName?.contains(codeClubOrClubName) == true |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) { |
||||||
|
self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory) |
||||||
|
} |
||||||
|
|
||||||
|
func teamLabel( |
||||||
|
_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&" |
||||||
|
) -> String { |
||||||
|
if let name { return name } |
||||||
|
return players().map { $0.playerLabel(displayStyle) }.joined( |
||||||
|
separator: twoLines ? "\n" : " \(separator) ") |
||||||
|
} |
||||||
|
|
||||||
|
func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String { |
||||||
|
[ |
||||||
|
displayTeamName ? name : nil, displayRank ? seedIndex() : nil, |
||||||
|
displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel(), |
||||||
|
].compactMap({ $0 }).joined(separator: " ") |
||||||
|
} |
||||||
|
|
||||||
|
func seedIndex() -> String? { |
||||||
|
guard let tournament = tournamentObject() else { return nil } |
||||||
|
guard let index = index(in: tournament.selectedSortedTeams()) else { return nil } |
||||||
|
return "(\(index + 1))" |
||||||
|
} |
||||||
|
|
||||||
|
func index(in teams: [TeamRegistration]) -> Int? { |
||||||
|
return teams.firstIndex(where: { $0.id == id }) |
||||||
|
} |
||||||
|
|
||||||
|
func formattedSeed(in teams: [TeamRegistration]? = nil) -> String { |
||||||
|
let selectedSortedTeams = teams ?? tournamentObject()?.selectedSortedTeams() ?? [] |
||||||
|
if let index = index(in: selectedSortedTeams) { |
||||||
|
return "#\(index + 1)" |
||||||
|
} else { |
||||||
|
return "###" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func contains(_ searchField: String) -> Bool { |
||||||
|
return unsortedPlayers().anySatisfy({ $0.contains(searchField) }) |
||||||
|
|| self.name?.localizedCaseInsensitiveContains(searchField) == true |
||||||
|
} |
||||||
|
|
||||||
|
func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool { |
||||||
|
let arrayOfIds: [String] = unsortedPlayers().compactMap({ |
||||||
|
$0.licenceId?.strippedLicense?.canonicalVersion |
||||||
|
}) |
||||||
|
let ids: Set<String> = Set<String>(arrayOfIds.sorted()) |
||||||
|
let searchedIds = Set<String>( |
||||||
|
playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted()) |
||||||
|
if ids.isEmpty || searchedIds.isEmpty { return false } |
||||||
|
return ids.hashValue == searchedIds.hashValue |
||||||
|
} |
||||||
|
|
||||||
|
func includes(players: [PlayerRegistration]) -> Bool { |
||||||
|
let unsortedPlayers = unsortedPlayers() |
||||||
|
guard players.count == unsortedPlayers.count else { return false } |
||||||
|
return players.allSatisfy { player in |
||||||
|
unsortedPlayers.anySatisfy { _player in |
||||||
|
_player.isSameAs(player) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func includes(player: PlayerRegistration) -> Bool { |
||||||
|
return unsortedPlayers().anySatisfy { _player in |
||||||
|
_player.isSameAs(player) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func canPlay() -> Bool { |
||||||
|
let unsortedPlayers = unsortedPlayers() |
||||||
|
if unsortedPlayers.isEmpty { return false } |
||||||
|
|
||||||
|
return matches().isEmpty == false |
||||||
|
|| unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived }) |
||||||
|
} |
||||||
|
|
||||||
|
func availableForSeedPick() -> Bool { |
||||||
|
return groupStage == nil && bracketPosition == nil |
||||||
|
} |
||||||
|
|
||||||
|
func inGroupStage() -> Bool { |
||||||
|
return groupStagePosition != nil |
||||||
|
} |
||||||
|
|
||||||
|
func inRound() -> Bool { |
||||||
|
return bracketPosition != nil |
||||||
|
} |
||||||
|
|
||||||
|
func positionLabel() -> String? { |
||||||
|
if groupStagePosition != nil { return "Poule" } |
||||||
|
if let initialRound = initialRound() { |
||||||
|
return initialRound.roundTitle() |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func initialRoundColor() -> Color? { |
||||||
|
if walkOut { return Color.logoRed } |
||||||
|
if groupStagePosition != nil || wildCardGroupStage { return Color.blue } |
||||||
|
if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] { |
||||||
|
return Color(uiColor: .init(fromHex: colorHex)) |
||||||
|
} else if wildCardBracket { |
||||||
|
return Color.mint |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func resetGroupeStagePosition() { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return } |
||||||
|
if let groupStage { |
||||||
|
let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map { |
||||||
|
$0.id |
||||||
|
} |
||||||
|
let teamScores = tournamentStore.teamScores.filter({ |
||||||
|
$0.teamRegistration == id && matches.contains($0.match) |
||||||
|
}) |
||||||
|
tournamentStore.teamScores.delete(contentOfs: teamScores) |
||||||
|
} |
||||||
|
//groupStageObject()?._matches().forEach({ $0.updateTeamScores() }) |
||||||
|
groupStage = nil |
||||||
|
groupStagePosition = nil |
||||||
|
} |
||||||
|
|
||||||
|
func resetBracketPosition() { |
||||||
|
guard let tournamentStore = self.tournamentStore else { return } |
||||||
|
let matches = tournamentStore.matches.filter({ $0.groupStage == nil }).map { $0.id } |
||||||
|
let teamScores = tournamentStore.teamScores.filter({ |
||||||
|
$0.teamRegistration == id && matches.contains($0.match) |
||||||
|
}) |
||||||
|
tournamentStore.teamScores.delete(contentOfs: teamScores) |
||||||
|
|
||||||
|
self.bracketPosition = nil |
||||||
|
} |
||||||
|
|
||||||
|
func resetPositions() { |
||||||
|
resetGroupeStagePosition() |
||||||
|
resetBracketPosition() |
||||||
|
} |
||||||
|
|
||||||
|
func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String { |
||||||
|
switch exportFormat { |
||||||
|
case .rawText: |
||||||
|
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name] |
||||||
|
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator()) |
||||||
|
case .csv: |
||||||
|
return [ |
||||||
|
index.formatted(), playersPasteData(exportFormat), |
||||||
|
isWildCard() ? "WC" : weight.formatted(), |
||||||
|
].joined(separator: exportFormat.separator()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var computedRegistrationDate: Date { |
||||||
|
return registrationDate ?? .distantFuture |
||||||
|
} |
||||||
|
|
||||||
|
func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? { |
||||||
|
guard let registrationDate else { return nil } |
||||||
|
|
||||||
|
let formattedDate = registrationDate.formatted( |
||||||
|
.dateTime.weekday().day().month().hour().minute()) |
||||||
|
let onlineSuffix = hasRegisteredOnline() ? " en ligne" : "" |
||||||
|
|
||||||
|
switch exportFormat { |
||||||
|
case .rawText: |
||||||
|
return "Inscrit\(onlineSuffix) le \(formattedDate)" |
||||||
|
case .csv: |
||||||
|
return formattedDate |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func formattedSummonDate(_ exportFormat: ExportFormat = .rawText) -> String? { |
||||||
|
|
||||||
|
switch exportFormat { |
||||||
|
case .rawText: |
||||||
|
if let callDate { |
||||||
|
return "Convoqué le " |
||||||
|
+ callDate.formatted(.dateTime.weekday().day().month().hour().minute()) |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
case .csv: |
||||||
|
if let callDate { |
||||||
|
return callDate.formatted(.dateTime.weekday().day().month().hour().minute()) |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String { |
||||||
|
switch exportFormat { |
||||||
|
case .rawText: |
||||||
|
return players().map { $0.pasteData(exportFormat) }.joined( |
||||||
|
separator: exportFormat.newLineSeparator()) |
||||||
|
case .csv: |
||||||
|
return players().map { |
||||||
|
[$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()] |
||||||
|
.joined(separator: exportFormat.separator()) |
||||||
|
}.joined(separator: exportFormat.separator()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func updatePlayers( |
||||||
|
_ players: Set<PlayerRegistration>, |
||||||
|
inTournamentCategory tournamentCategory: TournamentCategory |
||||||
|
) { |
||||||
|
let previousPlayers = Set(unsortedPlayers()) |
||||||
|
|
||||||
|
players.forEach { player in |
||||||
|
previousPlayers.forEach { oldPlayer in |
||||||
|
if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense, |
||||||
|
player.licenceId?.strippedLicense != nil |
||||||
|
{ |
||||||
|
player.registeredOnline = oldPlayer.registeredOnline |
||||||
|
player.paymentType = oldPlayer.paymentType |
||||||
|
player.paymentId = oldPlayer.paymentId |
||||||
|
player.registrationStatus = oldPlayer.registrationStatus |
||||||
|
player.timeToConfirm = oldPlayer.timeToConfirm |
||||||
|
player.coach = oldPlayer.coach |
||||||
|
player.tournamentPlayed = oldPlayer.tournamentPlayed |
||||||
|
player.points = oldPlayer.points |
||||||
|
player.captain = oldPlayer.captain |
||||||
|
player.assimilation = oldPlayer.assimilation |
||||||
|
player.ligueName = oldPlayer.ligueName |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let playersToRemove = previousPlayers.subtracting(players) |
||||||
|
self.tournamentStore?.playerRegistrations.delete(contentOfs: Array(playersToRemove)) |
||||||
|
setWeight(from: Array(players), inTournamentCategory: tournamentCategory) |
||||||
|
|
||||||
|
players.forEach { player in |
||||||
|
player.teamRegistration = id |
||||||
|
} |
||||||
|
|
||||||
|
// do { |
||||||
|
// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) |
||||||
|
// } catch { |
||||||
|
// Logger.error(error) |
||||||
|
// } |
||||||
|
} |
||||||
|
|
||||||
|
typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?) |
||||||
|
|
||||||
|
func replacementRange() -> TeamRange? { |
||||||
|
guard let tournamentObject = tournamentObject() else { return nil } |
||||||
|
guard let index = tournamentObject.indexOf(team: self) else { return nil } |
||||||
|
let selectedSortedTeams = tournamentObject.selectedSortedTeams() |
||||||
|
let left = selectedSortedTeams[safe: index - 1] |
||||||
|
let right = selectedSortedTeams[safe: index + 1] |
||||||
|
return (left: left, right: right) |
||||||
|
} |
||||||
|
|
||||||
|
func replacementRangeExtended() -> TeamRange? { |
||||||
|
guard let tournamentObject = tournamentObject() else { return nil } |
||||||
|
guard let groupStagePosition else { return nil } |
||||||
|
let selectedSortedTeams = tournamentObject.selectedSortedTeams() |
||||||
|
var left: TeamRegistration? = nil |
||||||
|
if groupStagePosition == 0 { |
||||||
|
left = tournamentObject.seeds().last |
||||||
|
} else { |
||||||
|
let previousHat = selectedSortedTeams.filter({ |
||||||
|
$0.groupStagePosition == groupStagePosition - 1 |
||||||
|
}).sorted(by: \.weight) |
||||||
|
left = previousHat.last |
||||||
|
} |
||||||
|
var right: TeamRegistration? = nil |
||||||
|
if groupStagePosition == tournamentObject.teamsPerGroupStage - 1 { |
||||||
|
right = nil |
||||||
|
} else { |
||||||
|
let previousHat = selectedSortedTeams.filter({ |
||||||
|
$0.groupStagePosition == groupStagePosition + 1 |
||||||
|
}).sorted(by: \.weight) |
||||||
|
right = previousHat.first |
||||||
|
} |
||||||
|
return (left: left, right: right) |
||||||
|
} |
||||||
|
|
||||||
|
typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool |
||||||
|
|
||||||
|
func players() -> [PlayerRegistration] { |
||||||
|
|
||||||
|
self.unsortedPlayers().sorted { (lhs, rhs) in |
||||||
|
let predicates: [AreInIncreasingOrder] = [ |
||||||
|
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, |
||||||
|
{ $0.rank ?? Int.max < $1.rank ?? Int.max }, |
||||||
|
{ $0.lastName < $1.lastName }, |
||||||
|
{ $0.firstName < $1.firstName }, |
||||||
|
] |
||||||
|
|
||||||
|
for predicate in predicates { |
||||||
|
if !predicate(lhs, rhs) && !predicate(rhs, lhs) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
return predicate(lhs, rhs) |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func coaches() -> [PlayerRegistration] { |
||||||
|
guard let store = self.tournamentStore else { return [] } |
||||||
|
return store.playerRegistrations.filter { $0.coach } |
||||||
|
} |
||||||
|
|
||||||
|
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,+) |
||||||
|
} |
||||||
|
|
||||||
|
func significantPlayerCount() -> Int { |
||||||
|
return tournamentObject()?.significantPlayerCount() ?? 2 |
||||||
|
} |
||||||
|
|
||||||
|
func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] { |
||||||
|
let players = unsortedPlayers() |
||||||
|
if players.count >= 2 { return [] } |
||||||
|
let s = players.compactMap { $0.sex?.rawValue } |
||||||
|
var missing = tournamentCategory.mandatoryPlayerType() |
||||||
|
s.forEach { i in |
||||||
|
if let index = missing.firstIndex(of: i) { |
||||||
|
missing.remove(at: index) |
||||||
|
} |
||||||
|
} |
||||||
|
return missing |
||||||
|
} |
||||||
|
|
||||||
|
func unrankValue(for malePlayer: Bool) -> Int { |
||||||
|
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000 |
||||||
|
} |
||||||
|
|
||||||
|
func groupStageObject() -> GroupStage? { |
||||||
|
guard let groupStage else { return nil } |
||||||
|
return self.tournamentStore?.groupStages.findById(groupStage) |
||||||
|
} |
||||||
|
|
||||||
|
func initialRound() -> Round? { |
||||||
|
guard let bracketPosition else { return nil } |
||||||
|
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2) |
||||||
|
return self.tournamentStore?.rounds.first(where: { $0.index == roundIndex }) |
||||||
|
} |
||||||
|
|
||||||
|
func initialMatch() -> Match? { |
||||||
|
guard let bracketPosition else { return nil } |
||||||
|
guard let initialRoundObject = initialRound() else { return nil } |
||||||
|
return self.tournamentStore?.matches.first(where: { |
||||||
|
$0.round == initialRoundObject.id && $0.index == bracketPosition / 2 |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func toggleSummonConfirmation() { |
||||||
|
if confirmationDate == nil { confirmationDate = Date() } else { confirmationDate = nil } |
||||||
|
} |
||||||
|
|
||||||
|
func didConfirmSummon() -> Bool { |
||||||
|
confirmationDate != nil |
||||||
|
} |
||||||
|
|
||||||
|
func tournamentObject() -> Tournament? { |
||||||
|
return Store.main.findById(tournament) |
||||||
|
} |
||||||
|
|
||||||
|
func groupStagePositionAtStep(_ step: Int) -> Int? { |
||||||
|
guard let groupStagePosition else { return nil } |
||||||
|
if step == 0 { |
||||||
|
return groupStagePosition |
||||||
|
} else if let groupStageObject = groupStageObject(), groupStageObject.hasEnded() { |
||||||
|
return groupStageObject.index |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func wildcardLabel() -> String? { |
||||||
|
if isWildCard() { |
||||||
|
let wildcardLabel: String = ["Wildcard", (wildCardBracket ? "Tableau" : "Poule")].joined(separator: " ") |
||||||
|
return wildcardLabel |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var _cachedRestingTime: (Bool, Date?)? |
||||||
|
|
||||||
|
func restingTime() -> Date? { |
||||||
|
if let _cachedRestingTime { return _cachedRestingTime.1 } |
||||||
|
let restingTime = matches().filter({ $0.hasEnded() }).sorted( |
||||||
|
by: \.computedEndDateForSorting |
||||||
|
).last?.endDate |
||||||
|
_cachedRestingTime = (true, restingTime) |
||||||
|
return restingTime |
||||||
|
} |
||||||
|
|
||||||
|
func resetRestingTime() { |
||||||
|
_cachedRestingTime = nil |
||||||
|
} |
||||||
|
|
||||||
|
var restingTimeForSorting: Date { |
||||||
|
restingTime()! |
||||||
|
} |
||||||
|
|
||||||
|
func teamNameLabel() -> String { |
||||||
|
if let name, name.isEmpty == false { |
||||||
|
return name |
||||||
|
} else { |
||||||
|
return "Toute l'équipe" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { |
||||||
|
if let bracketPosition, let drawMatchIndex { |
||||||
|
return drawMatchIndex != bracketPosition |
||||||
|
} else if bracketPosition != nil { |
||||||
|
return true |
||||||
|
} else if drawMatchIndex != nil { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func shouldDisplayRankAndWeight() -> Bool { |
||||||
|
unsortedPlayers().count > 0 |
||||||
|
} |
||||||
|
|
||||||
|
func teamInitialPositionBracket() -> String? { |
||||||
|
let round = initialMatch()?.roundAndMatchTitle() |
||||||
|
if let round { |
||||||
|
return round |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func teamInitialPositionGroupStage() -> String? { |
||||||
|
let groupStage = self.groupStageObject() |
||||||
|
let group = groupStage?.groupStageTitle(.title) |
||||||
|
var groupPositionLabel: String? = nil |
||||||
|
if let finalPosition = groupStage?.finalPosition(ofTeam: self) { |
||||||
|
groupPositionLabel = (finalPosition + 1).ordinalFormatted() |
||||||
|
} else if let groupStagePosition { |
||||||
|
groupPositionLabel = "\(groupStagePosition + 1)" |
||||||
|
} |
||||||
|
|
||||||
|
if let group { |
||||||
|
if let groupPositionLabel { |
||||||
|
return [group, "#\(groupPositionLabel)"].joined(separator: " ") |
||||||
|
} else { |
||||||
|
return group |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func qualifiedStatus(hideBracketStatus: Bool = false) -> String? { |
||||||
|
let teamInitialPositionBracket = teamInitialPositionBracket() |
||||||
|
let groupStageTitle = teamInitialPositionGroupStage() |
||||||
|
|
||||||
|
let base: String? = qualified ? "Qualifié" : nil |
||||||
|
if let groupStageTitle, let teamInitialPositionBracket, hideBracketStatus == false { |
||||||
|
return [base, groupStageTitle, ">", teamInitialPositionBracket].compactMap({ $0 }).joined(separator: " ") |
||||||
|
} else if let groupStageTitle { |
||||||
|
return [base, groupStageTitle].compactMap({ $0 }).joined(separator: " ") |
||||||
|
} else if hideBracketStatus == false { |
||||||
|
return teamInitialPositionBracket |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func insertOnServer() { |
||||||
|
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self) |
||||||
|
for playerRegistration in self.unsortedPlayers() { |
||||||
|
playerRegistration.insertOnServer() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
enum TeamDataSource: Int, Codable { |
||||||
|
case beachPadel |
||||||
|
} |
||||||
@ -0,0 +1,117 @@ |
|||||||
|
// |
||||||
|
// TeamScore.swift |
||||||
|
// Padel Tournament |
||||||
|
// |
||||||
|
// Created by razmig on 10/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
@Observable |
||||||
|
final class TeamScore: BaseTeamScore, SideStorable { |
||||||
|
|
||||||
|
|
||||||
|
// static func resourceName() -> String { "team-scores" } |
||||||
|
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } |
||||||
|
// static func filterByStoreIdentifier() -> Bool { return true } |
||||||
|
// static var relationshipNames: [String] = ["match"] |
||||||
|
// |
||||||
|
// var id: String = Store.randomId() |
||||||
|
// var lastUpdate: Date |
||||||
|
// var match: String |
||||||
|
// var teamRegistration: String? |
||||||
|
// //var playerRegistrations: [String] = [] |
||||||
|
// var score: String? |
||||||
|
// var walkOut: Int? |
||||||
|
// var luckyLoser: Int? |
||||||
|
// |
||||||
|
// var storeId: String? = nil |
||||||
|
|
||||||
|
init(match: String, teamRegistration: String? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Int? = nil) { |
||||||
|
super.init(match: match, teamRegistration: teamRegistration, score: score, walkOut: walkOut, luckyLoser: luckyLoser) |
||||||
|
|
||||||
|
// self.match = match |
||||||
|
// self.teamRegistration = teamRegistration |
||||||
|
//// self.playerRegistrations = playerRegistrations |
||||||
|
// self.score = score |
||||||
|
// self.walkOut = walkOut |
||||||
|
// self.luckyLoser = luckyLoser |
||||||
|
} |
||||||
|
|
||||||
|
init(match: String, team: TeamRegistration?) { |
||||||
|
super.init(match: match) |
||||||
|
if let team { |
||||||
|
self.teamRegistration = team.id |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws { |
||||||
|
try super.init(from: decoder) |
||||||
|
} |
||||||
|
|
||||||
|
required public init() { |
||||||
|
super.init() |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentStore: TournamentStore? { |
||||||
|
guard let storeId else { |
||||||
|
fatalError("missing store id for \(String(describing: type(of: self)))") |
||||||
|
} |
||||||
|
return TournamentLibrary.shared.store(tournamentId: storeId) |
||||||
|
// |
||||||
|
// if let store = self.store as? TournamentStore { |
||||||
|
// return store |
||||||
|
// } |
||||||
|
// fatalError("missing store for \(String(describing: type(of: self)))") |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - Computed dependencies |
||||||
|
|
||||||
|
func matchObject() -> Match? { |
||||||
|
return self.tournamentStore?.matches.findById(self.match) |
||||||
|
} |
||||||
|
|
||||||
|
var team: TeamRegistration? { |
||||||
|
guard let teamRegistration else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return self.tournamentStore?.teamRegistrations.findById(teamRegistration) |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - |
||||||
|
|
||||||
|
func isWalkOut() -> Bool { |
||||||
|
return walkOut != nil |
||||||
|
} |
||||||
|
|
||||||
|
// enum CodingKeys: String, CodingKey { |
||||||
|
// case _id = "id" |
||||||
|
// case _storeId = "storeId" |
||||||
|
// case _lastUpdate = "lastUpdate" |
||||||
|
// case _match = "match" |
||||||
|
// case _teamRegistration = "teamRegistration" |
||||||
|
// //case _playerRegistrations = "playerRegistrations" |
||||||
|
// case _score = "score" |
||||||
|
// case _walkOut = "walkOut" |
||||||
|
// case _luckyLoser = "luckyLoser" |
||||||
|
// } |
||||||
|
// |
||||||
|
// func encode(to encoder: Encoder) throws { |
||||||
|
// var container = encoder.container(keyedBy: CodingKeys.self) |
||||||
|
// |
||||||
|
// try container.encode(id, forKey: ._id) |
||||||
|
// try container.encode(storeId, forKey: ._storeId) |
||||||
|
// try container.encode(lastUpdate, forKey: ._lastUpdate) |
||||||
|
// try container.encode(match, forKey: ._match) |
||||||
|
// try container.encode(teamRegistration, forKey: ._teamRegistration) |
||||||
|
// try container.encode(score, forKey: ._score) |
||||||
|
// try container.encode(walkOut, forKey: ._walkOut) |
||||||
|
// try container.encode(luckyLoser, forKey: ._luckyLoser) |
||||||
|
// } |
||||||
|
|
||||||
|
func insertOnServer() { |
||||||
|
self.tournamentStore?.teamScores.writeChangeAndInsertOnServer(instance: self) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
// |
||||||
|
// TournamentLibrary.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 11/11/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
class TournamentLibrary { |
||||||
|
|
||||||
|
static let shared: TournamentLibrary = TournamentLibrary() |
||||||
|
|
||||||
|
fileprivate var _stores: [String : TournamentStore] = [:] |
||||||
|
|
||||||
|
func store(tournamentId: String) -> TournamentStore? { |
||||||
|
guard let tournament = DataStore.shared.tournaments.first(where: { $0.id == tournamentId }) else { return nil } |
||||||
|
|
||||||
|
if let store = self._stores[tournamentId] { |
||||||
|
return store |
||||||
|
} |
||||||
|
let store = StoreCenter.main.store(identifier: tournamentId) |
||||||
|
let tournamentStore = TournamentStore(store: store) |
||||||
|
self._stores[tournamentId] = tournamentStore |
||||||
|
return tournamentStore |
||||||
|
} |
||||||
|
|
||||||
|
func reset() { |
||||||
|
self._stores.removeAll() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
// |
||||||
|
// TournamentStore.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 26/06/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
class TournamentStore: ObservableObject { |
||||||
|
|
||||||
|
var store: Store |
||||||
|
|
||||||
|
fileprivate(set) var groupStages: SyncedCollection<GroupStage> = SyncedCollection.placeholder() |
||||||
|
fileprivate(set) var matches: SyncedCollection<Match> = SyncedCollection.placeholder() |
||||||
|
fileprivate(set) var teamRegistrations: SyncedCollection<TeamRegistration> = SyncedCollection.placeholder() |
||||||
|
fileprivate(set) var playerRegistrations: SyncedCollection<PlayerRegistration> = SyncedCollection.placeholder() |
||||||
|
fileprivate(set) var rounds: SyncedCollection<Round> = SyncedCollection.placeholder() |
||||||
|
fileprivate(set) var teamScores: SyncedCollection<TeamScore> = SyncedCollection.placeholder() |
||||||
|
|
||||||
|
fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder() |
||||||
|
fileprivate(set) var drawLogs: SyncedCollection<DrawLog> = SyncedCollection.placeholder() |
||||||
|
|
||||||
|
// convenience init(tournament: Tournament) { |
||||||
|
// let store = StoreCenter.main.store(identifier: tournament.id) |
||||||
|
// self.init(store: store) |
||||||
|
// self._initialize() |
||||||
|
// } |
||||||
|
|
||||||
|
init(store: Store) { |
||||||
|
self.store = store |
||||||
|
self._initialize() |
||||||
|
} |
||||||
|
|
||||||
|
fileprivate func _initialize() { |
||||||
|
|
||||||
|
let indexed: Bool = true |
||||||
|
|
||||||
|
self.groupStages = self.store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.rounds = self.store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.teamRegistrations = self.store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.playerRegistrations = self.store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.matches = self.store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.teamScores = self.store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
self.matchSchedulers = self.store.registerCollection(indexed: indexed) |
||||||
|
self.drawLogs = self.store.registerSynchronizedCollection(indexed: indexed) |
||||||
|
|
||||||
|
self.store.loadCollectionsFromServerIfNoFile() |
||||||
|
|
||||||
|
NotificationCenter.default.addObserver( |
||||||
|
self, |
||||||
|
selector: #selector(_leStorageDidSynchronize), |
||||||
|
name: NSNotification.Name.LeStorageDidSynchronize, |
||||||
|
object: nil) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@objc func _leStorageDidSynchronize(notification: Notification) { |
||||||
|
// Logger.log("SYNCED > teamRegistrations count = \(self.teamRegistrations.count)") |
||||||
|
} |
||||||
|
|
||||||
|
deinit { |
||||||
|
NotificationCenter.default.removeObserver(self) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,96 @@ |
|||||||
|
// |
||||||
|
// Array+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 03/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
extension Array { |
||||||
|
func chunked(into size: Int) -> [[Element]] { |
||||||
|
return stride(from: 0, to: count, by: size).map { |
||||||
|
Array(self[$0 ..< Swift.min($0 + size, count)]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func anySatisfy(_ p: (Element) -> Bool) -> Bool { |
||||||
|
return first(where: { p($0) }) != nil |
||||||
|
//return !self.allSatisfy { !p($0) } |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the number of elements in the sequence is even |
||||||
|
var isEven: Bool { |
||||||
|
return self.count % 2 == 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the number of elements in the sequence is odd |
||||||
|
var isOdd: Bool { |
||||||
|
return self.count % 2 != 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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 { |
||||||
|
func customJoined(separator: String, lastSeparator: String) -> String { |
||||||
|
switch count { |
||||||
|
case 0: |
||||||
|
return "" |
||||||
|
case 1: |
||||||
|
return "\(self[0])" |
||||||
|
case 2: |
||||||
|
return "\(self[0]) \(lastSeparator) \(self[1])" |
||||||
|
default: |
||||||
|
let firstPart = dropLast().map { "\($0)" }.joined(separator: ", ") |
||||||
|
let lastPart = "\(lastSeparator) \(last!)" |
||||||
|
return "\(firstPart) \(lastPart)" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
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 { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the key exists in the dictionary |
||||||
|
if var array = self[key] { |
||||||
|
// If it exists, append the element to the array |
||||||
|
array.append(element) |
||||||
|
self[key] = array |
||||||
|
} else { |
||||||
|
// If it doesn't exist, create a new array with the element |
||||||
|
self[key] = [element] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
extension Array where Element == String { |
||||||
|
func formatList(maxDisplay: Int = 2) -> [String] { |
||||||
|
// Check if the array has fewer or equal elements than the maximum display limit |
||||||
|
if self.count <= maxDisplay { |
||||||
|
// Join all elements with commas |
||||||
|
return self |
||||||
|
} else { |
||||||
|
// Join only the first `maxDisplay` elements and add "et plus" |
||||||
|
let displayedItems = self.prefix(maxDisplay) |
||||||
|
let remainingCount = self.count - maxDisplay |
||||||
|
return displayedItems.dropLast() + [displayedItems.last! + " et \(remainingCount) de plus"] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -1,25 +0,0 @@ |
|||||||
// |
|
||||||
// Badge+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 15/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import SwiftUI |
|
||||||
import PadelClubData |
|
||||||
|
|
||||||
extension Badge { |
|
||||||
|
|
||||||
func color() -> Color { |
|
||||||
switch self { |
|
||||||
case .checkmark: |
|
||||||
.green |
|
||||||
case .xmark: |
|
||||||
.logoRed |
|
||||||
case .custom(_, let color): |
|
||||||
color |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,71 @@ |
|||||||
|
// |
||||||
|
// Calendar+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 28/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
extension Calendar { |
||||||
|
func numberOfDaysBetween(_ from: Date?, and to: Date?) -> Int { |
||||||
|
guard let from, let to else { return 0 } |
||||||
|
let fromDate = startOfDay(for: from) |
||||||
|
let toDate = startOfDay(for: to) |
||||||
|
let numberOfDays = dateComponents([.day], from: fromDate, to: toDate) |
||||||
|
|
||||||
|
return numberOfDays.day! // <1> |
||||||
|
} |
||||||
|
|
||||||
|
func isSameDay(date1: Date?, date2: Date?) -> Bool { |
||||||
|
guard let date1, let date2 else { return false } |
||||||
|
|
||||||
|
return numberOfDaysBetween(date1, and: date2) == 0 |
||||||
|
} |
||||||
|
|
||||||
|
func getSportAge() -> Int { |
||||||
|
let currentDate = Date() |
||||||
|
|
||||||
|
// Get the current year |
||||||
|
let currentYear = component(.year, from: currentDate) |
||||||
|
|
||||||
|
// Define the date components for 1st September and 31st December of the current year |
||||||
|
let septemberFirstComponents = DateComponents(year: currentYear, month: 9, day: 1) |
||||||
|
let decemberThirtyFirstComponents = DateComponents(year: currentYear, month: 12, day: 31) |
||||||
|
|
||||||
|
// Get the actual dates for 1st September and 31st December |
||||||
|
let septemberFirst = date(from: septemberFirstComponents)! |
||||||
|
let decemberThirtyFirst = date(from: decemberThirtyFirstComponents)! |
||||||
|
|
||||||
|
// Determine the sport year |
||||||
|
let sportYear: Int |
||||||
|
if currentDate >= septemberFirst && currentDate <= decemberThirtyFirst { |
||||||
|
// If after 1st September and before 31st December, use current year + 1 |
||||||
|
sportYear = currentYear + 1 |
||||||
|
} else { |
||||||
|
// Otherwise, use the current year |
||||||
|
sportYear = currentYear |
||||||
|
} |
||||||
|
return sportYear |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Calendar { |
||||||
|
// Add or subtract months from a date |
||||||
|
func addMonths(_ months: Int, to date: Date) -> Date { |
||||||
|
return self.date(byAdding: .month, value: months, to: date)! |
||||||
|
} |
||||||
|
|
||||||
|
// Generate a list of month start dates between two dates |
||||||
|
func generateMonthRange(startDate: Date, endDate: Date) -> [Date] { |
||||||
|
var dates: [Date] = [] |
||||||
|
var currentDate = startDate |
||||||
|
|
||||||
|
while currentDate <= endDate { |
||||||
|
dates.append(currentDate) |
||||||
|
currentDate = self.addMonths(1, to: currentDate) |
||||||
|
} |
||||||
|
|
||||||
|
return dates |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
// |
||||||
|
// KeyedEncodingContainer+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 18/09/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
extension KeyedDecodingContainer { |
||||||
|
|
||||||
|
func decodeEncrypted(key: Key) throws -> String { |
||||||
|
let data = try self.decode(Data.self, forKey: key) |
||||||
|
return try data.decryptData(pass: CryptoKey.pass.rawValue) |
||||||
|
} |
||||||
|
|
||||||
|
func decodeEncryptedIfPresent(key: Key) throws -> String? { |
||||||
|
let data = try self.decodeIfPresent(Data.self, forKey: key) |
||||||
|
if let data { |
||||||
|
return try data.decryptData(pass: CryptoKey.pass.rawValue) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
extension KeyedEncodingContainer { |
||||||
|
|
||||||
|
mutating func encodeAndEncrypt(_ value: Data, forKey key: Key) throws { |
||||||
|
let encryped: Data = try value.encrypt(pass: CryptoKey.pass.rawValue) |
||||||
|
try self.encode(encryped, forKey: key) |
||||||
|
} |
||||||
|
|
||||||
|
mutating func encodeAndEncryptIfPresent(_ value: Data?, forKey key: Key) throws { |
||||||
|
guard let value else { |
||||||
|
try encodeNil(forKey: key) |
||||||
|
return |
||||||
|
} |
||||||
|
try self.encodeAndEncrypt(value, forKey: key) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
@ -1,22 +0,0 @@ |
|||||||
// |
|
||||||
// CustomUser+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 15/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import PadelClubData |
|
||||||
|
|
||||||
extension CustomUser { |
|
||||||
|
|
||||||
func currentPlayerData() -> ImportedPlayer? { |
|
||||||
guard let licenceId = self.licenceId?.strippedLicense else { return nil } |
|
||||||
let federalContext = PersistenceController.shared.localContainer.viewContext |
|
||||||
let fetchRequest = ImportedPlayer.fetchRequest() |
|
||||||
let predicate = NSPredicate(format: "license == %@", licenceId) |
|
||||||
fetchRequest.predicate = predicate |
|
||||||
return try? federalContext.fetch(fetchRequest).first |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,262 @@ |
|||||||
|
// |
||||||
|
// Date+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 01/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
enum TimeOfDay { |
||||||
|
case morning |
||||||
|
case noon |
||||||
|
case afternoon |
||||||
|
case evening |
||||||
|
case night |
||||||
|
|
||||||
|
var hello: String { |
||||||
|
switch self { |
||||||
|
case .morning, .noon, .afternoon: |
||||||
|
return "Bonjour" |
||||||
|
case .evening, .night: |
||||||
|
return "Bonsoir" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var goodbye: String { |
||||||
|
switch self { |
||||||
|
case .morning, .noon, .afternoon: |
||||||
|
return "Bonne journée" |
||||||
|
case .evening, .night: |
||||||
|
return "Bonne soirée" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extension Date { |
||||||
|
func withoutSeconds() -> Date { |
||||||
|
let calendar = Calendar.current |
||||||
|
return calendar.date(bySettingHour: calendar.component(.hour, from: self), |
||||||
|
minute: calendar.component(.minute, from: self), |
||||||
|
second: 0, |
||||||
|
of: self)! |
||||||
|
} |
||||||
|
|
||||||
|
func localizedDate() -> String { |
||||||
|
self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute() |
||||||
|
} |
||||||
|
|
||||||
|
func formattedAsHourMinute() -> String { |
||||||
|
formatted(.dateTime.hour().minute()) |
||||||
|
} |
||||||
|
|
||||||
|
func formattedAsDate() -> String { |
||||||
|
formatted(.dateTime.weekday().day(.twoDigits).month().year()) |
||||||
|
} |
||||||
|
|
||||||
|
var monthYearFormatted: String { |
||||||
|
formatted(.dateTime.month(.wide).year(.defaultDigits)) |
||||||
|
} |
||||||
|
|
||||||
|
var twoDigitsYearFormatted: String { |
||||||
|
formatted(Date.FormatStyle(date: .numeric, time: .omitted).locale(Locale(identifier: "fr_FR")).year(.twoDigits)) |
||||||
|
} |
||||||
|
|
||||||
|
var timeOfDay: TimeOfDay { |
||||||
|
let hour = Calendar.current.component(.hour, from: self) |
||||||
|
switch hour { |
||||||
|
case 6..<12 : return .morning |
||||||
|
case 12 : return .noon |
||||||
|
case 13..<17 : return .afternoon |
||||||
|
case 17..<22 : return .evening |
||||||
|
default: return .night |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
extension Date { |
||||||
|
func isInCurrentYear() -> Bool { |
||||||
|
let calendar = Calendar.current |
||||||
|
let currentYear = calendar.component(.year, from: Date()) |
||||||
|
let yearOfDate = calendar.component(.year, from: self) |
||||||
|
|
||||||
|
return currentYear == yearOfDate |
||||||
|
} |
||||||
|
|
||||||
|
func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents { |
||||||
|
return calendar.dateComponents(Set(components), from: self) |
||||||
|
} |
||||||
|
|
||||||
|
func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int { |
||||||
|
return calendar.component(component, from: self) |
||||||
|
} |
||||||
|
|
||||||
|
var tomorrowAtNine: Date { |
||||||
|
let currentHour = Calendar.current.component(.hour, from: self) |
||||||
|
let startOfDay = Calendar.current.startOfDay(for: self) |
||||||
|
if currentHour < 8 { |
||||||
|
return Calendar.current.date(byAdding: .hour, value: 9, to: startOfDay)! |
||||||
|
} else { |
||||||
|
let date = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay) |
||||||
|
return Calendar.current.date(byAdding: .hour, value: 9, to: date!)! |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func atBeginningOfDay(hourInt: Int = 9) -> Date { |
||||||
|
Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)! |
||||||
|
} |
||||||
|
|
||||||
|
static var firstDayOfWeek = Calendar.current.firstWeekday |
||||||
|
static var capitalizedFirstLettersOfWeekdays: [String] = { |
||||||
|
let calendar = Calendar.current |
||||||
|
// let weekdays = calendar.shortWeekdaySymbols |
||||||
|
|
||||||
|
// return weekdays.map { weekday in |
||||||
|
// guard let firstLetter = weekday.first else { return "" } |
||||||
|
// return String(firstLetter).capitalized |
||||||
|
// } |
||||||
|
// Adjusted for the different weekday starts |
||||||
|
var weekdays = calendar.veryShortStandaloneWeekdaySymbols |
||||||
|
if firstDayOfWeek > 1 { |
||||||
|
for _ in 1..<firstDayOfWeek { |
||||||
|
if let first = weekdays.first { |
||||||
|
weekdays.append(first) |
||||||
|
weekdays.removeFirst() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return weekdays.map { $0.capitalized } |
||||||
|
}() |
||||||
|
|
||||||
|
static var fullMonthNames: [String] = { |
||||||
|
let dateFormatter = DateFormatter() |
||||||
|
dateFormatter.locale = Locale.current |
||||||
|
|
||||||
|
return (1...12).compactMap { month in |
||||||
|
dateFormatter.setLocalizedDateFormatFromTemplate("MMMM") |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
var endOfMonth: Date { |
||||||
|
let lastDay = Calendar.current.dateInterval(of: .month, for: self)!.end |
||||||
|
return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)! |
||||||
|
} |
||||||
|
|
||||||
|
var startOfPreviousMonth: Date { |
||||||
|
let dayInPreviousMonth = Calendar.current.date(byAdding: .month, value: -1, to: self)! |
||||||
|
return dayInPreviousMonth.startOfMonth |
||||||
|
} |
||||||
|
|
||||||
|
var numberOfDaysInMonth: Int { |
||||||
|
Calendar.current.component(.day, from: endOfMonth) |
||||||
|
} |
||||||
|
|
||||||
|
// var sundayBeforeStart: Date { |
||||||
|
// let startOfMonthWeekday = Calendar.current.component(.weekday, from: startOfMonth) |
||||||
|
// let numberFromPreviousMonth = startOfMonthWeekday - 1 |
||||||
|
// return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)! |
||||||
|
// } |
||||||
|
// New to accomodate for different start of week days |
||||||
|
var firstWeekDayBeforeStart: Date { |
||||||
|
let startOfMonthWeekday = Calendar.current.component(.weekday, from: startOfMonth) |
||||||
|
let numberFromPreviousMonth = startOfMonthWeekday - Self.firstDayOfWeek |
||||||
|
return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)! |
||||||
|
} |
||||||
|
|
||||||
|
var calendarDisplayDays: [Date] { |
||||||
|
var days: [Date] = [] |
||||||
|
// Current month days |
||||||
|
for dayOffset in 0..<numberOfDaysInMonth { |
||||||
|
let newDay = Calendar.current.date(byAdding: .day, value: dayOffset, to: startOfMonth) |
||||||
|
days.append(newDay!) |
||||||
|
} |
||||||
|
// previous month days |
||||||
|
for dayOffset in 0..<startOfPreviousMonth.numberOfDaysInMonth { |
||||||
|
let newDay = Calendar.current.date(byAdding: .day, value: dayOffset, to: startOfPreviousMonth) |
||||||
|
days.append(newDay!) |
||||||
|
} |
||||||
|
|
||||||
|
// Fixed to accomodate different weekday starts |
||||||
|
return days.filter { $0 >= firstWeekDayBeforeStart && $0 <= endOfMonth }.sorted(by: <) |
||||||
|
} |
||||||
|
|
||||||
|
var monthInt: Int { |
||||||
|
Calendar.current.component(.month, from: self) |
||||||
|
} |
||||||
|
|
||||||
|
var yearInt: Int { |
||||||
|
Calendar.current.component(.year, from: self) |
||||||
|
} |
||||||
|
|
||||||
|
var dayInt: Int { |
||||||
|
Calendar.current.component(.day, from: self) |
||||||
|
} |
||||||
|
|
||||||
|
var startOfDay: Date { |
||||||
|
Calendar.current.startOfDay(for: self) |
||||||
|
} |
||||||
|
|
||||||
|
func endOfDay() -> Date { |
||||||
|
let calendar = Calendar.current |
||||||
|
return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)! |
||||||
|
} |
||||||
|
|
||||||
|
func atNine() -> Date { |
||||||
|
let calendar = Calendar.current |
||||||
|
return calendar.date(bySettingHour: 9, minute: 0, second: 0, of: self)! |
||||||
|
} |
||||||
|
|
||||||
|
func atEightAM() -> Date { |
||||||
|
let calendar = Calendar.current |
||||||
|
return calendar.date(bySettingHour: 8, minute: 0, second: 0, of: self)! |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Date { |
||||||
|
func isEarlierThan(_ date: Date) -> Bool { |
||||||
|
Calendar.current.compare(self, to: date, toGranularity: .minute) == .orderedAscending |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Date { |
||||||
|
func localizedTime() -> String { |
||||||
|
self.formattedAsHourMinute() |
||||||
|
} |
||||||
|
|
||||||
|
func localizedDay() -> String { |
||||||
|
self.formatted(.dateTime.weekday(.wide).day()) |
||||||
|
} |
||||||
|
|
||||||
|
func localizedWeekDay() -> String { |
||||||
|
self.formatted(.dateTime.weekday(.wide)) |
||||||
|
} |
||||||
|
|
||||||
|
func timeElapsedString() -> String { |
||||||
|
let timeInterval = abs(Date().timeIntervalSince(self)) |
||||||
|
let duration = Duration.seconds(timeInterval) |
||||||
|
|
||||||
|
let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) |
||||||
|
return formatStyle.format(duration) |
||||||
|
} |
||||||
|
|
||||||
|
static var hourMinuteFormatter: DateComponentsFormatter = { |
||||||
|
let formatter = DateComponentsFormatter() |
||||||
|
formatter.allowedUnits = [.hour, .minute] // Customize units |
||||||
|
formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short |
||||||
|
return formatter |
||||||
|
}() |
||||||
|
|
||||||
|
func truncateMinutesAndSeconds() -> Date { |
||||||
|
let calendar = Calendar.current |
||||||
|
return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds() |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
// |
||||||
|
// FixedWidthInteger+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 03/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
public extension FixedWidthInteger { |
||||||
|
func ordinalFormattedSuffix(feminine: Bool = false) -> String { |
||||||
|
switch self { |
||||||
|
case 1: return feminine ? "ère" : "er" |
||||||
|
default: return "ème" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func ordinalFormatted(feminine: Bool = false) -> String { |
||||||
|
return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine) |
||||||
|
} |
||||||
|
|
||||||
|
private var isMany: Bool { |
||||||
|
self > 1 || self < -1 |
||||||
|
} |
||||||
|
|
||||||
|
var pluralSuffix: String { |
||||||
|
return isMany ? "s" : "" |
||||||
|
} |
||||||
|
|
||||||
|
func localizedPluralSuffix(_ plural: String = "s") -> String { |
||||||
|
return isMany ? plural : "" |
||||||
|
} |
||||||
|
|
||||||
|
func formattedAsRawString() -> String { |
||||||
|
String(self) |
||||||
|
} |
||||||
|
|
||||||
|
func durationInHourMinutes() -> String { |
||||||
|
let duration = Duration.seconds(self*60) |
||||||
|
let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) |
||||||
|
return formatStyle.format(duration) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
// |
||||||
|
// Locale+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 03/04/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
extension Locale { |
||||||
|
|
||||||
|
static func countries() -> [String] { |
||||||
|
var countries: [String] = [] |
||||||
|
|
||||||
|
for countryCode in Locale.Region.isoRegions { |
||||||
|
if let countryName = Locale.current.localizedString(forRegionCode: countryCode.identifier) { |
||||||
|
countries.append(countryName) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return countries.sorted() |
||||||
|
} |
||||||
|
|
||||||
|
static func defaultCurrency() -> String { |
||||||
|
// return "EUR" |
||||||
|
Locale.current.currency?.identifier ?? "EUR" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
// |
||||||
|
// MySortDescriptor.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 26/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
struct MySortDescriptor<Value> { |
||||||
|
var comparator: (Value, Value) -> ComparisonResult |
||||||
|
} |
||||||
|
|
||||||
|
extension MySortDescriptor { |
||||||
|
static func keyPath<T: Comparable>(_ keyPath: KeyPath<Value, T>) -> Self { |
||||||
|
Self { rootA, rootB in |
||||||
|
let valueA = rootA[keyPath: keyPath] |
||||||
|
let valueB = rootB[keyPath: keyPath] |
||||||
|
|
||||||
|
guard valueA != valueB else { |
||||||
|
return .orderedSame |
||||||
|
} |
||||||
|
|
||||||
|
return valueA < valueB ? .orderedAscending : .orderedDescending |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
// |
||||||
|
// NumberFormatter+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 27/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
extension NumberFormatter { |
||||||
|
static var ordinal: NumberFormatter = { |
||||||
|
let formatter = NumberFormatter() |
||||||
|
formatter.numberStyle = .ordinal |
||||||
|
return formatter |
||||||
|
}() |
||||||
|
|
||||||
|
static var standard: NumberFormatter = { |
||||||
|
return NumberFormatter() |
||||||
|
}() |
||||||
|
} |
||||||
@ -1,230 +0,0 @@ |
|||||||
// |
|
||||||
// PlayerRegistration+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 15/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import PadelClubData |
|
||||||
|
|
||||||
extension PlayerRegistration { |
|
||||||
|
|
||||||
convenience init(importedPlayer: ImportedPlayer) { |
|
||||||
self.init() |
|
||||||
self.teamRegistration = "" |
|
||||||
self.firstName = (importedPlayer.firstName ?? "").prefixTrimmed(50).capitalized |
|
||||||
self.lastName = (importedPlayer.lastName ?? "").prefixTrimmed(50).uppercased() |
|
||||||
self.licenceId = importedPlayer.license?.prefixTrimmed(50) ?? nil |
|
||||||
self.rank = Int(importedPlayer.rank) |
|
||||||
self.sex = importedPlayer.male ? .male : .female |
|
||||||
self.tournamentPlayed = importedPlayer.tournamentPlayed |
|
||||||
self.points = importedPlayer.getPoints() |
|
||||||
self.clubName = importedPlayer.clubName?.prefixTrimmed(200) |
|
||||||
self.clubCode = importedPlayer.clubCode?.replaceCharactersFromSet(characterSet: .whitespaces).prefixTrimmed(20) |
|
||||||
self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200) |
|
||||||
self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50) |
|
||||||
self.source = .frenchFederation |
|
||||||
self.birthdate = importedPlayer.birthYear?.prefixTrimmed(50) |
|
||||||
} |
|
||||||
|
|
||||||
convenience init?(federalData: [String], sex: Int, sexUnknown: Bool) { |
|
||||||
self.init() |
|
||||||
let _lastName = federalData[0].trimmed.uppercased() |
|
||||||
let _firstName = federalData[1].trimmed.capitalized |
|
||||||
if _lastName.isEmpty && _firstName.isEmpty { return nil } |
|
||||||
lastName = _lastName.prefixTrimmed(50) |
|
||||||
firstName = _firstName.prefixTrimmed(50) |
|
||||||
birthdate = federalData[2].formattedAsBirthdate().prefixTrimmed(50) |
|
||||||
licenceId = federalData[3].prefixTrimmed(50) |
|
||||||
clubName = federalData[4].prefixTrimmed(200) |
|
||||||
let stringRank = federalData[5] |
|
||||||
if stringRank.isEmpty { |
|
||||||
rank = nil |
|
||||||
} else { |
|
||||||
rank = Int(stringRank) |
|
||||||
} |
|
||||||
let _email = federalData[6] |
|
||||||
if _email.isEmpty == false { |
|
||||||
self.email = _email.prefixTrimmed(50) |
|
||||||
} |
|
||||||
let _phoneNumber = federalData[7] |
|
||||||
if _phoneNumber.isEmpty == false { |
|
||||||
self.phoneNumber = _phoneNumber.prefixTrimmed(50) |
|
||||||
} |
|
||||||
|
|
||||||
source = .beachPadel |
|
||||||
if sexUnknown { |
|
||||||
if sex == 1 && FileImportManager.shared.foundInWomenData(license: federalData[3]) { |
|
||||||
self.sex = .female |
|
||||||
} else if FileImportManager.shared.foundInMenData(license: federalData[3]) { |
|
||||||
self.sex = .male |
|
||||||
} else { |
|
||||||
self.sex = nil |
|
||||||
} |
|
||||||
} else { |
|
||||||
self.sex = PlayerSexType(rawValue: sex) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
extension PlayerRegistration { |
|
||||||
|
|
||||||
func hasHomonym() -> Bool { |
|
||||||
let federalContext = PersistenceController.shared.localContainer.viewContext |
|
||||||
let fetchRequest = ImportedPlayer.fetchRequest() |
|
||||||
let predicate = NSPredicate(format: "firstName == %@ && lastName == %@", firstName, lastName) |
|
||||||
fetchRequest.predicate = predicate |
|
||||||
|
|
||||||
do { |
|
||||||
let count = try federalContext.count(for: fetchRequest) |
|
||||||
return count > 1 |
|
||||||
} catch { |
|
||||||
|
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
func updateRank(from sources: [CSVParser], lastRank: Int?) async throws { |
|
||||||
#if DEBUG_TIME |
|
||||||
let start = Date() |
|
||||||
defer { |
|
||||||
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
|
||||||
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
|
||||||
} |
|
||||||
#endif |
|
||||||
|
|
||||||
if let dataFound = try await history(from: sources) { |
|
||||||
rank = dataFound.rankValue?.toInt() |
|
||||||
points = dataFound.points |
|
||||||
tournamentPlayed = dataFound.tournamentCountValue?.toInt() |
|
||||||
} else if let dataFound = try await historyFromName(from: sources) { |
|
||||||
rank = dataFound.rankValue?.toInt() |
|
||||||
points = dataFound.points |
|
||||||
tournamentPlayed = dataFound.tournamentCountValue?.toInt() |
|
||||||
} else { |
|
||||||
rank = lastRank |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func history(from sources: [CSVParser]) async throws -> Line? { |
|
||||||
#if DEBUG_TIME |
|
||||||
let start = Date() |
|
||||||
defer { |
|
||||||
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
|
||||||
print("func history()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
|
||||||
} |
|
||||||
#endif |
|
||||||
|
|
||||||
guard let license = licenceId?.strippedLicense else { |
|
||||||
return nil // Do NOT call historyFromName here, let updateRank handle it |
|
||||||
} |
|
||||||
|
|
||||||
let filteredSources = sources.filter { $0.maleData == isMalePlayer() } |
|
||||||
|
|
||||||
return await withTaskGroup(of: Line?.self) { group in |
|
||||||
for source in filteredSources { |
|
||||||
group.addTask { |
|
||||||
guard !Task.isCancelled else { return nil } |
|
||||||
return try? await source.first { $0.rawValue.contains(";\(license);") } |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for await result in group { |
|
||||||
if let result { |
|
||||||
group.cancelAll() // Stop other tasks as soon as we find a match |
|
||||||
return result |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func historyFromName(from sources: [CSVParser]) async throws -> Line? { |
|
||||||
#if DEBUG |
|
||||||
let start = Date() |
|
||||||
defer { |
|
||||||
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
|
||||||
print("func historyFromName()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
|
||||||
} |
|
||||||
#endif |
|
||||||
|
|
||||||
let filteredSources = sources.filter { $0.maleData == isMalePlayer() } |
|
||||||
let normalizedLastName = lastName.canonicalVersionWithPunctuation |
|
||||||
let normalizedFirstName = firstName.canonicalVersionWithPunctuation |
|
||||||
|
|
||||||
return await withTaskGroup(of: Line?.self) { group in |
|
||||||
for source in filteredSources { |
|
||||||
group.addTask { |
|
||||||
guard !Task.isCancelled else { print("Cancelled"); return nil } |
|
||||||
return try? await source.first { |
|
||||||
let lineValue = $0.rawValue.canonicalVersionWithPunctuation |
|
||||||
return lineValue.contains(";\(normalizedLastName);\(normalizedFirstName);") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for await result in group { |
|
||||||
if let result { |
|
||||||
group.cancelAll() // Stop other tasks as soon as we find a match |
|
||||||
return result |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
extension PlayerRegistration: PlayerHolder { |
|
||||||
|
|
||||||
func getAssimilatedAsMaleRank() -> Int? { |
|
||||||
nil |
|
||||||
} |
|
||||||
|
|
||||||
func getFirstName() -> String { |
|
||||||
firstName |
|
||||||
} |
|
||||||
|
|
||||||
func getLastName() -> String { |
|
||||||
lastName |
|
||||||
} |
|
||||||
|
|
||||||
func getPoints() -> Double? { |
|
||||||
self.points |
|
||||||
} |
|
||||||
|
|
||||||
func getRank() -> Int? { |
|
||||||
rank |
|
||||||
} |
|
||||||
|
|
||||||
func isUnranked() -> Bool { |
|
||||||
rank == nil |
|
||||||
} |
|
||||||
|
|
||||||
func formattedRank() -> String { |
|
||||||
self.rankLabel() |
|
||||||
} |
|
||||||
|
|
||||||
func formattedLicense() -> String { |
|
||||||
if let licenceId { return licenceId.computedLicense } |
|
||||||
return "aucune licence" |
|
||||||
} |
|
||||||
|
|
||||||
var male: Bool { |
|
||||||
isMalePlayer() |
|
||||||
} |
|
||||||
|
|
||||||
func getBirthYear() -> Int? { |
|
||||||
nil |
|
||||||
} |
|
||||||
|
|
||||||
func getProgression() -> Int { |
|
||||||
0 |
|
||||||
} |
|
||||||
|
|
||||||
func getComputedRank() -> Int? { |
|
||||||
computedRank |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,33 +0,0 @@ |
|||||||
// |
|
||||||
// Round+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 30/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import PadelClubData |
|
||||||
|
|
||||||
extension Round { |
|
||||||
|
|
||||||
func loserBracketTurns() -> [LoserRound] { |
|
||||||
#if _DEBUG_TIME //DEBUGING TIME |
|
||||||
let start = Date() |
|
||||||
defer { |
|
||||||
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
|
||||||
print("func loserBracketTurns()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
|
||||||
} |
|
||||||
#endif |
|
||||||
var rounds = [LoserRound]() |
|
||||||
let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) |
|
||||||
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) |
|
||||||
|
|
||||||
for index in 0..<roundCount { |
|
||||||
let lr = LoserRound(roundIndex: roundCount - index - 1, turnIndex: index, upperBracketRound: self) |
|
||||||
rounds.append(lr) |
|
||||||
} |
|
||||||
|
|
||||||
return rounds |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,103 @@ |
|||||||
|
// |
||||||
|
// Sequence+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 03/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
extension Collection { |
||||||
|
/// Returns the element at the specified index if it is within bounds, otherwise nil. |
||||||
|
subscript (safe index: Index) -> Element? { |
||||||
|
return indices.contains(index) ? self[index] : nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Sequence { |
||||||
|
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { |
||||||
|
return sorted { a, b in |
||||||
|
return a[keyPath: keyPath] < b[keyPath: keyPath] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Sequence { |
||||||
|
func pairs() -> AnySequence<(Element, Element)> { |
||||||
|
AnySequence(zip(self, self.dropFirst())) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension Sequence { |
||||||
|
func concurrentForEach( |
||||||
|
_ operation: @escaping (Element) async throws -> Void |
||||||
|
) async throws { |
||||||
|
try await withThrowingTaskGroup(of: Void.self) { group in |
||||||
|
// First, create all tasks |
||||||
|
for element in self { |
||||||
|
group.addTask { |
||||||
|
try await operation(element) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Then wait for all tasks to complete |
||||||
|
for try await _ in group {} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func concurrentForEach( |
||||||
|
_ operation: @escaping (Element) async -> Void |
||||||
|
) async { |
||||||
|
await withTaskGroup(of: Void.self) { group in |
||||||
|
// First, add all tasks |
||||||
|
for element in self { |
||||||
|
group.addTask { |
||||||
|
await operation(element) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Then wait for all tasks to complete |
||||||
|
for await _ in group {} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
enum SortOrder { |
||||||
|
case ascending |
||||||
|
case descending |
||||||
|
} |
||||||
|
|
||||||
|
extension Sequence { |
||||||
|
func sorted(using descriptors: [MySortDescriptor<Element>], |
||||||
|
order: SortOrder) -> [Element] { |
||||||
|
sorted { valueA, valueB in |
||||||
|
for descriptor in descriptors { |
||||||
|
let result = descriptor.comparator(valueA, valueB) |
||||||
|
|
||||||
|
switch result { |
||||||
|
case .orderedSame: |
||||||
|
// Keep iterating if the two elements are equal, |
||||||
|
// since that'll let the next descriptor determine |
||||||
|
// the sort order: |
||||||
|
break |
||||||
|
case .orderedAscending: |
||||||
|
return order == .ascending |
||||||
|
case .orderedDescending: |
||||||
|
return order == .descending |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If no descriptor was able to determine the sort |
||||||
|
// order, we'll default to false (similar to when |
||||||
|
// using the '<' operator with the built-in API): |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
extension Sequence { |
||||||
|
func sorted(using descriptors: MySortDescriptor<Element>...) -> [Element] { |
||||||
|
sorted(using: descriptors, order: .ascending) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
@ -1,34 +0,0 @@ |
|||||||
// |
|
||||||
// SourceFileManager+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 15/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import LeStorage |
|
||||||
import PadelClubData |
|
||||||
|
|
||||||
extension SourceFileManager { |
|
||||||
|
|
||||||
func exportToCSV(_ prefix: String = "", players: [FederalPlayer], sourceFileType: SourceFile, date: Date) { |
|
||||||
let lastDateString = URL.importDateFormatter.string(from: date) |
|
||||||
let dateString = [prefix, "CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].filter({ $0.isEmpty == false }).joined(separator: "-") + "." + "csv" |
|
||||||
|
|
||||||
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)! |
|
||||||
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") |
|
||||||
var csvText : String = "" |
|
||||||
for player in players { |
|
||||||
csvText.append(player.exportToCSV() + "\n") |
|
||||||
} |
|
||||||
|
|
||||||
do { |
|
||||||
try csvText.write(to: destinationFileUrl, atomically: true, encoding: .utf8) |
|
||||||
print("CSV file exported successfully.") |
|
||||||
} catch { |
|
||||||
print("Error writing CSV file:", error) |
|
||||||
Logger.error(error) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,42 +0,0 @@ |
|||||||
// |
|
||||||
// SpinDrawable+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 15/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import PadelClubData |
|
||||||
|
|
||||||
extension String: SpinDrawable { |
|
||||||
public func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { |
|
||||||
[self] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
extension Match: SpinDrawable { |
|
||||||
public func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { |
|
||||||
let teams = teams() |
|
||||||
if teams.count == 1, hideNames == false { |
|
||||||
return teams.first!.segmentLabel(displayStyle, hideNames: hideNames) |
|
||||||
} else { |
|
||||||
return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 } |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
extension TeamRegistration: SpinDrawable { |
|
||||||
public func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { |
|
||||||
var strings: [String] = [] |
|
||||||
let indexLabel = tournamentObject()?.labelIndexOf(team: self) |
|
||||||
if let indexLabel { |
|
||||||
strings.append(indexLabel) |
|
||||||
if hideNames { |
|
||||||
return strings |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) }) |
|
||||||
return strings |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,47 @@ |
|||||||
|
// |
||||||
|
// String+Crypto.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 30/04/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import CryptoKit |
||||||
|
|
||||||
|
enum CryptoError: Error { |
||||||
|
case invalidUTF8 |
||||||
|
case cantConvertUTF8 |
||||||
|
case invalidBase64String |
||||||
|
case nilSeal |
||||||
|
} |
||||||
|
|
||||||
|
extension Data { |
||||||
|
|
||||||
|
func encrypt(pass: String) throws -> Data { |
||||||
|
let key = try self._createSymmetricKey(fromString: pass) |
||||||
|
let sealedBox = try AES.GCM.seal(self, using: key) |
||||||
|
if let combined = sealedBox.combined { |
||||||
|
return combined |
||||||
|
} |
||||||
|
throw CryptoError.nilSeal |
||||||
|
} |
||||||
|
|
||||||
|
func decryptData(pass: String) throws -> String { |
||||||
|
let key = try self._createSymmetricKey(fromString: pass) |
||||||
|
let sealedBox = try AES.GCM.SealedBox(combined: self) |
||||||
|
let decryptedData = try AES.GCM.open(sealedBox, using: key) |
||||||
|
guard let decryptedMessage = String(data: decryptedData, encoding: .utf8) else { |
||||||
|
throw CryptoError.invalidUTF8 |
||||||
|
} |
||||||
|
return decryptedMessage |
||||||
|
} |
||||||
|
|
||||||
|
fileprivate func _createSymmetricKey(fromString keyString: String) throws -> SymmetricKey { |
||||||
|
guard let keyData = Data(base64Encoded: keyString) else { |
||||||
|
throw CryptoError.invalidBase64String |
||||||
|
} |
||||||
|
return SymmetricKey(data: keyData) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,313 @@ |
|||||||
|
// |
||||||
|
// String+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 01/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
// MARK: - Trimming and stuff |
||||||
|
extension String { |
||||||
|
func trunc(length: Int, trailing: String = "…") -> String { |
||||||
|
if length <= 0 { return self } |
||||||
|
return (self.count > length) ? self.prefix(length) + trailing : self |
||||||
|
} |
||||||
|
|
||||||
|
func prefixTrimmed(_ length: Int) -> String { |
||||||
|
String(trimmed.prefix(length)) |
||||||
|
} |
||||||
|
|
||||||
|
func prefixMultilineTrimmed(_ length: Int) -> String { |
||||||
|
String(trimmedMultiline.prefix(length)) |
||||||
|
} |
||||||
|
|
||||||
|
var trimmed: String { |
||||||
|
replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines) |
||||||
|
} |
||||||
|
|
||||||
|
var trimmedMultiline: String { |
||||||
|
self.trimmingCharacters(in: .whitespacesAndNewlines) |
||||||
|
} |
||||||
|
|
||||||
|
func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String { |
||||||
|
components(separatedBy: characterSet).joined(separator:replacementString) |
||||||
|
} |
||||||
|
|
||||||
|
var canonicalVersion: String { |
||||||
|
trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased() |
||||||
|
} |
||||||
|
|
||||||
|
var canonicalVersionWithPunctuation: String { |
||||||
|
trimmed.folding(options: .diacriticInsensitive, locale: .current).lowercased() |
||||||
|
} |
||||||
|
|
||||||
|
var removingFirstCharacter: String { |
||||||
|
String(dropFirst()) |
||||||
|
} |
||||||
|
|
||||||
|
func isValidEmail() -> Bool { |
||||||
|
let emailRegEx = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}$" |
||||||
|
let emailPredicate = NSPredicate(format:"SELF MATCHES %@", emailRegEx) |
||||||
|
return emailPredicate.evaluate(with: self) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - Club Name |
||||||
|
extension String { |
||||||
|
func acronym() -> String { |
||||||
|
let acronym = canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) |
||||||
|
if acronym.count > 10 { |
||||||
|
return concatenateFirstLetters().uppercased() |
||||||
|
} else { |
||||||
|
return acronym.uppercased() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func concatenateFirstLetters() -> String { |
||||||
|
// Split the input into sentences |
||||||
|
let sentences = self.components(separatedBy: .whitespacesAndNewlines) |
||||||
|
if sentences.count == 1 { |
||||||
|
return String(self.prefix(10)) |
||||||
|
} |
||||||
|
// Extract the first character of each sentence |
||||||
|
let firstLetters = sentences.compactMap { sentence -> Character? in |
||||||
|
let trimmedSentence = sentence.trimmingCharacters(in: .whitespacesAndNewlines) |
||||||
|
if trimmedSentence.count > 2 { |
||||||
|
if let firstCharacter = trimmedSentence.first { |
||||||
|
return firstCharacter |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Join the first letters together into a string |
||||||
|
let result = String(firstLetters) |
||||||
|
return String(result.prefix(10)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - FFT License |
||||||
|
extension String { |
||||||
|
var computedLicense: String { |
||||||
|
if let licenseKey { |
||||||
|
return self + licenseKey |
||||||
|
} else { |
||||||
|
return self |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var strippedLicense: String? { |
||||||
|
var dropFirst = 0 |
||||||
|
if hasPrefix("0") { |
||||||
|
dropFirst = 1 |
||||||
|
} |
||||||
|
if let match = self.dropFirst(dropFirst).firstMatch(of: /[0-9]{6,8}/) { |
||||||
|
let lic = String(self.dropFirst(dropFirst)[match.range.lowerBound..<match.range.upperBound]) |
||||||
|
return lic |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var isLicenseNumber: Bool { |
||||||
|
if let match = self.firstMatch(of: /[0-9]{6,8}[A-Z]/) { |
||||||
|
let lic = String(self[match.range.lowerBound..<match.range.upperBound].dropLast(1)) |
||||||
|
let lastLetter = String(self[match.range.lowerBound..<match.range.upperBound].suffix(1)) |
||||||
|
|
||||||
|
if let lkey = lic.licenseKey { |
||||||
|
return lkey == lastLetter |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
var licenseKey: String? { |
||||||
|
if let intValue = Int(self) { |
||||||
|
var value = intValue |
||||||
|
value -= 1 |
||||||
|
value = value % 23 |
||||||
|
let v = UnicodeScalar("A").value |
||||||
|
let i = Int(v) |
||||||
|
if let s = UnicodeScalar(i + value) { |
||||||
|
var c = Character(s) |
||||||
|
if c >= "I" { |
||||||
|
value += 1 |
||||||
|
if let newS = UnicodeScalar(i + value) { |
||||||
|
c = Character(newS) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if c >= "O" { |
||||||
|
value += 1 |
||||||
|
if let newS = UnicodeScalar(i + value) { |
||||||
|
c = Character(newS) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
if c >= "Q" { |
||||||
|
value += 1 |
||||||
|
if let newS = UnicodeScalar(i + value) { |
||||||
|
c = Character(newS) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return String(c) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func licencesFound() -> [String] { |
||||||
|
// 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 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - FFT Source Importing |
||||||
|
extension String { |
||||||
|
enum RegexStatic { |
||||||
|
static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/ |
||||||
|
static let phoneNumber = /^(?:\+33|0033|0)[1-9](?:[ .-]?[0-9]{2}){4}$/ |
||||||
|
} |
||||||
|
|
||||||
|
func isMobileNumber() -> Bool { |
||||||
|
firstMatch(of: RegexStatic.mobileNumber) != nil |
||||||
|
} |
||||||
|
|
||||||
|
func isPhoneNumber() -> Bool { |
||||||
|
firstMatch(of: RegexStatic.phoneNumber) != nil |
||||||
|
} |
||||||
|
|
||||||
|
func cleanSearchText() -> String { |
||||||
|
// Create a character set of all punctuation except slashes and hyphens |
||||||
|
var punctuationToRemove = CharacterSet.punctuationCharacters |
||||||
|
punctuationToRemove.remove(charactersIn: "/-") |
||||||
|
|
||||||
|
// Remove the unwanted punctuation |
||||||
|
return self.components(separatedBy: punctuationToRemove) |
||||||
|
.joined(separator: " ") |
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines) |
||||||
|
} |
||||||
|
|
||||||
|
//april 04-2024 bug with accent characters / adobe / fft |
||||||
|
mutating func replace(characters: [(Character, Character)]) { |
||||||
|
for (targetChar, replacementChar) in characters { |
||||||
|
self = String(self.map { $0 == targetChar ? replacementChar : $0 }) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - Player Names |
||||||
|
extension StringProtocol { |
||||||
|
var firstUppercased: String { prefix(1).uppercased() + dropFirst() } |
||||||
|
var firstCapitalized: String { prefix(1).capitalized + dropFirst() } |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: - todo clean up ?? |
||||||
|
extension LosslessStringConvertible { |
||||||
|
var string: String { .init(self) } |
||||||
|
} |
||||||
|
|
||||||
|
extension String { |
||||||
|
func createFile(_ withName: String = "temp", _ exportedFormat: ExportFormat = .rawText) -> URL { |
||||||
|
let url = FileManager.default.temporaryDirectory |
||||||
|
.appendingPathComponent(withName) |
||||||
|
.appendingPathExtension(exportedFormat.suffix) |
||||||
|
let string = self |
||||||
|
try? FileManager.default.removeItem(at: url) |
||||||
|
try? string.write(to: url, atomically: true, encoding: .utf8) |
||||||
|
return url |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension String { |
||||||
|
func toInt() -> Int? { |
||||||
|
Int(self) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension String : @retroactive Identifiable { |
||||||
|
public var id: String { self } |
||||||
|
} |
||||||
|
|
||||||
|
extension String { |
||||||
|
/// Parses the birthdate string into a `Date` based on multiple formats. |
||||||
|
/// - Returns: A `Date` object if parsing is successful, or `nil` if the format is unrecognized. |
||||||
|
func parseAsBirthdate() -> Date? { |
||||||
|
let dateFormats = [ |
||||||
|
"yyyy-MM-dd", // Format for "1993-01-31" |
||||||
|
"dd/MM/yyyy", // Format for "27/07/1992" |
||||||
|
"dd/MM/yy" // Format for "27/07/92" |
||||||
|
] |
||||||
|
|
||||||
|
let dateFormatter = DateFormatter() |
||||||
|
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Ensure consistent parsing |
||||||
|
|
||||||
|
for format in dateFormats { |
||||||
|
dateFormatter.dateFormat = format |
||||||
|
|
||||||
|
if let date = dateFormatter.date(from: self) { |
||||||
|
return date // Return the parsed date if successful |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil // Return nil if no format matches |
||||||
|
} |
||||||
|
|
||||||
|
/// Formats the birthdate string into "DD/MM/YYYY". |
||||||
|
/// - Returns: A formatted birthdate string, or the original string if parsing fails. |
||||||
|
func formattedAsBirthdate() -> String { |
||||||
|
if let parsedDate = self.parseAsBirthdate() { |
||||||
|
let outputFormatter = DateFormatter() |
||||||
|
outputFormatter.dateFormat = "dd/MM/yyyy" // Desired output format |
||||||
|
return outputFormatter.string(from: parsedDate) |
||||||
|
} |
||||||
|
return self // Return the original string if parsing fails |
||||||
|
} |
||||||
|
} |
||||||
@ -1,84 +0,0 @@ |
|||||||
// |
|
||||||
// TeamRegistration+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 15/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import SwiftUI |
|
||||||
import PadelClubData |
|
||||||
|
|
||||||
extension TeamRegistration { |
|
||||||
|
|
||||||
func initialRoundColor() -> Color? { |
|
||||||
if walkOut { return Color.logoRed } |
|
||||||
if groupStagePosition != nil || wildCardGroupStage { return Color.blue } |
|
||||||
if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] { |
|
||||||
return Color(uiColor: .init(fromHex: colorHex)) |
|
||||||
} else if wildCardBracket { |
|
||||||
return Color.mint |
|
||||||
} else { |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) { |
|
||||||
self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory) |
|
||||||
} |
|
||||||
|
|
||||||
func updatePlayers( |
|
||||||
_ players: Set<PlayerRegistration>, |
|
||||||
inTournamentCategory tournamentCategory: TournamentCategory |
|
||||||
) { |
|
||||||
let previousPlayers = Set(unsortedPlayers()) |
|
||||||
|
|
||||||
players.forEach { player in |
|
||||||
previousPlayers.forEach { oldPlayer in |
|
||||||
if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense, |
|
||||||
player.licenceId?.strippedLicense != nil |
|
||||||
{ |
|
||||||
player.registeredOnline = oldPlayer.registeredOnline |
|
||||||
if player.email?.canonicalVersion != oldPlayer.email?.canonicalVersion { |
|
||||||
player.contactEmail = oldPlayer.email |
|
||||||
} else { |
|
||||||
player.contactEmail = oldPlayer.contactEmail |
|
||||||
} |
|
||||||
if areFrenchPhoneNumbersSimilar(player.phoneNumber, oldPlayer.phoneNumber) == false { |
|
||||||
player.contactPhoneNumber = oldPlayer.phoneNumber |
|
||||||
} else { |
|
||||||
player.contactPhoneNumber = oldPlayer.contactPhoneNumber |
|
||||||
} |
|
||||||
player.contactName = oldPlayer.contactName |
|
||||||
player.coach = oldPlayer.coach |
|
||||||
player.tournamentPlayed = oldPlayer.tournamentPlayed |
|
||||||
player.points = oldPlayer.points |
|
||||||
player.captain = oldPlayer.captain |
|
||||||
player.assimilation = oldPlayer.assimilation |
|
||||||
player.ligueName = oldPlayer.ligueName |
|
||||||
player.registrationStatus = oldPlayer.registrationStatus |
|
||||||
player.timeToConfirm = oldPlayer.timeToConfirm |
|
||||||
player.sex = oldPlayer.sex |
|
||||||
player.paymentType = oldPlayer.paymentType |
|
||||||
player.paymentId = oldPlayer.paymentId |
|
||||||
player.clubMember = oldPlayer.clubMember |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
let playersToRemove = previousPlayers.subtracting(players) |
|
||||||
self.tournamentStore?.playerRegistrations.delete(contentOfs: Array(playersToRemove)) |
|
||||||
setWeight(from: Array(players), inTournamentCategory: tournamentCategory) |
|
||||||
|
|
||||||
players.forEach { player in |
|
||||||
player.teamRegistration = id |
|
||||||
} |
|
||||||
|
|
||||||
// do { |
|
||||||
// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) |
|
||||||
// } catch { |
|
||||||
// Logger.error(error) |
|
||||||
// } |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,428 +0,0 @@ |
|||||||
// |
|
||||||
// Tournament+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Laurent Morvillier on 15/04/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import Foundation |
|
||||||
import SwiftUI |
|
||||||
import PadelClubData |
|
||||||
import LeStorage |
|
||||||
|
|
||||||
extension Tournament { |
|
||||||
|
|
||||||
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { |
|
||||||
let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date(), name: name) |
|
||||||
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) |
|
||||||
players.forEach { player in |
|
||||||
player.teamRegistration = team.id |
|
||||||
} |
|
||||||
if isAnimation() { |
|
||||||
if team.weight == 0 { |
|
||||||
team.weight = unsortedTeams().count |
|
||||||
} |
|
||||||
} |
|
||||||
return team |
|
||||||
} |
|
||||||
|
|
||||||
func addWildCardIfNeeded(_ count: Int, _ type: MatchType) { |
|
||||||
let currentCount = selectedSortedTeams().filter({ |
|
||||||
if type == .bracket { |
|
||||||
return $0.wildCardBracket |
|
||||||
} else { |
|
||||||
return $0.wildCardGroupStage |
|
||||||
} |
|
||||||
}).count |
|
||||||
|
|
||||||
if currentCount < count { |
|
||||||
let _diff = count - currentCount |
|
||||||
addWildCard(_diff, type) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func addEmptyTeamRegistration(_ count: Int) { |
|
||||||
|
|
||||||
guard let tournamentStore = self.tournamentStore else { return } |
|
||||||
|
|
||||||
let teams = (0..<count).map { _ in |
|
||||||
let team = TeamRegistration(tournament: id, registrationDate: Date()) |
|
||||||
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory) |
|
||||||
return team |
|
||||||
} |
|
||||||
|
|
||||||
do { |
|
||||||
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) |
|
||||||
} catch { |
|
||||||
Logger.error(error) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func addWildCard(_ count: Int, _ type: MatchType) { |
|
||||||
let wcs = (0..<count).map { _ in |
|
||||||
let team = TeamRegistration(tournament: id, registrationDate: Date()) |
|
||||||
if type == .bracket { |
|
||||||
team.wildCardBracket = true |
|
||||||
} else { |
|
||||||
team.wildCardGroupStage = true |
|
||||||
} |
|
||||||
|
|
||||||
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory) |
|
||||||
team.weight += 200_000 |
|
||||||
return team |
|
||||||
} |
|
||||||
|
|
||||||
do { |
|
||||||
try self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: wcs) |
|
||||||
} catch { |
|
||||||
Logger.error(error) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func teamsRanked() -> [TeamRegistration] { |
|
||||||
let selected = selectedSortedTeams().filter({ $0.finalRanking != nil }) |
|
||||||
return selected.sorted(by: \.finalRanking!, order: .ascending) |
|
||||||
} |
|
||||||
|
|
||||||
func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] { |
|
||||||
let licenseYearValidity = self.licenseYearValidity() |
|
||||||
return players.filter({ player in |
|
||||||
if player.isImported() { |
|
||||||
// Player is marked as imported: check if the license is valid |
|
||||||
return !player.isValidLicenseNumber(year: licenseYearValidity) |
|
||||||
} else { |
|
||||||
// Player is not imported: validate license and handle `isImported` flag for non-imported players |
|
||||||
let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true |
|
||||||
let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false |
|
||||||
|
|
||||||
// If global `isImported` is true, check license number as well |
|
||||||
let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity) |
|
||||||
|
|
||||||
return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func homonyms(in players: [PlayerRegistration]) -> [PlayerRegistration] { |
|
||||||
players.filter({ $0.hasHomonym() }) |
|
||||||
} |
|
||||||
|
|
||||||
func payIfNecessary() async throws { |
|
||||||
if self.payment != nil { return } |
|
||||||
if let payment = await Guard.main.paymentForNewTournament() { |
|
||||||
self.payment = payment |
|
||||||
DataStore.shared.tournaments.addOrUpdate(instance: self) |
|
||||||
return |
|
||||||
} |
|
||||||
throw PaymentError.cantPayTournament |
|
||||||
} |
|
||||||
|
|
||||||
func cutLabelColor(index: Int?, teamCount: Int?) -> Color { |
|
||||||
guard let index else { return Color.grayNotUniversal } |
|
||||||
let _teamCount = teamCount ?? selectedSortedTeams().count |
|
||||||
let groupStageCut = groupStageCut() |
|
||||||
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut) |
|
||||||
if index < bracketCut { |
|
||||||
return Color.mint |
|
||||||
} else if index - bracketCut < groupStageCut && _teamCount > 0 { |
|
||||||
return Color.indigo |
|
||||||
} else { |
|
||||||
return Color.grayNotUniversal |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func isPlayerAgeInadequate(player: PlayerHolder) -> Bool { |
|
||||||
guard let computedAge = player.computedAge else { return false } |
|
||||||
if federalTournamentAge.isAgeValid(age: computedAge) == false { |
|
||||||
return true |
|
||||||
} else { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func isPlayerRankInadequate(player: PlayerHolder) -> Bool { |
|
||||||
guard let rank = player.getRank() else { return false } |
|
||||||
let _rank = player.male ? rank : rank + addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0) |
|
||||||
if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge, seasonYear: startDate.seasonYear()) { |
|
||||||
return true |
|
||||||
} else { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { |
|
||||||
if startDate.isInCurrentYear() == false { |
|
||||||
return [] |
|
||||||
} |
|
||||||
return players.filter { player in |
|
||||||
return isPlayerRankInadequate(player: player) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] { |
|
||||||
if startDate.isInCurrentYear() == false { |
|
||||||
return [] |
|
||||||
} |
|
||||||
return players.filter { player in |
|
||||||
return isPlayerAgeInadequate(player: player) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func importTeams(_ teams: [FileImportManager.TeamHolder]) { |
|
||||||
var teamsToImport = [TeamRegistration]() |
|
||||||
let players = players().filter { $0.licenceId != nil } |
|
||||||
teams.forEach { team in |
|
||||||
if let previousTeam = team.previousTeam { |
|
||||||
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory) |
|
||||||
teamsToImport.append(previousTeam) |
|
||||||
} else { |
|
||||||
var registrationDate = team.registrationDate |
|
||||||
if let previousPlayer = players.first(where: { player in |
|
||||||
let ids = team.players.compactMap({ $0.licenceId }) |
|
||||||
return ids.contains(player.licenceId!) |
|
||||||
}), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate { |
|
||||||
registrationDate = previousTeamRegistrationDate |
|
||||||
} |
|
||||||
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name) |
|
||||||
if isAnimation() { |
|
||||||
if newTeam.weight == 0 { |
|
||||||
newTeam.weight = team.index(in: teams) ?? 0 |
|
||||||
} |
|
||||||
} |
|
||||||
teamsToImport.append(newTeam) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if let tournamentStore = self.tournamentStore { |
|
||||||
tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport) |
|
||||||
let playersToImport = teams.flatMap { $0.players } |
|
||||||
tournamentStore.playerRegistrations.addOrUpdate(contentOfs: playersToImport) |
|
||||||
} |
|
||||||
|
|
||||||
if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty { |
|
||||||
setGroupStage(randomize: groupStageSortMode == .random) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func registrationIssues(selectedTeams: [TeamRegistration]) async -> Int { |
|
||||||
let players : [PlayerRegistration] = selectedTeams.flatMap { $0.players() } |
|
||||||
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } |
|
||||||
let duplicates : [PlayerRegistration] = duplicates(in: players) |
|
||||||
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) |
|
||||||
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players) |
|
||||||
let homonyms = homonyms(in: players) |
|
||||||
let ageInadequatePlayers = ageInadequatePlayers(in: players) |
|
||||||
let isImported = players.anySatisfy({ $0.isImported() }) |
|
||||||
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported) |
|
||||||
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) |
|
||||||
let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams, includingWalkOuts: true) |
|
||||||
let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) |
|
||||||
let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) |
|
||||||
|
|
||||||
return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count |
|
||||||
} |
|
||||||
|
|
||||||
func updateRank(to newDate: Date?, forceRefreshLockWeight: Bool, providedSources: [CSVParser]?) async throws { |
|
||||||
refreshRanking = true |
|
||||||
#if DEBUG_TIME |
|
||||||
let start = Date() |
|
||||||
defer { |
|
||||||
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) |
|
||||||
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) |
|
||||||
} |
|
||||||
#endif |
|
||||||
|
|
||||||
guard let newDate else { return } |
|
||||||
rankSourceDate = newDate |
|
||||||
|
|
||||||
// Fetch current month data only once |
|
||||||
var monthData = currentMonthData() |
|
||||||
|
|
||||||
if monthData == nil { |
|
||||||
async let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) |
|
||||||
async let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) |
|
||||||
|
|
||||||
let formatted = URL.importDateFormatter.string(from: newDate) |
|
||||||
let newMonthData = MonthData(monthKey: formatted) |
|
||||||
|
|
||||||
newMonthData.maleUnrankedValue = await lastRankMan |
|
||||||
newMonthData.femaleUnrankedValue = await lastRankWoman |
|
||||||
DataStore.shared.monthData.addOrUpdate(instance: newMonthData) |
|
||||||
monthData = newMonthData |
|
||||||
} |
|
||||||
|
|
||||||
let lastRankMan = monthData?.maleUnrankedValue |
|
||||||
let lastRankWoman = monthData?.femaleUnrankedValue |
|
||||||
|
|
||||||
var chunkedParsers: [CSVParser] = [] |
|
||||||
if let providedSources { |
|
||||||
chunkedParsers = providedSources |
|
||||||
} else { |
|
||||||
// Fetch only the required files |
|
||||||
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } |
|
||||||
guard !dataURLs.isEmpty else { return } // Early return if no files found |
|
||||||
|
|
||||||
let sources = dataURLs.map { CSVParser(url: $0) } |
|
||||||
chunkedParsers = try await chunkAllSources(sources: sources, size: 10000) |
|
||||||
} |
|
||||||
|
|
||||||
let players = unsortedPlayers() |
|
||||||
for player in players { |
|
||||||
let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan |
|
||||||
try await player.updateRank(from: chunkedParsers, lastRank: lastRank) |
|
||||||
player.setComputedRank(in: self) |
|
||||||
} |
|
||||||
|
|
||||||
if providedSources == nil { |
|
||||||
try chunkedParsers.forEach { chunk in |
|
||||||
try FileManager.default.removeItem(at: chunk.url) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) |
|
||||||
|
|
||||||
let unsortedTeams = unsortedTeams() |
|
||||||
unsortedTeams.forEach { team in |
|
||||||
team.setWeight(from: team.players(), inTournamentCategory: tournamentCategory) |
|
||||||
if forceRefreshLockWeight { |
|
||||||
team.lockedWeight = team.weight |
|
||||||
} |
|
||||||
} |
|
||||||
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams) |
|
||||||
refreshRanking = false |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
extension Tournament { |
|
||||||
static func newEmptyInstance() -> Tournament { |
|
||||||
let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource |
|
||||||
var _mostRecentDateAvailable: Date? { |
|
||||||
guard let lastDataSource else { return nil } |
|
||||||
return URL.importDateFormatter.date(from: lastDataSource) |
|
||||||
} |
|
||||||
|
|
||||||
let rankSourceDate = _mostRecentDateAvailable |
|
||||||
return Tournament(rankSourceDate: rankSourceDate, currencyCode: Locale.defaultCurrency()) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
extension Tournament: FederalTournamentHolder { |
|
||||||
|
|
||||||
func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { |
|
||||||
if isAnimation() { |
|
||||||
if let name { |
|
||||||
return name.trunc(length: DeviceHelper.charLength()) |
|
||||||
} else if build.age == .unlisted, build.category == .unlisted { |
|
||||||
return build.level.localizedLevelLabel(.title) |
|
||||||
} else { |
|
||||||
return build.level.localizedLevelLabel(displayStyle) |
|
||||||
} |
|
||||||
} |
|
||||||
return build.level.localizedLevelLabel(displayStyle) |
|
||||||
} |
|
||||||
|
|
||||||
var codeClub: String? { |
|
||||||
club()?.code |
|
||||||
} |
|
||||||
|
|
||||||
var holderId: String { id } |
|
||||||
|
|
||||||
func clubLabel() -> String { |
|
||||||
locationLabel() |
|
||||||
} |
|
||||||
|
|
||||||
func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String { |
|
||||||
if isAnimation() { |
|
||||||
if displayAgeAndCategory(forBuild: build) == false { |
|
||||||
return [build.category.localizedCategoryLabel(ageCategory: build.age), build.age.localizedFederalAgeLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") |
|
||||||
} else if name != nil { |
|
||||||
return build.level.localizedLevelLabel(.title) |
|
||||||
} else { |
|
||||||
return "" |
|
||||||
} |
|
||||||
} else { |
|
||||||
return subtitle() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var tournaments: [any TournamentBuildHolder] { |
|
||||||
[ |
|
||||||
self |
|
||||||
] |
|
||||||
} |
|
||||||
|
|
||||||
var dayPeriod: DayPeriod { |
|
||||||
let day = startDate.get(.weekday) |
|
||||||
switch day { |
|
||||||
case 2...6: |
|
||||||
return .week |
|
||||||
default: |
|
||||||
return .weekend |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool { |
|
||||||
if isAnimation() { |
|
||||||
if let name, name.count < DeviceHelper.maxCharacter() { |
|
||||||
return true |
|
||||||
} else if build.age == .unlisted, build.category == .unlisted { |
|
||||||
return true |
|
||||||
} else { |
|
||||||
return DeviceHelper.isBigScreen() |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
extension Tournament: TournamentBuildHolder { |
|
||||||
public func buildHolderTitle(_ displayStyle: DisplayStyle) -> String { |
|
||||||
tournamentTitle(.short) |
|
||||||
} |
|
||||||
|
|
||||||
public var category: TournamentCategory { |
|
||||||
tournamentCategory |
|
||||||
} |
|
||||||
|
|
||||||
public var level: TournamentLevel { |
|
||||||
tournamentLevel |
|
||||||
} |
|
||||||
|
|
||||||
public var age: FederalTournamentAge { |
|
||||||
federalTournamentAge |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// MARK: - UI extensions |
|
||||||
|
|
||||||
extension Tournament { |
|
||||||
|
|
||||||
public var shouldShowPaymentInfo: Bool { |
|
||||||
if self.payment != nil { |
|
||||||
return false |
|
||||||
} |
|
||||||
switch self.state() { |
|
||||||
case .initial, .build, .running: |
|
||||||
return true |
|
||||||
default: |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
//extension Tournament { |
|
||||||
// func deadline(for type: TournamentDeadlineType) -> Date? { |
|
||||||
// guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } |
|
||||||
// |
|
||||||
// let daysOffset = type.daysOffset(level: tournamentLevel) |
|
||||||
// if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) { |
|
||||||
// let startOfDay = Calendar.current.startOfDay(for: date) |
|
||||||
// return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) |
|
||||||
// } |
|
||||||
// return nil |
|
||||||
// } |
|
||||||
//} |
|
||||||
@ -0,0 +1,182 @@ |
|||||||
|
// |
||||||
|
// URL+Extensions.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 01/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
extension URL { |
||||||
|
|
||||||
|
static var savedDateFormatter: DateFormatter = { |
||||||
|
let df = DateFormatter() |
||||||
|
df.dateFormat = "DD/MM/yyyy" |
||||||
|
return df |
||||||
|
}() |
||||||
|
|
||||||
|
static var importDateFormatter: DateFormatter = { |
||||||
|
let df = DateFormatter() |
||||||
|
df.dateFormat = "MM-yyyy" |
||||||
|
return df |
||||||
|
}() |
||||||
|
|
||||||
|
var dateFromPath: Date { |
||||||
|
let found = deletingPathExtension().path().components(separatedBy: "-").suffix(2).joined(separator: "-") |
||||||
|
if let date = URL.importDateFormatter.date(from: found) { |
||||||
|
return date |
||||||
|
} else { |
||||||
|
return Date() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var index: Int { |
||||||
|
if let i = path().dropLast(12).last?.wholeNumberValue { |
||||||
|
return i |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
var manData: Bool { |
||||||
|
path().contains("MESSIEURS") |
||||||
|
} |
||||||
|
|
||||||
|
var womanData: Bool { |
||||||
|
path().contains("DAMES") |
||||||
|
} |
||||||
|
|
||||||
|
static var seed: URL? { |
||||||
|
Bundle.main.url(forResource: "SeedData", withExtension: nil) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension URL { |
||||||
|
func creationDate() -> Date? { |
||||||
|
// Use FileManager to retrieve the file attributes |
||||||
|
do { |
||||||
|
let fileAttributes = try FileManager.default.attributesOfItem(atPath: self.path()) |
||||||
|
|
||||||
|
// Access the creationDate from the file attributes |
||||||
|
if let creationDate = fileAttributes[.creationDate] as? Date { |
||||||
|
print("File creationDate: \(creationDate)") |
||||||
|
return creationDate |
||||||
|
} else { |
||||||
|
print("creationDate not found.") |
||||||
|
} |
||||||
|
} catch { |
||||||
|
print("Error retrieving file attributes: \(error.localizedDescription)") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func fftImportingStatus() -> Int? { |
||||||
|
// Read the contents of the file |
||||||
|
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Split the contents by newline characters |
||||||
|
let lines = fileContents.components(separatedBy: .newlines) |
||||||
|
//0 means no need to reimport, just recalc |
||||||
|
//1 or missing means re-import |
||||||
|
if let line = lines.first(where: { |
||||||
|
$0.hasPrefix("import-status:") |
||||||
|
}) { |
||||||
|
return Int(line.replacingOccurrences(of: "import-status:", with: "")) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func fftImportingMaleUnrankValue() -> Int? { |
||||||
|
// Read the contents of the file |
||||||
|
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Split the contents by newline characters |
||||||
|
let lines = fileContents.components(separatedBy: .newlines) |
||||||
|
|
||||||
|
if let line = lines.first(where: { |
||||||
|
$0.hasPrefix("unrank-male-value:") |
||||||
|
}) { |
||||||
|
return Int(line.replacingOccurrences(of: "unrank-male-value:", with: "")) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func fileModelIdentifier() -> String? { |
||||||
|
// Read the contents of the file |
||||||
|
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Split the contents by newline characters |
||||||
|
let lines = fileContents.components(separatedBy: .newlines) |
||||||
|
|
||||||
|
if let line = lines.first(where: { |
||||||
|
$0.hasPrefix("file-model-version:") |
||||||
|
}) { |
||||||
|
return line.replacingOccurrences(of: "file-model-version:", with: "") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func fftImportingUncomplete() -> Int? { |
||||||
|
// Read the contents of the file |
||||||
|
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Split the contents by newline characters |
||||||
|
let lines = fileContents.components(separatedBy: .newlines) |
||||||
|
|
||||||
|
if let line = lines.first(where: { |
||||||
|
$0.hasPrefix("max-players:") |
||||||
|
}) { |
||||||
|
return Int(line.replacingOccurrences(of: "max-players:", with: "")) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func getUnrankedValue() -> Int? { |
||||||
|
// Read the contents of the file |
||||||
|
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Split the contents by newline characters |
||||||
|
let lines = fileContents.components(separatedBy: .newlines) |
||||||
|
|
||||||
|
// Get the last non-empty line |
||||||
|
var lastLine: String? |
||||||
|
for line in lines.reversed() { |
||||||
|
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines) |
||||||
|
if !trimmedLine.isEmpty { |
||||||
|
lastLine = trimmedLine |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
guard let rankString = lastLine?.components(separatedBy: ";").dropFirst().first, let rank = Int(rankString) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
// Define the regular expression pattern |
||||||
|
let pattern = "\\b\(NSRegularExpression.escapedPattern(for: rankString))\\b" |
||||||
|
|
||||||
|
// Create the regular expression object |
||||||
|
guard let regex = try? NSRegularExpression(pattern: pattern) else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get the matches |
||||||
|
let matches = regex.matches(in: fileContents, range: NSRange(fileContents.startIndex..., in: fileContents)) |
||||||
|
|
||||||
|
// Return the count of matches |
||||||
|
return matches.count + rank - 1 |
||||||
|
} |
||||||
|
} |
||||||
@ -1,22 +0,0 @@ |
|||||||
// |
|
||||||
// View+Extensions.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Razmig Sarkissian on 29/09/2025. |
|
||||||
// |
|
||||||
|
|
||||||
import SwiftUI |
|
||||||
|
|
||||||
extension View { |
|
||||||
/// Runs a transform only on iOS 26+, otherwise returns self |
|
||||||
@ViewBuilder |
|
||||||
func ifAvailableiOS26<Content: View>( |
|
||||||
@ViewBuilder transform: (Self) -> Content |
|
||||||
) -> some View { |
|
||||||
if #available(iOS 26.0, *) { |
|
||||||
transform(self) |
|
||||||
} else { |
|
||||||
self |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,14 +1,8 @@ |
|||||||
<li class="game game-top {{entrantOneWon}}" style="visibility:{{hidden}}; position: relative;"> |
<li class="game game-top {{entrantOneWon}}" style="visibility:{{hidden}}"> |
||||||
{{entrantOne}} |
{{entrantOne}} |
||||||
<div class="match-description-overlay" style="visibility:{{hidden}};">{{matchDescriptionTop}}</div> |
|
||||||
</li> |
</li> |
||||||
<li class="game game-spacer" style="visibility:{{hidden}}"> |
<li class="game game-spacer" style="visibility:{{hidden}}"><div class="multiline">{{matchDescription}}</div></li> |
||||||
<div class="center-match-overlay" style="visibility:{{hidden}};">{{centerMatchText}}</div> |
<li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}"> |
||||||
</li> |
{{entrantTwo}} |
||||||
<li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}; position: relative;"> |
|
||||||
<div style="transform: translateY(-100%);"> |
|
||||||
{{entrantTwo}} |
|
||||||
</div> |
|
||||||
<div class="match-description-overlay" style="visibility:{{hidden}};">{{matchDescriptionBottom}}</div> |
|
||||||
</li> |
</li> |
||||||
<li class="spacer"> </li> |
<li class="spacer"> </li> |
||||||
|
|||||||
@ -0,0 +1,254 @@ |
|||||||
|
// |
||||||
|
// ContactManager.swift |
||||||
|
// Padel Tournament |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 19/09/2023. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import SwiftUI |
||||||
|
import MessageUI |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
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" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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?) |
||||||
|
|
||||||
|
var id: Int { |
||||||
|
switch self { |
||||||
|
case .message: return 0 |
||||||
|
case .mail: return 1 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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)" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
struct MessageComposeView: UIViewControllerRepresentable { |
||||||
|
typealias Completion = (_ result: MessageComposeResult) -> Void |
||||||
|
|
||||||
|
static var canSendText: Bool { MFMessageComposeViewController.canSendText() } |
||||||
|
|
||||||
|
let recipients: [String]? |
||||||
|
let body: String? |
||||||
|
let completion: Completion? |
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIViewController { |
||||||
|
guard Self.canSendText else { |
||||||
|
let errorView = ContentUnavailableView("Aucun compte de messagerie", systemImage: "xmark", description: Text("Aucun compte de messagerie n'est configuré sur cet appareil.")) |
||||||
|
return UIHostingController(rootView: errorView) |
||||||
|
} |
||||||
|
|
||||||
|
let controller = MFMessageComposeViewController() |
||||||
|
controller.messageComposeDelegate = context.coordinator |
||||||
|
controller.recipients = recipients |
||||||
|
controller.body = body |
||||||
|
|
||||||
|
return controller |
||||||
|
} |
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} |
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator { |
||||||
|
Coordinator(completion: self.completion) |
||||||
|
} |
||||||
|
|
||||||
|
class Coordinator: NSObject, MFMessageComposeViewControllerDelegate { |
||||||
|
private let completion: Completion? |
||||||
|
|
||||||
|
public init(completion: Completion?) { |
||||||
|
self.completion = completion |
||||||
|
} |
||||||
|
|
||||||
|
public func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { |
||||||
|
controller.dismiss(animated: true, completion: { |
||||||
|
self.completion?(result) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct MailComposeView: UIViewControllerRepresentable { |
||||||
|
typealias Completion = (_ result: MFMailComposeResult) -> Void |
||||||
|
|
||||||
|
static var canSendMail: Bool { |
||||||
|
if let mailURL = URL(string: "mailto:?to=jap@padelclub.com") { |
||||||
|
let mailConfigured = UIApplication.shared.canOpenURL(mailURL) |
||||||
|
return mailConfigured && MFMailComposeViewController.canSendMail() |
||||||
|
} else { |
||||||
|
return MFMailComposeViewController.canSendMail() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let recipients: [String]? |
||||||
|
let bccRecipients: [String]? |
||||||
|
let body: String? |
||||||
|
let subject: String? |
||||||
|
var attachmentURL: URL? |
||||||
|
let completion: Completion? |
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIViewController { |
||||||
|
guard Self.canSendMail else { |
||||||
|
let errorView = ContentUnavailableView("Aucun compte mail", systemImage: "xmark", description: Text("Aucun compte mail n'est configuré sur cet appareil.")) |
||||||
|
return UIHostingController(rootView: errorView) |
||||||
|
} |
||||||
|
|
||||||
|
let controller = MFMailComposeViewController() |
||||||
|
controller.mailComposeDelegate = context.coordinator |
||||||
|
controller.setToRecipients(recipients) |
||||||
|
controller.setBccRecipients(bccRecipients) |
||||||
|
if let attachmentURL { |
||||||
|
do { |
||||||
|
let attachmentData = try Data(contentsOf: attachmentURL) |
||||||
|
controller.addAttachmentData(attachmentData, mimeType: "application/zip", fileName: "backup.zip") |
||||||
|
} catch { |
||||||
|
print("Could not attach file: \(error)") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if let body { |
||||||
|
controller.setMessageBody(body, isHTML: false) |
||||||
|
} |
||||||
|
if let subject { |
||||||
|
controller.setSubject(subject) |
||||||
|
} |
||||||
|
|
||||||
|
return controller |
||||||
|
} |
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} |
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator { |
||||||
|
Coordinator(completion: self.completion) |
||||||
|
} |
||||||
|
|
||||||
|
class Coordinator: NSObject, MFMailComposeViewControllerDelegate { |
||||||
|
private let completion: Completion? |
||||||
|
|
||||||
|
public init(completion: Completion?) { |
||||||
|
self.completion = completion |
||||||
|
} |
||||||
|
|
||||||
|
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { |
||||||
|
controller.dismiss(animated: true, completion: { |
||||||
|
self.completion?(result) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
// |
||||||
|
// Key.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Laurent Morvillier on 30/04/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
enum CryptoKey: String { |
||||||
|
case pass = "Aa9QDV1G5MP9ijF2FTFasibNbS/Zun4qXrubIL2P+Ik=" |
||||||
|
} |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
// |
||||||
|
// DisplayContext.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 20/03/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import UIKit |
||||||
|
|
||||||
|
enum DisplayContext { |
||||||
|
case addition |
||||||
|
case edition |
||||||
|
case lockedForEditing |
||||||
|
case selection |
||||||
|
} |
||||||
|
|
||||||
|
enum DisplayStyle { |
||||||
|
case title |
||||||
|
case wide |
||||||
|
case short |
||||||
|
} |
||||||
|
|
||||||
|
enum SummoningDisplayContext { |
||||||
|
case footer |
||||||
|
case menu |
||||||
|
} |
||||||
|
|
||||||
|
struct DeviceHelper { |
||||||
|
static func isBigScreen() -> Bool { |
||||||
|
switch UIDevice.current.userInterfaceIdiom { |
||||||
|
case .pad: // iPads |
||||||
|
return true |
||||||
|
case .phone: // iPhones (you can add more cases here for large vs small phones) |
||||||
|
if UIScreen.main.bounds.size.width > 375 { // iPhone X, 11, 12, 13 Pro Max etc. |
||||||
|
return true // large phones |
||||||
|
} else { |
||||||
|
return false // smaller phones |
||||||
|
} |
||||||
|
default: |
||||||
|
return false // Other devices (Apple Watch, TV, etc.) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static func maxCharacter() -> Int { |
||||||
|
switch UIDevice.current.userInterfaceIdiom { |
||||||
|
case .pad: // iPads |
||||||
|
return 30 |
||||||
|
case .phone: // iPhones (you can add more cases here for large vs small phones) |
||||||
|
if UIScreen.main.bounds.size.width > 375 { // iPhone X, 11, 12, 13 Pro Max etc. |
||||||
|
return 15 // large phones |
||||||
|
} else { |
||||||
|
return 9 // smaller phones |
||||||
|
} |
||||||
|
default: |
||||||
|
return 9 // Other devices (Apple Watch, TV, etc.) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static func charLength() -> Int { |
||||||
|
isBigScreen() ? 0 : 15 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
// |
||||||
|
// ExportFormat.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 19/07/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
enum ExportFormat: Int, Identifiable, CaseIterable { |
||||||
|
var id: Int { self.rawValue } |
||||||
|
|
||||||
|
case rawText |
||||||
|
case csv |
||||||
|
|
||||||
|
var suffix: String { |
||||||
|
switch self { |
||||||
|
case .rawText: |
||||||
|
return "txt" |
||||||
|
case .csv: |
||||||
|
return "csv" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func separator() -> String { |
||||||
|
switch self { |
||||||
|
case .rawText: |
||||||
|
return " " |
||||||
|
case .csv: |
||||||
|
return ";" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func newLineSeparator(_ count: Int = 1) -> String { |
||||||
|
return Array(repeating: "\n", count: count).joined() |
||||||
|
} |
||||||
|
} |
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue