You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
574 lines
19 KiB
574 lines
19 KiB
//
|
|
// PlayerRegistration.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by razmig on 10/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
|
|
@Observable
|
|
final class PlayerRegistration: ModelObject, Storable {
|
|
static func resourceName() -> String { "player-registrations" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
static func filterByStoreIdentifier() -> Bool { return true }
|
|
static var relationshipNames: [String] = ["teamRegistration"]
|
|
|
|
var id: String = Store.randomId()
|
|
var teamRegistration: String?
|
|
var firstName: String
|
|
var lastName: String
|
|
var licenceId: String?
|
|
var rank: Int?
|
|
var paymentType: PlayerPaymentType?
|
|
var sex: PlayerSexType?
|
|
|
|
var tournamentPlayed: Int?
|
|
var points: Double?
|
|
var clubName: String?
|
|
var ligueName: String?
|
|
var assimilation: String?
|
|
|
|
var phoneNumber: String?
|
|
var email: String?
|
|
var birthdate: String?
|
|
|
|
var computedRank: Int = 0
|
|
var source: PlayerDataSource?
|
|
|
|
var hasArrived: Bool = false
|
|
|
|
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: PlayerDataSource? = nil, hasArrived: Bool = false) {
|
|
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
|
|
}
|
|
|
|
internal init(importedPlayer: ImportedPlayer) {
|
|
self.teamRegistration = ""
|
|
self.firstName = (importedPlayer.firstName ?? "").trimmed.capitalized
|
|
self.lastName = (importedPlayer.lastName ?? "").trimmed.uppercased()
|
|
self.licenceId = importedPlayer.license ?? nil
|
|
self.rank = Int(importedPlayer.rank)
|
|
self.sex = importedPlayer.male ? .male : .female
|
|
self.tournamentPlayed = importedPlayer.tournamentPlayed
|
|
self.points = importedPlayer.getPoints()
|
|
self.clubName = importedPlayer.clubName
|
|
self.ligueName = importedPlayer.ligueName
|
|
self.assimilation = importedPlayer.assimilation
|
|
self.source = .frenchFederation
|
|
}
|
|
|
|
internal init?(federalData: [String], sex: Int, sexUnknown: Bool) {
|
|
let _lastName = federalData[0].trimmed.uppercased()
|
|
let _firstName = federalData[1].trimmed.capitalized
|
|
if _lastName.isEmpty && _firstName.isEmpty { return nil }
|
|
lastName = _lastName
|
|
firstName = _firstName
|
|
birthdate = federalData[2]
|
|
licenceId = federalData[3]
|
|
clubName = federalData[4]
|
|
let stringRank = federalData[5]
|
|
if stringRank.isEmpty {
|
|
rank = nil
|
|
} else {
|
|
rank = Int(stringRank)
|
|
}
|
|
let _email = federalData[6]
|
|
if _email.isEmpty == false {
|
|
self.email = _email
|
|
}
|
|
let _phoneNumber = federalData[7]
|
|
if _phoneNumber.isEmpty == false {
|
|
self.phoneNumber = _phoneNumber
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
var tournamentStore: TournamentStore {
|
|
if let store = self.store as? TournamentStore {
|
|
return store
|
|
}
|
|
fatalError("missing store for \(String(describing: type(of: self)))")
|
|
}
|
|
|
|
var computedAge: Int? {
|
|
if let birthdate {
|
|
let components = birthdate.components(separatedBy: "/")
|
|
if components.count == 3 {
|
|
if let year = Calendar.current.dateComponents([.year], from: Date()).year, let age = components.last, let ageInt = Int(age) {
|
|
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].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 {
|
|
firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField)
|
|
}
|
|
|
|
func isSameAs(_ player: PlayerRegistration) -> Bool {
|
|
firstName.localizedCaseInsensitiveCompare(player.firstName) == .orderedSame &&
|
|
lastName.localizedCaseInsensitiveCompare(player.lastName) == .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 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 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 getRank() -> Int {
|
|
computedRank
|
|
}
|
|
|
|
@MainActor
|
|
func updateRank(from sources: [CSVParser], lastRank: Int) async throws {
|
|
if let dataFound = try await history(from: sources) {
|
|
rank = dataFound.rankValue?.toInt()
|
|
points = dataFound.points
|
|
tournamentPlayed = dataFound.tournamentCountValue?.toInt()
|
|
} else {
|
|
rank = lastRank
|
|
}
|
|
}
|
|
|
|
func history(from sources: [CSVParser]) async throws -> Line? {
|
|
guard let license = licenceId?.strippedLicense else {
|
|
return try await historyFromName(from: sources)
|
|
}
|
|
|
|
return await withTaskGroup(of: Line?.self) { group in
|
|
for source in sources.filter({ $0.maleData == isMalePlayer() }) {
|
|
group.addTask {
|
|
guard !Task.isCancelled else { print("Cancelled"); return nil }
|
|
|
|
return try? await source.first(where: { line in
|
|
line.rawValue.contains(";\(license);")
|
|
})
|
|
}
|
|
}
|
|
|
|
if let first = await group.first(where: { $0 != nil }) {
|
|
group.cancelAll()
|
|
return first
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func historyFromName(from sources: [CSVParser]) async throws -> Line? {
|
|
return await withTaskGroup(of: Line?.self) { group in
|
|
for source in sources.filter({ $0.maleData == isMalePlayer() }) {
|
|
group.addTask { [lastName, firstName] in
|
|
guard !Task.isCancelled else { print("Cancelled"); return nil }
|
|
|
|
return try? await source.first(where: { line in
|
|
line.rawValue.canonicalVersionWithPunctuation.contains(";\(lastName.canonicalVersionWithPunctuation);\(firstName.canonicalVersionWithPunctuation);")
|
|
})
|
|
}
|
|
}
|
|
|
|
if let first = await group.first(where: { $0 != nil }) {
|
|
group.cancelAll()
|
|
return first
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func setComputedRank(in tournament: Tournament) {
|
|
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 70_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 {
|
|
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
|
|
}
|
|
|
|
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 _birthdate = "birthdate"
|
|
case _phoneNumber = "phoneNumber"
|
|
case _email = "email"
|
|
case _computedRank = "computedRank"
|
|
case _source = "source"
|
|
case _hasArrived = "hasArrived"
|
|
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(id, forKey: ._id)
|
|
|
|
if let teamRegistration = teamRegistration {
|
|
try container.encode(teamRegistration, forKey: ._teamRegistration)
|
|
} else {
|
|
try container.encodeNil(forKey: ._teamRegistration)
|
|
}
|
|
|
|
try container.encode(firstName, forKey: ._firstName)
|
|
try container.encode(lastName, forKey: ._lastName)
|
|
|
|
if let licenceId = licenceId {
|
|
try container.encode(licenceId, forKey: ._licenceId)
|
|
} else {
|
|
try container.encodeNil(forKey: ._licenceId)
|
|
}
|
|
|
|
if let rank = rank {
|
|
try container.encode(rank, forKey: ._rank)
|
|
} else {
|
|
try container.encodeNil(forKey: ._rank)
|
|
}
|
|
|
|
if let paymentType = paymentType {
|
|
try container.encode(paymentType, forKey: ._paymentType)
|
|
} else {
|
|
try container.encodeNil(forKey: ._paymentType)
|
|
}
|
|
|
|
if let sex = sex {
|
|
try container.encode(sex, forKey: ._sex)
|
|
} else {
|
|
try container.encodeNil(forKey: ._sex)
|
|
}
|
|
|
|
if let tournamentPlayed = tournamentPlayed {
|
|
try container.encode(tournamentPlayed, forKey: ._tournamentPlayed)
|
|
} else {
|
|
try container.encodeNil(forKey: ._tournamentPlayed)
|
|
}
|
|
|
|
if let points = points {
|
|
try container.encode(points, forKey: ._points)
|
|
} else {
|
|
try container.encodeNil(forKey: ._points)
|
|
}
|
|
|
|
if let clubName = clubName {
|
|
try container.encode(clubName, forKey: ._clubName)
|
|
} else {
|
|
try container.encodeNil(forKey: ._clubName)
|
|
}
|
|
|
|
if let ligueName = ligueName {
|
|
try container.encode(ligueName, forKey: ._ligueName)
|
|
} else {
|
|
try container.encodeNil(forKey: ._ligueName)
|
|
}
|
|
|
|
if let assimilation = assimilation {
|
|
try container.encode(assimilation, forKey: ._assimilation)
|
|
} else {
|
|
try container.encodeNil(forKey: ._assimilation)
|
|
}
|
|
|
|
if let phoneNumber = phoneNumber {
|
|
try container.encode(phoneNumber, forKey: ._phoneNumber)
|
|
} else {
|
|
try container.encodeNil(forKey: ._phoneNumber)
|
|
}
|
|
|
|
if let email = email {
|
|
try container.encode(email, forKey: ._email)
|
|
} else {
|
|
try container.encodeNil(forKey: ._email)
|
|
}
|
|
|
|
if let birthdate = birthdate {
|
|
try container.encode(birthdate, forKey: ._birthdate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._birthdate)
|
|
}
|
|
|
|
try container.encode(computedRank, forKey: ._computedRank)
|
|
|
|
if let source = source {
|
|
try container.encode(source, forKey: ._source)
|
|
} else {
|
|
try container.encodeNil(forKey: ._source)
|
|
}
|
|
|
|
try container.encode(hasArrived, forKey: ._hasArrived)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
case 1...10: return 400
|
|
case 11...30: return 1000
|
|
case 31...60: return 2000
|
|
case 61...100: return 3000
|
|
case 101...200: return 8000
|
|
case 201...500: return 12000
|
|
default:
|
|
return 15000
|
|
}
|
|
}
|
|
|
|
func insertOnServer() {
|
|
self.tournamentStore.playerRegistrations.writeChangeAndInsertOnServer(instance: self)
|
|
}
|
|
|
|
}
|
|
|
|
extension PlayerRegistration: Hashable {
|
|
static func == (lhs: PlayerRegistration, rhs: PlayerRegistration) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|