From 59fe22662b5f7520f96f4c54c3dcefa34df36389 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 11:34:21 +0100 Subject: [PATCH] paca chpship --- PadelClub/Data/PlayerRegistration.swift | 75 +++- PadelClub/Data/TeamRegistration.swift | 121 +++++- PadelClub/Data/Tournament.swift | 89 ++++- PadelClub/Extensions/String+Extensions.swift | 9 + PadelClub/Utils/ExportFormat.swift | 5 +- PadelClub/Utils/FileImportManager.swift | 346 ++++++++++++++++-- .../Navigation/Umpire/PadelClubView.swift | 15 +- .../Views/Tournament/FileImportView.swift | 23 +- .../Screen/InscriptionManagerView.swift | 25 ++ 9 files changed, 651 insertions(+), 57 deletions(-) diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 0ffc213..a6968ab 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.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 { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index aeb2701..0f478e0 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -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() { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b2f3407..a3abbe8 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -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 diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 491f0c6..4be124b 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -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 diff --git a/PadelClub/Utils/ExportFormat.swift b/PadelClub/Utils/ExportFormat.swift index 4a2cb61..643159b 100644 --- a/PadelClub/Utils/ExportFormat.swift +++ b/PadelClub/Utils/ExportFormat.swift @@ -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 ";" } } diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index f67228b..49c007f 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -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 + } +} diff --git a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift index 56e46d8..fd40480 100644 --- a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -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 { diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index a25219b..a1276ce 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -86,21 +86,6 @@ struct FileImportView: View { @State private var validatedTournamentIds: Set = 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 { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 4117005..e06335b 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -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()