From 0a3110967aa8b42378a94ef38a9639edd3a787b8 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 13 May 2024 14:03:09 +0200 Subject: [PATCH] handle may 2024 anonymous stuff --- .../Coredata/ImportedPlayer+Extensions.swift | 4 +- PadelClub/Data/Federal/FederalPlayer.swift | 14 +-- PadelClub/Data/Federal/PlayerHolder.swift | 4 + PadelClub/Utils/FileImportManager.swift | 53 ++++++++-- PadelClub/Views/Cashier/CashierView.swift | 2 +- .../Components/GroupStageTeamView.swift | 2 +- .../Components/MatchTeamDetailView.swift | 2 +- .../Navigation/Toolbox/PadelClubView.swift | 97 ++++++++++++++----- .../Components/EditablePlayerView.swift | 29 +++++- .../Views/Shared/ImportedPlayerView.swift | 8 +- .../Views/Tournament/FileImportView.swift | 50 ++++------ .../Screen/InscriptionManagerView.swift | 2 +- 12 files changed, 184 insertions(+), 83 deletions(-) diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index 9b8ce71..7288d76 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -24,11 +24,11 @@ extension ImportedPlayer: PlayerHolder { } func getFirstName() -> String { - self.firstName ?? "prénom inconnu" + self.firstName ?? "" } func getLastName() -> String { - self.lastName ?? "nom inconnu" + self.lastName ?? "" } func formattedLicense() -> String { diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 60d2c9d..1963c02 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -7,7 +7,7 @@ import Foundation -struct FederalPlayer: Decodable { +class FederalPlayer: Decodable { var rank: Int var lastName: String var firstName: String @@ -27,7 +27,7 @@ struct FederalPlayer: Decodable { let code, codeFov: String } - init(from decoder: Decoder) throws { + required init(from decoder: Decoder) throws { enum CodingKeys: String, CodingKey { case nom case prenom @@ -77,10 +77,9 @@ struct FederalPlayer: Decodable { tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois) let assimile = try container.decode(Bool.self, forKey: .assimile) assimilation = assimile ? "Oui" : "Non" - fullNameCanonical = _lastName.canonicalVersion + " " + _firstName.canonicalVersion } - + func exportToCSV() -> String { let pointsString = points != nil ? String(Int(points!)) : "" let tournamentCountString = tournamentCount != nil ? String(tournamentCount!) : "" @@ -108,9 +107,7 @@ struct FederalPlayer: Decodable { } return modifiedString } - - var fullNameCanonical: String - + /* ;RANG;NOM;PRENOM;Nationalité;N° Licence;POINTS;Assimilation;NB. DE TOURNOIS JOUES;LIGUE;CODE CLUB;CLUB; */ @@ -139,7 +136,7 @@ struct FederalPlayer: Decodable { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })) -// print(result) + //print(result) if result.count < 11 { return nil } @@ -151,7 +148,6 @@ struct FederalPlayer: Decodable { lastName = result[1] firstName = result[2] - fullNameCanonical = result[1].canonicalVersion + " " + result[2].canonicalVersion country = result[3] license = result[4] diff --git a/PadelClub/Data/Federal/PlayerHolder.swift b/PadelClub/Data/Federal/PlayerHolder.swift index ba00af2..e2ab0eb 100644 --- a/PadelClub/Data/Federal/PlayerHolder.swift +++ b/PadelClub/Data/Federal/PlayerHolder.swift @@ -29,4 +29,8 @@ extension PlayerHolder { var isAssimilated: Bool { assimilation == "Oui" } + + func isAnonymous() -> Bool { + getFirstName().isEmpty && getLastName().isEmpty + } } diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index d7ce2fc..c3a153d 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -11,6 +11,31 @@ import LeStorage class FileImportManager { static let shared = FileImportManager() + func updatePlayers(isMale: Bool, players: inout [FederalPlayer]) { + guard let mostRecentDateAvailable = URL.importDateFormatter.date(from: "05-2024") else { return } + let replacements: [(Character, Character)] = [("Á", "ç"), ("‡", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("…", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")] + + var playersLeft = players + SourceFileManager.shared.allFilesSortedByDate(isMale).filter({ $0.dateFromPath.isEarlierThan(mostRecentDateAvailable) }).forEach({ url in + if playersLeft.isEmpty == false { + let federalPlayers = readCSV(inputFile: url) + let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements + + playersLeft.forEach { importedPlayer in + if let federalPlayer = federalPlayers.first(where: { $0.license == importedPlayer.license }) { + var lastName = federalPlayer.lastName + lastName.replace(characters: replacementsCharacters) + var firstName = federalPlayer.firstName + firstName.replace(characters: replacementsCharacters) + importedPlayer.lastName = lastName + importedPlayer.firstName = firstName + } + } + playersLeft.removeAll(where: { $0.lastName.isEmpty == false }) + } + }) + } + func foundInWomenData(license: String?) -> Bool { guard let license = license?.strippedLicense else { return false @@ -68,11 +93,23 @@ class FileImportManager { let previousTeam: TeamRegistration? var registrationDate: Date? = nil - init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) { + init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil, tournament: Tournament) { self.players = Set(players) self.tournamentCategory = tournamentCategory self.previousTeam = previousTeam - self.weight = players.map { $0.computedRank }.reduce(0,+) + if players.count < 2 { + let s = players.compactMap { $0.sex?.rawValue } + var missing = tournamentCategory.mandatoryPlayerType() + s.forEach { i in + if let index = missing.firstIndex(of: i) { + missing.remove(at: index) + } + } + let significantPlayerCount = 2 + self.weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? 0 }).prefix(significantPlayerCount).reduce(0,+) + } else { + self.weight = players.map { $0.computedRank }.reduce(0,+) + } self.registrationDate = registrationDate } @@ -110,7 +147,7 @@ class FileImportManager { return await _getPadelBusinessLeagueTeams(from: fileContent, tournament: tournament) } } - + func importDataFromFFT() async -> String? { if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { for source in SourceFile.allCases { @@ -214,7 +251,7 @@ class FileImportManager { playerOne.setComputedRank(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setComputedRank(in: tournament) - let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament) results.append(team) } } @@ -261,7 +298,7 @@ class FileImportManager { let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setComputedRank(in: tournament) - let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament) results.append(team) } } @@ -270,11 +307,11 @@ class FileImportManager { } private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { - let lines = fileContent.components(separatedBy: "\n\n") + var lines = fileContent.components(separatedBy: "\n\n") var results: [TeamHolder] = [] let fetchRequest = ImportedPlayer.fetchRequest() let federalContext = PersistenceController.shared.localContainer.viewContext - + lines.removeAll(where: { $0.contains("Liste d'attente")}) lines.forEach { team in let data = team.components(separatedBy: "\n") let players = team.licencesFound() @@ -292,7 +329,7 @@ class FileImportManager { } return nil } - let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate) + let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate, tournament: tournament) results.append(team) } } diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index 5b5700d..8fab647 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -159,7 +159,7 @@ struct CashierView: View { @ViewBuilder func computedPlayerView(_ player: PlayerRegistration) -> some View { - EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) } private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 01e3d1d..f5ce7b8 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -17,7 +17,7 @@ struct GroupStageTeamView: View { List { Section { ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) } } diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift index b09a46e..b5fb3dd 100644 --- a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -31,7 +31,7 @@ struct MatchTeamDetailView: View { private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { Section { ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) } } header: { TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil) diff --git a/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift b/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift index f4401fd..2a5f834 100644 --- a/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift @@ -26,7 +26,12 @@ struct PadelClubView: View { animation: .default) private var players: FetchedResults - + @FetchRequest( + sortDescriptors: [], + predicate: NSPredicate(format: "lastName == %@ && firstName == %@", "", ""), + animation: .default) + private var anonymousPlayers: FetchedResults + var _mostRecentDateAvailable: Date? { SourceFileManager.shared.mostRecentDateAvailable } @@ -38,6 +43,37 @@ struct PadelClubView: View { var body: some View { List { + #if targetEnvironment(simulator) + /* + ["36435", "BOUNOUA", "walid", "France", "3311600", "15,00", "Non", "2", "AUVERGNE RHONE-ALPES", "50 73 0046", "CHAMBERY TC"] + ["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"] + + */ + Section { + RowButtonView("Exporter en csv") { + for fileURL in SourceFileManager.shared.jsonFiles() { + let decoder = JSONDecoder() + decoder.userInfo[.maleData] = fileURL.manData + + do { + let data = try Data(contentsOf: fileURL) + let players = try decoder.decode([FederalPlayer].self, from: data) + var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } + let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false } + + print("before anonymousPlayers.count", anonymousPlayers.count) + FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) + print("after anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } + .count) + SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) + } catch { + Logger.error(error) + } + } + } + } + #endif + if let _lastDataSourceDate { Section { LabeledContent { @@ -46,6 +82,11 @@ struct PadelClubView: View { Text(_lastDataSourceDate.monthYearFormatted) Text("Classement mensuel utilisé") } + .contextMenu { + Button("Ré-importer") { + _startImporting() + } + } } if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) { @@ -56,30 +97,26 @@ struct PadelClubView: View { } } - #if targetEnvironment(simulator) - /* - ["36435", "BOUNOUA", "walid", "France", "3311600", "15,00", "Non", "2", "AUVERGNE RHONE-ALPES", "50 73 0046", "CHAMBERY TC"] - ["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"] - - */ Section { - RowButtonView("Exporter en csv") { - for fileURL in SourceFileManager.shared.jsonFiles() { - let decoder = JSONDecoder() - decoder.userInfo[.maleData] = fileURL.manData - - do { - let data = try Data(contentsOf: fileURL) - let players = try decoder.decode([FederalPlayer].self, from: data) - SourceFileManager.shared.exportToCSV(players: players, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) - } catch { - Logger.error(error) - } - } + LabeledContent { + Text(players.filter{ $0.male }.count.formatted()) + } label: { + Text("Messieurs") + } + LabeledContent { + Text(players.filter{ $0.male == false }.count.formatted()) + } label: { + Text("Dames") + } + + LabeledContent { + Text(anonymousPlayers.count.formatted()) + } label: { + Text("Joueurs anonymes") } + } header: { + Text(players.count.formatted() + " joueurs") } - - #endif } if importingFiles { @@ -104,16 +141,16 @@ struct PadelClubView: View { Text(maleUnrankedValue.formatted()) } } label: { - Text("Messieurs") Text("Rang d'un non classé") + Text("Messieurs") } LabeledContent { if let femaleUnrankedValue = monthData.femaleUnrankedValue { Text(femaleUnrankedValue.formatted()) } } label: { - Text("Dames") Text("Rang d'une non classée") + Text("Dames") } } header: { Text(monthData.monthKey) @@ -129,13 +166,20 @@ struct PadelClubView: View { } } .headerProminence(.increased) - .navigationTitle("Source des données fédérales") + .navigationTitle("Données fédérales") } @ViewBuilder func _activityStatus() -> some View { if checkingFiles || importingFiles { - ProgressView() + HStack(spacing: 20) { + ProgressView() + if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable { + if mostRecentDateAvailable > SourceFileManager.shared.lastDataSourceDate() ?? .distantPast { + Text("import " + mostRecentDateAvailable.monthYearFormatted) + } + } + } } else if let _mostRecentDateAvailable { if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast { Text(_mostRecentDateAvailable.monthYearFormatted + " disponible à l'importation") @@ -168,6 +212,7 @@ struct PadelClubView: View { await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) } importingFiles = false + viewContext.refreshAllObjects() } } } diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index 21ae589..c957426 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -12,14 +12,17 @@ struct EditablePlayerView: View { enum PlayerEditingOption { case payment case licenceId + case name } @EnvironmentObject var dataStore: DataStore - var player: PlayerRegistration + @Bindable var player: PlayerRegistration var editingOptions: [PlayerEditingOption] @State private var editedLicenceId = "" @State private var shouldPresentLicenceIdEdition: Bool = false - + @State private var presentLastNameUpdate: Bool = false + @State private var presentFirstNameUpdate: Bool = false + var body: some View { computedPlayerView(player) .alert("Numéro de licence", isPresented: $shouldPresentLicenceIdEdition) { @@ -30,6 +33,19 @@ struct EditablePlayerView: View { try? dataStore.playerRegistrations.addOrUpdate(instance: player) } } + .alert("Prénom", isPresented: $presentFirstNameUpdate) { + TextField("Prénom", text: $player.firstName) + .onSubmit { + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + } + } + .alert("Nom", isPresented: $presentLastNameUpdate) { + TextField("Nom", text: $player.lastName) + .onSubmit { + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + } + } + } // TODO: Guard @@ -61,6 +77,15 @@ struct EditablePlayerView: View { } } + if editingOptions.contains(.name) { + Divider() + Button("Modifier le prénom") { + presentFirstNameUpdate = true + } + Button("Modifier le nom") { + presentLastNameUpdate = true + } + } if editingOptions.contains(.licenceId) { Divider() if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil { diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index 3bfa6e2..a254fc0 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -15,8 +15,12 @@ struct ImportedPlayerView: View { var body: some View { VStack(alignment: .leading) { HStack { - Text(player.getLastName().capitalized) - Text(player.getFirstName().capitalized) + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } if index == nil { Text(player.male ? "♂︎" : "♀︎") } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index d269e23..8b47214 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -14,9 +14,9 @@ struct FileImportView: View { @Environment(Tournament.self) var tournament: Tournament @Environment(\.dismiss) private var dismiss - let fileContent: String? let notFoundAreWalkOutTip = NotFoundAreWalkOutTip() + @State private var fileContent: String? @State private var teams: [FileImportManager.TeamHolder] = [] @State private var isShowing = false @State private var didImport = false @@ -40,24 +40,29 @@ struct FileImportView: View { convertingFile = false isShowing.toggle() } + } + + Section { + Picker(selection: $fileProvider) { + ForEach(FileImportManager.FileProvider.allCases) { + Text($0.localizedLabel).tag($0) + } + } label: { + Text("Source du fichier") + } + + RowButtonView("Démarrer l'importation") { + if let fileContent { + await _startImport(fileContent: fileContent) + } + } + .disabled(fileContent == nil) } footer: { if fileProvider == .frenchFederation { let footerString = "Fichier provenant de [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" Text(.init(footerString)) } } - - if fileContent != nil { - Section { - Picker(selection: $fileProvider) { - ForEach(FileImportManager.FileProvider.allCases) { - Text($0.localizedLabel).tag($0) - } - } label: { - Text("Source du fichier") - } - } - } } // if filteredTeams.isEmpty == false && tournament.unsortedTeams().isEmpty == false { @@ -160,13 +165,6 @@ struct FileImportView: View { } } } - .onAppear { - if let fileContent { - Task { - await _startImport(fileContent: fileContent) - } - } - } .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in switch results { @@ -178,16 +176,11 @@ struct FileImportView: View { teams.removeAll() Task { do { - var fileContent: String? if selectedFile.lastPathComponent.hasSuffix("xls") { fileContent = try await CloudConvert.manager.uploadFile(selectedFile) } else { fileContent = try String(contentsOf: selectedFile) } - - if let fileContent { - await _startImport(fileContent: fileContent) - } selectedFile.stopAccessingSecurityScopedResource() } catch { errorMessage = error.localizedDescription @@ -204,10 +197,7 @@ struct FileImportView: View { .onOpenURL { url in do { - let fileContent = try String(contentsOf: url) - Task { - await _startImport(fileContent: fileContent) - } + fileContent = try String(contentsOf: url) } catch { errorMessage = error.localizedDescription } @@ -302,7 +292,7 @@ struct FileImportView: View { } #Preview { - FileImportView(fileContent: nil) + FileImportView() .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index db50340..375b769 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -104,7 +104,7 @@ struct InscriptionManagerView: View { } .sheet(isPresented: $presentImportView) { NavigationStack { - FileImportView(fileContent: nil) + FileImportView() } .tint(.master) }