paca chpship

paca_championship
Raz 11 months ago
parent a356d9768c
commit 59fe22662b
  1. 75
      PadelClub/Data/PlayerRegistration.swift
  2. 121
      PadelClub/Data/TeamRegistration.swift
  3. 89
      PadelClub/Data/Tournament.swift
  4. 9
      PadelClub/Extensions/String+Extensions.swift
  5. 5
      PadelClub/Utils/ExportFormat.swift
  6. 346
      PadelClub/Utils/FileImportManager.swift
  7. 15
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  8. 23
      PadelClub/Views/Tournament/FileImportView.swift
  9. 25
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -41,6 +41,10 @@ final class PlayerRegistration: ModelObject, Storable {
var coach: Bool = false
var captain: Bool = false
var clubCode: String?
var sourceName: String?
var isNVEQ: Bool = false
func localizedSourceLabel() -> String {
switch source {
case .frenchFederation:
@ -159,13 +163,74 @@ final class PlayerRegistration: ModelObject, Storable {
return nil
}
func fetchUnrankPlayerData() async throws -> Player? {
guard let licence = licenceId?.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).trimmed.strippedLicense else {
return nil
}
return try await fetchPlayerData(for: licence)?.first
}
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())
case .championship:
let values = [
lastName.uppercased(),
firstName.capitalized,
"=\"" + formattedLicense() + "\"",
"\(computedRank)",
isNVEQ ? "NVEQ" : "EQ",
]
.joined(separator: exportFormat.separator())
return values
}
}
func championshipAlerts(tournament: Tournament) -> [ChampionshipAlert] {
var alerts = [ChampionshipAlert]()
if isUnranked() && source == nil {
alerts.append(.unranked(self))
} else {
if tournament.tournamentCategory == .men && isMalePlayer() == false {
alerts.append(.playerSexInvalid(self))
}
if tournament.tournamentCategory == .women && isMalePlayer() {
alerts.append(.playerSexInvalid(self))
}
if let computedAge, tournament.federalTournamentAge.isAgeValid(age: computedAge) == false {
alerts.append(.playerAgeInvalid(self))
}
if isClubCodeOK() == false {
alerts.append(.playerClubInvalid(self))
}
if isNameOK() == false {
alerts.append(.playerNameInvalid(self))
}
if isLicenceOK() == false {
alerts.append(.playerLicenseInvalid(self))
}
}
return alerts
}
func alertCount() -> Int {
return championshipAlerts(tournament: tournament()!).count
}
func isClubCodeOK() -> Bool {
team()?.clubCode?.trimmed.canonicalVersion == clubCode?.trimmed.canonicalVersion
}
func isLicenceOK() -> Bool {
licenceId?.trimmed.strippedLicense != nil
}
func isNameOK() -> Bool {
lastName.canonicalVersion == sourceName?.canonicalVersion
}
func isPlaying() -> Bool {
@ -380,7 +445,9 @@ final class PlayerRegistration: ModelObject, Storable {
case _hasArrived = "hasArrived"
case _coach = "coach"
case _captain = "captain"
case _clubCode = "clubCode"
case _sourceName = "sourceName"
case _isNVEQ = "isNVEQ"
}
init(from decoder: Decoder) throws {
@ -410,6 +477,9 @@ final class PlayerRegistration: ModelObject, Storable {
email = try container.decodeIfPresent(String.self, forKey: ._email)
birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate)
source = try container.decodeIfPresent(PlayerDataSource.self, forKey: ._source)
clubCode = try container.decodeIfPresent(String.self, forKey: ._clubCode)
isNVEQ = try container.decodeIfPresent(Bool.self, forKey: ._isNVEQ) ?? false
sourceName = try container.decodeIfPresent(String.self, forKey: ._sourceName)
}
func encode(to encoder: Encoder) throws {
@ -437,6 +507,9 @@ final class PlayerRegistration: ModelObject, Storable {
try container.encode(hasArrived, forKey: ._hasArrived)
try container.encode(captain, forKey: ._captain)
try container.encode(coach, forKey: ._coach)
try container.encode(clubCode, forKey: ._clubCode)
try container.encode(sourceName, forKey: ._sourceName)
try container.encode(isNVEQ, forKey: ._isNVEQ)
}
enum PlayerDataSource: Int, Codable {

@ -42,6 +42,10 @@ final class TeamRegistration: ModelObject, Storable {
var unregistered: Bool = false
var unregistrationDate: Date? = nil
var clubCode: String?
var registratonMail: String?
var clubName: String?
func hasUnregistered() -> Bool {
unregistered
}
@ -86,7 +90,7 @@ final class TeamRegistration: ModelObject, Storable {
// MARK: - Computed dependencies
func unsortedPlayers() -> [PlayerRegistration] {
return self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false }
return self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false && $0.captain == false }
}
// MARK: -
@ -381,9 +385,106 @@ final class TeamRegistration: ModelObject, Storable {
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())
case .championship:
var baseValue: [String] = [
formattedInscriptionDate(exportFormat) ?? "",
alertCountFormatted(teamIndex: index),
alertDescription(teamIndex: index),
"\(weight)",
jokerWeightFormatted(),
playerCountFormatted(),
nveqCountFormatted(),
unrankedCountFormatted(),
registratonMail ?? "",
clubCode ?? "",
clubName ?? "",
tournamentObject()?.tournamentCategory.importingRawValue.capitalized ?? "",
teamNameLabel(),
]
// if let captain = captain() {
// baseValue.append(contentsOf: captain.pasteData())
// } else {
// baseValue.append("")
// baseValue.append("")
// baseValue.append("")
// }
// if let captain = coach() {
// baseValue.append(contentsOf: captain.coach())
// } else {
// baseValue.append("")
// baseValue.append("")
// baseValue.append("")
// }
var final = baseValue.joined(separator: exportFormat.separator())
players().forEach { pr in
final.append(exportFormat.separator() + pr.pasteData(exportFormat))
}
return final
}
}
func unrankedCountFormatted() -> String {
players().filter({ $0.isUnranked() && $0.source == nil }).count.formatted()
}
func alertDescription(teamIndex: Int) -> String {
let multiLineString = championshipAlerts(teamIndex: teamIndex, tournament: tournamentObject()!).compactMap({ $0.errorDescription }).joined(separator: "\n")
let escapedString = "\"\(multiLineString.replacingOccurrences(of: "\"", with: "\"\""))\""
return escapedString
}
func championshipAlerts(teamIndex: Int, tournament: Tournament) -> [ChampionshipAlert] {
var alerts = [ChampionshipAlert]()
if clubCode?.isValidCodeClub(62) == false {
alerts.append(.clubCodeInvalid(self))
}
let players = players()
if teamIndex <= 16, players.filter({ $0.isNVEQ }).count > 2 {
alerts.append(.tooManyNVEQ(self))
}
if teamIndex <= 16, players.count > 8 {
alerts.append(.tooManyPlayers(self))
}
players.forEach { pr in
alerts.append(contentsOf: pr.championshipAlerts(tournament: tournament))
}
return alerts
}
func jokerWeightFormatted() -> String {
if let joker = players()[safe:5] {
return "\(joker.computedRank)"
} else {
return ""
}
}
func alertCountFormatted(teamIndex: Int) -> String {
let championshipAlertsCount = championshipAlerts(teamIndex: teamIndex, tournament: tournamentObject()!).count
return championshipAlertsCount.formatted()
}
func nveqCountFormatted() -> String {
players().filter({ $0.isNVEQ }).count.formatted()
}
func playerCountFormatted() -> String {
let unsortedPlayersCount = unsortedPlayers().count
return unsortedPlayersCount.formatted()
}
var computedRegistrationDate: Date {
return registrationDate ?? .distantFuture
}
@ -396,7 +497,7 @@ final class TeamRegistration: ModelObject, Storable {
} else {
return nil
}
case .csv:
case .csv, .championship:
if let registrationDate {
return registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
@ -414,7 +515,7 @@ final class TeamRegistration: ModelObject, Storable {
} else {
return nil
}
case .csv:
case .csv, .championship:
if let callDate {
return callDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
@ -429,6 +530,8 @@ final class TeamRegistration: ModelObject, Storable {
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())
case .championship:
return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.separator())
}
}
@ -486,7 +589,7 @@ final class TeamRegistration: ModelObject, Storable {
self.unsortedPlayers().sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $0.rank ?? 0 < $1.rank ?? 0 },
{ $0.rank ?? Int.max < $1.rank ?? Int.max },
{ $0.lastName < $1.lastName},
{ $0.firstName < $1.firstName }
]
@ -642,6 +745,9 @@ final class TeamRegistration: ModelObject, Storable {
case _pointsEarned = "pointsEarned"
case _unregistered = "unregistered"
case _unregistrationDate = "unregistrationDate"
case _clubCode = "clubCode"
case _clubName = "clubName"
case _registratonMail = "registratonMail"
}
init(from decoder: Decoder) throws {
@ -672,6 +778,10 @@ final class TeamRegistration: ModelObject, Storable {
finalRanking = try container.decodeIfPresent(Int.self, forKey: ._finalRanking)
pointsEarned = try container.decodeIfPresent(Int.self, forKey: ._pointsEarned)
unregistrationDate = try container.decodeIfPresent(Date.self, forKey: ._unregistrationDate)
clubCode = try container.decodeIfPresent(String.self, forKey: ._clubCode)
clubName = try container.decodeIfPresent(String.self, forKey: ._clubName)
registratonMail = try container.decodeIfPresent(String.self, forKey: ._registratonMail)
}
func encode(to encoder: Encoder) throws {
@ -700,6 +810,9 @@ final class TeamRegistration: ModelObject, Storable {
try container.encode(pointsEarned, forKey: ._pointsEarned)
try container.encode(unregistered, forKey: ._unregistered)
try container.encode(unregistrationDate, forKey: ._unregistrationDate)
try container.encode(clubCode, forKey: ._clubCode)
try container.encode(clubName, forKey: ._clubName)
try container.encode(registratonMail, forKey: ._registratonMail)
}
func insertOnServer() {

@ -597,6 +597,86 @@ defer {
teamPaste.append(team.pasteData(exportFormat, index + 1))
}
return teamPaste.joined(separator: exportFormat.newLineSeparator())
case .championship:
let headers = [
"Horodateur",
"alertCount",
"alertDescripion",
"poids",
"joker",
"playerCount",
"nveqCount",
"NC Non vérifié",
"Adresse e-mail",
"Code Club",
"Nom du Club",
"Catégorie",
"Numéro d'Equipe",
// "Nom du Capitaine (Il doit être licencié FFT 2025)",
// "Numéro du Téléphone",
// "E-mail",
// "Nom du Correspondant",
// "Numéro du Téléphone",
// "E-mail",
"JOUEUR 1 - Nom",
"JOUEUR 1 - Prénom",
"JOUEUR 1 - Licence",
"JOUEUR 1 - Ranking",
"JOUEUR 1 - Statut",
"JOUEUR 2 - Nom",
"JOUEUR 2 - Prénom",
"JOUEUR 2 - Licence",
"JOUEUR 2 - Ranking",
"JOUEUR 2 - Statut",
"JOUEUR 3 - Nom",
"JOUEUR 3 - Prénom",
"JOUEUR 3 - Licence",
"JOUEUR 3 - Ranking",
"JOUEUR 3 - Statut",
"JOUEUR 4 - Nom",
"JOUEUR 4 - Prénom",
"JOUEUR 4 - Licence",
"JOUEUR 4 - Ranking",
"JOUEUR 4 - Statut",
"JOUEUR 5 - Nom",
"JOUEUR 5 - Prénom",
"JOUEUR 5 - Licence",
"JOUEUR 5 - Ranking",
"JOUEUR 5 - Statut",
"JOUEUR 6 - Nom",
"JOUEUR 6 - Prénom",
"JOUEUR 6 - Licence",
"JOUEUR 6 - Ranking",
"JOUEUR 6 - Statut",
"JOUEUR 7 - Nom",
"JOUEUR 7 - Prénom",
"JOUEUR 7 - Licence",
"JOUEUR 7 - Ranking",
"JOUEUR 7 - Statut",
"JOUEUR 8 - Nom",
"JOUEUR 8 - Prénom",
"JOUEUR 8 - Licence",
"JOUEUR 8 - Ranking",
"JOUEUR 8 - Statut",
"JOUEUR 9 - Nom",
"JOUEUR 9 - Prénom",
"JOUEUR 9 - Licence",
"JOUEUR 9 - Ranking",
"JOUEUR 9 - Statut",
"JOUEUR 10 - Nom",
"JOUEUR 10 - Prénom",
"JOUEUR 10 - Licence",
"JOUEUR 10 - Ranking",
"JOUEUR 10 - Statut",
].joined(separator: exportFormat.separator())
var teamPaste = [headers]
for (index, team) in selectedSortedTeams.enumerated() {
teamPaste.append(team.pasteData(exportFormat, index + 1))
}
return teamPaste.joined(separator: exportFormat.newLineSeparator())
}
}
@ -1066,7 +1146,7 @@ defer {
//todo
func significantPlayerCount() -> Int {
return minimumPlayerPerTeam
return 6
}
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
@ -1146,14 +1226,17 @@ defer {
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory)
teamsToImport.append(previousTeam)
} else {
var registrationDate = team.registrationDate
var registrationDate = team.registrationDate ?? team.teamChampionship?.getRegistrationDate()
if let previousPlayer = players.first(where: { player in
let ids = team.players.compactMap({ $0.licenceId })
return ids.contains(player.licenceId!)
}), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate {
registrationDate = previousTeamRegistrationDate
}
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name)
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name ?? team.teamChampionship?.teamIndex)
newTeam.clubCode = team.teamChampionship?.clubCode
newTeam.clubName = team.teamChampionship?.clubName
newTeam.registratonMail = team.teamChampionship?.registrationMail
if isAnimation() {
if newTeam.weight == 0 {
newTeam.weight = team.index(in: teams) ?? 0

@ -165,6 +165,15 @@ extension String {
let matches = self.matches(of: /[1-9][0-9]{5,7}/)
return matches.map { String(self[$0.range]) }
}
func isValidCodeClub(_ codeClubPrefix: Int) -> Bool {
let code = trimmed.replaceCharactersFromSet(characterSet: .whitespaces)
guard code.hasPrefix(String(codeClubPrefix)) else { return false }
guard code.count == 8 else { return false }
return true
}
}
// MARK: - FFT Source Importing

@ -12,12 +12,13 @@ enum ExportFormat: Int, Identifiable, CaseIterable {
case rawText
case csv
case championship
var suffix: String {
switch self {
case .rawText:
return "txt"
case .csv:
case .csv, .championship:
return "csv"
}
}
@ -26,7 +27,7 @@ enum ExportFormat: Int, Identifiable, CaseIterable {
switch self {
case .rawText:
return " "
case .csv:
case .csv, .championship:
return ";"
}
}

@ -131,13 +131,15 @@ class FileImportManager {
let previousTeam: TeamRegistration?
var registrationDate: Date? = nil
var name: String? = nil
var teamChampionship: TeamChampionship?
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, tournamentAgeCategory: FederalTournamentAge, previousTeam: TeamRegistration?, registrationDate: Date? = nil, name: String? = nil, tournament: Tournament) {
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, tournamentAgeCategory: FederalTournamentAge, previousTeam: TeamRegistration?, registrationDate: Date? = nil, name: String? = nil, teamChampionship: TeamChampionship? = nil, tournament: Tournament) {
self.players = Set(players)
self.tournamentCategory = tournamentCategory
self.tournamentAgeCategory = tournamentAgeCategory
self.name = name
self.previousTeam = previousTeam
self.teamChampionship = teamChampionship
if players.count < 2 {
let s = players.compactMap { $0.sex?.rawValue }
var missing = tournamentCategory.mandatoryPlayerType()
@ -146,7 +148,7 @@ class FileImportManager {
missing.remove(at: index)
}
}
let significantPlayerCount = 2
let significantPlayerCount = tournament.significantPlayerCount()
let pl = players.prefix(significantPlayerCount).map { $0.computedRank }
let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 70_000 : 10_000) }).prefix(significantPlayerCount)
self.weight = pl.reduce(0,+) + missingPl.reduce(0,+)
@ -179,7 +181,7 @@ class FileImportManager {
static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur"
func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation, checkingCategoryDisabled: Bool, chunkByParameter: Bool) async throws -> [TeamHolder] {
func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation, checkingCategoryDisabled: Bool, chunkMode: ChunkMode) async throws -> [TeamHolder] {
switch fileProvider {
case .frenchFederation:
@ -187,9 +189,9 @@ class FileImportManager {
case .padelClub:
return await _getPadelClubTeams(from: fileContent, tournament: tournament)
case .custom:
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkByParameter: chunkByParameter, autoSearch: false, tournament: tournament)
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkMode: chunkMode, autoSearch: false, tournament: tournament)
case .customAutoSearch:
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkByParameter: chunkByParameter, autoSearch: true, tournament: tournament)
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkMode: chunkMode, autoSearch: true, tournament: tournament)
}
}
@ -425,7 +427,7 @@ class FileImportManager {
return results
}
private func _getPadelBusinessLeagueTeams(from fileContent: String, chunkByParameter: Bool, autoSearch: Bool, tournament: Tournament) async -> [TeamHolder] {
private func _getPadelBusinessLeagueTeams(from fileContent: String, chunkMode: ChunkMode, autoSearch: Bool, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.replacingOccurrences(of: "\"", with: "").components(separatedBy: "\n")
guard let firstLine = lines.first else { return [] }
var separator = ","
@ -436,13 +438,27 @@ class FileImportManager {
let federalContext = PersistenceController.shared.localContainer.viewContext
var chunks: [[String]] = []
if chunkByParameter {
switch chunkMode {
case .byParameter:
chunks = lines.chunked(byParameterAt: 1)
} else {
case .byCoupleOfLines:
chunks = lines.chunked(into: 2)
case .byColumn:
chunks = lines.extractPlayers(filterKey: tournament.tournamentCategory.importingRawValue.capitalized, separator: separator)
}
let results = chunks.map { team in
let results = chunks.map { teamSource in
var teamName: String? = nil
var teamChampionship: TeamChampionship? = nil
var team = teamSource
if chunkMode == .byColumn {
if let first = teamSource.first?.components(separatedBy: separator) {
team = Array(teamSource.dropFirst())
teamChampionship = TeamChampionship(registrationDate: first[0], registrationMail: first[1], clubCode: first[2], teamIndex: first[3], clubName: first[4])
}
}
let players = team.map { player in
let data = player.components(separatedBy: separator)
let lastName : String = data[safe: 2]?.prefixTrimmed(50) ?? ""
@ -456,27 +472,54 @@ class FileImportManager {
let rank : Int? = data[safe: 6]?.trimmed.toInt()
let licenceId : String? = data[safe: 7]?.prefixTrimmed(50)
let club : String? = data[safe: 8]?.prefixTrimmed(200)
let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName)
fetchRequest.predicate = predicate
let found = try? federalContext.fetch(fetchRequest).first
if let found, autoSearch {
let player = PlayerRegistration(importedPlayer: found)
player.setComputedRank(in: tournament)
player.email = email
player.phoneNumber = phoneNumber
return player
let status : String? = data[safe: 9]
if chunkMode == .byColumn {
let predicate = NSPredicate(format: "license == %@", licenceId!.strippedLicense!)
fetchRequest.predicate = predicate
let found = try? federalContext.fetch(fetchRequest).first
if let found {
let player = PlayerRegistration(importedPlayer: found)
player.setComputedRank(in: tournament)
player.sourceName = lastName
player.isNVEQ = status == "NVEQ"
player.clubCode = found.clubCode
return player
} else {
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email)
player.sourceName = lastName
player.isNVEQ = status == "NVEQ"
if rank == nil {
player.setComputedRank(in: tournament)
} else {
player.computedRank = rank ?? 0
}
return player
}
} else {
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email)
if rank == nil, autoSearch {
let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName)
fetchRequest.predicate = predicate
let found = try? federalContext.fetch(fetchRequest).first
if let found, autoSearch {
let player = PlayerRegistration(importedPlayer: found)
player.setComputedRank(in: tournament)
player.email = email
player.phoneNumber = phoneNumber
return player
} else {
player.computedRank = rank ?? 0
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email)
if rank == nil, autoSearch {
player.setComputedRank(in: tournament)
} else {
player.computedRank = rank ?? 0
}
return player
}
return player
}
}
return TeamHolder(players: players, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: nil, name: teamName, tournament: tournament)
return TeamHolder(players: players, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: nil, teamChampionship: teamChampionship, tournament: tournament)
}
return results
}
@ -513,5 +556,262 @@ extension Array where Element == String {
return groups.map { $0.value }
}
}
func extractPlayers(filterKey: String = "Messieurs", separator: String = ";") -> [[String]] {
return self.dropFirst().compactMap { line in
let components = line.components(separatedBy: separator)
guard components.count >= 62 else { return nil }
guard components.contains(filterKey) else { return nil }
var players: [PlayerChampionship] = []
let teamChampionship = TeamChampionship(registrationDate: components[0], registrationMail: components[1], clubCode: components[3], teamIndex: components[5], clubName: components[2])
// Add captain and coach first
// players.append(PlayerChampionship.captain(components))
// players.append(PlayerChampionship.coach(components))
// Extract team information
let teamType = components[4]
let sex = teamType.lowercased().contains("dames") ? "f" : "m"
// Process up to 10 players
for i in 0..<10 {
let lastNameIndex = 12 + (i * 5)
let firstNameIndex = 13 + (i * 5)
let licenseIndex = 14 + (i * 5)
let rankingIndex = 15 + (i * 5)
let statusIndex = 16 + (i * 5)
guard lastNameIndex < components.count,
!components[lastNameIndex].isEmpty else {
continue
}
let statusString = components[statusIndex]
let status: PlayerChampionship.Status = statusString.hasPrefix("NVEQ") ? .nveq : .eq
var licenseNumber = components[licenseIndex]
//var ranking = components[rankingIndex]
let strippedLicense = components[licenseIndex].strippedLicense
let strippedLicenseRank = components[rankingIndex].strippedLicense
if strippedLicense == nil && strippedLicenseRank != nil {
licenseNumber = components[rankingIndex]
//ranking = components[licenseIndex]
}
let player = PlayerChampionship(
lastName: components[lastNameIndex],
firstName: components[firstNameIndex],
licenseNumber: licenseNumber,
ranking: nil,
status: status,
email: nil,
mobileNumber: nil
)
players.append(player)
}
return [teamChampionship.rawValue(separator: separator)] + players.map { player in
player.rawValue(separator: separator, sex: sex)
}
}
}
}
struct TeamChampionship {
let registrationDate: String
let registrationMail: String
let clubCode: String
let teamIndex: String
let clubName: String
func getRegistrationDate() -> Date? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
if let date = dateFormatter.date(from: registrationDate) {
return date
} else {
return nil
}
}
func rawValue(separator: String = ";") -> String {
let components = [
registrationDate,
registrationMail,
clubCode.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).trimmed,
teamIndex.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).trimmed,
clubName.trimmed
]
return components.joined(separator: separator)
}
}
struct PlayerChampionship {
enum Status: String {
case eq = "EQ" // était licencié au club l'an dernier
case nveq = "NVEQ" // N'a pas joué avec le club l'an dernier
case captain = "CAPTAIN"
case coach = "COACH"
}
let lastName: String
let firstName: String
let licenseNumber: String
let ranking: Int?
let status: Status
let email: String?
let mobileNumber: String?
static func captain(_ components: [String]) -> PlayerChampionship {
let fullName = components[6].components(separatedBy: " ")
let lastName = fullName.count > 1 ? fullName[0] : components[6]
let firstName = fullName.count > 1 ? fullName[1] : ""
return PlayerChampionship(
lastName: lastName,
firstName: firstName,
licenseNumber: "",
ranking: 0,
status: .captain,
email: components[8],
mobileNumber: components[7]
)
}
static func coach(_ components: [String]) -> PlayerChampionship {
let fullName = components[9].components(separatedBy: " ")
let lastName = fullName.count > 1 ? fullName[0] : components[9]
let firstName = fullName.count > 1 ? fullName[1] : ""
return PlayerChampionship(
lastName: lastName,
firstName: firstName,
licenseNumber: "",
ranking: 0,
status: .coach,
email: components[11],
mobileNumber: components[10]
)
}
func rawValue(separator: String = ";", sex: String = "m") -> String {
let components = [
sex,
"",
lastName.trimmed,
firstName.trimmed,
"",
"",
"",
licenseNumber.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines),
"",
status.rawValue
]
return components.joined(separator: separator)
}
}
enum ChunkMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case byParameter
case byCoupleOfLines
case byColumn
func localizedChunkModeLabel() -> String {
switch self {
case .byParameter:
return "Nom d'équipe"
case .byCoupleOfLines:
return "Groupe de 2 lignes"
case .byColumn:
return "Par colonne"
}
}
}
enum ChampionshipAlert: LocalizedError {
case clubCodeInvalid(TeamRegistration)
case tooManyNVEQ(TeamRegistration)
case tooManyPlayers(TeamRegistration)
case playerClubInvalid(PlayerRegistration)
case playerLicenseInvalid(PlayerRegistration)
case playerNameInvalid(PlayerRegistration)
case unranked(PlayerRegistration)
case playerAgeInvalid(PlayerRegistration)
case playerSexInvalid(PlayerRegistration)
var errorDescription: String? {
switch self {
case .clubCodeInvalid(let teamRegistration):
if let clubCode = teamRegistration.clubCode {
return "CODE NOK : \(clubCode)"
} else {
return "aucun code club"
}
case .tooManyNVEQ(let teamRegistration):
return "TOO MANY NVEQ"
case .tooManyPlayers(let teamRegistration):
return "TOO MANY PLAYERS"
case .playerClubInvalid(let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .playerLicenseInvalid(let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .playerNameInvalid(let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .unranked(let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .playerAgeInvalid(let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
case .playerSexInvalid(let playerRegistration):
return playerRegistration.errorDescription(championshipAlert: self)
}
}
}
extension PlayerRegistration {
func errorDescription(championshipAlert: ChampionshipAlert) -> String? {
var message = lastName + " -> "
switch championshipAlert {
case .clubCodeInvalid, .tooManyNVEQ, .tooManyPlayers:
return nil
case .unranked:
message += "NC NOT FOUND"
case .playerClubInvalid:
if let clubCode {
message += "CLUB NOK : " + clubCode
} else {
message += "aucun club"
}
case .playerLicenseInvalid:
if let licenceId {
message += "LICENCE NOK : " + licenceId
} else {
message += "aucune licence"
}
case .playerNameInvalid:
if let sourceName {
message += "NOM NOK : " + sourceName
} else {
message += "aucun nom source"
}
case .playerAgeInvalid:
if let computedAge {
message += "AGE NOK : \(computedAge)" + " ans"
} else {
message += "aucun âge"
}
case .playerSexInvalid:
message += "SEXE NOK : " + (isMalePlayer() ? "H" : "F")
}
return message
}
}

@ -250,7 +250,8 @@ struct PadelClubView: View {
// Function to fetch data for a single license ID
func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=82477107&numeroLicence=\(licenseID)") else {
let homologation = "82469282"
guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=\(homologation)&numeroLicence=\(licenseID)") else {
throw URLError(.badURL)
}
@ -264,11 +265,11 @@ func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
request.setValue("beach-padel.app.fft.fr", forHTTPHeaderField: "Host")
request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15", forHTTPHeaderField: "User-Agent")
request.setValue("keep-alive", forHTTPHeaderField: "Connection")
request.setValue("https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation=82477107", forHTTPHeaderField: "Referer")
request.setValue("https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation=\(homologation)", forHTTPHeaderField: "Referer")
request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
// Add cookies if needed (example cookie header value shown, replace with valid cookies)
request.setValue("JSESSIONID=F4ED2A1BCF3CD2694FE0B111B8027999; AWSALB=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; AWSALBCORS=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; datadome=KlbIdnrCgaY1zLVIZ5CfLJm~KXv9_YnXGhaQdqMEn6Ja9R6imBH~vhzmyuiLxGi1D0z90v5x2EiGDvQ7zsw~fajWLbOupFEajulc86PSJ7RIHpOiduCQ~cNoITQYJOXa; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQLNQOPLOSLJOZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQKSMOZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOQMSLNZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQNSJMZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOSJMLJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLRPQMQQNRQRZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLRPQNKSLOMSZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLSNSOPMSOPJZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMJQSRLJSOOJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMJRJPJMSSKRZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; tCdebugLib=1; incap_ses_2222_2712217=ui9wOOAjNziUTlU3gCHWHtv/KWcAAAAAhSzbpyITRp7YwRT3vJB2vg==; incap_ses_2224_2712217=NepDAr2kUDShMiCJaDzdHqbjKWcAAAAA0kLlk3lgvGnwWSTMceZoEw==; xtan=-; xtant=1; incap_ses_1350_2712217=g+XhSJRwOS8JlWTYCSq8EtOBJGcAAAAAffg2IobkPUW2BtvgJGHbMw==; TCSESSION=124101910177775608913; nlbi_2712217=jnhtOC5KDiLvfpy/b9lUTgAAAAA7zduh8JyZOVrEfGsEdFlq; TCID=12481811494814553052; xtvrn=$548419$; TCPID=12471746148351334672; visid_incap_2712217=PSfJngzoSuiowsuXXhvOu5K+7mUAAAAAQUIPAAAAAAAleL9ldvN/FC1VykkU9ret; SessionStatId=10.91.140.42.1662124965429001", forHTTPHeaderField: "Cookie")
request.setValue("JSESSIONID=0A23429250749BFDD2A869CC1AF504E6; AWSALB=aUE6ypZc8yvsIjM20SSKgQgvtnVb6NJQrngf7HjwQfL7T9xbgAsOsywQZ5gIJ68SaOrUMk3Wa4wpGDbTBi8s2uICY+P7pUNijn83S1VG19Ut9448W4xvlczp1nh1; AWSALBCORS=aUE6ypZc8yvsIjM20SSKgQgvtnVb6NJQrngf7HjwQfL7T9xbgAsOsywQZ5gIJ68SaOrUMk3Wa4wpGDbTBi8s2uICY+P7pUNijn83S1VG19Ut9448W4xvlczp1nh1; datadome=bkjv7vJ9V9vQ7MP9KJcsYXOSlEfkwTFS5B4scCfp1ugdXe2jsnXTTUeiwz7TFdjd04_WqZMH2U2zqV4mmTklpMBYF46~iqCugDRCrPgLvqSc1~KLwJw4h3dm5m6gH9HK; xtan=-; xtant=1; tc_cj_v2=%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMMORPMMMQNNZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMMQMPMPMKOPZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; tCdebugLib=1; _pcid=%7B%22browserId%22%3A%22m42mi4kbtfuyj367%22%2C%22_t%22%3A%22mjr1fm32%7Cm42mi4r2%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbAFYwAjADN%2BAZgCsAH34AWAEz96CmNJABfIA; _pprv=eyJjb25zZW50Ijp7IjAiOnsibW9kZSI6ImVzc2VudGlhbCJ9LCI3Ijp7Im1vZGUiOiJvcHQtaW4ifX0sInB1cnBvc2VzIjpudWxsLCJfdCI6Im1qcjFmbHdofG00Mm1pNGtoIn0%3D; TCID=124122155494907703483; TCPID=124115115191501043230; xtvrn=$548419$; visid_incap_2712217=PSfJngzoSuiowsuXXhvOu5K+7mUAAAAAQUIPAAAAAAAleL9ldvN/FC1VykkU9ret; SessionStatId=10.91.140.42.1662124965429001", forHTTPHeaderField: "Cookie")
let (data, _) = try await URLSession.shared.data(for: request)
let decoder = JSONDecoder()
@ -293,6 +294,8 @@ func fetchPlayersDataSequentially(for licenseIDs: inout [FederalPlayer]) async {
if let playerData = try await fetchPlayerData(for: licenseID.license)?.first {
licenseID.lastName = playerData.nom
licenseID.firstName = playerData.prenom
licenseID.birthYear = playerData.birthYear()
licenseID.clubCode = playerData.codeClub
}
} catch {
print(error)
@ -306,6 +309,12 @@ struct Player: Codable {
let nom: String
let prenom: String
let sexe: String
let codeClub: String
let dateNaissanceFr: String
func birthYear() -> Int? {
return Int(dateNaissanceFr.suffix(4))
}
}
struct Response: Codable {

@ -86,21 +86,6 @@ struct FileImportView: View {
@State private var validatedTournamentIds: Set<String> = Set()
@State private var chunkMode: ChunkMode = .byParameter
enum ChunkMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case byParameter
case byCoupleOfLines
func localizedChunkModeLabel() -> String {
switch self {
case .byParameter:
return "Nom d'équipe"
case .byCoupleOfLines:
return "Groupe de 2 lignes"
}
}
}
init(defaultFileProvider: FileImportManager.FileProvider = .frenchFederation) {
_fileProvider = .init(wrappedValue: defaultFileProvider)
}
@ -109,10 +94,6 @@ struct FileImportView: View {
return self.tournament.tournamentStore
}
var chunkByParameter: Bool {
return chunkMode == .byParameter
}
private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] {
if tournament.isAnimation() {
return teams.sorted(by: \.weight)
@ -547,7 +528,7 @@ struct FileImportView: View {
for someTournament in event.tournaments {
let combinedCategory = CombinedCategory(tournamentCategory: someTournament.tournamentCategory, federalTournamentAge: someTournament.federalTournamentAge)
if categoriesDone.contains(combinedCategory) == false {
let _teams = try await FileImportManager.shared.createTeams(from: fileContent, tournament: someTournament, fileProvider: fileProvider, checkingCategoryDisabled: false, chunkByParameter: chunkByParameter)
let _teams = try await FileImportManager.shared.createTeams(from: fileContent, tournament: someTournament, fileProvider: fileProvider, checkingCategoryDisabled: false, chunkMode: chunkMode)
self.teams += _teams
categoriesDone.append(combinedCategory)
} else {
@ -555,7 +536,7 @@ struct FileImportView: View {
}
}
} else {
self.teams = try await FileImportManager.shared.createTeams(from: fileContent, tournament: tournament, fileProvider: fileProvider, checkingCategoryDisabled: true, chunkByParameter: chunkByParameter)
self.teams = try await FileImportManager.shared.createTeams(from: fileContent, tournament: tournament, fileProvider: fileProvider, checkingCategoryDisabled: true, chunkMode: chunkMode)
}
await MainActor.run {

@ -471,6 +471,12 @@ struct InscriptionManagerView: View {
Text("En csv")
}
}
if let teamPaste = teamPaste(.championship) {
ShareLink(item: teamPaste) {
Text("En csv pour la ligue")
}
}
} label: {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
}
@ -858,6 +864,25 @@ struct InscriptionManagerView: View {
}
}
}
Button("Récupérer les non-classés") {
Task {
for player in tournament.players().filter({ $0.isUnranked() }) {
do {
if let playerData = try await player.fetchUnrankPlayerData() {
player.lastName = playerData.nom
player.birthdate = playerData.dateNaissanceFr
player.clubCode = playerData.codeClub
player.source = .frenchFederation
try tournamentStore.playerRegistrations.addOrUpdate(instance: player)
}
} catch {
print(error)
}
}
}
}
} header: {
HStack {
Spacer()

Loading…
Cancel
Save