Laurent 2 years ago
commit 2d896e1b0f
  1. 94
      PadelClub/Data/Federal/FederalPlayer.swift
  2. 3
      PadelClub/Data/Tournament.swift
  3. 3
      PadelClub/Utils/FileImportManager.swift
  4. 28
      PadelClub/Utils/SourceFileManager.swift
  5. 6
      PadelClub/Views/Navigation/MainView.swift
  6. 67
      PadelClub/Views/Navigation/Toolbox/PadelClubView.swift
  7. 2
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift

@ -7,7 +7,7 @@
import Foundation import Foundation
struct FederalPlayer { struct FederalPlayer: Decodable {
var rank: Int var rank: Int
var lastName: String var lastName: String
var firstName: String var firstName: String
@ -21,6 +21,94 @@ struct FederalPlayer {
var club: String var club: String
var isMale: Bool var isMale: Bool
// MARK: - Nationnalite
struct Nationnalite: Hashable, Codable {
let code, codeFov: String
}
init(from decoder: Decoder) throws {
enum CodingKeys: String, CodingKey {
case nom
case prenom
case licence
case meilleurClassement
case nationnalite
case anneeNaissance
case codeClub
case nomClub
case nomLigue
case rang
case progression
case points
case nombreDeTournois
case assimile
}
let container = try decoder.container(keyedBy: CodingKeys.self)
isMale = (decoder.userInfo[.maleData] as? Bool) == true
let _lastName = try container.decode(String.self, forKey: .nom)
let _firstName = try container.decode(String.self, forKey: .prenom)
lastName = _lastName
firstName = _firstName
if let lic = try? container.decodeIfPresent(Int.self, forKey: .licence) {
license = String(lic)
} else {
license = ""
}
let nationnalite = try container.decode(Nationnalite.self, forKey: .nationnalite)
country = nationnalite.code
//meilleurClassement = try container.decode(Int.self, forKey: .meilleurClassement)
//anneeNaissance = try container.decode(Int.self, forKey: .anneeNaissance)
clubCode = try container.decode(String.self, forKey: .codeClub)
club = try container.decode(String.self, forKey: .nomClub)
ligue = try container.decode(String.self, forKey: .nomLigue)
rank = try container.decode(Int.self, forKey: .rang)
//progression = try? container.decodeIfPresent(Int.self, forKey: .progression)
let pointsAsInt = try? container.decodeIfPresent(Int.self, forKey: .points)
if let pointsAsInt {
points = Double(pointsAsInt)
} else {
points = nil
}
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!) : ""
let strippedLicense = license.strippedLicense ?? ""
let line = ";\(rank);\(lastName);\(firstName);\(country);\(strippedLicense);\(pointsString);\(assimilation);\(tournamentCountString);\(ligue);\(formatNumbers(clubCode));\(club);"
return line
}
func formatNumbers(_ input: String) -> String {
// Insert spaces at appropriate positions
let formattedString = insertSeparator(input, separator: " ", every: [2, 4])
return formattedString
}
func insertSeparator(_ input: String, separator: String, every positions: [Int]) -> String {
var modifiedString = input
// Adjust for the index shift caused by inserting characters
var offset = 0
// Insert separator at specified positions
for position in positions {
let index = modifiedString.index(modifiedString.startIndex, offsetBy: position + offset)
modifiedString.insert(contentsOf: separator, at: index)
// Increase offset to adjust for the inserted character
offset += separator.count
}
return modifiedString
}
var fullNameCanonical: String var fullNameCanonical: String
/* /*
@ -115,3 +203,7 @@ struct FederalPlayer {
} }
} }
extension CodingUserInfoKey {
static let maleData = Self(rawValue: "maleData")!
}

@ -872,11 +872,10 @@ class Tournament : ModelObject, Storable {
let lastRankMan = currentMonthData()?.maleUnrankedValue let lastRankMan = currentMonthData()?.maleUnrankedValue
let lastRankWoman = currentMonthData()?.femaleUnrankedValue let lastRankWoman = currentMonthData()?.femaleUnrankedValue
try await unsortedPlayers().concurrentForEach { player in
let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate }) let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate })
let sources = dataURLs.map { CSVParser(url: $0) } let sources = dataURLs.map { CSVParser(url: $0) }
try await unsortedPlayers().concurrentForEach { player in
try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0) try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0)
} }
} }

@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import LeStorage
class FileImportManager { class FileImportManager {
static let shared = FileImportManager() static let shared = FileImportManager()
@ -147,7 +148,7 @@ class FileImportManager {
} }
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) { for chunk in players.chunked(into: 2000) {
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
} }
} }

@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import LeStorage
class SourceFileManager { class SourceFileManager {
static let shared = SourceFileManager() static let shared = SourceFileManager()
@ -60,6 +61,26 @@ class SourceFileManager {
} }
} }
func exportToCSV(players: [FederalPlayer], sourceFileType: SourceFile, date: Date) {
let lastDateString = URL.importDateFormatter.string(from: date)
let dateString = ["CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].joined(separator: "-") + "." + "csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
var csvText : String = ""
for player in players {
csvText.append(player.exportToCSV() + "\n")
}
do {
try csvText.write(to: destinationFileUrl, atomically: true, encoding: .utf8)
print("CSV file exported successfully.")
} catch {
print("Error writing CSV file:", error)
Logger.error(error)
}
}
func fetchData(fromDate current: Date) async { func fetchData(fromDate current: Date) async {
let lastStringDate = URL.importDateFormatter.string(from: current) let lastStringDate = URL.importDateFormatter.string(from: current)
@ -148,6 +169,13 @@ class SourceFileManager {
} }
} }
func jsonFiles() -> [URL] {
let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "json"
})
return allJSONFiles
}
var allFiles: [URL] { var allFiles: [URL] {
let allFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in let allFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "csv" url.pathExtension == "csv"

@ -126,11 +126,7 @@ struct MainView: View {
Task { Task {
let lastDataSource = await FileImportManager.shared.importDataFromFFT() let lastDataSource = await FileImportManager.shared.importDataFromFFT()
dataStore.appSettings.lastDataSource = lastDataSource dataStore.appSettings.lastDataSource = lastDataSource
do { dataStore.appSettingsStorage.write()
try dataStore.appSettingsStorage.write()
} catch {
Logger.error(error)
}
if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) { if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
} }

@ -47,6 +47,53 @@ struct PadelClubView: View {
Text("Classement mensuel utilisé") Text("Classement mensuel utilisé")
} }
} }
if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) {
Section {
RowButtonView("Importer \(URL.importDateFormatter.string(from: mostRecentDateAvailable))") {
_startImporting()
}
}
}
#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)
}
}
}
}
#endif
}
if importingFiles {
ContentUnavailableView("Importation en cours", systemImage: "server.rack", description: Text("Une importation des données fédérales publiques est en cours, veuillez patienter."))
} else if (players.isEmpty || lastDataSource == nil) {
ContentUnavailableView {
Label("Aucun joueur importé", systemImage: "person.slash")
} description: {
Text("Padel Club peut importer toutes les données publiques de la FFT concernant tous les compétiteurs et compétitrices.")
} actions: {
RowButtonView("Démarrer l'importation") {
_startImporting()
}
}
} }
let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed() let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed()
@ -72,20 +119,6 @@ struct PadelClubView: View {
Text(monthData.monthKey) Text(monthData.monthKey)
} }
} }
if importingFiles {
ContentUnavailableView("Importation en cours", systemImage: "server.rack", description: Text("Une importation des données fédérales publiques est en cours, veuillez patienter."))
} else if (players.isEmpty || lastDataSource == nil) {
ContentUnavailableView {
Label("Aucun joueur importé", systemImage: "person.slash")
} description: {
Text("Padel Club peut importer toutes les données publiques de la FFT concernant tous les compétiteurs et compétitrices.")
} actions: {
RowButtonView("Démarrer l'importation") {
_startImporting()
}
}
}
} }
.task { .task {
await self._checkSourceFileAvailability() await self._checkSourceFileAvailability()
@ -130,11 +163,7 @@ struct PadelClubView: View {
Task { Task {
let lastDataSource = await FileImportManager.shared.importDataFromFFT() let lastDataSource = await FileImportManager.shared.importDataFromFFT()
dataStore.appSettings.lastDataSource = lastDataSource dataStore.appSettings.lastDataSource = lastDataSource
do { dataStore.appSettingsStorage.write()
try dataStore.appSettingsStorage.write()
} catch {
Logger.error(error)
}
if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) { if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
} }

@ -42,7 +42,7 @@ struct ToolboxView: View {
Label("Définir les durées moyennes", systemImage: "deskclock") Label("Définir les durées moyennes", systemImage: "deskclock")
} }
} footer: { } footer: {
Text("Vous pouvez définir vos propores estimations de durées de match en fonction du format de jeu.") Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.")
} }
} }
.navigationTitle(TabDestination.toolbox.title) .navigationTitle(TabDestination.toolbox.title)

Loading…
Cancel
Save